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 时区,问题就解决了。

注意:Tick 数据量极大。A股一天大约产生 3-5 亿条 Tick。如果你的回测跑十年数据,内存里放不下。我建议用内存映射文件或者数据库流式读取,别一股脑全加载。

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'

我曾经犯过一个错误:把订单状态和订单请求混在一个事件里。结果回测时,一个订单被重复执行了三次,亏了不少虚拟资金。后来我严格区分了 指令事件状态事件,才彻底解决这个问题。

小技巧:订单 ID 建议用 UUID,别用自增整数。多策略并行时,整数 ID 很容易冲突。

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 MarketDataEvent OrderEvent SignalEvent TickEvent BarEvent OrderRequest OrderReport AccountEvent CustomEvent 基类 一级事件 二级事件 三级事件

这张图展示了事件类型的继承关系。从 BaseEvent 出发,衍生出市场数据、订单、信号三大类,再往下细分到具体的实现。账户事件和自定义事件则作为补充,覆盖了回测系统的所有需求。

最后说一句。事件类型的设计没有标准答案,每个团队都有自己的习惯。但有一条原则是通用的:事件应该足够细粒度,但又不能太碎。太粗了丢失信息,太细了性能扛不住。这个度,得靠实际项目来把握。


专注资料整理