后端12 min read最后更新 Updated 2026.09.14

回测跑完以后,还要回答什么

开始阅读

做量化研究,最容易吸引注意的是收益曲线。

策略赚了多少、最大回撤多大、夏普比率怎样,往往也是结果页面最显眼的部分。

但研究做得久了,总会碰到一些更基础的问题:

  • 上周的结果为什么今天跑不出来?
  • 两份报告用了同一批数据吗?
  • 程序退出前显示计算完成,结果到底存下来没有?
  • 一笔收益究竟是怎么形成的?
  • AI 对结果的解释,依据在哪里?

这些问题大多落在后端。

Python 负责计算,FastAPI 提供接口,Pydantic 检查数据结构,SQLite 保存状态,Parquet 存放结果。

工具都很常见,真正的设计功夫,藏在它们如何处理一次运行的前后关系里。


一、回测能不能复现,不只是保存参数

先说复现。

保存策略名称和参数,只留下了实验的一部分。

几个月以后,真正影响结果的东西可能已经发生变化:

  • 行情数据被修订;
  • 手续费规则调整;
  • 策略代码修改;
  • 依赖库升级;
  • 交易日历变化;
  • 计算规则更新。

即使找回了当时的参数,也未必找得回当时的实验条件。

保存一份完整的运行清单

比较扎实的做法,是在运行前保存一份完整清单,把关键条件固定下来,例如:

TEXT
运行清单
├── 输入数据版本
├── 策略代码版本
├── 策略参数
├── 手续费与成交规则
├── 交易日历
├── 计算环境
└── 相关规则版本

同时,可以对关键输入计算内容摘要,用来检查它们之后有没有发生变化。

重跑时,系统先核对这份清单。

如果条件已经不完整,就明确告诉用户缺在哪里:

  • 原始数据已经不存在;
  • 策略实现已经改变;
  • 依赖版本无法恢复;
  • 当前手续费模型与原实验不同。

无法复现本身也是一个需要被记录的结果。

系统不应该默默使用当前条件,然后把新的结果包装成旧实验的重现。


二、比较两次结果时,要先分清比较的是什么

两份结果“相同”,其实可能有很多不同含义。

例如:

  • 两个文件字节完全一致;
  • 两份报告的核心指标一致;
  • 两次运行的输入条件一致;
  • 两次计算过程使用同一套规则;
  • 两个结果在允许误差范围内一致。

这些结论不能互相替代。

文件完整性与业务内容要分开检查

两个文件字节不同,可能只是因为:

  • 运行 ID 不同;
  • 时间戳不同;
  • Parquet 编码方式变化;
  • 元数据顺序变化。

反过来,两份报告收益率一样,也不能据此认定:

两次计算过程完全一致。

因此,可以把检查拆成两类:

检查类型 回答的问题
文件完整性检查 文件是否损坏、被修改或发生字节变化
业务内容检查 按约定口径,两次结果是否表达相同业务事实

例如,SHA-256 可以帮助确认文件有没有变化。

但如果真正关心的是:

两次回测是否得到了相同的持仓、成交和收益序列?

就必须按照业务字段重新比较。

只有先说明“比较的对象是什么”,才能准确表达两次运行究竟相同在哪里。


三、“计算结束”不等于“任务完成”

另一个容易被忽略的问题,是:

完成究竟发生在什么时候?

计算进程结束时,结果可能还只存在于内存中。

文件写出来以后,数据库也可能还没有保存对应引用。

如果服务恰好在这些时刻退出,页面上的“成功”就会变得含糊。

把完成过程拆成几个明确步骤

后端可以把一次运行拆成:

TEXT
执行计算
   ↓
写入暂存结果
   ↓
检查文件完整性
   ↓
检查结果内容
   ↓
发布正式结果
   ↓
数据库保存结果引用
   ↓
任务正式完成

SQLite 管理:

  • 运行记录;
  • 当前状态;
  • 执行身份;
  • 结果引用。

Parquet 等文件则保存:

  • 成交明细;
  • 净值序列;
  • 持仓变化;
  • 大型分析结果。

两边职责不同,也存在不同的失败窗口。


四、中途失败时,系统要知道自己停在哪里

假设任务在不同阶段中断:

中断位置 可以确认的事实
计算过程中退出 完整结果还没有产生
暂存文件写了一半 可能存在部分产物
文件已经发布,数据库尚未提交 文件存在,但正式成功记录还没有建立
数据库已提交,响应丢失 任务实际上可能已经成功
历史成功文件后来损坏 当时完成过,但当前结果不可读取

