📈 策略回测专题
目录
一、什么是回测
回测(Backtesting)是指:用历史数据模拟交易策略在过去的表现,评估策略的有效性、稳健性和风险特征的过程。
通俗地说:
"假设我在2024年1月1日就开始用这个策略交易,到今天我会赚多少钱?中间最多亏过多少?胜率是多少?"
回测是连接"理论策略"与"实盘交易"的桥梁。
| 不做回测的风险 | 做回测的好处 |
|---|---|
| 策略可能只是运气 | 验证策略是否真的有"优势" |
| 不知道最大亏损 | 了解最坏情况下会亏多少 |
| 参数选择凭感觉 | 科学优化参数,避免过拟合 |
| 心理没底,拿不住 | 增强信心,知道策略的"性格" |
| 真金白银做实验 | 用历史数据做实验,零成本 |
二、回测的核心指标
收益率类
- 总收益率(Total Return):策略整个周期的盈亏比例
- 年化收益率(Annualized Return):换算成年化后的收益率
- 超额收益(Alpha):策略收益 - 基准收益
风险类
- 最大回撤(Max Drawdown) :从峰值到谷底的最大亏损幅度
- 公式:MDD = max(Peak - Trough) / Peak
- 波动率(Volatility):收益率的标准差
- 夏普比率(Sharpe Ratio):(策略收益 - 无风险收益) / 波动率
交易质量类
- 胜率(Win Rate):盈利交易次数 / 总交易次数
- 盈亏比(Profit/Loss Ratio):平均盈利 / 平均亏损
- 交易次数(Trade Count):策略触发的买卖次数
三、回测的陷阱
过拟合(Overfitting)
策略在历史数据上表现极好,但未来失效。原因:参数调得太多,恰好适应了历史数据的噪声。防范:样本外测试、交叉验证、简化策略。
未来函数(Look-ahead Bias)
用"未来的信息"做"现在的决策"。例子:用当天的收盘价决定当天的买卖。防范:严格按时间顺序,只用已知信息。
幸存者偏差(Survivorship Bias)
只测试现在还活着的股票,忽略了已经退市的。结果:策略表现被人为高估。
交易成本忽略
回测时没算手续费、滑点、印花税。结果:实盘收益远低于回测。
四、兆易创新双均线策略回测
4.1 回测设置
| 参数 | 数值 |
|---|---|
| 初始资金 | ¥100,000 |
| 策略 | MA5/MA20 双均线 |
| 手续费 | 万3 |
| 滑点 | 0.1% |
4.2 回测结果
| 指标 | 数值 |
|---|---|
| 最终资产 | ¥165,264.78 |
| 策略总收益率 | +65.26% |
| 买入持有收益率 | +69.03% |
| 超额收益(Alpha) | -3.77% |
| 最大回撤 | -28.16% |
| 交易次数 | 2次 |
| 完成交易对 | 1对 |
| 胜率 | 100% |
4.3 交易明细
| 日期 | 类型 | 价格 | 说明 |
|---|---|---|---|
| 2026-05-18 | 买入 | ¥400.40 | 金叉▲买入 |
| 2026-07-09 | 卖出 | ¥662.83 | 死叉▼卖出 |
4.4 回测可视化

上图:策略净值(绿色)vs 买入持有(蓝色虚线),红色区域=策略跑输,绿色区域=策略跑赢
中图:回撤对比,策略最大回撤-28.2%,大盘最大回撤-44.9%
下图:日收益率分布,策略日收益更集中在0附近(空仓期无波动)
五、NVDA策略vs大盘
5.1 回测结果
| 指标 | 数值 |
|---|---|
| 策略最终净值 | $164,053.92 |
| 大盘最终净值 | $153,993.86 |
| 策略收益率 | +64.05% |
| 大盘收益率 | +53.99% |
| 超额收益 | +10.06% |
| 策略最大回撤 | -8.55% |
| 大盘最大回撤 | -36.0% |
| 交易次数 | 4次 |
| 胜率 | 100% |
5.2 策略vs大盘净值图

上图:绿色=策略净值,蓝色虚线=买入持有净值。绿色填充=策略跑赢区域,红色填充=策略跑输区域
下图:回撤对比,策略最大回撤仅-8.5%,而大盘回撤-36%,策略有效控制了风险
六、1年vs5年最大回撤对比

| 对比维度 | 1年 | 5年 |
|---|---|---|
| 最大回撤 | -63.8% | -65.4% |
| 包含周期 | 仅高位震荡回调 | 完整牛熊周期 |
| 信息完整性 | 局部视角 | 全局视角 |
结论:5年最大回撤更深,因为包含了2022年的大熊市和2025年的高位回调。我更能接受5年视角,虽然回撤深,但了解完整周期有助于坚持策略。
七、什么是胜率
胜率(Win Rate)= 盈利交易次数 / 总交易次数 × 100%
胜率反映的是策略"猜对方向"的概率,但高胜率不等于高收益:
| 场景 | 胜率 | 结果 |
|---|---|---|
| 高频小额盈利,低频大额亏损 | 90% | 可能最终亏损 |
| 低频大额盈利,高频小额亏损 | 40% | 可能最终盈利 |
关键公式:
期望收益 = 胜率 × 平均盈利 - (1-胜率) × 平均亏损
索普的启示:51%的胜率,配合1:1的盈亏比,长期就能盈利。胜率不是越高越好,关键是期望收益为正。
八、作业解答
作业1:NVDA策略跑赢还是跑输?
答案:策略跑赢大盘10.06个百分点!
策略收益率+64.05%,大盘收益率+53.99%。MA5/MA20策略在NVDA上产生了正超额收益,且最大回撤远小于大盘(-8.5% vs -36%)。
作业2:1年vs5年最大回撤哪个更深?
答案:5年最大回撤更深(-65.4% vs -63.8%)。
5年包含了完整的牛熊周期,2022年大熊市和2025年高位回调都被纳入。我更能接受5年视角,因为它展示了策略在极端情况下的表现,有助于建立合理预期。
作业3:三句话解释回测
回测就是用历史数据"彩排"你的交易策略,看看如果早几年用这个策略,能赚多少钱、最多亏多少,避免用真金白银去试错的笨办法。
九、本期学习总结与个人感悟
9.1 本期核心内容回顾
| 主题 | 关键收获 |
|---|---|
| 回测定义 | 用历史数据验证策略,零成本实验 |
| 核心指标 | 收益率、最大回撤、夏普比率、胜率 |
| 回测陷阱 | 过拟合、未来函数、幸存者偏差、交易成本 |
| 兆易创新回测 | 策略+65%,但跑输买入持有-3.77% |
| NVDA回测 | 策略+64%,跑赢大盘+10%,回撤仅-8.5% |
| 1年vs5年 | 长期视角回撤更深但更真实 |
9.2 个人感悟
-
回测不是万能的,但没有回测是万万不能的。回测不能保证未来收益,但能排除明显糟糕的策略。
-
胜率只是故事的一半。100%胜率听起来很美,但如果只交易了1次,样本量太小毫无意义。关键是期望收益为正。
-
回撤比收益更重要。NVDA策略收益+64%,但最大回撤仅-8.5%,这意味着我可以安心睡觉。相比之下,大盘-36%的回撤足以让人割肉离场。
-
参数周期影响巨大。MA5/MA20在兆易创新上跑输大盘,但在NVDA上跑赢。没有放之四海而皆准的参数,必须针对具体标的优化。
-
5年视角让我清醒。看到-65%的最大回撤,我明白了为什么量化投资需要严格的风险管理。这不是游戏,是真金白银。
十、完整代码附录
10.1 回测引擎核心代码
python
def backtest_ma_strategy(df, short_window, long_window, initial_capital=100000,
commission_rate=0.0003, slippage=0.001):
data = df.copy()
data['short_ma'] = data['收盘'].rolling(window=short_window).mean()
data['long_ma'] = data['收盘'].rolling(window=long_window).mean()
data['signal'] = 0
data.loc[data.index[long_window:], 'signal'] = np.where(
data['short_ma'].iloc[long_window:] > data['long_ma'].iloc[long_window:], 1, 0
)
data['position_change'] = data['signal'].diff()
capital = initial_capital
position = 0
trades = []
daily_values = []
for i in range(len(data)):
row = data.iloc[i]
price = row['收盘']
signal_change = row['position_change']
if signal_change == 1 and position == 0:
buy_price = price * (1 + slippage)
max_shares = int(capital * (1 - commission_rate) / buy_price)
if max_shares > 0:
cost = max_shares * buy_price * (1 + commission_rate)
capital -= cost
position = max_shares
trades.append({'date': row['日期'], 'type': '买入',
'price': buy_price, 'shares': max_shares})
elif signal_change == -1 and position > 0:
sell_price = price * (1 - slippage)
revenue = position * sell_price * (1 - commission_rate)
capital += revenue
trades.append({'date': row['日期'], 'type': '卖出',
'price': sell_price, 'shares': position})
position = 0
stock_value = position * price
daily_values.append({
'date': row['日期'], 'total_value': capital + stock_value,
'position': position
})
values_df = pd.DataFrame(daily_values)
total_return = (values_df['total_value'].iloc[-1] / initial_capital - 1) * 100
peak = values_df['total_value'].cummax()
drawdown = (values_df['total_value'] - peak) / peak * 100
max_drawdown = drawdown.min()
return {
'total_return': total_return,
'max_drawdown': max_drawdown,
'trades_df': pd.DataFrame(trades),
'values_df': values_df
}