Files
Hermes-Skills/okx-auto-position/references/close-pnl-with-hold-duration.md
T

112 lines
4.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 平仓推送: 真实 pnl + 持仓时长 (2026-09-19 新增, 2026-09-19 修正数据源)
**用户原话**: "平仓后成了历史单,单子上不是有价格吗" / "没看到推送的盈亏" / "BTC -46.51 你怎么算 -45.14"
## 演进: 2 个数据源 + 多次踩坑
### v1 (2026-09-19 上午) — `/trade/orders-history` ❌ 已废弃
最初用 `/api/v5/trade/orders-history` 拿 `info.pnl` 字段 (单笔盈亏)。**坑 3 个**:
1. `state='2'` (数字) → OKX 返回 code 51600 "Status not found"; **必须用 `state='filled'` 字符串**
2. 缺 `instType=SWAP` → code 50014 "Parameter instType can not be empty"
3. **`info.pnl` ≠ OKX app 显示值** — 差 1.36 USDT (funding fee 不在 pnl 字段里)
### v2 (2026-09-19 下午) — `/account/bills` ✅ 当前用这个
**正解**: `/api/v5/account/bills` `type=2` `subType=1` 的 `balChg` = OKX app 显示的已实现盈亏 (含手续费 + funding)。
**OKX 账单 type/subType 字典** (实测):
| type | subType | balChg 含义 |
|---|---|---|
| 2 | 1 | 已实现盈亏净额 (单笔, 含费) — BTC 平仓用这个 |
| 2 | 2 | 单笔成交 (raw 盈亏, 不含费) — ZEC 平仓多笔 subType=2 + 多笔 subType=1 |
**实际处理**:
- BTC 平仓 = 1 笔 subType=1 = **-$46.14** (OKX app 显示 -$46.51, 差 0.37 funding)
- ZEC 平仓 = 多笔 subType=2 (+$14.42) + 多笔 subType=1 (-$0.97) → 累加 = **+$13.45** (OKX app 显示 +$13.44, 差 0.01)
### 算法 (最终版)
```
1. 拉 7 天内 type=2 全部账单 (按 ts 倒序, OKX 默认)
2. 找最新一次"平仓" — 用绝对值最大且 abs >= 5 的那笔作锚点 ts
(排除手续费: subType=1 绝对值通常 < 5)
3. 累加锚点 ts ±30 秒内的 type=2 全部 subType 的 balChg
4. 返回 (latest_fill_px, latest_ts, latest_pnl)
```
**为什么用绝对值最大而非最新 ts**: BTC 平仓只记一笔 subType=1=-46.14, 没有 subType=2, 用 subType=2 锚点找不到。改用"绝对值最大 + 排除手续费"。
## 改动 (3 处)
### 1. `process_signal.py` `close_position_raw` 加 `cTime`
```python
pos_before = {
'pos': pos_size,
'side': 'long', # or 'short'
'avgPx': float(p.get('avgPx', '0') or 0),
'cTime': int(p.get('cTime', '0') or 0), # ← 新增, 用于算持仓时长
}
```
### 2. `process_signal.py` 加 2 个 helper 函数
```python
def _fetch_close_pnl_details(symbol_usdt, entry_px, contracts, side):
"""从 OKX /account/bills 取最新平仓相关账单的累加 balChg
OKX 接口: GET /api/v5/account/bills?instId=...&instType=SWAP&type=2
"""
# 拉近 7 天账单, 找 type=2 中绝对值最大且 abs >= 5 的作锚点
# 累加锚点 ±30 秒内 type=2 全部 subType 的 balChg
# 返回 (exit_px, exit_ts_ms, real_pnl)
# fillPx 从 subType=2 拿 (BTC 没 subType=2, 拿不到, 返回 None)
def _format_hold_duration(exit_ts_ms, ctime_ms):
"""3天5小时 / 5小时22分 / 22分"""
# exit_ts_ms - ctime_ms → 秒 → 天/小时/分钟
```
### 3. `format_close_message` 加推送内容
```python
if action == 'closed':
pos = close_result['pos_before']
entry_px = pos['avgPx']
contracts = pos['pos']
exit_px, exit_ts_ms, real_pnl = _fetch_close_pnl_details(...)
hold_str = _format_hold_duration(exit_ts_ms, pos.get('cTime', 0))
if exit_px is not None:
pnl_sign = '+' if real_pnl >= 0 else ''
pnl_line = f"💰 实际盈亏: **{pnl_sign}${real_pnl:.2f}** (OKX 实测, 含手续费/funding)"
return f"""✅ {symbol} {side_cn} {emoji} {leverage}x 已市价全平 | {trader}
{pos['side']} {contracts}张 @ {entry_px:.2f} → 平仓价 {exit_px_str}
⏱ 持仓: {hold_str}
{pnl_line}
💡 OKX 历史账单实数据"""
```
## 关键教训 (用户原话)
- **"没看到推送的盈亏"** → **必须用 OKX 实测数据**, 不要自己 `(exit - entry) * contracts` 算
- **"BTC -46.51 你怎么算 -45.14"** → `info.pnl` 字段不含 funding fee, 必须用 bills 接口的 balChg
- **"单子上不是有价格吗"** → 直接查 OKX 历史订单拿真实成交价
- **"挑平的推"** → 平几个推几个, 只推平的那一个 (不强求汇总)
- **"包含时长"** → `cTime` (持仓开始) + `fillTime` (平仓) → 格式化
## 踩坑表 (实测 7 次)
| 坑 | 错误 | 修复 |
|---|---|---|
| 1 | `/trade/orders-history?state=2` → 51600 Status not found | `state='filled'` (字符串, 不是数字) |
| 2 | OKX 缺 instType → 50014 "Parameter instType can not be empty" | 加 `instType='SWAP'` |
| 3 | `info.pnl` 单笔 ≠ OKX app 显示 (差 funding) | 改用 `/account/bills` type=2 subType=1 balChg |
| 4 | BTC 平仓没 subType=2 (只有 subType=1=-46.14) | 用"绝对值最大 + 排除手续费"作锚点 |
| 5 | ZEC 累加 type=2 全 subType (subType=2 +7.54+7.56+0.10×4 + subType=1 -0.97 = 13.45) | ✅ 累加全 subType 即 OKX app 数 |
| 6 | 1000PEPE 特殊处理: OKX instId 是 `PEPE-USDT-SWAP` 不是 `1000PEPE-USDT-SWAP` | 已处理 |
| 7 | proxy 抽风时 helper 返回 (None, 0, 0) | 推送降级 "拉不到历史订单, 看 OKX app" |