订单管理接口:下单、撤单、查询与批量操作
订单管理,说白了就是交易系统的「手」和「眼」。
手负责执行——下单、撤单、改单。眼负责监控——查状态、查成交、查持仓。我做了这么多年量化系统,见过太多因为订单接口设计不合理导致的悲剧:要么下单太慢错过行情,要么撤单失败造成敞口风险,要么查询接口把数据库打爆。
今天咱们就把这块掰开揉碎,好好聊聊。
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 订单状态机与生命周期
理解订单的状态流转,是设计订单管理接口的基础。我画了一张图,帮你理清思路。
从这张图可以看出,订单的终态只有三个:全部成交、已撤销、已拒绝、已过期。其他状态都是中间态。嗯,这里要注意:部分成交状态是可以持续很久的,比如一个限价单可能几天才成交一部分。
4.6 接口设计的几个铁律
最后,我总结几条这些年用血泪换来的设计铁律:
- 所有订单操作必须记录日志。包括请求参数、响应结果、处理耗时。出问题时有据可查。
- 订单ID必须全局唯一。我习惯用雪花算法生成,包含时间戳+机器ID+序列号。
- 客户端订单ID(clientOrderId)必须支持。这样用户可以在自己的系统里做关联,排查问题更方便。
- 接口限流不能少。下单接口尤其要限流,防止程序bug导致海量订单打爆交易所。
- 异步处理 + 回调通知。订单状态变化时,通过WebSocket或消息队列推送给客户端,而不是让客户端轮询。
总结一下:订单管理接口是交易系统的核心命脉。设计得好,系统稳定高效;设计得不好,轻则丢单,重则爆仓。我建议你在开发初期就把这些接口的规范定死,后期改起来成本极高。