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_idcreated_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 市场数据更新 数据推送 SignalEvent 策略生成信号 信号传递 OrderEvent 订单生成 执行 FillEvent 成交回报 更新持仓 说明:实线箭头表示事件正向流转,虚线箭头表示成交后更新持仓信息 每个事件都包含独立的时间戳和唯一标识,便于追踪和调试

从这张图你能看到,事件是单向流动的。市场数据进来,策略分析后生成信号,信号转成订单,订单执行后返回成交。虚线表示成交事件会反过来更新策略的持仓状态,形成一个闭环。

注意:千万不要在事件对象里保存「全局状态」。比如在 MarketEvent 里保存当前所有持仓信息。这会导致事件之间耦合过紧,一旦某个环节出错,整个系统都跟着乱。每个事件只应该携带它「自己」的信息。

好了,四个核心事件类型就讲完了。你可能会问:为什么非要定义这么多事件类型?直接用一个万能事件不行吗?嗯,我刚开始也这么想过。但后来发现,事件类型越清晰,系统的可维护性就越高。每个模块只关心它需要的事件类型,互不干扰。这才是事件驱动架构的精髓。