发布于VeighNa社区公众号【vnpy-community】
原文作者:Fusion小助手 | 发布时间:2026-10-25
上一篇我们对照着策略说明书,把移动止损的代码检查了一遍。如果代码里漏掉了最高价、最低价的持续更新,移动止损就会退化成固定止损。但麻烦的是,不管写成哪一种,Python 都不会报错,回测也能正常跑出完整的统计报告。
用 AI 辅助编写策略时,特别需要注意的就是这类情况:语法或接口报错很容易发现,反倒是那些能顺利运行、但交易逻辑写偏了的代码不容易察觉。
AI 生成的策略代码,尤其危险的不是跑不起来的,而是跑得起来但逻辑是错的。
拿到一份生成好的策略代码后,除了看它能不能运行,具体应该检查哪些地方?结合平时在社区答疑中常遇到的实际问题,我们整理了五个重点排查方向。
排查 AI 策略代码的五个重点
1. 代码写的和说明书想的不一致
第 4 篇讲的其实就是这个问题。比如移动止损写成了固定止损、方向过滤条件里的“大于”写成了“大于等于”、原本要求挂停止单在价格触及轨道时入场,代码里却写成了等 K 线收盘突破后再发限价单。
这些写法在语法上完全合规,代码检查工具不会报错,因为工具并不知道你原本打算怎么交易。要找出这类偏差,只能拿着上一篇写好的策略逻辑说明书,对照代码里的五个部分逐条核对。
2. 指标参数传错,或历史数据预热不够
首先是指标函数传错了窗口参数。比如计算 ATR 止损距离时,错误传入了布林带的窗口周期:
# 反面示例:算 ATR 却传了布林带的窗口参数
self.atr_value = self.am.atr(int(self.boll_window)) # 应传入 self.atr_window
因为两个参数都是整数,程序照样能运行,但算出来的止损宽度已经不是设定的 14 周期 ATR 了。看代码时,需要把 calculate_indicators 里每个指标的入参名字逐个核对一遍。
另一个常见问题出在 ArrayManager 的初始化预热与容量设置上,这里有三处细节需要同时对上。
第一,不能漏写 if not self.am.inited: return。ArrayManager 默认容量(size)为 100,初始化时底层数组填满了 0.0,要塞满 100 根 K 线后 inited 才会变为 True。如果漏掉这行保护,在数组里还有大量 0.0 时就开始算指标,算出来的轨道和止损价会严重失真。
第二,指标窗口不能超过 ArrayManager 的容量。如果策略把某个指标周期设成了 120,却忘了把 ArrayManager(size=100) 的容量一起调大,底层指标库因为样本凑不够 120 根,每次都会返回 nan(空值)。在 Python 中,nan 与任何数字比较大小结果都是 False,条件永远不成立,策略就会一直空仓且不报错。
第三,还要检查 on_init 里的 self.load_bar(10) 够不够填满这 100 根缓存。本案例用 1 分钟合成 15 分钟 K 线,预加载 10 天大约能合成一百五六十根,刚好够用;但如果改成 1 小时周期,或把天数改小成 load_bar(3),实盘启动后合成出来的 K 线凑不够 100 根,am.inited 会连续几天保持为 False,同样会导致策略一直不发单。
3. 状态变量漏了分支,回测正常、实盘出错
策略运行中需要靠内部变量来记录极值和持仓状态,只要漏掉一个分支,实盘状态就会出错。
以第 4 篇里的成交回调函数 on_trade 为例,当时生成的代码在平仓重置最高价、最低价时,是把平仓枚举值列出来判断的:
elif trade.offset in (Offset.CLOSE, Offset.CLOSETODAY):
self.intra_trade_high = 0.0
self.intra_trade_low = 0.0
在 VeighNa 中,开仓只有一个枚举值 Offset.OPEN,但平仓由于国内交易所规则不同,有普通平仓(CLOSE)、平今(CLOSETODAY)、平昨(CLOSEYESTERDAY)多个枚举值。AI 生成代码时常会逐个列举平仓类型,有时只写一个 Offset.CLOSE,有时像上面这样写了两个,结果把“平昨”漏掉了。
回测引擎平仓时不区分今昨仓,统一返回 Offset.CLOSE,所以在回测里跑几年数据都完全正常;可一旦到了上期所实盘环境,平掉隔夜持仓时柜台返回 Offset.CLOSEYESTERDAY,这行重置代码就不会执行,极值变量里会一直残留着上一笔交易的旧数值。如果策略依赖“极值是否为 0”来判断初始化,或者在分批成交时用 max 更新极值,下一笔交易的止损价就会算错。
一笔期货成交回报除了开仓就是平仓。既然开仓只有 Offset.OPEN 一个值,判断平仓更稳妥的写法是直接判断“不等于 Offset.OPEN”(elif trade.offset != Offset.OPEN:),这样无论在哪个交易所、平今还是平昨,都不会遗漏分支。
类似的还有委托清理:每走完一根 15 分钟 K 线去更新停止单时,如果前面忘了写 self.cancel_all() 先撤掉上一轮的旧委托,未成交委托就会越积越多。此外还要注意,代码里的 self.pos 是策略内部维护的逻辑净持仓,实盘如果有做过手动平仓操作,策略记录的 pos 就可能与账户实际持仓不一致。
4. 未来函数与下单时序:T 时刻下的单,什么时候才撮合?
未来函数指的是在下单决策那一刻,引用了当时尚未发生、必须等整根 K 线走完才能确定的信息。比如很多人在自己写简单的回测脚本时,常会写出类似下面这样的逻辑:
# 反面示例:看到当根 K 线最高价突破上轨,却填入这根 K 线的开盘价去下单
if bar.high_price > self.boll_up:
self.buy(bar.open_price, 1)
一根 15 分钟 K 线的开盘价在第 1 分钟就已经确定,而最高价(high_price)要等 15 分钟全部走完才知道。如果回测程序直接按当根 K 线的 open_price 撮合成交,就相当于先看到 15 分钟走完的结果,再倒回去按 15 分钟前的开盘价买入,回测结果自然会失真。
不过在 VeighNa 的 vnpy_ctastrategy 回测引擎中,哪怕 AI 写出了上面这段代码,也不会产生未来函数,顶多只会导致委托无法成交。
原因在于 VeighNa 采用的是事件驱动回测机制:每推进一根新 K 线,引擎固定先用当前 K 线的价格去撮合此前挂出的未成交委托,撮合完成后才调用策略的 on_bar(或 on_15min_bar)回调。
也就是说,策略在 T 时刻 K 线回调里发出的任何委托,只会在 T+1 时刻及之后的 K 线里参与撮合。当第 T 根阳线已经突破上轨走完时,它的 open_price 通常明显低于当前价格;此时发出的低价买单到了第 T+1 根 K 线,只会挂在盘口下方无法成交。这种机制从底层时序上直接避免了单根 K 线内的未来函数。
理解了“T 时刻下单、T+1 时刻才撮合”,检查生成代码时就可以重点核对两件事:
一是如果希望在盘中价格触及上轨时入场,代码必须像第 4 篇那样,在上一根 K 线收盘时提前把 self.boll_up 作为触发价挂出停止单(stop=True),在下一根 K 线盘中触发,而不能等突破走完才拿旧价格去挂限价单;二是如果策略额外加载外部数据,要检查有没有把当前 K 线时间点之后的数据混进来。
5. 把回测里的理想撮合当成了实盘成交
回测引擎里的撮合是一个简化模型。对于限价单,只要后续 K 线的最低价触及或低于买入限价(或最高价触及或高于卖出限价),回测就默认全部成交,既不考虑盘口排队,也不看当时是否有足够的对手盘;遇到涨跌停封死、盘口有价无量时,回测里同样可能判定为成交。
AI 生成突破策略时,有时会在 K 线刚走完时直接写 self.buy(bar.close_price, 1),也就是拿刚收盘的价格发限价单。回测里只要下一根 K 线的最低价碰到这个收盘价,委托就能立刻成交;但到了实盘,限价单需要排队,遇到顺势突破行情时价格跳动很快,按上一根收盘价发出的买单常会挂在下方无法成交,反倒是行情假突破、掉头回落时才会成交。
顺带提一句,vnpy_ctastrategy 模块只支持限价单(默认 stop=False)和停止单(stop=True),并没有提供市价单接口。这是因为国内各家期货交易所对市价单指令的支持不统一,而且在流动性不足或极端行情下,直接发市价单很容易产生过大的滑点。
在实际开发中,如果策略确实需要在信号出现时尽快成交,推荐用**“超价限价单”**来替代市价单——例如买入时在 bar.close_price 基础上主动加上几个最小变动价位发限价单,既能提高成交概率,又能把最大滑点控制在预期范围内。
因此审查代码时,要看清下单用的是限价单还是停止单,以及限价单的委托价格有没有预留合理的超价空间。
拿到代码后的八项核对清单
把上面五个方向汇总起来,拿到一份生成的策略代码后,可以顺着从上往下的读代码顺序逐项检查:
- 策略说明书里的每一条规则,在代码里都能找到对应位置吗?
- 判断条件的边界是怎么写的:是大于还是大于等于,方向不明和已有持仓时如何处理?
calculate_indicators里每个指标传入的窗口参数,是不是它自己的参数名?- 算指标前有没有
if not self.am.inited: return?指标最大窗口是否小于ArrayManager容量,且load_bar预加载天数够不够填满合成后的缓存? - 下单写法是否符合“T 时刻下单、T+1 时刻撮合”的时序:盘中突破触发有没有在上一根 K 线提前挂出
stop=True停止单? - 发单用的是限价单还是停止单?如果用限价单追单,委托价格有没有做合理的超价处理?
- 每次挂新的入场单或更新止损单之前,有没有先调用
cancel_all()撤掉未成交的旧委托? - 状态变量在所有成交分支下都更新了吗,平仓判断有没有直接使用
!= Offset.OPEN来覆盖全部平仓类型?
如果核对时发现有对不上的地方,先把代码修改准确,再进入后面的历史回测。
在 Fusion 里怎么配合做代码检查
上面这八项查的是交易逻辑,需要人工把关;而语法错误、格式混乱、变量类型写错这类技术问题,可以由软件在回测前自动完成检查。
在 VeighNa Fusion 的智策投研里,生成策略代码后不会直接跳去跑回测,中间有一道“代码检查”环节:检查不通过就会退回上一步自动修复,全部通过后才进入回测。

