J-QuantAPI(プレミアム)で前場後場リターンを分析する
※この記事は2024年のマケデコアドベントカレンダーの16日目の枠で掲載する予定の記事です。
はじめまして、塩辛botterと申します。
日本株、仮想通貨のbot構築をメインで扱っています!
最近情報発信を始めたばかりで、不慣れなところも多々ありますが、よろしくお願い致します。
なお投資は自己責任で、NFA & DYORでお願いします
前場後場リターンに着目する理由
株式botを10年以上運用していますが、以下のような悩みがずっとありました。
- 指値が遠くなりすぎて絶対に刺さらないので注文をキャンセルした
- 寄り指が刺さらず失効した
- 寄りで決済した。
→このとき、前場時点で買付余力は復活するものの、私が運用しているのは寄り引けのbotがほとんどであり、仕掛けができるのは当日引け後です。後場は何もできず指をくわえてみているだけとなり、資金効率が悪くなっていました。
J-Quants APIには前場4本値を昼休み中に提供してくれるAPIがあると知り、これを使えば昼休みに自動でテクニカル分析から発注処理を行い、後場の間だけでも取引して資金効率アップにつなげられるのでは!? と考え、検証してみました。
前場4本値の取り方
公式クライアントを使うと簡単にできます。(プレミアムプランが必要なことに注意)
pip install jquants-api-client
前場終了時点のデータは当然ですが終値などが欠損(NaN)になっています。間違ってdropnaしないように注意が必要です。
なおget_prices_prices_amで取得できるデータは当日の前場のみです。土日など非営業日に実行しても以下の処理は必ず失敗するので注意してください。
import jquantsapi as ja
cli = ja.Client(mail_address=mail_address, password=password)
# 前日以前の4本値を取得する
# -7 は適宜調整してください。テクニカル指標で使う足の分だけ取得する
start_dt = order_dt + timedelta(days=-7)
end_dt = order_dt + timedelta(days=-1)
df_ohlcv1 = cli.get_price_range(start_dt=start_dt, end_dt=end_dt)
# 前場4本値を取得
df_ohlcv2 = cli.get_prices_prices_am()
# 長さが0だったら、60秒SLEEPしながら10回待つ(フライング対策)
for _ in range(10):
if len(df_ohlcv2) > 0:
break
sleep(60)
df_ohlcv2 = cli.get_prices_prices_am()
if len(df_ohlcv2) == 0:
raise ValueError("前場データの取得に失敗")
df = pd.concat([df_ohlcv1, df_ohlcv2], axis=0)
df = strip_code(df) # 5ケタコードをきりつめて4ケタ化
df = df.set_index('Date', drop=True)
return df
## Codeを切り詰める処理。
def strip_code(df, col="Code"):
df = df.rename(columns={col: 'code_raw'})
df['code'] = df['code_raw'].str[:-1]
return df
タスクスケジューラやcronなど何でもよいので12時過ぎの時刻で起動し、上記の処理でDataFrameを取得します。あとは通常の株botと同じように指標の計算と発注対象の選定、発注までを自動化することで、後場向けの戦略を構築可能です
前場足、後場足を分析する
さて、具体的な戦略に入る前に、そもそも前場や後場はどういった値動きなのか分析が必要です。
以降は特に記載しない限りTOPIX500を2012年以降について分析しています
以下のように前場・後場の値動きを計算し、Waterfallチャートにプロットしてみます
dfs = []
for code in tqdm(df_ohlcv['code'].unique()):
df = df_ohlcv[df_ohlcv['code'] == code]
df = df.sort_values('Date')
# 前日終値→当日始値
df['prev_cl_to_op'] = df['AdjustmentOpen']/df.shift(1)['AdjustmentClose'] - 1
# 始値→前場終値
df['op_to_morn_cl'] = df['MorningClose']/df['Open'] - 1
# 前場終値→後場始値
df['morn_cl_to_aft_op'] = df['AfternoonOpen']/df['MorningClose'] - 1
# 後場始値→終値
df['aft_op_to_cl'] = df['Close']/df['AfternoonOpen'] - 1
# 前場、後場の高値と安値の比率
df['morn_high_low'] = df['MorningHigh']/df['MorningLow'] - 1
df['afternoon_high_low'] = df['AfternoonHigh']/df['AfternoonLow'] - 1
# 前日終値→当日前場終値
df['prev_cl_to_morn_cl'] = df['MorningAdjustmentClose']/df.shift(1)['AdjustmentClose'] - 1
dfs.append(df)
df_ohlcv = pd.concat(dfs)
上のWaterfallは、前日終値→当日始値、当日始値→前場終値、前場終値→後場始値、後場始値→後場終値の動きを図示したものです
これを見ると前日終値→当日始値が圧倒的に強いですね。いわゆるオーバーナイト効果です。
その後、前場は陰線を描き、昼休みはちょっとだけギャップアップし、後場は陽線で終わるというのが最近の日本株のようです。
前場と後場の高値安値比率と標準偏差は以下の通りです。
前場:1.892%, 0.0128
後場:1.199%, 0.0086
想定通り、前場の方が値動きが激しいようです。
これを見ると素直にオーバーナイト効果をLongで狙った方がよさそうですが、一方で持ち越しはリスクも伴います。私はすでに別の戦略でリスクを取りまくっているので、後場リターンの+0.0097%を狙っていこうと思います。
※余談ですが、J-Quants APIが出てくる前は、JPXデータクラウドというサイトから高額なCSV(Tickデータ)を買ってきて、自分で前場足・後場足に変換が必要でした。月ごとに購入しなければならず、データサイズも何ギガもあり、かつ一定期間が過ぎると再ダウンロードができなくなるというクソ仕様でした。上の図を作るのにも、初期投資で何万円もかかり、データ加工に1か月ぐらいはかかったでしょう。いまはAPI一発で、しかも分割調整済みの前場足・後場足を手軽に取れるのは非常にいい時代になりました。プレミアムプランの価格も全然安いものです。
前場と後場(と前日終値)の相関性
前場と後場のリターンには相関性があるかを調べましょう
import seaborn as sns
sns.lmplot(data=df_ohlcv, x='op_to_morn_cl', y='aft_op_to_cl')
corr = df_ohlcv['op_to_morn_cl'].corr(df_ohlcv['aft_op_to_cl'])
print(corr)
順位相関係数= -0.00870であり、わずかですが逆相関があります。
つまり前場上げたものは後場下げ、前場下げたものは後場上げるようです。
ここでふと思いついたのですが、普段皆さんが株アプリやマーケットニュースで見る数字は前日終値をベースにしたものではないでしょうか?
たとえば、昼休みのサラリーマンがスマホで株価を見て、保有株が暴騰しており「+10%だ! 利益確定しよう」と売りに出すかもしれません。これを先回りしたいと考えたとき、やはり前場終値よりも前日終値をベースに見た方がよさそうです。
# 前日終値→当日前場終値と後場始値→終値の相関を見る
corr = df_ohlcv['prev_cl_to_morn_cl'].corr(df_ohlcv['aft_op_to_cl'], method='spearman')
print(corr)
順位相関係数 = -0.00892 とわずかながら改善しました。
実際のトレードシミュレーション
特徴量について逆張り(上位25銘柄に売り、下位25銘柄に買いをしかけたとき)は以下のような損益グラフになります
# Long, Shortでそれぞれ何銘柄仕掛けるか
max_pos = 25
# ランキング対象とする特徴量
feat = 'prev_cl_to_morn_cl'
# ranking
df_ohlcv['rank_asc_' + feat] = df_ohlcv.groupby('Date')[feat].rank(ascending=True, method='first')
df_ohlcv['rank_desc_' + feat] = df_ohlcv.groupby('Date')[feat].rank(ascending=False, method='first')
# TARGET. わかりやすいようにLongは"L", Shortは"S"で表記
df_ohlcv['aft_op_to_cl_L'] = df_ohlcv['aft_op_to_cl']
df_ohlcv['aft_op_to_cl_S'] = -df_ohlcv['aft_op_to_cl']
# plot
fig, ax = plt.subplots(figsize=(10,6))
df_ohlcv = df_ohlcv.sort_index()
df_ohlcv['profit_L'] = np.where(df_ohlcv[f'rank_asc_{feat}'] <= max_pos, df_ohlcv['aft_op_to_cl_L'], np.nan)
df_ohlcv['profit_S'] = np.where(df_ohlcv[f'rank_desc_{feat}'] <= max_pos, df_ohlcv['aft_op_to_cl_S'], np.nan)
df_ohlcv['profit_L+S'] = np.where(df_ohlcv[f'rank_asc_{feat}'] <= max_pos, df_ohlcv['aft_op_to_cl_L'], np.where(df_ohlcv[f'rank_desc_{feat}'] <= max_pos, df_ohlcv['aft_op_to_cl_S'], np.nan))
df_ohlcv['profit_L'].cumsum().plot(ax=ax, label='Long')
df_ohlcv['profit_S'].cumsum().plot(ax=ax, label='Short')
df_ohlcv['profit_L+S'].cumsum().plot(ax=ax, label='Long+Short')
plt.legend()
plt.show()
Longの方が強いのは最初のWaterfallで見た通り、後場は全体平均として陽線だったので納得感があります。
期待値0.034%, Sharpeは年率0.48となりました。
0.034%をレバ2(LongとShortでレバ1ずつ)、年間200営業日で回すとすると、0.034*2*200 = 14.28%の利回りです。
さらなる改善
と、儲かりそうな話を書いてきましたが、このままではなかなかきつい気がしています。
0.034%というのは1,000円の株にとっての0.3~0.4円です。TOPIX500の薄商いの株であったら、自分が仕掛けたマーケットインパクトで吹っ飛んでしまいそうです。
ではTOPIX100の超大型に絞ってみてはどうか? 結果は以下となりました
期待値は0.016%と半分以下に下がってしまいました。
しかも近年右肩下がりであるため、このままでは運用は厳しそうです。
多少シグナルが減ってもよいので、期待値を上げる取り組みが必要となりそうです。
ぱっと思いついたのは以下です。
- 業種で絞ってみる。
- ETF(指数)とのペアにする。
- 前場で大きなボラが出たときだけ仕掛ける(ランキングだけでなく、絶対的な閾値も設ける)
- 曜日特性や月末月初、連休明けなどの要因を加味する
- 値動きだけでなく売買高や移動平均乖離、その他のテクニカル指標も合わせてランキング化する。→MLを活用できないか
- 決算発表前後の銘柄はリスクが高いため仕掛けない。これは
/fins/announcementエンドポイントで取得できますが、3月・9月期決算の銘柄しか取れない点に注意が必要です。
何かいい検証が出てきた方はtwitterからこっそり教えてください
補足
- 手数料およびマーケットインパクトを考慮していない点に注意してください。ただ日計りなら最近は手数料ゼロが当たり前になってきています。
- 後場が15:30まで伸びましたね‥。それによりエッジが失われている可能性も十分あります。
- STOPはりつきで売買できない場合を考慮していないです。が、TOPIX500ではそこまで気にしなくてもよさそう。
- TOPIX500は最新の500銘柄を過去にわたって適用しています。本当はその日時点での500銘柄を使うべきですが、、、この辺はAPIで対応してもらえると非常にありがたかったりします
まとめ
ここではあまり詳しく書けないですが、上記とはまったく異なる観点の前場後場分析で、botの期待値を大幅に改善できたことがあり、J-Quants APIには感謝してもしきれないです。
Discussion