chundev
日期:2026-04-20 情境:量化策略參數搜尋後的顯著性驗證
當你為了找最佳策略嘗試 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。沒修正就發表,等於在宣告噪音是訊號。
來源: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))
三個要素:
觀察 Sharpe SR_obs:用 Time-based Sharpe,不是 position-based(position-based 會虛高,只算有持倉的日子)。
期望最大 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
觀察 Sharpe 的標準誤差 σ(SR_obs):Lo (2002) 修正,考慮報酬的 skew 和 kurtosis:
σ(SR) = √((1 - γ₃·SR + (γ₄ - 1)/4 · SR²) / (T - 1))
γ₃ = skewness, γ₄ = kurtosis。非常態分布會放大 Sharpe 的不確定性。
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, /* ... */ };
}
DSR > 0.95:有 95%+ 機率真實 Sharpe > 0,通過0.80 < DSR < 0.95:邊緣,需要更多樣本DSR < 0.80:多重檢驗修正後 edge 不顯著來源:White, H. (2000) A Reality Check for Data Snooping.
Bootstrap 實證:隨機重組自己的交易報酬(去除均值 → null hypothesis),重複抽 N 組虛擬策略,看「其中最好那組」的年化報酬會不會蓋過觀察值。
Null: 觀察到的績效純粹來自運氣 + 多次嘗試
(真實 expected return = 0,報酬分布形狀不變)
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,
};
}
p < 0.01:極度顯著,通過p < 0.05:顯著,通過p > 0.10:不顯著兩個方法都需要輸入「你測試過幾個策略」。這個數字不是精確的,因為:
實戰做法:給一個保守估計區間(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%(穩健)。
實測策略:68 筆交易、年化 38.17%、Time-based Sharpe 1.22。
測試策略數 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%
✅ 通過
測試策略數 N:65
觀察年化:38.17%
Null 95% 臨界:56.03%
Null 99% 臨界:64.31%
p-value:0.5837
❌ 不通過
兩個給出相反結論。為什麼?
關鍵:兩個測的是不同東西。
| 項目 | 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 但樣本還不夠」的策略,不是聖杯。
長期而言:策略的風險效率(Sharpe)才是學術文獻重視的指標,DSR 通過就可以發實務期刊;要發 top asset pricing 期刊則需 T > 150 + RC 通過。