📝

Draft article

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

← 返回文章列表
August 21, 2026
5 分鐘閱讀

Synthetic Control Methods for Evaluating Trading Strategies

Synthetic Control Methods for Evaluating Trading Strategies
#causal-inference
#synthetic-control
#counterfactual
#evaluation
#econometrics

本系列長期探討一條導向虛假優勢的路徑:選擇折減夏普比率為搜尋中的勝者定價,回測過度擬合概率則為搜尋本身定價。誠實的負面結果說明,當你把兩者都用在一個本來就想交易的策略上時會發生什麼。這套工具很好地回答了一個問題:我是因為反覆查看太多次才找到它的嗎?

但它完全沒有觸及另一條獨立的路徑:混淆。你只改變了一件事——部署新的執行演算法、切換參數、對狀態作出反應——而績效改善了。沒有搜尋,也沒有多重檢驗問題。但改善仍然可能與你無關,因為你上線的那一週波動率翻倍了。任何折減都抓不到這一點,因為折減修正的是試驗次數,而你只執行了一次。

Abadie 和 Gardeazabal(2003)提出、Abadie、Diamond 和 Hainmueller(2010、2015)改進的合成控制法(SCM)直接處理混淆。它建立一個反事實——由你沒有觸碰的工具組成的加權組合,並選擇權重使其在干預之前緊密追蹤受處置工具。干預後兩者的分歧就是效果估計。關鍵在於,SCM 附帶一項偽造檢驗準則,告訴你何時無法產生可信的反事實;還附帶一個基於置換的 p 值,不需要你相信任何漸近理論。

為什麼顯而易見的替代方案會失敗

反事實比較路徑

干預前後比較會把干預與當天其他所有變化混在一起。與單一基準工具比較也會失敗,因為沒有任何單一資產能追蹤你的資產——這是另一個方向上的單一工具陷阱:如果一個工具的特有結構能偽造優勢,那麼另一個工具的特有結構也能偽造一個控制組。SCM 的供體池正是對這一反對意見的正式回答:不選一個可比資產,而是讓干預前資料選出一個由多個資產組成的加權籃子。

差分中的差分值得多說幾句,因為它是最接近但仍然錯失的方法。DiD 比較處置單位與控制組的變化,只有在平行趨勢假設下才能識別效果:如果沒有干預,處置組和控制組本應同步變動。在金融市場中,這個假設幾乎無法辯護——資產的 beta、波動率狀態、流動性層級都不同,相對路徑也會漂移。SCM 用一個擬合出的對象替代這個假設:不是假定一個平行變動的控制,而是構造一個,然後展示它實際追蹤得有多好。如果追蹤得很差,方法會告訴你停止。

方法

合成控制供體構造

設定與符號

考慮 J+1J + 1 個單位(資產、策略或投資組合),在 TT 個期間內觀察。單位 1 是處置單位——你在其中進行干預的資產或策略。單位 2,,J+12, \ldots, J+1 組成可比的未處置單位供體池

Y1tIY_{1t}^I 表示處置單位在時間 tt 受到干預時的結果,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})' 滿足 wj0w_j \geq 0 對所有 jj 均成立,且 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} 通過交叉驗證選擇:選取 V\mathbf{V},使得到的 w(V)\mathbf{w}^*(\mathbf{V}) 在干預前期間最小化結果變數的均方預測誤差(MSPE)。這是巢狀最佳化——外層迴圈遍歷 V\mathbf{V},內層在約束下求解 w\mathbf{w}

交易應用的預測變數

Abadie 等人使用人均 GDP 和產業構成,而交易應用需要:

  • 收益特徵:干預前期間的平均收益、波動率、偏度、峰度
  • 流動性指標:平均價差、日成交量、Amihud 非流動性比率
  • 微觀結構特徵:訂單流失衡、交易到達率、報價到成交比率
  • 因子暴露:對市場、動量、價值和波動率因子的 beta
  • 滯後結果:干預前若干日期的累積 PnL 值

