订单生命周期与状态机
深入分析订单从创建到终态的完整生命周期,涵盖状态流转、各类型订单的处理路径、大单拆解、撤单平仓以及并发幂等设计。
1. 订单状态机
每个订单在其生命周期中经历以下状态。所有状态变更通过事件驱动,保证可审计与可重放。
状态说明
订单已提交但尚未被系统确认。此时正在进行参数校验、风控检查和资金冻结。
订单已通过风控,资金已冻结,进入 Kafka 队列等待撮合。限价单会被放入订单簿。
订单已部分成交。记录 cumQty(累计成交量)和 avgPrice(平均成交价),剩余量继续在簿。
终态。订单全量成交,冻结资金全部结算完毕。
用户提交撤单请求,等待撮合引擎确认。此期间可能有最后的成交。
终态。订单撤销,未成交部分的冻结资金已解冻退回。
终态。订单被系统拒绝(余额不足、价格异常、风控限制等),无资金变动。
终态。IOC/FOK/GTT 订单超时或不满足条件自动过期。
动手试试状态转换
点击下方按钮,模拟订单从创建到终态的完整生命周期:
订单已提交,正在进行风控校验和资金冻结...
2. 订单数据模型
订单对象是整个交易系统的核心实体,贯穿从接入到撮合再到结算的全流程。
interface Order {
// === 标识 ===
orderId: string; // 系统分配的全局唯一 ID(Snowflake)
clientOrderId: string; // 客户端幂等 ID
userId: number; // 用户 ID
accountId: number; // 账户 ID(支持子账户)
// === 交易对 ===
symbol: string; // 如 "BTCUSDT"
baseAsset: string; // 基础资产 "BTC"
quoteAsset: string; // 计价资产 "USDT"
// === 订单参数 ===
side: "BUY" | "SELL";
type: "LIMIT" | "MARKET" | "STOP_LIMIT" | "STOP_MARKET";
timeInForce: "GTC" | "IOC" | "FOK" | "GTT";
price: string; // 限价价格(decimal string 防精度丢失)
stopPrice: string; // 止损触发价
quantity: string; // 原始委托量
quoteOrderQty: string; // 市价按金额下单时使用
// === 标志位 ===
postOnly: boolean; // 只做 Maker
reduceOnly: boolean; // 只减仓
isLiquidation: boolean; // 强平订单标志
// === 成交状态 ===
status: OrderStatus;
executedQty: string; // 累计成交量
cummulativeQuoteQty: string; // 累计成交额
avgPrice: string; // 平均成交价
remainingQty: string; // 剩余未成交量
// === 手续费 ===
totalFee: string; // 累计手续费
feeAsset: string; // 手续费币种
feeRole: "MAKER" | "TAKER";
// === 杠杆(保证金交易) ===
leverage: number; // 杠杆倍数
marginType: "CROSS" | "ISOLATED";
frozenMargin: string; // 冻结保证金
// === 时间 ===
createdAt: number; // 创建时间戳 ms
updatedAt: number; // 最后更新时间戳 ms
expireAt: number; // GTT 过期时间
// === 版本控制 ===
version: number; // 乐观锁版本号
}Trade(成交记录)数据模型
interface Trade {
tradeId: string; // 全局唯一成交 ID
symbol: string;
price: string; // 成交价格
quantity: string; // 成交数量
quoteQty: string; // 成交额 = price * quantity
// === 买卖双方 ===
buyOrderId: string;
buyUserId: number;
sellOrderId: string;
sellUserId: number;
// === 角色 ===
buyerIsMaker: boolean; // true = 买方是 Maker
// === 手续费 ===
buyFee: string;
buyFeeAsset: string;
sellFee: string;
sellFeeAsset: string;
// === 时间 ===
tradeTime: number; // 成交时间戳 ms
sequenceId: number; // 撮合引擎内部序列号
}3. 限价单完整流程
限价单(Limit Order)是最核心的订单类型:指定价格和数量,只在等于或优于指定价格时成交。
关键细节
买单冻结 price * qty 的计价资产(USDT);卖单冻结 qty 的基础资产(BTC)。冻结确保成交后有资金结算。
Taker 按 Maker 挂单价成交。如买单 42000 吃到挂卖 41950,则以 41950 成交,买方获得更优价格。
未完全成交的限价单进入订单簿排队。买盘按价格降序、卖盘按价格升序;同价位按时间先入先出(FIFO)。
每次部分成交都产生独立的 trade.done 事件,订单状态更新为 PARTIALLY_FILLED,下游可实时感知。
4. 市价单完整流程
市价单(Market Order)不指定价格,以当前最优价格立即成交,不会进入订单簿。
市价单 vs 限价单
市价单永远是 Taker,按 Taker 费率收费;成交后不留在簿中。
系统设定最大滑点保护(如 bestPrice * 5%),超过后停止成交并取消剩余。
支持 quoteOrderQty 模式:如 "花 10000 USDT 买 BTC",按价格逐级消耗直至金额用尽。
因无法预知最终成交价,按 bestPrice + 安全系数冻结;成交后退还差额。
5. 止损单触发流程
止损单不立即进入撮合,而是在市场价到达触发价时被激活为普通限价单或市价单。
Stop-Limit/Stop-Market 使用 Last Price 或 Mark Price 作为触发参考。买方 stopPrice >= 当前价触发,卖方 stopPrice <= 当前价触发。
触发后变为限价单进入订单簿。如果限价与市场价差距太大可能无法立即成交。
触发后变为市价单立即成交。保证执行但不保证价格,适用于快速止损场景。
One-Cancels-Other:同时挂止损单和止盈单,任一触发后自动撤销另一个。通过订单关联 ID 实现。
6. IOC / FOK / Post-Only
时间有效性策略(Time in Force)决定未成交部分的处理方式。
默认策略。订单一直挂在簿中直到完全成交或用户手动撤单。大多数限价单使用此策略。
立即执行可成交部分,剩余立即取消。适合需要快速成交但不想长期挂单的场景。可能产生部分成交。
全有或全无。撮合前先检查订单簿流动性是否足够全量成交,不足则整单拒绝。适合大额交易需要确定性的场景。
强制只做 Maker。如果提交时价格会导致立即成交(Taker),则拒绝订单。用于确保享受 Maker 低费率。
撮合引擎中的实现
function matchOrder(order):
// FOK: 先检查流动性
if order.timeInForce == "FOK":
if !hasEnoughLiquidity(order): return reject(order)
// Post-Only: 检查是否会 Taker
if order.postOnly:
if wouldMatchImmediately(order): return reject(order)
return addToBook(order) // 直接挂单
// 正常撮合
trades = executeMatching(order)
// IOC: 撤销剩余
if order.timeInForce == "IOC" and order.remainingQty > 0:
cancelRemaining(order)
return
// GTC: 挂入订单簿
if order.remainingQty > 0:
addToBook(order)7. 大单拆解成交
大额订单在撮合引擎中会逐价位消耗对手盘,产生多笔成交记录。这不是预先拆分子单,而是在撮合过程中自然拆解。
成交明细
// 大单拆解过程(10 BTC 市价买入)
Trade #1: price=42000, qty=3, amount=126,000 → 累计 3/10
Trade #2: price=42050, qty=2, amount= 84,100 → 累计 5/10
Trade #3: price=42100, qty=4, amount=168,400 → 累计 9/10
Trade #4: price=42200, qty=1, amount= 42,200 → 累计 10/10
总成交额: 420,700 USDT
平均成交价: 42,070 USDT
滑点: (42,070 - 42,000) / 42,000 = 0.167%
// 订单状态变化:
NEW → PARTIALLY_FILLED (trade#1)
→ PARTIALLY_FILLED (trade#2)
→ PARTIALLY_FILLED (trade#3)
→ FILLED (trade#4)撮合引擎不预拆子单。大单按价格优先逐级消耗,每级产生一个 Trade,直至成交完或无流动性。
可设置 maxSlippage 参数。如超过 bestPrice * (1 + maxSlippage) 则停止继续吃单。
将大单按固定小额分批提交(如 10 BTC 拆成 10 个 1 BTC),每笔成交后提交下一笔,减少市场冲击。
算法单:按时间加权或成交量加权拆分,定时提交子单。由上层策略引擎驱动,撮合引擎只处理单个订单。
大单拆解模拟
拖动滑块调整订单大小,观察大单如何逐价位消耗卖盘:
8. 撤单完整流程
重复撤单请求不会重复解冻。撮合引擎按 orderId 查找,不存在则忽略。
撤单和成交可能竞争同一订单。撮合引擎按 Kafka 消息顺序处理,谁先到谁先执行。
支持按 symbol 或全部撤单。Order Service 批量写 cancel.req,撮合引擎逐条处理。
CANCELED 后 Settlement 计算:已成交部分正常结算,未成交部分冻结金额退回可用余额。
9. 平仓流程
平仓是杠杆/合约交易中关闭持仓的操作。现货中等价于反向卖出持有的资产。
平仓订单必须标记 reduceOnly=true,防止超量平仓变成反向开仓。撮合时 qty 不超过当前持仓量。
做多 PnL = (exitPrice - entryPrice) * qty;做空 PnL = (entryPrice - exitPrice) * qty。
可以只平一部分仓位。如持仓 5 BTC 只平 2 BTC,剩余 3 BTC 继续持有,entryPrice 不变。
强平订单优先级最高,插入队列头部。成交后若仍亏损超过保证金,差额由保险基金或 ADL 覆盖。
10. 订单 ID 生成策略
Snowflake ID
64 位:1 bit 符号 + 41 bit 时间戳 + 10 bit 机器 ID + 12 bit 序列号。每毫秒可生成 4096 个有序 ID。
clientOrderId
客户端提供的幂等 ID,全局唯一(userId + clientOrderId)。重复提交返回已有订单而非新建。
tradeId
每次成交的唯一 ID,由撮合引擎的 sequenceId 生成,全局递增,保证成交顺序可追溯。
orderId 索引
PostgreSQL 主键 + 用户维度联合索引 (userId, createdAt),支持按用户查历史订单。
11. 并发与幂等保障
clientOrderId + userId 在 Redis 中 SETNX,TTL 24 小时。重复请求直接返回已存在的订单信息。
撮合引擎重放 Kafka 消息时,已处理的 orderId 跳过。内存维护已处理集合,快照时持久化。
Settlement 按 tradeId 去重。PostgreSQL 的 trades 表 tradeId 唯一约束,重复插入直接忽略。
冻结/解冻操作以 orderId 为幂等键。同一订单的冻结只执行一次,解冻只在状态变更时触发。
附: 手续费计算器
输入成交金额、选择角色和 VIP 等级,实时计算手续费:
| 等级 | 费率 | 手续费 | 平台币抵扣后 | 节省 |
|---|---|---|---|---|
| 普通用户 | 0.10% | 10.00 | 7.50 | - |
| VIP 1 | 0.10% | 10.00 | 7.50 | 0.00 |
| VIP 2 | 0.10% | 10.00 | 7.50 | 0.00 |
| VIP 3 | 0.08% | 8.00 | 6.00 | 2.00 |
| VIP 4 | 0.06% | 6.00 | 4.50 | 4.00 |
| VIP 5 | 0.04% | 4.00 | 3.00 | 6.00 |
| VIP 6 (做市) | 0.03% | 3.00 | 2.25 | 7.00 |