Share Notes

chundev

View the Project on GitHub latteouka/share-notes

策略回測的多重檢驗修正 — Deflated Sharpe Ratio 與 White’s Reality Check

日期:2026-04-20 情境:量化策略參數搜尋後的顯著性驗證


TL;DR

當你為了找最佳策略嘗試 N 組參數,「看起來最好的那一組」一定高估了真實 edge — 這是 selection bias。正確做法是用 Deflated Sharpe Ratio(DSR)(解析解)+ White’s Reality Check(RC)(bootstrap 實證)兩種方法修正。兩者測不同東西:DSR 測風險調整後 edge(Sharpe/變異數比),RC 測絕對報酬;小樣本 + 高變異數時常常 DSR 通過、RC 不通過,這不是 bug,而是反映了「策略風險效率好、但絕對報酬還不夠突出」的真實狀態。


為什麼需要修正

量化策略開發的常見流程:

1. 想到一個 idea(例如 P/C Ratio ≥ 130 做多)
2. 跑 grid search 找最佳參數
   - putCallThreshold: [110, 115, 120, ..., 140]
   - maxHoldDays: [5, 6, ..., 30]
   - takeProfitPercent: [3, 5, 8, ..., 20]
   - ...十幾個維度
3. 挑 Sharpe 最高的組合發表 → 「策略 Sharpe 3.2!」

陷阱:如果所有策略的真實 Sharpe 都是 0(純雜訊),光是「取 N 次中最大值」也會產生看起來很高的 Sharpe

數學直覺:N 個 iid 隨機 Sharpe 的最大值期望值:

E[max SR] ≈ √(2 · ln N) · σ_SR_null

其中 σ_SR_null ≈ 1 / √T(T = 交易筆數)。T=68 時 σ ≈ 0.12:

N(測試策略數) E[max SR]
1 0
10 0.26
100 0.55
1000 0.83

意思是:做 100 組 grid search,就算每組真實 edge 都是零,你期望看到的「最佳 Sharpe」也有 0.55。沒修正就發表,等於在宣告噪音是訊號。


方法 A:Deflated Sharpe Ratio(DSR)

來源:Bailey, D.H. & López de Prado, M. (2014) The Deflated Sharpe Ratio.

核心公式

DSR = P(真實 SR > 0 | 觀察到 SR_obs, 測了 N 個策略)
    = Φ((SR_obs - E[max SR_null]) / σ(SR_obs))

三個要素:

  1. 觀察 Sharpe SR_obs:用 Time-based Sharpe,不是 position-based(position-based 會虛高,只算有持倉的日子)。

  2. 期望最大 null Sharpe E[max SR_null]

    精確版(含 Euler-Mascheroni 修正):

    E[max SR] = σ_null × [
      (1 - γ) × Φ⁻¹(1 - 1/N)
      + γ × Φ⁻¹(1 - 1/(N·e))
    ]
    γ ≈ 0.5772(Euler-Mascheroni 常數)
    σ_null = 1 / √T
    
  3. 觀察 Sharpe 的標準誤差 σ(SR_obs):Lo (2002) 修正,考慮報酬的 skew 和 kurtosis:

    σ(SR) = √((1 - γ₃·SR + (γ₄ - 1)/4 · SR²) / (T - 1))
    

    γ₃ = skewness, γ₄ = kurtosis。非常態分布會放大 Sharpe 的不確定性

實作(TypeScript)

export function calculateDSR(
  trades: Trade[],
  observedSharpe: number,
  numTrials: number
): DeflatedSharpeResult {
  const returns = trades.map((t) => t.returnRate / 100);
  const T = returns.length;

  // Skew, kurtosis
  const mean = returns.reduce((s, r) => s + r, 0) / T;
  const variance = returns.reduce((s, r) => s + (r - mean) ** 2, 0) / T;
  const stdDev = Math.sqrt(variance);
  const skew = returns.reduce((s, r) => s + ((r - mean) / stdDev) ** 3, 0) / T;
  const kurtosis = returns.reduce((s, r) => s + ((r - mean) / stdDev) ** 4, 0) / T;

  // σ(SR) — Lo (2002) 修正
  const sr = observedSharpe;
  const sharpeVar = (1 - skew * sr + ((kurtosis - 1) / 4) * sr * sr) / (T - 1);
  const sharpeStdErr = Math.sqrt(Math.max(sharpeVar, 0.0001));

  // E[max SR] 下 null
  const EULER = 0.5772156649;
  const sigmaSrNull = 1 / Math.sqrt(T);
  const invNormN = inverseNormalCDF(1 - 1 / numTrials);
  const invNormNe = inverseNormalCDF(1 - 1 / (numTrials * Math.E));
  const expectedMaxSharpe =
    sigmaSrNull * ((1 - EULER) * invNormN + EULER * invNormNe);

  // DSR
  const zscore = (sr - expectedMaxSharpe) / sharpeStdErr;
  const dsr = normalCDF(zscore);

  return { dsr, passed: dsr > 0.95, /* ... */ };
}

判讀


方法 B:White’s Reality Check(RC)

來源:White, H. (2000) A Reality Check for Data Snooping.

核心想法

