在量化交易的世界里,回测是策略从构想走向实战的必经之门。一道完美的回测资金曲线,是每个交易者梦寐以求的圣杯。然而,这条曲线往往隐藏着致命的幻觉——许多在回测中表现优异的策略,一旦投入实盘,便会失效甚至亏损。究其根源,“信号闪烁”与“未来函数偷价”是制造这些幻觉的两大元凶。只有彻底理解并根除它们,才能锻造出经得起市场考验的可靠策略。
信号闪烁,指的是在单根K线的形成过程中,交易信号在某个瞬间出现,但在该K线最终走完并收盘时,这个信号又消失了的现象。
其核心原因在于:策略逻辑错误地依赖于K线盘中不稳定的、瞬时的中间价格数据,如最新价(close),而不是一个稳定的图表价格价格。
典型场景还原:
//错误的写法
Events
OnBar(ArrayRef<Integer> indexs)
{
Numeric a = Highest(high[1], 5);
if(close > a)
{
buy(1,close);
}
}
黄金法则:永远基于已经确定的历史数据做决策。
//正确
Events
OnBar(ArrayRef<Integer> indexs)
{
Numeric a = Highest(high[1], 5);
if(high > a)
{
buy(1,max(open,a));
}
}
3. 审阅交易记录: 在回测报告中,仔细检查每一笔交易的开仓信号。问自己一个问题:“在这根K线彻底走完后,这个开仓信号是否依然成立?” 如果答案是否定的,那么你的策略就存在信号闪烁。
4. 进行逐笔数据回测: 在有条件的情况下,盘中或利用回放观察策略变化,有闪烁肉眼可以看到。
如果说信号闪烁是让你“抓不到”信号,那么偷价则是让你在回测中“作弊”,它制造出的完美盈利假象,危害性更大。
两者的共同本质: 都是在回测中开辟了一条“隧道”,让策略能够“先知先觉”,而在实盘中完全无法复现回测的辉煌战绩。
场景一:最低价偷价买入
//错误案例
Events
OnBar(ArrayRef<Integer> indexs)
{
buy(1,low);
sell(1,high);
}场景二:信号价格加入有利偏移
//错误
Events
OnBar(ArrayRef<Integer> indexs)
{
Numeric a = Highest(high[1], 5);
if(high > a)
{
buy(1,max(open,a)-10);
}
}在买入开仓上,减去一个额外的点差。相当于每次偷了一个固定金额。不要小看偷得少(比如1跳),积累下来曲线就会更好看。
核心原则:在回测的任何一个时间点,策略只能使用该时点及之前的历史数据。
“信号闪烁”与“未来函数偷价”是量化交易者在策略开发初期必须跨过的两道鸿沟。它们一个让策略信号变得虚无缥缈,无法捕捉;另一个则让策略绩效变得虚假繁荣,不堪一击。
要打造一个坚实可靠的策略,必须秉持着比法庭证据更严谨的态度对待回测:
最终,一个策略的价值不在于它在历史数据上能画出多美的曲线,而在于其逻辑的严谨性与在实盘中的复现能力。记住,在量化交易中,可靠性永远优先于回测收益率。 只有经得起“真实性”拷问的策略,才真正具备在残酷市场中生存并盈利的资格。
王老师,我看你的文章,感到更困惑了, 我看了这个视频:
https://video.tbquant.net/video?id=video412
https://video.tbquant.net/video?id=video444
介绍了为了解决信号闪烁问题尽量用固定价格, 还说用Open作为成交价是偷价行为, 那您这个文章总结如下:
核心原则:在回测的任何一个时间点,策略只能使用该时点及之前的历史数据。
这个应该和视频介绍的恰恰相反, 您推荐用Open作为成交价。我不知道您和视频说的到底哪个是对的? 另外自己尝试了一下, 用tb内置的策略, 里面大多都是Open作为成交价, 有些夏普可以达到3以上, 但是如果我改为close作为成交价, 夏普直接都是-10, 误差太大了。
所以您能否给讲解一下下列两种情况:
1. 如果我在OnbarClose中做最后一次计算和发单, 也不会有闪烁问题, 那应该用Open发单还是close发单?
2. 如果我在Onbar中计算指标和发单, 不考虑信号闪烁问题,那应该是用Open还是close发单?
3. 我用Open发单,是否tb默认就是在下一个bar的开盘价成交? 但是这样回测和实盘能否一致? 因为我发现我在OnbarClose中用Open发单(夏普是3)和Close发单(夏普是-10),误差也是很大的。
以上问题让我很困惑 , 能否给指导一下,我如何实现回测与实盘尽量一致,到底用哪个价格发单更合理。
注意在什么情况下用的open, 你用open必然也是在开盘的时刻下单
你在非K线开盘的位置下单用open,那也是错的
王老师👍
学习了