封存文章
本文源自 MCM 2022 Problem C。它是回測方法教學,不是投資建議。 舊版本用完整五年資料選出 15/80 日參數,再在同一期間報 Sharpe 1.27 和 $15,789.84;這是樣本內最佳化結果,且交易執行存在 same-bar look-ahead, 不能視為可交易績效。
題目給定 2016-09-11 至 2021-09-10 的黃金與 Bitcoin 價格,起始資金 $1,000:
黃金佣金為 1%,Bitcoin 為 2%;Bitcoin 每日可交易,黃金只在其市場開放日交易。 每天的決策只能使用當時已知的價格歷史。
這個限制令研究重點由「找一條最高回報曲線」變成:
- 資料在甚麼時間變成可知?
- 訊號在甚麼價格執行?
- 不同日曆如何估值與交易?
- 參數如何在不偷看測試期下選擇?
- 佣金後的風險與 benchmark 比較是否仍成立?
一、狀態與交易
日末 portfolio value:
若買入價值 的資產 ,比例佣金 :
賣出 單位:
關鍵是訊號以 日收市價計算時,不能亦假設以同一個未知收市價成交。 保守 backtest 可在 的可交易 open/close 執行,並加入 slippage。
二、交易日曆
建立完整 calendar index,保留兩種資訊:
mark_price:非交易日可 forward-fill,只用於 portfolio valuation;tradable:只有真實報價/市場開放日才可下單。
calendar = pd.date_range(start, end, freq="D")
prices = raw.reindex(calendar)
prices["gold_tradable"] = prices["gold"].notna()
prices["btc_tradable"] = prices["bitcoin"].notna()
prices["gold_mark"] = prices["gold"].ffill()
prices["btc_mark"] = prices["bitcoin"].ffill()
不可因黃金星期末沒有價格便 continue 整日,否則 Bitcoin 的週末交易及估值都被刪除;
亦不可把 forward-filled gold price 當成該日可成交價格。
三、訊號
Moving-average state
target position 是 ,交易量由
決定。
def ma_target(price, short, long):
short_ma = price.rolling(short, min_periods=short).mean()
long_ma = price.rolling(long, min_periods=long).mean()
return (short_ma > long_ma).astype(float)
執行時把 target shift 一日:
orders = ma_target(close, short, long).shift(1)
這表示今日交易只使用昨日收市或更早資料。若題目假設每日報價在 decision 前可知, 也仍要把 timeline 寫清楚。
Mean reversion
rolling z-score:
可用 state machine: 進場,
離場。舊程式先產生 ,再 .diff(),
可能得到 ,但 backtester 只處理恰好 1 或 -1,
令部分訊號靜默失效。
四、事件驅動回測
一個可靠 loop 的次序是:
- 用截至 的資料取得 target;
- 檢查資產在 是否 tradable;
- 用 execution price、commission、slippage 成交;
- 更新 cash 和 holdings;
- 用 mark prices 計算日末 value;
- 記錄 orders、fills 和 rejection reason。
def apply_trade(cash, units, target_value, price, fee):
current_value = units * price
delta = target_value - current_value
if delta > 0:
spend = min(delta, cash / (1 + fee))
units += spend / price
cash -= spend * (1 + fee)
elif delta < 0:
sell_value = min(-delta, current_value)
sold_units = sell_value / price
units -= sold_units
cash += sell_value * (1 - fee)
return cash, units
不要用 max(cash, 0) 或 max(total, 0) 掩蓋 accounting bug;
若 cash 變負應觸發 assertion。
assert cash >= -1e-9
assert gold_units >= -1e-12
assert btc_units >= -1e-12
五、資產配置
舊策略每個 buy signal 使用「當時 cash 的 50%」:
- Bitcoin 先買會改變 gold 可用 cash;
- order of execution 影響配置;
- repeated buy signal 可能累積持倉;
- 沒有明確 target weights。
更清楚的是設定:
按 pre-trade portfolio value 計算所有 target values,再一起 re-balance。 例如兩個訊號皆開時各 40%,保留 20% cash;只有一個開時可配置 60%。 這是策略假設,必須預先指定而非由 loop 次序偶然決定。
六、績效指標
日報酬:
若以 calendar-day series:
若只用共同交易日則可能用 252,但 Bitcoin 週末風險消失,
兩者要與資料日曆一致。舊公式文字把分母寫成
std × sqrt(252),程式則正確地用
mean/std × sqrt(252);文章應保持一致。
Maximum drawdown:
另外報告:
- CAGR;
- annualized volatility;
- downside deviation/Sortino;
- turnover 和總 commissions;
- exposure;
- worst day/month;
- time under water;
- terminal wealth 的 uncertainty。
56.13% drawdown 不應僅因 Bitcoin 波動高便稱為「可接受」;可接受性取決於 mandate 和 investor loss tolerance。
七、Benchmark
至少比較:
- all cash;
- buy-and-hold Bitcoin;
- buy-and-hold gold;
- fixed-weight buy-and-hold;
- periodic rebalancing;
- same risk/same exposure benchmark。
所有 benchmark 使用同一初始日期、佣金、交易日曆和估值規則。 若策略持有大量 cash,應同時比較 volatility-matched benchmark, 避免把較低 exposure 當成 alpha。
八、參數選擇與 data snooping
舊程式在完整五年內遍歷 short/long windows,取最高 Sharpe, 再用同一五年報告 Sharpe 1.27 和 $15,789.84:
這是 in-sample maximum,會從噪聲中挑出幸運組合。正確流程是 walk-forward:
for train_end, test_end in folds:
train = data.loc[:train_end]
test = data.loc[train_end:test_end]
params = choose_params_on_train(train)
result = backtest_out_of_sample(test, params, warmup=train)
fold_results.append(result)
每個 test fold 的參數只能由之前資料選擇;最後串接所有 out-of-sample returns。 可使用 anchored expanding window 或 fixed rolling window。
Multiple testing
嘗試的 indicators、wavelets、HMM states、windows 和 rules 越多, 最佳 Sharpe 的 upward bias 越大。應:
- 預先登記有限策略族;
- 留 untouched final test period;
- 報告所有試過的變體;
- 使用 deflated Sharpe/White’s Reality Check 等修正;
- 檢查鄰近參數是否形成穩定 plateau,而非單一尖峰。
九、HMM、Granger 與 wavelet
這些方法只有在進入交易決策並 out-of-sample 驗證時才有價值。
- 在完整資料 fit HMM,再用同一資料 decoded states 交易,會偷看未來;
- Granger causality 需要 stationary specification 和 lag selection, 也不等於可獲利因果;
- wavelet decomposition 若使用雙側 filter,當前 coefficient 可能依賴未來值;
- normality test 拒絕與否不會直接產生交易規則。
每個 feature 必須有 as-of timestamp 和 real-time computability test。
十、交易成本與市場摩擦
題目只指定比例 commission,但研究仍要測試:
原敏感度表顯示成本提高時 Sharpe 下降,這是合理方向; 但因同一資料已用來選參數,不能證明 robustness。 應在每個 walk-forward fold 以不同成本重跑,並報 break-even fee。
黃金資料若是 benchmark fixing price,而非可交易即時價格, execution assumption 亦要標明。
十一、壓力測試
直接把某幾日 return 改成 crash/rally 而不重新計算 signal、fill 和 liquidity, 只能作 portfolio shock。較完整情景包括:
- spread 擴大;
- trading halt/gold holiday;
- Bitcoin weekend gap;
- price crash 同時 liquidity 下降;
- exchange/custody outage;
- delayed execution;
- regime shift 後趨勢策略連續 whipsaw。
壓力測試應報 margin/cash constraint、最大單日損失、能否執行 exit, 而非只畫 shocked equity curve。
十二、可重現研究設計
資料切分
- 2016–2018:初始研究與參數範圍;
- 2019–2020:walk-forward validation;
- 2021:最終 untouched test;
或使用多個 rolling folds,視樣本量調整。
預先固定
- signal timestamp;
- execution timestamp;
- allocation rule;
- commissions 和 slippage;
- benchmark;
- missing-day policy;
- parameter grid;
- primary metric;
- failure criteria。
測試
- 無交易時 portfolio 恆等於 cash;
- 單次買賣的 commission 可手算;
- 黃金休市不能成交,但仍可估值;
- Bitcoin 週末可交易;
- signal shift 防止 same-bar fill;
- cash 和 holdings 永不憑空增加;
- final liquidation 是否計佣金要一致;
- benchmark 使用同一日曆。
十三、舊結果的合適標籤
原輸出:
- short 15、long 80;
- 樣本內 Sharpe 1.27;
- maximum drawdown 56.13%;
- $1,000 變 $15,789.84。
應標為:
探索性、樣本內、經參數搜尋後的 backtest output。
它沒有經 walk-forward 或 untouched test,且執行時點和 missing-day accounting 仍有問題,所以不能稱為 robust strategy,也不能預期未來重現。
結論
交易策略研究最難的部分不是計算 moving average,而是重建每一天的資訊集合和執行條件。 一個可信的答案需要:
- 分開 valuation 與 tradability;
- 將訊號至少 lag 到下一個可執行時點;
- 用 target weights 而非 order-dependent cash fractions;
- 完整記錄佣金、slippage 和 skipped orders;
- 用 walk-forward 選參與測試;
- 與公平 benchmark 比較;
- 把所有嘗試納入 multiple-testing 風險。
修正後,這篇文章不再以 $15,789.84 作成功宣言。 真正有研究價值的成果是一個不偷看未來、會在休市日正確運作、 並能讓讀者重現每一筆交易的 backtesting protocol。
參考資料
- COMAP, Inc. (2022). MCM 2022 Problem C: Trading Strategies and accompanying price data.
- Bailey, D. H., Borwein, J. M., López de Prado, M., & Zhu, Q. J. (2017). The probability of backtest overfitting. Journal of Computational Finance, 20(4), 39–69.
- Harvey, C. R., Liu, Y., & Zhu, H. (2016). …and the cross-section of expected returns. Review of Financial Studies, 29(1), 5–68.
- López de Prado, M. (2018). Advances in Financial Machine Learning. Wiley.
- White, H. (2000). A reality check for data snooping. Econometrica, 68(5), 1097–1126.