Pine Script security策略回测消除1bar延迟的合规实现方法咨询
问题背景
我多次阅读了security() revisited [PineCoders]和How to avoid repainting when using security() - PineCoders FAQ两篇文档后,仍对如何正确将security函数返回的元组数据与当前图表时间帧的数据结合使用存在疑问。
经PineCoders团队推荐,我向资深开发者请教后拿到了如下处理元组的security封装函数及规则:
f_secWrapper2Tuple( _symbol, _resolution, _data1, _data2 ) => security( _symbol, _resolution, [ _data1[1], _data2[1] ], lookahead = true )
- 正常调用指标函数获取无延迟的原始数据,比如基础RSI计算结果
- 将原始数据直接传入security函数,由security自动施加1根K线的延迟
- 绘图或使用当前图表时间帧的非security原始数据判定买入信号前,手动加1根K线延迟;security返回的数据已内置延迟,无需额外处理
- 由于买入条件使用的前K线数据永远不会重绘,因此使用once per bar模式在K线开盘时生成警报,不要使用once per bar close模式。
现有方案表现
指标脚本(study)表现符合预期
我对所有原始数据都施加1根K线延迟,无论是否与security函数的数据共同构成买入信号。这套方案在指标脚本中生成警报的效果很好,调用更高时间帧(HTF)数据时无任何延迟:如果脚本运行在1小时低时间帧(LTF),结合日线HTF数据时,HTF条件在当日K线开盘时判定为真(通过1K延迟校验前一根已收盘HTF K线),LTF在当日00:00就会同步触发信号,不会额外延迟1根K线。
这套方案在指标脚本中生成警报的表现非常理想,只要K线开盘时条件满足,警报就会触发。对应截图如下:
策略脚本(strategy)回测存在1K延迟
但我在策略脚本中回测这套逻辑时,会产生1根LTF K线的延迟,相同的买入信号直到1小时后的19:00才会触发:
我单独测试了仅使用当前时间帧信号(无security调用)的场景,对比不读取前K线数据、使用once per bar close生成警报的脚本,发现1K延迟会让2020年1月1日起的回测净利润从约3000%下降到约2000%。我尝试开启calc_on_every_tick和calc_on_order_fills参数,也没有解决问题。
我有三个核心疑问:
- 回测的用法是否存在错误?
- 如果用法正确,有没有可行的 workaround 可以让回测时买入订单在正确的K线开盘时成交?
- 是否可以在回测时移除security和当前时间帧数据的1K延迟,或者使用其他security函数写法,让回测性能结果与实盘运行的指标脚本(施加1K延迟、使用once per bar警报)的预期表现一致?我在指标脚本中使用security的方式是否也适用于策略回测?
补充示例代码
指标版本(运行正常)
//@version=4 study( "SMA Security Demo (study)", overlay = true ) i_useSecurity = input ( true, "Use security", type = input.bool ) i_symbol = input( title="Symbol", type = input.symbol, defval = "") i_resolution = input( title="Resolution", type = input.resolution, defval = "" ) // 1 is what I use for study scripts to send alerts on once per bar (open) i_realtimeOffset = input( title = "Reatime Offset", defval = 1 , options = [ 1, 0 ] ) // 1 is what I use as 0 means using repainting realtime bar data and I am not trying to do this i_securityOffset = input( title = "Reatime Offset", defval = 1 , options = [ 1, 0 ] ) f_secWrapper( _symbol, _resolution, _data1 ) => security( _symbol, _resolution, _data1[i_securityOffset], lookahead = true ) sma50Raw = sma( close, 50 ) sma100Raw = sma( close, 100 ) smaBuyRaw = sma50Raw > sma100Raw smaBuySec = f_secWrapper( syminfo.tickerid, "240", smaBuyRaw ) buy = smaBuyRaw[i_realtimeOffset] and ( i_useSecurity ? smaBuySec : true ) plot( sma50Raw, color = color.yellow ) plot( sma100Raw, color = color.blue ) bgcolor( buy ? color.green : color.red )
上方指标脚本运行在1小时周期的上层图表中,当本周期黄色SMA50上穿蓝色SMA100后,且开启use security开关时,会同步校验4小时周期的相同条件,满足后触发买入信号。我使用的实时偏移参数为1,背景从红转绿即代表买入信号触发,由于始终引用前一根已收盘K线的数据,在当前K线开盘时入场没有repainting风险,警报使用once per bar而非once per bar close模式生成。下层的4小时周期图表仅运行了同条件的背景变色脚本作为参考。
从上图可以看到,读取4小时HTF数据没有任何延迟:当UTC时间18:00新的1小时和4小时K线同时开盘时,两个周期的条件首次同时满足(上次卖出信号后)。原因是1小时周期的前K线已经满足SMA50大于SMA100的条件,而4小时周期的前K线刚好在收盘时首次满足该条件。
策略版本(存在1K延迟)
//@version=4 strategy("SMA Security Demo (strategy)", overlay=true, margin_long=100, margin_short=100) i_useSecurity = input ( true, "Use security", type = input.bool ) i_symbol = input( title="Symbol", type = input.symbol, defval = "") i_resolution = input( title="Resolution", type = input.resolution, defval = "" ) // 1 is what I use for study scripts to send alerts on once per bar (open) i_realtimeOffset = input( title = "Reatime Offset", defval = 1 , options = [ 1, 0 ] ) // 1 is what I use as 0 means using repainting realtime bar data and I am not trying to do this i_securityOffset = input( title = "Security Offset", defval = 1 , options = [ 1, 0 ] ) f_secWrapper( _symbol, _resolution, _data1 ) => security( _symbol, _resolution, _data1[i_securityOffset], lookahead = true ) sma50Raw = sma( close, 50 ) sma100Raw = sma( close, 100 ) smaBuyRaw = sma50Raw > sma100Raw smaBuySec = f_secWrapper( i_symbol, i_resolution, smaBuyRaw ) buy = smaBuyRaw[i_realtimeOffset] and ( i_useSecurity ? smaBuySec : true ) plot( sma50Raw, color = color.yellow ) plot( sma100Raw, color = color.blue ) bgcolor( buy ? color.new( color.lime, 75 ) : color.new( color.red, 75) ) if buy strategy.entry("buy", strategy.long ) else strategy.entry("sell", strategy.short )
使用相同代码时会出现1根K线的延迟,如下图所示:
我在代码中增加了偏移参数选项,不仅支持调整实时数据偏移,也支持调整security数据偏移——尽管后者会用到重绘数据,这不是我想要的效果。
将实时偏移设为0不会改变策略订单的触发位置,订单仍在UTC 18:00才触发。如果将security偏移设为0,由于用到了重绘数据,条件会提前4小时在UTC 08:00就判定为真,但策略仍然延迟到下一根K线才下单。
我希望找到支持元组的security函数写法,在指标脚本中实现无延迟不重绘的HTF数据调用,同时不需要对LTF/当前图表数据施加延迟,还能支持once per bar close警报和正常回测。
问题原因与解决方案
延迟的核心原因
TradingView策略默认的订单成交规则是:当K线运行过程中条件触发时,默认会在下一根K线的开盘价成交,避免用到未来数据;而指标的bgcolor是在当前K线开盘就可以根据历史数据渲染,两者的触发逻辑本身就存在时序差。你使用的lookahead=true参数在指标端可以正确读取前序HTF K线的已收盘数据,但在策略回测时,系统会自动规避前瞻逻辑,额外多推了一根K线的成交时机。
可行修复方案
- 调整策略的成交时机参数:在strategy声明中增加
process_orders_on_close=true参数,同时把i_realtimeOffset设为0,i_securityOffset保持为1。这个参数会让策略在当前K线收盘时就判定条件并提交订单,成交价格用下一根K线的开盘价,刚好匹配你实盘开盘触发警报入场的逻辑,不会产生额外延迟。 - security函数的写法不需要改动,原来带
lookahead=true且传入偏移1的写法本身不会产生重绘,在调整策略参数后可以直接复用,同时支持元组返回值。 - 不需要修改alert的触发逻辑,实盘仍然用once per bar模式在开盘触发警报,回测结果和实盘表现可以完全对齐。
修改后的策略核心代码示例
// 修改strategy声明,添加process_orders_on_close=true strategy("SMA Security Demo (strategy)", overlay=true, margin_long=100, margin_short=100, process_orders_on_close=true) // 其他代码不变,调整输入参数默认值即可 i_realtimeOffset = input( title = "Reatime Offset", defval = 0 , options = [ 1, 0 ] ) i_securityOffset = input( title = "Security Offset", defval = 1 , options = [ 1, 0 ] )
内容的提问来源于stack exchange,提问作者saltyeggs

