2026年8月19日 1 分钟阅读

量化回测最难防的不是 SQL 写错:用 h5i-db 把未来数据挡在决策时点之外

tinyash 0 条评论

在研究交易策略时,最危险的问题往往不是指标公式写错,而是数据“悄悄穿越”了时间边界。比如你在 10:00 生成交易信号,却让 SQL 结果混入 10:01 才能知道的成交记录;回测曲线会变得漂亮,但那不是策略能力,而是未来信息造成的泄漏。

h5i-db 是一个用 Rust 编写、嵌入式运行的时序数据库与回测引擎。它把时序 SQL、事件驱动回测和终端 Notebook 放到同一个本地工作区里。项目采用 Apache-2.0 许可证,最新公开版本为 v0.1.6。对开发者而言,它特别值得关注的点不是“又一个 SQL 引擎”,而是把决策时点(decision time)变成查询接口的一部分:传给 pandas 或后续策略代码的数据,可以被限制为不晚于指定时刻。

先区分两个时间:事件发生与当时可知

时序数据通常至少有两种时间语义:一是事件本身发生的时间,例如一笔成交的 ts;二是研究者或系统实际看到它的时间。行情修订、延迟数据源、日终回填和人工清洗都会让两者产生差异。

很多回测脚本只有一个 WHERE ts <= ... 条件。这个条件当然有用,但它容易散落在各段 SQL、DataFrame 筛选和特征工程中;某次重构漏掉一个过滤器,泄漏就重新出现。h5i-db 的 --decision-time 参数提供了更集中的边界:查询在这个时点之后的行不可读。它不能替你判断数据源的可得性,也不能神奇修复错误时间戳,却能把“不要读取未来”从团队约定变成可复用的执行约束。

从 Parquet 建立一个可追溯的最小研究库

下面的命令来自项目 README。假设 ticks.parquet 已含时间列 ts,以及 symbolpricesize 等字段:

cargo install h5i-db-cli

h5i-db init market.db
h5i-db create-table market.db trades --like ticks.parquet --time-column ts
h5i-db ingest market.db trades ticks.parquet --idempotency-key load-1

h5i-db query market.db \
  "SELECT symbol, vwap(price, size) AS vwap FROM trades GROUP BY symbol"

h5i-db query market.db \
  "SELECT count(*) FROM trades" \
  --decision-time 2026-07-01T00:00:00Z

前三步分别初始化嵌入式数据库、按 Parquet 的 schema 创建带时间列的表、再导入文件。--idempotency-key 的价值在于给一次导入操作一个可识别的键,适合放进可重复执行的研究流水线。示例中的 vwap(price, size) 是成交量加权平均价;项目还声明支持 ASOF join、时区感知的 time_bucket、补齐/重采样、滚动窗口和 ewma 等时序操作。

真正需要审视的是最后一条命令:这里的时间不是 SQL 业务条件的装饰,而是读取上限。实际研究中,建议把每次信号生成的截止时刻作为配置项传入,并把该值、数据版本和策略提交一并记录。这样某次结果异常时,才能回答“当时究竟看到了哪些行”,而不是只保存一个收益率数字。

不要把推荐功能误当成正确性证明

点时读取只能降低一类泄漏风险。至少还有三件事必须由研究流程负责。

第一,确认原始数据的时间列语义。交易所撮合时间、供应商推送时间和本地落盘时间不能混用;若数据晚到,单纯按事件时间截断仍可能让历史回放比实时系统更理想。第二,企业行为、停牌、退市标的和成分股变更也会制造幸存者偏差,数据库不会自动替你补齐这些事实。第三,训练、调参和最终留出集必须分开;反复查看同一段测试期的结果,本质上仍是在对测试集过拟合。

因此,一个更稳妥的做法是:先以固定 decision time 生成特征,再在独立的未来窗口计算表现;参数筛选只发生在训练窗口;最后才运行一次不再改参数的留出验证。任何“高夏普率”结论都应能追溯到输入文件、导入批次、读取边界和代码版本。

用版本、fork 与终端 Notebook 缩短试验闭环

