2. 事件类型与数据结构:市场数据事件、订单事件、信号事件、账户事件、自定义事件
好,咱们进入第二章。这一章我打算把事件驱动架构里最核心的零件——事件类型,一个个拆开来讲。
你想想看,一个回测系统跑起来,本质上就是在处理各种事件。没有事件,系统就是死的。我最早做回测框架时,把所有事件都塞进一个字典里,结果调试起来简直想砸电脑。后来才明白,事件类型的设计,直接决定了系统的扩展性和可维护性。
2.1 市场数据事件:Tick 与 Bar
市场数据事件是回测系统的燃料。没有它,其他事件都无从谈起。
Tick 事件
Tick 是市场数据的最小颗粒度。每一笔成交都会产生一个 Tick。我习惯把它设计成不可变对象,原因很简单——历史数据不能改。
@dataclass(frozen=True)
class TickEvent:
symbol: str
timestamp: datetime
last_price: float
volume: int
bid: float
ask: float
bid_size: int
ask_size: int
exchange: str = ""
这里有个坑。我曾经把 timestamp 设计成字符串,结果不同数据源的时间格式五花八门,解析时出了不少 bug。后来统一用 datetime 对象,配合 UTC 时区,问题就解决了。
Bar 事件
Bar 是 Tick 的聚合。1分钟、5分钟、日线,本质上都是 Bar。我个人习惯把 Bar 事件设计成包含 OHLCV 的简单结构:
@dataclass
class BarEvent:
symbol: str
timestamp: datetime
open: float
high: float
low: float
close: float
volume: int
interval: str # '1min', '5min', '1d'
嗯,这里要注意 interval 字段。我见过有人把不同周期的 Bar 混在一起处理,结果策略信号全乱了。我的做法是:每个周期单独一个事件队列,互不干扰。
2.2 订单事件
订单事件是策略和市场的桥梁。你发出指令,市场给你反馈。这个过程涉及两个方向的事件:
- 订单请求:策略想做什么
- 订单报告:交易所告诉你发生了什么
我设计订单事件时,参考了 FIX 协议的标准字段,但做了精简:
@dataclass
class OrderEvent:
order_id: str
symbol: str
side: str # 'BUY' or 'SELL'
order_type: str # 'MARKET', 'LIMIT', 'STOP'
quantity: int
price: float
timestamp: datetime
status: str # 'NEW', 'PARTIALLY_FILLED', 'FILLED', 'CANCELED'
我曾经犯过一个错误:把订单状态和订单请求混在一个事件里。结果回测时,一个订单被重复执行了三次,亏了不少虚拟资金。后来我严格区分了 指令事件 和 状态事件,才彻底解决这个问题。
2.3 信号事件
信号事件是策略的核心输出。它告诉系统:现在该买还是该卖。
我见过有人把信号直接写成订单,然后发出去。这其实是个坏习惯。信号和订单之间应该有一层 风险管理 和 资金管理 的过滤。信号只是建议,订单才是执行。
@dataclass
class SignalEvent:
symbol: str
timestamp: datetime
direction: str # 'LONG', 'SHORT', 'EXIT'
strength: float # 0.0 to 1.0
strategy_id: str
metadata: dict = None
strength 字段是我后来加的。有些策略会同时产生多个信号,比如做多信号强度 0.8,做空信号强度 0.3。这时候系统可以根据强度来决定执行哪个。
说白了,信号事件就是策略的「想法」。至于这个想法能不能落地,得看后面的风控模块怎么说。
2.4 账户事件
账户事件记录你的资金变动。每次成交、每次分红、每次手续费扣除,都应该产生一个账户事件。
我习惯把账户事件分成两类:
- 快照事件:记录某个时间点的完整账户状态
- 增量事件:记录某笔操作引起的变动
@dataclass
class AccountEvent:
timestamp: datetime
event_type: str # 'SNAPSHOT' or 'DELTA'
total_equity: float
cash: float
positions: dict # {symbol: quantity}
unrealized_pnl: float
realized_pnl: float
这里有个经验之谈:千万别只记录增量事件。我早期做回测时,只记录每次交易的盈亏,结果系统跑着跑着,账户余额就对不上了。后来我改成每根 Bar 结束时生成一个快照,发现问题时能快速定位到出错的区间。
2.5 自定义事件
这是事件驱动架构最灵活的地方。你永远不知道未来会需要什么事件,所以系统必须支持扩展。
我一般会定义一个基础事件类,然后让所有事件继承它:
@dataclass
class BaseEvent:
timestamp: datetime
event_type: str
class CustomEvent(BaseEvent):
data: Any
source: str
举个例子。我之前做一个期权回测系统,需要处理隐含波动率的变化。这不是标准的市场数据,也不是订单或信号。怎么办?直接定义一个 VolSurfaceEvent,继承 BaseEvent,然后扔进事件队列里。系统其他模块只要订阅这个事件类型,就能自动响应。
你想想看,这种设计的好处是什么?解耦。新增一种事件类型,完全不需要修改现有代码。只需要写一个新的事件类,然后在需要的地方发布和订阅就行了。
2.6 事件类型的整体架构
说了这么多,咱们用一张图来总结一下事件类型的层次关系:
这张图展示了事件类型的继承关系。从 BaseEvent 出发,衍生出市场数据、订单、信号三大类,再往下细分到具体的实现。账户事件和自定义事件则作为补充,覆盖了回测系统的所有需求。
最后说一句。事件类型的设计没有标准答案,每个团队都有自己的习惯。但有一条原则是通用的:事件应该足够细粒度,但又不能太碎。太粗了丢失信息,太细了性能扛不住。这个度,得靠实际项目来把握。