Jupyter Notebook で Highcharts を使ってローソク足を表示してみました

以前、Python で為替レートの折れ線グラフを表示してみましたの記事を書いたときは、ローソク足を表示できなかったのですが、 Jupyter Notebook から JavaScript のライブラリーの Highcharts を読み込んで、ローソク足を表示してみました。

けっこう無理やり実装しました。 ので、あまり参考にならないかもしれません。

環境

  • Python 3.5.2
  • numpy-1.14.3
  • pandas-0.22.0
  • requests 2.18.4
  • Highcharts v6.1.0
  • Highstock v6.1.0

Jupyter Notebook はこの記事の通りインストールしておきました。 それから pandas もこの記事の通りインストールしておきました。

%%html マジックコマンド

%html [--isolated]

Render the cell as a block of HTML

optional arguments:

–isolated
Annotate the cell as ‘isolated’. Isolated cells are rendered inside their own <iframe> tag

%%html

Jupyter Notebook のセルの中で HTML を記述するには %%html マジックコマンドを使うようです。

--isolated オプションをつけると <iframe> の中に結果を表示してくれるようです。 このオプションはどういうときに効果を発揮するのかいまいちよくわかりませんでした。

%%javascript マジックコマンド

Run the cell block of Javascript code

%%javascript

Jupyter Notebook のセルの中で JavaScript を記述するには %%javascript マジックコマンドを使うようです。

%%js と記述しても同じ意味のようで、 %%javascript のエイリアスになっているようです。

%%javascript のマジックコマンドを使わなくても、 %%html マジックコマンドの中に <script> を記述しても JavaScript は動かすことができるみたいです。

Highcharts

最初に Highcharts を使ってみます。 また、 Your First Chart | Highcharts を参考にしてみます。

この記事の 1 つコードブロックを Jupyter Notebook の 1 つのセルに入力しながら進めました。

Highcharts の読み込み

Highcharts を読み込みました。

%%html
<script src="https://code.highcharts.com/highcharts.js"></script>

チャートの表示領域の定義

チャートを表示する領域を <div> で記述しました。

%%html
<div id="container" style="width:100%; height:400px;"></div>

チャートの表示

チャートを表示する処理を JavaScript で記述しました。

%%javascript
$(function () {
    var myChart = Highcharts.chart('container', {
        chart: {
            type: 'bar'
        },
        title: {
            text: 'Fruit Consumption'
        },
        xAxis: {
            categories: ['Apples', 'Bananas', 'Oranges']
        },
        yAxis: {
            title: {
                text: 'Fruit eaten'
            }
        },
        series: [{
            name: 'Jane',
            data: [1, 0, 4]
        }, {
            name: 'John',
            data: [5, 7, 3]
        }]
    });
});

このセルを実行すると、 1 つ前のセルに入力した <div> の箇所に棒グラフが表示されました。 $(function () { ... }); って jQuery ですけど、 Jupyter Notebook は jQuery が標準で使えるようになっているみたいでした。 初めて知りました。 Jupyter Notebook はセルを追加したり、 DOM の操作をしたりするのに jQuery を使っているようでした。

Highcharts is not defined

ただ、このままだと問題があって、 .ipynb ファイルを閉じて、開きなおすと、次のエラーが表示されました。

Javascript error adding output!
ReferenceError: Highcharts is not defined
See your browser Javascript console for more details.

Highcharts の読み込みが終わる前にチャートを表示しようとしているように見えました。 ちょっとこれじゃ都合が悪いです。

Most browsers provide similar functionality in the form of a DOMContentLoaded event.

.ready() | jQuery API Documentation

$(function () { ... }); って、 DOMContentLoaded のイベントと同じ意味のようです。

DOMContentLoaded イベントは、最初のHTMLドキュメントの読み込みと解析が完了した時に発火し、 スタイルシートや画像、サブフレームの読み込みが終わるのを待ちません。 ページが完全に読み込み終わったことを検知するためにのみ、全く異なるイベント ─ load ─ を使用するべきです。 DOMContentLoaded がより適切である場合に load を誤って使用することが、 信じられないほど頻繁に行われています。

