#自己紹介
3ヶ月ほど前から趣味でPythonを触っている、金融の知識ゼロの超初心者です。
そんな超初心者の僕がSmartTrade社でインターンをさせていただくことになったので、プログラミングも投資も初心者でも理解出来るように、学んだ内容を随時更新していきたいと思います。
#QuantXとは
QuantXとはSmart Trade社が提供しているプラットホームです。QuantXを用いることで、無料で株価データを分析し、アルゴリズムを開発することができます。この記事では、QuantXの公式サンプルコードを読んでいくだけなので、登録は必須ではありませんが、無料で登録することができるので登録してみてください。登録はQuantXに入門してみるを参考にするといいかもしれません。
#この記事について
この記事は、QuantXの移動平均を使ったサンプルアルゴリズムの解説をしていきます。
以前僕が書いたQuantXの標準テンプレートを理解するとも繋がっているので、まだ読んでいないよ、という方は一度読んでみてください。
#[DEMO] 移動平均を使った基本的なアルゴリズム
画面右上のヘルプから移動平均を使ったサンプルを選択すると、
def initialize(ctx):
# 設定
ctx.logger.debug("initialize() called")
ctx.configure(
target="jp.stock.daily",
channels={ # 利用チャンネル
"jp.stock": {
"symbols": [
"jp.stock.1305",
"jp.stock.9984",
"jp.stock.9983",
"jp.stock.7201",
"jp.stock.9201",
"jp.stock.9202",
"jp.stock.7203"
],
"columns": [
#"open_price_adj", # 始値(株式分割調整後)
#"high_price_adj", # 高値(株式分割調整後)
#"low_price_adj", # 安値(株式分割調整後)
"close_price", # 終値
"close_price_adj", # 終値(株式分割調整後)
"volume_adj", # 出来高
"txn_volume", # 売買代金
]
}
}
)
def _mavg_signal(data):
m25 = data["close_price_adj"].fillna(method='ffill').rolling(window=25, center=False).mean()
m75 = data["close_price_adj"].fillna(method='ffill').rolling(window=75, center=False).mean()
ratio = m25 / m75
buy_sig = ratio[(ratio > 1.02) & (ratio < 1.05)]
sell_sig = ratio[ratio < 0.95]
return {
"mavg_25:price": m25,
"mavg_75:price": m75,
#"mavg_ratio:ratio": ratio,
"buy:sig": buy_sig,
"sell:sig": sell_sig,
}
# シグナル登録
ctx.regist_signal("mavg_signal", _mavg_signal)
def handle_signals(ctx, date, current):
'''
current: pd.DataFrame
'''
done_syms = set([])
for (sym,val) in ctx.portfolio.positions.items():
returns = val["returns"]
#ctx.logger.debug("%s %f" % (sym, returns))
if returns < -0.03:
sec = ctx.getSecurity(sym)
sec.order(-val["amount"], comment="損切り(%f)" % returns)
done_syms.add(sym)
elif returns > 0.05:
sec = ctx.getSecurity(sym)
sec.order(-val["amount"], comment="利益確定売(%f)" % returns)
done_syms.add(sym)
buy = current["buy:sig"].dropna()
for (sym,val) in buy.items():
if sym in done_syms:
continue
sec = ctx.getSecurity(sym)
sec.order(sec.unit() * 1, comment="SIGNAL BUY")
#ctx.logger.debug("BUY: %s, %f" % (sec.code(), val))
pass
sell = current["sell:sig"].dropna()
for (sym,val) in sell.items():
if sym in done_syms:
continue
sec = ctx.getSecurity(sym)
sec.order(sec.unit() * -1, comment="SIGNAL SELL")
#ctx.logger.debug("SELL: %s, %f" % (sec.code(), val))
pass
というコードが表示されると思います。
#移動平均とは
移動平均線とは、一定期間の株価の終値の平均をつなぎ合わせた指標です。例えば、5日移動平均線の1月5日の値=「1/1〜1/5の終値の平均値」となっています。
#コードの解説
基本的には以前書いたQuantXの標準テンプレートを理解するを読めば前半部分は理解できるかと思います。この記事では、QuantXの標準テンプレートでは出てこなかった箇所についての解説をしていこうと思います。
#売買シグナル生成部分
def _mavg_signal(data):
m25 = data["close_price_adj"].fillna(method='ffill').rolling(window=25, center=False).mean()
m75 = data["close_price_adj"].fillna(method='ffill').rolling(window=75, center=False).mean()
ratio = m25 / m75
buy_sig = ratio[(ratio > 1.02) & (ratio < 1.05)]
sell_sig = ratio[ratio < 0.95]
return {
"mavg_25:price": m25,
"mavg_75:price": m75,
#"mavg_ratio:ratio": ratio,
"buy:sig": buy_sig,
"sell:sig": sell_sig,
}
ctx.regist_signal("mavg_signal", _mavg_signal)
###def _mavg_signal(data):
ここはQuantXの標準テンプレートにもあった通り売買シグナルを指定する関数を定義しています。
###m25 = data["close_price_adj"].fillna(method='ffill').rolling(window=25, center=False).mean()
ここは複雑なので1つずつ見て行きましょう
####m25 = data["close_price_adj"]
jp.stock.1305 | jp.stock.9984 | jp.stock.9983 | jp.stock.7201 | jp.stock.9201 | jp.stock.9202 | jp.stock.7203 | |
---|---|---|---|---|---|---|---|
2017/5/1 | 終値 | 終値 | 終値 | 終値 | 終値 | 終値 | 終値 |
2017/5/2 | 終値 | 終値 | 終値 | 終値 | 終値 | 終値 | 終値 |
2017/5/3 | 終値 | 終値 | 終値 | 終値 | 終値 | 終値 | 終値 |
2017/5/4 | 終値 | 終値 | 終値 | 終値 | 終値 | 終値 | 終値 |
2017/5/5 | 終値 | 終値 | 終値 | 終値 | 終値 | 終値 | 終値 |
2017/5/6 | 終値 | 終値 | 終値 | 終値 | 終値 | 終値 | 終値 |
2017/5/7 | 終値 | 終値 | 終値 | 終値 | 終値 | 終値 | 終値 |
︙ | ︙ | ︙ | ︙ | ︙ | ︙ | ︙ | ︙ |
####.fillna(method='ffill')
欠損値の補完を行なっています
引数は
method='ffill':前の値で置き換え
method='bfill':後ろの値で置き換え
という意味になります
####.rolling(window=25, center=False).mean()
rollingメソッドはwindowsで指定した数の平均をとります。例えば
example
index | 値 |
---|---|
1 | 1 |
2 | 3 |
3 | 5 |
4 | 7 |
5 | 9 |
6 | 11 |
に対してexample.rolling(window=3, center=False).mean()を行うと、 |
index | 値 |
---|---|
1 | NaN |
2 | NaN |
3 | 3 |
4 | 5 |
5 | 7 |
6 | 9 |
となります。(例えばindex3にはexampleのindex1,2,3の3つの数字(1,3,5)の平均である3が、index4にはexampleのindex2,3,4の3つの数字(3,5,7)の平均である5が入ります) |
従って、data["close_price_adj"].fillna(method='ffill').rolling(window=25, center=False).mean()では
jp.stock.1305 | jp.stock.9984 | jp.stock.9983 | jp.stock.7201 | jp.stock.9201 | jp.stock.9202 | jp.stock.7203 | |
---|---|---|---|---|---|---|---|
2017/5/1 | 25日移動平均 | 25日移動平均 | 25日移動平均 | 25日移動平均 | 25日移動平均 | 25日移動平均 | 25日移動平均 |
2017/5/2 | 25日移動平均 | 25日移動平均 | 25日移動平均 | 25日移動平均 | 25日移動平均 | 25日移動平均 | 25日移動平均 |
2017/5/3 | 25日移動平均 | 25日移動平均 | 25日移動平均 | 25日移動平均 | 25日移動平均 | 25日移動平均 | 25日移動平均 |
2017/5/4 | 25日移動平均 | 25日移動平均 | 25日移動平均 | 25日移動平均 | 25日移動平均 | 25日移動平均 | 25日移動平均 |
2017/5/5 | 25日移動平均 | 25日移動平均 | 25日移動平均 | 25日移動平均 | 25日移動平均 | 25日移動平均 | 25日移動平均 |
2017/5/6 | 25日移動平均 | 25日移動平均 | 25日移動平均 | 25日移動平均 | 25日移動平均 | 25日移動平均 | 25日移動平均 |
2017/5/7 | 25日移動平均 | 25日移動平均 | 25日移動平均 | 25日移動平均 | 25日移動平均 | 25日移動平均 | 25日移動平均 |
︙ | ︙ | ︙ | ︙ | ︙ | ︙ | ︙ | ︙ |
と25日移動平均が出力されます。 | |||||||
###m75 = data["close_price_adj"].fillna(method='ffill').rolling(window=75, center=False).mean() | |||||||
同様に75日移動平均が出力されます。 | |||||||
###ratio = m25 / m75 | |||||||
25日移動平均と75日移動平均の比率を計算しています。 | |||||||
###buy_sig = ratio[(ratio > 1.02) & (ratio < 1.05)] | |||||||
ratioが 1.02<ratio<1.05 である要素だけがbuy_sigに入り、それ以外は None になります。 |
e.g.)
jp.stock.1305 | jp.stock.9984 | jp.stock.9983 | jp.stock.7201 | jp.stock.9201 | jp.stock.9202 | jp.stock.7203 | |
---|---|---|---|---|---|---|---|
2017/5/1 | 値 | 値 | None | 値 | None | 値 | None |
2017/5/2 | None | None | 値 | 値 | None | 値 | 値 |
2017/5/3 | None | 値 | None | None | 値 | None | 値 |
2017/5/4 | 値 | 値 | 値 | 値 | 値 | None | 値 |
2017/5/5 | None | 値 | None | 値 | None | None | 値 |
2017/5/6 | 値 | None | None | 値 | None | 値 | 値 |
2017/5/7 | 値 | None | 値 | None | 値 | 値 | None |
︙ | ︙ | ︙ | ︙ | ︙ | ︙ | ︙ | ︙ |
###sell_sig = ratio[ratio < 0.95] | |||||||
同様にratioが ratio<0.95 である要素だけがsell_sigに入り、それ以外は None になります。 | |||||||
###return {"mavg_25:price": m25,"mavg_75:price": m75,"buy:sig": buy_sig, "sell:sig": sell_sig,} | |||||||
後ほど利用する配列を定義して残します。 また、チャートで表示する際の項目名も定義します。 例えば「buy_sig」という配列を"buy:sig"という名前でチャートの下に表示させます。 |
なお、suffixとしてsigが使用された場合(例えば今回では'buy:sig'と'sell:sig')は、特別扱いされ、 チャート上で矩形がfillされて範囲の確認が可能になります。 このとき、
keyにbuy,pos が含まれる場合には、ピンク
keyにsell,neg が含まれる場合には、水色
の色が固定的に利用されます。
###ctx.regist_signal("mavg_signal", _mavg_signal)
標準テンプレートでもやったように、シグナルを登録します
#日ごとの処理部分の記述
def handle_signals(ctx, date, current):
done_syms = set([])
for (sym,val) in ctx.portfolio.positions.items():
returns = val["returns"]
#ctx.logger.debug("%s %f" % (sym, returns))
if returns < -0.03:
sec = ctx.getSecurity(sym)
sec.order(-val["amount"], comment="損切り(%f)" % returns)
done_syms.add(sym)
elif returns > 0.05:
sec = ctx.getSecurity(sym)
sec.order(-val["amount"], comment="利益確定売(%f)" % returns)
done_syms.add(sym)
###def handle_signals(ctx, date, current):
currentは、dateの当日のデータとシグナルを含んだ pandas.DataFrame オブジェクトです。以下のようなpandas.DataFrame オブジェクトになっています。
| |mavg_25:price|mavg_75:price|buy:sig |sell:sig |
|---|---|---|---|---|---|
|jp.stock.1305 |値 |値 |値 |値 |
|jp.stock.9984 |値 |値 |値 |値 |
|jp.stock.9983 |値 |値 |値 |値 |
|jp.stock.7201 |値 |値 |値 |値 |
|jp.stock.9201 |値 |値 |値 |値 |
|jp.stock.9202 |値 |値 |値 |値 |
|jp.stock.7203 |値 |値 |値 |値 |
###done_syms = set([])
後に必要になる、空のsetオブジェクトを用意しておきます。
###for (sym,val) in ctx.portfolio.positions.items():
ctx.portfolio.positionsには
毎日のポジションが,下記のような辞書型で格納されています。
{'jp.stock.1305':
{'position_ratio': 値, 'amount': 値, 'pnl': -値,
'total_buy_price': 値, 'value': 値, 'portfolio_ratio': 値,
'returns': 値, 'buy_price': 値, 'total_sell_price': 値,
'sell_price': 値, 'max_returns': 値},
'jp.stock.9984':
{'position_ratio': 値, 'amount': 値, 'pnl': 値,
'total_buy_price': 値, 'value': 値, 'portfolio_ratio': 値,
'returns': 値, 'buy_price': 値, 'total_sell_price': 値,
'sell_price': 値, 'max_returns': 値}}
....
for 文のsymは
'ctx.configureで指定した証券コード'
for文のvalは
{'position_ratio': 値, 'amount': 値, 'pnl': -値,
'total_buy_price': 値, 'value': 値, 'portfolio_ratio': 値,
'returns': 値, 'buy_price': 値, 'total_sell_price': 値,
'sell_price': 値, 'max_returns': 値}
がctx.configureで指定した証券コードの数だけ入っています。
###returns = val["returns"]
for文のvalにはctx.configureで指定した証券コードの'returns'、つまり損益率の値が入っています。
###if returns < -0.03:
return=証券コードの損益率なので、ここでは損益率が-0.03未満になった場合に行う操作が入っています。つまり3%以上落下した場合に実行されます。
###sec = ctx.getSecurity(sym)
Securityとは銘柄を表すオブジェクトであり、ctx.getSecurity() で取得できます。このオブジェクトを使用して注文を行ないます。symには前述した通りctx.configureで指定した証券コードが入っています。
###sec.order(-val["amount"], comment="損切り(%f)" % returns)
order()メソッドで数量を指定して注文を行ないます。
第一引数=注文数。株式の場合は株数。
第二引数=コメント。バックテスト結果(個別銘柄詳細)に表示されます。
ここでは引数が(-val["amount"], comment="損切り(%f)" % returns)となっているので、-val["amount"]だけ注文をします。マイナスなので売っていますね。また、コメントに"損切り(値)"と表示されていることが確認できると思います。ちなみに、%returnsは(%f)にreturnsを代入するという意味のコードです。
done_syms.add(sym)
for文の前に作っておいたdone_syms = set([])にsym=損益率が-0.03未満の証券コードを追加します。
###elif returns > 0.05:
同様にreturns が 0.05より大きい場合に利益確定売をするようなコードを書きます。
###buy = current["buy:sig"].dropna()
buyシグナルの項目のpandas.Seriesオブジェクトを取得します。dropna()は欠損値のあるデータを除外するメソッドです。
###for (sym,val) in buy.items():
buyシグナルの項目のpandas.Seriesオブジェクトの値を取得しています。
###if sym in done_syms:
もしdone_symsにすでにsymが入っていたらシグナルによる売買と「損切り、利益確定売り」が被ってしまうのでdone_symsにすでにsymが入っていないかを確認します。
###continue
もしdone_symsにすでにsymが入っていたら、それ以降の処理が実行されずにスキップされ、次のループに進みます。done_symsにすでにsymが入っていない場合、つまりシグナルによる売買と「損切り、利益確定売り」の被りがない場合のみ次の処理に進みます。
###sec = ctx.getSecurity(sym)
Securityとは銘柄を表すオブジェクトであり、ctx.getSecurity() で取得できます。このオブジェクトを使用して注文を行ないます。symには前述した通りctx.configureで指定した証券コードが入っています。
###sec.order(sec.unit() * 1, comment="SIGNAL BUY")
再びorder()メソッドです。sec.unit() * 1で「売買単位×1」の注文をするという量を指定し、comment="SIGNAL BUY"で"SIGNAL BUY"というコメントを出力します。
###pass
pass文は、「何も行わない」という動作の命令です。
###sell = current["sell:sig"].dropna() 以降
同様に売り注文も実行します。
#宣伝
毎週、SmartTrade社のオフィス(神田千代田共同ビル4階)で勉強会(水曜日18時〜もくもく会、金曜日19時〜勉強会)を行なっています。気軽に参加してみてください!
Pythonアルゴリズム勉強会HP:https://python-algo.connpass.com/
(connpassって言うイベントサイトに飛びます)