← 返回目录
p8.7动手⏱ 约 17 分钟

模拟撮合引擎:七条规则,加一条回测里不存在的状态

206 笔委托、98 笔成交、108 笔被拒——而"被拒"这个概念在向量化回测里根本不存在

🔎 最后验证 2026-08📚 来源:SimMatcher 撮合实测(622 个交易日),2026-08-01 于 ECS 实测,输出见 code/outputs/stdout.txt🧰 Python 3.12.13、qsys(本课自研)
为什么学这个

P4 那台撮合器已经有了七条 A 股规则。这一节把它装进系统,并且加上一条回测里根本不存在的东西:

委托状态。

回测里,委托只有两种命运:成交、或者不存在。而真实世界里,委托可以挂着、可以被拒、可以部分成交。

实测 622 个交易日:

委托 206   成交 98   被拒 108

超过一半的委托没有成交。

而这 108 笔被拒的原因分布,本身就是一份诊断报告——它告诉你策略在什么地方撞了墙。

⚠️ 这是本课唯一的撮合实现,它不连接任何真实交易通道。为什么停在这里,见 p8.10。

💡 打个比方

点外卖时,"下单"和"骑手接单"是两件事。

你点了,不代表有人接。

回测把这两件事合成了一件——所以它永远不会告诉你"没人接单"。

一、七条规则(全部继承自 P4 的单元测试)

规则实现要点
涨跌停一字涨停买不进、一字跌停卖不出
T+1locked 字段;new_day() 才解锁
整手买入按 100 股向下取整
停牌tradable=False 直接拒单
佣金双边 2.5bp,最低 5 元
印花税仅卖出收 5bp
滑点买入向上、卖出向下各 5bp

第 ⑤ 条的"最低 5 元"是很多实现会漏的:小额交易的实际成本远高于比例费率。一笔 1000 元的买入,按 2.5bp 只有 0.25 元佣金,而实际要收 5 元——真实费率是 50bp,20 倍

二、回测里不存在的那条状态

@dataclass
class Position:
    qty: int = 0
    locked: int = 0          # T+1:当日买入不可卖
    cost: float = 0.0

    @property
    def sellable(self):
        return max(self.qty - self.locked, 0)

locked 这个字段在向量化回测里没有对应物——因为向量化回测里"持仓"就是一个数,没有"其中多少不能卖"这回事。

而正是这个字段,让 T+1 从一句口号变成一个会拒单的约束。

🔧 动手做:看一眼向量化回测永远给不了你的那 108 笔(5 分钟)

跑 SimMatcher(模拟撮合),看它的成交与被拒统计。

你会看到(实测):622 个交易日,206 笔委托里 98 笔成交、108 笔被拒——而"被拒"这个概念,在向量化回测里根本不存在(它默认"目标仓位一定达成")。

想明白:那 108 笔被拒(涨跌停封板、停牌、资金不足、T+1 锁定)是一份诊断报告:它告诉你策略在什么时候"想做却做不到"。向量化回测把这些失败的交易全部静默吞掉,于是你看到的净值比现实好看。能把"被拒"显式记下来,是模拟撮合相对向量化最本质的价值。

三、108 笔被拒:一份诊断报告

⚠️ 避坑

被拒原因的分布,比被拒数量更有信息量。

实测 108 笔全部是同一个原因:

不足一手    108 笔(100.0%)

这不是 bug,是策略状态的真实反映:仓位接近满仓后,策略仍在要求"再加一点",而剩余现金买不起一手。

而这正是 p4.1 建这套拒单记录的理由:

如果静默跳过,回测会显示"策略选择空仓"——净值曲线一模一样,结论完全不同

一个只报成交、不报被拒的系统,会把"你没钱建仓"伪装成"策略判断如此"。

这两件事在净值上看不出区别,在改进策略时却指向完全相反的方向

四、撮合器与回测引擎的三个差别

P4 的 run_backtestqsys 的 SimMatcher
数据一次性给全一天一天喂
委托立即撮合,无状态open_orders,可挂单
账户函数内部变量可查询的快照(/account)

第一条差别是纸上交易的全部价值所在:一次性给全的数据,让"我不小心用了未来数据"这件事无法被发现;而一天一天喂,结构上就堵住了这条路

五、为什么这套撮合值得自己写

不是因为它比商业系统好,而是因为你需要知道它假设了什么。

你写的你就知道
一字板判定用 low >= 涨停价不是所有涨停都买不进,只有一字板
成交价用 open × (1+滑点)你的回测假设了"次日开盘成交"
最低佣金 5 元小额交易的真实费率是多少
locked 字段T+1 到底在什么时候生效

而用别人的系统时,这些假设通常写在文档的第 37 页,或者根本没写。

❓ 测验
你的系统报告「今天下了 20 笔委托,成交 8 笔」,另外 12 笔全部因为「不足一手」被拒。这说明什么?
✏️ 填空
`Position` 里的 ___ 字段在向量化回测里没有对应物——正是它让 T+1 从一句口号变成一个会拒单的约束。

📌 免责:本课为技术教学,SimMatcher模拟撮合,不连接任何真实交易通道;不构成投资建议。

✅ 小结

四件事:

  1. 七条 A 股规则全部继承自 P4 的单元测试。其中"最低佣金 5 元"最容易漏:一笔 1000 元的买入按 2.5bp 只要 0.25 元,而实际收 5 元——真实费率 50bp,20 倍
  2. locked 字段在向量化回测里没有对应物——向量化回测的"持仓"就是一个数,没有"其中多少不能卖"这回事。正是这个字段让 T+1 从一句口号变成一个会拒单的约束
  3. 108 笔被拒全部是"不足一手",这不是 bug,是策略状态的真实反映。一个只报成交、不报被拒的系统,会把"你没钱建仓"伪装成"策略判断如此"——两件事在净值上看不出区别,在改进策略时却指向完全相反的方向
  4. 与回测的三个差别:数据一天一天喂(而非一次性给全)· 委托有状态可挂单 · 账户是可查询的快照。第一条是纸上交易的全部价值——一次性给全的数据让"用了未来数据"无法被发现,而一天一天喂,结构上就堵住了这条路

自己写撮合的理由不是它比商业系统好,而是你需要知道它假设了什么:一字板怎么判、成交价用哪个、最低佣金多少、T+1 何时生效——用别人的系统时,这些假设通常写在文档第 37 页,或者根本没写

下一节把它跑成闭环:信号 → 委托 → 成交 → 复盘,并讲一个纸上交易当场暴露、而回测掩盖了的缺陷。

下一节 → 纸上交易全流程
🔎 来源与核验· 1 条,点开核对
本节每个关键论断都对应一个可追溯的来源 —— 这是本课程"靠谱、不过时"的底线。
「SimMatcher 实测(sh.600000,2024-01~2026-07,622 个交易日,初始资金 100 万,佣金 2.5bp/印花税 5bp/滑点 5bp/最低佣金 5 元):委托 206、成交 98、被拒 108;被拒原因 100% 为「不足一手」」
📚 本节 code/p8_demo.py 与 _shared/qsys/adapters/match_sim.py,2026-08-01 于 ECS 实测,输出见 code/outputs/stdout.txt✓ 已核验 2026-08
SimMatcher 为模拟撮合实现,规则继承自 P4 的单元测试;系统不含任何真实交易通道。
智图软件的赞赏码
都看到这了,打个赏呗!
接下来 · p8.8
纸上交易全流程:它当场抓到了一个回测掩盖的缺陷
继续读下一节 →