# 持仓、订单记录、统计与币安一致性说明 在引入「订单号前后一致」处理(`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 ``` 若 `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 或脚本统计「成交未落库」笔数,便于与币安对账。