在持仓同步功能中,增加了对系统订单前缀的默认处理,确保在无配置时使用默认前缀 "ats_"。同时,调整了日志记录逻辑,明确区分系统单与手动单的补建条件,提升了日志的可读性与准确性。这一改动旨在增强系统的可用性与用户友好性,确保持仓与数据库记录的一致性。
168 lines
11 KiB
Markdown
168 lines
11 KiB
Markdown
# 订单记录与币安对账流程
|
||
|
||
本文梳理:**订单是怎么记的**、**何时写入 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 接口校验。 |
|