本节摘要:多源数据的默认状态是「互相看不懂」:同一时刻在不同源里差着时区、同一价格差着精度、同一交易对差着写法。本节先做三重归一:时区(对内一律 UTC 存储,展示层再本地化)、精度(价格与数量按品种规则取整,浮点比较禁用等号)、符号(建立规范名到各源方言的映射表);再处理缺口:用时间步进扫描检测缺口,用 REST 回填历史区间,回填不了的要留「已知缺口」标记而不是假装连续;然后给出一张十项质检清单作为数据放行的验收标准;最后用一个可运行的缺口扫描脚本把检测落地。本节是数据层的出口:从这里走出去的数据,才被允许进入第 4 章的指标与第 5 章的策略。
| 维度 | 不统一的样子 | 归一约定 |
|---|---|---|
| 时区 | 源 A 给 UTC 时间戳,源 B 给带偏移的本地时间,源 C 给字符串 | 对内一律 UTC(毫秒时间戳或等价形式);展示层再转本地时区 |
| 精度 | 同一价格 42000.5 与 42000.50000;浮点累加出 0.30000000000000004 | 按品种规则统一小数位;比较用容差或整数化最小变动单位 |
| 符号 | BTC-USDT、BTC/USDT、BTCUSDT 并存 | 建立规范名(自选一种)到各源方言的映射表,落库只用规范名 |
# symbol_map.py —— 符号与精度归一示意(教学示意,映射表以实际数据源为准) CANONICAL = "BTC/USDT" # 规范名:自选一种,全库唯一 DIALECTS = { "exchange_a": "BTC-USDT", "exchange_b": "BTCUSDT", "exchange_c": "btc/usdt", } TO_CANONICAL = {v.lower(): CANONICAL for v in DIALECTS.values()} TICK_SIZE = 0.5 # 最小变动单位(示意值) def to_canonical(raw: str) -> str: key = raw.strip().lower().replace("_", "-").replace("/", "-") return TO_CANONICAL.get(key, TO_CANONICAL.get(raw.strip().lower(), raw)) def round_price(price: float) -> float: return round(round(price / TICK_SIZE) * TICK_SIZE, 2) if __name__ == "__main__": print(to_canonical("BTCUSDT")) # BTC/USDT print(round_price(42000.37)) # 42000.5(按示意最小变动单位取整)
时区是三重里最容易埋雷的:一次「差八小时」能让两套策略回测结果天差地别。约定要硬:入库前转换、库内无本地时间、出库展示时才本地化——三处任何一处走捷径,前面两处就白做。
缺口来自断流、维护窗口、上线晚于回测起点。检测思路是步进扫描:相邻两根 K 线的时间差超过一个步长,中间就缺了若干根。
# gap_scan.py —— K 线缺口检测(教学示意,可独立运行) from datetime import datetime, timedelta def scan_gaps(bars, step_minutes): """bars: [(datetime, close), ...] 升序;返回缺口描述列表""" step = timedelta(minutes=step_minutes) gaps = [] for prev, curr in zip(bars, bars[1:]): if curr[0] - prev[0] > step: missing = int((curr[0] - prev[0]) / step) - 1 gaps.append({"after": prev[0], "until": curr[0], "missing": missing}) return gaps if __name__ == "__main__": demo = [(datetime(2026, 1, 1, 0, 0), 100.0), (datetime(2026, 1, 1, 0, 5), 100.2), (datetime(2026, 1, 1, 0, 30), 101.0)] for g in scan_gaps(demo, step_minutes=5): print(g["after"], "→", g["until"], "缺", g["missing"], "根") # 期望输出:缺 4 根(00:05 与 00:30 之间)
回填的边界要认清:能补的用 REST 补(按 3.1 节的通道接力),补不了的必须显式标记「已知缺口」,让下游指标与回测自行决定跳过或告警——绝不允许用插值悄悄抹平缺口,那等于伪造历史。补回来的数据还要过一遍「与既有邻接数据无矛盾」的检查(闭合价衔接、量纲一致)。
| 序 | 检查项 | 达标标准 |
|---|---|---|
| 1 | 时区统一 | 全库时间戳可追溯到 UTC 口径 |
| 2 | 符号规范 | 无方言符号入库,映射表覆盖在用数据源 |
| 3 | 精度一致 | 价格、数量符合品种规则,无浮点尾影 |
| 4 | 网格完整 | 步进扫描无未标记缺口 |
| 5 | 缺口处置 | 已回填或已标记,无插值抹平 |
| 6 | 值域合理 | 价格大于零、量非负、无穿越上下限的异常值 |
| 7 | 重复检测 | 同键(符号+时间戳)无重复行 |
| 8 | 闭合价衔接 | 相邻 K 线开收价符合市场可解释关系 |
| 9 | 源间抽核 | 多源重合区间抽样对比,偏差在容差内 |
| 10 | 新鲜度 | 最新数据时间距当前在预期延迟内 |
清单用法:新接入一个数据源、一次大范围回填、一次异常重启之后,各跑一遍;把结果留档。第 10 项的新鲜度阈值按数据类型设定(K 线分钟级、宏观按发布节奏)。
多源数据 ──▶ 三重归一 ──▶ 落库 ──▶ 十项清单 ──▶ 放行 │ │ └── 不合格:修复或标记缺口,绝不放行带病数据 ──┘
这套流程的精神与《量化投资方法论》第 15 章《批判性思维与研究纪律》一脉相承:对数据的每一次纵容,都会在回测报告里连本带利地还回来。质检不是数据层的收尾杂务,而是研究可信度的地基——第 6 章的每一个绩效数字,可信度上限都在这里被决定。
数据层至此闭环。接下来让合格的数据开口说话——第 4 章把行情翻译成指标:overlay 叠加线、marker 事件点、band 区间带,三种形态覆盖你见过的绝大多数技术指标。