118 lines
4.8 KiB
Markdown
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) |