订单管理接口:下单、撤单、查询与批量操作

订单管理,说白了就是交易系统的「手」和「眼」。

手负责执行——下单、撤单、改单。眼负责监控——查状态、查成交、查持仓。我做了这么多年量化系统,见过太多因为订单接口设计不合理导致的悲剧:要么下单太慢错过行情,要么撤单失败造成敞口风险,要么查询接口把数据库打爆。

今天咱们就把这块掰开揉碎,好好聊聊。

4.1 下单接口:限价、市价、条件单

下单接口是所有交易操作的入口。我个人习惯把它设计成三个独立的方法,而不是一个万能接口。为什么?因为每种订单的生命周期和校验逻辑完全不同,混在一起反而容易出bug。

4.1.1 限价单(Limit Order)

限价单是最基础的订单类型。你指定价格和数量,系统只在达到或优于这个价格时成交。

核心设计要点:

  • 价格精度校验:必须与交易所的最小价格变动单位对齐
  • 数量精度校验:最小交易单位、最大持仓限制
  • 订单有效期:默认当日有效(DAY),支持GTC(一直有效直到取消)
// 限价单请求体示例
POST /api/v1/orders/limit
{
  "symbol": "BTCUSDT",
  "side": "BUY",
  "price": "45000.50",
  "quantity": "0.01",
  "timeInForce": "DAY",
  "clientOrderId": "ORD_20240101_001"
}

// 响应示例
{
  "orderId": "123456789",
  "clientOrderId": "ORD_20240101_001",
  "status": "NEW",
  "transactTime": "2024-01-01T10:00:00.000Z"
}

避坑指南:我曾经遇到过一个坑——限价单的价格精度没做校验,用户传了小数点后8位,交易所直接拒单。后来我强制在服务端做精度截断,并且返回截断后的实际价格给用户确认。

4.1.2 市价单(Market Order)

市价单追求的是「成交速度」,而不是「成交价格」。你想想看,在行情剧烈波动时,限价单可能永远成交不了,这时候市价单就派上用场了。

但市价单有个风险:滑点。我建议在接口层面做滑点保护。

// 市价单请求体示例
POST /api/v1/orders/market
{
  "symbol": "ETHUSDT",
  "side": "SELL",
  "quantity": "1.5",
  "slippageProtection": {
    "maxSlippage": "0.5%",   // 最大允许滑点
    "referencePrice": "3200.00"  // 参考价格
  }
}

注意:市价单的成交金额可能远超预期。我见过一个新手用市价单买了1000个ETH,结果因为流动性不足,成交均价高了2%。所以滑点保护不是可选项,是必选项。

4.1.3 条件单(Conditional Order / Stop Order)

条件单是「触发后执行」的订单。最常见的两种:止损单和止盈单。

条件单的设计难点在于「触发条件」的判定。我个人习惯把条件单拆成两个阶段:

  • 监控阶段:系统持续检查市场行情是否满足触发条件
  • 执行阶段:条件满足后,立即转为限价单或市价单提交
// 条件单请求体示例
POST /api/v1/orders/conditional
{
  "symbol": "BTCUSDT",
  "side": "SELL",
  "condition": {
    "type": "STOP_LOSS",
    "triggerPrice": "44000.00",
    "triggerDirection": "BELOW"  // 价格低于触发价时激活
  },
  "execution": {
    "type": "LIMIT",
    "price": "43950.00",
    "quantity": "0.05"
  }
}

经验之谈:条件单的触发价格和成交价格之间一定要留足缓冲。我曾经设计过一个系统,止损单的触发价和限价完全一样,结果行情一跳穿,单子永远成交不了。后来我强制要求至少留0.1%的缓冲。

4.2 撤单接口

撤单看似简单,其实暗藏玄机。核心问题是:撤单请求和成交回报可能同时到达。

我建议采用「幂等撤单」设计:

  • 每个撤单请求必须携带唯一的请求ID
  • 系统对同一个订单的重复撤单请求,返回相同的结果
  • 如果订单已经成交,撤单请求返回「订单已成交,无法撤销」
// 撤单请求
DELETE /api/v1/orders/{orderId}
{
  "cancelRequestId": "CANCEL_20240101_001"
}

// 成功响应
{
  "orderId": "123456789",
  "status": "CANCELED",
  "cancelTime": "2024-01-01T10:05:00.000Z"
}

// 已成交响应
{
  "orderId": "123456789",
  "status": "FILLED",
  "message": "订单已全部成交,无法撤销"
}

我曾经踩过的坑:撤单接口没有做幂等处理,结果网络重试导致同一个订单被撤了两次。第一次成功,第二次返回错误,但用户以为撤单失败,又手动撤了一次……最后订单状态乱成一锅粥。从那以后,幂等设计成了我的铁律。

4.3 查询订单接口

查询接口最容易出性能问题。你想想看,一个高频交易系统每秒可能产生上千笔订单,如果每个查询都去扫数据库,系统很快就挂了。

我建议的查询接口设计原则:

  • 按订单ID查询:走缓存,毫秒级响应
  • 按条件查询:必须带时间范围,防止全表扫描
  • 分页查询:使用游标分页(cursor-based pagination),而不是传统的offset分页
// 按订单ID查询
GET /api/v1/orders/{orderId}

// 按条件查询(带时间范围)
GET /api/v1/orders?symbol=BTCUSDT&status=FILLED&startTime=2024-01-01T00:00:00Z&endTime=2024-01-02T00:00:00Z&limit=100

// 游标分页响应
{
  "orders": [...],
  "nextCursor": "2024-01-01T12:00:00.000Z_123456790",
  "hasMore": true
}

性能优化技巧:我习惯在订单表上建立复合索引:(symbol, status, transactTime)。这样大部分查询都能走索引,避免全表扫描。另外,历史订单超过3天的自动归档到冷存储,查询时走异步任务。

4.4 批量操作接口

批量操作是高频交易系统的刚需。比如同时下10个不同品种的订单,或者一次性撤销所有未成交的订单。

批量操作的设计要点:

  • 原子性:要么全部成功,要么全部失败(或部分成功部分失败,但必须明确告知)
  • 顺序性:批量中的每个操作按顺序执行,后一个操作依赖前一个的结果
  • 错误隔离:一个操作失败不影响其他操作
// 批量下单
POST /api/v1/orders/batch
{
  "orders": [
    {
      "type": "LIMIT",
      "symbol": "BTCUSDT",
      "side": "BUY",
      "price": "45000",
      "quantity": "0.01"
    },
    {
      "type": "MARKET",
      "symbol": "ETHUSDT",
      "side": "SELL",
      "quantity": "1.0"
    }
  ]
}

// 批量响应
{
  "results": [
    {
      "index": 0,
      "status": "SUCCESS",
      "orderId": "123456789"
    },
    {
      "index": 1,
      "status": "FAILED",
      "errorCode": "INSUFFICIENT_BALANCE",
      "message": "余额不足"
    }
  ]
}

批量撤单的典型场景:行情突变时,交易员需要一键撤销所有未成交订单。我建议提供两种批量撤单方式:

  • 按品种撤单:撤销某个品种的所有未成交订单
  • 按策略撤单:撤销某个策略ID下的所有订单
// 按品种批量撤单
DELETE /api/v1/orders/batch?symbol=BTCUSDT

// 按策略批量撤单
DELETE /api/v1/orders/batch?strategyId=STRAT_001

4.5 订单状态机与生命周期

理解订单的状态流转,是设计订单管理接口的基础。我画了一张图,帮你理清思路。

订单状态机流转图 NEW(新订单) PARTIALLY_FILLED(部分成交) FILLED(全部成交) CANCELED(已撤销) REJECTED(已拒绝) EXPIRED(已过期) 成交 全部成交 剩余成交 撤单 撤单 风控拒绝 超时过期 图例 活跃状态 终态(成功) 终态(失败) 终态(其他)

从这张图可以看出,订单的终态只有三个:全部成交、已撤销、已拒绝、已过期。其他状态都是中间态。嗯,这里要注意:部分成交状态是可以持续很久的,比如一个限价单可能几天才成交一部分。

4.6 接口设计的几个铁律

最后,我总结几条这些年用血泪换来的设计铁律:

  1. 所有订单操作必须记录日志。包括请求参数、响应结果、处理耗时。出问题时有据可查。
  2. 订单ID必须全局唯一。我习惯用雪花算法生成,包含时间戳+机器ID+序列号。
  3. 客户端订单ID(clientOrderId)必须支持。这样用户可以在自己的系统里做关联,排查问题更方便。
  4. 接口限流不能少。下单接口尤其要限流,防止程序bug导致海量订单打爆交易所。
  5. 异步处理 + 回调通知。订单状态变化时,通过WebSocket或消息队列推送给客户端,而不是让客户端轮询。

总结一下:订单管理接口是交易系统的核心命脉。设计得好,系统稳定高效;设计得不好,轻则丢单,重则爆仓。我建议你在开发初期就把这些接口的规范定死,后期改起来成本极高。


专注资料整理