ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

【量化系统从0到1】回测引擎:性能不是难点,语义才是

【量化系统从0到1】回测引擎:性能不是难点,语义才是 回测是量化系统的裁判。策略好不好不看信号生成得多漂亮看回测数字。裁判出错后面的一切都是沙上建塔。回测引擎有两种出错方式语义错——数字不对但错得隐蔽性能慢——跑不完但错得显眼。我做完这个系统最大的体会是语义错的代价远大于性能慢。所以这篇讲回测引擎主线不是怎么跑得快是怎么把 A 股语义做对。为什么自研不用现成回测库设计稿第一次讨论回测引擎时第一个问题是用现成库还是自研现成库很多backtrader、zipline、vectorbt。但它们有个共同前提——都是美股语义。美股 T0、无涨跌停、可以卖空、没有 100 股整数倍费用结构也完全不同。A 股语义T1、一字涨跌停买不进卖不出、停牌不可交易、100 股整数倍、佣金印花税过户费三件套。用现成库要么接受它的语义改自己的需求要么花大力气改库两条路都不轻松。而我的需求本身很窄日线、盘后、单策略逐票回测。撮合逻辑说到底是一个逐 bar 循环拿到 T 日信号在 T1 开盘撮合处理费用和涨跌停。这个核心用 Python 写了 142 行。现成库省下的时间不够抵消它和 A 股语义的差异。自研要分清哪些必须做对、哪些可以以后再说A 股语义是功能需求必须做对性能是优化需求按需再做。后面所有决策都按这个区分来。A 股语义清单哪些必须做对设计稿把 A 股语义列成了清单落地时一条条对着实现T 日收盘信号 → T1 开盘成交。信号用后复权价计算防止除权造成假交叉成交用 T1 开盘价。这是无未来函数的根基。一字涨停买不进、一字跌停卖不出。用真实价和涨跌停价比较开盘价 ≥ 涨停价买单拒开盘价 ≤ 跌停价卖单拒。停牌不可交易。停牌日的信号作废不顺延。100 股整数倍。买入数量向下取整到 100 的倍数。费用三件套佣金万 2.5最低 5 元 印花税卖出 0.05% 过户费0.001% 双向滑点可选默认 0。这张清单看着不起眼但每一条都对应真实场景。2023-2026 的真实行情里两条拒单规则各触发过一次某票信号日次日一字涨停买单被拒另一票信号后恰逢吸收合并停牌交易挂起。合成数据自测过的逻辑在真实数据里各自验证了一遍。价格口径回测里最隐蔽的错回测里最容易犯、最隐蔽的错是价格口径不自洽。第一版回测用真实价撮合、持仓股数固定。这个口径在除权日会出事股票 10 送 10价格当天腰斩但回测里的持仓股数没变——权益凭空缩水一半。反过来配股之类的场景就出现假暴涨。样本从 5 只扩到 50 只后报告里冒出两只刺眼的票313%、425%。追下去不是行情暴涨是除权日的假跳变。修法是整个账户换成后复权口径成交价用后复权开盘价、市值用后复权收盘价、权益 现金 持仓 × 后复权价。后复权价在除权日是连续的权益不再假跳变。代价也有费用按后复权金额计算比例费率不变但最低佣金 5 元的阈值在放大后的价格尺度下轻微失真——对大盘股几乎不触发可接受记进决策。这个 bug 的教训回测的价格口径必须自洽。真实价撮合看起来贴近实际但除权日会出错后复权口径牺牲一点直觉换来权益连续。一分钱也要对费用计算用 Decimal回测里第二个隐蔽的错是浮点舍入。过户费 909500 × 0.00001 9.095round(9.095, 2)在二进制浮点下可能得到 9.09而手工四舍五入是 9.10——一分钱之差。单看无关痛痒但回测的退出条件之一就是逐笔费用与手工核算完全一致。一分钱对不上就是核算不过。修法很朴素费用计算全走 Decimal分项各自保留两位ROUND_HALF_UP合计 分项之和。从此与手工核算完全一致。全仓买不入的细节与买不起的茅台还有一个细节全仓买入要预留费用空间。初始实现按现金 / 价格直接算股数某票开盘 50 元、买 20000 股正好 100 万加上佣金和过户费就超支了——直接拒单一股没买。验证脚本报应成交但未成交。修法是超支时降档 100 股重试。还有一个插曲初始资金设了 10 万回测跑完茅台 0 笔交易。不是没信号是买不起——茅台每股 1500 多10 万连一手100 股都买不起。资金约束是回测设计的一部分不是 bug。把初始资金调到 100 万/票样本才都能交易。验证三层检查回测语义对不对靠验证。三层合成数据自测。造一只假股票故意放一天一字涨停、一天停牌验证买单被拒、卖单被拒、停牌不成交。写正式回测之前先过这一关。自动断言。814 项T1 成交价等于次一交易日开盘价、股数全是 100 的倍数、费用逐笔重算一致、信号只用当日及以前的数据。手工逐笔核算。挑 3 笔交易——成交价、股数、佣金、印花税、过户费一笔一笔用真实行情手算和系统输出完全一致。三个 bug——全仓费用空间、浮点舍入、权益恒等差一分钱——都不是事先想到的是逐笔核对时发现的。从那以后验证就作为流程的一部分固定下来不是最后补的检查。Rust 为什么没有写设计稿里回测引擎最重的规划是 Rust 撮合核心PyO3理由是现成库非 A 股特化、Python 太慢、“不接受一次策略跑数小时”。这个理由在写代码之前听着很充分。Python 版撮合写完后我按路线图的原则先做了 profiling再决定要不要上 Rust当时的样本50 只股票、43K 行 bar、2478 笔成交逐票回测加指标计算实测 0.30 秒摊到单票 0.006 秒读数据、因子、信号、IC、报告加起来纯计算不到 1 秒按行数线性外推估算非实测全市场 5000 只股票回测约 30 秒Python 太慢是当时的预判。实测结果是全市场回测也就 30 秒量级离一次跑数小时很远。设计稿里写过一条假设——“如果 Python 回测足够快整个 Rust 取消”。它落地了Rust 没有写。结论只记在脑子里不够。我把触发条件写进了决策文档单票回测超过 0.1 秒或全市场回测超过 1 分钟再考虑 Rust。将来规模真的上来直接对照阈值做决定不用重新争论。小结回测引擎的可信度来自三件事A 股语义清单做对、价格口径自洽、验证方法兜底。性能不是难点语义才是——回测的数字错得再快也没有意义跑得再慢也总比数字错强。Rust 没有写和存储选型里没引入 DuckDB、PostgreSQL 是一个道理不是技术不好是当前的规模没有触发它。设计稿里的方案是候选最后用不用由真实数据和实测决定。
返回列表