← 返回目录
p8.1动手⏱ 约 16 分钟

事件驱动架构:Gateway / Event / Adapter 三层

622 根 Bar、206 笔委托、108 笔被拒、98 笔成交——系统里发生过什么,全部有据可查

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

P4 那台回测引擎能算,但它没法上线:数据是一次性给全的、没有服务、没有存储、没有委托状态。

P8 把它装进一个系统。三层职责,任何一层都可以被单独替换:

Gateway   对外统一入口 —— 上层只认它,不认具体实现
Event     系统内唯一的通信媒介 —— 所有状态变化都是事件
Adapter   对接外部世界 —— 换数据源/换撮合 = 换一个文件

端到端跑一遍(sh.600000,2024-01 ~ 2026-07):

Bar        622        OrderReq   206
OrderAck   206        Rejected   108
Fill        98

每一个数字背后都有一条落盘记录。 而这正是回测做不到的事——回测只给你一条净值曲线,它不告诉你你想下的单有多少根本没成交

⚠️ 合规边界(整章不可动):本系统只做模拟撮合与纸上交易,不接真实券商下单通道。为什么停在这里,见 p8.10。

💡 打个比方

餐厅的后厨、前台、供货商是三件事。

前台只跟后厨说"要一份三号套餐",它不需要知道番茄是从哪个供货商买的。

换供货商,前台一句话都不用改——因为它们之间隔着一层接口

Gateway 就是那个前台。

一、为什么是事件,而不是函数调用

设计原则代价换来什么
所有状态变化都是事件代码比直接调用长每一次状态变化都可回放、可审计
事件不可变(frozen)不能就地修改谁也不能偷偷改别人发出去的事件
每个事件带 ts 与 source多两个字段出问题时能回答"这是谁在什么时候发的"
事件总线是同步的吞吐低顺序即纪律——见下
⚠️ 避坑

"同步总线"是一个刻意的取舍,不是偷懒。

异步能提高吞吐,但会让下面这个顺序变得难以保证:

① 新的一天 → 解锁昨日买入(T+1)
② 推入今日 Bar → 更新行情
③ 用「昨日及之前」的数据算信号 —— 绝不用今日
④ 按信号生成委托 → 撮合
⑤ 收盘后记账并落盘

而 P4 整章都建立在这个顺序上。一旦 ③ 和 ② 的顺序颠倒,就是未来函数(p1.2);一旦 ① 漏掉,T+1 就形同虚设(p4.4)。

在交易系统里,顺序不是性能问题,是正确性问题。

需要吞吐的时候再上异步,但要先有一套能证明顺序正确的同步实现作为对照

二、三层各自的职责

Gateway:上层只认它

class Gateway:
    def replay(self, symbol, start, end): ...   # 数据
    def send_order(self, req): ...              # 交易
    def account(self): ...                      # 账户

换数据源、换撮合、换券商 = 换一个 Adapter,Gateway 的接口一行不用改。

Event:七种事件覆盖全流程

事件谁发说明
Bar数据适配器行情
Signal策略spec_hash——接 p5.1,信号必须能追到规格指纹
OrderReq / OrderAck上层 / 撮合委托与受理
Fill / Rejected撮合被拒也是一等公民(p4.1)
AccountSnapshot撮合账户

Adapter:换外部世界只改这里

adapters/data_local.py    本地 parquet —— **只读落盘文件,不联网**
adapters/match_sim.py     模拟撮合 —— 复用 P4 那套已过单测的 A 股规则

data_local 不联网,是 p1.5 / p5.9 那条纪律在系统层的落实: 联网重取的系统不可复现,而不可复现的系统无法排障。

🔧 动手做:让系统自己交代它干过什么(5 分钟)

跑 qsys 的端到端例子,看它吐出的事件流。

你会看到(实测):622 个交易日里——622 根 Bar、206 笔委托、108 笔被拒、98 笔成交,每一笔都带着时间、原因、状态,全部有据可查。

