蒙特卡洛自助法:如何用10行程式碼獲取回測的置信區間
你對一個策略進行了回測。得到了PnL +42%,Sharpe 1.8,MaxDD -12%。結果看起來非常好。你把機器人部署到生產環境,一個月後發現回撤已經到了-28%,PnL正趨向於零。
出了什麼問題?不是程式bug,也不是"市場變了"。問題在於你基於一個數字做出了決策——一個單點估計。你知道策略顯示了+42%,但你不知道你能在多大程度上信任這個數字。
單點估計的問題
單個數據點(左)給出了誤導性的畫面,而完整的分佈(右)揭示了可能結果的真實範圍。
在歷史資料上的回測只是對一個特定市場事件序列的單次執行。結果取決於交易的順序:同一個策略的同樣交易,但順序不同,可能顯示出完全不同的最大回撤。
想像491筆交易。每筆交易都是一個具有特定收益分佈的隨機事件。歷史回測只展示了這個過程的一次實現。這就像擲一次骰子就斷定骰子永遠會擲出四點。
我們真正需要的是:
- 不是點估計,而是區間:"有95%的機率,最終PnL在X到Y之間"
- 不是單個最大回撤,而是分佈:"在最差的5%場景中,回撤超過Z%"
- 不是均值,而是尾部:如果運氣不站在你這邊會怎樣?
這正是蒙特卡洛自助法的用途。
什麼是蒙特卡洛自助法
自助法通過從原始資料集中有放回地重取樣交易,生成數千條替代權益軌跡。
自助法是Bradley Efron在1979年提出的重取樣方法。思路很優雅:如果我們有一個數據樣本,我們可以通過從原始資料中有放回地隨機抽取元素來生成數千個"新"樣本。
在回測的場景中,它是這樣工作的:
- 你有一個每筆交易收益率的陣列——例如491個值
- 你從這個陣列中有放回地隨機抽取491個值——有些交易會出現兩次,有些則完全不會出現
- 你用這個新樣本構建權益曲線
- 重複10,000次
- 你得到最終指標的分佈,而不是單個數字
每次迭代都是一個"替代場景":如果交易的順序和組合略有不同,可能會發生什麼。
10行程式碼實現
以下是完整的可執行實現:
import numpy as np
def max_drawdown(equity_curve):
"""计算权益曲线的最大回撤。"""
peak = np.maximum.accumulate(equity_curve)
drawdown = (equity_curve - peak) / peak
return drawdown.min()
trade_returns = [...] # 491个值,例如 [0.012, -0.005, 0.008, ...]
n_simulations = 10000
results = []
for _ in range(n_simulations):
sampled = np.random.choice(trade_returns, size=len(trade_returns), replace=True)
equity = np.cumprod(1 + sampled)
results.append({
"final_pnl": equity[-1] - 1,
"max_dd": max_drawdown(equity),
"sharpe": np.mean(sampled) / np.std(sampled) * np.sqrt(252)
})
執行時間:在普通筆記型電腦上約2秒。10,000個你的策略的替代歷史。
提取置信區間
關鍵策略指標的置信區間:PnL、MaxDD和Sharpe Ratio,顯示第5百分位(最差)、第50百分位(中位數)和第95百分位(最佳)的區間帶。
現在我們有的不是一個數字,而是一個分佈。以下是如何從中提取有用資訊:
import pandas as pd
df = pd.DataFrame(results)
pnl_5 = np.percentile(df['final_pnl'], 5)
pnl_50 = np.percentile(df['final_pnl'], 50)
pnl_95 = np.percentile(df['final_pnl'], 95)
dd_5 = np.percentile(df['max_dd'], 5) # 第5百分位——最差情况
dd_50 = np.percentile(df['max_dd'], 50)
dd_95 = np.percentile(df['max_dd'], 95) # 第95百分位——最佳情况
print(f"PnL: {pnl_5:.1%} | {pnl_50:.1%} | {pnl_95:.1%}")
print(f"MaxDD: {dd_5:.1%} | {dd_50:.1%} | {dd_95:.1%}")
print(f"Sharpe: {np.percentile(df['sharpe'], 5):.2f} — {np.percentile(df['sharpe'], 95):.2f}")
一個真實策略的輸出示例:
| 指標 | 第5百分位(最差) | 中位數 | 第95百分位(最佳) |
|---|---|---|---|
| PnL | +18.3% | +41.7% | +72.1% |
| MaxDD | -23.4% | -12.8% | -5.1% |
| Sharpe | 1.12 | 1.76 | 2.41 |
現在差異顯而易見:
- 回測顯示PnL +42%——但在最差的5%場景中,PnL僅為+18.3%
- 回測顯示MaxDD -12%——但在最差的5%場景中,回撤達-23.4%
- Sharpe 1.8——但下界為1.12
**第5百分位是你的"現實最壞情況"。**如果策略在第5百分位就不再盈利,那麼將其部署到生產環境是有風險的。
視覺化:扇形圖
蒙特卡洛自助法自然地視覺化為扇形圖——權益曲線的扇形展開:
import matplotlib.pyplot as plt
fig, axes = plt.subplots(1, 2, figsize=(16, 6))
ax = axes[0]
for i in range(min(500, n_simulations)):
sampled = np.random.choice(trade_returns, size=len(trade_returns), replace=True)
equity = np.cumprod(1 + sampled)
ax.plot(equity, alpha=0.02, color='#4FC3F7')
all_equities = []
for _ in range(n_simulations):
sampled = np.random.choice(trade_returns, size=len(trade_returns), replace=True)
equity = np.cumprod(1 + sampled)
all_equities.append(equity)
all_equities = np.array(all_equities)
p5 = np.percentile(all_equities, 5, axis=0)
p50 = np.percentile(all_equities, 50, axis=0)
p95 = np.percentile(all_equities, 95, axis=0)
ax.fill_between(range(len(p5)), p5, p95, alpha=0.3, color='#7C4DFF', label='90% CI')
ax.plot(p50, color='#E040FB', linewidth=2, label='中位数')
ax.set_title('蒙特卡洛自助法:权益曲线')
ax.legend()
ax = axes[1]
ax.hist(df['final_pnl'] * 100, bins=80, color='#4FC3F7', alpha=0.7, edgecolor='#1A237E')
ax.axvline(pnl_5 * 100, color='#FF5252', linestyle='--', label=f'第5百分位: {pnl_5:.1%}')
ax.axvline(pnl_50 * 100, color='#E040FB', linestyle='--', label=f'中位数: {pnl_50:.1%}')
ax.axvline(pnl_95 * 100, color='#69F0AE', linestyle='--', label=f'第95百分位: {pnl_95:.1%}')
ax.set_title('最终PnL分布')
ax.set_xlabel('PnL, %')
ax.legend()
plt.tight_layout()
plt.savefig('monte_carlo_fan_chart.png', dpi=150)
plt.show()
扇形圖直觀地展示了可能結果的離散程度。扇形窄表示策略穩定。扇形寬表示結果很大程度上取決於交易序列的"運氣"。
扇形圖(左)顯示了可能的權益軌跡範圍,直方圖(右)顯示了最終收益的機率密度分佈,標註了置信區間(5%、50%、95%)。
進階分析:破產機率
破產機率視覺化:存活的權益路徑(青色)向上彎曲,而破產的路徑(紅色)跌落至零權益懸崖之下。
自助法允許你回答一個關鍵問題:策略虧損X%資金的機率是多少?
ruin_threshold = -0.20
prob_ruin = (df['max_dd'] < ruin_threshold).mean()
print(f"P(MaxDD < -20%) = {prob_ruin:.1%}")
prob_loss = (df['final_pnl'] < 0).mean()
print(f"P(PnL < 0) = {prob_loss:.1%}")
worst_5pct = df['final_pnl'].quantile(0.05)
cvar = df[df['final_pnl'] <= worst_5pct]['final_pnl'].mean()
print(f"CVaR(5%) = {cvar:.1%}")
這些指標不可能從單次回測執行中獲得。而它們對於做出策略上線決策至關重要。
關於深度回撤為何在數學上是危險的以及收益不對稱性如何運作的更多內容,請閱讀我們的文章虧損與利潤的不對稱性。
經典自助法失效的情況
該方法有一些需要了解的侷限性。
收益的自相關性
經典自助法假設交易是獨立的。現實中往往並非如此——策略可能存在連續盈利和連續虧損的序列(streak)。如果自相關性顯著,請使用分塊自助法:
def block_bootstrap(returns, block_size=10, n_simulations=10000):
"""保留局部依赖结构的自助法。"""
n = len(returns)
results = []
for _ in range(n_simulations):
starts = np.random.randint(0, n - block_size + 1, size=n // block_size + 1)
sampled = np.concatenate([returns[s:s+block_size] for s in starts])[:n]
equity = np.cumprod(1 + sampled)
results.append({
"final_pnl": equity[-1] - 1,
"max_dd": max_drawdown(equity),
})
return pd.DataFrame(results)
分塊自助法保留了連續交易之間的區域性依賴性,為MaxDD提供了更真實的置信區間。
分塊自助法通過將交易序列分割成塊並有放回地重取樣,保留了塊內的自相關性。
市場非平穩性
自助法使用原始交易分佈。如果市場發生了結構性變化(例如波動率下降或流動性變化),歷史交易可能不具代表性。為此可以:
- 使用滾動視窗:僅對最近N筆交易進行自助抽樣
- 給予近期交易更大權重:加權自助法
- 按市場狀態分割資料,分別進行自助抽樣
交易數量過少
自助法在交易數 n > 30 時是可靠的。如果你只有10筆交易——再多的重取樣也無濟於事。491筆交易是優秀的樣本,結果值得信賴。
回測穩健性評估方法對比
| 方法 | 提供什麼 | 複雜度 | 時間 | 何時使用 |
|---|---|---|---|---|
| 單次回測 | 一個點估計 | 最低 | 秒級 | 絕不作為最終結果 |
| 前推分析 | 樣本外指標 | 中等 | 分鐘級 | 檢查過擬合 |
| 蒙特卡洛自助法 | 置信區間 | 最低 | 約2秒 | 上線前必做 |
| 蒙特卡洛路徑 | 新價格路徑 | 高 | 分鐘到小時 | 壓力測試 |
| 交叉驗證 | 各折的平均指標 | 中等 | 分鐘級 | 參數調優 |
蒙特卡洛自助法是唯一能在最短時間內提供完整風險全景的方法。
檢查清單:結果解讀
以下是我們推薦的蒙特卡洛自助法結果解讀方式:
可以部署到生產環境,如果:
- 第5百分位PnL為正數
- 第5百分位MaxDD在你的風險承受範圍內
- 破產機率 < 1%
- 第5百分位Sharpe > 0.5
需要改進,如果:
- 第5百分位PnL接近零
- 第5百分位MaxDD明顯差於第50百分位
- 扇形圖展開很寬——策略不穩定
不要部署,如果:
- 第5百分位PnL為負數
- 破產機率 > 5%
- Sharpe的置信區間包含0
我們在marketmaker.cc的經驗
在marketmaker.cc,我們開發了自己的回測引擎,蒙特卡洛自助法是我們流程中不可或缺的一部分。每個策略在獲准進行實盤交易之前都會自動經過自助法分析。
我們將自助法直接整合到回測引擎中:執行結束後,你得到的不僅是最終PnL,而是包含置信區間、扇形圖、破產機率以及分塊自助法與標準自助法對比的完整報告。這額外花費2-3秒——為了理解真實風險,這是微不足道的代價。
根據我們的經驗:大約30%的策略在單點估計下看起來很有吸引力,但在蒙特卡洛自助法後被篩除。它們的第5百分位PnL變為負數,或MaxDD不可接受。如果沒有自助法,這些策略就會進入生產環境,很可能會造成虧損。
結論
蒙特卡洛自助法只需約10行程式碼和約2秒計算時間。它將回測中的單個數字轉化為帶有置信區間的完整分佈。這可能是所有量化分析工具中投資回報率最高的:
- 最低成本:30分鐘即可實現
- 最大回報:理解策略的真實風險
- 無依賴:只需NumPy
如果你還沒有使用自助法——今天就把它加入你的流程。這是瞭解你能在多大程度上信任回測結果的唯一方法。
參考文獻
- Efron, B. — Bootstrap Methods: Another Look at the Jackknife (1979)
- Davison, A.C., Hinkley, D.V. — Bootstrap Methods and their Application (Cambridge)
- Aronson, D.R. — Evidence-Based Technical Analysis: Monte Carlo permutation
- QuantStart — Monte Carlo Simulation for Backtest Analysis
- Marcos Lopez de Prado — Advances in Financial Machine Learning, Chapter 12: Backtesting
- Kevin Davey — Building Winning Algorithmic Trading Systems: Monte Carlo Analysis
- NumPy — numpy.random.choice
引用
@software{soloviov2026montecarlobootstrap,
author = {Soloviov, Eugen},
title = {蒙特卡洛自助法:如何用10行代码获取回测的置信区间},
year = {2026},
url = {https://marketmaker.cc/ru/blog/post/monte-carlo-bootstrap-backtest},
version = {0.1.0},
description = {为什么回测的单点估计是一种危险的幻觉。蒙特卡洛自助法如何在2秒内计算出PnL和MaxDD的95\%置信区间,以及为什么这是策略上线前的必要步骤。}
}
Authors
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.