⚠️ 免責事項: 本記事は技術解説であり、金融商品取引法上の投資助言には該当しません。シミュレーション結果は将来の利益を保証するものではなく、先物取引には元本超過損のリスクがあります。実運用はご自身の責任で行ってください。
BTC 自動売買 × モンテカルロ法の記事で紹介した「1万通りのシナリオを回してから売買判断する」アーキテクチャを、日本株インデックス — 具体的には 日経225 マイクロ先物 に応用してみます。
BTC と違って日経225 はザラ場の時間が決まっています。しかしナイトセッションがあり流動性は世界最高水準、さらに分散効果で個別株のジャンプリスクが平滑化されるため、幾何ブラウン運動(GBM)を前提とするモンテカルロと相性が良いという利点があります。
TL;DR
- 勝率 55% かつ期待値 50pt 以上のときだけ発注する二段ゲート
- ケリー基準で枚数を自動調整(上限 25%)
- 1万パスの Monte Carlo を翌営業日終値分布として生成
日経225 マイクロ先物がモンテカルロ法と相性が良い 3 つの理由#
個別株を Monte Carlo で扱おうとすると、決算・TOB・不祥事・配当落ち といった「ジャンプイベント」が頻発し、log-normal を前提とする GBM の精度が出にくくなります。日経225 は約 225 銘柄の加重平均なので、これらの個別イベントが分散効果で平滑化されます。
主な利点は次の通りです。
- GBM の前提との整合性: 個別株より裾が薄く、log-normal 近似が機能する
- 流動性: 大証ラージ・ミニ・マイクロいずれも世界トップクラスの板厚
- 取引時間: 日中 8:45〜15:45、ナイト 17:00〜翌6:00(2024年11月5日以降の現行仕様)で、米国市場と連動した値動きをほぼ即時に反映できる
- マクロ情報主体: FOMC・日銀会合・CPI・地政学など、Claude が要約しやすい情報源で判断できる
日経225 マイクロ先物の仕様#
マイクロ先物は 2023年5月29日に大阪取引所で取引が開始された商品で、ミニ先物のさらに 1/10 サイズです。
| 項目 | 内容 |
|---|
| 取引単位 | 日経平均株価 × 10 円 |
| 呼値 | 5 円 |
| 必要証拠金(目安) | 1〜2万円台/枚(SPAN により変動) |
| 取引時間 | 日中 8:45-15:45 / ナイト 17:00-翌6:00 ※2024年11月5日以降 |
| 限月 | 期近〜期先複数 |
たとえば日経225 が 38,000 円のとき、1 枚あたりの想定元本は 380,000 円ですが、レバレッジで実際の証拠金は 20,000 円前後。1 ティック(5円)動くと 50円の損益なので、検証コストが非常に低く、自動売買のプロトタイピングに向いています。
システム構成#
全体像はこんなイメージです。
- データ取得層: ヒストリカルは yfinance、本番は J-Quants API
- モデル層: GBM パラメータ推定 + 10,000 パス Monte Carlo
- Claude 補正層: 直近マクロイベントを要約させて μ を ±α 補正
- 判定層: 勝率 + 期待値 + ケリー基準の二段ゲート
- 発注層: kabu ステーション API(三菱UFJ eスマート証券 / 旧 auカブコム証券)で自動発注
Python 実装例(GBM + 1 万パス Monte Carlo)#
最小構成の売買判定スクリプトを示します。Python 3.11+ で動作確認しています。なお TRADING_DAYS = 252 は米国市場基準で、日本市場は実際には約 245 営業日/年ですが、慣例として 252 を使っています(年率換算の若干の差異は σ で吸収されます)。また yfinance の終値は配当落ち未調整である点に留意してください。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
| """
日経225 マイクロ先物 × Monte Carlo 売買判定
- 取引単位: 指数 × 10 円
- 証拠金: 約 20,000 円/枚(SPAN により変動、ここでは固定値で簡略化)
"""
import numpy as np
import pandas as pd
import yfinance as yf
from dataclasses import dataclass
from datetime import datetime, timedelta
MICRO_MULTIPLIER = 10 # マイクロ先物 1 枚 = 指数 × 10 円
MICRO_MARGIN = 20_000 # 証拠金概算(SPAN により変動)
TRADING_DAYS = 252
# ------------------------------------------------------------
# 1. 価格データ取得(yfinance を MVP として、本番は J-Quants API 推奨)
# ------------------------------------------------------------
def fetch_nikkei225(lookback_days: int = 252) -> pd.Series:
end = datetime.now()
start = end - timedelta(days=lookback_days * 2)
df = yf.Ticker("^N225").history(start=start, end=end)
return df["Close"].dropna().tail(lookback_days)
# ------------------------------------------------------------
# 2. GBM パラメータ推定
# ------------------------------------------------------------
@dataclass
class GBMParams:
mu: float # 年率ドリフト
sigma: float # 年率ボラ
s0: float # 現在価格
def estimate_gbm(prices: pd.Series) -> GBMParams:
log_ret = np.log(prices / prices.shift(1)).dropna()
return GBMParams(
mu=float(log_ret.mean() * TRADING_DAYS),
sigma=float(log_ret.std() * np.sqrt(TRADING_DAYS)),
s0=float(prices.iloc[-1]),
)
# ------------------------------------------------------------
# 3. Monte Carlo シミュレーション
# ------------------------------------------------------------
def simulate_paths(p: GBMParams, n_paths: int = 10_000,
horizon_days: int = 1) -> np.ndarray:
"""horizon_days 後の価格分布を GBM で生成"""
dt = horizon_days / TRADING_DAYS
z = np.random.standard_normal(n_paths)
drift = (p.mu - 0.5 * p.sigma ** 2) * dt
diffusion = p.sigma * np.sqrt(dt) * z
return p.s0 * np.exp(drift + diffusion)
# ------------------------------------------------------------
# 4. 売買判定(勝率 + 期待値 + ケリー基準で枚数決定)
# ------------------------------------------------------------
@dataclass
class TradeSignal:
action: str # "LONG" / "SHORT" / "FLAT"
win_rate: float
expected_pt: float # 期待損益(指数ポイント)
var95_pt: float # 95% VaR
contracts: int
def _kelly_fraction(win_rate: float, ev: float, var95: float) -> float:
"""簡易ケリー: f* = (b*p - q) / b、b = ev/var95"""
if ev <= 0 or var95 <= 0:
return 0.0
b = ev / var95
f = (b * win_rate - (1 - win_rate)) / b
return float(np.clip(f, 0.0, 0.25)) # 25% でキャップ
def decide_trade(p: GBMParams, paths: np.ndarray, capital: float,
win_threshold: float = 0.55,
ev_threshold_pt: float = 50.0) -> TradeSignal:
pnl_long = paths - p.s0
pnl_short = p.s0 - paths
max_contracts = int(capital // MICRO_MARGIN)
candidates = [
("LONG", pnl_long),
("SHORT", pnl_short),
]
best = TradeSignal("FLAT", 0.0, 0.0, 0.0, 0)
for side, pnl in candidates:
win = float((pnl > 0).mean())
ev = float(pnl.mean())
var95 = float(-np.percentile(pnl, 5))
if win >= win_threshold and ev >= ev_threshold_pt:
f = _kelly_fraction(win, ev, var95)
contracts = max(1, int(max_contracts * f))
if ev > best.expected_pt:
best = TradeSignal(side, win, ev, var95, contracts)
return best
# ------------------------------------------------------------
# 5. 実行
# ------------------------------------------------------------
def main(capital: float = 500_000):
prices = fetch_nikkei225()
params = estimate_gbm(prices)
paths = simulate_paths(params, n_paths=10_000, horizon_days=1)
print(f"現在値 : {params.s0:>10,.0f}")
print(f"年率ドリフト : {params.mu:>10.2%}")
print(f"年率ボラ : {params.sigma:>10.2%}")
print(f"シナリオ平均 : {paths.mean():>10,.0f}")
print(f" 5%ile / 95%ile: {np.percentile(paths,5):,.0f} / {np.percentile(paths,95):,.0f}")
sig = decide_trade(params, paths, capital=capital)
print("\n=== シグナル ===")
print(f"アクション : {sig.action}")
print(f"勝率 : {sig.win_rate:.1%}")
print(f"期待損益 : {sig.expected_pt:+.1f} pt "
f"= {sig.expected_pt * MICRO_MULTIPLIER:+,.0f} 円/枚")
print(f"95% VaR : {sig.var95_pt:.1f} pt "
f"= {sig.var95_pt * MICRO_MULTIPLIER:,.0f} 円/枚")
print(f"発注枚数 : {sig.contracts} 枚 "
f"(必要証拠金 {sig.contracts * MICRO_MARGIN:,} 円)")
if __name__ == "__main__":
main()
|
出力イメージ#
実行するとこんな具合の判定結果が出ます(値は時点・乱数依存)。
現在値 : 38,420
年率ドリフト : 8.21%
年率ボラ : 18.94%
シナリオ平均 : 38,431
5%ile / 95%ile: 37,665 / 39,210
=== シグナル ===
アクション : LONG
勝率 : 57.3%
期待損益 : +11.4 pt = +114 円/枚
95% VaR : 754.6 pt = 7,546 円/枚
発注枚数 : 2 枚 (必要証拠金 40,000 円)
ロジックのポイント#
1. 二段ゲート(勝率 + 期待値)#
シミュレーション結果のうち「利益が出る割合(勝率)」と「期待損益(期待値)」を両方使うのが肝心です。勝率だけで判定すると「ほぼ確実に小さく勝つが、たまに大きく負ける」シナリオを過大評価してしまいます。期待値だけだと、勝率が低すぎてメンタル的に続かない戦略を選んでしまう。
ここでは「勝率 55% かつ期待値 50pt 以上」を発注ゲートにしました。BTC 版も基本的にはこの構造です。
2. ケリー基準で枚数を自動調整#
ケリー基準は「期待値と分散から、長期的に資産成長率を最大化する張り方」を導く公式です。簡易版として f* = (b*p - q) / b(b は損益比、p は勝率、q = 1 - p)を使い、最大 25% でキャップしています。フルケリーは資産曲線のドローダウンと破産確率が極端に大きくなるため、実務では 1/4 〜 1/2 ケリー に留めるのが一般的で、ここでは保守的に 1/4 を採用しました。なお正規のケリーでは b = 平均利益 / 平均損失 ですが、より厳しめに評価するため b = 期待損益 / 95%VaR を使っています。
マイクロ先物は単位が小さいので、ケリー比率に従って 1〜数枚 という細かい刻みで張れるのが強みです。ミニ先物だと最小単位の絶対額が大きすぎて、ケリーの恩恵を受けにくい。
3. GBM の限界#
このスクリプトは GBM 1択ですが、現実の市場には「ジャンプ」が頻発します。日銀の政策修正、地政学イベント、米雇用統計サプライズなどです。
改善案は次のとおりです。
- ヒストリカル ブートストラップ: 過去のリターン分布から復元抽出する。GBM より厚い裾を再現できる。
- Merton ジャンプ拡散モデル: ポアソン過程によるジャンプ項を GBM に追加する。
- GARCH 系: ボラティリティのクラスタリングを捉える。
最初は GBM で実装して、バックテストで「ドローダウンの実測値が VaR より大きい」と分かったら拡張する、という順序がおすすめです。
Claude をどう組み込むか#
ここまでは数値モデルだけの話ですが、ここに Claude を挟むと精度が上がる可能性があります。
具体的には、decide_trade の前に「直近のマクロイベント要約」を Claude に作らせ、mu(年率ドリフト)を補正します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
| from anthropic import Anthropic
client = Anthropic()
def adjust_mu_with_claude(base_mu: float, news_summary: str) -> float:
msg = client.messages.create(
model="claude-opus-4-7",
max_tokens=512,
messages=[{
"role": "user",
"content": (
"以下は直近24時間の日経225関連マクロニュースです。\n"
"翌営業日のドリフト方向に対する影響度を -1.0〜+1.0 で答えてください。\n"
"数値のみ JSON で返してください: {\"impact\": <float>}\n\n"
f"{news_summary}"
)
}]
)
import json, re
m = re.search(r'\{[^}]+\}', msg.content[0].text)
if m is None:
return base_mu # JSON 抽出失敗時は補正しない
try:
impact = float(json.loads(m.group()).get("impact", 0.0))
except (json.JSONDecodeError, ValueError):
return base_mu
impact = max(-1.0, min(1.0, impact)) # クリッピング
# 年率換算で最大 ±5% 補正
return base_mu + impact * 0.05
|
ポイントは「Claude にトレードそのものを判断させない」こと。Claude の出力は「直近ニュースが上下どちらに効いているか」のスカラー値だけにとどめ、最終判定は数値モデルに残します。これで Claude のハルシネーションが直接損益に効くリスクを抑えられます。
なお、Claude のレスポンスを安定させるには、構造化出力(JSON モード相当)を使うか、上記のように正規表現で抽出するのが現実的です。
本番運用時の API 構成#
検証から本番に進むときは、次の置き換えが必要です。
データ取得#
- yfinance → J-Quants API(JPX 公式)
- フリープランは 12 週間遅延データ、ライト以上のプランでリアルタイム化・分足対応
- 上場銘柄一覧、株価四本値、財務情報、信用残などが揃う
- kabu ステーション API(三菱UFJ eスマート証券 / 旧 auカブコム証券)
バックテスト#
まとめ#
- BTC 用の Monte Carlo + Claude 構成は、日経225 マイクロ先物に素直に移植可能
- 個別株より GBM の前提に合うため、統計モデルの精度が出やすい
- マイクロ先物は単位が小さく、ケリー基準の細かい刻みで張れるのが大きな利点
- Claude は「判断者」ではなく「マクロ情報のスカラー化器」として使うと安定
- 本番運用では J-Quants API + kabu ステーション API の組み合わせが現実的
⚠️ 再掲: 本記事は技術解説であり、投資助言には該当しません。シミュレーション結果は将来の損益を保証するものではなく、先物取引は元本超過損のリスクを伴います。実運用は必ずご自身の責任で行ってください。