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类可能很简单,没关系,先跑起来再说。