这一步在后台依次运行四个静态检查工具:ruff format 整理代码格式,py_compile 检查语法能否正常编译,ruff check 检查代码规范,mypy 检查类型标注。
这四项检查主要用来确认代码能否正常加载和运行,不会校验具体的交易逻辑。通过软件的静态检查后,仍然需要对照上面的清单人工核对交易规则。
第一步,在智策工作台等流程走到“代码检查”,查看四项静态检查的结果。

第二步,如果其中某一项报错,流程会自动回到代码生成环节做修复,改完后再重新检查。

第三步,四项检查全部通过后,流程才会继续进入历史回测。
本篇小结
- 代码能跑通回测,只代表语法和接口没报错,不代表交易逻辑写对了。
- 审查代码时重点看五处:规则有没有写走样、指标参数与
load_bar预热天数够不够、平仓状态重置有没有覆盖全部开平分支(建议用!= Offset.OPEN)、下单写法是否匹配“T 时刻下单、T+1 时刻撮合”的事件驱动时序,以及限价单超价与撮合假设是否合理。 - 智策投研里的“代码检查”负责拦截语法、格式和类型问题,交易逻辑仍需对照清单人工核对。
接下来讲什么
代码检查通过了,逻辑也核对无误,下一步就是运行回测。不过回测报告里的数字同样需要甄别:如果区间挑得巧、手续费和滑点设得太乐观,再平庸的策略也能跑出一条好看的资金曲线。
下一篇:回测很好看,先别急着相信。
VeighNa Fusion 是韦纳软件面向期货公司推出的标准化期货量化交易平台,本系列文中演示均基于 Fusion 内置功能完成,如希望了解详情,请扫描下方二维码添加小助手交流:
