跳到正文

研究指南

股票回测与实验

股票信号为何晚一天成交:核对策略shift与引擎次日开盘对齐

诊断股票次开盘成交,应把一次决策依次追踪到三条序列:基于收盘的原始条件、策略实际返回值、引擎最终可执行目标。策略里已经移动过的输出看起来没有前视,引擎却可能再移动一次;反过来,删除所有移动又可能让收盘之后才知道的条件,在当天更早的开盘成交。

本文用五条合成bar和两套完整现金账户展示差异。代码是作者独立编写的对齐复现,不是实际运行Vibe或CoTrading的测试。真实产品的信号语义、移动位置和撮合实现,仍需查看并实际执行产品自身记录后才能确认,不能从本例推断已经完成联调。

先把信息时间写成可验收契约

原始条件是目标多头状态:完整收盘严格大于9.5时为一,否则为零。一表示持有四十股,不是每天再买四十股;零表示空仓,不是无论现有库存多少都卖四十股。首个开盘没有提供更早收盘,所以账户从空仓开始,不能借用第一条收盘预先建立仓位。

对索引i的收盘条件,本契约最早允许索引i加一的开盘成交。样例引擎固定把策略返回值延迟一条。如果策略直接返回原始条件,总延迟为一;如果策略内部也延迟一次,经过相同引擎后总延迟为二。两种做法都可能没有同开盘读取当日收盘,但实现的是不同的交易时点。

不能仅凭列名signal或position猜测契约。要确认函数返回的是目标状态、进入事件、带方向订单,还是已经可直接成交的目标。还需检查下游按时间标签还是数组位置对齐,以及索引是否又被移动。局部操作看起来安全,并不能在上下游语义未知时证明整条链路正确。

五条开盘依次为10、11、15、9、8,完整收盘为10、12、8、9、8,所以原始条件为一、一、零、零、零。开收盘故意不同,使对齐差异可以反映到实际现金。如果把信号收盘也当成交价格,就容易掩盖延迟错误带来的成本,甚至把不可在当时成交的价写进账户。

单次延迟账户逐行追踪

策略内部不移动时,引擎目标为零、一、一、零、零。索引零开盘不下单,索引一的目标来自索引零收盘,以11买入四十股。账户起始一千,支付本金440及费用一,现金559,持仓四十股,累计费用一。不能在第一条开盘以10买入,因为相关收盘尚未发生。

索引二开盘的目标仍是一,来自索引一收盘。账户已经有四十股,所以没有新买单。这里能够直接区分目标状态与进入事件:若每个一都再买四十,就会变成八十股,消耗额外资金,并引入原规格没有允许的加仓行为。只看信号图可能看不出这类语义错误。

索引二收盘为8,原始条件转零,经过引擎一次延迟,在索引三开盘执行退出。四十股以9卖出,扣费净收359,最终现金918、持仓零、累计费用二。现金桥是一千减441加359,价格损失八十,扣费后净损失八十二,不能只报告一个不含费用的价格变化。

每行还输出source_close_index,首开盘为空,之后依次为零、一、二、三。相比笼统说“已经shift”,这个字段明确指出每个可执行目标究竟来自哪条观测。调试真实引擎时应保留该字段或等价的决策编号,让信号时间和实际订单能够对应。

重现策略与引擎重复移动

第二套账户把相同原始条件先在策略里延迟一条,再经过引擎固定延迟,最终目标变成零、零、一、一、零。买入移到索引二开盘15,四十股含费支出601,剩余现金399。索引三仍然持有,因为索引二收盘的零还未到达执行层。

索引四开盘以8卖出四十股,扣费净收319,最终现金718、持仓零、累计费用二。相对单次延迟账户多损失二百,来自买入价15而非11、卖出价8而非9,并不是多收佣金,也不是原始条件改变。两套账户在相同输入下隔离了时间处理的影响。

这不能证明一次延迟总能获得更好收益;另一条价格路径可能反过来。正确性来自预先声明的信息时间与执行契约,不能事后选赚钱的延迟。如果研究本来就要求等待两个开盘,应明确写成该规则并独立测试,而不是把它称作错误后又因结果较好选择保留。

