程序员量化交易实战 12:回测不能只看最终收益
程序员量化交易实战 12:回测不能只看最终收益
古董级程序员,大厂出来后一直在创业公司,现在仍活跃在一线做 AI 相关的开发。这个专栏会把一个 A 股量化平台从 0 到 1 拆开写:数据、策略、回测、模拟盘、提醒和生产化,尽量用真实代码和真实运行结果说话。更完整的更新也会同步到微信公众号「字与码」。
第 11 篇有了多标的组合回测。现在如果只看 final_equity,很容易误判策略。
最终收益高,可能是中间回撤很深;胜率高,可能是小赚大亏;交易次数太少,可能只是偶然命中。第 12 篇要把这些指标统一成一组可复用的回测评价语言。

指标是策略验收语言
回测指标不是装饰图表。
当两个策略结果不一样时,我们要知道差异来自哪里:收益、波动、回撤、交易频率、胜率还是盈亏比。
常见指标可以先按用途分开看:
| 指标 | 主要回答的问题 |
|---|---|
| 总收益 / 年化收益 | 赚了多少,换算到一年大概是什么水平 |
| 年化波动 | 过程是否剧烈,收益是不是靠大幅波动换来的 |
| 最大回撤 | 从高点到低点最多亏过多少 |
| 胜率 | 已平仓交易里赚钱的比例 |
| 盈亏比 | 平均赚钱交易和平均亏钱交易的比例 |
| 换手率 | 策略交易是否过于频繁 |
这些指标没有哪个单独决定策略好坏。复盘时更常见的是组合判断:收益高但回撤深,可能不适合真实账户;胜率高但盈亏比很差,可能是小赚大亏;换手率高但收益一般,费用和滑点一加就可能失真。
新增指标对象
第 12 章新增 app/performance_metrics.py。
@dataclass(frozen=True)
class PerformanceMetrics:
total_return: float
annualized_return: float
annualized_volatility: float
sharpe_like: float | None
max_drawdown: float
win_rate: float | None
profit_loss_ratio: float | None
trade_count: int
turnover: float
这里叫 sharpe_like,不是严格金融教科书里的 Sharpe。因为当前还没有无风险利率输入,只是用年化收益除以年化波动,作为早期工程比较指标。
从权益曲线计算收益率
权益曲线先转成逐日收益:
def equity_returns(equity_curve: Iterable[dict[str, object]]) -> list[float]:
values = [float(row["equity"]) for row in equity_curve]
out: list[float] = []
for previous, current in zip(values, values[1:], strict=False):
if previous:
out.append(round(current / previous - 1, 8))
return out
有了逐日收益,年化波动就是样本标准差乘以 sqrt(252):
variance = sum((value - mean) ** 2 for value in returns) / (len(returns) - 1)
return round(math.sqrt(variance * 252), 6)
胜率要从成对交易算
单笔买入不产生盈亏,只有买入和卖出配对后才有交易盈亏。
def trade_pnls(trades: Iterable[MiniBacktestTrade]) -> list[float]:
pnls: list[float] = []
open_cost: float | None = None
for trade in trades:
if trade.side == "buy":
open_cost = trade.amount + trade.fee
elif trade.side == "sell" and open_cost is not None:
pnls.append(round(trade.amount - trade.fee - open_cost, 2))
open_cost = None
return pnls
这一步能避免把未平仓持仓误算成已实现胜率。
真实运行截图
第 12 篇继续使用同一个联动示例,直接读取第 11 篇组合回测的权益曲线和交易明细:
uv run python -m scripts.chapter_examples factor-backtest --source sample
真实输出如下:

这组样例的 total_return 是 0.025156,max_drawdown 是 -0.012924,turnover 是 0.39872。win_rate 和 profit_loss_ratio 是 None,因为样例里只有买入,没有完成买卖配对。这个细节很重要:没有平仓交易时,不应该硬算胜率,否则复盘会把浮盈浮亏和已实现盈亏混在一起。
本章更新与代码仓库
本章更新内容:
- 新增
app/performance_metrics.py。 - 实现总收益、年化收益、年化波动、类 Sharpe、胜率、盈亏比、换手率和交易次数。
- 新增
tests/test_performance_metrics.py,覆盖空曲线、交易配对和指标汇总。 - 补充当前主线联动示例的指标真实运行截图。
- 补充总收益、波动、回撤、胜率、盈亏比和换手率的复盘解释。
代码仓库:
https://github.com/ax2/zi-quant-platform
本章代码:
git clone https://github.com/ax2/zi-quant-platform.git
cd zi-quant-platform
git checkout chapter-12
uv sync --extra dev
uv run pytest tests/test_performance_metrics.py
第 12 章全量测试通过:176 passed,仍只有既有 FastAPI deprecation warning。
本篇小结
最终收益只是策略结果的一部分。
第 12 篇把回测评价指标变成可复用代码。后面做参数搜索和策略晋升时,不再靠“看起来收益更高”,而是用统一指标做比较。
微信公众号
欢迎关注「字与码」
如果这篇文章对你有用,也欢迎在微信里继续关注后续更新。