半年前跑出漂亮曲线的回测策略,今天同样的代码和参数却得不到相同结果。问题很可能不在策略本身,而在数据输入层——复权方式、样本池版本、行情抓取时间、原始响应是否留存,任何一项缺失都会让旧回测无法复现。
半年前跑出来的回测曲线很好看。今天把代码翻出来,参数没改,逻辑没改,区间也没改,但结果就是对不上。
第一反应是代码哪里改坏了。查完提交记录,策略代码确实没动。这时候你开始怀疑自己:是不是当时看错了?是不是记错了回测参数?
真正变化的,往往不是策略,而是输入数据。
复权用的是前复权还是后复权?股票池是当时的版本还是今天的版本?停牌和退市怎么处理?行情数据是哪一天抓的?今天重新下载同一段历史 K 线,能不能证明拿到的是同一份输入?
旧回测要复现,除了策略代码,还得留下当时的数据入口证据。本文通过一次真实调用实测,展示如何用请求参数、原始响应和 SHA256 哈希,为历史 K 线输入建立可追溯的证据链。
一、代码没变,不等于输入没变
一段历史 K 线进入回测系统之前,经过了一串选择。大多数人只保存了策略代码和回测结果,但数据输入链条上的关键环节——怎么请求的、返回了什么、有没有被清洗过——往往没有留版本。
| 层级 | 你以为保存了什么 | 实际还要保存什么 |
|---|---|---|
| 策略层 | 策略代码、参数、回测区间 | 下单规则、滑点、手续费、调仓日、缺失值处理 |
| 样本层 | 股票列表 | 样本池版本、成分生效日期、退市/停牌处理 |
| 行情层 | OHLCV 数据 | 接口、symbol、interval、start/end、复权口径、抓取时间 |
| 证据层 | 数据能重新下载 | 原始响应、normalized rows、hash、请求日志 |
只保存策略代码,等于只保存了回测的一部分。
几个月后重新跑一次,结果变了,你没法判断变化来自策略、数据、样本,还是某个清洗规则。排查的第一步,不是怀疑策略失效,而是先确认输入是否一致。
二、用 TickDB 拉一段 K 线,建立复现基线
这次实测只做一件小事:用 TickDB 拉取 600519.SH 的一段日 K,保存请求参数、原始响应和 normalized hash。然后重复请求一次,再改变结束日期,比较重叠区间。
| 项目 | 内容 |
|---|---|
| API | GET /v1/market/kline |
| symbol | 600519.SH |
| interval | 1d |
| exact window | 2024-06-03 至 2024-06-14 |
| extended window | 2024-06-03 至 2024-06-21 |
| 实测时间 | 2026-07-09 20:52:53 +08:00 |
随后做了两类比较:
- 同参数重复请求,看 normalized rows 是否一致;
- 固定起点、改变结束日期,看两个窗口的重叠区间是否一致。
| 检查项 | 结果 |
|---|---|
| exact call 1 | OK |
| exact call 2 | OK |
| extended call | OK |
| exact window 行数 | 8 |
| 同参数重复请求 normalized rows | 一致 |
| exact/extended 重叠区间行数 | 8 |
| exact/extended 重叠区间差异行数 | 0 |
| normalized rows sha256 | 2f3f685d9b3283fd4266d6c253efc5fab7ee5a22fe6f5a04142091605e7b9893 |
这一步的价值不在于“拉到几根 K 线”,而在于:这段历史行情从请求参数到返回摘要,再到 normalized hash,都有了可追溯证据。
以后任何时候复现这次回测,你都能先回答“输入是否一致”,再讨论“策略逻辑是否需要复盘”。
三、实测过程:请求、归一化与比对
以下是本次实测的关键代码片段,用于说明请求参数如何固定、返回数据如何归一化、hash 如何生成。
BASE_URL = "//api.tickdb.ai"
SYMBOL = "600519.SH"
INTERVAL = "1d"
ASSET_TYPE = "stock"
WINDOW_START = "2024-06-03"
WINDOW_EXACT_END = "2024-06-14"
WINDOW_EXTENDED_END = "2024-06-21"
def sha256_json(payload):
data = json.dumps(
payload,
ensure_ascii=False,
sort_keys=True,
separators=(",", ":"),
).encode("utf-8")
return hashlib.sha256(data).hexdigest()
def request_json(name, path, params, key):
query = urlencode(params)
url = f"{BASE_URL}{path}?{query}"
req = Request(
url,
headers={
"X-API-Key": key,
"Accept": "application/json",
"User-Agent": "TickDB-QL-OLD-004-evidence/1.0",
},
method="GET",
)
with urlopen(req, timeout=20) as resp:
body = resp.read().decode("utf-8", errors="replace")
status = resp.status
payload = json.loads(body)
return {
"name": name,
"endpoint": path,
"params": params,
"http_status": status,
"payload_sha256": sha256_json(payload),
"payload": payload,
}
这段代码做了三件事:
- 固定
symbol、interval、start_time、end_time等请求参数; - 保存请求返回的原始 payload;
- 对 payload 或 normalized rows 做 SHA256 留痕。
在回测复现里,hash 不是为了显得技术复杂,而是为了减少争论。如果两次 normalized rows 的 hash 一样,你至少可以先把“行情输入是否相同”这件事往前推进一步;如果 hash 不一样,就继续看是哪几行、哪些字段变了。
四、旧回测复现前,先留下这 6 件证据
回测复现时,真正有价值的不是“我又下载了一次 K 线”,而是你能不能回答:
- 上次请求的参数是什么;
- 上次抓取的时间是什么;
- 上次原始响应还在不在;
- 这次重新抓取后,重叠区间是否一致;
- 如果不一致,是价格、成交量、时间戳,还是字段结构变了;
- 这些变化会不会影响策略结果。
很多团队会保存回测结果,却不保存当时的数据入口证据。等到结果对不上,只能凭印象排查:复权口径变了?样本池变了?交易日历变了?某个清洗脚本变了?
这些问题都值得查。但排查之前,先要有一个最小基线:当时的数据输入是什么。
五、改变结束日期,为什么重叠区间也要查
旧回测复现时,常见操作是重新拉一段更长的历史数据。
直觉上,新增后面几天,不应该改变前面已经重叠的日 K。但研究系统里不能只靠直觉,最好把重叠区间拿出来逐行比较。
本次实测里,exact window 是 2024-06-03 至 2024-06-14,extended window 是 2024-06-03 至 2024-06-21。两者重叠区间共 8 根日 K,差异行数为 0,hash 一致。
这个检查的意义在于:本次同一数据源、同一 symbol、同一 interval、同一起点但不同结束日期的查询里,重叠区间没有发现差异。
下次复现回测时,如果这个检查不通过,重叠区间的数据变了,你就知道应该优先回到数据输入层排查,而不是先怀疑策略代码。
六、复权、PIT 和数据版本,不要混成一件事
回测结果变了,很多人会立刻说:“是不是有前视偏差?”
这可能对,也可能不对。
前视偏差/PIT 是一个问题,复权口径是另一个问题,数据版本留痕又是第三个问题。它们经常一起影响回测,但不应该混成一句话。
| 问题 | 关注点 | 需要记录什么 |
|---|---|---|
| 复权口径 | 价格序列如何调整 | 是否复权、前复权/后复权、不复权、复权因子版本 |
| PIT | 当时是否能知道这个信息 | 财报发布时间、成分生效日、退市/停牌状态、数据可见时间 |
| 数据版本 | 这次输入是否和上次一致 | 请求参数、抓取时间、原始响应、normalized hash |
| 样本池 | 当时策略能交易哪些标的 | 历史成分、筛选规则、剔除规则、幸存者偏差 |
分清这几个问题,下次回测结果对不上时,排查路径才不会散掉。
TickDB 能提供的是行情数据入口和证据留痕的基础:用统一 API 拉取结构化行情,保存请求参数、返回字段、原始响应和 hash。复权口径、PIT 判断和样本池版本,仍然需要和行情证据一起记录。
七、旧回测复现检查卡
下次发现旧回测跑不回去,先不要急着改策略代码。按这张卡排一遍:
| # | 检查项 | 为什么重要 |
|---|---|---|
| 1 | 策略代码和参数是否有版本 | 先确认策略逻辑有没有变化 |
| 2 | 请求参数是否完整保存 | symbol、interval、start/end、复权口径会改变输入 |
| 3 | 抓取时间是否保存 | 同一历史区间在不同抓取时间需要可追溯 |
| 4 | 原始响应是否保存 | 后续排查不能只看清洗后的结果 |
| 5 | normalized rows 是否有 hash | 快速判断两次输入是否一致 |
| 6 | 样本池/PIT/复权是否单独记录 | 避免把所有变化都归因给“数据变了” |
没有留痕时,你只能猜。有留痕时,你可以先判断:到底是输入变了,还是输入没变但策略结果变了。
八、本文不能证明什么
- 本文不构成投资建议,不推荐任何具体证券或交易方向。
- 本文不判断
600519.SH或任何标的的涨跌方向。 - 本文不承诺策略收益或回测绩效。
- 本次实测结果仅来自
2026-07-09的 TickDB REST K 线接口调用,不自动代表其他时间、其他品种或其他接口的行为。 - 同参数 hash 一致说明行情输入可对齐到同一份 normalized rows,不代表回测一定能完整复现;完整复现还需要策略代码、样本池、复权、PIT、清洗规则和执行逻辑共同一致。
声明:本文仅讨论量化回测中的数据输入管理、版本留痕与复现方法,所有接口描述来自 TickDB 公开文档。文中提及的 symbol 仅用于数据格式说明,不构成投资标的推荐。

