跳到正文

研究指南

股票回测与实验

同一份股票策略为什么重跑不同:逐层核对输入与输出

两次运行使用同一个策略文件名,仍可能执行了不同实验。行情可能被修订,解析后的配置可能不同,依赖版本可能变化,隐藏的执行设置也可能改变成交。先定位第一个出现差异的层级,再解释最终盈利为何变化。最终指标压缩了太多信息,不能单靠它猜原因。

本文比较的是已经冻结的股票程序。先保存生成代码再执行;重新请求外部模型生成一次代码,属于另一个阶段,不满足同一程序的比较前提。这里没有验证CoTrading行为。独立Python样例使用合成观测和刻意简化的执行规则,没有调用行情源、模型或券商。

明确两次运行声称共享什么

每次运行保存代码字节、校验后配置、数据字节、引擎构建、依赖版本、解释器、日历及执行政策。还应记录随机状态归属、工作线程数、相关时区设置及初始账户状态。必须引用进程真正消耗的值,不能只截图界面上的配置,然后假定覆盖顺序和默认值都已正确传入。

复现包不应包含凭证。重放已经捕获的合成或合法离线数据,不需要账户令牌。如果执行中仍会调用实时服务或外部模型,输入就没有完全冻结,应明确标为未冻结依赖。排查可能得到的结论是尚不满足确定性重放前提,而不是立即宣称引擎存在缺陷。

提前确定比较规则。日期、证券身份、数量、状态和信号方向通常要求精确相等。浮点值则需要带单位的容差。Python的isclose文档说明了相对和绝对容差。接近零时,仅用相对容差往往不够,应根据计算含义设置绝对界限,不能看到差额后才放宽门槛。

按因果顺序比对产物

比较顺序为实际数据、解析配置、声明运行环境、信号、成交和账户净值。较早的差异可能解释后面多个变化。发现成交不同,不能在信号输入已经变化时直接归因撮合器。首差异指当前记录手段下最早观察到的不一致,不证明上游没有隐藏依赖。

层级比较内容首个动作
数据数值、顺序、时间戳、复权版本定位第一条变化观测
配置生效参数、成本、初始状态追默认值和覆盖顺序
环境代码、引擎、依赖、随机政策重建记录中的环境
信号时点、证券、方向、数量比对生成信号的输入窗
成交信号引用、时间、价格、数量、费用用相同信号重放执行
净值现金、持仓、估值和聚合勾稽首次状态转移

除了摘要,也保留行数和稳定事件键。摘要能够提示序列化内容是否不同,却不能指出究竟哪条记录变化。hashlib官方文档提供本例的摘要机制;分层顺序和比较政策则是本文设计的研究操作,不是摘要函数自动给出的因果诊断。

规范表示形式时不要抹掉交易含义

结构快照可按键排序、统一分隔符后序列化,再计算摘要。事件数组有顺序含义时必须保留顺序。同一时点的两张订单如果交换位置,可能改变可用现金或成交优先级;为了让摘要相等而任意排序全部记录,会消除最需要的证据。

原始字节和规范化产物应同时保存。文件排版、对象键顺序或展示时间格式可能改变字节,却不改变实验。反过来,把所有价格先四舍五入到两位小数,可能隐藏真实的成交差异。说明每一种规范化为什么只影响表示,不能把“让文件一样”当作处理目标。

适合精确检查的字段先比摘要,再下钻行级数值。容差只用于事先选定的数值字段,不作用于整个对象。最终余额接近,不能豁免漏单或证券身份变化;轻微浮点求和误差,也不一定意味着策略逻辑改变。比较结论要与字段的实际含义对应。

主例:数据修订怎样传到信号

合成价格为[100, 102, 101, 104],初始现金一千,随机种子七。在索引一和二,如果当前价格高于上一观测,信号为一。每个正信号在下一观测买一股,成交价加入一个固定种子产生的微小合成增量。这样保留信号时点与后续成交时点的区别,但没有模拟真实流动性或市场限制。

先原样跑两次,得到相同信号[1, 0],每层内容与规范化摘要都相同。然后仅把第三个价格从一百零一修订为一百零三,序列变成[100, 102, 103, 104],信号随之变为[1, 1]。首个变化出现在数据层,即使后续成交和最终净值也同时变化。

