# 订单记录简化流程（支付式闭环）

参考 `订单交易更新推送.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_<ts>_<rand>
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`。
