# 订单记录与币安对账流程

本文梳理：**订单是怎么记的**、**何时写入 entry_order_id / exit_order_id**、**如何与币安保持一致**。

---

## 〇、常见误解：为什么「持仓 5 单、订单记录只有 1 单」？

- **持仓列表**：数据来自**币安 API**（`get_open_positions()`），是交易所上真实存在的持仓，与 DB 无关。
- **订单记录**：数据来自**本系统数据库 `trades` 表**，只有「本系统开仓并成功写库」或「同步/补建」时才会有一条记录。

因此**不是**「持仓存在就一定先在 DB 有了」——恰恰相反：币安上可以有 5 个持仓（本系统开的、手动在 App 开的、其它工具开的，或本系统开了但当时写库失败），而 DB 里只有我们成功写入或补建的那几条，所以订单记录会少于或等于持仓数。要对齐做法：使用**持仓同步**（见下文「补建」）或**同步币安订单**，把「币安有仓、DB 无记录」的持仓补建到 DB，订单记录就会和持仓一致。

---

## 一、数据模型（与币安对应的关键字段）

| 字段 | 含义 | 与币安对应 |
|------|------|------------|
| `entry_order_id` | 币安开仓订单号 (orderId) | 开仓订单唯一标识，用于与币安「开仓订单」一一对应 |
| `exit_order_id` | 币安平仓订单号 (orderId) | 平仓订单唯一标识，用于与币安「平仓订单」一一对应 |
| `client_order_id` | 币安 clientOrderId | 系统下单时用 `SYSTEM_ORDER_ID_PREFIX_时间戳_随机`，便于区分本系统单 |
| `entry_time` | 开仓时间 | 建议从币安订单/成交取真实时间，便于统计与早止盈 |
| `status` | open / closed / pending | open=持仓中，closed=已平仓，pending=已下单待成交 |

**「可对账」定义**（与币安一致的口径）：

- 有 `entry_order_id`（非空且不为 0）
- 若 `status = closed`，还须有 `exit_order_id`（非空且不为 0）

接口默认 `reconciled_only=true` 时，只返回/统计满足上述条件的记录。

---

## 二、订单记录是在哪里写的？

### 1. 本系统开仓（策略/限价开仓）

**位置**：`trading_system/position_manager.py`（开仓成功、成交确认后）

- **流程**：下单 → 等待成交（REST 轮询或 WS）→ 取到 `orderId`、成交价、数量 → 写库。
- **写库方式**：
  - 若之前已建 `status=pending`（限价先挂单）：用 `Trade.update_pending_to_filled(client_order_id, account_id, entry_order_id, entry_price, quantity)` 或 `Trade.update_pending_by_entry_order_id(symbol, account_id, entry_order_id, entry_price, quantity)` 完善为 open，并写入 `entry_order_id`。
  - 否则直接 `Trade.create(..., entry_order_id=orderId, client_order_id=..., status='open', entry_time=...)`。
- **entry_order_id 来源**：REST 下单返回的 `order.get("orderId")`，或成交确认时从订单查询得到。
- **entry_time**：支持传入；不传则用当前北京时间；补建/手动同步路径会从币安订单或成交取真实开仓时间。

**结论**：本系统开的仓，在正常落库且无异常时，**开仓时就会带上 entry_order_id**，与币安开仓订单一一对应。

---

### 2. User Data Stream（WS 推送）补全订单号

**位置**：`trading_system/user_data_stream.py`

- **开仓成交 (ORDER_TRADE_UPDATE, X=FILLED, 非 reduceOnly)**  
  - 若有 `clientOrderId`：`Trade.update_pending_to_filled(client_order_id, account_id, order_id, price, quantity)`，把对应 pending 记录完善为 open 并写入 `entry_order_id`。  
  - 若无 clientOrderId：`Trade.update_pending_by_entry_order_id(symbol, account_id, order_id, price, quantity)`，用 symbol+account 下「唯一一条 pending 且无 entry_order_id」的记录做兜底补全。