Bootstrap 實證:隨機重組自己的交易報酬(去除均值 → null hypothesis),重複抽 N 組虛擬策略,看「其中最好那組」的年化報酬會不會蓋過觀察值。

Null: 觀察到的績效純粹來自運氣 + 多次嘗試
     (真實 expected return = 0,報酬分布形狀不變)

實作(TypeScript)

export function runRealityCheck(
  trades: Trade[],
  actualAnnualizedReturn: number,
  numTrials: number,       // K = 測試過的策略數
  iterations = 5000
): RealityCheckResult {
  const T = trades.length;
  const returns = trades.map((t) => t.returnRate);
  const avgHoldDays = trades.reduce((s, t) => s + t.holdDays, 0) / T;
  const tradesPerYear = 252 / avgHoldDays;

  // Null hypothesis:去均值
  const meanReturn = returns.reduce((s, r) => s + r, 0) / T;
  const centered = returns.map((r) => r - meanReturn);

  const nullDistribution: number[] = [];

  for (let iter = 0; iter < iterations; iter++) {
    // N 個虛擬策略,每個 bootstrap T 筆報酬
    let maxAnnualizedAmong_N = -Infinity;

    for (let n = 0; n < numTrials; n++) {
      let sumReturn = 0;
      for (let i = 0; i < T; i++) {
        const idx = Math.floor(Math.random() * T);
        sumReturn += centered[idx]!;
      }
      const avgR = sumReturn / T;
      const annualized = avgR * tradesPerYear;
      if (annualized > maxAnnualizedAmong_N) {
        maxAnnualizedAmong_N = annualized;
      }
    }
    nullDistribution.push(maxAnnualizedAmong_N);
  }

  nullDistribution.sort((a, b) => a - b);
  const greaterCount = nullDistribution.filter(
    (n) => n >= actualAnnualizedReturn
  ).length;
  const pValue = greaterCount / iterations;

  return {
    pValue,
    critical95: nullDistribution[Math.floor(iterations * 0.95)]!,
    passed: pValue < 0.05,
  };
}

判讀


估計 N 的困難

兩個方法都需要輸入「你測試過幾個策略」。這個數字不是精確的,因為:

  1. 多維 grid 的維度不獨立:stopLoss=5% 和 6% 幾乎是一樣的策略,不該算兩個 effective trials
  2. 非正式嘗試難以計算:除了 grid search,你還可能手動試過、讀文獻後微調,這些也是 trials
  3. 相關性難以量化:Harvey & Liu (2015) 提出用 cross-correlation 估計 effective N,但實務上極難準確

實戰做法:給一個保守估計區間(e.g. 總 raw trials × 30% ~ 100%),兩端都跑一次看結論是否穩健。

以一個我盤點的案例:

類別 raw 試驗數
putCallThreshold(grid) 7
maxHoldDays(grid) 20
takeProfitPercent 9
stopLossPercent 12
scaleIn 組合 30
entryConfidence 條件 12
tier 結構 10
multiplier leverage 8
maxContracts 6
profitRetentionRate 8
capitalCeiling 14
其他雜項 81
合計 raw 217
effective(30% 獨立) 65

套 DSR 時兩個 N 都跑:65 → DSR 100%,217 → DSR 100%(穩健)。


DSR 通過、RC 不通過的真實案例

實測策略:68 筆交易、年化 38.17%、Time-based Sharpe 1.22。

DSR 結果

測試策略數 N:65
觀察 Sharpe:1.222
期望最大 Sharpe (null):0.288
Sharpe 標準誤差:0.134

Z-score = (1.222 - 0.288) / 0.134 = 6.97
DSR = Φ(6.97) ≈ 100.0%

✅ 通過

RC 結果

測試策略數 N:65
觀察年化:38.17%
Null 95% 臨界:56.03%
Null 99% 臨界:64.31%
p-value:0.5837

❌ 不通過

兩個給出相反結論。為什麼?


為什麼 DSR 和 RC 會分歧

關鍵:兩個測的是不同東西

項目 DSR RC
測量對象 Sharpe Ratio(報酬/風險) 絕對年化報酬
修正方法 解析解(基於常態近似 + skew/kurt) 實證 bootstrap
對報酬變異敏感度 分母就是變異數,已標準化 變異大時 null 分布也變寬
小樣本穩定性 受益於 normalization 容易被幾筆大 outlier 主導

數學直覺

實務解讀

觀察值 Sharpe 1.22 vs null max 0.29 → 差 7 個標準誤(極度顯著)。
觀察值年化 38% vs null 95% 臨界 56% → 落在 null 分布內(不顯著)。

這代表:

誠實結論:這是「有 edge 但樣本還不夠」的策略,不是聖杯。


解決 RC 不通過的路徑

  1. 累積更多交易:T 從 68 → 150 後,RC 的 null 分布會收窄
  2. 降低 grid search 規模:跑過 217 組 → 減到 50 組,N 變小,E[max SR_null] 變小
  3. 先用 cross-validation / WF 鎖定方向,再用獨立樣本驗證:把開發和驗證分開,避免在同一份資料上 overfit

長期而言:策略的風險效率(Sharpe)才是學術文獻重視的指標,DSR 通過就可以發實務期刊;要發 top asset pricing 期刊則需 T > 150 + RC 通過。


學到的事


參考資料