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

118 lines
4.8 KiB
Markdown

# 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)