滯後結果最重要。Abadie 等人(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} < 0 應在 T0T_0 之後持續出現;更重要的是,處置單位的差距應落在供體差距雲之外。

場景 2:市場事件的影響

費率表變更、最小跳動單位試點、交易稅或熔斷規則變更影響一個交易場所。處置單位是你在受影響場所的策略;供體池是變更未觸及的場所上的同一交易對。執行中的費率結構影響在maker-taker 費用與返傭中單獨處理;SCM 則把「變更後我們的成交變差了」轉化為帶有零假設的估計。

場景 3:按狀態切換策略

狀態檢測訊號觸發時,你在一項資產上從均值回歸切換到動量。這次切換是否勝過維持原狀?供體池是訊號觸發但你沒有切換的相似資產。注意這裡的陷阱——如果狀態訊號在全市場觸發,你的供體也同樣受到處置,設計就會崩潰。參見下方的溢出規則。

安慰劑檢驗與推斷

安慰劑軌跡與受檢驗路徑

SCM 的推斷基於置換。關於通過重採樣建立經驗零分布,而不是相信閉式分布的一般理由,見回測的蒙特卡洛與 bootstrap 方法;下面則是 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 將勝者與試驗中最佳 N 個結果的零假設比較;SCM 將處置單位與跨單位的零假設比較。估計量不同,紀律相同,警告也相同:當 J=15J = 15 個供體時,可達到的最小 p 值是 1/160.061/16 \approx 0.06。置換檢驗無法超越自身的粒度,因此小供體池會限制你有資格聲稱的顯著性,無論效果看起來多大。

實務篩選

Abadie 等人建議排除干預前擬合很差的安慰劑單位——干預前 MSPE 超過處置單位 kk 倍,其中 kk 通常為 2 到 20。擬合差的安慰劑分母很小,會產生巨大的 MSPE 比率,污染參考分布,使你的處置單位看起來並不突出。在幾個閾值下報告比率;如果你的 p 值只在某個特定 kk 下顯著,那你找到的是調參參數,而不是效果。

實作

反事實分析流程

下面的程式碼只使用 numpyscipy,因此可以看清機制。生產替代方案——SparseSC(Microsoft Research)、pysynconSyntheticControlMethods——能處理邊界情況並更好地擴展。

構造預測矩陣

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 欄是處置單位,其餘欄是供體,每一行對應一根 bar。使用累積收益率,不要使用價格水平。

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}")

校準估計量

有一個值得運行的演示,也有一個應該拒絕的演示。生成合成面板資料,注入每天 +0.3% 的效果,恢復約 +0.3%/天,然後把它當作證據,只能證明 SLSQP 會收斂。這是循環論證,本博客不發布這種內容。

合成資料真正適合做的是校準——與DSR 文章PBO中採用的標準相同:已知零假設正是使統計量值得信任的原因。以下兩項檢查都應在由線性因子模型生成的資料上運行,並使用你實際擁有的供體數量和干預前長度:

  1. 已知效果大小下的檢驗力。 注入一個已知幅度的效果。估計的干預後差距能否恢復它,誤差是多少?逐步降低效果大小,直到恢復失敗——這個下限就是面板幾何能檢測到的最小效果;任何低於它的真實結果,無論 p 值如何都是噪聲。
  2. 真實零假設下的尺寸。 什麼都不注入。運行完整的安慰劑流程。p 值應在 [0,1][0, 1] 上近似均勻,因此重複抽樣時約有 10% 的結果低於 0.10。如果觸發頻率遠高於此,你的預測變數集或篩選閾值正在製造顯著性,流程在接觸真實資料前就已經壞了。

第二項是人們會跳過、但最重要的一項。先運行它。

實務注意事項

平衡的因果評估系統

選擇供體池

供體必須與處置單位在合理意義上可比,且不受干預影響。對執行研究而言:相似工具上的同一演算法——相同板塊、市值分位、流動性層級。多標的驗證中的層級分類可直接在此重用;使工具成為公平穩健性測試的屬性,也使它們成為公平供體池。對交易場所事件而言:在變更未觸及的交易場所交易同一工具。