想明白:事件驱动架构(Gateway → Event → Adapter 三层)最大的好处不是"跑得对",是每一件发生过的事都留了痕。向量化回测给你一条净值曲线,你无法回答"这天为什么没成交";事件系统能告诉你"因为封涨停被拒"。能复盘,才谈得上能信任。

三、实测:系统里发生过什么

数据适配器:LocalParquetData(51 个标的,fetched_at=2026-07-31 14:38:54)
撮合适配器:SimMatcher(初始资金 1,000,000,佣金 2.5bp,印花税 5.0bp,滑点 5.0bp)

交易日 622   委托 206   成交 98   被拒 108
期末:现金 11,280.38   权益 1,288,952.95   持仓 sh.600000 × 10,300

事件流:Bar 622 · OrderReq 206 · OrderAck 206 · Rejected 108 · Fill 98
落盘:fills=98  rejects=108  equity=622(JSON Lines,追加写、永不改写)

206 笔委托里 108 笔被拒——超过一半。

而这个数字在回测里是看不见的:向量化回测根本没有"被拒"这个概念(p4.1)。

四、这套架构的三个直接收益

  1. 可回放:事件日志本身就是一次完整记录,截取一段就能重放排障(p8.9)
  2. 可替换:换数据源/撮合只动一个文件,上层零改动
  3. 可审计:每一笔成交、每一次被拒都落盘,而且是追加写、永不改写——交易记录是流水账,改写流水账本身就是问题
❓ 测验
有人建议把事件总线改成异步以提高吞吐。按本节的观点,最该先问什么?
✏️ 填空
qsys 的三层是 Gateway、Event 和 ___——换数据源或换撮合,只需要换后者的一个文件。

📌 免责:本课为技术教学,系统只做模拟撮合与纸上交易,不接真实券商下单通道;不构成投资建议。

✅ 小结

四件事:

  1. 三层职责:Gateway(上层只认它)· Event(唯一通信媒介)· Adapter(换外部世界只改这里)。换数据源/换撮合 = 换一个文件,上层零改动
  2. 同步总线是刻意的取舍:异步提高吞吐,但会让"解锁 T+1 → 更新行情 → 用昨日算信号 → 撮合 → 记账"这个顺序难以保证。在交易系统里,顺序不是性能问题,是正确性问题
  3. 实测 622 个交易日:委托 206、成交 98、被拒 108——超过一半的委托被拒,而这个数字在向量化回测里根本不存在(p4.1)
  4. 三个收益:可回放(事件日志即记录)· 可替换(只动一个 Adapter)· 可审计(追加写、永不改写——改写流水账本身就是问题)

数据适配器只读落盘文件、不联网,是 p1.5 / p5.9 那条纪律在系统层的落实:联网重取的系统不可复现,而不可复现的系统无法排障。

下一节把它变成服务:REST 契约、类型校验、自动文档,以及一个最容易被忽略的东西——幂等键。

下一节 → 后端服务:REST 契约与幂等
🔎 来源与核验· 1 条,点开核对
本节每个关键论断都对应一个可追溯的来源 —— 这是本课程"靠谱、不过时"的底线。
「qsys 端到端实测(sh.600000,2024-01-01~2026-07-29,622 个交易日):委托 206、成交 98、被拒 108;事件流 Bar 622 / OrderReq 206 / OrderAck 206 / Rejected 108 / Fill 98;期末现金 11,280.38、权益 1,288,952.95、持仓 10,300 股;落盘 fills=98 / rejects=108 / equity=622」
📚 本节 code/p8_demo.py 与 _shared/qsys/,2026-08-01 于 ECS 实测,输出见 code/outputs/stdout.txt✓ 已核验 2026-08
系统仅含模拟撮合适配器(SimMatcher),不含任何真实券商通道;数据适配器只读本地 parquet,不联网。
智图软件的赞赏码
都看到这了,打个赏呗!
接下来 · p8.2
后端服务:REST 契约、类型校验与幂等键
继续读下一节 →