回测服务与前端:一次回测 = 一条可追溯的任务记录
三组参数跑出年化 2.99% / 10.83% / 21.07%——而每一条都带着策略指纹与数据指纹
一次回测不是一次函数调用,是一条任务记录。
数据指纹 d55258de9e46(数据变了 → 指纹变 → 旧结果自动作废)
任务 策略 策略指纹 累计% 年化% 回撤% 成交 被拒
bt-5-20 ma_5_20 b1cbd591ad4a 28.90 10.83 -27.40 98 108
bt-10-30 ma_10_30 cbdd0d8211fe 7.55 2.99 -31.45 101 99
bt-20-60 ma_20_60 31b0f675189d 60.29 21.07 -19.06 88 107
年化从 2.99% 到 21.07%——而这三行的区别只有均线参数。
如果没有右边那两个指纹,三个月后你无法回答:这条 21.07% 的曲线,用的是哪份规格、哪份数据。
而 p5.4 已经证明:报告最优的那一组,等于报告 N 次比较的最大值。
任务记录不能阻止你挑最好的那条,但它能让别人看见你一共跑了几条。
实验记录本上,每一次实验都要写:日期、试剂批号、参数、结果、操作人。
不是为了好看,是为了三个月后有人问"这个数据怎么来的"时,你能翻到那一页。
回测任务记录就是那一页。
一、任务记录必须有的六样
@dataclass
class BacktestTask:
task_id: str
strategy: str
strategy_fp: str # ① 策略指纹(p5.1 / p8.4)
symbol: str
start: str; end: str # ② 区间
cost: dict # ③ 成本假设(p4.2:值 74 个百分点)
data_fp: str # ④ 数据指纹
status: str
result: dict # ⑤ 结果
finished_at: str # ⑥ 完成时间
没有这六样,三个月后你无法回答"这条曲线是怎么来的"。
其中最容易被省掉的是 ④ 数据指纹:
def data_fingerprint(path):
df = pd.read_parquet(path, columns=["symbol", "date", "close"])
return hashlib.md5(df.to_numpy().tobytes()).hexdigest()[:12]
数据变了 → 指纹变 → 所有引用旧指纹的结果自动作废。
这正是 p5.9 那次事故需要的东西:当时如果每个结果都带数据指纹,ERC 期末倍数在 3.38~3.95 之间漂这件事,当场就会被发现。
二、结果存储:前端只读一个契约
落盘 3 条任务记录(JSON Lines,追加写)
字段:task_id / strategy / strategy_fp / symbol / start / end
cost / data_fp / status / result / finished_at
前端不需要知道回测是怎么跑的,它只消费这个契约。
而这个契约里有策略指纹和数据指纹——所以图上的每条线都能追回去。
🔧 动手做:同一个策略跑三组参数,看每条结果带没带"身份证"(5 分钟)
跑回测任务服务,用三组参数各跑一次。
你会看到(实测):三条结果年化分别是 2.99% / 10.83% / 21.07%——而每一条都带着策略指纹和数据指纹(数据指纹 d55258de9e46)。
想明白:一次回测不是"一个数字",是一条可追溯的记录:什么策略(指纹)、在什么数据(指纹)、什么时候跑的。三个月后你看到"年化 21%",能立刻查出它是哪套参数、哪份数据跑的——而不是一个来历不明的漂亮数字。没有指纹的回测结果,等于没有出处的传言。
三、前端图表要什么(与 p4.8 的六联图对齐)
| 面板 | 回答什么 | 数据来自 |
|---|---|---|
| 净值 + 基准(对数轴) | 跑赢了没有 | equity_curve |
| 水下图 | 我要熬多久 | 由 equity_curve 计算 |
| 月度收益热力 | 收益是否均匀 | 由 equity_curve 计算 |
| 成交点标注 | 在什么位置进出 | fills.jsonl |
| 被拒委托时间轴 | 向量化回测画不出来 | rejects.jsonl |
| 血缘水印 | 这张图是怎么来的 | strategy_fp + data_fp + cost |
最后一行是很多系统会省掉的。
图会被截屏、转发、三个月后翻出来。没有水印的图,三天后就是一张不可追溯的图片(p1.8)。
而回测报告的水印比行情图多两项:引擎名与成本假设——因为 p4.2 已经证明,成本假设值 74 个百分点。
另外注意倒数第二行:被拒委托时间轴。
本节三个任务的被拒笔数是 108 / 99 / 107——和成交笔数几乎一样多。一张不画被拒的报告图,会让人以为这些委托都成交了。
四、任务化带来的三个能力
| 能力 | 怎么来的 |
|---|---|
| 批量与对比 | 任务是数据,可以一次提交一批(本节 3 个)并排比 |
| 幂等重跑 | (strategy_fp, data_fp, cost, 区间) 相同 → 直接返回旧结果,不重算 |
| 自动作废 | 数据指纹变了 → 引用旧指纹的结果标记为过期 |
第三条是任务化最大的收益:它把"数据更新了要不要重跑"这个靠人记的事,变成了系统能判断的事。
📌 免责:本课为技术教学,回测参数与结果均为演示口径,不构成投资建议。
四件事:
- 一次回测 = 一条任务记录,而不是一次函数调用。六样必备:策略指纹 · 区间 · 成本假设 · 数据指纹 · 结果 · 完成时间。实测三组参数年化 2.99% / 10.83% / 21.07%——区别只有均线参数
- 最容易被省掉的是数据指纹。有了它,数据变了 → 指纹变 → 旧结果自动作废;p5.9 那次事故如果每个结果都带数据指纹,当场就会被发现
- 前端只消费一个契约,不需要知道回测怎么跑。而契约里有两个指纹,所以图上的每条线都能追回去
- 图表六联与 p4.8 对齐,其中两项最常被省:被拒委托时间轴(本节三个任务被拒 108/99/107,和成交笔数几乎一样多)与血缘水印(比行情图多引擎名与成本假设两项,因为 p4.2 证明成本值 74 个百分点)
任务化带来三个能力:批量对比 · 幂等重跑 · 自动作废。第三条把"数据更新了要不要重跑"从靠人记的事,变成了系统能判断的事。
下一节接实时行情:限频、抖动、断线——以及一个用 300 步模拟量化出来的结论。
🔎 来源与核验· 1 条,点开核对