排除存在溢出的單位。 如果你改變一項工具上的演算法,改變了你在相關工具上的報價,那些工具就部分受到處置。把它們留在池中會使合成控制偏向處置單位的干預後路徑,並將估計值向零縮小——這種設計缺陷與糟糕控制會製造正結果一樣容易製造零結果。

干預前擬合就是全部論據

SCM 的可信度完全建立在干預前擬合上。報告干預前 MSPE 並展示擬合。干預前擬合很差,T0T_0 之後的一切都無效。

Abadie 等人(2015)直截了當地給出規則:如果供體的任何凸組合都無法重現處置單位的干預前軌跡,不要使用該方法。這是一項誠實的偽造標準,但實踐中出現得比應有的少——大多數估計量無論輸入什麼都會返回一個數字。SCM 會告訴你何時無法產生可信的反事實,而對這個訊息的正確回應是發布失敗,而不是不斷擴大供體池直到擬合改善。為了追逐干預前擬合而擴大池,是本系列其餘部分所討論的同一種過擬合,只是搬到了控制構造步驟。

短面板中的過擬合

SCM 特有、需要關注的量是供體數 JJ 與干預前觀測數 T0T_0 的比率。使用 JJ 個自由權重在 T0T_0 個點上擬合時,隨著 JJ 接近 T0T_0,完美干預前擬合會因構造而變得可能——而以這種方式得到的完美擬合不攜帶任何資訊。讓 T0T_0 大幅高於 JJ;如果策略在變更前運行了 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 等,2022),適用於在不同時間受到處置的單位。

何時不要使用 SCM

模糊的因果結構

適用於:已知日期發生的離散、定義清晰的干預;由可比未處置單位組成的合理供體池;事前未被預期的干預,因為預期會在 T0T_0 之前改變處置單位並破壞識別。

不適用於:連續處置,例如數週內逐漸漂移的參數;沒有真正未處置可比對象的環境;高頻評估,其中信噪比使權重估計無法完成。

結論

實際與合成軌跡共同收斂

選擇與混淆是不同的失敗模式,需要不同工具。DSR 和 PBO 為搜尋定價。SCM 為反事實定價;在本系列中,只有它處理了這種情況:你恰好運行了一次實驗,卻仍然無法信任結果。

偏好它而不是臨時基準的原因,不是它會產生更大的效果——它經常產生更小的效果——而是它可以拒絕。如果沒有供體的凸組合能在干預前追蹤你的工具,SCM 會說明這一點,你就應該停止。如果處置差距位於供體雲內,安慰劑 p 值會說明這一點,你就應該報告零結果。這兩個退出條件就是方法的價值;而一篇沒有報告干預前 MSPE 和安慰劑 p 值的 SCM 文章,跳過了使它不只是兩條分歧曲線圖的唯一部分。


延伸閱讀

  • Abadie, A., Diamond, A., & Hainmueller, J. (2010). "Synthetic Control Methods for Comparative Case Studies." Journal of the American Statistical Association, 105(490), 493-505.
  • Abadie, A. (2021). "Using Synthetic Controls: Feasibility, Data Requirements, and Methodological Aspects." Journal of Economic Literature, 59(2), 391-425.
  • Ben-Michael, E., Feller, A., & Rothstein, J. (2021). "The Augmented Synthetic Control Method." Journal of the American Statistical Association, 116(536), 1789-1803.
  • Cunningham, S. (2021). Causal Inference: The Mixtape. Chapter 10: Synthetic Control. Available at mixtape.scunning.com.
  • Microsoft Research. SparseSC: Sparse Synthetic Controls. github.com/microsoft/SparseSC.
免責宣告:本文提供的資訊僅用於教育和參考目的,不構成財務、投資或交易建議。加密貨幣交易涉及重大損失風險。

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 交易見解、市場分析和平台更新。

我們尊重您的隱私。您可以隨時退訂。