修复方向应是查清实际快照为何变更。可以恢复原始数据用于重放,也可以声明这是修订数据上的新实验。调整随机种子或放宽最终净值容差,都没有处理这个案例已经证明的数据原因。保留两份快照和逐观测差异,才有办法解释修订对既有结论的影响。

不同机制反例:隐藏执行手续费

反例保持数据、声明配置、环境记录和信号完全一致,但在执行函数内注入每笔零点二五的手续费。基线只有一次买入,因此首个记录到的差异位于成交层,最终净值恰好少零点二五。样例故意不把费用写进声明配置,用于暴露记录手段的缺口。

这不意味着费用可以合理地隐藏。正确修复应把全部执行设置纳入解析配置,冻结后重新运行。修复后,费用变化应该更早被归类为配置层差异。在尚未补齐记录时,成交层的不一致告诉排查者应从哪里追查未登记行为,而不能把它误称为纯粹随机波动。

把相同信号作为重放输入,可以隔离撮合和记账。如果成交完全相同而净值不同,下一步检查现金流水、持仓更新与估值标记。避免一次修改多个层级:最终数字可能恢复一致,但原始原因仍然没有解释清楚,也无法确认修复是否只是在误差之间互相抵消。

实际运行最小诊断包

核心比较器保持简单,完整脚本负责产生数据、模拟运行、注入差异和执行断言:

LAYERS = ["data", "config", "runtime", "signals", "fills", "equity"]

def first_difference(left, right):
    return next((layer for layer in LAYERS
                 if left[layer] != right[layer]), None)

在仓库运行python docs/growth/research/round-5/examples/R622.py,实际输出为:

repeat_first_difference=None normalized_digest_equal=True
revised_first_difference=data signals=[1, 0]->[1, 1]
hidden_fee_first_difference=fills final_equity_delta=0.250000
reordered_keys_normalized_equal=True

脚本还反向插入字典键,构造语义相同但键顺序不同的对象,断言规范化摘要仍然相同。这项表示检查与改价格、加费用的解释不同,避免把文件顺序变化报道成交易行为变化。真实复现包还应保存事件键与差异行,不能只有一句“摘要不一致”。

管理随机状态并关闭问题

示例每次运行单独创建Random(7)实例。Python随机模块文档讨论种子复现及版本限制。一个种子不能冻结线程调度、其他库的随机流或外部模型响应。要记录运行版本和每条随机流归属;并行工作线程共享随机流时,抽样值分配给哪个事件可能发生变化。

修复验收需要在更正后的冻结上下文中重新运行,并沿全部要求层级满足原定比较政策。保存两份运行清单、最小输入、首次不同事件、后续影响、原因解释和修复重跑证据。作废的研究结论应明确标注,不能覆盖旧运行,让后续读者误以为从未发生差异。

数据、配置、依赖、引擎、并发或执行政策变化时重新开启检查。若只有规范化产物相同而原始字节不同,就报告这一有限结论。若外部依赖仍未冻结,就说明只复现了已捕获的离线路径。此合成包通过只证明它包含的断言,不证明真实历史盈利,也不证明任何未测试产品流程具有确定性。

让首差异可以独立复核

问题记录应附稳定事件键、变化前后字段值、紧邻上游输入及采用的比较规则。价格修订主例中,变化观测是索引二的一百零一与一百零三,该索引信号从零变一;费用反例则保留原有买入时点和股数,只把费用改为零点二五。检查者可以先核对这些局部状态转移,再接受最终净值差的解释。复现包还要写明执行入口、工作目录要求和预期状态,而不仅是一组截图。若缩小数据后问题消失,应记录仍然不可删除的前置观测,不能删到一个已不再触发问题的小样例,然后把它称作复现成功。

下载完整示例。使用Python 3.12或更新版本,在下载目录运行python R622.py;仅使用标准库与合成输入。

常见问题

是否先设置随机种子就能解决问题?

要记录随机状态,但仍应先核对实际数据和生效设置。种子无法修复被修订的价格,也无法让被忽略的参数生效。

首个不同层级就是最终根因吗?

它是已保存层级中最早看到的差异。存在隐藏依赖时,还需要补充记录才能确认最终根因。

相关阅读