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

4.9 KiB
Raw Permalink Blame History

平仓推送: 真实 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

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"