5.4 KiB
5.4 KiB
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)
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']。
✅ 正确实现
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 返回结构
{
'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 数据。
🎯 行动建议
短期 (现在能做的)
- 用户每次看 advisor 推送 PNL 都打折 —— 实际可能 -$50 到 +$5 (手续费吃掉浮盈)
- 不要用浮盈做决策 —— 等 fillPnl 真值,可能 -$30
中期 (改 okx_position_advisor.py)
- 修
close_position()line 653:pnl = float(pos.get('unrealizedPnl', 0))→pnl = float(fill_info.get('fillPnl', 0)) - 加
fee字段到 results - 加
net_pnl=fillPnl - abs(fee) format_close_result()加 fee + net_pnl 显示
长期
- advisor 默认走
fillPnl - fee,不要用 unrealizedPnl - 实盘前加手续费计算提示(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 返回成功无 fillreferences/v4.5.36-2026-07-08-reply-vs-qq-side-separation.md— 回复 vs QQ 推送分离
共同主题: OKX 操作反馈链路不完整,任何"成功"反馈都需要 raw REST 反查 + fillPnl 真值校验,不能信 advisor 自报数据。