- **平仓成交 (ORDER_TRADE_UPDATE, X=FILLED, reduceOnly)**  
  - `Trade.set_exit_order_id_for_open_trade(symbol, account_id, order_id, entry_order_id_hint)`：给该 symbol 下当前 open 且无 exit_order_id 的记录写入平仓订单号（有 entry_order_id 时优先按 entry_order_id 精确匹配）。
- **条件单触发 (ALGO_UPDATE, X=TRIGGERED/FINISHED)**  
  - `ai` = 触发后的普通订单 orderId，同样调用 `Trade.set_exit_order_id_for_open_trade(symbol, account_id, ai, ...)` 回写 `exit_order_id`。

**结论**：WS 负责在「开仓/平仓成交或条件单触发」时，把币安订单号回写到 DB，保证与币安一致。

---

### 3. 本系统平仓（止损/止盈/手动平仓）

**位置**：`trading_system/position_manager.py`（close_position 成功后）

- **流程**：市价平仓或条件单触发 → 拿到平仓订单号 → `Trade.update_exit(trade_id, exit_price, exit_reason, pnl, pnl_percent, exit_order_id=..., exit_time_ts=..., commission=..., realized_pnl=...)`。
- **exit_order_id 来源**：平仓接口返回的 `order.get('orderId')`，或从 `get_recent_trades` 按订单号汇总。
- **commission / realized_pnl**：从币安成交 `get_recent_trades` 按该订单号汇总，写入 DB，统计与币安一致。

**结论**：本系统平的仓，平仓路径会写入 `exit_order_id` 及手续费/实际盈亏，与币安平仓订单一致。

---

### 4. 补建「币安有仓、DB 无记录」（状态同步）

**位置**：`trading_system/position_manager.py`（sync_positions_with_binance）

- **何时发生**：定时同步或调用 `POST /api/account/positions/sync` 时，发现某 symbol 在币安有持仓，但 DB 没有对应 open 记录。
- **补建逻辑**（简要）：
  - 若配置了 `SYSTEM_ORDER_ID_PREFIX`：会查该 symbol 的开仓订单（如 get_all_orders / get_recent_trades），取**同方向、时间合理**的订单作为 `entry_order_id`；若查到 `clientOrderId` 且**以系统前缀开头**则视为系统单并补建；若**明确不以系统前缀开头**则视为手动单，按配置可跳过或仍补建（如开启 SYNC_CREATE_MANUAL_ENTRY_RECORD）。
  - 补建时调用 `Trade.create(..., entry_reason='sync_recovered' 或 'manual_entry', entry_order_id=..., entry_time=...)`，**尽量从币安订单/成交取真实 entry_order_id 和 entry_time**。
- **entry_order_id 来源**：`futures_get_order(entry_order_id)` 或同方向成交中最早一笔的 orderId；若拿不到则可能为空（后续靠「同步币安订单」补全）。

**结论**：补建记录会尽量带上 `entry_order_id` 和真实开仓时间；若当时拿不到，需依赖「同步币安订单」补全。

---

### 5. 同步币安订单（后端接口）

**位置**：`backend/api/routes/trades.py`，`POST /api/trades/sync-binance`

- **作用**：按时间范围拉取币安历史订单，与 DB 对齐：**补全缺失的 entry_order_id / exit_order_id**，必要时**新建 DB 记录**。
- **开仓订单**：
  - 若 `Trade.get_by_entry_order_id(order_id)` 已存在 → 跳过。
  - 否则在该 symbol 下找「时间窗口内且无 entry_order_id」的记录，按价格/数量匹配（允许少量误差）→ 匹配到则 `Trade.update_entry_order_id(trade_id, order_id)` 补全；匹配不到则若开启「全量同步」可 `Trade.create(..., entry_order_id=order_id, entry_reason='sync_from_binance', status='open')` 新建。
- **平仓订单 (reduceOnly)**：
  - 若 `Trade.get_by_exit_order_id(order_id)` 已存在 → 视情况跳过。
  - 否则找该 symbol 的 open 或「已 closed 但 exit_order_id 为空」的记录，匹配后 `Trade.update_exit(..., exit_order_id=order_id, ...)`，并可从成交拉取 commission/realized_pnl 写入。

