订单系统

订单生命周期与状态机

深入分析订单从创建到终态的完整生命周期,涵盖状态流转、各类型订单的处理路径、大单拆解、撤单平仓以及并发幂等设计。

1. 订单状态机

每个订单在其生命周期中经历以下状态。所有状态变更通过事件驱动,保证可审计与可重放。

状态说明

PENDING_NEW

订单已提交但尚未被系统确认。此时正在进行参数校验、风控检查和资金冻结。

NEW

订单已通过风控,资金已冻结,进入 Kafka 队列等待撮合。限价单会被放入订单簿。

PARTIALLY_FILLED

订单已部分成交。记录 cumQty(累计成交量)和 avgPrice(平均成交价),剩余量继续在簿。

FILLED

终态。订单全量成交,冻结资金全部结算完毕。

PENDING_CANCEL

用户提交撤单请求,等待撮合引擎确认。此期间可能有最后的成交。

CANCELED

终态。订单撤销,未成交部分的冻结资金已解冻退回。

REJECTED

终态。订单被系统拒绝(余额不足、价格异常、风控限制等),无资金变动。

EXPIRED

终态。IOC/FOK/GTT 订单超时或不满足条件自动过期。

动手试试状态转换

点击下方按钮,模拟订单从创建到终态的完整生命周期:

订单状态机模拟器 — 点击按钮触发状态转换
PENDING_NEW
📋 NEW
PARTIALLY_FILLED
FILLED
🔄 PENDING_CANCEL
CANCELED
🚫 REJECTED
EXPIRED
⏳ 当前状态: PENDING_NEW
订单已提交,正在进行风控校验和资金冻结...

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 <= 当前价触发。

止损限价

触发后变为限价单进入订单簿。如果限价与市场价差距太大可能无法立即成交。

止损市价

触发后变为市价单立即成交。保证执行但不保证价格,适用于快速止损场景。

OCO 单

One-Cancels-Other:同时挂止损单和止盈单,任一触发后自动撤销另一个。通过订单关联 ID 实现。

6. IOC / FOK / Post-Only

时间有效性策略(Time in Force)决定未成交部分的处理方式。

GTC

默认策略。订单一直挂在簿中直到完全成交或用户手动撤单。大多数限价单使用此策略。

IOC

立即执行可成交部分,剩余立即取消。适合需要快速成交但不想长期挂单的场景。可能产生部分成交。

FOK

全有或全无。撮合前先检查订单簿流动性是否足够全量成交,不足则整单拒绝。适合大额交易需要确定性的场景。

Post-Only

强制只做 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),每笔成交后提交下一笔,减少市场冲击。

TWAP/VWAP

算法单:按时间加权或成交量加权拆分,定时提交子单。由上层策略引擎驱动,撮合引擎只处理单个订单。

大单拆解模拟

拖动滑块调整订单大小,观察大单如何逐价位消耗卖盘:

大单拆解模拟器 — 拖动滑块调整订单大小,点击播放观看
0.5 BTC小单中单大单15 BTC

42000
1.00
1.0
42020
0.50
0.5
42050
2.00
2.0
42080
1.50
1.5
42100
3.00
3.0
42150
2.50
2.5
42200
4.00
4.0
原始量8.0 BTC
已成交0.0000 BTC
剩余8.0000 BTC

8. 撤单完整流程

撤单幂等

重复撤单请求不会重复解冻。撮合引擎按 orderId 查找,不存在则忽略。

竞态处理

撤单和成交可能竞争同一订单。撮合引擎按 Kafka 消息顺序处理,谁先到谁先执行。

批量撤单

支持按 symbol 或全部撤单。Order Service 批量写 cancel.req,撮合引擎逐条处理。

资金解冻

CANCELED 后 Settlement 计算:已成交部分正常结算,未成交部分冻结金额退回可用余额。

9. 平仓流程

平仓是杠杆/合约交易中关闭持仓的操作。现货中等价于反向卖出持有的资产。

reduceOnly

平仓订单必须标记 reduceOnly=true,防止超量平仓变成反向开仓。撮合时 qty 不超过当前持仓量。

PnL 计算

做多 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. 并发与幂等保障

API 去重

clientOrderId + userId 在 Redis 中 SETNX,TTL 24 小时。重复请求直接返回已存在的订单信息。

撮合幂等

撮合引擎重放 Kafka 消息时,已处理的 orderId 跳过。内存维护已处理集合,快照时持久化。

结算幂等

Settlement 按 tradeId 去重。PostgreSQL 的 trades 表 tradeId 唯一约束,重复插入直接忽略。

资金幂等

冻结/解冻操作以 orderId 为幂等键。同一订单的冻结只执行一次,解冻只在状态变更时触发。

附: 手续费计算器

输入成交金额、选择角色和 VIP 等级,实时计算手续费:

手续费计算器 — 输入参数实时计算费用

基础费率0.1000%
实际费率0.1000%

成交金额10,000 USDT
手续费10.0000 USDT
净额9990.0000 USDT
等级费率手续费平台币抵扣后节省
普通用户0.10%10.007.50-
VIP 10.10%10.007.500.00
VIP 20.10%10.007.500.00
VIP 30.08%8.006.002.00
VIP 40.06%6.004.504.00
VIP 50.04%4.003.006.00
VIP 6 (做市)0.03%3.002.257.00