Skip to content

04 · 交易接口与订单生命周期:系统的心脏

行情模块做错了,损失的是数据质量;交易模块做错了,损失的是真金白银。交易接口是整个对接系统里**最不容许「试错」**的部分:重复下单、撤不掉单、状态错乱、回报乱序,任何一个都在直接烧钱。

本篇从接口形态讲起,贯穿订单类型、状态机、撮合原理、生命周期管理、成交回报、对账、限频,最后给出多年对接血泪总结的 bug 清单。


1. 接口形态

接口形态特点适用场景代表
REST请求-应答,天然幂等(查询类);下单需靠「客户端订单号」幂等低频下单、查询、对账、管理类操作币安 REST、OKX REST、券商普通接口
WebSocket长连接、全双工、回调式推送;适合订单回报与账户推送加密交易所回报订阅、前端实时状态币安 WS 用户数据流、OKX WS
FIX金融行业标准文本协议,字段字典固定,广泛用于机构间海外市场(CME 等)、机构对接CME FIX/FAST
CTP 专有二进制C++ DLL、结构体消息、回调模型,国内柜台事实标准国内期货主流路径CTP(详见 02-交易所与柜台.md

选择建议

  • 下单走 REST(加密)/ 柜台 API(国内)——查询、改单、撤单都有明确的请求-应答语义,便于超时重试与幂等。
  • 回报必须走推送通道(WS/回调),绝不能用轮询拉回报——轮询的延迟和漏单风险在交易场景不可接受。
  • FIX 只在海外机构场景出现,团队首次接触建议先做协议仿真再上。

2. 订单类型全览

订单类型定义适用场景
限价单(Limit)以指定价格(或更优价格)成交,不保证成交绝大多数策略的默认选择;T+1/支持改价
市价单(Market)以当前市场最优价立即成交,不保证价格快速离场、流动性好的品种;国内期货无标准市价单(部分柜台以「市价转限价」实现,以柜台规则为准)
**止损**单(Stop)价格触发后转为市价/限价单突破入场、风险离场
止损限价单(Stop-Limit)触发后转限价单,价差有保护但可能不成交既要止损又怕**滑点**
IOC(立即成交否则取消)可部分成交,未成交部分立即撤销抢入场、套利
FOK(全成否则全撤)要么全部成交,要么全部撤销一揽子/组合单、套利腿的强约束
冰山单(Iceberg)只暴露部分数量,隐藏剩余部分大单拆小、避免冲击
条件单(Conditional / OCO)一组条件单:一个触发则其余自动撤销止盈+止损同时挂,离场自动化
盘口一档委托(对手方最优档)以对手方最优一档价格委托,未成交部分撤销/保留A 股常见的市价委托类型(如「对手方最优五档即时成交剩余撤销」等,以交易所规则为准)

两点提示:① 加密交易所的订单类型名称与行为(如币安 STOP_LOSS_LIMITTAKE_PROFIT_MARKET 等)与国内期货差别很大,按交易所文档逐一定义映射;② 「止损单」在柜台侧是否支持、触发后转什么单,以柜台实现为准——很多国内柜台不支持交易所级止损单,需要客户端自己盯价格触发(这也是风控模块的职责之一)。

💀 国内柜台不支持交易所级止损单客户端必须自己盯

很多国内柜台不支持交易所级止损单,需要客户端自己盯价格触发。 依赖柜台提供止损单等于把命交给对方——柜台风控层一旦失效,你的止损就只是「一句代码」,必须把它实现进自己系统的风控闸门里。


3. 订单状态机

无论哪家交易所/柜台,订单的底层状态都可以归纳为下面这张图(状态命名参考交易所通用枚举,具体以各交易所文档为准):

订单状态机:NEW → 部分成交 → 全部成交/撤销,任何状态都可能被拒

关键状态与失败路径

状态含义常见触发
NEW订单已受理,等待撮合正常申报
PARTIALLY_FILLED部分成交,剩余部分继续挂大单分笔成交
FILLED全部成交正常
CANCELED被撤销主动撤单、超时自动撤销、部分成交后剩余撤销
REJECTED被拒绝,没有成为订单资金不足、价格超限、品种停牌、参数非法、频率超限
EXPIRED(加密常见)有效期到期未成交被撤设置了 timeInForce

工程铁律

  1. 状态只能前进、不能回退:NEW → FILLED 是合法的;FILLED 之后永远不能变成其他状态。任何「状态回退」的回报都要按异常处理(报警 + 冻结该单)。
  2. REJECTED 不等于撤单:被拒的单不需要再撤,也不能再撤;很多 bug 就是「收到 REJECTED 后仍然执行了撤单流程」。

💀 重复下单是超时后直接重发造成的

超时重试策略:下单请求超时 → 先查询,再决定(query-first)。 禁止「超时直接重发」——这是重复下单的第一来源。曾有团队因重试逻辑没有幂等,一次网络抖动造成同一笔单重复成交了 30 次。

  1. 撤单也有状态:撤单请求同样可能失败(如订单刚成交)、可能被拒,撤单的回报(OrderCancelRejected / 撤单失败)必须同样处理。

4. 撮合原理

4.1 价格优先、时间优先

连续竞价的核心规则:价格优先(买价高/卖价低的先成交),价格相同时先到先得(时间优先)。这决定了:

  • 限价单成交的确定性取决于你在队列中的位置——同一价格,别人先挂就比你先成交。
  • 市价单/止损单的成交价 = 队列中对手方最优价,可能远差于你的心理价位。

4.2 集合竞价与连续竞价

  • 集合竞价(国内开盘前):集中撮合出一个开盘价,成交价唯一,所有高于开盘价的买单、低于开盘价的卖单全部按开盘价成交。
  • 连续竞价(交易时段):逐笔撮合,价格实时跳动。
  • 对对接的意义:集合竞价期间部分接口行为不同(如国内期货集合竞价不接受市价类委托、加密没有集合竞价概念),代码要按交易所规则区分处理。

4.3 做市商与盘口撮合差异

  • 期货/股票市场:订单簿撮合,有没有对手方取决于盘口深度。
  • 加密交易所:同样是订单簿撮合,但部分平台对特定品种有做市商激励、以及「止盈止损」等条件触发在交易所侧实现——触发逻辑在交易所 vs 在客户端的差异,直接决定断网时你的单能不能动。

5. 订单生命周期管理

5.1 标准流程

text
策略/用户提交意图(目标品种、方向、数量、价格)


风控前置校验(见 05 篇)——不通过则拦截,永远不进下单链路


生成本地订单(分配本地订单号 clientOrderId)


调用交易所接口下单(携带 clientOrderId)

        ▼ 请求超时?
        ├── 是 → 查询订单状态(幂等确认),根据结果决定「补发/撤回请求/标记未知」
        └── 否 → 等待回报

回报驱动状态机更新(NEW → PARTIALLY → FILLED / CANCELED / REJECTED)


成交回报落库 + 通知策略 + 通知前端

5.2 幂等设计(最重要)

  • 客户端订单号(clientOrderId / ClientID):下单请求里必须携带全局唯一的本地订单号,交易所把它与订单绑定。重试同一笔下单时使用同一个 clientOrderId——交易所据此去重(支持与否以交易所文档为准),杜绝「网络超时重发,结果下了两单」。
  • 撤单幂等:对同一订单的撤单请求同样要用「撤单请求号」去重;重复撤单在部分交易所会返回错误(说明已撤/不存在),要按「幂等成功」处理而不是按异常报错。
  • 超时重试策略:下单请求超时 → 先查询,再决定(query-first)。禁止「超时直接重发」——这是重复下单的第一来源。

5.3 本地订单号与交易所订单号映射

  • 下单前:本地订单号(clientOrderId)本地唯一即可。
  • 下单后:交易所返回交易所订单号(orderId),必须与本地订单号双向映射并持久化:以后所有查询、撤单、对账都用交易所订单号。
  • 映射丢失的后果:撤不了单、对不了账——所以映射要在下单返回后第一时间落库,宁可先写库再响应用户。

6. 成交回报处理

6.1 逐笔回报与批量回报

  • 逐笔回报(order + trade 分开推):先推订单状态变化,再推成交明细(可能一笔订单多次部分成交,推多条 trade)。订单回报与成交回报是两回事OnRtnOrder 说「单子的状态」,OnRtnTrade 说「真的成交了多少钱量」。
  • 批量回报(加密常见):一个事件里携带多条成交/状态变更,处理时注意幂等(同一条回报可能因重连被重推)。
  • 处理原则:以 trade 为准记持仓与资金,以 order 为准记状态;两者按时间戳/序号对齐,顺序错乱时以「状态只能前进」为约束自愈。

6.2 tick 级成交与「最后成交价」的区分

  • tick 级成交(逐笔成交回报):每一笔成交的精确价格、数量、时间——记持仓、记成本、做滑点分析都用它。
  • 「最后成交价」(行情快照里的最新价):是行情层面的聚合结果,不能当成本价,也不代表你这一笔的成交价。
  • 常见错误:用行情最新价给成交单记账 → 对账永远对不上。成本只认回报,不认行情。

⚠️ 成本只认回报不认行情

用行情最新价给成交单记账 → 对账永远对不上。成本只认回报,不认行情。 tick 级成交回报才是记持仓、记成本、做滑点分析的依据,行情快照里的「最后成交价」是行情层面的聚合结果,不能当成本价。


7. 对账机制

text
客户端本地状态(持仓/资金/订单)

        ▼  周期性(每日收盘 + 盘中抽样)
交易所/柜台状态(查询接口)


逐项比对:持仓数量/均价、资金余额、当日委托数、成交数


差异 → 分级处理:可自愈(漏回报→补拉)自动修复;
        不可自愈(金额差)→ 冻结相关交易 + 人工介入
对账场景时机处理
当日委托/成交对账每个交易日收盘后拉全部当日委托与成交,与本地逐条核对;差异按漏回报补数据
持仓对账每日 + 盘中抽查本地持仓 vs 柜台持仓;不一致先暂停该合约交易
资金对账每日余额、冻结、手续费差异逐笔排查
重启恢复进程重启后拉取当日全部委托/成交/持仓重建本地状态,再恢复自动交易(恢复期间禁止下单)

对账不是「上线时做一次」的仪式,而是每天自动运行、差异自动报警的常态化机制。对账对不上时,唯一正确动作是先停相关交易,再排查


8. 限频与并发

8.1 rate limit 规则

交易所典型限频(以官方文档为准)
币安权重制(weight-based):每请求消耗权重,每分钟额度(如 6000 权重/分钟),查询与下单权重不同
OKX每秒/每分钟请求数限制,按接口分档
CTP 等柜台通常无公开固定值,但高频报单会触发柜台/交易所「异常交易行为」监控(如撤单率、报撤比超限)

8.2 处理策略

  • 排队:全局请求队列 + 令牌桶/滑动窗口限速器,把请求速率压在线内,宁可排队不可超限。
  • 退避:收到 429/限频错误 → 指数退避重试,退避期间不叠加请求。
  • 分级限速:下单类请求优先级高于查询类;查询失败不影响下单路径。
  • 多账户并发控制:多个交易账号并发时,每个账号独立限额,同时全局队列兜底;防止「单账户限频 → 全系统超时 → 回报堆积」。

8.3 限频与行情的关系

  • 行情订阅请求与交易请求共用限频池(部分交易所如此),行情订阅失败会连带影响交易——订阅管理要复用连接、减少订阅请求次数。

9. 常见对接 bug 清单

#Bug成因对策
1重复下单超时后直接重发,无幂等clientOrderId 幂等 + query-first 重试
2重复撤单撤单请求无幂等号,或撤单回报延迟期间再次撤撤单请求号去重 + 按幂等成功处理
3状态丢失漏处理某类回报(如取消失败、过期),本地状态停在旧值状态机全路径覆盖 + 每日对账兜底
4回报乱序多线程处理回报、或重连后旧回报与新回报交错单连接单消费者;按时间戳/序号排序;状态只前进
5用行情价记账成本用最新价而不是成交回报成本只认 trade 回报
6拒单当撤单REJECTED 后仍执行撤单流程状态机区分 REJECTED / CANCELED
7重启后裸奔进程重启后直接恢复自动交易,本地状态是旧的先重建状态(拉当日委托/持仓)再恢复
8限频炸锅请求超限被拒后全部重试,雪崩退避 + 排队 + 分级限速
9集合竞价窗口错乱未按交易所时段规则区分委托行为交易日历 + 时段状态机(详见 02 篇 9.3)
10订单号格式非法clientOrderId 用了不允许的字符/超长按交易所文档校验字段再发送

风险提示

⚠️ 风险提示

交易接口环节的事故往往是「高频 + 错误叠加」:曾有团队上线时把下单参数里的价格小数位配错,在毫秒级循环里连续打出数千笔离谱价格的有效订单,等发现时已无法全部撤掉;也有团队因为重试逻辑没有幂等,一次网络抖动造成同一笔单重复成交了 30 次。请务必记住:任何自动重试都必须先查询确认任何参数变更都必须先仿真验证再小资金试运行;上线当天禁止改代码。真实事故往往不是「某个 bug 多严重」,而是「错的代码跑得有多快」——把速度留给正确的系统,把刹车装在任何出错之前(风控详见 05-风控与资金管理.md)。

相关阅读

仅供学习与研究,不构成任何投资建议。市场有风险,投资需谨慎。