设计的重点不是消灭所有失败,而是:

失败以后,系统仍然能够说明已经发生了什么。

这样下次启动时,就不用猜:

  • 上一次到底有没有成功;
  • 这个文件是不是完整结果;
  • 能不能直接开放读取;
  • 是否需要重新计算。

五、异常场景才最能检验长任务设计

这类设计在正常运行时通常不显眼。

真正体现价值的是:

  • 磁盘写入失败;
  • Worker 被强制终止;
  • 用户连续点击取消;
  • 服务重启;
  • 响应丢失;
  • 同一个任务被重复提交。

如果设计足够清楚,系统仍然能够回答:

  • 哪些结果已经可读取;
  • 哪些仍处于暂存状态;
  • 哪些任务已经失败;
  • 哪些执行进程还没有清理;
  • 当前是否允许再次运行。

对个人研究工具来说,这并不一定要求庞大的基础设施。

一个统一的数据库写入入口,加上独立计算进程和明确的执行限制,就能够解决不少实际问题。


六、重复提交、取消和结果保存都需要明确规则

例如,用户连续点击两次“开始回测”。

系统需要决定:

这是两个实验,还是同一个请求的重复提交?

可以通过幂等键和请求摘要,让相同请求返回原任务,而不是无意中启动两次计算。

取消也一样。

用户点击“取消”,只能说明:

取消意图已经产生。

它不代表:

Worker 已经退出。

因此,需要分别记录:

TEXT
取消已请求
Worker 正在退出
Worker 已停止
资源已释放

如果取消和结果保存恰好同时发生,也需要提前规定:

最终以哪个已经持久化的事实为准?

这些规则在任务量很小时也值得明确,因为它们决定了结果到底能不能信。

当然,单机数据库、单写线程和有限 Worker 数量都有自己的吞吐边界。

系统适合什么负载,也需要如实说明。


七、拿到数据,不代表数据适合这次研究

数据可用性也值得单独管理。

接口成功返回行情,只能证明:

数据拿到了。

它并不能自动证明:

  • 日期覆盖完整;
  • 缺失值是正常停牌还是数据问题;
  • 交易日历已经对齐;
  • 时间戳含义一致;
  • 公司行动已经正确处理;
  • 当前价格口径适合这项研究。

“可用”应该带条件

更合适的表达方式不是:

TEXT
这份数据可用

而是:

TEXT
这份数据
在某个日期范围内
针对某个策略版本
使用某套参数和规则
适用于某类研究

相关条件发生变化,就重新判断。

例如:

  • 换了研究区间;
  • 换了策略版本;
  • 调整了参数;
  • 更新了公司行动处理方式。

都可能使旧的资格结论失效。

缺少证据的地方,应该保留为空或标明“未验证”,而不是默认当作通过。


八、模拟交易最终还是要回到账本事实

到了模拟交易,问题就从回测结果进一步落到账本上。

一笔买入可能改变:

  • 现金;
  • 持仓数量;
  • 持仓成本;
  • 可卖数量。

一笔卖出还会涉及:

  • 手续费;
  • 印花税;
  • 已实现收益;
  • 剩余成本。

如果是 A 股,还可能涉及:

  • T+1;
  • 交易日;
  • 权益登记日;
  • 分红除权;
  • 分红税;
  • 价格调整口径。

这些规则都会直接改变最终数字。


九、不要只保存余额,要保存形成余额的事实

如果系统只保存:

TEXT
当前现金:xxx
当前持仓:xxx

一旦数字出现差异,就很难追查原因。

更可靠的方式,是按顺序保存原始事件:

TEXT
成交
费用
入金 / 出金
分红
送股
拆并股
归属变化
估值事件

系统可以根据这些历史事实重新计算账户,再与当前状态核对。

如果发现差额,就可以继续追踪:

  • 哪笔成交没有入账;
  • 哪项费用算错;
  • 哪个权益事件遗漏;
  • 哪次估值口径不同。

内部账本对得上,只说明在既定账户模型下计算能够自洽。

它并不能自动证明:

与真实券商或交易所最终结果完全一致。

后者仍然需要单独对账与验证。


十、金额和收益计算也需要明确规则

数值问题不能只交给默认浮点行为。

不同数据适合不同处理:

类型 常见选择
股票数量 整数
金额 整数最小单位或 Decimal
手续费 明确的小数位与舍入规则
科学计算 浮点数 + 约定误差容限
收益指标 明确公式、观察区间和缺失条件

如果使用 Decimal,也应该明确:

  • 精度;
  • 舍入方式;
  • 规则版本。

而不是简单认为:

Decimal 就天然完全精确。

业务规则本身也应该拥有版本。

否则几个月以后,即使输入数据完全相同,也可能因为费用和交易规则改变而得到不同结果。


十一、AI 接入以后,同样要回答“依据在哪里”

AI 助手加入研究工作台以后,会出现同一个问题:

它说的这句话,依据是什么?

真正复杂的通常不是调用模型 API,而是:

  • 给模型准备哪些材料;
  • 材料来自哪个实验;
  • 是否允许访问;
  • 回答是否引用了真实存在的证据。

一种清晰的流程可以是:

TEXT
用户提出问题
   ↓
读取允许访问的研究结果
   ↓
保留来源、版本和时间
   ↓
交给模型解释
   ↓
检查回答中的引用
   ↓
保存回答与证据关系

这样,模型说:

“最大回撤主要发生在某段市场下跌期间。”

用户至少可以继续点回:

  • 对应回测;
  • 净值数据;
  • 交易明细;
  • 时间区间。

十二、来源事实、分析判断和证据不足要分开表达

AI 回答最好区分三种东西。

来源事实

例如:

本次回测最大回撤为 12.4%。

这是直接来自实验结果的数据。

分析判断

例如:

回撤可能与高仓位集中有关。

这是对事实的解释。

证据不足

例如:

当前结果没有保存行业暴露数据,因此无法判断是否由行业集中造成。

这是对信息边界的表达。

三者如果混在一起,很容易让用户把模型推断误当成系统保存的事实。


十三、引用正确,不代表推理一定正确

系统可以检查:

  • 引用对象存在;
  • 版本匹配;
  • 引用确实来自本次提供的材料;
  • 内容没有越过允许访问范围。

但这只能说明:

模型引用了真实存在的证据。

不能证明:

模型一定正确理解了这份证据。

因此仍然需要评估:

  • 事实有没有读错;
  • 推断是否过度;
  • 信息不足时是否承认不知道;
  • 是否把缺失值解释成真实的零;
  • 是否超出证据允许的范围。

十四、让 AI 保持只读,会让边界清楚很多

对于研究解释型助手,一个很实用的起点是:

让 AI 只负责解释,不直接改变研究事实。

例如:

AI 可以:

  • 阅读回测结果;
  • 对比实验;
  • 解释指标;
  • 总结风险;
  • 查找证据。

但以下操作继续通过明确的业务流程完成:

  • 修改策略;
  • 启动回测;
  • 删除结果;
  • 调整账户;
  • 修改交易记录。

这样做可以把:

解释能力

和:

执行权限

分开。

即使模型解释出错,也不会直接改变研究状态。


十五、评价一个研究后端,不只是问“算得快不快”

当然,计算速度很重要。

但一个长期使用的量化研究后端,还可以被追问更多问题:

关于复现

旧结果能不能找回原来的实验条件?

关于任务状态

失败以后,能不能说清楚停在哪一步?

关于结果交付

页面显示成功时,文件和数据库是否真的完成交付?

关于数据

这份数据究竟适合支持哪些研究?

关于账户

一笔收益能不能追溯到具体成交、费用和资金变化?

关于 AI

一段解释能不能返回原始依据?

这些问题共同指向同一件事:

结果是否值得继续被使用。


结语:回测结束,只是研究结果生命周期的开始

收益曲线当然重要。

但一套研究系统真正开始变得可靠,是在它能够继续回答这些问题以后:

  • 这次运行用了什么条件?
  • 为什么今天的结果和上周不同?
  • 结果是否真正完成保存?
  • 当前数据是否适合这个研究?
  • 一笔盈亏是如何形成的?
  • AI 的解释依据是什么?
  • 哪些内容仍然未知?

把这些问题处理好,研究者才有条件放心使用一个结果。

更重要的是:

当结果看起来可疑时,也有能力找到依据、定位差异,并推翻它。

可信的研究工具,不是让每次回测都显得正确。

而是让每个结论都能被重新检查。

相关文章

2026.09.14常见技术栈如何构建可信的长任务后端
返回文章列表