# 持仓、订单记录、统计与币安一致性说明

在引入「订单号前后一致」处理（`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 无记录。 |
| **补建时拿不到订单号** | 已优化：配置了 **SYSTEM_ORDER_ID_PREFIX** 时，补建**优先**用 `futures_get_all_orders(symbol, 最近24h)` 按开仓订单（reduceOnly=false、FILLED、同向）且 **clientOrderId 以系统前缀开头** 取本系统单，再按成交价/数量匹配取 `orderId` 与 `clientOrderId`，不依赖「最近 30 条成交」。若无前缀或该接口无结果，再用 `get_recent_trades(symbol, limit=100)` 兜底，空时重试一次；若配置了前缀，会从同向成交里逐条查订单的 clientOrderId，只采用前缀匹配的订单号，避免把手动单绑到补建记录。 |

**建议**：保证交易进程与 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 或脚本统计「成交未落库」笔数，便于与币安对账。
