46
52

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Plotlyでぐりぐり動かせる為替チャートを作る(1)

Last updated at Posted at 2017-04-21

下準備

モジュールインポート

必要なモジュールをインポートします。

# ----------General Module----------
import numpy as np
import pandas as pd
# ----------User Module----------
from randomwalk import randomwalk
import stockplot as sp
# ----------Hide General Module----------
import stockstats
import plotly
  • General Module, Hide General Moduleは一般に配布されているパッケージなので、condaやpipといったパッケージ管理ソフトなどで追加してください。
    • General ModuleはこのJupyter Notebook内で使います。
    • Hide General Moduleはstockplot内で使用します。

conda install plotly
pip install stockstats


* User Moduleのstockplotについては以下にソースコード貼ります。
    * 旧バージョン[Qiita - u1and0 / plotlyでキャンドルチャートプロット](http://qiita.com/u1and0/items/0ebcf097a1d61c636eb9)
* random_walkについては[Qiita - u1and0 / pythonでローソク足(candle chart)の描画](http://qiita.com/u1and0/items/1d9afdb7216c3d2320ef)

## サンプルデータの作成


```python
# Make sample data
np.random.seed(1)
df = randomwalk(60 * 60 * 24 * 90, freq='S', tick=0.01, start=pd.datetime(2017, 3, 20))\
    .resample('T').ohlc() + 115  # 90日分の1分足, 初期値が115

ランダムな為替チャートを作成します。
randomwalk関数で2017/3/20からの1分足を90日分作成します。

インスタンス化

# Convert DataFrame as StockPlot
fx = sp.StockPlot(df)

StockPlotクラスでインスタンス化します。

ローソク足の描画

インスタンス化されたら時間足を変換します。
変換する際はresampleメソッドを使います。

fx.resample('D').head()
low open close high
2017-03-20 112.71 115.00 114.22 116.80
2017-03-21 113.67 114.23 115.52 116.23
2017-03-22 112.23 115.51 112.29 117.44
2017-03-23 111.88 112.28 116.02 116.08
2017-03-24 114.76 116.03 118.60 119.10

1分足として入力したデータを日足に変換したデータが返されました。
変換されたデータはstock_dataframeというインスタンス変数に格納されます。

fx.stock_dataframe.head(), fx.stock_dataframe.tail()
(               low    open   close    high
 2017-03-20  112.71  115.00  114.22  116.80
 2017-03-21  113.67  114.23  115.52  116.23
 2017-03-22  112.23  115.51  112.29  117.44
 2017-03-23  111.88  112.28  116.02  116.08
 2017-03-24  114.76  116.03  118.60  119.10,
                low    open   close    high
 2017-06-13  103.18  106.19  106.12  106.28
 2017-06-14  104.59  106.13  108.07  108.51
 2017-06-15  103.97  108.06  105.66  108.86
 2017-06-16  104.94  105.66  108.25  108.59
 2017-06-17  107.31  108.24  109.22  110.73)

2017/3/20-2017/6/17の日足ができたことを確認しました。

時間足の変換が済むと、プロットが可能です。
プロットするときはplotメソッドを使います。

fx.plot()
{'data': [{'boxpoints': False,
   'fillcolor': '#3D9970',
   'line': {'color': '#3D9970'},
   'name': 'Increasing',
   'showlegend': False,
   'type': 'box',
   'whiskerwidth': 0,
   'x': [Timestamp('2017-03-21 00:00:00', freq='D'),
    Timestamp('2017-03-21 00:00:00', freq='D'),
    Timestamp('2017-03-21 00:00:00', freq='D'),
    Timestamp('2017-03-21 00:00:00', freq='D'),
    Timestamp('2017-03-21 00:00:00', freq='D'),
    Timestamp('2017-03-21 00:00:00', freq='D'),
    (略)...
    {"showLink": true, "linkText": "Export to plot.ly"})});</script>

fx.plot()plotlyで出力する形式plotly.graph_objs.graph_objs.Figure(datalayoutがキーとなった辞書)が返されます。

画像を見るにはmatplotlib.pyplotのようにshowメソッドを使います。
showメソッドの第一引数howのデフォルト引数はhtmlです。
引数なしでshowするとブラウザの新しいタブが立ち上がってそこに表示されます。
今はJupyter Notebook上で描きたいので、how=jupyter、または単にjupyterを引数にします。

def show(self, how='html', filebasename='candlestick_and_trace'):
    """Export file type"""
    if how == 'html':
        ax = pyo.plot(self._fig, filename=filebasename + '.html',
                      validate=False)  # for HTML
    elif how == 'jupyter':
        ax = pyo.iplot(self._fig, filename=filebasename + '.html',
                       validate=False)  # for Jupyter Notebook
    elif how in ('png', 'jpeg', 'webp', 'svg'):
        ax = pyo.plot(self._fig, image=how, image_filename=filebasename,
                      validate=False)  # for file exporting
    else:
        raise KeyError(how)
    return ax
fx.show(how='jupyter')

gif1.gif

2017/3/20-2017/6/17の日足が描かれました。

plotlyの操作は

  • グラフ上のマウスオーバーで値の表示
  • グラフ上のドラッグでズームイン
  • 軸上(真ん中)のドラッグでスクロール
  • 軸上(端)のドラッグでズームアウト
  • ダブルクリックで元のビューに戻る
  • トリプルクリックで全体表示

時間足の変更

日足だけじゃなくて別の時間足も見たいです。

そういうときはresampleメソッドを使って時間幅を変更します。

fx.resample('H')  # 1時間足に変更
fx.plot()  # ローソク足プロット
fx.show('jupyter')  # プロットの表示をJupyter Notebookで開く

gif2.gif

1時間足がプロットされました。
あえて時間をかけてマウスオーバーしているのですが、1時間ごとにプロットされていることがわかりましたでしょうか。

ここで再度stock_dataframeを確認してみますと、1時間足に変わっていることがわかります。

fx.stock_dataframe.head(), fx.stock_dataframe.tail()
(                        low    open   close    high
 2017-03-20 00:00:00  114.76  115.00  115.26  115.49
 2017-03-20 01:00:00  115.27  115.27  116.11  116.47
 2017-03-20 02:00:00  115.69  116.10  115.69  116.53
 2017-03-20 03:00:00  115.62  115.68  116.02  116.19
 2017-03-20 04:00:00  115.74  116.01  116.00  116.31,
                         low    open   close    high
 2017-06-17 19:00:00  108.50  108.65  109.91  109.93
 2017-06-17 20:00:00  109.56  109.90  109.76  110.03
 2017-06-17 21:00:00  109.47  109.76  109.77  110.06
 2017-06-17 22:00:00  109.27  109.77  109.31  110.10
 2017-06-17 23:00:00  108.96  109.30  109.22  109.70)

'open', 'high', 'low', 'close'のカラムを持ったデータフレームの変換を行うresampleメソッドは以下のように記述しました。

def resample(self, freq: str):
    """Convert ohlc time span

    USAGE: `fx.resample('D')  # 日足に変換`

    * Args:  変更したい期間 M(onth) | W(eek) | D(ay) | H(our) | T(Minute) | S(econd)
    * Return: スパン変更後のデータフレーム
    """
    self.freq = freq  # plotやviewの範囲を決めるために後で使うのでインスタンス変数に入れる
    self.stock_dataframe = self._init_stock_dataframe.ix[:, ['open', 'high', 'low', 'close']]\
        .resample(freq).agg({'open': 'first', 'high': 'max', 'low': 'min', 'close': 'last'})\
        .dropna()
    return self.stock_dataframe
df.resample(freq).ohlc()

とすると階層が分かれたohlcのデータフレームが出来上がってしまうので

df.resample(freq).agg({'open': 'first', 'high': 'max', 'low': 'min', 'close': 'last'})

のようにaggメソッドを使います。

freqdf.resampleで使える時間であれば自由なので、例えばfreq='1D4H2T24S'とすると'1日と4時間2分24秒足'といった変な時間足を作れます。

fx.resample('1D4H2T24S').head()
low open close high
2017-03-20 00:00:00 112.71 115.00 114.28 116.80
2017-03-21 04:02:24 113.67 114.28 116.92 117.44
2017-03-22 08:04:48 111.93 116.91 112.38 117.26
2017-03-23 12:07:12 111.88 112.38 117.79 118.01
2017-03-24 16:09:36 116.71 117.80 120.74 121.27

plot範囲の指定

plotメソッドはstock_dataframeの中身をすべてグラフ化しません
デフォルトの場合、最後の足から数えて300本足がグラフ化されます。

例として、5分足のチャートを描きます。

fx.resample('5T')  # 5分足に変換
fx.plot()
fx.show('jupyter')

gif6.gif

# stock_dataframeは2017/3/20から
fx.stock_dataframe.index
DatetimeIndex(['2017-03-20 00:00:00', '2017-03-20 00:05:00',
               '2017-03-20 00:10:00', '2017-03-20 00:15:00',
               '2017-03-20 00:20:00', '2017-03-20 00:25:00',
               '2017-03-20 00:30:00', '2017-03-20 00:35:00',
               '2017-03-20 00:40:00', '2017-03-20 00:45:00',
               ...
               '2017-06-17 23:10:00', '2017-06-17 23:15:00',
               '2017-06-17 23:20:00', '2017-06-17 23:25:00',
               '2017-06-17 23:30:00', '2017-06-17 23:35:00',
               '2017-06-17 23:40:00', '2017-06-17 23:45:00',
               '2017-06-17 23:50:00', '2017-06-17 23:55:00'],
              dtype='datetime64[ns]', length=25920, freq='5T')

2017/3/20-2017/6/17ののデータフレームを5分足に変換してローソク足を描きました。
最初の足が2017/3/20ではなく2017/6/16で途切れています。
これはグラフ化される範囲が5分足の300本足で切られているためです。

描画されるデータが大きいとshowメソッド時に大変リソースを食います。
グラフとして見る範囲は限定的だろうとの考えから、plotメソッドはstock_dataframeから一部切り出した形をグラフ化(plot)します。

グラフ化する範囲は、plotメソッドの引数として与えることができます。

  • plotメソッドのプロット範囲を決める引数
    • start_plot: グラフ化する最初の日付・時間
    • end_plot: グラフ化する最後の日付・時間
    • periods_plot: グラフ化する足の数(int型)
  • start, end, periodsのうち二つが指定されている必要がある。
  • 何も指定しなければ、デフォルト値が入力される。
# Default Args
if com._count_not_none(start_plot,
                       end_plot, periods_plot) == 0:  # すべてNoneのままだったら
    end_plot = 'last'  # 最後の足から
    periods_plot = 300  # 300本足で切る
# first/last
start_plot = self.stock_dataframe.index[0] if start_plot == 'first' else start_plot
end_plot = self.stock_dataframe.index[-1] if end_plot == 'last' else end_plot  # 'last'=最後の足とはindexの最後

start_plot, end_plotを指定して描画してみます。

# fx.resample('5T')  # 既に5分足に変換されているので必要ない
start = pd.datetime(2017,6,17,9,0,0)     # 2017/6/17 09:00
end = pd.datetime(2017,6,17,23,0,0)      # 2017/6/17 23:00 
fx.plot(start_plot=start, end_plot=end)  # 2017/6/17 09:00-23:00までをプロットする
fx.show('jupyter')

gif7.gif

2017/6/17 09:00 - 2017/6/17 23:00の5分足が描かれました。

view範囲の指定

plotlyのズームイン / アウト、スクロールを使えば表示範囲外のところも見れます。
しかし、見たい期間が最初から決まっているのにもかかわらず、グラフ化してからスクロールするのはメンドウです。

そこで、plotメソッドではグラフ化して最初に見えるビュー範囲(view)を指定できます。

例えば2017/5/8から2017/6/5の4時間足が見たいとしましょう。

fx.resample('4H')  # 4時間足に変換
start = pd.datetime(2017,5,8)   # 2017/5/8
end = pd.Timestamp('20170605')  # 2017/6/5(Timestampでも指定可能)
fx.plot(start_view=start, end_view=end) # 2017/5/8 - 2017/6/5を表示する
fx.show('jupyter')

gif8.gif

次はstart_view, end_viewの指定ではなく、end_view, periods_viewを使って表示してみます。

fx.resample('D')  # 日足に変換
fx.plot(periods_view=20, end_view='last')
    # `end_view`を'last' 最後の足に設定する
    # `periods_view`で20本足表示する
fx.show('html')  # html形式で表示

gif4.gif

  • plotメソッドのビュー範囲を決める引数
    • start_view: 表示する最初の日付・時間
    • end_view: 表示する最後の日付・時間
    • periods_view: 表示する足の数(int型)
  • start, end, periodsのうち二つが指定されている必要がある。
  • 何も指定しなければ、デフォルト値が入力される。

Default Args

if com._count_not_none(start_view,
end_view, periods_view) == 0: # すべてNoneのままだったら
end_view = 'last' # 最後の足から
periods_view = 50 # 50本足までを表示する

first/last

start_view = plot_dataframe.index[0] if start_view == 'first' else start_view
end_view = plot_dataframe.index[-1] if end_view == 'last' else end_view # 'last'はindexの最後


`periods`の指定は`end`が指定された場合は`start`、`start`が指定された場合は`end`を計算します。
計算する関数は次のようにしました。

```python
from pandas.core import common as com
def set_span(start=None, end=None, periods=None, freq='D'):
    """ 引数のstart, end, periodsに対して
    startとendの時間を返す。

    * start, end, periods合わせて2つの引数が指定されていなければエラー
    * start, endが指定されていたらそのまま返す
    * start, periodsが指定されていたら、endを計算する
    * end, periodsが指定されていたら、startを計算する
    """
    if com._count_not_none(start, end, periods) != 2:  # 引数が2個以外であればエラー
        raise ValueError('Must specify two of start, end, or periods')
    # `start`が指定されていれば`start`をそのまま返し、そうでなければ`end`から`periods`引いた時間を`start`とする。
    start = start if start else (pd.Period(end, freq) - periods).start_time
    # `end`が指定されていれば`end`をそのまま返し、そうでなければ`start`から`periods`足した時間を`end`とする。
    end = end if end else (pd.Period(start, freq) + periods).start_time
    return start, end

呼び出すときは次のようにします。

start_view, end_view = set_span(start_view, end_view, periods_view, self.freq)

説明は省きましたが、グラフ化する時間足もviewと同様にperiods_plot引数として指定できます。

viewself._figlayoutにおいて、xaxisの範囲(range)を変更するのに使います。

変更する際、unix時間に変換する必要があるので、to_unix_time関数に通します。

def to_unix_time(*dt: pd.datetime)->iter:
    """datetimeをunix秒に変換
    引数: datetime(複数指定可能)
    戻り値: unix秒に直されたイテレータ"""
    epoch = pd.datetime.utcfromtimestamp(0)
    return ((i - epoch).total_seconds() * 1000 for i in dt)
view = list(to_unix_time(start_view, end_view))
# ---------Plot graph----------
self._fig['layout'].update(xaxis={'showgrid': showgrid, 'range': view},
                           yaxis={"autorange": True})

右側に空白を作る

引数shiftに指定した足の本数だけ、右側に空白を作ります。

時間足が短いとうまくいきません。原因究明中です。
想定より多めに足の数を設定することでとりあえず回避しています。

予測線を引いたり一目均衡表を使うとき必要になる機能だと思います。

fx.plot()
fx.show('jupyter')
fx.plot(shift=30)
fx.show('jupyter')

gif5.gif

plotメソッドのshift引数を30とし、30本の足だけの空白を右側(時間の遅い側)に作ることができました。
処理としては、先ほど出てきたset_span関数を使って、end_viewに30本足分の時間足を足してあげます。

end_view = set_span(start=end_view, periods=shift,
                    freq=self.freq)[-1] if shift else end_view

data範囲、plot範囲, view範囲、shiftまとめ

図示すると以下のような感じです。
png4.PNG

まとめ

メソッド一覧

  • __init__
    • pandas.Dataframeをインスタンス化
    • open, high, low, closeのカラムを持たないとエラー
    • indexがDatetimeIndexでなければエラー
  • resampleメソッド
    • freq引数で時間足を決める。
    • stock_dataframeを決める。
  • plotメソッド
    • plot範囲(plot_dataframe)を決める。
      • start_plot
      • end_plot
      • periods_plot
    • view範囲を決める。
      • start_view
      • end_view
      • periods_view
    • グラフの右側の空白(shift)を決める。
      • shift
  • showメソッド
    • 出力形式を決める。
      • how='jupyter', 'html', 'png', 'jpeg', 'webp', 'svg'
    • ファイル名を決める。
      • filebasename

フローチャート

各メソッドの呼び出しに使う引数と戻り値、プロットに使うフローは以下の図の通りです。

figure1.PNG

ソースコード

そのうちgithubリポジトリ作ります。
githubに上げました。
github - u1and0/stockplot

import pandas as pd
from pandas.core import common as com
import stockstats as ss
from plotly.tools import FigureFactory as FF
import plotly.offline as pyo
pyo.init_notebook_mode(connected=True)


def set_span(start=None, end=None, periods=None, freq='D'):
    """ 引数のstart, end, periodsに対して
    startとendの時間を返す。

    * start, end, periods合わせて2つの引数が指定されていなければエラー
    * start, endが指定されていたらそのまま返す
    * start, periodsが指定されていたら、endを計算する
    * end, periodsが指定されていたら、startを計算する
    """
    if com._count_not_none(start, end, periods) != 2:  # Like a pd.date_range Error
        raise ValueError('Must specify two of start, end, or periods')
    start = start if start else (pd.Period(end, freq) - periods).start_time
    end = end if end else (pd.Period(start, freq) + periods).start_time
    return start, end


def to_unix_time(*dt: pd.datetime)->iter:
    """datetimeをunix秒に変換
    引数: datetime(複数指定可能)
    戻り値: unix秒に直されたリスト"""
    epoch = pd.datetime.utcfromtimestamp(0)
    return ((i - epoch).total_seconds() * 1000 for i in dt)


class StockPlot:
    """Plot candle chart using Plotly & StockDataFrame
    # USAGE

    ```
    # Convert StockDataFrame as StockPlot
    fx = StockPlot(sdf)

    # Add indicator
    fx.append('close_25_sma')

    # Remove indicator
    fx.append('close_25_sma')

    # Plot candle chart
    fx.plot()
    fx.show()
    ```
    """

    def __init__(self, df: pd.core.frame.DataFrame):
        # Arg Check
        co = ['open', 'high', 'low', 'close']
        assert all(i in df.columns for i in co), 'arg\'s columns must have {}, but it has {}'\
            .format(co, df.columns)
        if not type(df.index) == pd.tseries.index.DatetimeIndex:
            raise TypeError(df.index)
        self._init_stock_dataframe = ss.StockDataFrame(df)  # スパン変更前のデータフレーム
        self.stock_dataframe = None  # スパン変更後、インジケータ追加後のデータフレーム
        self.freq = None  # 足の時間幅
        self._fig = None  # <-- plotly.graph_objs

    def resample(self, freq: str):
        """Convert ohlc time span

        USAGE: `fx.resample('D')  # 日足に変換`

        * Args:  変更したい期間 M(onth) | W(eek) | D(ay) | H(our) | T(Minute) | S(econd)
        * Return: スパン変更後のデータフレーム
        """
        self.freq = freq
        self.stock_dataframe = self._init_stock_dataframe.ix[:, ['open', 'high', 'low', 'close']]\
            .resample(freq).agg({'open': 'first', 'high': 'max', 'low': 'min', 'close': 'last'})\
            .dropna()
        return self.stock_dataframe

    def plot(self, start_view=None, end_view=None, periods_view=None, shift=None,
             start_plot=None, end_plot=None, periods_plot=None,
             showgrid=True, validate=False, **kwargs):
        """Retrun plotly candle chart graph

        USAGE: `fx.plot()`

        * Args:
            * start, end: 最初と最後のdatetime, 'first'でindexの最初、'last'でindexの最後
            * periods: 足の本数
            > **start, end, periods合わせて2つの引数が必要**
            * shift: shiftの本数の足だけ右側に空白
        * Return: グラフデータとレイアウト(plotly.graph_objs.graph_objs.Figure)
        """
        # ---------Set "plot_dataframe"----------
        # Default Args
        if com._count_not_none(start_plot,
                               end_plot, periods_plot) == 0:
            end_plot = 'last'
            periods_plot = 300
        # first/last
        start_plot = self.stock_dataframe.index[0] if start_plot == 'first' else start_plot
        end_plot = self.stock_dataframe.index[-1] if end_plot == 'last' else end_plot
        # Set "plot_dataframe"
        start_plot, end_plot = set_span(start_plot, end_plot, periods_plot, self.freq)
        plot_dataframe = self.stock_dataframe.loc[start_plot:end_plot]
        self._fig = FF.create_candlestick(plot_dataframe.open,
                                          plot_dataframe.high,
                                          plot_dataframe.low,
                                          plot_dataframe.close,
                                          dates=plot_dataframe.index)
        # ---------Set "view"----------
        # Default Args
        if com._count_not_none(start_view,
                               end_view, periods_view) == 0:
            end_view = 'last'
            periods_view = 50
        # first/last
        start_view = plot_dataframe.index[0] if start_view == 'first' else start_view
        end_view = plot_dataframe.index[-1] if end_view == 'last' else end_view
        # Set "view"
        start_view, end_view = set_span(start_view, end_view, periods_view, self.freq)
        end_view = set_span(start=end_view, periods=shift,
                            freq=self.freq)[-1] if shift else end_view
        view = list(to_unix_time(start_view, end_view))
        # ---------Plot graph----------
        self._fig['layout'].update(xaxis={'showgrid': showgrid, 'range': view},
                                   yaxis={"autorange": True})
        return self._fig

    def show(self, how='html', filebasename='candlestick_and_trace'):
        """Export file type"""
        if how == 'html':
            ax = pyo.plot(self._fig, filename=filebasename + '.html',
                          validate=False)  # for HTML
        elif how == 'jupyter':
            ax = pyo.iplot(self._fig, filename=filebasename + '.html',
                           validate=False)  # for Jupyter Notebook
        elif how in ('png', 'jpeg', 'webp', 'svg'):
            ax = pyo.plot(self._fig, image=how, image_filename=filebasename,
                          validate=False)  # for file exporting
        else:
            raise KeyError(how)
        return ax

TODOs

46
52
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
46
52

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?