Files
Hermes-Skills/okx-auto-position/references/v4.5.32-2026-07-08-close-dedup-bug-and-title-position-contradiction.md

4.8 KiB

v4.5.32 (2026-07-08) 平仓 dedup 误跳 bug 实战修复 + 标题/仓位矛盾第 N 次复发

事件

用户指令收到"🚨 已平仓提醒"信号时,第一次提示 agent "这个没推平仓信号",追问"要是我没发现,就一直不推了吗?重试机制呢"。

根因 (commit 8c03e22 修复前)

process_signal.py 的主流程:

parse → dedup → classify → execute

is_duplicate() 的 2 分钟窗口去重 早于 classify_signal(),导致:

  • 熬鹰发出 ETH 减仓信号 (raw_text = "📉 ETH 减仓 ... 4500张")
  • 紧接着 ETH 平仓信号 (raw_text = "🚨 ETH 平仓 ... 850张, +11.23%")
  • 平仓信号因 2 分钟窗口内同币种同交易员 → dedup 命中 → 跳过
  • 新规则"平仓信号也跟单"从未触发
  • 用户持仓 ETH short 1.85张 长期不平,浮亏扩大到 -$6.58

修复 (commit 8c03e22, v4.5.4)

classify_signal 提到 is_duplicate 之前:

signal_type = classify_signal(fields)
if signal_type == 'close':
    # 独立通道:只看 raw_text hash 唯一性
    msg_hash = hashlib.md5(text.encode()).hexdigest()
    if dedup_conn.execute("SELECT 1 FROM processed WHERE msg_hash = ?", (msg_hash,)).fetchone():
        return "⏭️ 平仓信号完全重复跳过"
    # close_position_raw + 推送
    return f"✅ 平仓处理: {close_result['action']} | {symbol} {side}"
# 其他信号(open/reduce) 才走 2 分钟窗口去重

关键点:close 信号天然 unique (raw_text 内含收益额 + 标记价),不需要 2 分钟窗口。

实战验证

  • 修后第一条 ETH 平仓信号 → 平仓处理: closed | ETH short
  • ETH short 1.85张 真实平仓 → USDT 回到 $106.34
  • 后续 GRAM short 平仓 → 平仓处理: closed | GRAM short

教训

  1. dedup 永远应该晚于 classify——业务信号(close/open)的优先级 > 文本重复
  2. close 信号天然 unique——不同 close 信号的最终收益/标记价不同,无需 dedup
  3. 用户的"重试机制呢"是金句——agent 不应该静默吞信号,必须告知执行状态

标题 vs 仓位矛盾 第 N 次复发

本次会话麻吉大哥 ETH 多次出现 "标题: 加仓, 实际仓位: 减小":

  • 6000 → 4500张 标题写"加仓"(实际 -25%)
  • 6360 → 6330张 标题写"加仓"(实际 -0.5%)

process_signal 字面信"加仓",推错的加仓建议 0.32张,但用户已有 ETH short 同币种反向持仓,实际应该是减仓。

修复方向 (未部署)

应在 classify_signal 中对比仓位变化:

def classify_signal(fields, prev_position=None):
    text = fields.get('_raw', '')
    pnl = float(fields.get('pnl', '0').replace('+', ''))
    current_size = float(fields.get('size', '0').replace(',', ''))
    
    if prev_position:
        delta = (current_size - prev_position) / prev_position
        # 仓位减小但标题写"加仓"
        if delta < -0.02 and '加仓' in text:
            return 'reduce'  # 强制覆盖,不信标题
    # ... 原 classify_signal 逻辑

需要 signal_tracker.py 提供 prev_position 数据——这是 v4.5.33 的 TODO。

平仓信号 vs 同方向持仓 联动

v4.5.3 起的规则:

  1. 收到平仓信号 → 查自己是否有 同币种同方向 持仓
  2. 同方向 → 市价全平(close_position_raw)
  3. 反方向 → 不动(避免方向错位)
  4. 无持仓 → 只推送通知

实战确认正确:

  • 熬鹰 ETH 平仓 short → 你 ETH short 1.85张 → 已平
  • 熬鹰 SPCX 平仓 short → 你无 SPCX short → 只通知

raw REST close_position 函数 (scripts/process_signal.py 关键代码)

def close_position_raw(symbol_usdt, signal_side_en):
    """查 OKX 持仓 → 同方向全平(市价 reduceOnly)"""
    import requests, hmac, hashlib, base64, time, json
    creds = open('/home/openclaw/.bashrc').read()
    api_key = sec = pw = None
    for line in creds.split('\n'):
        if line.startswith('export OKX_API_KEY='): api_key = ...
        elif line.startswith('export OKX_SECRET='): sec = ...
        elif line.startswith('export OKX_PASSPHRASE='): pw = ...
    
    ts = time.strftime('%Y-%m-%dT%H:%M:%S.000Z', time.gmtime())
    sig_msg = ts + 'GET' + '/api/v5/account/positions'
    sig = base64.b64encode(hmac.new(sec.encode(), sig_msg.encode(), hashlib.sha256).digest()).decode()
    headers = {'OK-ACCESS-KEY': api_key, 'OK-ACCESS-SIGN': sig, 'OK-ACCESS-TIMESTAMP': ts, 'OK-ACCESS-PASSPHRASE': pw}
    px = {'https': 'http://127.0.0.1:7890'}
    pos = requests.get('https://www.okx.com/api/v5/account/positions', params={'instId': f'{symbol_usdt}-USDT-SWAP'}, headers=headers, proxies=px, timeout=15).json()
    
    for p in pos.get('data', []):
        size = float(p.get('pos', 0))
        if size == 0: continue
        actual_side = 'short' if size < 0 else 'long'
        if actual_side != signal_side_en: continue  # 方向错位跳过
        # 同方向 → 市价全平
        ...

(完整代码见 scripts/process_signal.py)