**结论**：**与币安订单一致的关键一步**。DB 里已有记录但缺订单号时，用此接口可把 entry_order_id / exit_order_id 补全；若 DB 完全没有记录且开了全量同步，会按币安开仓订单新建记录，保证「订单记录」与币安可对账。

---

## 三、如何与币安订单一致？（操作与口径）

### 1. 保证「有订单号」

- **本系统开/平仓**：正常落库 + WS 回写，会自动有 `entry_order_id` / `exit_order_id`。
- **补建/手动同步**：补建时已尽量从币安取 `entry_order_id` 和 `entry_time`；若仍缺订单号（如历史旧数据），可：
  - 在前端「交易记录」页使用 **「同步订单」**（即 `POST /api/trades/sync-binance`），选择时间范围（如今天/7 天），必要时勾选「全量同步」；
  - 同步后会补全「开仓/平仓订单号」，并可能新建缺失记录；同步结果会提示补全了多少个 entry_order_id / exit_order_id。

### 2. 只用「可对账」口径看订单与统计

- 前端「交易记录」默认 **勾选「仅可对账（与币安一致）」**（`reconciled_only=true`）。
- 接口行为：
  - `GET /api/trades?reconciled_only=true`：只返回「有 entry_order_id，且若已平仓则有 exit_order_id」的记录。
  - `GET /api/trades/stats?reconciled_only=true`：只统计上述记录，日盈亏、胜率、总盈亏与币安一致。
- 这样**订单记录与统计都只基于「能和币安一一对上」的数据**，避免无订单号或脏数据干扰。

### 3. 主动校验是否一致

- **接口**：`GET /api/trades/verify-binance?days=30&limit=100`（需登录与账号）。
- **作用**：对「可对账」记录逐条用币安 `futures_get_order` 校验开仓/平仓订单是否存在、symbol/side/数量是否一致；返回一致/缺失/不一致数量及明细。
- 建议定期或在对账有疑问时调用，确认 DB 与币安一致。

---

## 四、流程简图（何时有 entry_order_id / exit_order_id）

```
本系统开仓
  → 下单得到 orderId
  → Trade.create(..., entry_order_id=orderId) 或 update_pending_to_filled / update_pending_by_entry_order_id
  → [WS] ORDER_TRADE_UPDATE FILLED 可再次补全 entry_order_id（若之前未写入）

本系统平仓
  → 平仓得到 orderId
  → Trade.update_exit(..., exit_order_id=orderId)
  → [WS] ORDER_TRADE_UPDATE FILLED / ALGO_UPDATE 可再次补全 exit_order_id（若之前未写入）

币安有仓、DB 无（补建）
  → Trade.create(..., entry_order_id=从币安订单/成交取, entry_time=从币安取)
  → 若当时取不到 entry_order_id，后续靠「同步币安订单」补全

同步币安订单 (POST /api/trades/sync-binance)
  → 开仓订单：匹配 DB 无 entry_order_id 的记录 → update_entry_order_id；或新建 Trade(..., entry_order_id=orderId)
  → 平仓订单：匹配 open 或 closed 无 exit_order_id → update_exit(..., exit_order_id=orderId)
  → 结果：DB 与币安在「谁开的、谁平的」上一致
```

---

## 五、小结

| 问题 | 答案 |
|------|------|
| 订单是怎么记录的？ | 开仓：`Trade.create` 或 update pending → open，并写 `entry_order_id`；平仓：`Trade.update_exit` 写 `exit_order_id`。补建与同步也会创建/更新记录。 |
| entry_order_id 从哪来？ | 本系统开仓：下单/成交返回的 orderId；补建：币安 get_all_orders / get_recent_trades / futures_get_order；缺失时：同步币安订单接口按时间/价格匹配补全。 |
| exit_order_id 从哪来？ | 本系统平仓：平仓接口返回的 orderId；WS：ORDER_TRADE_UPDATE / ALGO_UPDATE 回写；同步币安订单：按 reduceOnly 订单匹配并 update_exit。 |
| 如何与币安一致？ | 1）保证写库路径都尽量写入订单号；2）缺订单号时用「同步订单」补全；3）前端与统计只用「仅可对账」口径（reconciled_only=true）；4）需要时用 verify-binance 接口校验。 |
