# 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` 之前: ```python 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` 中对比仓位变化: ```python 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 关键代码) ```python 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)