Skip to content

[Bug] matched_entry_price 全部写 0.0 + Ghost close 凭空利润 (paper trading, server_trailing_stop) #140

@frstyle189

Description

@frstyle189

Bug 概述

两个相关 bug 同时出现在 paper trading 模式下的 qd_strategy_trades 表:

  1. Bug 1: 任何 close 笔的 matched_entry_price 都 = 0.0(FIFO 配对信息丢失
  2. Bug 2: 偶发 ghost close(无对应开仓记录的 close 笔)→ 凭空利润虚增 PnL

Bug 1: matched_entry_price 全部写 0.0

症状: 不论 close_long / close_short / reduce_long / reduce_short / server_trailing_stopmatched_entry_price 字段都 = 0.0。entry_price 字段是对的(开仓价 / 平仓价),但 matched_entry_price 没传。

根因 (trading_executor.py 4 处):

位置 类型 缺什么
L3462 server_trailing_stop (long trigger) 返回 dict 缺 matched_entry_price 字段
L3478 server_trailing_stop (short trigger) 返回 dict 缺 matched_entry_price 字段
L4661 reduce_long caller _record_trade() 没传 matched_entry_price=old_entry
L4675 close_long caller _record_trade() 没传 matched_entry_price=old_entry
L4687 close_short caller _record_trade() 没传 matched_entry_price=old_entry
L4713 reduce_short caller _record_trade() 没传 matched_entry_price=old_entry

records.py 第 423 行 INSERT 用 kwargs.get('matched_entry_price', 0.0) —— caller 没传就用 0.0 默认值

修复 diff (4 处 + 1 处 None 保护):

# L3462 / L3478 (server_trailing_stop trigger)
return {
    'type': 'close_long',  # or 'close_short'
    'trigger_price': 0,
    'position_size': 0,
    'timestamp': candle_ts,
    'reason': 'server_trailing_stop',
    'matched_entry_price': entry_price,  # ← 新增 (L3462 用 entry, L3478 用 entry_price)
    'trailing_stop_price': stop_line,
    'highest_price': hp,  # or 'lowest_price': lp
}

# L4661 / L4675 / L4687 / L4713 (caller)
self._record_trade(
    strategy=strategy, symbol=symbol, side='long', qty=close_qty, price=fill_px,
    timestamp=fill_ts, profit=profit, status='closed', fee=fee, raw_data=raw,
    close_reason=_exit_reason,
    matched_entry_price=old_entry if old_entry else 0,  # ← 新增 (4 处都加)
)

验证: 策略 id=6 paper trading 跑 1 周, 20 笔 trades 全部 matched_entry_price 修复(10 open + 10 close),未来新 close 自动写入正确值。

Bug 2: Ghost close 凭空利润

症状: 策略 id=6 总 PnL 显示 +2611.48 USDT实际是 -5.56 USDT。差异 2617.04 来自 2 笔 ghost close:

id type price profit close_reason created_at
283 close_short 1761.94 +789.83 server_trailing_stop 2026-06-04 09:46:47
291 close_short 1735.62 +1840.23 server_trailing_stop 2026-06-05 04:30:23

根因:

  • 22 笔 trades = 10 笔 open_short (id=6, 16, 23, 42, 53, 69, 77, 100, 117, 187) + 12 笔 close_short(含 2 笔 ghost id=283/291)
  • ghost close 没有对应 open_short 记录
  • apply_fill_to_local_position 计算 profit 时 cur_entry 字段值 stale(= 0 或 上一笔 close 后的 entry)
  • 结果:凭空算出 +789.83 和 +1840.23,加到 PnL 总数上

修复:

  • 历史数据: 删 2 笔 ghost close(建议 SQL migration)
  • 代码层: server_trailing_stop trigger 需要校验 "有对应未平仓 open 才能触发",否则不写 ghost close 记录;或者 apply_fill_to_local_position 检测到 cur_entry 不存在时跳过 profit 计算

复现步骤

  1. git clone https://github.com/brokermr810/QuantDinger && cd QuantDinger && docker compose up -d --build
  2. 登录后建 ETH/USDT paper trading 策略 (IndicatorStrategy),带 trailingEnabled true
  3. 跑 1-2 周,触发 server_trailing_stop
  4. 查 db:
    SELECT id, type, price, profit, close_reason, matched_entry_price
    FROM qd_strategy_trades
    WHERE strategy_id = X ORDER BY id;
  5. 看到:matched_entry_price = 0.0 (Bug 1) + 偶发 ghost close with server_trailing_stop (Bug 2)

期望行为

  • matched_entry_price = 对应 open 的 price (FIFO 配对)
  • 总 PnL 基于真实 open/close 配对,ghost close 应被 filter 掉

部署方式

  • Docker Compose
  • 镜像: quantdinger-backend 重建时间 2026-06-05 16:32
  • 复现概率: 100% (matched_entry_price 全 0), 偶发 (ghost close)

临时修复 (在我们自己 db 已做)

-- 1. 删 ghost close (profit 先改 0 保留痕迹)
UPDATE qd_strategy_trades SET profit = 0 WHERE strategy_id = X AND id IN (283, 291);
DELETE FROM qd_strategy_trades WHERE strategy_id = X AND id IN (283, 291);

-- 2. 补 matched_entry_price (FIFO, 同 amount 配对)
UPDATE qd_strategy_trades c
SET matched_entry_price = o.price
FROM (
    SELECT c2.*, ROW_NUMBER() OVER (PARTITION BY c2.amount ORDER BY c2.created_at) AS c_seq
    FROM qd_strategy_trades c2
    WHERE c2.strategy_id = X AND c2.type = 'close_short'
) cc
JOIN (
    SELECT o2.*, ROW_NUMBER() OVER (PARTITION BY o2.amount ORDER BY o2.created_at) AS o_seq
    FROM qd_strategy_trades o2
    WHERE o2.strategy_id = X AND o2.type = 'open_short'
) o ON cc.amount = o.amount AND cc.c_seq = o.o_seq
WHERE c.id = cc.id;

-- 3. open 笔 matched_entry_price = own price
UPDATE qd_strategy_trades
SET matched_entry_price = price
WHERE strategy_id = X AND type = 'open_short' AND matched_entry_price = 0;

修复后:20 笔 trades 全部 matched_entry_price 正确,PnL 真实反映 -5.56 USDT。

关键日志

2026-06-05 04:30:23,280 - app.services.trading_executor - INFO - Strategy 6 triggered signals:
  [{'type': 'close_short', 'trigger_price': 0, 'position_size': 0.0, 'timestamp': ...}]
# 注意: trigger_price=0, position_size=0 (ghost trigger 没填真值)

建议 upstream 加日志告警:if trigger_price == 0 and position_size == 0: log.warning('Ghost signal detected')

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions