📝

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})' 满足对所有 jjwj0w_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} 通过交叉验证选择:选择 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。如果出现频率远高于此,说明你的预测变量集合或筛选阈值正在制造显著性,流程在接触真实数据之前就已经坏了。

第 2 项是人们常跳过、却最重要的一项。先运行它。

实务考量

平衡的因果评估系统

选择供体池

供体必须与处理单元具有合理可比性,并且不受干预影响。对于执行工作,应选择相似工具上的相同算法——相同板块、市值分位、流动性层级。多符号验证中的层级分类可以直接复用;使工具成为公平稳健性测试的那些属性,也使它们成为公平供体池。对于交易场所事件,应选择变化未波及的交易场所上的同一工具。

排除有溢出的单元。 如果你在一个工具上改变算法,导致你在相关工具上的报价也改变,那么这些工具就部分接受了处理。把它们留在供体池中,会使合成控制偏向处理单元的干预后路径,并将估计值压向零——这是一种设计缺陷,和糟糕的控制组制造正结果一样容易制造零结果。

干预前拟合就是全部论据

SCM 的可信度完全建立在干预前拟合上。报告干预前 MSPE 并展示拟合。干预前拟合很差,会使 T0T_0 之后的一切都失效。

Abadie 等人(2015)明确提出这条规则:如果没有任何供体的凸组合能够重现处理单元的干预前轨迹,就不要应用该方法。这是诚实的证伪检验标准,但实际使用频率低于应有水平——多数估计量无论输入什么都会返回一个数字。SCM 会告诉你何时无法产生可信的反事实;对此信息的正确回应是发布失败,而不是不断扩大供体池直到拟合改善。为追逐干预前拟合而扩大供体池,就是本系列一直讨论的同一种过拟合,只不过它搬到了控制构造步骤。

短面板中的过拟合

SCM 特有的关注量是供体数 JJ 与干预前观测数 T0T_0 的比值。在 T0T_0 个点上拟合 JJ 个自由权重时,随着 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)。"用于比较案例研究的合成控制方法。" 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
免责声明:本文提供的信息仅用于教育和参考目的,不构成财务、投资或交易建议。加密货币交易涉及重大损失风险。

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

我们尊重您的隐私。您可以随时退订。