DOMContentLoaded - Web 技術のリファレンス | MDN

ページが完全に読み込み終わった後に処理したかったので、 load のイベントを実装しました。 が、それでも同じエラーになってしまいました。

.ipynb ファイルの内容

読み込みはどのようになっているのかな、と思って、ソースとかを見てみました。

  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/html": [
       "<script src=\"https://code.highcharts.com/highcharts.js\"></script>"
      ],
      "text/plain": [
       "<IPython.core.display.HTML object>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "%%html\n",
    "<script src=\"https://code.highcharts.com/highcharts.js\"></script>"
   ]
  },

保存した .ipynb ファイルの Highcharts のモジュールの読み込みの箇所を抜粋してみました。 .ipynb ファイルは JSON の形式になっているようで、セルに入力した内容や、出力の結果が含まれているようです。

Jupyter Notebook がドキュメントの読み込み後 ($(function () { ... });) に、この .ipynb ファイルの内容を DOM に反映しているように見えました。 Jupyter Notebook のページのソースをブラウザーから確認したらすごくシンプルで、どこにも .ipynb の内容がなかったので、動的に DOM を書き換えることによって、 .ipynb ファイルの内容を反映しているように見えたからです。

ドキュメントの読み込み後に <script src=\"https://code.highcharts.com/highcharts.js\"></script> という内容が DOM に追加されて、そこで初めて Highcharts の読み込みが始まることになります。 だから、ページが完全に読み込み終わったことを検知する load イベントでは、 jQuery によって動的に書き換えられた <script> の読み込みの終わりまでは検知できないのだと思います。 だぶん、 load イベントで検知できるのは、静的なページが完全に読み込み終わったことまでだと思います。

Highcharts が読み込み終わったことを検知するために

無理やりですけど、ここでは Highcharts の Highcharts.chart 関数を使っているので、 Highcharts.chart が関数として定義されていたら Highcharts の読み込みが終わったものとして、チャートを表示する処理に進めるようにしてみました。

読み込みが終わっていない場合は 1 秒後 (1000 ミリ秒後)にまた同じ処理をするようにしました。

%%javascript
const plot = () => {
    if (typeof Highcharts !== 'object' ||
        typeof Highcharts.chart !== 'function') {
        const timeoutID = setTimeout(plot, 1000);
        return {};
    }
    var myChart = Highcharts.chart('container', {
        chart: {
            type: 'bar'
        },
        title: {
            text: 'Fruit Consumption'
        },
        xAxis: {
            categories: ['Apples', 'Bananas', 'Oranges']
        },
        yAxis: {
            title: {
                text: 'Fruit eaten'
            }
        },
        series: [{
            name: 'Jane',
            data: [1, 0, 4]
        }, {
            name: 'John',
            data: [5, 7, 3]
        }]
    });
    return myChart;
};
plot();

結果は、 Gist にアップしました。 が、アクセスしても棒グラフは表示されませんでした。 %%html%%javascript を使ったセルは Gist じゃ表示できないみたいでした。 初めて知りました。

Jupyter Notebook Viewer で見ると棒グラフが表示されました。 小細工 (%%html%%javascript) をする場合は Jupyter Notebook Viewer を使った方がいいみたいでした。

Highcharts error #16

まだ、エラーがあります。

Error: Highcharts error #16: www.highcharts.com/errors/16

Highcharts already defined in the page

Highcharts Error #16

Highcharts を読み込むセルを何度も実行するとこのエラーがコンソールに出力されるのですが、まあ、放置しました。 たぶん、 Gist や Jupyter Notebook Viewer のビューアーで表示する分には、セルを何度も実行することはできないので、エラーになることはないかな、と思いましたので。

Highstock

Highstock のサンプルも試してみました。 Two panes, candlestick and volume | Highcharts を参考にしました。 無駄にテーマも適用してみました。

