Python×共和分でトヨタ×ホンダのペアトレードをバックテスト【製造業株の統計的裁定】

Python実装・コード

製造業銘柄を見ていると「トヨタが上がるとホンダも追いかけてくる」感覚がずっとあった。感覚だけで売買するのはギャンブルだけど、これが統計的に証明できるなら話は変わる。「共和分」という概念を知ったのは子供の寝かしつけ後の深夜勉強中で、正直最初は「なんかすごそうだけど難しそう」しか思わなかった。でも実装してみたら意外とシンプルだったので、同じように最初で詰まっている人に向けてまとめる。

ペアトレードの基本的な考え方

ペアトレードとは、相関が高い2銘柄の価格差(スプレッド)が一時的に乖離したとき「割高な方を売り、割安な方を買う」戦略だ。スプレッドが平均に戻ることで利益を得る。重要なのは「相関が高い」だけでなく「共和分(cointegration)関係にある」こと——長期的に一定の均衡関係を保ちながら動くペアでないと、スプレッドが戻ってこない。

共和分検定とは

Engle-Granger法またはJohansen法で「2つの時系列が長期均衡関係にあるか」を統計的に検定する。p値が0.05未満なら共和分ありと判断する。Pythonでは statsmodelscoint 関数一発で確認できる。

環境準備

pip install yfinance statsmodels pandas numpy matplotlib

データ取得と共和分検定

import yfinance as yf
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from statsmodels.tsa.stattools import coint
from statsmodels.regression.linear_model import OLS
from statsmodels.tools import add_constant

# トヨタ(7203) と ホンダ(7267) の日足終値を取得(2年分)
tickers = ["7203.T", "7267.T"]
data = yf.download(tickers, period="2y", auto_adjust=True)["Close"]
data.columns = ["TOYOTA", "HONDA"]
data = data.dropna()

print(data.tail())

# Engle-Granger共和分検定
score, pvalue, _ = coint(data["TOYOTA"], data["HONDA"])
print(f"\n共和分検定 p値: {pvalue:.4f}")
if pvalue < 0.05:
    print("→ 共和分あり(ペアトレード対象として有望)")
else:
    print("→ 共和分なし(この期間では有効でない可能性)")

p値が0.05未満なら「共和分あり」——つまりスプレッドが長期的に平均回帰する性質があると判断できる。実際に試したところ、直近2年のデータでは有意な共和分関係が確認できた。

スプレッドの計算とZスコア化

# OLS回帰でヘッジ比率を求める
X = add_constant(data["HONDA"])
result = OLS(data["TOYOTA"], X).fit()
hedge_ratio = result.params["HONDA"]
print(f"ヘッジ比率: {hedge_ratio:.4f}")

# スプレッド = TOYOTA - hedge_ratio × HONDA
data["spread"] = data["TOYOTA"] - hedge_ratio * data["HONDA"]

# Zスコア化(過去30日の移動平均・標準偏差を使う)
window = 30
data["spread_mean"] = data["spread"].rolling(window).mean()
data["spread_std"]  = data["spread"].rolling(window).std()
data["z_score"] = (data["spread"] - data["spread_mean"]) / data["spread_std"]
data = data.dropna()

# Zスコアの可視化
plt.figure(figsize=(12, 4))
plt.plot(data.index, data["z_score"], color="#34d399", linewidth=1)
plt.axhline(2.0,  color="red",  linestyle="--", label="+2σ(売りシグナル)")
plt.axhline(-2.0, color="blue", linestyle="--", label="-2σ(買いシグナル)")
plt.axhline(0.0,  color="gray", linestyle="-",  alpha=0.5)
plt.title("トヨタ/ホンダ スプレッドZスコア")
plt.legend()
plt.tight_layout()
plt.savefig("spread_zscore.png", dpi=150)
plt.show()

バックテスト:Zスコアでエントリー・イグジット

ルールはシンプル。Zスコアが+2を超えたら「トヨタ売り・ホンダ買い」、-2を下回ったら「トヨタ買い・ホンダ売り」、0に戻ったらクローズ。

# シグナル生成
ENTRY_Z  = 2.0   # エントリー閾値
EXIT_Z   = 0.0   # イグジット閾値(Zスコアが0に戻ったとき)

data["signal"] = 0
data.loc[data["z_score"] >  ENTRY_Z, "signal"] = -1  # スプレッド割高→ショート
data.loc[data["z_score"] < -ENTRY_Z, "signal"] =  1  # スプレッド割安→ロング

# ポジションの保持(シグナルがない間は前日を引き継ぐ、ただしゼロ交差でクローズ)
position = []
pos = 0
for i, row in data.iterrows():
    if row["signal"] != 0:
        pos = row["signal"]
    elif abs(row["z_score"]) < abs(EXIT_Z):
        pos = 0
    position.append(pos)
data["position"] = position

# スプレッドの1日リターン(スプレッドの変化 × ポジション)
data["spread_ret"] = data["spread"].diff()
data["strategy_ret"] = data["position"].shift(1) * data["spread_ret"]

# 累積収益(スプレッドベース、参考値)
data["cum_ret"] = data["strategy_ret"].cumsum()

plt.figure(figsize=(12, 4))
plt.plot(data.index, data["cum_ret"], color="#34d399", linewidth=1.5)
plt.title("ペアトレード 累積損益(スプレッドベース)")
plt.ylabel("累積損益(円)")
plt.xlabel("日付")
plt.tight_layout()
plt.savefig("pairs_cumret.png", dpi=150)
plt.show()

# 統計サマリ
total = data["strategy_ret"].sum()
sharpe = data["strategy_ret"].mean() / data["strategy_ret"].std() * np.sqrt(252)
print(f"\n合計損益: {total:.1f}円(スプレッドベース)")
print(f"シャープレシオ: {sharpe:.2f}")

実用上の注意点

このバックテストは「スプレッドベース」の参考値であり、実際の損益計算には株数・ヘッジ比率の端数処理・手数料・信用取引のコストが加わる。また、ヘッジ比率は時間とともに変化するため(パラメーター不安定性)、定期的に再推定が必要だ。より高度にやるならカルマンフィルターで動的にヘッジ比率を更新する手法もある。

まとめ

「なんとなく連動してる」という感覚を共和分検定で数字に落とし、スプレッドのZスコアでトレードルールを作る——このフローを一通り実装できた。製造業株はセクターが同じで連動しやすいので、ペアトレードの候補を探しやすいと感じた。次はデンソーvsアイシンとか、素材系のクボタvs三菱マテリアルとかで試してみたい。

タイトルとURLをコピーしました