回测44%胜率、实盘却不到20%:共用信号模块只是第一步
AH-0063是这一批最值得留下的量化素材之一。 不是因为它展示了一个“30%收益策略”。 恰恰相反。 它展示了一个更有价值、也更真实的失败: 回放里看起来能跑,实盘一上就变样。 原作者写得很具体:同一套策略在K线回放和实盘系统里分别实现,回放胜率能到44%、收益率30%多;最近十几笔实盘却胜率不到20%。把真实成交记录重新拿回同一段K线回放,进场点、订单数量和胜率“完全就是两码事”。 这
回测44%胜率、实盘却不到20%:共用信号模块只是第一步
AH-0063是这一批最值得留下的量化素材之一。
不是因为它展示了一个“30%收益策略”。
恰恰相反。
它展示了一个更有价值、也更真实的失败:
回放里看起来能跑,实盘一上就变样。
原作者写得很具体:同一套策略在K线回放和实盘系统里分别实现,回放胜率能到44%、收益率30%多;最近十几笔实盘却胜率不到20%。把真实成交记录重新拿回同一段K线回放,进场点、订单数量和胜率“完全就是两码事”。
这个案例真正值得研究的不是策略。
而是:
> Backtest 和 Live 到底怎样才算“同一个系统”?
第一层问题:同一个策略,被写了两遍
原作者最后定位到一个非常典型的软件问题:
回放系统有一套信号逻辑。
实盘系统又写了一套“看起来一样”的信号逻辑。
业务规则只要复制两份,就会慢慢漂移。
一个阈值改了,另一边忘了。
一个时间边界用收盘价,另一边用最新tick。
一个函数对空数据返回None,另一边当成False。
最后你以为在验证:
“策略能不能赚钱?”
实际验证的是:
两套并不完全相同的程序。
因此,把信号计算抽成一个共享模块,是对的。
这是所谓 Single Source of Truth。
但这里必须加一句:
必要,不充分。
第二层问题:共享信号以后,实盘仍然可能和回测不同
原文在改成共用信号模块以后说:
“回放有效,实盘也就问题不大。”
这句话太乐观。
QuantConnect的当前官方文档专门把Live Reconciliation当成一项独立工作,因为即使代码一致,回测和实盘仍可能因为以下因素分叉:
Data
历史数据与实时数据可能不同。修订数据、缺失K线、时区、数据聚合方式、look-ahead bias,都会让同一逻辑看到不同输入。
Time
回测时间是离散推进。实盘数据是流式到达。
“15:00整判断一次”在回测里可能总能命中,在实盘里可能因为事件时间、网络和调度偏差错过。
Fill
回测成交是模拟。实盘成交由交易所/券商真实撮合。
限价单可能不成交、部分成交、滑点、排队;市场单也会受深度影响。
Fees / Slippage / Market Impact
回测如果没真实建模手续费、资金费、滑点、市场冲击,收益曲线天然更漂亮。State
程序重启以后:- 当前持仓;
- 未完成订单;
- 上一次信号;
- 冷却期;
- 已处理事件ID;
如果恢复不一致,会出现重复下单或漏单。
Broker / Exchange State
交易所的最小数量、精度、保证金、风控、API重试、网络异常,都属于实盘系统的一部分。所以正确架构不是:
> 回测和实盘共用 signal() → 完成。
而是:
> 同一决策核心 + 两套执行适配器 + 一个持续Reconciliation层。
一个更可靠的量化架构
Layer 1:Canonical Market Event
无论历史还是实时,先把输入标准化成相同事件结构:
- symbol
- timestamp
- open/high/low/close
- volume
- timeframe
- data_version
- source
- signal_id
- direction
- reason
- target_risk
- invalidation
- generated_at
- 信号数量;
- 信号时间;
- 方向;
- 目标仓位;
- order数量;
- fill时间;
- fill价格;
- fee;
- 最终持仓。
- 平均盈利;
- 平均亏损;
- payoff ratio;
- 最大回撤;
- tail loss;
- turnover;
- fees;
- exposure;
- sample size。
- 信号数应该是7;
- 第3个信号timestamp必须等于X;
- position_size必须等于Y;
- 无信号阶段不允许产生order;
- restart后重复事件不能二次下单。
- 回测信号数;
- 实盘信号数;
- matched signal数;
- 回测订单数;
- 实盘订单数;
- 匹配fill数;
- 平均回测fill;
- 平均实盘fill;
- 费用差异;
- signal parity;
- order parity;
- fill parity;
- 简化slippage差异;
不要让策略层直接知道“这是CSV还是WebSocket”。
Layer 2:Shared Signal Engine
策略只接受标准事件和标准状态。
输出不是“直接下单”,而是一个声明式信号:
回测和实盘共用这一层。
Layer 3:Risk Engine
仓位、最大损失、组合敞口、杠杆限制和禁开条件单独管理。
这层也要尽量共享。
Layer 4:Execution Adapter
回测adapter负责模拟fill。
实盘adapter负责真实API、订单状态、重试和取消。
这两层本来就不可能完全一样。
关键不是强行统一,而是让差异显式。
Layer 5:Ledger
每一个事件都记录:
input → signal → risk decision → order request → exchange response → fill → position
没有这条链,就很难知道“到底从哪一步开始不一样”。
Layer 6:Reconciliation
把同一时间段的实盘日志重新喂给OOS回测,对比:
不是看两条收益曲线大概像。
而是逐层找第一处divergence。
“胜率”本身也不是验收指标
原文用44% vs <20%作为最直观的异常。
这可以帮助发现问题,但不能拿胜率判断策略好坏。
44%胜率可能赚钱。
70%胜率也可能亏钱。
还要看:
源素材里的“30%多收益”也没有完整周期、最大回撤、手续费、滑点和样本外细节,因此不能被alphahole写成“已证明有效”。
这条的价值是工程失败证据,不是收益证据。
AI Agent写交易代码,最该防的是什么?
原作者提到Agent有时会“信誓旦旦说实现了”,但一回放发现完全不对。
这非常重要。
对资金系统而言,LLM输出的“已完成”永远不是验收。
你需要的是机器可以验证的contract。
例如:
给定固定输入事件序列:
这类测试比“让AI再检查一遍代码”可靠得多。
本批做了一个 Backtest-Live Reconciliation Lab
AH-0063网站里包含:
backtest_live_reconciliation_lab.html
它不跑策略,也不接交易所。
你可以填:
工具计算:
然后告诉你更应该先查哪一层。
这是一个调试工具,不是盈利工具。
最值得保存的一句话
如果回测赚钱、实盘亏钱,不要第一反应去改策略参数。
先问:
> 实盘真的执行了你回测的那套决策吗?
然后继续问第二句:
> 即使决策一样,真实世界的成交、数据、时间和状态有没有被模型化?
把信号模块统一,只解决第一句。
真正成熟的量化系统,还必须持续回答第二句。