Highstock の読み込み

Highstock と関連するモジュールを読み込みました。

%%html
<script src="https://code.highcharts.com/stock/highstock.js"></script>
<script src="https://code.highcharts.com/stock/modules/drag-panes.js"></script>
<script src="https://code.highcharts.com/stock/modules/exporting.js"></script>

チャートの表示領域の定義

チャートを表示する領域を <div> で記述しました。

%%html
<div id="container" style="height: 400px; min-width: 310px"></div>

チャートの表示

Highstock のモジュールが読み込めているかどうかの判断は Highcharts のときと同じように実装しました。

%%javascript
const plot = () => {
    if (typeof Highcharts !== 'object' ||
        typeof Highcharts.stockChart !== 'function') {
        const timeoutID = setTimeout(plot, 1000);
        return;
    }
    // [CORS](https://developer.mozilla.org/ja/docs/Web/HTTP/HTTP_access_control)
    //$.getJSON('https://www.highcharts.com/samples/data/aapl-ohlcv.json', function (data) {
    $.getJSON('https://gist.githubusercontent.com/va2577/5a9ec41b4875deb25894147c25dc859e/raw/2b3de82e00a73a874a4a4cfe1b12dcc713cd6b39/aapl-ohlcv.json', function (data) {

        // split the data set into ohlc and volume
        var ohlc = [],
            volume = [],
            dataLength = data.length,
            // set the allowed units for data grouping
            groupingUnits = [[
                'week',                         // unit name
                [1]                             // allowed multiples
            ], [
                'month',
                [1, 2, 3, 4, 6]
            ]],

            i = 0;

        for (i; i < dataLength; i += 1) {
            ohlc.push([
                data[i][0], // the date
                data[i][1], // open
                data[i][2], // high
                data[i][3], // low
                data[i][4] // close
            ]);

            volume.push([
                data[i][0], // the date
                data[i][5] // the volume
            ]);
        }


        // create the chart
        Highcharts.stockChart('container', {

            rangeSelector: {
                selected: 1
            },

            title: {
                text: 'AAPL Historical'
            },

            yAxis: [{
                labels: {
                    align: 'right',
                    x: -3
                },
                title: {
                    text: 'OHLC'
                },
                height: '60%',
                lineWidth: 2,
                resize: {
                    enabled: true
                }
            }, {
                labels: {
                    align: 'right',
                    x: -3
                },
                title: {
                    text: 'Volume'
                },
                top: '65%',
                height: '35%',
                offset: 0,
                lineWidth: 2
            }],

            tooltip: {
                split: true
            },

            series: [{
                type: 'candlestick',
                name: 'AAPL',
                data: ohlc,
                dataGrouping: {
                    units: groupingUnits
                }
            }, {
                type: 'column',
                name: 'Volume',
                data: volume,
                yAxis: 1,
                dataGrouping: {
                    units: groupingUnits
                }
            }]
        });
    });
};
plot();

Highstock のデモでは、 $.getJSON でサーバーからデータをとってきているようでした。 CORS の問題があって、この www.highcharts.com からデータを取ってくることができないので、 Gist にアップして、それをとってくるようにしてしまいました。 大丈夫かな。

テーマ

Dark Unica のテーマを highcharts/dark-unica.js at master · highcharts/highcharts からコピペしました。

