Ⅴ · 数据与交付 — 5.1
数据的一生
从券商导出到进入回测引擎,中间有五段。MT5 把它们全部隐藏,自研平台里每一段都要有人负责。
1 · 获取:三类来源
| 来源 | 成本 | 历史长度 | 质量 | 备注 |
|---|---|---|---|---|
| 券商 / 交易所 API | 随账户免费 | 通常较短 | 随经纪商差异大 | 与实盘执行同源,一致性最好 |
| 专业数据商 | 付费 | 长 | 高,含元数据 | Nautilus 原生支持 databento tardis |
| MT5 终端导出 | 免费 | 随经纪商 | 需完整校验 | 受 Windows 平台限制,见 5.2 |
交付要点:客户的历史数据源与实盘执行数据源若不同源,回测与实盘之间会引入系统性偏差。此项应在交付前明确,而不是上线后归因。
2 · 落地:三层不是三选一
| 是什么 | 职责 | 引入时机 | |
|---|---|---|---|
| Parquet | 列存文件格式 | 冷存、归档、交付、跨引擎交换 | 第一天 |
| DuckDB | 单机查询引擎,可直查 Parquet | 研究阶段的查询与聚合 | 第一天 |
| ClickHouse | 服务端数据库 | 多人并发查询、高频实时写入 | 出现并发需求时 |
**关键性质:Parquet 是格式而非引擎。**同一份文件可被 DuckDB、ClickHouse、pandas、Polars 与框架自身同时读取——格式选择不锁定引擎选择。
# 落地:一次写入,此后视为不可变
df.to_parquet(f"data/EURUSD/M15/{year}.parquet",
compression="zstd", index=False)
3 · 校验:七项必查
数据接入的核心交付能力。前四项在跨源比对时是最常见的差异来源:
| 检查项 | 典型问题 |
|---|---|
| 时间连续性 | 缺失区间、数据中断 |
| 时区一致性 | 服务器时间与 UTC 混用、夏令时切换 |
| 时间戳含义 | 标记的是 bar 开盘时刻还是收盘时刻 |
| 重复时间戳 | 同一时刻多条记录 |
| 价格合理性 | 尖刺、零值、买价高于卖价 |
| 非交易时段 | 周末与假日出现的 bar |
| 合约与复权 | 期货主力合约切换、股票除权除息 |
def validate(df, expected_freq="15min"):
issues = {}
# 时间连续性:按预期频率重建索引,找缺口
full = pd.date_range(df.ts.min(), df.ts.max(), freq=expected_freq, tz="UTC")
issues["missing"] = len(full.difference(df.ts))
# 重复时间戳
issues["dupes"] = int(df.ts.duplicated().sum())
# 价格合理性
issues["bad_ohlc"] = int(((df.high df.ask).sum()) # 买价高于卖价
issues["zero_price"] = int((df[["open","high","low","close"]] <= 0).any(axis=1).sum())
# 非交易时段(FX:周六休市;注意周日晚间开市)
issues["saturday"] = int((df.ts.dt.dayofweek == 5).sum())
return issues
实测佐证:Nautilus 的 ParquetDataCatalog 提供 get_missing_intervals_for_request() ✓,但源码明确说明它依据文件名模式判断哪些区间没有落盘,不检查文件的实际内容。
它能发现“这段时间没有数据文件”,不能发现“文件里的 bar 有孔洞或异常值”——上面这段校验仍需自建。
4 · 编目:三个坐标
一次回测结果的可复现需要三样东西同时被记录:
可复现的回测 = 代码版本 + 数据版本 + 参数版本
git manifest 配置文件
| 坐标 | 方案 |
|---|---|
| 代码 | git;notebook 提交前剥离输出,避免 JSON 输出污染 diff |
| 数据 | Parquet 视为不可变;同目录 manifest.json 记录来源、时间范围、行数、校验和 |
| 参数 | 配置文件随代码进版本库;回测记录中保存 commit hash |
// data/EURUSD/M15/manifest.json
{
"source": "MT5 export / BrokerX / server GMT+2",
"tz_note": "已转 UTC;原始为服务器时间,含夏令时切换",
"range": ["2015-01-01", "2024-12-31"],
"rows": 249600,
"sha256": "3f1c…",
"validated": {"missing": 412, "dupes": 0, "crossed": 0}
}
参数版本化对应 MT5 策略测试器的 .set 文件概念,区别在于与代码一同纳入版本控制。不引入 DVC / MLflow / W&B——在实验规模超出人工可追溯范围之前,上述三项已足够。
5 · 取用:研究与回测走不同的门
# 研究轨:DuckDB 直查 Parquet,不需要导入任何数据库
df = duckdb.sql("""
SELECT date_trunc('hour', ts) AS h,
first(open) o, max(high) h_, min(low) l, last(close) c
FROM 'data/EURUSD/M15/*.parquet'
WHERE ts >= '2024-01-01'
GROUP BY 1 ORDER BY 1
""").df()
# 生产轨:写进引擎自己的目录格式
# Nautilus → ParquetDataCatalog(原生 Arrow / Parquet,与上面同格式)
# LEAN → data/forex/<market>/<res>/<symbol>/YYYYMMDD_quote.zip(见 5.2)
**两个引擎在这一步的成本不同。**Nautilus 的目录原生基于 Arrow / Parquet,与研究轨同格式,几乎零转换;LEAN 需要转成它自己的分日 zip 结构——实测转换脚本 45 行,但有四个隐性约定要处理,见 5.2。
tick 还是 bar
| 存储成本 | 可重放的市场状态 | 适用 | |
|---|---|---|---|
| tick / quote | 高,单品种年度 GB 级 | 真实买卖价与点差 | 点差敏感、盘口敏感策略 |
| bar | 低 | 需通过模型假设点差与滑点 | 中低频、方向性策略 |
分层方案:tick 级以 Parquet 冷存,常用周期的 bar 预聚合供研究使用。这一选择决定回测保真度的上限——撮合引擎需要双边市场,单边序列无法成交(5.4 有实测佐证)。