做量化研究,最容易吸引注意的是收益曲线。
策略赚了多少、最大回撤多大、夏普比率怎样,往往也是结果页面最显眼的部分。
但研究做得久了,总会碰到一些更基础的问题:
- 上周的结果为什么今天跑不出来?
- 两份报告用了同一批数据吗?
- 程序退出前显示计算完成,结果到底存下来没有?
- 一笔收益究竟是怎么形成的?
- AI 对结果的解释,依据在哪里?
这些问题大多落在后端。
Python 负责计算,FastAPI 提供接口,Pydantic 检查数据结构,SQLite 保存状态,Parquet 存放结果。
工具都很常见,真正的设计功夫,藏在它们如何处理一次运行的前后关系里。
一、回测能不能复现,不只是保存参数
先说复现。
保存策略名称和参数,只留下了实验的一部分。
几个月以后,真正影响结果的东西可能已经发生变化:
- 行情数据被修订;
- 手续费规则调整;
- 策略代码修改;
- 依赖库升级;
- 交易日历变化;
- 计算规则更新。
即使找回了当时的参数,也未必找得回当时的实验条件。
保存一份完整的运行清单
比较扎实的做法,是在运行前保存一份完整清单,把关键条件固定下来,例如:
运行清单
├── 输入数据版本
├── 策略代码版本
├── 策略参数
├── 手续费与成交规则
├── 交易日历
├── 计算环境
└── 相关规则版本
同时,可以对关键输入计算内容摘要,用来检查它们之后有没有发生变化。
重跑时,系统先核对这份清单。
如果条件已经不完整,就明确告诉用户缺在哪里:
- 原始数据已经不存在;
- 策略实现已经改变;
- 依赖版本无法恢复;
- 当前手续费模型与原实验不同。
无法复现本身也是一个需要被记录的结果。
系统不应该默默使用当前条件,然后把新的结果包装成旧实验的重现。
二、比较两次结果时,要先分清比较的是什么
两份结果“相同”,其实可能有很多不同含义。
例如:
- 两个文件字节完全一致;
- 两份报告的核心指标一致;
- 两次运行的输入条件一致;
- 两次计算过程使用同一套规则;
- 两个结果在允许误差范围内一致。
这些结论不能互相替代。
文件完整性与业务内容要分开检查
两个文件字节不同,可能只是因为:
- 运行 ID 不同;
- 时间戳不同;
- Parquet 编码方式变化;
- 元数据顺序变化。
反过来,两份报告收益率一样,也不能据此认定:
两次计算过程完全一致。
因此,可以把检查拆成两类:
| 检查类型 | 回答的问题 |
|---|---|
| 文件完整性检查 | 文件是否损坏、被修改或发生字节变化 |
| 业务内容检查 | 按约定口径,两次结果是否表达相同业务事实 |
例如,SHA-256 可以帮助确认文件有没有变化。
但如果真正关心的是:
两次回测是否得到了相同的持仓、成交和收益序列?
就必须按照业务字段重新比较。
只有先说明“比较的对象是什么”,才能准确表达两次运行究竟相同在哪里。
三、“计算结束”不等于“任务完成”
另一个容易被忽略的问题,是:
完成究竟发生在什么时候?
计算进程结束时,结果可能还只存在于内存中。
文件写出来以后,数据库也可能还没有保存对应引用。
如果服务恰好在这些时刻退出,页面上的“成功”就会变得含糊。
把完成过程拆成几个明确步骤
后端可以把一次运行拆成:
执行计算
↓
写入暂存结果
↓
检查文件完整性
↓
检查结果内容
↓
发布正式结果
↓
数据库保存结果引用
↓
任务正式完成
SQLite 管理:
- 运行记录;
- 当前状态;
- 执行身份;
- 结果引用。
Parquet 等文件则保存:
- 成交明细;
- 净值序列;
- 持仓变化;
- 大型分析结果。
两边职责不同,也存在不同的失败窗口。
四、中途失败时,系统要知道自己停在哪里
假设任务在不同阶段中断:
| 中断位置 | 可以确认的事实 |
|---|---|
| 计算过程中退出 | 完整结果还没有产生 |
| 暂存文件写了一半 | 可能存在部分产物 |
| 文件已经发布,数据库尚未提交 | 文件存在,但正式成功记录还没有建立 |
| 数据库已提交,响应丢失 | 任务实际上可能已经成功 |
| 历史成功文件后来损坏 | 当时完成过,但当前结果不可读取 |
设计的重点不是消灭所有失败,而是:
失败以后,系统仍然能够说明已经发生了什么。
这样下次启动时,就不用猜:
- 上一次到底有没有成功;
- 这个文件是不是完整结果;
- 能不能直接开放读取;
- 是否需要重新计算。
五、异常场景才最能检验长任务设计
这类设计在正常运行时通常不显眼。
真正体现价值的是:
- 磁盘写入失败;
- Worker 被强制终止;
- 用户连续点击取消;
- 服务重启;
- 响应丢失;
- 同一个任务被重复提交。
如果设计足够清楚,系统仍然能够回答:
- 哪些结果已经可读取;
- 哪些仍处于暂存状态;
- 哪些任务已经失败;
- 哪些执行进程还没有清理;
- 当前是否允许再次运行。
对个人研究工具来说,这并不一定要求庞大的基础设施。
一个统一的数据库写入入口,加上独立计算进程和明确的执行限制,就能够解决不少实际问题。
六、重复提交、取消和结果保存都需要明确规则
例如,用户连续点击两次“开始回测”。
系统需要决定:
这是两个实验,还是同一个请求的重复提交?
可以通过幂等键和请求摘要,让相同请求返回原任务,而不是无意中启动两次计算。
取消也一样。
用户点击“取消”,只能说明:
取消意图已经产生。
它不代表:
Worker 已经退出。
因此,需要分别记录:
取消已请求
Worker 正在退出
Worker 已停止
资源已释放
如果取消和结果保存恰好同时发生,也需要提前规定:
最终以哪个已经持久化的事实为准?
这些规则在任务量很小时也值得明确,因为它们决定了结果到底能不能信。
当然,单机数据库、单写线程和有限 Worker 数量都有自己的吞吐边界。
系统适合什么负载,也需要如实说明。
七、拿到数据,不代表数据适合这次研究
数据可用性也值得单独管理。
接口成功返回行情,只能证明:
数据拿到了。
它并不能自动证明:
- 日期覆盖完整;
- 缺失值是正常停牌还是数据问题;
- 交易日历已经对齐;
- 时间戳含义一致;
- 公司行动已经正确处理;
- 当前价格口径适合这项研究。
“可用”应该带条件
更合适的表达方式不是:
这份数据可用
而是:
这份数据
在某个日期范围内
针对某个策略版本
使用某套参数和规则
适用于某类研究
相关条件发生变化,就重新判断。
例如:
- 换了研究区间;
- 换了策略版本;
- 调整了参数;
- 更新了公司行动处理方式。
都可能使旧的资格结论失效。
缺少证据的地方,应该保留为空或标明“未验证”,而不是默认当作通过。
八、模拟交易最终还是要回到账本事实
到了模拟交易,问题就从回测结果进一步落到账本上。
一笔买入可能改变:
- 现金;
- 持仓数量;
- 持仓成本;
- 可卖数量。
一笔卖出还会涉及:
- 手续费;
- 印花税;
- 已实现收益;
- 剩余成本。
如果是 A 股,还可能涉及:
- T+1;
- 交易日;
- 权益登记日;
- 分红除权;
- 分红税;
- 价格调整口径。
这些规则都会直接改变最终数字。
九、不要只保存余额,要保存形成余额的事实
如果系统只保存:
当前现金:xxx
当前持仓:xxx
一旦数字出现差异,就很难追查原因。
更可靠的方式,是按顺序保存原始事件:
成交
费用
入金 / 出金
分红
送股
拆并股
归属变化
估值事件
系统可以根据这些历史事实重新计算账户,再与当前状态核对。
如果发现差额,就可以继续追踪:
- 哪笔成交没有入账;
- 哪项费用算错;
- 哪个权益事件遗漏;
- 哪次估值口径不同。
内部账本对得上,只说明在既定账户模型下计算能够自洽。
它并不能自动证明:
与真实券商或交易所最终结果完全一致。
后者仍然需要单独对账与验证。
十、金额和收益计算也需要明确规则
数值问题不能只交给默认浮点行为。
不同数据适合不同处理:
| 类型 | 常见选择 |
|---|---|
| 股票数量 | 整数 |
| 金额 | 整数最小单位或 Decimal |
| 手续费 | 明确的小数位与舍入规则 |
| 科学计算 | 浮点数 + 约定误差容限 |
| 收益指标 | 明确公式、观察区间和缺失条件 |
如果使用 Decimal,也应该明确:
- 精度;
- 舍入方式;
- 规则版本。
而不是简单认为:
Decimal 就天然完全精确。
业务规则本身也应该拥有版本。
否则几个月以后,即使输入数据完全相同,也可能因为费用和交易规则改变而得到不同结果。
十一、AI 接入以后,同样要回答“依据在哪里”
AI 助手加入研究工作台以后,会出现同一个问题:
它说的这句话,依据是什么?
真正复杂的通常不是调用模型 API,而是:
- 给模型准备哪些材料;
- 材料来自哪个实验;
- 是否允许访问;
- 回答是否引用了真实存在的证据。
一种清晰的流程可以是:
用户提出问题
↓
读取允许访问的研究结果
↓
保留来源、版本和时间
↓
交给模型解释
↓
检查回答中的引用
↓
保存回答与证据关系
这样,模型说:
“最大回撤主要发生在某段市场下跌期间。”
用户至少可以继续点回:
- 对应回测;
- 净值数据;
- 交易明细;
- 时间区间。
十二、来源事实、分析判断和证据不足要分开表达
AI 回答最好区分三种东西。
来源事实
例如:
本次回测最大回撤为 12.4%。
这是直接来自实验结果的数据。
分析判断
例如:
回撤可能与高仓位集中有关。
这是对事实的解释。
证据不足
例如:
当前结果没有保存行业暴露数据,因此无法判断是否由行业集中造成。
这是对信息边界的表达。
三者如果混在一起,很容易让用户把模型推断误当成系统保存的事实。
十三、引用正确,不代表推理一定正确
系统可以检查:
- 引用对象存在;
- 版本匹配;
- 引用确实来自本次提供的材料;
- 内容没有越过允许访问范围。
但这只能说明:
模型引用了真实存在的证据。
不能证明:
模型一定正确理解了这份证据。
因此仍然需要评估:
- 事实有没有读错;
- 推断是否过度;
- 信息不足时是否承认不知道;
- 是否把缺失值解释成真实的零;
- 是否超出证据允许的范围。
十四、让 AI 保持只读,会让边界清楚很多
对于研究解释型助手,一个很实用的起点是:
让 AI 只负责解释,不直接改变研究事实。
例如:
AI 可以:
- 阅读回测结果;
- 对比实验;
- 解释指标;
- 总结风险;
- 查找证据。
但以下操作继续通过明确的业务流程完成:
- 修改策略;
- 启动回测;
- 删除结果;
- 调整账户;
- 修改交易记录。
这样做可以把:
解释能力
和:
执行权限
分开。
即使模型解释出错,也不会直接改变研究状态。
十五、评价一个研究后端,不只是问“算得快不快”
当然,计算速度很重要。
但一个长期使用的量化研究后端,还可以被追问更多问题:
关于复现
旧结果能不能找回原来的实验条件?
关于任务状态
失败以后,能不能说清楚停在哪一步?
关于结果交付
页面显示成功时,文件和数据库是否真的完成交付?
关于数据
这份数据究竟适合支持哪些研究?
关于账户
一笔收益能不能追溯到具体成交、费用和资金变化?
关于 AI
一段解释能不能返回原始依据?
这些问题共同指向同一件事:
结果是否值得继续被使用。
结语:回测结束,只是研究结果生命周期的开始
收益曲线当然重要。
但一套研究系统真正开始变得可靠,是在它能够继续回答这些问题以后:
- 这次运行用了什么条件?
- 为什么今天的结果和上周不同?
- 结果是否真正完成保存?
- 当前数据是否适合这个研究?
- 一笔盈亏是如何形成的?
- AI 的解释依据是什么?
- 哪些内容仍然未知?
把这些问题处理好,研究者才有条件放心使用一个结果。
更重要的是:
当结果看起来可疑时,也有能力找到依据、定位差异,并推翻它。
可信的研究工具,不是让每次回测都显得正确。
而是让每个结论都能被重新检查。