auto_trade_sys/docs/订单与统计一致性说明.md
薇薇安 aa073099f2 feat(position_manager): 添加落库失败独立日志功能以便于排查
在 `position_manager` 中新增日志记录功能,当币安订单已成交但未成功写入数据库时,将相关信息记录到独立的 `trade_db_failures.log` 文件中。此功能有助于排查与对账,确保交易记录的准确性和完整性。
2026-02-16 14:23:01 +08:00

173 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 持仓、订单记录、统计与币安一致性说明
在引入「订单号前后一致」处理(`entry_order_id` / `exit_order_id` / `SYSTEM_ORDER_ID_PREFIX`)后,以下内容的状态如下。
---
## 一、已能保证的部分
### 1. 持仓与币安一致
- **仪表板「当前持仓」**:数据来自 **币安实时持仓**`get_open_positions()`),与币安页面一致(仅受 `POSITION_MIN_NOTIONAL_USDT` 过滤影响)。
- **补建逻辑**:配置了 `SYSTEM_ORDER_ID_PREFIX` 时,**仅当**能明确查到开仓订单且 `clientOrderId` 非空且**不以**该前缀开头时视为手动单并跳过其余含历史单、查不到订单、cid 为空)一律补建并加入监控,避免漏掉系统单。
### 2. 订单记录与币安可对账
- **开仓**:交易系统**所有开仓单**(非 reduce_only在配置了 `SYSTEM_ORDER_ID_PREFIX` 时都会带 `newClientOrderId = 前缀_时间戳_随机`(如 `SYS_xxx`),成交后写入 DB 的 `entry_order_id``client_order_id`,与币安一致。
- **成交后落库**:开仓成交后 `position_manager` 会调用 `Trade.create(..., entry_order_id=..., client_order_id=...)`,订单记录与币安一一对应;若当时落库失败(超时/崩溃),后续同步会通过「补建」把缺失记录补上并纳入监控。
- **平仓**:平仓时保存 `exit_order_id``Trade.update_exit` 会做 `get_by_exit_order_id` 防重复。
- **同步**
- `POST /api/account/positions/sync` 与进程内定时同步:仅当开仓订单 `clientOrderId` **明确非**系统前缀时跳过补建,其余一律补建(含历史单、未带前缀的单),保证「币安有仓且为系统单」都会被监控。
- `POST /api/trades/sync-binance`:用 `get_by_exit_order_id(order_id)` 判断是否已同步,避免重复;开仓侧用 `get_by_entry_order_id(order_id)` 判断是否已存在。
- 因此:**每条 DB 记录都可与币安订单一一对应**(通过 `entry_order_id` / `exit_order_id`),订单记录与币安在「谁开的、谁平的」上一致。
### 3. 统计口径与去重
- **统计**:来自 `Trade.get_all(..., account_id)`,只统计该账号的 DB 记录。
- **净盈亏**`get_net_pnl(t)` 逻辑为:
- 若有 `realized_pnl`:用 `realized_pnl`,再若 `commission_asset == 'USDT'` 则减去 `commission`
- 否则用 `pnl`(按价格差算)。
- 胜率、总盈亏、盈亏比等均基于上述净盈亏汇总,**不会因为重复同步同一条平仓而重复计入**(由 `exit_order_id` 唯一性保证)。
---
## 二、仍依赖「谁在写」的部分
### 1. 手续费commission
| 场景 | 是否写入 commission | 说明 |
|--------------------|----------------------|------|
| 交易系统内平仓 | ✅ 是 | 从 `get_recent_trades``exit_order_id` 汇总 commission写入 `update_exit`。 |
| 仪表板「平仓」按钮 | ✅ 是 | 同上,取成交里的 commission/realizedPnl 写入。 |
| 持仓同步positions/sync | ✅ 是(已补全) | 更新 closed 前按 `exit_order_id` 拉取 `get_recent_trades`,汇总 commission/realizedPnl 并写入。 |
| 订单同步trades/sync-binance | ✅ 是(已补全) | 更新平仓记录前按订单号拉取成交,写入 commission/realized_pnl。 |
因此:**所有标记为已平仓的记录都会尽量带上手续费与实际盈亏**,统计与币安一致。
### 2. 实际盈亏realized_pnl
- **交易系统平仓、仪表板平仓**:从币安成交取 `realizedPnl` 并写入。
- **持仓同步、订单同步**:已补全逻辑,同样按 `exit_order_id` 拉取成交并写入 `realized_pnl`
- 统计中若存在 `realized_pnl` 会优先使用并再扣 USDT 手续费,否则用价格差 `pnl`
---
## 三、订单记录与统计「与币安一致」的根治方式
- **问题**DB 中可能存在币安上看不到的订单(补建脏数据、重复单、无订单号记录),导致系统统计与币安实际盈亏偏差(如系统显示盈利、币安实际亏损)。
- **根治**:接口支持 **仅可对账** 口径:
- **可对账** 定义:有 `entry_order_id`(开仓订单号),且若已平仓则还有 `exit_order_id`(平仓订单号),能与币安订单一一对应。
- **GET /api/trades**、**GET /api/trades/stats** 均支持查询参数 **`reconciled_only`**(默认 **true**
- `reconciled_only=true`:只返回/只统计上述可对账记录,**日盈亏与策略统计与币安一致**。
- `reconciled_only=false`:返回/统计全部 DB 记录(含无订单号的补建等),可能与币安不一致。
- 前端「交易记录」页默认勾选「仅可对账(与币安一致)」,可取消勾选查看全部记录。
## 四、小结:能否「直接保证」?
- **持仓与币安一致**:可以,当前实现已保证(实时持仓 + 按前缀补建)。
- **订单记录与币安可对账**:可以,`entry_order_id` / `exit_order_id` 与防重复逻辑已保证一一对应、不重复。
- **统计与币安一致**:使用 **仅可对账** 口径(`reconciled_only=true`,默认)时,日盈亏、胜率、总盈亏等只基于可对账记录,与币安一致;同一笔平仓只被记录一次,手续费与实际盈亏已在关仓路径补全。
---
## 五、下单路径与「意外订单」排查
### 1. 会向币安下单的入口(汇总)
| 类型 | 入口 | 触发条件 | 是否可能「意外」开仓 |
|------|------|----------|----------------------|
| **开仓** | `trading_system/position_manager.open_position` | 仅由策略在「通过风控 + 有信号」后调用;`risk_manager.should_trade` 会检查:持仓数 ≥ MAX_OPEN_POSITIONS 则 return False且检查该 symbol 是否已有持仓 | 否。持仓数/已有仓检查均基于币安 `get_open_positions()`,到上限或已有该 symbol 即跳过,不会下单。 |
| **开仓** | `backend/api/routes/account.py` 限价下单接口 | 用户在前端/API 主动发起的「手动限价开仓」 | 否,需显式调用。 |
| **平仓** | `position_manager.close_position` | 仅对 `active_positions` 中存在的 symbol 调用(止损/止盈/手动);`check_stop_loss_take_profit` 只遍历 `active_positions` | 否,只平我们监控的仓。 |
| **平仓** | backend 平仓/一键平仓 | 用户主动平仓 | 否,需显式调用。 |
| **止盈/止损单** | `position_manager._ensure_exchange_sltp_orders` | 仅在两处调用:(1) 开仓成功后为该 symbol 挂 SL/TP(2) 补建「仅币安有、DB 无」的持仓时,若开启 SYNC_CREATE_MANUAL_ENTRY_RECORD为补建记录补挂 SL/TP。挂前会先取消该 symbol 下同类型 Algo 单STOP_MARKET/TAKE_PROFIT_MARKET再下新单 | 不会产生「开仓」;可能覆盖用户在该 symbol 上已有的同类型保护单(属预期:补建后统一用系统计算的 SL/TP。 |
结论:**没有逻辑会在「不该开仓」时自动下开仓单**;止盈/止损挂单仅对「我们已纳入监控的持仓」补挂或覆盖,不会对「仅币安有、且未补建」的持仓主动挂单(因未进 `active_positions` 不会走到 `_ensure_exchange_sltp_orders`)。
### 2. 日志里「总是想下一些单」是否正常?
- **策略扫描日志**(如 `处理交易对: SIRENUSDT (UP 31.32%, 市场状态: ranging)`、`SIRENUSDT 4H趋势中性信号质量可能降低`、`SIRENUSDT 持仓数量已达上限16/16跳过开仓`
表示:每轮扫描会**评估**各交易对并可能尝试开仓,但 **`持仓数量已达上限16/16跳过开仓``risk_manager.should_trade` 已 return False****不会调用 `open_position`,也不会向币安下任何单**。这是正常、预期行为。
- **挂止盈/止损相关日志**(如「已挂币安保护单」「挂止盈单失败」等)
表示:仅对 **当前已在 `active_positions` 中且具备 SL/TP 价格的持仓** 在交易所侧挂或更新保护单;每次挂前会先取消同类型旧单再挂新单,不会重复堆积同一 symbol 的多组 SL/TP。
若希望减少「想下单」类日志的干扰,可将「跳过开仓」类日志级别调低(如改为 debug或仅在有实际下单时再打 info。
---
## 六、如何确认订单记录的准确性
策略执行情况分析依赖订单记录,必须保证「能对上的」记录与币安一致。可按下面步骤把握。
### 1. 日常使用:只用「可对账」口径
- 前端「交易记录」**保持勾选「仅可对账(与币安一致)」**(默认即勾选)。
- 看日盈亏、胜率、策略统计时,接口已默认 `reconciled_only=true`,只统计有 `entry_order_id` 且已平仓有 `exit_order_id` 的记录,与币安可一一对应。
- 分析策略执行、复盘盈亏时,以这批记录为准,避免被无订单号或补建脏数据干扰。
### 2. 主动校验:调用「对账校验」接口
- **接口**`GET /api/trades/verify-binance`(需登录并带 `X-Account-Id` 指定账号)。
- **参数**`days`(校验最近 N 天,默认 30、`limit`(最多校验条数,默认 100
- **作用**:对当前账号在时间范围内的「可对账」记录,逐条用币安 `futures_get_order` 校验:
- 开仓订单订单是否存在、symbol/side/数量是否与 DB 一致;
- 平仓订单(已平仓且存在 `exit_order_id`):同上,并确认为 reduceOnly。
- **返回**`summary`(一致/缺失/不一致数量)+ `details`(每条记录的 `entry_verified`/`exit_verified` 及说明)。便于快速发现「对不上」的记录。
**示例**(校验最近 30 天、最多 100 条):
```http
GET /api/trades/verify-binance?days=30&limit=100
X-Account-Id: 1
Authorization: Bearer <token>
```
`entry_missing``exit_missing` 不为 0说明 DB 里有的订单号在币安查不到(可能错写、错账号或历史清理);若 `entry_mismatch`/`exit_mismatch` 不为 0说明订单存在但 symbol/side/数量等与 DB 不一致,需排查写入或同步逻辑。
### 3. 发现对不上时怎么处理
- **仅做策略分析**:继续以「仅可对账」口径为准;有问题的记录因缺订单号或校验不通过,不会进入默认统计。
- **希望 DB 与币安尽量一致**:可先执行 `POST /api/trades/sync-binance`(按「最近 N 天」从币安拉订单并更新/补全平仓与 `exit_order_id`);再跑一次 `GET /api/trades/verify-binance` 看是否仍存在缺失或不一致,再针对单条记录排查或人工修正。
总结:**日常用「仅可对账」统计 + 定期或按需调 `verify-binance` 校验**,即可把订单记录的准确性把握住,策略执行分析所依赖的数据与币安一致。
---
## 七、持仓里「未知来源」订单是哪里来的?
持仓单里出现「未知」或「未知来源」,通常来自下面几类情况。
### 1. 仪表板「入场类型:未知」
- **数据来源**`GET /api/account/positions` 用**币安实时持仓**列表,再按 symbol 匹配本账号 DB 里 `status=open` 的记录,取 `entry_order_id`;若有订单号再调币安 `futures_get_order` 取订单类型 → 得到「限价/市价/未知」。
- **出现「未知」的两种情况**
- **币安有仓、DB 没有对应 open 记录**(尚未补建或未匹配上):没有 `entry_order_id`,自然没有订单类型 → 显示「未知」。
- **DB 有记录但 `entry_order_id` 为空**(例如补建时没查到开仓订单、或历史单无订单号):同样无法查订单类型 → 显示「未知」。
即:**凡是「币安有仓但系统没有可关联的开仓订单号」的持仓,在仪表板上都会显示为入场类型「未知」。**
### 2. 交易记录里「入场原因」= sync_recovered_unknown_origin来历不明
- **产生位置****trading_system 的持仓状态同步**`position_manager.sync_positions_with_binance`在「币安有仓、DB 无 open 记录」时会对该仓**补建**一条交易记录。
- **何时标成「未知来源」**`entry_reason = sync_recovered_unknown_origin`
- 配置了 **SYSTEM_ORDER_ID_PREFIX** 且能查到开仓订单时:若该订单的 **clientOrderId 不以系统前缀开头**(例如手动在币安下单、或其它系统下的单),则视为「来历不明」,仍会补建、挂止损止盈并纳入监控,但 `entry_reason` 记为 `sync_recovered_unknown_origin`
- **未配置** SYSTEM_ORDER_ID_PREFIX 且开启了 **SYNC_RECOVER_ONLY_WHEN_HAS_SLTP** 时:若该仓**没有**止损/止盈单,也记为「来历不明」并补建。
- **后端 POST /api/account/positions/sync**:逻辑不同——会**跳过**「明确非系统前缀」的仓(不补建),所以不会在「同步接口」里产生 unknown_origin只有**交易进程内**的定时同步会按上面规则补建并标记 `sync_recovered_unknown_origin`
因此:**持仓里「未知来源」的订单 = 币安上存在、但开仓不是本系统下的单(或无法用系统前缀/止损止盈判断为本系统单),由交易系统状态同步补建并标记为「来历不明」的那部分。** 系统仍会为它们挂止损止盈并监控,只是统计/分析时可通过「仅可对账」过滤掉无 `entry_order_id` 的这类记录。
### 3. 本系统开的仓为什么会出现「没正确落库」或「没记订单号」?
正常流程是:**开仓订单在币安成交 → 等待 FILLED → 用实际成交价/数量调用 `Trade.create(..., entry_order_id=..., client_order_id=...)`**。下面情况会导致「开了不落库」或补建时「没订单号」。
| 原因 | 说明 |
|------|------|
| **数据库不可用** | `position_manager` 启动时尝试导入 `backend.database.models`,若 `backend` 目录不存在、或导入失败缺依赖、Python 路径错误、DB 连不上等),会设 `DB_AVAILABLE = False`,成交后直接跳过保存并打日志「数据库不可用,跳过保存」。 |
| **Trade.create 抛异常** | 落库时发生异常(如连接超时、唯一约束冲突、磁盘满、字段错误等),代码会打错并 `return None`,不会重试,币安已有仓但 DB 无记录。 |
| **进程在落库前退出** | 订单已在币安成交,但在执行到 `Trade.create` 之前进程被 kill、崩溃或重启也会出现币安有仓、DB 无记录。 |
| **补建时拿不到订单号** | 同步补建时用 `get_recent_trades(symbol, limit=30)` 取最近同向成交并取第一条的 `orderId` 作为 `entry_order_id`。若接口失败、返回空、或该笔开仓已不在最近 30 条成交里,则 `entry_order_id` 为空,补建记录就会「没记订单号」。 |
**建议**:保证交易进程与 backend 同机或能访问同一 DB、且 `backend` 路径正确,避免 `DB_AVAILABLE = False`;若曾出现「开了不落库」,可查交易进程日志中的「保存交易记录到数据库失败」或「数据库不可用」;补建后仍无订单号的记录可用「仅可对账」过滤,不影响基于可对账记录的统计。
**落库失败独立日志**:当币安订单已成交但未写入 DB 时(如 `Trade.create` 抛异常、或 `DB_AVAILABLE=False`、或 `Trade` 未导入),会额外写入项目目录下 **`logs/trade_db_failures.log`**,每行一条 JSON`ts`、`symbol`、`entry_order_id`、`side`、`quantity`、`entry_price`、`account_id`、`reason`、以及失败时的 `error_type`/`error_message`)。不依赖 DB/Redis便于单独 grep 或脚本统计「成交未落库」笔数,便于与币安对账。