下準備
モジュールインポート
必要なモジュールをインポートします。
# ----------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
(data
とlayout
がキーとなった辞書)が返されます。
画像を見るには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')
2017/3/20-2017/6/17の日足が描かれました。
plotlyの操作は
- グラフ上のマウスオーバーで値の表示
- グラフ上のドラッグでズームイン
- 軸上(真ん中)のドラッグでスクロール
- 軸上(端)のドラッグでズームアウト
- ダブルクリックで元のビューに戻る
- トリプルクリックで全体表示
時間足の変更
日足だけじゃなくて別の時間足も見たいです。
そういうときはresample
メソッドを使って時間幅を変更します。
fx.resample('H') # 1時間足に変更
fx.plot() # ローソク足プロット
fx.show('jupyter') # プロットの表示をJupyter Notebookで開く
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
メソッドを使います。
freq
はdf.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')
# 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')
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')
次は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形式で表示
-
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
引数として指定できます。
view
はself._fig
のlayout
において、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')
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まとめ
まとめ
メソッド一覧
-
__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
- plot範囲(
-
show
メソッド- 出力形式を決める。
how='jupyter', 'html', 'png', 'jpeg', 'webp', 'svg'
- ファイル名を決める。
filebasename
- 出力形式を決める。
フローチャート
各メソッドの呼び出しに使う引数と戻り値、プロットに使うフローは以下の図の通りです。
ソースコード
そのうち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
- 平均足プロット
- サブプロット
-
指標の追加/削除--> 続き Plotlyでぐりぐり動かせる為替チャートを作る(2)