ドル円FX戦略を「市場レジーム検出」で自動切り替えする【Pythonコード付き】

今年の春、ドル円がひたすらレンジを行ったり来たりしてた時期に、僕のトレンドフォロー戦略は完全に機能停止しました。ブレイクアウトでエントリー→すぐに反転→損切り、を何度繰り返したことか。。。「同じ戦略でもトレンド相場とレンジ相場では結果が全然違う」というのを骨身に染みて感じて、「相場の状態を自動で判定して戦略を切り替えられないか」と調べ始めたのが、今回の市場レジーム検出(Market Regime Filter)です。

市場レジームとは何か

「レジーム(Regime)」とは相場の「状態」のことです。大きく分けると:

トレンド相場:方向性が明確。トレンドフォロー戦略が有効
レンジ相場:一定の価格帯を往復。平均回帰(逆張り)戦略が有効
高ボラティリティ相場:方向性は不明だが振れ幅が大きい。リスク管理最優先

ドル円の場合、FOMC前後のトレンド相場と、指標待ちのレンジ相場が交互に来ることが多く、これを自動で識別できれば戦略の使い分けができます。

ADXを使ったレジーム判定

最もシンプルな方法は ADX(Average Directional Index) を使う方法です。ADXはトレンドの「強さ」を0〜100で表す指標で、方向性は問いません:

・ADX < 20 → レンジ相場(トレンドが弱い)
・ADX 20〜40 → 中程度のトレンド
・ADX > 40 → 強いトレンド

さらに200日移動平均との位置関係を組み合わせることで、「上昇トレンド」「下降トレンド」「レンジ」の3パターンに分類できます。

Pythonで実装する

OANDAのAPIは以前の記事で紹介したので、今回はyfinanceでドル円の日足データを使って試します。

import yfinance as yf
import pandas_ta as ta
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches

# ドル円日足データ取得(2年分)
df = yf.download("JPY=X", period="2y", auto_adjust=True)
df.columns = df.columns.get_level_values(0)
df = df.rename(columns={"Close": "close", "High": "high", "Low": "low", "Open": "open"})

# ADX計算(14日)
adx = ta.adx(df["high"], df["low"], df["close"], length=14)
df["ADX"] = adx["ADX_14"]
df["DMP"] = adx["DMP_14"]   # +DI(上昇方向の強さ)
df["DMN"] = adx["DMN_14"]   # -DI(下降方向の強さ)

# 200日移動平均
df["MA200"] = df["close"].rolling(200).mean()

# レジーム判定ロジック
def classify_regime(row):
    if pd.isna(row["ADX"]) or pd.isna(row["MA200"]):
        return "不明"
    adx_val = row["ADX"]
    if adx_val < 20:
        return "レンジ"
    elif row["close"] > row["MA200"] and row["DMP"] > row["DMN"]:
        return "上昇トレンド"
    elif row["close"] < row["MA200"] and row["DMN"] > row["DMP"]:
        return "下降トレンド"
    else:
        return "混在"

df["regime"] = df.apply(classify_regime, axis=1)

# レジーム別の集計
print("=== レジーム分布 ===")
print(df["regime"].value_counts())
print(f"\nデータ期間: {df.index[0].date()} 〜 {df.index[-1].date()}")

レジーム別にシンプルな戦略を切り替えてバックテストする

レジームを識別できたら、次はそれに応じて戦略を切り替えます。シンプルに「トレンド相場→トレンドフォロー、レンジ→逆張り」を試してみましょう。

# 戦略の実装
# トレンドフォロー: 上昇トレンドで買いポジション、下降トレンドで売りポジション
# レンジ逆張り: RSIが30以下で買い、70以上で売り

# RSIを追加
df["RSI"] = ta.rsi(df["close"], length=14)

# ポジション決定
df["position"] = 0

for i in range(1, len(df)):
    regime = df["regime"].iloc[i]

    if regime == "上昇トレンド":
        df.iloc[i, df.columns.get_loc("position")] = 1  # 買いホールド

    elif regime == "下降トレンド":
        df.iloc[i, df.columns.get_loc("position")] = -1  # 売りホールド

    elif regime == "レンジ":
        rsi = df["RSI"].iloc[i]
        if rsi < 30:
            df.iloc[i, df.columns.get_loc("position")] = 1   # 逆張り買い
        elif rsi > 70:
            df.iloc[i, df.columns.get_loc("position")] = -1  # 逆張り売り
        else:
            df.iloc[i, df.columns.get_loc("position")] = 0   # ノーポジ

# リターン計算(スプレッドは0.03円と想定)
df["returns"] = df["close"].pct_change()
df["strategy_returns"] = df["position"].shift(1) * df["returns"]

# 手数料・スプレッドを差し引く(ポジション変化時にコストが発生)
df["pos_change"] = df["position"].diff().abs()
df["cost"] = df["pos_change"] * 0.03 / df["close"]  # スプレッド分
df["net_returns"] = df["strategy_returns"] - df["cost"]

# パフォーマンス集計
cumulative = (1 + df["net_returns"]).cumprod()
bah = (1 + df["returns"]).cumprod()  # バイ&ホールド

total_return = (cumulative.iloc[-1] - 1) * 100
bah_return = (bah.iloc[-1] - 1) * 100
sharpe = df["net_returns"].mean() / df["net_returns"].std() * np.sqrt(252)

print(f"=== バックテスト結果 ===")
print(f"レジーム切り替え戦略: {total_return:.1f}%")
print(f"バイ&ホールド(ドル円): {bah_return:.1f}%")
print(f"シャープレシオ: {sharpe:.2f}")

# 取引回数
trades = df["pos_change"].sum() / 2
print(f"取引回数: {int(trades)}回")

# プロット
fig, axes = plt.subplots(3, 1, figsize=(14, 10), sharex=True)
fig.patch.set_facecolor("#0d1117")

# レジーム背景色
colors = {"上昇トレンド": "#34d399", "下降トレンド": "#f87171", "レンジ": "#fbbf24", "混在": "#8b949e", "不明": "#374151"}

ax1 = axes[0]
ax1.plot(df.index, df["close"], color="#f0f6fc", linewidth=1)
ax1.set_facecolor("#0d1117")
ax1.set_title("ドル円 + レジーム", color="#f0f6fc")

for regime, color in colors.items():
    mask = df["regime"] == regime
    ax1.fill_between(df.index, df["close"].min(), df["close"].max(),
                     where=mask, alpha=0.15, color=color, label=regime)
ax1.legend(fontsize=8, facecolor="#161b22", labelcolor="white")

ax2 = axes[1]
ax2.plot(df.index, df["ADX"], color="#38bdf8", label="ADX")
ax2.axhline(20, color="#f87171", linestyle="--", alpha=0.5, label="ADX=20")
ax2.set_facecolor("#0d1117")
ax2.set_title("ADX(14)", color="#f0f6fc")
ax2.legend(fontsize=8, facecolor="#161b22", labelcolor="white")

ax3 = axes[2]
ax3.plot(df.index, cumulative, color="#34d399", label="レジーム切替戦略")
ax3.plot(df.index, bah, color="#8b949e", linestyle="--", label="バイ&ホールド")
ax3.set_facecolor("#0d1117")
ax3.set_title("累積リターン", color="#f0f6fc")
ax3.legend(fontsize=8, facecolor="#161b22", labelcolor="white")

plt.tight_layout()
plt.savefig("regime_filter_usdjpy.png", dpi=150, bbox_inches="tight")
plt.show()

実際に試してみた結果と気づき

僕が試した範囲(2年分の日足)だと、レジーム切り替え戦略はシンプルなバイ&ホールドに負けることも多かったです(苦笑)。特にレンジ判定のRSI逆張りが機能しにくいフェーズが結構あって。。。

ただ、「全期間で使う戦略」より「レジームを確認してから手動で戦略を選ぶ」補助ツールとして使うと効果的でした。特にFOMC後に「今はトレンドか?レンジか?」をADXで確認する習慣ができて、無駄なエントリーが減った気がします。

まとめ

市場レジーム検出は「万能の答え」ではないですが、相場の状態を客観的に把握するための強力なフレームワークです。ADX + 移動平均の組み合わせはシンプルながら実用的で、Pythonで簡単に実装できます。

次のステップとしては、HMM(隠れマルコフモデル)を使ったより高度なレジーム検出も試してみたいなと思っています。数学的に難しそうで怖いですが。。。でもhmmlearnというライブラリを使えば意外と簡単らしいので、また記事にします!

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