%%javascript
const theme = () => {
    if (typeof Highcharts !== 'object' ||
        typeof Highcharts.createElement !== 'function' ||
        typeof Highcharts.setOptions !== 'function') {
        const timeoutID = setTimeout(theme, 1000);
        return;
    }
    /**
     * (c) 2010-2017 Torstein Honsi
     *
     * License: www.highcharts.com/license
     *
     * Dark theme for Highcharts JS
     * @author Torstein Honsi
     */

    'use strict';
    /* global document */
    // Load the fonts
    /*
    comment out
    import Highcharts from '../parts/Globals.js';
    */
    Highcharts.createElement('link', {
        href: 'https://fonts.googleapis.com/css?family=Unica+One',
        rel: 'stylesheet',
        type: 'text/css'
    }, null, document.getElementsByTagName('head')[0]);

    Highcharts.theme = {
        colors: ['#2b908f', '#90ee7e', '#f45b5b', '#7798BF', '#aaeeee', '#ff0066',
            '#eeaaee', '#55BF3B', '#DF5353', '#7798BF', '#aaeeee'],
        chart: {
            backgroundColor: {
                linearGradient: { x1: 0, y1: 0, x2: 1, y2: 1 },
                stops: [
                    [0, '#2a2a2b'],
                    [1, '#3e3e40']
                ]
            },
            style: {
                fontFamily: '\'Unica One\', sans-serif'
            },
            plotBorderColor: '#606063'
        },
        title: {
            style: {
                color: '#E0E0E3',
                textTransform: 'uppercase',
                fontSize: '20px'
            }
        },
        subtitle: {
            style: {
                color: '#E0E0E3',
                textTransform: 'uppercase'
            }
        },
        xAxis: {
            gridLineColor: '#707073',
            labels: {
                style: {
                    color: '#E0E0E3'
                }
            },
            lineColor: '#707073',
            minorGridLineColor: '#505053',
            tickColor: '#707073',
            title: {
                style: {
                    color: '#A0A0A3'

                }
            }
        },
        yAxis: {
            gridLineColor: '#707073',
            labels: {
                style: {
                    color: '#E0E0E3'
                }
            },
            lineColor: '#707073',
            minorGridLineColor: '#505053',
            tickColor: '#707073',
            tickWidth: 1,
            title: {
                style: {
                    color: '#A0A0A3'
                }
            }
        },
        tooltip: {
            backgroundColor: 'rgba(0, 0, 0, 0.85)',
            style: {
                color: '#F0F0F0'
            }
        },
        plotOptions: {
            series: {
                dataLabels: {
                    color: '#B0B0B3'
                },
                marker: {
                    lineColor: '#333'
                }
            },
            boxplot: {
                fillColor: '#505053'
            },
            candlestick: {
                lineColor: 'white'
            },
            errorbar: {
                color: 'white'
            }
        },
        legend: {
            itemStyle: {
                color: '#E0E0E3'
            },
            itemHoverStyle: {
                color: '#FFF'
            },
            itemHiddenStyle: {
                color: '#606063'
            }
        },
        credits: {
            style: {
                color: '#666'
            }
        },
        labels: {
            style: {
                color: '#707073'
            }
        },

        drilldown: {
            activeAxisLabelStyle: {
                color: '#F0F0F3'
            },
            activeDataLabelStyle: {
                color: '#F0F0F3'
            }
        },

        navigation: {
            buttonOptions: {
                symbolStroke: '#DDDDDD',
                theme: {
                    fill: '#505053'
                }
            }
        },

        // scroll charts
        rangeSelector: {
            buttonTheme: {
                fill: '#505053',
                stroke: '#000000',
                style: {
                    color: '#CCC'
                },
                states: {
                    hover: {
                        fill: '#707073',
                        stroke: '#000000',
                        style: {
                            color: 'white'
                        }
                    },
                    select: {
                        fill: '#000003',
                        stroke: '#000000',
                        style: {
                            color: 'white'
                        }
                    }
                }
            },
            inputBoxBorderColor: '#505053',
            inputStyle: {
                backgroundColor: '#333',
                color: 'silver'
            },
            labelStyle: {
                color: 'silver'
            }
        },

        navigator: {
            handles: {
                backgroundColor: '#666',
                borderColor: '#AAA'
            },
            outlineColor: '#CCC',
            maskFill: 'rgba(255,255,255,0.1)',
            series: {
                color: '#7798BF',
                lineColor: '#A6C7ED'
            },
            xAxis: {
                gridLineColor: '#505053'
            }
        },

        scrollbar: {
            barBackgroundColor: '#808083',
            barBorderColor: '#808083',
            buttonArrowColor: '#CCC',
            buttonBackgroundColor: '#606063',
            buttonBorderColor: '#606063',
            rifleColor: '#FFF',
            trackBackgroundColor: '#404043',
            trackBorderColor: '#404043'
        },

        // special colors for some of the
        legendBackgroundColor: 'rgba(0, 0, 0, 0.5)',
        background2: '#505053',
        dataLabelsColor: '#B0B0B3',
        textColor: '#C0C0C0',
        contrastTextColor: '#F0F0F3',
        maskColor: 'rgba(255,255,255,0.3)'
    };

    // Apply the theme
    Highcharts.setOptions(Highcharts.theme);
};
theme();

