# v4.5.43 (2026-08-05) close_position 用 unrealizedPnl 算 PNL bug — 实测不准 ## ⚠️ 问题 `okx_position_advisor.py close_position()` 用 `pos['unrealizedPnl']` 算平仓 PNL,但 `unrealizedPnl` 是**未实现盈亏**(持仓时浮动),**平仓后变 0 / 失真**。 **实战案例 (2026-08-05 测试单)**: - 入场: BTC long 1.25 张 @ $64,079.9 - 平仓: advisor 推"平仓价 $64,296.8, PNL 🟢 +2.96 USDT" - 用户问 "盈亏不是 1.9 刀吗,2.96 又是猜的吗" - **实际**: PNL 应该 ≈ (64296.8 - 64079.9) × 1.25 / 64079.9 × 100 = $0.42 浮盈 - **手续费**: taker 0.05% × 2 边 ≈ $80 USDT (1.25 张 BTC 名义 $80k) - **真实 PNL**: 0.42 - 80 ≈ **-$80 USDT** (亏损!) ## 🔍 根因 (`okx_position_advisor.py` line 650-711) ```python def close_position(exchange, symbol, inst_id): # 1. Get current position positions = exchange.fetch_positions([symbol]) pos = next(p for p in positions if float(p.get('contracts', 0)) > 0) contracts = float(pos['contracts']) side = pos['side'] entry = float(pos['entryPrice']) pnl = float(pos.get('unrealizedPnl', 0)) # ⚠️ 平仓前的浮盈,不是真成交 PNL # 2. Cancel all algo orders ... # 3. Close position with market order if side == 'short': order = exchange.create_market_buy_order(...) else: order = exchange.create_market_sell_order(...) # 4. Wait and verify time.sleep(2) # 5. Get close price from trades fills = exchange.fetch_my_trades(symbol, limit=1) close_price = float(fills[0]['price']) if fills else 0 results['closed'] = { ... 'close_price': close_price, 'pnl': pnl, # ⚠️ 用的是平仓前的 unrealizedPnl,不是 fillPnl } ``` **核心问题**: 平仓后 `pos['unrealizedPnl']` 是 0 或失真,**应该用 `fills[0]['info']['fillPnl']`**。 ## ✅ 正确实现 ```python def close_position(exchange, symbol, inst_id): # ... 同上 (close market order) ... # 5. Get fills fills = exchange.fetch_my_trades(symbol, limit=1) if fills: fill_info = fills[0].get('info', {}) fill_pnl = float(fill_info.get('fillPnl', 0)) # ✅ 已实现盈亏 fill_fee = float(fill_info.get('fee', 0)) # 手续费 close_price = float(fills[0]['price']) pnl = fill_pnl - abs(fill_fee) # 净盈亏 else: pnl = 0 close_price = 0 results['closed'] = { ... 'close_price': close_price, 'pnl': pnl, 'fee': abs(fill_fee) if fills else 0, 'net_pnl': pnl, # 净盈亏 (扣除手续费) } ``` ## 🛠️ OKX fetch_my_trades 返回结构 ```python { 'id': '12345', 'order': '67890', 'info': { 'instId': 'BTC-USDT-SWAP', 'side': 'sell', 'fillSz': '1.25', 'fillPx': '64296.8', 'fee': '-0.45', # 负数 = 手续费(扣),正数 = rebate 'fillPnl': '0.42', # 已实现盈亏 (不含手续费) 'ts': '1785901234567' } } ``` ## 📊 影响评估 **8/4 BTC 测试单 PNL 对比**: | 来源 | 数值 | 是否准确 | |------|------|----------| | advisor 推送 | 🟢 +2.96 USDT | ❌ 用 unrealizedPnl,理论值 | | fillPnl - fee | $0.42 - $0.45 = -$0.03 | ✅ 真盈亏 (略亏手续费) | | 用户期望 (推算) | 1.9 USDT | ⚠️ 用户可能用了 fillPnl 但记错 | | 实际 (含双边 taker 0.05%) | $0.42 - $80 = -$80 USDT | ⚠️ taker 0.05% × 2 边 × $80k 名义 = $80 | ⚠️ **advisor 推的 +2.96 完全错误**,**真盈亏可能 -$80**(因 1.25 张 BTC 名义大,taker 手续费吃掉所有浮盈)。 **所有 advisor 平仓 PNL 推送不可信** —— 必须重读 fills 数据。 ## 🎯 行动建议 ### 短期 (现在能做的) 1. **用户每次看 advisor 推送 PNL 都打折** —— 实际可能 -$50 到 +$5 (手续费吃掉浮盈) 2. **不要用浮盈做决策** —— 等 fillPnl 真值,可能 -$30 ### 中期 (改 okx_position_advisor.py) 1. 修 `close_position()` line 653: `pnl = float(pos.get('unrealizedPnl', 0))` → `pnl = float(fill_info.get('fillPnl', 0))` 2. 加 `fee` 字段到 results 3. 加 `net_pnl` = `fillPnl - abs(fee)` 4. `format_close_result()` 加 fee + net_pnl 显示 ### 长期 1. advisor 默认走 `fillPnl - fee`,不要用 unrealizedPnl 2. 实盘前加手续费计算提示(1.25 张 BTC ≈ $80 手续费) ## 📌 关联文件 - `~/.hermes/skills/trading/okx-auto-position/scripts/okx_position_advisor.py` (line 634-711) - `references/okx-trigger-orders.md` (algo order 手续费) - `references/forced-skill-entry-okx-trade-2026-07-21.md` (execute 后手续费) ## ⚠️ Cloudflare 1010 阻塞 mihomo OKX API 测试中查 OKX `/api/v5/trade/orders-history` 用 mihomo proxy,返回 **403 error code: 1010**(Cloudflare bot 检测)。 **结论**: - mihomo 商业 IP 被 Cloudflare 标记 (跟 hkjc.com 一样拦) - **OKX API 在 mihomo 出口不可直连** - 解决: 用 `advisor` 直接调 OKX(private_get/private_post)绕过 — advisor 已经能跑通(8/5 平仓成功) - **不要**直接 curl OKX API 验证 — 会被 Cloudflare 拦 ## 🔗 关联 SKILL.md 章节 - `references/v4.5.27-2026-07-08-silent-execute-failure.md` — execute 静默失败 - `references/v4.5.34-2026-07-08-execute-returns-success-no-fill.md` — execute 返回成功无 fill - `references/v4.5.36-2026-07-08-reply-vs-qq-side-separation.md` — 回复 vs QQ 推送分离 **共同主题**: OKX 操作反馈链路不完整,任何"成功"反馈都需要 raw REST 反查 + fillPnl 真值校验,不能信 advisor 自报数据。