📝

Draft article

This draft is visible to admins and superusers only. Sign in with an authorized account.

← 記事一覧に戻る
August 21, 2026
読了時間: 5分

トレーディング戦略を評価するための合成コントロール法

トレーディング戦略を評価するための合成コントロール法
#causal-inference
#synthetic-control
#counterfactual
#evaluation
#econometrics

このシリーズでは、偽のエッジに至る一つの経路、選択を長く扱ってきた。デフレート・シャープレシオ は探索の勝者を評価し、バックテスト過学習確率 は探索そのものを評価する。正直なネガティブ結果 は、実際に取引したかった戦略へその両方を適用したときに起きることだ。この仕組みがうまく答えるのは一つの問いである:何度も調べたからこれを見つけたのか?

しかし、まったく別の経路である 交絡 には触れていない。一つだけ変えた——新しい執行アルゴリズムを導入した、パラメータを切り替えた、レジームに反応した——ところ、パフォーマンスが改善した。探索はしていない。多重検定の問題もない。それでも改善はあなたとは無関係かもしれない。リリースした週にボラティリティが倍になったからだ。デフレーションではこれを捉えられない。デフレーションが補正するのは試行回数であり、実行した試行は一回だけだからだ。

Abadie and Gardeazabal(2003)が開発し、Abadie、Diamond、Hainmueller(2010、2015)が改良した合成コントロール法(SCM)は、交絡を直接攻撃する。これは 反実仮想、つまり 手を加えなかった 銘柄の加重組み合わせを構成する。介入前に処置対象の銘柄をよく追跡するように選ぶのである。介入後に両者が分岐した差が効果の推定値になる。重要なのは、信頼できる反実仮想を作れない場合を告げる反証基準と、漸近分布の仮定を信じる必要がない置換ベースの p 値が SCM に備わっていることだ。

明白な代替手法が失敗する理由

反実仮想との比較経路

介入前後の比較では、その日に変わった他のあらゆるものと介入が交絡する。単一のベンチマーク銘柄との比較も、あなたの銘柄を追跡する単一資産はないため失敗する。これは別の方向から見た 単一銘柄の罠 だ。一つの銘柄固有の構造がエッジを偽装できるなら、一つの銘柄固有の構造は コントロール も偽装できる。SCM のドナープールはまさにこの異議への形式的な答えである。比較対象を一つ選ぶ代わりに、介入前データから重み付きバスケットを選ばせる。

差分の差分法については、単なる一文以上の説明に値する。これはニアミスだからだ。DiD は処置単位の変化をコントロール群の変化と比較し、平行トレンドの下でのみ効果を識別する。介入がなければ処置群とコントロール群が一緒に動いていた、という仮定である。金融市場ではこの仮定は擁護しにくい。資産ごとにベータ、ボラティリティレジーム、流動性層が異なり、相対経路もさまようからだ。SCM はこの仮定をフィットした対象に置き換える。平行に動くコントロールを 仮定 するのではなく、一つを 構成 して、実際にどれだけ追跡したかを示す。追跡が悪ければ、停止するよう告げてくれる。

手法

合成コントロールのドナー構成

設定と記法

TT 期間にわたり観測された J+1J + 1 個の単位(資産、戦略、ポートフォリオ)を考える。単位 1 は 処置単位、つまり介入を行った資産または戦略である。単位 2,,J+12, \ldots, J+1 は比較可能な未処置単位の ドナープール を構成する。

時刻 tt に介入を受けた処置単位の結果を Y1tIY_{1t}^I、介入がなかった場合の結果を Y1tNY_{1t}^N とする。t>T0t > T_0 における 処置効果 は次の通り。

τ1t=Y1tIY1tN\tau_{1t} = Y_{1t}^I - Y_{1t}^N

Y1tIY_{1t}^I は直接観測できる。問題は Y1tNY_{1t}^N を推定することに尽きる。

合成コントロールの構成

SCM はドナーの結果の加重平均として Y1tNY_{1t}^N を推定する。

Y^1tN=j=2J+1wjYjt\hat{Y}_{1t}^N = \sum_{j=2}^{J+1} w_j \, Y_{jt}

ここで重みベクトル w=(w2,,wJ+1)\mathbf{w} = (w_2, \ldots, w_{J+1})' は、すべての jj について wj0w_j \geq 0、かつ jwj=1\sum_j w_j = 1 を満たす。

この二つの制約は実質的な役割を果たす。合成コントロールをドナーの 凸結合 に限定し、データの支持範囲外への外挿を防ぐのだ。制約のない回帰では、あるコインを 3.4 倍ロングし別のコインを 2.4 倍ショートするような反実仮想を返せる——介入前のフィットは数値上は素晴らしくても、誰も保有できないポートフォリオに対応する反実仮想である。凸性制約により反実仮想は解釈可能になり、重みも疎になる。通常、非ゼロの重みを持つドナーは少数だけで、その中身を読み取れる。

最適な重みの選択

介入前期間における処置単位と合成コントロールの距離が最小になるように重みを選ぶ。

minwX1X0wV=minw(X1X0w)V(X1X0w)\min_{\mathbf{w}} \| \mathbf{X}_1 - \mathbf{X}_0 \mathbf{w} \|_V = \min_{\mathbf{w}} (\mathbf{X}_1 - \mathbf{X}_0 \mathbf{w})' \mathbf{V} (\mathbf{X}_1 - \mathbf{X}_0 \mathbf{w})

ここで X1\mathbf{X}_1 は処置単位の介入前特性を並べた (k×1)(k \times 1) ベクトル、X0\mathbf{X}_0 はドナープールの同じ特性を並べた (k×J)(k \times J) 行列、V\mathbf{V} は各予測変数の重要度を割り当てる (k×k)(k \times k) の半正定値対角行列である。

V\mathbf{V} は交差検証で選ぶ。結果として得られる w(V)\mathbf{w}^*(\mathbf{V}) が、介入前期間の結果変数に対する平均二乗予測誤差(MSPE)を最小化するような V\mathbf{V} を選ぶのだ。これは入れ子の最適化で、外側ループが V\mathbf{V} を、内側の制約付き求解が w\mathbf{w} を最適化する。

トレーディングへの適用で使う予測変数

Abadie らが一人当たり GDP や産業構成を使ったのに対し、トレーディングでは次を使う。

  • リターン特性:介入前期間の平均リターン、ボラティリティ、歪度、尖度
  • 流動性指標:平均スプレッド、日次出来高、Amihud 非流動性比率
  • マイクロストラクチャ特徴:オーダーフロー不均衡、約定到着率、気配・約定比率
  • ファクターエクスポージャー:市場、モメンタム、バリュー、ボラティリティ各ファクターへのベータ
  • ラグ付き結果:介入前の複数時点における累積 PnL 値

ラグ付き結果が最も重要だ。Abadie et al.(2010)は、合成コントロールが長い介入前期間にわたり処置単位のラグ付き結果と一致すれば、線形因子モデルの下で観測されない交絡因子を暗黙に制御できることを示した——この手法が手間をかける価値がある理由そのものだ。PnL を動かした交絡因子の名前を特定する必要はない。その因子にもさらされていたドナーを見つける必要がある。

これが適用できる場面

市場レジームをまたぐ因果の橋

シナリオ 1:新しい執行アルゴリズムの評価

一つの銘柄に新しい TWAP スケジュールを導入し、実行不足への影響を知りたいとする。指標とアルゴリズム群については別の記事で詳しく扱っている。コスト分解は 実行不足と TCA、スケジューラ自体は TWAP/VWAP/POV アルゴリズム を参照してほしい。SCM が加えるのはその上の評価層だ。同じ期間に旧スケジューラを動かし続ける同じ銘柄をドナープールにするため、取引所全体のスプレッドや出来高の変化が処置単位とドナーの双方に等しく作用する。

介入前期間:切り替え前の 60 取引日。介入後期間:切り替え後の 30 取引日。効果推定値は次の差である。

τ^1t=Y1tactualY^1tsynthetic,t>T0\hat{\tau}_{1t} = Y_{1t}^{\text{actual}} - \hat{Y}_{1t}^{\text{synthetic}}, \quad t > T_0

新しいスケジューラが本当にコストを下げるなら、τ^1t<0\hat{\tau}_{1t} < 0T0T_0 後も持続する。さらに重要なのは、処置単位の差がドナーの差の雲の外側に位置することだ。

シナリオ 2:市場イベントの影響

手数料体系の変更、ティックサイズの実証実験、取引税、サーキットブレーカー規則の変更が一つの取引所に影響したとする。処置単位は影響を受けた取引所でのあなたの戦略、ドナープールは変更の影響を受けなかった取引所の同じペアである。執行における手数料体系の効果は メイカー・テイカー手数料とリベート で別に扱う。SCM は「変更後に約定が悪化した」を帰無仮説付きの推定値に変える。

シナリオ 3:レジーム条件付きの戦略切り替え

レジーム検出シグナル が発火したとき、一つの資産で平均回帰からモメンタムへ切り替える。切り替えずに留まるよりよかったか?ドナープールは、シグナルが発火したが切り替えなかった類似資産である。ただし罠がある。レジームシグナルが市場全体で発火するなら、ドナーも処置を受けてしまい設計が崩れる。下記の波及ルールを参照。

プラセボ検定と推論

プラセボ経路と検定対象の経路

SCM の推論は置換ベースである。閉形式の分布を信じるのではなくリサンプリングで経験的帰無分布を作る一般的な考え方は バックテストのモンテカルロ法とブートストラップ法 で説明した。以下は SCM 固有の構成である。

時間内プラセボ

介入日を介入前期間の途中、たとえば半分の時点まで戻して再実行する。何も起きていない日に SCM が「処置効果」を見つけるなら、介入ではなく既存の乖離を拾っている。信頼できる合成コントロールは、偽の介入日には効果を示さない。

空間内プラセボ(置換検定)

各ドナーに SCM の手順全体を適用し、順番に処置単位だったと仮定する。

  1. 各ドナー jj について、残りのドナーと元の処置単位から合成コントロールを構成する。
  2. 介入後の差 gapjt=YjtY^jtsynth\text{gap}_{jt} = Y_{jt} - \hat{Y}_{jt}^{\text{synth}} を計算する。
  3. 処置単位の差をプラセボの差の分布と比較する。

p 値は順位である。

p=rank of treated unit’s post/pre MSPE ratioJ+1p = \frac{\text{rank of treated unit's post/pre MSPE ratio}}{J + 1}

ここで

MSPE ratio=MSPEpostMSPEpre=1TT0t>T0(Y1tY^1tN)21T0tT0(Y1tY^1tN)2\text{MSPE ratio} = \frac{\text{MSPE}_{\text{post}}}{\text{MSPE}_{\text{pre}}} = \frac{\frac{1}{T - T_0} \sum_{t > T_0} (Y_{1t} - \hat{Y}_{1t}^N)^2}{\frac{1}{T_0} \sum_{t \leq T_0} (Y_{1t} - \hat{Y}_{1t}^N)^2}

この比率が生の介入後差ではなく適切な統計量である。決して十分にフィットしなかったドナーは、介入とは無関係な理由で介入後の差が大きくなる。介入前 MSPE で割ることで、まさにそれを罰する。

これは デフレート・シャープレシオ と同じ推論上の動きだ。候補をゼロではなく経験的に生成した参照分布内で順位付けする。DSR は試行全体における best-of-N 帰無分布に対して勝者を順位付けし、SCM は 単位 全体の帰無分布に対して処置単位を順位付けする。推定量は違っても規律は同じで、警告も同じだ。ドナーが J=15J = 15 なら、達成可能な最小 p 値は 1/160.061/16 \approx 0.06 である。置換検定は自身の粒度を超えて解像できないため、効果がどれほど大きく見えても、ドナープールが小さければ主張できる有意性には上限がある。

実務的なフィルタリング

Abadie らは、介入前のフィットが悪いプラセボ単位を除外することを推奨している。処置単位の kk 倍を超える介入前 MSPE(通常 kk は 2〜20)を持つ単位だ。フィットの悪いプラセボは分母が小さく、巨大な MSPE 比を生み、参照分布を汚染して処置単位を平凡に見せる。複数の閾値で比率を報告すること。p 値が特定の kk でのみ有意なら、それは効果ではなく調整パラメータを見つけたということだ。

実装

反実仮想分析パイプライン

以下のコードは仕組みを見せるため numpyscipy だけを使う。実運用の代替(Microsoft Research の SparseSCpysynconSyntheticControlMethods)は、エッジケースを処理し、より大規模に動作する。

予測変数行列の構成

import numpy as np
import pandas as pd
from scipy.optimize import minimize


def build_predictors(series: pd.Series, T0: int) -> np.ndarray:
    """
    Extract predictor vector from pre-intervention data.
    Features: mean return, volatility, skewness, kurtosis,
              plus lagged outcome values at regular intervals.
    """
    pre = series.iloc[:T0]
    daily_ret = pre.diff().dropna()

    stats = [
        daily_ret.mean(),
        daily_ret.std(),
        daily_ret.skew(),
        daily_ret.kurtosis(),
    ]

    lag_indices = np.linspace(0, T0 - 1, 5, dtype=int)
    lags = pre.iloc[lag_indices].values.tolist()

    return np.array(stats + lags)

ここでの df は累積リターンのパネルで、0 列目が処置単位、残りがドナー、各行が一つのバーである。価格水準ではなく累積リターンを使うこと。

X1 = build_predictors(df["Treated"], T0).reshape(-1, 1)   # (k x 1)
X0 = np.column_stack([                                    # (k x J)
    build_predictors(df[col], T0) for col in df.columns[1:]
])

重みの最適化

def solve_weights(
    X1: np.ndarray,
    X0: np.ndarray,
    Y1_pre: np.ndarray,
    Y0_pre: np.ndarray,
) -> np.ndarray:
    """
    Solve for optimal synthetic control weights.

    Nested optimization:
      Outer: optimize V (predictor importance)
      Inner: optimize w (donor weights) given V
    """
    k = X1.shape[0]
    J = X0.shape[1]

    def solve_w_given_V(V_diag: np.ndarray) -> np.ndarray:
        """Inner: find w minimizing weighted predictor distance."""
        V = np.diag(V_diag)

        def objective(w):
            gap = X1.flatten() - X0 @ w
            return gap @ V @ gap

        constraints = {"type": "eq", "fun": lambda w: np.sum(w) - 1.0}
        bounds = [(0, 1)] * J
        w0 = np.ones(J) / J

        result = minimize(objective, w0, method="SLSQP",
                         bounds=bounds, constraints=constraints,
                         options={"maxiter": 1000, "ftol": 1e-12})
        return result.x

    def outer_objective(V_diag: np.ndarray) -> float:
        """Outer: minimize pre-period MSPE given V."""
        w = solve_w_given_V(np.abs(V_diag))
        return np.mean((Y1_pre - Y0_pre @ w) ** 2)

    V0 = np.ones(k)
    result = minimize(outer_objective, V0, method="Nelder-Mead",
                     options={"maxiter": 5000, "xatol": 1e-8})

    return solve_w_given_V(np.abs(result.x))


Y1_pre = df["Treated"].iloc[:T0].values
Y0_pre = df.iloc[:T0, 1:].values
weights = solve_weights(X1, X0, Y1_pre, Y0_pre)

for col, w in zip(df.columns[1:], weights):
    if w > 0.01:
        print(f"  {col}: {w:.4f}")

差の系列は df["Treated"].values - df.iloc[:, 1:].values @ weights となる。これを介入日とともにプロットし、下のプラセボ実行から得たドナーの差と並べる。

プラセボ検定

def run_placebo_tests(df: pd.DataFrame, T0: int) -> pd.DataFrame:
    """In-space placebo: treat each unit in turn as if intervened upon."""
    results = []
    all_columns = df.columns.tolist()

    for placebo_col in all_columns:
        donor_cols = [c for c in all_columns if c != placebo_col]
        donor_df = df[donor_cols]

        X1_p = build_predictors(df[placebo_col], T0).reshape(-1, 1)
        X0_p = np.column_stack([
            build_predictors(donor_df[c], T0) for c in donor_cols
        ])

        w_p = solve_weights(
            X1_p, X0_p,
            df[placebo_col].iloc[:T0].values,
            donor_df.iloc[:T0].values,
        )
        gap_p = df[placebo_col].values - donor_df.values @ w_p

        mspe_pre = np.mean(gap_p[:T0] ** 2)
        mspe_post = np.mean(gap_p[T0:] ** 2)

        results.append({
            "unit": placebo_col,
            "mspe_pre": mspe_pre,
            "mspe_post": mspe_post,
            "mspe_ratio": mspe_post / mspe_pre if mspe_pre > 1e-10 else np.inf,
            "gap_series": gap_p,
            "is_treated": placebo_col == "Treated",
        })

    return pd.DataFrame(results)


placebo_results = run_placebo_tests(df, T0)

treated_pre = placebo_results.loc[placebo_results["is_treated"], "mspe_pre"].values[0]
filtered = placebo_results[placebo_results["mspe_pre"] <= 5 * treated_pre]

treated_ratio = filtered.loc[filtered["is_treated"], "mspe_ratio"].values[0]
p_value = (filtered["mspe_ratio"] >= treated_ratio).sum() / len(filtered)
print(f"MSPE ratio (treated): {treated_ratio:.2f}   p = {p_value:.3f}")

推定量のキャリブレーション

実行する価値があるデモと、拒否する価値があるデモがある。合成パネルデータを生成し、1 日あたり +0.3% の効果を注入し、およそ +0.3%/日を回収して証拠として示しても、SLSQP が収束することしか証明しない。循環論法であり、このブログでは公開しない。

合成データが 役に立つ のはキャリブレーションだ。DSR の記事PBO に適用したのと同じ基準で、既知の帰無が統計量を信頼できるものにする。実際に持つドナー数と介入前期間の長さを使い、線形因子モデルから生成したデータで二つの検証を行う。

  1. 既知の効果量に対する検出力。 既知の大きさの効果を注入する。推定された介入後差はそれを、どの程度の誤差で回収するか?効果量を小さくして回収に失敗するまで掃引する。その下限が、パネルの構造で検出できる最小効果であり、それを下回る実結果は p 値にかかわらずノイズである。
  2. 真の帰無におけるサイズ。 何も 注入しない。プラセボの全パイプラインを実行する。p 値は [0,1][0, 1] 上でおよそ一様になるはずなので、繰り返し抽出すれば約 10% の割合で 0.10 未満になるはずだ。それよりはるかに頻繁に発火するなら、予測変数集合またはフィルタ閾値が有意性を作り出しており、実データに触れる前からパイプラインが壊れている。

検証 2 は人々が飛ばしがちで、最も重要なものだ。まずこれを実行すること。

実務上の考慮事項

均衡した因果評価システム

ドナープールの選択

ドナーは処置単位と もっともらしく比較可能 であり、介入の 影響を受けていない 必要がある。執行なら、類似銘柄で同じアルゴリズム——同じセクター、時価総額デシル、流動性層——を使う。複数銘柄検証 の層分類はここでもそのまま使える。銘柄を公正な頑健性テストにする性質は、公正なドナープールにする性質でもある。取引所イベントなら、変更が届かなかった取引所の同じ銘柄を使う。

波及がある単位は除外する。 一つの銘柄でアルゴリズムを変えた結果、相関銘柄へのクォートも変わったなら、それらの銘柄は部分的に処置されている。残すと合成コントロールが処置単位の介入後経路に引っ張られ、推定値をゼロへ縮める——悪いコントロールが正の結果を作るのと同じくらい容易に、設計上の欠陥が帰無結果を作ってしまう。

介入前フィットが議論のすべて

SCM の信頼性は完全に介入前フィットにかかっている。介入前 MSPE を報告し、フィットを示すこと。T0T_0 より後のすべては、介入前フィットが悪ければ無効になる。

Abadie et al.(2015)はルールを明確に述べている。ドナーの凸結合で処置単位の介入前経路を再現できないなら、手法を適用してはならない。これは正直な反証基準であり、本来より稀だ。ほとんどの推定量は何を入力しても数値を返す。SCM は信頼できる反実仮想を作れないときに教えてくれる。そのメッセージへの正しい対応は失敗を公開することであり、フィットが改善するまでドナープールを広げることではない。介入前フィットを追ってプールを広げるのは、このシリーズが扱う 同じ過学習 をコントロール構成段階へ移しただけである。

短いパネルにおける過学習

SCM 固有に注目すべき量は、ドナー数 JJ と介入前観測数 T0T_0 の比率である。T0T_0 点に JJ 個の自由な重みをフィットすると、JJT0T_0 に近づくほど完全な介入前フィットが構成上可能になる——この方法で得た完全なフィットには情報がない。T0T_0JJ より十分大きく保つこと。変更前に戦略を 30 日間運用して候補ドナーが 20 個あるなら、SCM の問題ではなく補間である。対策は次の通り。

  1. 正則化:SparseSC は重み最適化に 1\ell_12\ell_2 のペナルティを加える。
  2. ドナープールを制限する:根拠のある少数のドナーは、多数のドナーより優れている。ただし JJ が最小 p 値も決めるため、置換グリッドが粗くなる。この緊張関係は現実にあり、きれいな解決策はない。
  3. 拡張 SCM:Ben-Michael、Feller、Rothstein(2021)は結果モデルを使い、完全な介入前フィットが不可能な場合にバイアスを補正する。

非定常性

累積リターンに対して SCM を実行し、価格水準では決して実行しない——水準は見せかけのフィットを招く。介入後の乖離を信頼する前に、介入前の差が平均ゼロで定常であることを確認する。そのための ADF と Engle-Granger の仕組み、観測系列ではなくフィットした残差を検定する際に必要な補正済み臨界値は、統計的アービトラージとペアトレーディング で扱っている。

複数の処置単位

複数資産へ同時に展開すると、単一処置単位の設計が崩れる。拡張には、プール SCM(処置単位全体の平均効果)と、異なる時点で処置された単位向けの 段階的導入 設計(Ben-Michael et al., 2022)がある。

SCM を使わない場合

曖昧な因果構造

適しているのは、既知の日付における 離散的で明確な介入、比較可能な未処置単位からなる 妥当なドナープール、そして事前に予想されていない介入である。予想されると処置単位が T0T_0 より前に動き、識別が破れる。

適さないのは、数週間にわたりパラメータが漂うような連続処置、真に未処置の比較対象が存在しない状況、シグナル対ノイズ比が重み推定を阻む高頻度評価である。

結論

実測経路と合成経路がともに収束する様子

選択と交絡は異なる失敗モードであり、異なる道具を必要とする。DSR と PBO は探索を評価する。SCM は反実仮想を評価し、このシリーズで唯一、実験を一回だけ行ったのに結果を信頼できない場合へ対処する道具である。

場当たり的なベンチマークより SCM を好む理由は、より大きな効果を出すからではない——むしろ小さくなることが多い——拒否できるからだ。介入前にドナーの凸結合が銘柄を追跡できなければ、SCM はそう告げ、停止する。処置単位の差がドナーの雲の内側なら、プラセボ p 値がそう告げ、帰無を報告する。この二つの出口が手法の価値であり、介入前 MSPE とプラセボ p 値を報告しない SCM の記述は、二本の線が分岐するチャート以上のものにする唯一の部分を飛ばしている。


参考文献

  • Abadie, A., Diamond, A., & Hainmueller, J. (2010). "比較事例研究のための合成コントロール法。" Journal of the American Statistical Association, 105(490), 493-505.
  • Abadie, A. (2021). "合成コントロールの利用:実行可能性、データ要件、方法論的側面。" Journal of Economic Literature, 59(2), 391-425.
  • Ben-Michael, E., Feller, A., & Rothstein, J. (2021). "拡張合成コントロール法。" Journal of the American Statistical Association, 116(536), 1789-1803.
  • Cunningham, S. (2021). 因果推論:ミックステープ. 第10章:合成コントロール。mixtape.scunning.com で閲覧可能。
  • Microsoft Research. SparseSC:スパース合成コントロール。github.com/microsoft/SparseSC
blog.disclaimer

Authors

Eugen Soloviov
Eugen Soloviov

Trading-systems engineer

Trading-systems engineer building bots since 2017: cross-exchange arbitrage (connected up to 30 venues), cointegration-based pairs arbitrage across spot and futures, scalping, news and sentiment-driven strategies, trend algorithms, and portfolio management and balancing algorithms. Also builds sub-millisecond order execution, big-data warehouses, backtesting engines, AI agents, and trading interfaces (incl. open-source profitmaker.cc). Stack: JS/TS, Python, Rust/Zig/Go, DevOps, backend, frontend, architecture.

Newsletter

市場の先を行く

ニュースレターを購読して、独占的なAI取引の洞察、市場分析、プラットフォームの更新情報を受け取りましょう。

プライバシーを尊重します。いつでも配信停止可能です。