import Highcharts from '../parts/Globals.js'; の箇所はコメントアウトしました。 代わりに Highcharts のオブジェクトが使えるように Highcharts の読み込みを待つように修正しました。

Gist にアップしました。 Jupyter Notebook Viewer から見ることができます。

ローソク足を表示することができました。

pandas の DataFrame からローソク足を表示

今回、最終的にやりたかったことはこういうことじゃなくて、 pandas で加工してきたデータを Highstock のローソク足で表示したいのです。

調べてみたのですが、 Jupyter Notebook で、 Python のコードのセルから、 %%html%%javascript のセルにデータを渡す方法が見つけられませんでした。

代わりに次のクラスと関数を見つけました。

これを使うと、 Python のコードから Jupyter Notebook へ HTML を出力することができるようです。 %%html もやっていることは同じみたいでした。 たぶん。

なので、 Highstock を操作する JavaScript のコード自体を Python の文字列として定義しておいて、それをもとに IPython.display.HTML のオブジェクトを生成して、 IPython.display.display に渡してあげればやりたいことが実現できるようです。

モジュールを読み込む

pandas と HTML と display を読み込みました。 それから、データをみんかぶ FX の米ドル/円のチャートから取ってこようと思うので、 requests も読み込みました。 requests は事前にインストールしておく必要があります。 pip install requests でインストールしておきました。

import pandas as pd
import requests
from IPython.display import HTML
from IPython.display import display

データを取ってくる

以前、みんかぶ FX のチャートのレートを取得してみましたの記事を書いたのと同じところからデータを取ってきました。

headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.140 Safari/537.36 Edge/17.17134' }
r = requests.get('https://fx.minkabu.jp/api/v2/bar/USDJPY/daily.json?count=240', headers=headers)
r.raise_for_status()
r.json()[:10]

結果です。 r.json() はリスト・配列の形式になっているようで、先頭の 10 行を表示してみました。

[[1497474000000, '109.554', '110.979', '109.258', '110.911'],
 [1497560400000, '110.925', '111.415', '110.637', '110.89'],
 [1497819600000, '110.841', '111.601', '110.704', '111.526'],
 [1497906000000, '111.514', '111.782', '111.306', '111.454'],
 [1497992400000, '111.434', '111.738', '111.035', '111.375'],
 [1498078800000, '111.337', '111.446', '110.93', '111.319'],
 [1498165200000, '111.32', '111.427', '111.142', '111.226'],
 [1498424400000, '111.157', '111.939', '111.101', '111.86'],
 [1498510800000, '111.82', '112.464', '111.459', '112.337'],
 [1498597200000, '112.337', '112.416', '111.829', '112.28']]

pandas の DataFrame

データは、日時と OHLC の 2 次元配列の形式になっているので、あまり引数を指定することなく pandas の DataFrame にできました。

df = pd.DataFrame(data=r.json(), columns=['time', 'open', 'high', 'low', 'close'])
df.head()

結果です。 Highstock のデモのデータの形式を見ると、こちらも 2 次元の配列で日時と OHLC を渡していたので、日時はインデックスにしませんでした。

 	time 	open 	high 	low 	close
0 	1497474000000 	109.554 	110.979 	109.258 	110.911
1 	1497560400000 	110.925 	111.415 	110.637 	110.89
2 	1497819600000 	110.841 	111.601 	110.704 	111.526
3 	1497906000000 	111.514 	111.782 	111.306 	111.454
4 	1497992400000 	111.434 	111.738 	111.035 	111.375

