事件驱动架构:Gateway / Event / Adapter 三层
622 根 Bar、206 笔委托、108 笔被拒、98 笔成交——系统里发生过什么,全部有据可查
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)。
四、这套架构的三个直接收益
- 可回放:事件日志本身就是一次完整记录,截取一段就能重放排障(p8.9)
- 可替换:换数据源/撮合只动一个文件,上层零改动
- 可审计:每一笔成交、每一次被拒都落盘,而且是追加写、永不改写——交易记录是流水账,改写流水账本身就是问题
📌 免责:本课为技术教学,系统只做模拟撮合与纸上交易,不接真实券商下单通道;不构成投资建议。
四件事:
- 三层职责:Gateway(上层只认它)· Event(唯一通信媒介)· Adapter(换外部世界只改这里)。换数据源/换撮合 = 换一个文件,上层零改动
- 同步总线是刻意的取舍:异步提高吞吐,但会让"解锁 T+1 → 更新行情 → 用昨日算信号 → 撮合 → 记账"这个顺序难以保证。在交易系统里,顺序不是性能问题,是正确性问题
- 实测 622 个交易日:委托 206、成交 98、被拒 108——超过一半的委托被拒,而这个数字在向量化回测里根本不存在(p4.1)
- 三个收益:可回放(事件日志即记录)· 可替换(只动一个 Adapter)· 可审计(追加写、永不改写——改写流水账本身就是问题)
数据适配器只读落盘文件、不联网,是 p1.5 / p5.9 那条纪律在系统层的落实:联网重取的系统不可复现,而不可复现的系统无法排障。
下一节把它变成服务:REST 契约、类型校验、自动文档,以及一个最容易被忽略的东西——幂等键。
🔎 来源与核验· 1 条,点开核对