h5i-db 的另一个工程化设计是原子化、版本化写入:项目文档说明可以读取任意历史版本,错误导入可以通过 restore 回退。它还提供共享底层数据的数据库 fork,用于“fork、修改、评估、丢弃”的试验循环。这里的关键取舍是把试验隔离做得便宜,而不是让所有试验自动可信:fork 仍然应继承清晰的输入版本与 decision time,不能成为随意覆盖基准数据的理由。

如果团队已经用 Jupyter 保存研究过程,也不需要换文件格式。README 中的终端 Notebook 命令操作普通 .ipynb 文件;安装 Python kernel 后,可以在终端查看、执行或导出:

pip install ipykernel
h5i-db nb new research.ipynb --kernel python3 --db market.db
h5i-db nb view research.ipynb
h5i-db nb exec research.ipynb

这使 Notebook 可以继续由 JupyterLab 打开,同时让自动化任务通过 nb exec 以非交互方式运行。适合把“数据导入—边界查询—特征计算—回测输出”收敛为可审计的项目资产,而不是散落在临时 shell 历史里。

什么时候值得尝试

h5i-db 更适合需要本地嵌入式工作流、时序 SQL、回测和 Notebook 联动的量化研究或数据工程团队。若你的核心需求是大规模多租户在线分析、跨地区高可用,仍应优先评估成熟的分布式数据库与运维能力;若只是一次性分析几个 CSV,直接使用现有的 DataFrame 工具可能更轻。

它的正确打开方式也不是立刻把所有策略迁移过去。挑一个边界清晰的策略、固定一份 Parquet 输入,并把 --decision-time 写入自动化脚本。先验证:同一输入在不同机器上是否得到相同的可见数据范围;故意加入晚于边界的记录后,结果是否保持不变。只有这些失败模式被实际测试过,点时读取才会从宣传词变成研究系统的防线。

把边界检查放进 CI,而不是放在复盘会上

研究工作流一旦交给多个脚本、Notebook 或 Agent 协作,最容易失效的恰恰是“大家应该记得过滤时间”的口头约定。可以为每个策略准备一份极小的合成数据:前半段是决策时刻之前的正常记录,后半段故意放入一条会显著改变 VWAP、均线或信号方向的未来记录。测试脚本分别在边界前后执行同一查询:边界之前的输出不应包含该记录,边界放宽后才应发生变化。

这个测试不需要证明策略赚钱,只需验证研究系统没有跨越自己声明的时间边界。它尤其适合在导入逻辑、SQL 模板、特征库或 Agent 自动改写 Notebook 后运行。若测试失败,优先检查时间列是否被错误转换为本地时区、decision time 是否真正传到最终查询、以及下游 DataFrame 是否又额外合并了未经约束的数据。

还应把数据版本与输出绑定。一次可复现的研究记录至少应保存:输入文件的校验信息或数据版本、导入使用的 idempotency key、决策时点、查询文本、策略提交版本,以及生成结果的时间。这样才能区分“市场条件变化导致策略失效”和“重跑时读取集合已经变化”这两种完全不同的问题。

对于由 Agent 执行的研究任务,还要把写入权限与读取权限分开。让 Agent 先在 fork 或临时数据库中导入、生成查询与回测报告;只有人工或独立校验任务确认输入范围、参数和结果解释后,才把经过审查的产物提升为基准。即使底层写入可回退,也不意味着每一次自动修改都应该直接进入共享研究库。把“可撤销”当作缓冲,而不是跳过评审的理由,能避免试验结果与正式数据混在一起。

另一个常被忽略的边界是成本。嵌入式数据库降低了启动和复制试验的摩擦,但不会减少数据清洗、存储、行情授权和人工审阅的成本。策略参数越多,越应限制搜索空间、事先定义评价指标,并记录失败实验;否则,快速 fork 只会更快地产生难以解释的候选。将性能优化排在可得性与可复现性之后,通常更符合研究系统的长期价值。

对团队来说,最实用的收益不是减少一条命令,而是把时间可见性明确成接口、测试和审计项。先用一张表、一个简单指标和一条故意的未来记录跑通这套流程,再扩展到多标的、事件回放和参数搜索,通常比直接搭建庞大回测平台更可靠。

相关链接

发表评论

你的邮箱地址不会被公开,带 * 的为必填项。