okx-auto-position: process_signal 四项修复 (平仓None bug/盈亏推送/静默/白名单防护)

1. close_position_raw 函数体被 adjust_leverage_raw 错位截断导致永远 return None
   -> 所有平仓信号都误报 'proxy 抽风' (9/21 BTC 亏损翻倍事故根因), 已修复
2. 平仓推送真实盈亏: /account/bills type=2 取绝对值最大 ts 锚点, 累加30秒窗口
   -> 匹配 OKX app 显示值 (BTC -46.14 / ZEC +14.42 实测通过), 含持仓时长
3. 无持仓减仓信号静默 (msg=None), 不再刷屏
4. run_advisor/execute_order 传完整 symbol (修 SNDK BadSymbol)
This commit is contained in:
2026-09-25 01:24:46 +08:00
parent 9c40e7eaf5
commit 7d156b8534
+349 -37
View File
@@ -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,25 +251,37 @@ 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'
# 默认为新开仓
# 减仓信号 (必须显式含 "减仓" 关键词)
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'
# ─── 格式化 ──────────────────────────────────────────────────────────────
def format_message(fields, rec, signal_type):
@@ -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:
text = sys.stdin.read()
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()
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)