独立检查同日收盘泄漏

保持开盘不变,把索引二收盘从8改到1000,原始条件立即改变,但截至索引二开盘的订单、现金与股数必须全部不变。该开盘早于该日收盘,一旦同开盘受影响,就违反了收盘条件驱动次开盘的契约。这项扰动直接测试信息时间,而不是从最终收益间接猜测是否存在前视。

到了索引三开盘,改变后的条件允许生效,单次延迟账户不再卖出。第二个断言同样必要:如果只要求过去不变,一个完全忽略所有信号的程序也可能通过。过去保持一致、最早合格未来确实发生变化,两者共同证明信号既没有提前使用,也没有永久丢失。

只运行前三条bar时,账本应与完整运行的前三行完全一致,账户仍持仓,不因数据刚好结束而强制清仓。否则末行退出会污染原本用于检查对齐的前缀。修改未来价格与截断数据是不同路径的测试,分别检验未来值影响和数据边界行为,不能只用其中一个就宣布所有前视问题已解决。

对齐正确与真实成交分别核实

对齐正确的订单也不能证明真实市场一定按给定开盘成交。Investor.gov订单说明区分订单要求与成交价格保证。本独立夹具假设开盘可执行、有限且为正,每笔实际完成订单收费一单位,没有实现开盘集合竞价、限价、部分成交或市场冲击。

样例拒绝非法开盘和买入资金不足,输入长度必须一致。收盘序列是受控数值夹具,不是通用缺失数据接入层。真实集成应先验证价格、时间戳和字段质量,再追踪对齐;移动检查通过不能成为接受NaN收盘或错时区行情的理由。数据质量与时间边界是独立验收环节。

实际调试时,应冻结生效参数、数据身份、原始条件、策略返回值、引擎目标和真实订单,先找最早出现差异的索引。还没定位就直接修改策略移动,可能只让一个窗口碰巧吻合,却在别处重复延迟或删除必要延迟。小样本逐行追踪的价值,是让差异发生在哪一层变得可解释。

运行完整例子并核对结果

下载完整示例,在Python 3.12或更新版本执行python R596.py。只依赖标准库,使用合成bar。输出两套账户每一行的条件、目标、来源收盘索引、订单、现金、库存、累计费用与按给定开盘估值的权益,不连接行情或交易服务。

精确断言要求两套买入分别发生在索引一和二,卖出分别在三和四,最终现金918与718,两者清仓且费用二。这些值是可重现机制,不是历史表现。真实产品只有在自身契约被核实、实际运行产生等价追踪以后,才可以另行声称支持;不能把本脚本替代成产品运行证据。

估值变化不能冒充订单变化

账本在处理当次开盘订单之后,以该开盘给现有持仓估值;估值本身不用于生成当次目标。单次延迟账户在索引二按15估值会看到更高持仓市值,但仍然只有四十股,没有新买入。权益变化与股数变化应分别检查,特别是图表把组合曲线、策略输出和成交标记画在一起时,不能因曲线跳动就推断发生了交易。

核验真实引擎还应记录每个时间戳代表开始、结束还是数据标签。某份日线以日期命名,不表示其中收盘在当天零点可知;信号序列与价格序列标签相同,也不保证信息先后相同。把来源收盘索引和订单执行索引一并导出,可以在不改变策略条件的情况下定位对齐层。本文只支持内部移动零或一,其他请求直接拒绝,不能把未测试任意延迟默认为可用。未来扩展多市场或时区时,仍须重建各市场会话与数据可得顺序,而不是只沿用数组位置。

常见问题

策略内部移动是否一定错误?

不一定,取决于引擎期望。本例引擎已经延迟一次,再加一次就把次开盘规则改成了两个开盘之后。

正确延迟能否保证真实开盘成交价?

不能。时间上合格和真实成交是两件事,本例假设给定开盘可执行,不证明集合竞价或券商行为。

相关阅读