150 lines
5.4 KiB
Markdown
150 lines
5.4 KiB
Markdown
# 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 自报数据。 |