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:
@@ -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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user