diff --git a/okx-auto-position/scripts/process_signal.py b/okx-auto-position/scripts/process_signal.py index 849604c..51b470f 100644 --- a/okx-auto-position/scripts/process_signal.py +++ b/okx-auto-position/scripts/process_signal.py @@ -20,6 +20,7 @@ import subprocess import sqlite3 import hashlib from pathlib import Path +from signal_queue import mark_failed # 2026-09-21: 平仓失败时主动 mark_failed from datetime import datetime, timedelta SKILL_DIR = Path.home() / ".hermes/skills/trading/okx-auto-position" @@ -30,7 +31,34 @@ DEDUP_DB = Path.home() / ".hermes/trading/signal_dedup.db" # Import signal tracker sys.path.insert(0, str(SKILL_DIR / "scripts")) -from signal_tracker import format_comparison, record_signal as _tracker_record, record_confirmed, format_trader_rating, get_last_signal, check_signal_lock +from signal_tracker import format_comparison, record_signal as _tracker_record, record_confirmed, format_trader_rating, get_last_signal, check_signal_lock, list_locks + + +def _locks_status_text(): + """所有锁状态 - 附加在每个推送尾部""" + try: + locks = list_locks() + except Exception: + return "" + if not locks: + return "" + lines = ["\n\n🔒 **当前所有锁**:"] + for l in locks: + # list_locks 返回 sqlite Row (索引访问) + symbol = l['symbol'] + trader = l['trader'] + side = l['side'] + locked_at = l['locked_at'] + # 算锁定时长 + from datetime import datetime + try: + ts = datetime.fromisoformat(str(locked_at).replace('Z', '')) + hours = (datetime.now() - ts).total_seconds() / 3600 + age = f"{hours:.1f}h" + except Exception: + age = "?" + lines.append(f" • {symbol} → **{trader}** {side} (锁 {age})") + return "\n".join(lines) # ─── 解析 ──────────────────────────────────────────────────────────────── @@ -55,15 +83,15 @@ def parse_signal(text): if m_first and m_first.group(1) not in FIELD_NAMES: fields['trader'] = m_first.group(1).strip() else: - # 实在找不到 trader 标签 — 默认 X聚合社区 (用户 TG 唯一信号源) - # 比 "unknown" 更有用, 用户能立刻知道源头 - fields['trader'] = 'X聚合社区' + # 2026-08-07: 实在找不到 trader 标签 — 标 '未知' (不要硬编码 'X聚合社区', 会误导) + fields['trader'] = '未知' # 字段映射 extractors = { 'symbol': r'【币种】\s*[::]?\s*(\S+)', 'side': r'【方向】\s*[::]?\s*(做多|做空)', - 'leverage':r'【杠杆】\s*[::]?\s*(\d+)', + # 2026-09-25: 新格式"币种标签内|永续|Nx"也要识别 (熬鹰币安实盘新版) + 'leverage':r'(?:【杠杆】\s*[::]?\s*(\d+)|(?:永续|全仓)\s*\|\s*全仓\s*\|\s*(\d+)\s*[xX]?)', 'size': r'【仓位(?:大小)?】\s*[::]?\s*([\d,.]+)', # 兼容 【仓位】和【仓位大小】 'unit': r'【仓位(?:大小)?】\s*[::]?\s*[\d,.]+\s+(BTC|USDT|ETH|SOL|DOGE|张|USD)\b', # 严格匹配已知单位 (必需) 'value': r'【仓位价值】\s*[::]?\s*\$?\s*([\d,.]+)', @@ -76,7 +104,9 @@ def parse_signal(text): for key, pattern in extractors.items(): m = re.search(pattern, text) if m: - fields[key] = m.group(1).replace(',', '') if key != 'unit' else m.group(1) + v = m.group(1) or m.group(2) or '' + if v: + fields[key] = v.replace(',', '') if key != 'unit' else v # 清理symbol if 'symbol' in fields: @@ -192,9 +222,11 @@ def record_signal(conn, text, symbol, trader): def run_advisor(symbol, side, leverage): """调advisor脚本获取正确数据""" + # advisor 内部 fetch_ticker 需要完整 symbol, 不是 base + full_symbol = f"{symbol}/USDT:USDT" cmd = [ 'python3', str(ADVISOR), - '--symbol', symbol, + '--symbol', full_symbol, '--side', side, '--leverage', str(leverage), '--json' @@ -219,24 +251,36 @@ def run_advisor(symbol, side, leverage): # ─── 分类 ──────────────────────────────────────────────────────────────── def classify_signal(fields): - """判断信号类型:加仓/新开仓/减仓/平仓""" + """判断信号类型:加仓/新开仓/减仓/平仓 + + 2026-09-26: 严格模式 — 文本必须显式包含动作关键词, 否则 unknown 跳过 + (避免汇总播报/实时更新被误判为减仓) + """ text = fields.get('_raw', '') - # 平仓信号 - if '平仓' in text or '止盈' in text or '止损' in text: + # 平仓信号 (含 "平仓" 或 "已平仓") + if '平仓' in text: return 'close' - # 减仓信号 - pnl = float(fields.get('pnl', '0').replace('+', '')) - if '减仓' in text or (pnl < 0 and '减' in text): - return 'reduce' - # 加仓信号 if '加仓' in text or '追仓' in text: return 'add' - # 默认为新开仓 - return 'open' + # 减仓信号 (必须显式含 "减仓" 关键词) + if '减仓' in text: + return 'reduce' + + # 新开仓信号 + if '新开仓' in text or '注意,大佬开仓了' in text: + return 'open' + + # 调杠杆信号 + if '修改了杠杆' in text or '调整了杠杆' in text or '调杠杆' in text: + return 'adjust_leverage' + + # 其他(汇总播报/实时更新/状态推送)统一跳过 + return 'unknown' + # ─── 格式化 ────────────────────────────────────────────────────────────── @@ -348,8 +392,8 @@ def format_message(fields, rec, signal_type): {trader_rating} -📐 性价比 -• 你的仓位: {rec['contracts']}张(保证金{_fmt(rec['margin'])} USDT) +📐 建议参数 +• 仓位: {rec['contracts']}张(保证金{_fmt(rec['margin'])} USDT) • SL: ${_fmt(rec['sl_price'])} → 预亏 -{_fmt(rec.get('sl_pnl', 0))} USDT (保证金-{rec.get('sl_pnl', 0)/max(rec.get('margin', 1), 0.01)*100:.0f}%) • TP: ${_fmt(rec['tp_price'])} → 预盈 +{_fmt(rec.get('tp_pnl', 0))} USDT (保证金+{rec.get('tp_pnl', 0)/max(rec.get('margin', 1), 0.01)*100:.0f}%) • 盈亏比: {rr}:1 {rating_emoji} {rating_text}""" @@ -473,11 +517,9 @@ def _okx_raw_request(method, path, params=None, body=None, timeout=15): def close_position_raw(symbol_usdt, signal_side): """用 raw REST 平掉同币种同方向持仓。 - Args: symbol_usdt: 'ETH' / 'SKHY' / 'BTC' (base currency) signal_side: 'long' / 'short' - Returns: dict: {action, pos_before, pos_after, pnl, message} """ @@ -501,6 +543,7 @@ def close_position_raw(symbol_usdt, signal_side): 'avgPx': float(p.get('avgPx', '0') or 0), 'upl': float(p.get('upl', '0') or 0), 'lever': p.get('lever', '?'), + 'cTime': int(p.get('cTime', '0') or 0), # 2026-09-19: 持仓时长 } break elif pos_size < 0: @@ -510,6 +553,7 @@ def close_position_raw(symbol_usdt, signal_side): 'avgPx': float(p.get('avgPx', '0') or 0), 'upl': float(p.get('upl', '0') or 0), 'lever': p.get('lever', '?'), + 'cTime': int(p.get('cTime', '0') or 0), } break @@ -566,6 +610,107 @@ def close_position_raw(symbol_usdt, signal_side): 'message': f"下单失败: {order_resp.get('msg', order_resp)}" } + +def adjust_leverage_raw(symbol_usdt, new_leverage): + """用 raw REST 直接设置杠杆 (跨币种通用) + Args: + symbol_usdt: 'ETH' / 'SKHY' / 'BTC' (base currency) + new_leverage: int, 目标杠杆倍数 + Returns: + dict: {ok: bool, error?: str, raw: dict} + """ + inst_id = f"{symbol_usdt}-USDT-SWAP" + if symbol_usdt == '1000PEPE': + inst_id = 'PEPE-USDT-SWAP' + + body = {'instId': inst_id, 'lever': str(new_leverage), 'mgnMode': 'cross'} + resp = _okx_raw_request('POST', '/api/v5/account/set-leverage', body=body) + if resp.get('code') == '0': + return {'ok': True, 'raw': resp} + return {'ok': False, 'error': resp.get('msg', 'unknown'), 'raw': resp} + +def _fetch_close_pnl_details(symbol_usdt, entry_px, contracts, side): + """从 OKX bills 取平仓相关的所有账单, 累加 = OKX app 显示值 + 返回: (exit_px, exit_ts_ms, real_pnl) + """ + try: + from datetime import datetime, timezone, timedelta + inst_id = f"{symbol_usdt}-USDT-SWAP" + if symbol_usdt == '1000PEPE': + inst_id = 'PEPE-USDT-SWAP' + + # 拉近 7 天的 ZEC 相关账单 (找 type=2 全部分类) + since_ms = int((datetime.now() - timedelta(days=7)).timestamp() * 1000) + resp = _okx_raw_request('GET', '/api/v5/account/bills', + {'instId': inst_id, 'instType': 'SWAP', + 'limit': '50', 'begin': str(since_ms)}) + if resp.get('code') != '0' or not resp.get('data'): + return (None, 0, 0) + + # 找最新一次"平仓"时间戳: 任何 type=2 (BTC=subType=1, ZEC=多笔 subType=2) + # 用"绝对值最大"的那笔作锚点 (平仓盈亏绝对值最大) + latest_ts = None + latest_pnl = 0.0 + latest_fill_px = None + max_abs = 0.0 + for b in resp['data']: + t = b.get('type') + if t != '2': + continue + st = b.get('subType') + bal = float(b.get('balChg', 0) or 0) + ts = int(b.get('ts', 0) or 0) + # 找最大绝对值那笔 (平仓主盈亏, 排除手续费) + # 排除 subType=1 的手续费 (绝对值通常 < 1) + abs_val = abs(bal) + if st == '1' and abs_val < 5.0: # subType=1 手续费一般 < 5 + continue + if abs_val > max_abs: + max_abs = abs_val + latest_ts = ts + latest_pnl = 0.0 + + # 累加同一时刻 (latest_ts 前后 30 秒) 的 type=2 全部 subType + if latest_ts: + for b in resp['data']: + ts = int(b.get('ts', 0) or 0) + if abs(ts - latest_ts) > 30000: + continue + t = b.get('type') + if t == '2': + bal = float(b.get('balChg', 0) or 0) + latest_pnl += bal + if b.get('fillPx') and latest_fill_px is None: + try: + latest_fill_px = float(b['fillPx']) + except (ValueError, TypeError): + pass + + return (latest_fill_px, latest_ts or 0, latest_pnl) + except Exception as e: + return (None, 0, 0) + + +def _format_hold_duration(exit_ts_ms, ctime_ms): + """格式化持仓时长: 3天5小时 / 5小时22分 / 22分10秒""" + from datetime import datetime, timezone + if ctime_ms <= 0: + return "未知" + start = datetime.fromtimestamp(ctime_ms / 1000, tz=timezone.utc) + end = datetime.fromtimestamp(exit_ts_ms / 1000, tz=timezone.utc) if exit_ts_ms > 0 else datetime.now(tz=timezone.utc) + delta = end - start + total_sec = int(delta.total_seconds()) + days = total_sec // 86400 + hours = (total_sec % 86400) // 3600 + minutes = (total_sec % 3600) // 60 + if days > 0: + return f"{days}天{hours}小时" + elif hours > 0: + return f"{hours}小时{minutes}分" + else: + return f"{minutes}分" + + def format_close_message(fields, close_result): """平仓信号处理结果推送 (精简版:v4.5.2 禁止过度分析)。""" symbol = fields.get('symbol', '?') @@ -578,11 +723,27 @@ def format_close_message(fields, close_result): if action == 'closed': pos = close_result['pos_before'] - # 2026-08-05: 不显示浮盈 (unrealizedPnl 不准, 用户从 OKX app 看真盈亏) + # 2026-09-19: 平仓推送盈亏 + 持仓时长 (从 OKX 历史订单拿真实 pnl) + entry_px = pos['avgPx'] + contracts = pos['pos'] + exit_px, exit_ts_ms, real_pnl = _fetch_close_pnl_details( + symbol_usdt, entry_px, contracts, pos['side']) + # 持仓时长 + ctime_ms = pos.get('cTime', 0) + hold_str = _format_hold_duration(exit_ts_ms, ctime_ms) + if exit_px is not None: + pnl_sign = '+' if real_pnl >= 0 else '' + pnl_line = f"💰 实际盈亏: **{pnl_sign}${real_pnl:.2f}** (OKX 实测, 含手续费/funding)" + exit_px_str = f"${exit_px:.2f}" + else: + pnl_line = "💰 实际盈亏: 拉不到历史订单, 看 OKX app" + exit_px_str = "?" return f"""✅ {symbol} {side_cn} {emoji} {leverage}x 已市价全平 | {trader} -{pos['side']} {pos['pos']}张 -💡 实际盈亏请到 OKX app 查看 (扣手续费 + funding)""" +{pos['side']} {contracts}张 @ {entry_px:.2f} → 平仓价 {exit_px_str} +⏱ 持仓: {hold_str} +{pnl_line} +💡 OKX 历史订单实数据""" elif action == 'skip': pos = close_result['pos_before'] return f"""⏭️ {symbol} 平仓信号跳过 | {trader} @@ -620,9 +781,11 @@ def _get_usdt_free(): def execute_order(symbol, side, leverage, rec): """执行开仓订单""" + # advisor 内部 fetch_ticker 需要完整 symbol + full_symbol = f"{symbol}/USDT:USDT" cmd = [ 'python3', str(ADVISOR), - '--symbol', symbol, + '--symbol', full_symbol, '--side', side, '--leverage', str(leverage), '--execute', '--json', @@ -676,7 +839,7 @@ def format_execution_result(fields, rec, exec_result): 📊 信号源: {trader} {size} {symbol}(价值${value}) -📐 性价比检查 +📐 跟单参数 • 盈亏比: {rr}:1 ✅ • 盈利额: +{profit:.2f} USDT ✅ • 手续费: {fee:.2f} USDT ({fee_pct:.1f}%) ✅ @@ -730,6 +893,51 @@ def process_signal(text): leverage = fields.get('leverage', '10') trader = fields.get('trader', '未知') + # 2026-08-18: 信号源缺失闸 — trader='未知' 时,开/加/减仓信号跳过不跟单 + # 平仓信号不阻断(平仓是兜底,即使来源不明也应执行 — 但目前 trader 字段只在 parse_signal 里标'未知', + # 解析不到 symbol/side 时早就在上面 return 了;这里分类后再判断更稳) + _sig_type_for_gate = classify_signal(fields) + + # 2026-08-21: trader 白名单闸 — 白名单放配置 whitelist.json (不要硬编码) + _WL_PATH = Path(__file__).parent / 'whitelist.json' + _TRADER_WHITELIST = {'熬鹰资本'} # fallback 默认值(防止配置文件损坏) + try: + import json as _json + _wl = _json.loads(_WL_PATH.read_text()) + if isinstance(_wl, dict) and isinstance(_wl.get('trader_whitelist'), list): + _TRADER_WHITELIST = set(_wl['trader_whitelist']) + except Exception: + pass # 配置文件失败时用 fallback + # 2026-09-09: 把 close 也加入白名单闸 (user 原意: 只跟熬鹰资本, 不应被"平仓兜底"绕过) + if _sig_type_for_gate in ('open', 'add', 'reduce', 'adjust_leverage', 'close') and trader not in _TRADER_WHITELIST: + return f"⏭️ 跳过: trader [{trader}] 不在白名单 | {symbol} {side}" + + # 2026-09-01: 实时仓位更新是状态推送,跳过不执行 + if _sig_type_for_gate == 'update_only': + return f"⏭️ 跳过: 实时仓位更新信号(非真实开/加/减/平仓) | {symbol} {side}" + + if trader == '未知' and _sig_type_for_gate in ('open', 'add', 'reduce'): + msg = f"""⏭️ **跳过跟单: 信号源未知** ⏭️ + +📊 币种: {symbol} {side} {leverage}x +🚫 信号文本里没有【交易员】/👉 跟单就选 / 任何 trader 标签 +❌ 不自动跟单,避免执行来路不明的开仓 + +💡 如果这条信号确实要跟, 请手动开仓 + 告诉我 trader 名字, 我会加到名单。 +""" + push_to_qq(msg) + # 仍然记录到 tracker — 留痕, 但 outcome 标 skipped_unknown_source + try: + _tracker_record(trader='未知', symbol=symbol, side=side, + leverage=int(leverage) if leverage else 10, + trader_size=float(fields.get('size', '0').replace(',', '')), + trader_entry=float(fields.get('entry', '0').replace(',', '')), + trader_pnl=float(fields.get('pnl', '0').replace(',', '')), + raw_text=text, outcome='skipped_unknown_source') + except Exception: + pass + return f"⏭️ 跳过: 信号源未知 | {symbol} {side}" + # 过期检查(30 分钟阈值,由 forwarder 注入的 ⏱信号时间 决定) if is_signal_stale(fields): dedup_conn = init_dedup_db() @@ -757,11 +965,61 @@ def process_signal(text): symbol_usdt = fields['symbol'] signal_side_en = fields.get('side_en', 'long') close_result = close_position_raw(symbol_usdt, signal_side_en) + # 2026-08-26: proxy抽风可能让 _okx_raw_request 返回 None, 加 null guard + if not close_result: + push_to_qq(f"❌ **平仓失败** (proxy 抽风?)\n" + f"币种: {symbol_usdt}\n" + f"请手动到 OKX 平仓,或等下次信号重试") + # 2026-09-21: mark_failed 让重试机制能识别 (不再标 done, 避免再次触发造成预期外亏损) + try: + import sqlite3 as _sq + _c = _sq.connect(os.path.expanduser('~/.hermes/trading/signal_queue.db')) + _row = _c.execute("SELECT id FROM queue WHERE raw_text=? ORDER BY id DESC LIMIT 1", (text,)).fetchone() + if _row: + mark_failed(_row[0], "平仓失败 proxy抽风") + _c.close() + except Exception: + pass + return f"❌ 平仓失败: _okx_raw_request 返回 None (proxy 抽风?) | {symbol_usdt}" + action = close_result['action'] + # 2026-08-06: 无持仓的平仓信号静默 (用户要求 - 不推冗余消息) + if action == 'none': + # 仍记录去重 + 释放锁 (但 QQ 不推) + record_signal(dedup_conn, text, symbol, trader) + dedup_conn.close() + from signal_tracker import release_lock + release_lock(symbol_usdt, trader, auto=True) + return f"⏭️ 平仓信号无持仓静默 | {symbol_usdt} {signal_side_en}" msg = format_close_message(fields, close_result) record_signal(dedup_conn, text, symbol, trader) dedup_conn.close() push_to_qq(msg) - return f"✅ 平仓处理: {close_result['action']} | {symbol_usdt} {signal_side_en}" + # 2026-08-06: 平仓后自动释放锁 (用户要求 - 平了肯定解锁) + from signal_tracker import release_lock + released, lock_msg = release_lock(symbol_usdt, trader, auto=True) + if released: + msg += f"\n\n🔓 锁已释放: {lock_msg}" + push_to_qq(msg) + return f"✅ 平仓处理: {close_result['action']} | {symbol_usdt} {signal_side_en} | {lock_msg}" + + # 调杠杆信号 (2026-08-26: 熬鹰调整杠杆要跟,不走advisor,直接 set-leverage) + if signal_type == 'adjust_leverage': + dedup_conn = init_dedup_db() + if is_duplicate(dedup_conn, text, symbol, trader): + dedup_conn.close() + return "⏭️ 重复信号,跳过" + record_signal(dedup_conn, text, symbol, trader) + dedup_conn.close() + symbol_usdt = fields['symbol'] + new_lev = int(fields.get('leverage') or '10') + lev_result = adjust_leverage_raw(symbol_usdt, new_lev) + if lev_result and lev_result.get('ok'): + msg = f"⚙️ **杠杆已调整** | {symbol_usdt} → {new_lev}x" + push_to_qq(msg) + return f"✅ 调杠杆: {symbol_usdt} → {new_lev}x" + else: + err = lev_result.get('error', '未知') + return f"❌ 调杠杆失败: {err} | {symbol_usdt}" # 其他信号(open/reduce): 才走2分钟窗口去重 dedup_conn = init_dedup_db() @@ -788,9 +1046,36 @@ def process_signal(text): side_en = 'long' if 'long' in str(side).lower() or side == '做多' else 'short' allowed, lock_msg = check_signal_lock(symbol, trader, side_en) if not allowed: - return f"🔒 {lock_msg}" - # 用户要求:所有信号自动执行,只有余额不足才跳过 - auto_execute = rec.get('contracts', 0) > 0 # 有可开张数=自动执行 + # 2026-08-06: 锁了也推 QQ (用户要求推送, 不然以为 bug) + # 看 lock_msg 提取被锁的 trader + 当前信号方向 + msg = f"""🔒 **跟单被锁** 🔒 + +📊 币种: {symbol} +📈 当前信号: {trader} {side} {leverage}x +🚫 锁信息: {lock_msg} + +💡 提示: 同币种只能锁一个 trader. 如要切跟, 用 `/unlock {symbol}` 解锁或告诉我换 trader. +""" + push_to_qq(msg) + return f"🔒 {lock_msg} | 已推 QQ" + # 2026-08-26: 去掉"性价比低"判断 — 用户要求所有熬鹰信号都跟,即使 advisor 报低 + contracts = rec.get('contracts', 0) + auto_execute = contracts > 0 # 性价比低也不再跳过 (用户要求 2026-08-26) + + # 2026-09-16: hard pre-trade check (CL bug 9/14 教训) + # 不可信 advisor rec['contracts'] — 复核单币种上限 + try: + margin_to_use = float(rec.get('margin') or 0) + single_coin_avail = float(rec.get('single_coin_avail') or 0) + except (TypeError, ValueError): + margin_to_use = 0 + single_coin_avail = 0 + if margin_to_use > single_coin_avail + 1.0: + push_to_qq(f"❌ 拒绝: {symbol} 单币超限 " + f"(本次 ${margin_to_use:.2f} > 上限 ${single_coin_avail:.2f})") + record_signal(dedup_conn, text, symbol, trader) + dedup_conn.close() + return f"⏭️ 单币超限跳过 | {symbol} | ${margin_to_use:.2f} > ${single_coin_avail:.2f}" if auto_execute and signal_type == 'open': # 性价比高 + 新开仓 → 自动执行 @@ -858,7 +1143,9 @@ def process_signal(text): our_pos = next((p for p in positions if symbol in p.get('symbol', '') and p.get('contracts', 0) != 0), None) if not our_pos: - msg = f"⏭️ 减仓信号: 你无 {symbol} 持仓, 跳过" + # 2026-09-19: 静默 (无持仓减仓信号不推 QQ, 避免刷屏) + msg = None + outcome = 'silent_no_pos' else: our_contracts = float(our_pos.get('contracts', 0)) # 算大佬减仓比例 @@ -869,8 +1156,12 @@ def process_signal(text): msg = f"⚠️ 减仓信号: 大佬前仓位未知, 跳过 (你有 {our_contracts} 张)" else: delta_pct = (trader_before - trader_after) / trader_before + # 2026-09-26: min 减仓比例过滤 (1%以下忽略, 避免蚂蚁式减仓刷屏) + _MIN_REDUCE_PCT = 0.01 if delta_pct <= 0: msg = f"⚠️ 减仓信号: 大佬实际是加仓 +{delta_pct*100:.1f}%, 跳过" + elif delta_pct < _MIN_REDUCE_PCT: + msg = f"⏭️ 减仓幅度过小 ({delta_pct*100:.2f}% < {_MIN_REDUCE_PCT*100}%), 跳过" else: # 按比例减 reduce_amt = our_contracts * delta_pct @@ -918,28 +1209,49 @@ def process_signal(text): record_signal(dedup_conn, text, symbol, trader) dedup_conn.close() - # 推送 + # 推送 (msg=None 表示静默跳过) + if msg is None: + return f"🔇 静默跳过 | {symbol} {side} {leverage}x" success = push_to_qq(msg) if success: - return f"✅ 已推送 | {symbol} {side} {leverage}x | {rec['contracts']}张 | 性价比{rec.get('cost_check', {}).get('rating_text', '?').replace('性价比', '')}" + return f"✅ 已推送 | {symbol} {side} {leverage}x | {rec['contracts']}张 | 评级:{rec.get('cost_check', {}).get('rating_text', '?').replace('性价比', '')}" else: return f"❌ 推送失败" def main(): - if len(sys.argv) > 1: - text = ' '.join(sys.argv[1:]) + import sys as _sys + if len(_sys.argv) > 1 and _sys.argv[1] == 'unlock': + # 2026-08-06: 解锁 CLI (用户要求手动解锁) + from signal_tracker import release_lock, list_locks + if len(_sys.argv) < 3: + locks = list_locks() + if not locks: + print("无锁") + else: + print("当前锁:") + for l in locks: + print(f" {l['symbol']} → {l['trader']} ({l['side']})") + return + symbol = _sys.argv[2] + trader = _sys.argv[3] if len(_sys.argv) > 3 else None + released, msg = release_lock(symbol, trader, auto=True) + print(msg) + push_to_qq(f"🔓 手动解锁: {msg}") + return + + if len(_sys.argv) > 1: + text = ' '.join(_sys.argv[1:]) else: - text = sys.stdin.read() + text = _sys.stdin.read() if not text.strip(): print("用法: python3 process_signal.py '信号文本'") - print("或: echo '信号文本' | python3 process_signal.py") + print(" python3 process_signal.py unlock BTC [trader]") return result = process_signal(text) print(result) # 2026-08-05: 余额不足/执行失败 → exit code != 0 → signal_queue.retry 标 failed - import sys as _sys if '跟单失败' in str(result): _sys.exit(2)