# 订单记录简化流程(支付式闭环) 参考 `订单交易更新推送.txt`、`条件订单交易更新推送.txt`,把订单记录做成「先本地单号 → 写 DB + 下单 → 仅靠 WS 推送更新状态」的闭环,和支付系统类似。 --- ## 一、目标流程(你期望的) 1. **先本地生成订单号**(如 `client_order_id = SYS_时间戳_随机`)。 2. **写 DB 与下单尽量一体**:先插入一条「待成交」记录(带 `client_order_id`),再立刻用该 id 去交易所下单(REST 或 WS,选更稳的方式);逻辑上视为「同一事务」——下单失败则把该条记录标为失败/取消。 3. **状态只跟 WS 走**:用币安 **ORDER_TRADE_UPDATE**(和必要时 **ALGO_UPDATE**)驱动所有「成交、平仓、取消」等状态与字段更新,DB 只根据推送更新,不依赖 REST 轮询结果做主数据。 这样:**一条 DB 记录 = 一次「开仓意图」或「平仓意图」**,用本地 id 和交易所 id 串联,WS 是唯一事实来源,逻辑简单、易对账。 --- ## 二、币安推送里用到的字段(docs/bian) ### ORDER_TRADE_UPDATE(订单交易更新推送) | 字段 | 含义 | 用途 | |------|------|------| | `e` | 事件类型 | ORDER_TRADE_UPDATE | | `o.c` | 客户端自定义订单 ID | **clientOrderId**,我们下单时传的,用来唯一匹配「本地这条记录」 | | `o.i` | 订单 ID | **orderId**,交易所订单号,写 entry_order_id / exit_order_id | | `o.x` | 本次事件类型 | NEW / TRADE / CANCELED / EXPIRED 等 | | `o.X` | 订单当前状态 | NEW / PARTIALLY_FILLED / **FILLED** / CANCELED 等 | | `o.ap` | 订单平均成交价 | 成交后更新 entry_price 或 exit_price | | `o.z` | 订单累计成交量 | 成交数量 | | `o.R` | 是否只减仓 | true = 平仓单 | | `o.rp` | 该笔实现盈亏 | 平仓时写 pnl/realized_pnl | **开仓**:`o.R != true` 且 `o.X == FILLED` → 用 `o.c` 找到 pending 记录,更新为 open,写入 `entry_order_id=o.i`、`entry_price=o.ap`、`quantity=o.z`。 **平仓**:`o.R == true` 且 `o.X == FILLED` → 用 `o.s`(symbol) + 当前 open 记录匹配,写入 `exit_order_id=o.i`、`exit_price=o.ap`、`pnl` 等(可用 `o.rp`)。 ### ALGO_UPDATE(条件订单交易更新推送) | 字段 | 含义 | 用途 | |------|------|------| | `o.X` | 条件单状态 | TRIGGERED / FINISHED 表示已触发 | | `o.ai` | 触发后普通订单 id | 平仓单触发后,用 **ai** 作为 exit_order_id,并等 ORDER_TRADE_UPDATE(ai) 拿成交价、rp | 止损/止盈是「条件单」:先下 Algo 单,触发后生成一笔普通订单,推送里给 `ai`。我们应: 1)在 ALGO_UPDATE 里用 `ai` 回写 `exit_order_id`; 2)在随后收到的 ORDER_TRADE_UPDATE(`o.i == ai`)里用 `ap/z/rp` 回写 exit_price、pnl 等,这样平仓数据也闭环。 --- ## 三、闭环流程(按事件串起来) ### 开仓 ``` 1. 生成 client_order_id = SYS__ 2. 写 DB:INSERT 一条 status=pending, client_order_id=client_order_id, symbol/side/quantity/... 3. 下单:REST 或 WS order.place,带 newClientOrderId=client_order_id - 若下单失败:UPDATE 该条为 status=canceled 或 failed(保证「有记录」且状态明确) 4. 之后只依赖 WS: - 收到 ORDER_TRADE_UPDATE,o.c=client_order_id,o.X=FILLED,非 R - → UPDATE 该条:status=open, entry_order_id=o.i, entry_price=o.ap, quantity=o.z ``` 这样:**开仓是否成交、成交价/量、交易所 orderId** 全部由 WS 一次更新完成,不依赖 REST 轮询。 ### 平仓(市价/限价主动平) ``` 1. 不新建 DB 记录,只「选一条当前 open 记录」准备关仓 2. 下单:reduceOnly 市价/限价单(可带 newClientOrderId 便于对账) 3. 只依赖 WS: - 收到 ORDER_TRADE_UPDATE,o.R=true,o.X=FILLED,o.s=symbol - → 按 symbol(+ 可选 orderId/clientOrderId) 匹配那条 open - → UPDATE:status=closed, exit_order_id=o.i, exit_price=o.ap, pnl/realized_pnl 等(可用 o.rp) ``` ### 平仓(条件单触发:止损/止盈) ``` 1. 不新建 DB 记录,只为当前 open 挂 Algo 单(STOP_MARKET/TAKE_PROFIT_MARKET 等) 2. 触发后只依赖 WS: - 先收到 ALGO_UPDATE:o.X=TRIGGERED/FINISHED,o.ai=触发后订单 id → UPDATE 该 open:exit_order_id=o.ai(先占位) - 再收到 ORDER_TRADE_UPDATE:o.i=o.ai,o.X=FILLED,o.R=true → 同一条记录:exit_price=o.ap,pnl/realized_pnl 用 o.rp,必要时补 duration/exit_time ``` 这样:**条件单触发的平仓**也完全由 WS 闭环,不依赖 REST 或定时同步做主数据。 --- ## 四、和当前实现的对应关系 | 目标步骤 | 当前实现 | 说明 | |----------|----------|------| | 本地先生成 client_order_id | ✅ 已有 | position_manager 里 `SYS_ts_rand`,并传 `newClientOrderId` | | 先写 DB 再下单 | ✅ 已有 | 先 `Trade.create(..., status='pending', client_order_id=...)`,再下单 | | 下单失败把记录标失败 | ⚠️ 部分 | 有失败路径,但不一定统一 UPDATE 为 canceled/failed | | 开仓成交只靠 WS 更新 | ✅ 已有 | User Data Stream 里 ORDER_TRADE_UPDATE FILLED + 非 R → `update_pending_to_filled(client_order_id, ..., order_id, ap, z)` | | 平仓成交只靠 WS 更新 exit_order_id | ✅ 已有 | ORDER_TRADE_UPDATE FILLED + R → `set_exit_order_id_for_open_trade(symbol, account_id, order_id)` | | 平仓成交用 WS 更新 exit_price / pnl | ❌ 缺口 | 目前只写了 exit_order_id,**没有**在 WS 里用 `ap/rp` 调 `update_exit(..., exit_price, pnl, ...)`,条件单触发的平仓尤其缺 | | 条件单触发用 ai 写 exit_order_id | ✅ 已有 | ALGO_UPDATE 里 `set_exit_order_id_for_open_trade(symbol, account_id, ai)` | | 「DB + 下单」原子性 | ⚠️ 语义上的 | DB 与交易所是两套系统,无法真 2PC;只能「先写 DB 再下单,失败则把该条标为失败」 | 所以:**主流程已经接近「支付式」**,真正缺的闭环是:**WS 收到平仓成交(含条件单触发的 ai 那笔)时,不仅要写 exit_order_id,还要用推送里的 ap/rp 等把 exit_price、pnl、exit_time 等一次更新掉**。 --- ## 五、建议补的一步(闭环平仓数据) 在 **User Data Stream** 里,当 `ORDER_TRADE_UPDATE` 中 `o.R == true` 且 `o.X == FILLED` 时,除现有 `set_exit_order_id_for_open_trade(symbol, account_id, order_id)` 外,建议: - 用同一推送里的 `o.ap`、`o.z`、`o.rp`(及可选 `o.N`/`o.n` 手续费)和当前时间(或 `o.T` 成交时间),对**同一条 open 记录**再调一次 **update_exit**(或等价的「按 exit_order_id 补全 exit_price / pnl / commission」接口),把: - exit_price - pnl / pnl_percent(或 realized_pnl) - commission(若有) - exit_time 都从 WS 一次写库。这样: - 市价/限价平仓:ORDER_TRADE_UPDATE 一次即可完成「exit_order_id + 价格 + 盈亏」闭环。 - 条件单平仓:ALGO_UPDATE 写 exit_order_id → 等 ORDER_TRADE_UPDATE(ai) 再按上面补全价格和盈亏,也闭环。 --- ## 六、小结 - **你的理解**:先本地单号 → 写 DB + 下单(尽量一体)→ 只靠 WS 通知更新订单状态与成交数据,是正确且更清晰的设计。 - **当前实现**:开仓侧已经是「先生成 client_order_id、先写 pending、再下单、WS 完善」;平仓侧 **exit_order_id** 已由 WS 回写,但 **exit_price / pnl 等还未在 WS 路径里统一写库**,算一个遗漏。 - **补上这一块**(WS 平仓成交时顺带 update_exit 价格与盈亏),就能在「不依赖 REST 轮询、不依赖同步接口做主数据」的前提下,做到订单记录与币安完全以 WS 为源的闭环。 - **事务**:DB 与交易所无法真事务,只能「先 DB 再下单,失败则把该条记录标为失败/取消」,当前逻辑已接近,可再统一失败态标记。 文档参考:`docs/bian/订单交易更新推送.txt`、`docs/bian/条件订单交易更新推送.txt`。