策略跑得好好的,一上实盘就翻车?90% 的工程师都忽略了一个细节:回测和实盘用的数据不是同一套。本篇教你用数据指纹 + 缺失检测 + 版本快照,把数据一致性做成可验证的工程能力。
一、为什么回测和实盘数据会「对不上」
很多量化开发者都踩过这个坑:
- 回测时用了本地 CSV,实盘时调了 API,两条数据源字段名不同、复权方式不同
- 某天的 K 线缺失,回测时 pandas 自动填充了 NaN,实盘时却拿到了真实价格
- 策略跑了三个月,才发现月初补采的数据把历史 bar 改了,回测结果全变
数据不一致 = 回测无效 = 实盘踩雷。本文给你一套可复用的数据校验流水线。
二、本文你将得到什么
- 数据指纹(MD5/SHA256):像 Git 管理代码一样管理数据版本
- 缺失交易日检测:自动识别 K 线跳空,避免「我以为有数据」
- 版本快照机制:每次拉取保存指纹,数据漂移可追踪
- 一致性比对脚本:两次拉取对比,发现 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 完全一致,说明该区间数据稳定。
七、常见坑
- 哈希前未排序:如果 DataFrame 行序不同(比如两次查询默认排序有差异),指纹会变。务必
df.sort_values("date")后再算哈希。 - 浮点数精度:
11.05vs11.0500000001会导致哈希不同。如需容忍微小差异,可先df.round(4)再转字符串。 - 列序不一致:
df[cols]显式指定列序,不要依赖df.columns默认顺序。 - 忽略复权变更:API 返回的日线可能是「前复权」,历史数据会随时间推移而变(因为复权基准每天都在变)。数据漂移不一定是 bug,可能是正常的复权调整。
八、小结
本文解决了一个被严重低估的工程问题:数据一致性。
- 数据指纹(MD5/SHA256):给 K 线发身份证,任何变动可追踪
- 缺失检测:自动识别非周末跳空,避免「静默缺失」
- 版本快照:JSON 存档,三个月后发现数据变了,能翻出「当时是什么」
- 一致性比对:两次拉取比对,第一时间发现 API 侧变更
下一篇(#05)我们将解决「多策略并行」——当你有 3 个策略同时跑,怎么避免它们互相抢资源、写乱日志?
免责声明:本文仅供技术学习交流,不构成任何投资建议。量化策略回测表现不代表未来收益,投资有风险,决策需谨慎。
代码与文档:github.com/MaiRuiApi