4. 订单对象建模:设计Order类

做量化交易,订单就是我们的子弹。你想想看,一个订单对象要是设计得不好,后面所有策略逻辑都得跟着遭殃。我个人习惯在动手写代码之前,先把订单的「骨架」想清楚。

说白了,一个订单对象就是一笔交易指令的数字化表达。它得告诉系统:谁下的单、什么方向、多少钱、多少量、什么时候下的。嗯,就这么简单,但细节里全是坑。

4.1 订单的核心属性

我在项目中遇到过不下十种订单结构设计,最后沉淀下来,最核心的属性就这五个:

属性 类型 说明
价格 float / Decimal 限价单的指定价格,市价单可设为0或None
数量 float / int 委托的股数或合约张数
时间戳 int (纳秒/微秒) 订单生成或进入撮合引擎的时间
订单ID str / UUID 全局唯一标识,用于追踪和撤销
方向 enum (BUY/SELL) 买入还是卖出

你可能会问:为什么不用浮点数表示价格?我曾经在回测系统里用float存价格,结果因为精度问题,两个明明相等的价格在比较时出了bug,导致一笔本该成交的单子被系统忽略了。从那以后,涉及金钱的字段我全换成了Decimal。

4.2 代码实现:一个扎实的Order类

直接上代码。这是我目前在实盘系统里用的版本,经过多次迭代,算是比较稳了。

from decimal import Decimal
from enum import Enum
from dataclasses import dataclass
from typing import Optional
import uuid
import time

class OrderSide(Enum):
    BUY = "BUY"
    SELL = "SELL"

@dataclass
class Order:
    order_id: str
    price: Decimal
    quantity: Decimal
    timestamp: int
    side: OrderSide
    symbol: str = ""  # 交易对,比如"BTC/USDT"
    
    def __post_init__(self):
        """初始化后的校验逻辑"""
        if self.quantity <= 0:
            raise ValueError(f"数量必须大于0,当前: {self.quantity}")
        if self.price < 0:
            raise ValueError(f"价格不能为负,当前: {self.price}")
    
    @classmethod
    def create_limit_order(cls, symbol: str, side: OrderSide, 
                           price: Decimal, quantity: Decimal) -> "Order":
        """创建一个限价单"""
        return cls(
            order_id=str(uuid.uuid4()),
            price=price,
            quantity=quantity,
            timestamp=time.time_ns(),
            side=side,
            symbol=symbol
        )
    
    @classmethod
    def create_market_order(cls, symbol: str, side: OrderSide,
                            quantity: Decimal) -> "Order":
        """创建一个市价单,价格设为0表示由系统决定"""
        return cls(
            order_id=str(uuid.uuid4()),
            price=Decimal("0"),
            quantity=quantity,
            timestamp=time.time_ns(),
            side=side,
            symbol=symbol
        )
    
    def is_buy(self) -> bool:
        return self.side == OrderSide.BUY
    
    def is_sell(self) -> bool:
        return self.side == OrderSide.SELL
    
    def __repr__(self) -> str:
        return (f"Order(id={self.order_id[:8]}..., "
                f"{self.side.value} {self.quantity} @ {self.price})")

设计要点:

  • dataclass省去大量样板代码,Python 3.7+ 都支持
  • 时间戳用纳秒级,避免同一毫秒内多笔订单冲突
  • 订单ID用UUID,保证分布式环境下也不会重复
  • 价格和数量用Decimal,浮点数精度问题在金融系统里是致命伤

4.3 为什么时间戳这么重要?

我记得有一次做回测,发现同样的订单序列,每次跑出来的成交结果都不一样。查了半天,原来是时间戳精度不够,同一微秒内的订单顺序是随机的。

在限价订单簿里,时间戳决定了订单的优先级。价格相同的情况下,先来的先成交。如果你的时间戳只精确到毫秒,那高频场景下基本等于随机排序。

我的建议:time.time_ns()获取纳秒级时间戳。如果系统对性能要求极高,可以考虑用单调递增的序列号替代时间戳,但要注意分布式场景下的全局有序问题。

4.4 订单的生命周期状态

一个订单从创建到最终消失,会经历几个状态。我习惯在Order类里加一个状态字段,虽然它不是「属性」层面的东西,但对交易系统来说至关重要。

class OrderStatus(Enum):
    PENDING = "PENDING"      # 等待进入订单簿
    ACTIVE = "ACTIVE"        # 已在订单簿中
    PARTIAL = "PARTIAL"      # 部分成交
    FILLED = "FILLED"        # 完全成交
    CANCELED = "CANCELED"    # 已撤销
    REJECTED = "REJECTED"    # 被拒绝

你想想看,一个订单提交后,可能因为风控检查没通过被拒绝,也可能部分成交后挂在订单簿上。如果没有状态管理,你根本不知道这笔单子现在是什么情况。

避坑指南:我曾经在状态流转里漏掉了PARTIAL状态,结果部分成交的订单直接被标记为ACTIVE,导致后续的撤单逻辑完全失效。记住:状态机一定要覆盖所有可能的路径。

4.5 订单对象的扩展思考

实际生产环境中,Order类往往还会包含更多字段。比如:

  • 账户ID:区分不同交易员的订单
  • 策略ID:方便归因分析,知道这笔单子是哪个策略下的
  • 订单类型:限价单、市价单、止损单、冰山订单等
  • 有效时间:GTC(一直有效)、IOC(立即成交否则撤销)、FOK(全部成交否则撤销)

但核心原则是:不要一开始就把所有字段都塞进去。我见过一个团队把Order类搞成了30多个字段的庞然大物,结果维护成本极高。先做减法,等真正需要的时候再加。

核心总结:

订单对象是交易系统的基石。价格、数量、时间戳、订单ID、方向这五个属性,缺一不可。用Decimal保精度,用UUID保唯一,用纳秒时间戳保顺序。记住:一个设计良好的Order类,能让后面的撮合引擎、风控模块、回测系统都少踩很多坑。

订单对象核心结构 Order 类 价格 (Decimal) 数量 (Decimal) 时间戳 (纳秒) 订单ID (UUID) 方向 (BUY/SELL) 扩展属性(按需添加) 账户ID 策略ID 订单类型 有效时间 核心属性是订单的骨架,扩展属性是血肉,按实际场景灵活组合

嗯,订单对象建模就聊到这儿。记住:好的设计不是一次完成的,而是在一次次踩坑中迭代出来的。你的第一个Order类可能很简单,没关系,先跑起来再说。

专注资料整理