2. 核心事件类型定义:市场数据事件、订单事件、成交事件、信号事件
好,咱们直接进入正题。事件驱动回测引擎里,最核心的东西就是「事件」本身。你想想看,整个系统就像一条流水线,事件就是流水线上传递的零件。没有这些零件,引擎就是一堆死代码。
我个人习惯把事件分成四大类:市场数据事件、信号事件、订单事件和成交事件。这四类事件串联起了从「行情进来」到「交易完成」的完整闭环。下面我一个一个拆开讲。
2.1 市场数据事件(MarketEvent)
市场数据事件是所有事件的起点。说白了,它就是告诉引擎:「嘿,新的行情数据到了!」
我在项目中遇到过不少新手,一上来就把K线数据直接塞进策略逻辑里,结果回测和实盘对不上。为什么?因为市场数据事件应该是一个统一的数据包装器,而不是原始数据本身。
核心要点:MarketEvent 只负责通知「数据已更新」,不负责具体数据内容。具体数据通过事件对象的属性传递。
来看一个我常用的定义方式:
class MarketEvent:
"""市场数据事件"""
def __init__(self, symbol: str, data: dict, event_type: str = 'BAR'):
self.type = 'MARKET'
self.symbol = symbol # 交易品种
self.data = data # 实际行情数据
self.event_type = event_type # 'BAR' 或 'TICK'
self.timestamp = None # 时间戳,由引擎填充
这里有个小细节:event_type 字段。我建议保留它,因为实盘中你可能同时处理分钟线和Tick数据。有了这个字段,策略层就能区分「哦,这是新的分钟线来了」还是「这是新的逐笔成交来了」。
我的习惯:在 MarketEvent 里额外加一个 is_snapshot 布尔字段。回测时设为 True,实盘时设为 False。这样同一个事件类可以同时支持回测和实盘,省得写两套代码。
2.2 信号事件(SignalEvent)
信号事件是策略层的输出。当策略分析完市场数据后,它会生成一个信号——「我觉得该买」或者「我觉得该卖」。
嗯,这里要注意:信号不等于订单。信号只是策略的「想法」,而订单是真正要发出去的「指令」。把两者分开,是为了让风控模块有机会在中间插一脚。
class SignalEvent:
"""交易信号事件"""
def __init__(self, symbol: str, direction: str, strength: float = 1.0):
self.type = 'SIGNAL'
self.symbol = symbol
self.direction = direction # 'BUY' 或 'SELL'
self.strength = strength # 信号强度,0~1之间
self.signal_id = None # 唯一标识,用于追踪
我曾经踩过一个坑:信号强度 strength 字段一开始没加。结果策略出了多个信号,风控模块不知道哪个信号更可靠。后来我加了这个字段,风控就能根据强度做仓位管理了。
避坑指南:信号事件里不要包含具体价格。价格应该在订单生成阶段从市场数据中获取。否则,信号生成时间和订单执行时间之间的价格变化会导致回测失真。
2.3 订单事件(OrderEvent)
订单事件是真正要发给「交易所」的指令。它比信号事件多了很多细节:价格、数量、订单类型等等。
我个人习惯把订单事件设计成不可变对象。为什么?因为订单一旦发出,就不应该被修改。如果真想改,应该先撤单再重新下单。
class OrderEvent:
"""订单事件"""
def __init__(self, symbol: str, order_type: str,
quantity: int, direction: str, price: float = None):
self.type = 'ORDER'
self.symbol = symbol
self.order_type = order_type # 'MARKET', 'LIMIT', 'STOP'
self.quantity = quantity
self.direction = direction # 'BUY' 或 'SELL'
self.price = price # 限价单必填,市价单为None
self.order_id = None # 由引擎分配
self.created_time = None # 创建时间戳
你看,这里我把 order_id 和 created_time 都留空了。这些字段由引擎在接收到订单事件后自动填充。这样做的好处是:策略层不需要关心订单的全局唯一ID怎么生成,引擎层统一管理就行。
| 订单类型 | price 字段 | 适用场景 |
|---|---|---|
| 市价单 (MARKET) | None | 追求立即成交,不关心价格 |
| 限价单 (LIMIT) | 指定价格 | 追求特定价格,不急于成交 |
| 止损单 (STOP) | 触发价格 | 控制亏损,价格达到后变市价单 |
我的建议:在 OrderEvent 里加一个 expiry 字段,表示订单有效期。回测时设为当天收盘,实盘时可以根据需要设置。这样能避免「僵尸订单」一直挂在系统里。
2.4 成交事件(FillEvent)
成交事件是订单被「执行」后的结果。它包含了实际成交的价格、数量、手续费等信息。
我记得刚开始做回测引擎时,总觉得成交事件就是订单事件的「镜像」。后来发现完全不是那么回事。订单可能部分成交、可能滑点成交、可能被拒单。成交事件需要记录所有这些细节。
class FillEvent:
"""成交事件"""
def __init__(self, symbol: str, order_id: str,
fill_price: float, fill_quantity: int,
commission: float = 0.0):
self.type = 'FILL'
self.symbol = symbol
self.order_id = order_id # 对应的订单ID
self.fill_price = fill_price # 实际成交价
self.fill_quantity = fill_quantity # 实际成交量
self.commission = commission # 手续费
self.fill_time = None # 成交时间戳
self.slippage = 0.0 # 滑点
这里我特别想强调 slippage 字段。回测时很多人忽略滑点,结果回测收益漂亮得不行,实盘一跑就亏。我建议在成交事件里记录滑点,这样事后分析时能清楚看到「理想成交价」和「实际成交价」之间的差距。
核心逻辑:四个事件类型的流转顺序是:
MarketEvent → SignalEvent → OrderEvent → FillEvent
每个事件都携带上一阶段的信息,但又各自独立。这样设计的好处是:任何一个环节出问题,都能单独排查。
2.5 事件流转关系图
下面我用一张SVG图来展示这四个事件类型之间的关系。你一看就明白了。
从这张图你能看到,事件是单向流动的。市场数据进来,策略分析后生成信号,信号转成订单,订单执行后返回成交。虚线表示成交事件会反过来更新策略的持仓状态,形成一个闭环。
注意:千万不要在事件对象里保存「全局状态」。比如在 MarketEvent 里保存当前所有持仓信息。这会导致事件之间耦合过紧,一旦某个环节出错,整个系统都跟着乱。每个事件只应该携带它「自己」的信息。
好了,四个核心事件类型就讲完了。你可能会问:为什么非要定义这么多事件类型?直接用一个万能事件不行吗?嗯,我刚开始也这么想过。但后来发现,事件类型越清晰,系统的可维护性就越高。每个模块只关心它需要的事件类型,互不干扰。这才是事件驱动架构的精髓。