auto_trade_sys/docs/订单与统计一致性说明.md
薇薇安 225cb436d1 feat(trades): 添加可对账记录筛选功能以确保与币安一致
在获取交易记录和统计时,新增 `reconciled_only` 参数,默认值为 true,确保仅返回可对账的交易记录(包含 entry_order_id 和 exit_order_id)。此改动有助于提高统计的准确性,确保系统盈亏与币安一致。
2026-02-16 12:42:58 +08:00

8.2 KiB
Raw Blame History

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

在引入「订单号前后一致」处理(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_idclient_order_id,与币安一致。
  • 成交后落库:开仓成交后 position_manager 会调用 Trade.create(..., entry_order_id=..., client_order_id=...),订单记录与币安一一对应;若当时落库失败(超时/崩溃),后续同步会通过「补建」把缺失记录补上并纳入监控。
  • 平仓:平仓时保存 exit_order_idTrade.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_tradesexit_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/tradesGET /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。