摘要
很多人做 A 股回测,拿到历史 K 线数据就直接跑策略。但真正让回测结果失真的,往往不是模型,而是那份看起来完整的数据------symbol 被悄悄改过后缀、时间范围不明确、K 线周期混用、OHLCV 字段口径不一致、异常缺口被默认值填平。本文设计一个 BacktestDataGate 验收流程:定义 14 个验收字段、5 个核心检查项、4 类异常分支,用 Python 伪代码帮你把回测数据验收标准化。不写策略,只写"这份数据能不能喂进回测系统"。
很多人拿到历史 K 线数据后的第一件事,是导入回测框架跑一遍。策略代码跑通了,收益曲线画出来了,看起来一切顺利。
一个月后复查,发现某个时间段内的日线成交量全是 0。不是接口没返回,是那段时间标的停牌,返回了空数据,你的入库脚本用默认值把空给填平了。整整一段行情,回测框架在拿"0 成交量"做信号计算。
短数据的问题你看得见。长数据的问题会被回测曲线包装得很平滑,直到某个信号异常逼你回头翻原始数据。
所以,历史 K 线进入回测框架之前,先做 5 个检查。下面给出一套 BacktestDataGate 的验收流程。

一、14 个验收字段:一条 K 线记录的完整定义
在动代码之前,先定义清楚一条历史 K 线记录应该包含哪些字段。这些字段不只是"存下来",而是"每一个字段都能在验收时被核对"。
| 字段名 | 来源 | 说明 |
|---|---|---|
request_symbol |
请求参数 | 你请求时传的 symbol,如 600519.SH |
response_symbol |
响应字段 | 接口实际返回的 symbol,逐字符比对 |
interval |
请求参数 | K 线周期,如 1d、1w、1M |
start_time |
请求参数 | 查询起始时间 |
end_time |
请求参数 | 查询结束时间 |
time |
响应字段 | 单根 K 线的时间戳 |
open |
响应字段 | 开盘价 |
high |
响应字段 | 最高价 |
low |
响应字段 | 最低价 |
close |
响应字段 | 收盘价 |
volume |
响应字段 | 成交量 |
raw_snapshot |
响应体 | 原始 JSON 快照,用于复查 |
checked_at |
客户端生成 | 验收通过并写入的时间 |
error/message |
响应字段 | 异常时的错误信息 |
request_symbol 和 response_symbol 是两个字段,必须分开存。 你以为自己在查 600519,接口返回的可能已经是 600519.SH。如果只存一个 symbol 字段,这个修正过程就被抹掉了。复查时你不知道自己当初传了什么、接口实际返回了什么。
二、BacktestDataGate:5 个检查的验收流程
下面用 Python 伪代码把验收流程写成 BacktestDataGate 类。它不连接任何具体的 API------只定义检查规则,让你集成到自己的回测数据管道中。
python
from decimal import Decimal, InvalidOperation
from datetime import datetime, timezone
from typing import List, Dict, Any, Optional
import hashlib
import json
class BacktestDataGate:
"""
回测数据入口验收器。
对历史 K 线数据执行 5 项检查,全部通过后才放行进入回测系统。
使用 Decimal 处理所有价格和成交量,避免浮点精度问题。
"""
def __init__(self):
self.results = []
def _hash(self, data: Any) -> str:
"""生成原始快照的 SHA256 摘要。"""
raw = json.dumps(data, sort_keys=True, ensure_ascii=False, default=str)
return hashlib.sha256(raw.encode()).hexdigest()[:16]
# ------------------------------------------------------------
# 检查 1:symbol 一致性
# ------------------------------------------------------------
def check_symbol_consistency(self, request_symbol: str,
response_symbol: str) -> Dict:
"""请求的 symbol 和返回的 symbol 必须逐字符一致。"""
passed = request_symbol == response_symbol
return {
"check": "symbol_consistency",
"passed": passed,
"detail": f"request={request_symbol}, response={response_symbol}",
"issue": None if passed else "symbol mismatch"
}
# ------------------------------------------------------------
# 检查 2:时间范围覆盖
# ------------------------------------------------------------
def check_time_range(self, klines: List[Dict],
expected_start: str,
expected_end: str) -> Dict:
"""K 线数据的时间范围应覆盖请求的起止区间。"""
if not klines:
return {"check": "time_range", "passed": False,
"detail": "empty klines", "issue": "no data returned"}
actual_start = klines[0].get("time")
actual_end = klines[-1].get("time")
issues = []
if actual_start is None or actual_end is None:
issues.append("missing time field in klines")
return {
"check": "time_range",
"passed": len(issues) == 0,
"detail": f"expected={expected_start}~{expected_end}, actual={actual_start}~{actual_end}",
"issue": "; ".join(issues) if issues else None
}
# ------------------------------------------------------------
# 检查 3:K 线周期一致性
# ------------------------------------------------------------
def check_interval_consistency(self, klines: List[Dict],
expected_interval: str) -> Dict:
"""相邻 K 线之间的时间间隔应与声明的周期一致。"""
if len(klines) < 2:
return {"check": "interval_consistency", "passed": True,
"detail": "only one kline, skip interval check", "issue": None}
# 按 expected_interval 推算预期间隔(教学占位,实际按交易日历精确计算)
interval_map = {"1d": 86400, "1w": 604800, "1M": 2592000}
expected_sec = interval_map.get(expected_interval)
if expected_sec is None:
return {"check": "interval_consistency", "passed": True,
"detail": f"unknown interval {expected_interval}, skip", "issue": None}
issues = []
for i in range(1, len(klines)):
t1 = klines[i-1].get("time")
t2 = klines[i].get("time")
if t1 and t2 and isinstance(t1, (int, float)) and isinstance(t2, (int, float)):
diff = abs(t2 - t1)
if diff > expected_sec * 2:
issues.append(f"gap at index {i}: {diff}sec")
return {
"check": "interval_consistency",
"passed": len(issues) == 0,
"detail": f"checked {len(klines)} klines",
"issue": "; ".join(issues) if issues else None
}
# ------------------------------------------------------------
# 检查 4:OHLCV 字段校验
# ------------------------------------------------------------
def check_ohlcv_fields(self, klines: List[Dict]) -> Dict:
"""
逐 bar 校验 OHLCV 字段。
使用 Decimal 解析价格和成交量,拒绝 NaN/Infinity/负数价格。
"""
issues = []
for i, bar in enumerate(klines):
for field in ("open", "high", "low", "close", "volume"):
raw = bar.get(field)
if raw is None:
issues.append(f"bar[{i}].{field} is missing")
continue
try:
d = Decimal(str(raw))
if not d.is_finite():
issues.append(f"bar[{i}].{field} is not finite: {raw}")
if field in ("open", "high", "low", "close") and d < 0:
issues.append(f"bar[{i}].{field} is negative: {raw}")
except (InvalidOperation, ValueError):
issues.append(f"bar[{i}].{field} parse failed: {raw}")
return {
"check": "ohlcv_fields",
"passed": len(issues) == 0,
"detail": f"checked {len(klines)} bars, {len(issues)} field issues",
"issue": "; ".join(issues[:10]) if issues else None
}
# ------------------------------------------------------------
# 检查 5:异常与缺口
# ------------------------------------------------------------
def check_anomalies(self, response: Dict) -> Dict:
"""
检查响应层面是否存在异常。
返回 code 非 0、data 为空、错误信息时记录异常。
"""
code = response.get("code")
message = response.get("message", "")
data = response.get("data", [])
issues = []
if code is not None and code != 0:
issues.append(f"response code={code}, message={message}")
if not data:
issues.append("data is empty")
return {
"check": "anomalies",
"passed": len(issues) == 0,
"detail": f"code={code}, data_len={len(data) if isinstance(data, list) else 'N/A'}",
"issue": "; ".join(issues) if issues else None
}
# ------------------------------------------------------------
# 完整验收流程
# ------------------------------------------------------------
def validate(self,
request_symbol: str,
response_symbol: str,
expected_start: str,
expected_end: str,
expected_interval: str,
klines: List[Dict],
response: Dict) -> Dict:
"""
执行全部 5 项检查,生成验收报告。
任何一项不通过,整批数据标记为 need_review。
"""
checks = [
self.check_symbol_consistency(request_symbol, response_symbol),
self.check_time_range(klines, expected_start, expected_end),
self.check_interval_consistency(klines, expected_interval),
self.check_ohlcv_fields(klines),
self.check_anomalies(response),
]
all_passed = all(c["passed"] for c in checks)
failed = [c["check"] for c in checks if not c["passed"]]
report = {
"checked_at": datetime.now(timezone.utc).isoformat(),
"request_symbol": request_symbol,
"response_symbol": response_symbol,
"interval": expected_interval,
"time_range": f"{expected_start} ~ {expected_end}",
"bar_count": len(klines),
"all_checks_passed": all_passed,
"failed_checks": failed,
"checks": checks,
"raw_snapshot_hash": self._hash(response),
"status": "ok" if all_passed else "need_review"
}
self.results.append(report)
return report
核心逻辑 :5 个检查全部通过,这批 K 线数据才能写进回测数据库。任何一项失败,状态标记为 need_review------进入回测前必须人工确认。
三、4 类异常分支
| 异常场景 | 触发条件 | 处理方式 |
|---|---|---|
| symbol 不一致 | request_symbol ≠ response_symbol |
阻断,检查接口是否做了自动修正 |
| 时间范围不匹配 | K 线数组为空或起止时间明显偏离 | 阻断,确认查询参数和接口返回 |
| 字段解析失败 | Decimal 解析异常、NaN、Infinity、负价格 | 阻断,记录具体 bar 和字段名 |
| 响应异常 | 业务码非 0、data 为空、错误信息返回 | 阻断,检查 Key 权限、symbol 格式、当前时段 |
四、TickDB 的工程边界
上面这套 BacktestDataGate 验收流程,不管你用的是哪个行情数据源,都可以直接集成。如果你选择 TickDB 作为历史 K 线入口,需要以官方文档或审核为准,确认接口端点、鉴权方式(REST 使用 X-API-Key Header)、字段返回结构和错误码语义。
TickDB 在这里的工程价值是:它的 K 线接口支持 interval 参数查询,返回结构化的 time、open、high、low、close、volume 字段,异常场景(如超范围查询、无效 symbol)有明确的错误返回,便于你把 5 个检查项程序化执行。但它不替你完成验收------所有字段路径和语义确认,仍以你的实测和官方文档为准。
本文不讨论 :TickDB 的 MCP 工具(使用 X-TickDB-Key)和 WebSocket 推送(使用 api_key),这两者是独立的命名空间,鉴权方式和接入场景与 REST 不同,不混用。
五、回测数据验收的最小检查表
| 检查项 | 通过标准 | 失败时 |
|---|---|---|
| symbol | request_symbol 与 response_symbol 逐字符一致 | 阻断,标注 mismatch |
| 时间范围 | K 线数组非空,起止时间覆盖请求区间 | 阻断,标注时间偏差 |
| K 线周期 | 相邻 bar 时间间隔与声明周期一致 | 阻断,标注 gap 位置 |
| OHLCV 字段 | 全部可解析为有限 Decimal,价格非负 | 阻断,标注具体 bar 和字段 |
| 异常返回 | 业务码为 0,data 非空,无错误信息 | 阻断,记录错误详情 |
你们现在的回测数据管道,入库前会做这 5 个检查吗?踩过最多的坑是 symbol 不一致、时间缺口,还是字段解析异常?
📡 本文以 TickDB REST API 作为历史 K 线数据示例。文中代码为 Python 教学骨架,不依赖任何特定数据源的端点或字段。本文仅讨论数据验收的工程方法,不构成投资建议。
标签: Python, 回测, 数据验收, K线, TickDB