量化回测结果复现不了?代码没改,先查历史 K 线数据输入版本

用户头像Jacktick
2026-07-10 发布

半年前跑出漂亮曲线的回测策略,今天同样的代码和参数却得不到相同结果。问题很可能不在策略本身,而在数据输入层——复权方式、样本池版本、行情抓取时间、原始响应是否留存,任何一项缺失都会让旧回测无法复现。

半年前跑出来的回测曲线很好看。今天把代码翻出来,参数没改,逻辑没改,区间也没改,但结果就是对不上。

第一反应是代码哪里改坏了。查完提交记录,策略代码确实没动。这时候你开始怀疑自己:是不是当时看错了?是不是记错了回测参数?

真正变化的,往往不是策略,而是输入数据。

复权用的是前复权还是后复权?股票池是当时的版本还是今天的版本?停牌和退市怎么处理?行情数据是哪一天抓的?今天重新下载同一段历史 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-032024-06-14
extended window 2024-06-032024-06-21
实测时间 2026-07-09 20:52:53 +08:00

随后做了两类比较:

  1. 同参数重复请求,看 normalized rows 是否一致;
  2. 固定起点、改变结束日期,看两个窗口的重叠区间是否一致。
检查项 结果
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,
    }

这段代码做了三件事:

  1. 固定 symbolintervalstart_timeend_time 等请求参数;
  2. 保存请求返回的原始 payload;
  3. 对 payload 或 normalized rows 做 SHA256 留痕。

在回测复现里,hash 不是为了显得技术复杂,而是为了减少争论。如果两次 normalized rows 的 hash 一样,你至少可以先把“行情输入是否相同”这件事往前推进一步;如果 hash 不一样,就继续看是哪几行、哪些字段变了。


四、旧回测复现前,先留下这 6 件证据

回测复现时,真正有价值的不是“我又下载了一次 K 线”,而是你能不能回答:

  1. 上次请求的参数是什么;
  2. 上次抓取的时间是什么;
  3. 上次原始响应还在不在;
  4. 这次重新抓取后,重叠区间是否一致;
  5. 如果不一致,是价格、成交量、时间戳,还是字段结构变了;
  6. 这些变化会不会影响策略结果。

很多团队会保存回测结果,却不保存当时的数据入口证据。等到结果对不上,只能凭印象排查:复权口径变了?样本池变了?交易日历变了?某个清洗脚本变了?

这些问题都值得查。但排查之前,先要有一个最小基线:当时的数据输入是什么。


五、改变结束日期,为什么重叠区间也要查

旧回测复现时,常见操作是重新拉一段更长的历史数据。

直觉上,新增后面几天,不应该改变前面已经重叠的日 K。但研究系统里不能只靠直觉,最好把重叠区间拿出来逐行比较。

本次实测里,exact window 是 2024-06-032024-06-14,extended window 是 2024-06-032024-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 仅用于数据格式说明,不构成投资标的推荐。

评论