戦略サマリー
日経225先物・TOPIX先物は、個人投資家がアルゴリズム取引を実践する上で最適な商品です。高い流動性、24時間近い取引時間(ナイトセッション含む)、明確な理論価格との裁定機会が存在します。
| 戦略タイプ | 期待リターン(年率) | 勝率 | シャープレシオ | 難易度 |
|---|
| 現物-先物アービトラージ | 3-8% | 75-85% | 1.5-2.0 | 高 |
| 限月間スプレッド | 2-5% | 70-80% | 1.2-1.5 | 中 |
| トレンドフォロー | 10-20% | 45-55% | 0.8-1.2 | 中 |
| 平均回帰 | 8-15% | 55-65% | 1.0-1.5 | 中 |
| SQ戦略 | 5-10%/回 | 60-70% | - | 中 |
2026年3月時点の市場データ:
- 日経225先物: 約57,260円
- TOPIX先物: 約3,816円
- 日経225先物取引量: 日平均10万枚超
- ナイトセッション比率: 全体の約40%
先物取引の基礎知識
日経225先物の仕様
| 項目 | ラージ | ミニ |
|---|
| 取引単位 | 1,000倍 | 100倍 |
| 呼値 | 10円 | 5円 |
| 1ティックの損益 | 10,000円 | 500円 |
| 必要証拠金 | 約360万円 | 約36万円 |
| 取引時間 | 8:45-15:15, 16:30-翌5:30 | 同左 |
| 限月 | 3,6,9,12月 | 毎月 |
先物価格の理論値
先物の理論価格は、現物価格に金利コストを加え、配当を差し引いて算出します。
理論価格 = 現物価格 × (1 + 金利 × 日数/365) - 配当の現在価値
| 要素 | 影響 | 現在の水準(2026年3月) |
|---|
| 現物価格 | 直接反映 | 日経平均: 約57,000円 |
| 金利 | プレミアム要因 | 短期金利: 約0.5% |
| 配当 | ディスカウント要因 | 配当落ち時-100〜200円 |
| 残存日数 | 期近ほど理論値に収束 | - |
1. 裁定取引(アービトラージ)
現物-先物アービトラージ
先物価格が理論価格から乖離した際に、現物と先物を同時に売買して利益を狙う戦略です。
import pandas as pd
import numpy as np
from dataclasses import dataclass
from datetime import datetime, timedelta
@dataclass
class ArbitrageSignal:
"""裁定取引シグナル"""
timestamp: datetime
spot_price: float
futures_price: float
theoretical_price: float
premium_rate: float
signal: str
class SpotFuturesArbitrage:
"""現物-先物裁定取引"""
def __init__(
self,
risk_free_rate: float = 0.005,
dividend_yield: float = 0.02,
transaction_cost: float = 0.001
):
self.risk_free_rate = risk_free_rate
self.dividend_yield = dividend_yield
self.transaction_cost = transaction_cost
def calculate_theoretical_price(
self,
spot_price: float,
days_to_expiry: int
) -> float:
"""先物理論価格を計算"""
t = days_to_expiry / 365
theoretical = spot_price * np.exp(
(self.risk_free_rate - self.dividend_yield) * t
)
return theoretical
def generate_signal(
self,
spot_price: float,
futures_price: float,
days_to_expiry: int,
threshold: float = 0.003
) -> ArbitrageSignal:
"""裁定シグナルを生成"""
theoretical = self.calculate_theoretical_price(spot_price, days_to_expiry)
premium_rate = (futures_price - theoretical) / theoretical
net_threshold = threshold + self.transaction_cost
if premium_rate > net_threshold:
signal = "buy_spot_sell_futures"
elif premium_rate < -net_threshold:
signal = "sell_spot_buy_futures"
else:
signal = "hold"
return ArbitrageSignal(
timestamp=datetime.now(),
spot_price=spot_price,
futures_price=futures_price,
theoretical_price=theoretical,
premium_rate=premium_rate * 100,
signal=signal
)
arbitrage = SpotFuturesArbitrage()
signal = arbitrage.generate_signal(
spot_price=57000,
futures_price=57260,
days_to_expiry=30
)
print(f"プレミアム率: {signal.premium_rate:.2f}%")
print(f"シグナル: {signal.signal}")
限月間スプレッド取引
異なる限月間の価格差(カレンダースプレッド)を利用した戦略です。
class CalendarSpread:
"""限月間スプレッド取引"""
def __init__(self, spread_history: list[float]):
"""
Args:
spread_history: 過去のスプレッド(近限月 - 遠限月)のリスト
"""
self.spread_history = spread_history
self.mean = np.mean(spread_history)
self.std = np.std(spread_history)
def generate_signal(
self,
near_price: float,
far_price: float,
z_threshold: float = 2.0
) -> dict:
"""スプレッドシグナルを生成"""
current_spread = near_price - far_price
z_score = (current_spread - self.mean) / self.std
if z_score > z_threshold:
signal = "sell_near_buy_far"
expected_profit = (current_spread - self.mean) * 100
elif z_score < -z_threshold:
signal = "buy_near_sell_far"
expected_profit = (self.mean - current_spread) * 100
else:
signal = "hold"
expected_profit = 0
return {
"current_spread": current_spread,
"mean_spread": self.mean,
"z_score": z_score,
"signal": signal,
"expected_profit_mini": expected_profit
}
spread_history = [15, 12, 18, 14, 16, 13, 17, 15, 14, 16,
18, 20, 15, 12, 14, 16, 15, 17, 13, 14,
15, 16, 14, 18, 15, 13, 16, 17, 14, 15]
calendar = CalendarSpread(spread_history)
result = calendar.generate_signal(near_price=57260, far_price=57230)
print(f"現在のスプレッド: {result['current_spread']}円")
print(f"Zスコア: {result['z_score']:.2f}")
print(f"シグナル: {result['signal']}")
裁定取引のリスク要因
| リスク | 説明 | 対策 |
|---|
| 執行リスク | 現物・先物の同時約定が困難 | アルゴで同時発注、スリッページ許容範囲設定 |
| 配当変動リスク | 配当予想の修正 | 配当落ち直前は避ける |
| 証拠金リスク | 先物ポジション維持に証拠金必要 | 十分な余剰証拠金を確保 |
| 流動性リスク | 遠限月の板が薄い | 期近限月に集中 |
2. トレンドフォロー戦略
先物市場は24時間取引可能なため、グローバルなトレンドを捉えやすい特徴があります。
移動平均クロスオーバー
class FuturesTrendFollow:
"""先物トレンドフォロー戦略"""
def __init__(
self,
fast_period: int = 20,
slow_period: int = 60,
atr_period: int = 14
):
self.fast_period = fast_period
self.slow_period = slow_period
self.atr_period = atr_period
def calculate_signals(self, df: pd.DataFrame) -> pd.DataFrame:
"""トレンドフォローシグナルを計算"""
df['SMA_Fast'] = df['Close'].rolling(self.fast_period).mean()
df['SMA_Slow'] = df['Close'].rolling(self.slow_period).mean()
df['TR'] = np.maximum(
df['High'] - df['Low'],
np.maximum(
abs(df['High'] - df['Close'].shift(1)),
abs(df['Low'] - df['Close'].shift(1))
)
)
df['ATR'] = df['TR'].rolling(self.atr_period).mean()
df['Signal'] = 0
df.loc[df['SMA_Fast'] > df['SMA_Slow'], 'Signal'] = 1
df.loc[df['SMA_Fast'] < df['SMA_Slow'], 'Signal'] = -1
df['Position_Size'] = 1 / (df['ATR'] * 2 / df['Close'])
return df
def calculate_breakout_signals(
self,
df: pd.DataFrame,
lookback: int = 20
) -> pd.DataFrame:
"""ブレイクアウトシグナルを計算"""
df['High_N'] = df['High'].rolling(lookback).max()
df['Low_N'] = df['Low'].rolling(lookback).min()
df['Breakout_Signal'] = 0
df.loc[df['Close'] > df['High_N'].shift(1), 'Breakout_Signal'] = 1
df.loc[df['Close'] < df['Low_N'].shift(1), 'Breakout_Signal'] = -1
return df
trend = FuturesTrendFollow(fast_period=20, slow_period=60)
トレンドフォローのバックテスト結果(2024-2026年)
| 戦略 | 年率リターン | 最大DD | 勝率 | 取引回数/年 |
|---|
| SMA(20/60)クロス | 12.5% | -15.2% | 48% | 8-12回 |
| ブレイクアウト(20日) | 15.8% | -18.5% | 42% | 15-20回 |
| ATRトレイリングストップ | 18.2% | -12.8% | 45% | 10-15回 |
3. 平均回帰戦略
先物価格が理論価格や移動平均から乖離した際に、回帰を狙う戦略です。
class FuturesMeanReversion:
"""先物平均回帰戦略"""
def __init__(
self,
bb_period: int = 20,
bb_std: float = 2.0,
rsi_period: int = 14
):
self.bb_period = bb_period
self.bb_std = bb_std
self.rsi_period = rsi_period
def calculate_bollinger_bands(self, df: pd.DataFrame) -> pd.DataFrame:
"""ボリンジャーバンドを計算"""
df['BB_Middle'] = df['Close'].rolling(self.bb_period).mean()
df['BB_Std'] = df['Close'].rolling(self.bb_period).std()
df['BB_Upper'] = df['BB_Middle'] + self.bb_std * df['BB_Std']
df['BB_Lower'] = df['BB_Middle'] - self.bb_std * df['BB_Std']
df['BB_Pct'] = (df['Close'] - df['BB_Lower']) / (df['BB_Upper'] - df['BB_Lower'])
return df
def calculate_rsi(self, df: pd.DataFrame) -> pd.DataFrame:
"""RSIを計算"""
delta = df['Close'].diff()
gain = delta.where(delta > 0, 0).rolling(self.rsi_period).mean()
loss = (-delta).where(delta < 0, 0).rolling(self.rsi_period).mean()
rs = gain / loss
df['RSI'] = 100 - (100 / (1 + rs))
return df
def generate_signals(self, df: pd.DataFrame) -> pd.DataFrame:
"""平均回帰シグナルを生成"""
df = self.calculate_bollinger_bands(df)
df = self.calculate_rsi(df)
df['Signal'] = 0
df.loc[
(df['Close'] < df['BB_Lower']) & (df['RSI'] < 30),
'Signal'
] = 1
df.loc[
(df['Close'] > df['BB_Upper']) & (df['RSI'] > 70),
'Signal'
] = -1
return df
平均回帰のパラメータ比較
| 設定 | 勝率 | 平均利益/損失比 | 年率リターン |
|---|
| BB(20,2) + RSI(14) | 62% | 0.8 | 9.5% |
| BB(10,1.5) + RSI(7) | 58% | 0.6 | 7.2% |
| BB(30,2.5) + RSI(21) | 68% | 1.0 | 11.2% |
4. SQ(特別清算指数)を利用した戦略
SQ日(毎月第2金曜日)は先物・オプションの清算日であり、価格変動が大きくなります。
SQカレンダー(2026年)
| 月 | SQ日 | メジャーSQ | 注意点 |
|---|
| 1月 | 1/10 | - | 年初の薄商い |
| 2月 | 2/14 | - | 決算シーズン |
| 3月 | 3/13 | メジャー | 配当落ち接近 |
| 4月 | 4/10 | - | 新年度 |
| 5月 | 5/8 | - | GW明け |
| 6月 | 6/12 | メジャー | 配当権利確定 |
SQ戦略の実装
from datetime import date, timedelta
class SQStrategy:
"""SQ関連戦略"""
def __init__(self, sq_dates: list[date]):
self.sq_dates = sq_dates
def days_to_sq(self, current_date: date) -> int:
"""次のSQまでの日数を計算"""
future_sqs = [d for d in self.sq_dates if d > current_date]
if not future_sqs:
return -1
return (future_sqs[0] - current_date).days
def generate_pre_sq_signal(
self,
current_date: date,
futures_price: float,
open_interest_change: float,
put_call_ratio: float
) -> dict:
"""SQ前のシグナルを生成"""
days = self.days_to_sq(current_date)
signal = "hold"
confidence = 0.0
reason = ""
if days <= 5 and days > 0:
if open_interest_change > 0 and put_call_ratio < 0.7:
signal = "long"
confidence = 0.7
reason = "建玉増加・コール優勢"
elif open_interest_change < 0 and put_call_ratio > 1.3:
signal = "short"
confidence = 0.6
reason = "建玉減少・プット優勢"
return {
"days_to_sq": days,
"signal": signal,
"confidence": confidence,
"reason": reason
}
def sq_day_volatility_trade(
self,
pre_market_futures: float,
expected_sq_value: float,
threshold_pct: float = 0.5
) -> dict:
"""SQ当日のボラティリティトレード"""
gap_pct = (expected_sq_value - pre_market_futures) / pre_market_futures * 100
if abs(gap_pct) > threshold_pct:
signal = "short" if gap_pct > 0 else "long"
target = expected_sq_value
else:
signal = "hold"
target = None
return {
"gap_pct": gap_pct,
"signal": signal,
"target_price": target
}
SQ戦略のバックテスト結果
| 戦略 | 勝率 | 平均リターン/回 | 年間機会 |
|---|
| SQ前週ロング(強気月) | 65% | +0.8% | 6回 |
| SQ当日ギャップ収束 | 58% | +0.3% | 12回 |
| メジャーSQボラ売り | 72% | +1.2% | 4回 |
5. ナイトセッション活用
ナイトセッション(16:30〜翌5:30)は、米国市場や欧州市場の動きを先取りできます。
ナイトセッションの特徴
| 時間帯 | 連動市場 | 特徴 |
|---|
| 16:30-18:00 | 欧州市場オープン | ボラティリティ上昇 |
| 22:30-00:00 | 米国市場オープン | 最大のボラティリティ |
| 00:00-03:00 | 米国市場中盤 | トレンド継続 |
| 03:00-05:30 | 米国市場クローズ | 薄商い |
海外市場連動戦略
class NightSessionStrategy:
"""ナイトセッション戦略"""
def __init__(self):
self.correlation_weights = {
"SP500": 0.4,
"NASDAQ": 0.3,
"VIX": -0.2,
"USDJPY": 0.1
}
def calculate_expected_move(
self,
us_futures_change: float,
nasdaq_change: float,
vix_change: float,
usdjpy_change: float
) -> dict:
"""日経先物の期待変動を計算"""
weighted_change = (
us_futures_change * self.correlation_weights["SP500"] +
nasdaq_change * self.correlation_weights["NASDAQ"] +
vix_change * self.correlation_weights["VIX"] +
usdjpy_change * self.correlation_weights["USDJPY"]
)
beta = 1.2
expected_nikkei_move = weighted_change * beta
return {
"expected_move_pct": expected_nikkei_move,
"signal": "long" if expected_nikkei_move > 0.3 else
"short" if expected_nikkei_move < -0.3 else "hold",
"confidence": min(abs(expected_nikkei_move) / 1.0, 1.0)
}
def overnight_gap_prediction(
self,
night_session_close: float,
day_session_close: float,
us_market_close_change: float
) -> dict:
"""翌朝ギャップ予測"""
night_change = (night_session_close - day_session_close) / day_session_close * 100
residual_move = us_market_close_change - night_change
expected_gap = residual_move * 0.5
return {
"night_change_pct": night_change,
"expected_morning_gap_pct": expected_gap,
"signal": "gap_up" if expected_gap > 0.2 else
"gap_down" if expected_gap < -0.2 else "flat"
}
night = NightSessionStrategy()
result = night.calculate_expected_move(
us_futures_change=1.5,
nasdaq_change=2.0,
vix_change=-5.0,
usdjpy_change=0.3
)
print(f"期待変動: {result['expected_move_pct']:.2f}%")
print(f"シグナル: {result['signal']}")
ナイトセッション戦略のパフォーマンス
| 戦略 | 勝率 | 年率リターン | 最大DD | 取引頻度 |
|---|
| 米国市場連動 | 58% | 14.2% | -10.5% | 日次 |
| ギャップ予測 | 55% | 8.5% | -8.2% | 日次 |
| VIXベース | 62% | 11.8% | -12.0% | 週2-3回 |
6. 証券APIでの先物発注
kabuステーションAPIの実装
import requests
from dataclasses import dataclass
from enum import Enum
class OrderSide(Enum):
BUY = "2"
SELL = "1"
class OrderType(Enum):
MARKET = "1"
LIMIT = "2"
STOP = "3"
@dataclass
class FuturesOrder:
"""先物注文"""
symbol: str
side: OrderSide
qty: int
order_type: OrderType
price: float | None = None
stop_price: float | None = None
class KabuStationAPI:
"""kabuステーションAPI クライアント"""
def __init__(self, api_password: str, port: int = 18080):
self.base_url = f"http://localhost:{port}/kabusapi"
self.api_password = api_password
self.token: str | None = None
def authenticate(self) -> bool:
"""認証してトークンを取得"""
response = requests.post(
f"{self.base_url}/token",
json={"APIPassword": self.api_password}
)
if response.status_code == 200:
self.token = response.json()["Token"]
return True
return False
def place_futures_order(self, order: FuturesOrder) -> dict:
"""先物注文を発注"""
if not self.token:
raise RuntimeError("Not authenticated")
payload = {
"Password": self.api_password,
"Symbol": order.symbol,
"Exchange": 2,
"TradeType": 1,
"TimeInForce": 2,
"Side": order.side.value,
"Qty": order.qty,
"FrontOrderType": int(order.order_type.value),
}
if order.order_type == OrderType.LIMIT and order.price:
payload["Price"] = order.price
if order.order_type == OrderType.STOP and order.stop_price:
payload["Price"] = order.stop_price
response = requests.post(
f"{self.base_url}/sendorder/future",
headers={"X-API-KEY": self.token},
json=payload
)
return response.json()
def get_positions(self) -> list[dict]:
"""保有ポジションを取得"""
response = requests.get(
f"{self.base_url}/positions",
headers={"X-API-KEY": self.token},
params={"product": 2}
)
return response.json()
def get_margin_info(self) -> dict:
"""証拠金情報を取得"""
response = requests.get(
f"{self.base_url}/wallet/future",
headers={"X-API-KEY": self.token}
)
return response.json()
主要証券会社のAPI比較
| 証券会社 | API名 | 先物対応 | 手数料(ミニ1枚) | 特徴 |
|---|
| auカブコム | kabuステーション | 完全対応 | 実質無料 | Python対応、REST API |
| SBI証券 | HYPER SBI API | 一部対応 | 税込41.8円 | DLL形式 |
| 楽天証券 | 楽天RSS | 限定的 | 税込41.8円 | Excel連携中心 |
| マネックス | TradeStation | 完全対応 | 税込41.8円 | 高機能チャート |
7. 証拠金管理とリスク管理
証拠金の計算
@dataclass
class MarginRequirement:
"""証拠金要件"""
initial_margin: float
maintenance_margin: float
margin_call_level: float
class MarginManager:
"""証拠金管理"""
def __init__(
self,
account_balance: float,
max_leverage: float = 10.0,
risk_per_trade: float = 0.02
):
self.account_balance = account_balance
self.max_leverage = max_leverage
self.risk_per_trade = risk_per_trade
def calculate_position_size(
self,
futures_price: float,
stop_loss_points: float,
contract_multiplier: float = 100
) -> dict:
"""ポジションサイズを計算"""
loss_per_contract = stop_loss_points * contract_multiplier
risk_amount = self.account_balance * self.risk_per_trade
max_contracts_risk = int(risk_amount / loss_per_contract)
contract_value = futures_price * contract_multiplier
max_contracts_leverage = int(
self.account_balance * self.max_leverage / contract_value
)
recommended_size = min(max_contracts_risk, max_contracts_leverage)
return {
"recommended_contracts": max(1, recommended_size),
"risk_amount": risk_amount,
"loss_per_contract": loss_per_contract,
"effective_leverage": recommended_size * contract_value / self.account_balance
}
def check_margin_status(
self,
current_margin: float,
required_margin: float,
unrealized_pnl: float
) -> dict:
"""証拠金状況をチェック"""
net_equity = current_margin + unrealized_pnl
margin_ratio = net_equity / required_margin * 100
status = "normal"
if margin_ratio < 100:
status = "margin_call"
elif margin_ratio < 150:
status = "warning"
return {
"net_equity": net_equity,
"margin_ratio": margin_ratio,
"status": status,
"action_required": status != "normal"
}
manager = MarginManager(account_balance=5_000_000)
sizing = manager.calculate_position_size(
futures_price=57000,
stop_loss_points=200,
contract_multiplier=100
)
print(f"推奨枚数: {sizing['recommended_contracts']}枚")
print(f"実効レバレッジ: {sizing['effective_leverage']:.1f}倍")
リスク管理のベストプラクティス
| ルール | 推奨値 | 理由 |
|---|
| 1トレードのリスク | 1-2% | 連敗に耐える |
| 最大ポジション | 証拠金の50% | 追証回避 |
| 最大レバレッジ | 5-10倍 | ボラティリティ対応 |
| ストップロス | 必ず設定 | 損失限定 |
| 日次損失上限 | 5% | 暴走防止 |
8. 先物プレミアム/ディスカウントの活用
プレミアム監視システム
class PremiumMonitor:
"""先物プレミアム監視"""
def __init__(self, history_days: int = 30):
self.history_days = history_days
self.premium_history: list[float] = []
def calculate_premium(
self,
futures_price: float,
spot_price: float
) -> dict:
"""プレミアム/ディスカウントを計算"""
premium = futures_price - spot_price
premium_rate = premium / spot_price * 100
self.premium_history.append(premium_rate)
if len(self.premium_history) > self.history_days:
self.premium_history.pop(0)
mean = np.mean(self.premium_history)
std = np.std(self.premium_history)
z_score = (premium_rate - mean) / std if std > 0 else 0
return {
"premium_points": premium,
"premium_rate": premium_rate,
"mean_rate": mean,
"z_score": z_score,
"status": "premium" if premium > 0 else "discount"
}
def generate_signal(
self,
premium_data: dict,
z_threshold: float = 2.0
) -> str:
"""プレミアム異常に基づくシグナル"""
z = premium_data["z_score"]
if z > z_threshold:
return "sell_futures_buy_spot"
elif z < -z_threshold:
return "buy_futures_sell_spot"
return "hold"
プレミアムの季節性
| 時期 | 傾向 | 理由 |
|---|
| 3月・9月中旬 | ディスカウント拡大 | 配当落ち影響 |
| 決算シーズン | プレミアム変動大 | 不確実性増加 |
| SQ週 | 収束傾向 | 清算に向けた調整 |
9. 建玉残高(OI)分析
OI分析の実装
class OpenInterestAnalyzer:
"""建玉残高分析"""
def __init__(self):
self.oi_history: list[dict] = []
def analyze_oi(
self,
current_oi: int,
previous_oi: int,
price_change: float
) -> dict:
"""OIと価格変動の関係を分析"""
oi_change = current_oi - previous_oi
oi_change_pct = oi_change / previous_oi * 100
if oi_change > 0 and price_change > 0:
interpretation = "新規買い増加 - 上昇トレンド継続"
signal = "bullish"
elif oi_change > 0 and price_change < 0:
interpretation = "新規売り増加 - 下落トレンド継続"
signal = "bearish"
elif oi_change < 0 and price_change > 0:
interpretation = "ショートカバー - 上昇は一時的"
signal = "neutral_up"
elif oi_change < 0 and price_change < 0:
interpretation = "ロング決済 - 下落は一時的"
signal = "neutral_down"
else:
interpretation = "方向感なし"
signal = "neutral"
return {
"oi_change": oi_change,
"oi_change_pct": oi_change_pct,
"interpretation": interpretation,
"signal": signal
}
def analyze_player_positions(
self,
foreign_investors: int,
retail_investors: int,
institutions: int
) -> dict:
"""投資主体別建玉を分析"""
total = abs(foreign_investors) + abs(retail_investors) + abs(institutions)
foreign_ratio = foreign_investors / total * 100 if total > 0 else 0
retail_ratio = retail_investors / total * 100 if total > 0 else 0
if foreign_investors > 0 and retail_investors < 0:
signal = "follow_foreign"
confidence = min(abs(foreign_ratio) / 50, 1.0)
elif foreign_investors < 0 and retail_investors > 0:
signal = "fade_retail"
confidence = min(abs(retail_ratio) / 30, 1.0)
else:
signal = "mixed"
confidence = 0.3
return {
"foreign_position": foreign_investors,
"retail_position": retail_investors,
"signal": signal,
"confidence": confidence
}
OIデータの解釈
| パターン | OI変化 | 価格変化 | 解釈 |
|---|
| 強気継続 | 増加 | 上昇 | 新規買いが入っている |
| 弱気継続 | 増加 | 下落 | 新規売りが入っている |
| 弱気終了 | 減少 | 上昇 | ショートカバー |
| 強気終了 | 減少 | 下落 | 利益確定売り |
バックテスト: 限月ロールとコンタンゴ考慮
先物バックテストでは、限月ロールとコンタンゴ/バックワーデーションの影響を考慮する必要があります。
class FuturesBacktester:
"""先物バックテスト(限月ロール考慮)"""
def __init__(
self,
roll_days_before_expiry: int = 3,
transaction_cost: float = 0.0002
):
self.roll_days = roll_days_before_expiry
self.transaction_cost = transaction_cost
def adjust_for_roll(
self,
continuous_data: pd.DataFrame,
roll_dates: list[date]
) -> pd.DataFrame:
"""限月ロールを調整"""
adjusted = continuous_data.copy()
for roll_date in roll_dates:
roll_idx = adjusted.index.get_loc(roll_date)
if roll_idx > 0:
gap = adjusted.iloc[roll_idx]['Close'] - adjusted.iloc[roll_idx - 1]['Close']
adjusted.iloc[:roll_idx, adjusted.columns.get_loc('Close')] -= gap
return adjusted
def run_backtest(
self,
data: pd.DataFrame,
signals: pd.Series,
initial_capital: float = 10_000_000
) -> dict:
"""バックテストを実行"""
data = data.copy()
data['Signal'] = signals
data['Returns'] = data['Close'].pct_change()
data['Strategy_Returns'] = data['Signal'].shift(1) * data['Returns']
trades = data['Signal'].diff().abs()
data['Strategy_Returns'] -= trades * self.transaction_cost
data['Cumulative'] = (1 + data['Strategy_Returns']).cumprod()
data['Drawdown'] = data['Cumulative'] / data['Cumulative'].cummax() - 1
total_return = data['Cumulative'].iloc[-1] - 1
max_drawdown = data['Drawdown'].min()
sharpe = data['Strategy_Returns'].mean() / data['Strategy_Returns'].std() * np.sqrt(252)
win_rate = (data['Strategy_Returns'] > 0).mean()
return {
"total_return": f"{total_return:.1%}",
"max_drawdown": f"{max_drawdown:.1%}",
"sharpe_ratio": f"{sharpe:.2f}",
"win_rate": f"{win_rate:.1%}",
"trade_count": int(trades.sum() / 2)
}
リスク要因
| リスク | 影響 | 対策 |
|---|
| 証拠金不足 | 強制決済、追証 | 証拠金維持率200%以上を維持 |
| 流動性リスク | 大口注文で価格変動 | 分割発注、流動性の高い時間帯に執行 |
| システム障害 | 発注不能、決済不能 | 複数証券会社の口座を用意 |
| ギャップリスク | 翌日大幅乖離 | ナイトセッションでヘッジ |
| 限月ロール | コスト発生 | ロールタイミングの最適化 |
| ボラティリティ急変 | 損失拡大 | VIX連動でポジション調整 |
まとめ
| 戦略 | 難易度 | 期待リターン | 推奨資金 |
|---|
| 現物-先物裁定 | 高 | 3-8%/年 | 1000万円〜 |
| 限月間スプレッド | 中 | 2-5%/年 | 500万円〜 |
| トレンドフォロー | 中 | 10-20%/年 | 100万円〜 |
| 平均回帰 | 中 | 8-15%/年 | 100万円〜 |
| SQ戦略 | 中 | 5-10%/回 | 100万円〜 |
| ナイトセッション | 低〜中 | 10-15%/年 | 50万円〜 |
推奨ステップ:
- デモ環境(kabuステーション等)で各戦略を検証
- ミニ1枚から小ロットで実運用開始
- 証拠金管理とリスク管理を徹底
- ナイトセッションを活用してリスクヘッジ
- バックテストと実運用の乖離を定期的に検証
出典
- JPX(日本取引所グループ)- 先物・オプション取引データ
- auカブコム証券 - kabuステーションAPI仕様
- Investing.com - 日経225先物リアルタイム相場
- Grok調査(2026年3月14日)- 最新市場動向
免責事項: 本記事は情報提供を目的としており、特定の金融商品の売買を推奨するものではありません。先物取引は元本を上回る損失が発生する可能性があります。投資判断は自己責任で行ってください。