4.9 KiB
4.9 KiB
平仓推送: 真实 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 个:
state='2'(数字) → OKX 返回 code 51600 "Status not found"; 必须用state='filled'字符串- 缺
instType=SWAP→ code 50014 "Parameter instType can not be empty" 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
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 函数
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 加推送内容
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" |