【Python量化系统工程实战 #04】回测和实盘数据对不上?数据一致性校验与版本管理实战

0 阅读5分钟

策略跑得好好的,一上实盘就翻车?90% 的工程师都忽略了一个细节:回测和实盘用的数据不是同一套。本篇教你用数据指纹 + 缺失检测 + 版本快照,把数据一致性做成可验证的工程能力。

一、为什么回测和实盘数据会「对不上」

很多量化开发者都踩过这个坑:

  • 回测时用了本地 CSV,实盘时调了 API,两条数据源字段名不同、复权方式不同
  • 某天的 K 线缺失,回测时 pandas 自动填充了 NaN,实盘时却拿到了真实价格
  • 策略跑了三个月,才发现月初补采的数据把历史 bar 改了,回测结果全变

数据不一致 = 回测无效 = 实盘踩雷。本文给你一套可复用的数据校验流水线。

二、本文你将得到什么

  1. 数据指纹(MD5/SHA256):像 Git 管理代码一样管理数据版本
  2. 缺失交易日检测:自动识别 K 线跳空,避免「我以为有数据」
  3. 版本快照机制:每次拉取保存指纹,数据漂移可追踪
  4. 一致性比对脚本:两次拉取对比,发现 API 侧数据变更

三、数据指纹:给 K 线算一个「身份证号」

核心思路:把 DataFrame 按固定格式转字符串,算哈希值。同一套数据,指纹永远一样;任何字段变动,指纹立刻变。

import hashlib
import pandas as pd
from mairui import Client

client = Client("LICENCE-66D8-9F96-0C7F0FBCD073")
raw = client.stock_history("600519", "d", "f")
df = pd.DataFrame(raw)
df["date"] = pd.to_datetime(df["t"])

# 按固定列序转 CSV 字符串,确保可复现
cols = ["date", "o", "h", "l", "c", "v"]
body = df[cols].to_csv(index=False)

md5 = hashlib.md5(body.encode("utf-8")).hexdigest()
sha256 = hashlib.sha256(body.encode("utf-8")).hexdigest()
print(f"MD5: {md5}")
print(f"SHA256: {sha256}")

实测输出(50 条日线):

MD5: dcfa43d042a902cedeed787c4d9af20a
SHA256: a57e8cf2aad7e955273939256819845ad4160d78e015aeff95e71cb82944b7aa
rows: 50, cols: 6
start: 2025-09-15, end: 2025-12-01

为什么用 CSV 字符串而不是 df.to_json()?

  • CSV 列序固定、无索引噪声,不同 pandas 版本的 JSON 输出可能带类型标记
  • MD5 足够做快速比对;SHA256 防碰撞,用于长期存档

四、缺失交易日检测:别被「看起来连续」骗了

K 线缺一天,策略信号可能完全跑偏。检测逻辑:遍历日期,发现非周末的连续跳空 >1 天即报警。

from datetime import timedelta

def detect_missing_dates(df):
    dates = sorted(df["date"].dt.date.unique())
    gaps = []
    for i in range(1, len(dates)):
        prev, curr = dates[i-1], dates[i]
        delta = (curr - prev).days
        if delta > 1:
            missing = [prev + timedelta(days=d) for d in range(1, delta)
                       if (prev + timedelta(days=d)).weekday() < 5]
            if missing:
                gaps.append({"after": prev.isoformat(),
                             "before": curr.isoformat(),
                             "missing": [d.isoformat() for d in missing]})
    return gaps

gaps = detect_missing_dates(df)
print(f"发现 {len(gaps)} 处缺失")

实测输出:

发现 1 处缺失:
在 2025-09-30 ~ 2025-10-09 之间缺失 6 天
['2025-10-01', '2025-10-02', '2025-10-03', ...]

注意:2025-10-01 至 10-07 是国庆假期,属正常休市。检测逻辑只识别「非周末跳空」,节假日需结合交易所日历做更精确判断(可用 exchange_calendars 库)。

五、版本快照:每次拉取都留一个「备份标签」

把指纹 + 元数据写成 JSON,后续数据漂移可追溯:

import json
from datetime import datetime

snapshot = {
    "version": 1,
    "created_at": datetime.now().isoformat(),
    "code": "600519",
    "fingerprint": {
        "md5": md5,
        "sha256": sha256,
        "rows": len(df),
        "cols": len(cols),
    },
    "gaps": gaps,
    "source": "mairui_api",
}

with open("snapshot_v1.json", "w", encoding="utf-8") as f:
    json.dump(snapshot, f, ensure_ascii=False, indent=2)

快照文件内容:

{
  "version": 1,
  "created_at": "2026-09-10T10:18:00",
  "code": "600519",
  "fingerprint": {
    "md5": "dcfa43d042a902cedeed787c4d9af20a",
    "sha256": "a57e8cf2aad7e955273939256819845ad4160d78e015aeff95e71cb82944b7aa",
    "rows": 50,
    "cols": 6
  },
  "gaps": [...],
  "source": "mairui_api"
}

六、一致性比对:两次拉取,指纹是否相同?

API 侧数据可能因复权调整、补采而变更。定期重拉同一区间,比对指纹即可发现:

df2 = pd.DataFrame(client.stock_history("600519", "d", "f"))
body2 = df2[cols].to_csv(index=False)
md5_2 = hashlib.md5(body2.encode("utf-8")).hexdigest()

if md5 == md5_2:
    print("✅ 数据一致")
else:
    print("❌ 数据漂移,需排查原因")

实测两次拉取 MD5 完全一致,说明该区间数据稳定。

七、常见坑

  1. 哈希前未排序:如果 DataFrame 行序不同(比如两次查询默认排序有差异),指纹会变。务必 df.sort_values("date") 后再算哈希。
  2. 浮点数精度:11.05 vs 11.0500000001 会导致哈希不同。如需容忍微小差异,可先 df.round(4) 再转字符串。
  3. 列序不一致:df[cols] 显式指定列序,不要依赖 df.columns 默认顺序。
  4. 忽略复权变更:API 返回的日线可能是「前复权」,历史数据会随时间推移而变(因为复权基准每天都在变)。数据漂移不一定是 bug,可能是正常的复权调整。

八、小结

本文解决了一个被严重低估的工程问题:数据一致性。

  • 数据指纹(MD5/SHA256):给 K 线发身份证,任何变动可追踪
  • 缺失检测:自动识别非周末跳空,避免「静默缺失」
  • 版本快照:JSON 存档,三个月后发现数据变了,能翻出「当时是什么」
  • 一致性比对:两次拉取比对,第一时间发现 API 侧变更

下一篇(#05)我们将解决「多策略并行」——当你有 3 个策略同时跑,怎么避免它们互相抢资源、写乱日志?


免责声明:本文仅供技术学习交流,不构成任何投资建议。量化策略回测表现不代表未来收益,投资有风险,决策需谨慎。

代码与文档:github.com/MaiRuiApi