DataFrame を編集する

日時が UTC (協定世界)なので、 EEST(Eastern European Summer Time)(UTC+3) にしました。 こうすると、週の始まりが月曜日の 0:00 からになり(通常時間は 1:00 からですけれども)、日足が 5 本になるからです。

# [From Timestamps to Epoch](https://pandas.pydata.org/pandas-docs/stable/timeseries.html#from-timestamps-to-epoch)
utc = pd.to_datetime(df['time'], unit='ms')
utc3 = utc + pd.DateOffset(hours=3)
epoch = (utc3 - pd.Timestamp("1970-01-01")) / pd.Timedelta('1ms')
data1 = { 'time': epoch.astype('int64').values, 'open': df['open'].astype('float64').values, 'high': df['high'].astype('float64').values, 'low': df['low'].astype('float64').values, 'close': df['close'].astype('float64').values}
columns1 = ['time', 'open', 'high', 'low', 'close']
df2 = pd.DataFrame(data=data1, columns=columns1)
df2.head()

結果です。

time 	open 	high 	low 	close
0 	1497484800000 	109.554 	110.979 	109.258 	110.911
1 	1497571200000 	110.925 	111.415 	110.637 	110.890
2 	1497830400000 	110.841 	111.601 	110.704 	111.526
3 	1497916800000 	111.514 	111.782 	111.306 	111.454
4 	1498003200000 	111.434 	111.738 	111.035 	111.375

Highstock のローソク足のチャートを表示する

Highstock のローソク足を表示するコードを次のように記述しました。

template = """
<div id="container" style="height: 400px; min-width: 310px"></div>
<script src="https://code.highcharts.com/stock/highstock.js"></script>
<script src="https://code.highcharts.com/stock/modules/exporting.js"></script>
<script>
    const plot = () => {{
        if (typeof Highcharts !== 'object' ||
            typeof Highcharts.stockChart !== 'function') {{
            const timeoutID = setTimeout(plot, 1000);
            return;
        }}
        // create the chart
        Highcharts.stockChart('container', {{
            rangeSelector: {{
                selected: 1
            }},
            title: {{
                text: '米ドル/円 daily'
            }},
            series: [{{
                type: 'candlestick',
                name: 'AAPL',
                data: {data}
            }}]
        }});
    }};
    plot();
</script>
"""
display(HTML(template.format(data=df2.values.tolist())))

ほとんど文字列の定義になってしまっています。 その文字列は JavaScript のコードになっています。 Highstock のライブラリーを操作するコードです。

フォーマットできる形式になっていて、 JavaScript の {...} は Python の書式に影響してしまうので、 {{...}} のようにエスケープしました。 エスケープしていない data: {data} の箇所に pandas の DataFrame のデータを埋め込みます。

最後の行の display(HTML(template.format(data=df2.values.tolist()))) が Python のコードです。

template.format(data=df2.values.tolist()) で、文字列で定義していた JavaScript のコードの data: {data} の箇所に pandas で編集したデータを埋め込んでいます。 Highstock のローソク足のチャートのデータの形式が 2 次元配列なので、 df.values.tolist() のようにして、 DataFrame から 2 次元配列の形式(時刻と OHLC) に変換しています。

HTML(...) のところで Python の文字列を Jupyter Notebook の IPython.display.HTML クラスにしています。

display(...) のところで HTML を Jupyter Notebook に表示しています。

JavaScript のコードを全部 Python の文字列で記述してしまっていて、あまり綺麗じゃないですけど、 Python と pandas と Jupyter Notebook を使って、 Highstock のローソク足のチャートを表示することができました。

Gist のソース

Gist にアップしました。

Jupyter Notebook Viewer から結果を見ることができます。

終わり

次の記事を書いたときに Jupyter Notebook にローソク足を表示しようと思っていたのですが、ようやく表示することができました。