#!/usr/bin/env python3 """ 交易信号处理器(no_agent模式): 1. 解析TG信号文本 2. 调advisor脚本获取正确金额 3. 格式化含📐完整模板 4. 推QQ 5. 信号去重/合并 用法: python3 process_signal.py "信号文本" 或: echo "信号文本" | python3 process_signal.py cron模式: 作为no_agent cron job的script使用 """ import sys import re import json import subprocess import sqlite3 import hashlib from pathlib import Path from datetime import datetime, timedelta SKILL_DIR = Path.home() / ".hermes/skills/trading/okx-auto-position" ADVISOR = SKILL_DIR / "scripts" / "okx_position_advisor.py" QQ_PUSH = Path.home() / ".hermes/scripts/push_to_qq.sh" SIGNAL_DB = Path.home() / ".hermes/trading/signal_history.db" 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 # ─── 解析 ──────────────────────────────────────────────────────────────── def parse_signal(text): """从TG信号文本提取关键字段""" fields = {} # 交易员 m = re.search(r'【([^】]{1,20})】', text) if m: fields['trader'] = m.group(1) # 字段映射 extractors = { 'symbol': r'【币种】\s*[::]?\s*(\S+)', 'side': r'【方向】\s*[::]?\s*(做多|做空)', 'leverage':r'【杠杆】\s*[::]?\s*(\d+)', 'size': r'【仓位大小】\s*[::]?\s*([\d,.]+)', 'value': r'【仓位价值】\s*[::]?\s*\$?\s*([\d,.]+)', 'entry': r'【开仓价】\s*[::]?\s*([\d,.]+)', 'current': r'【当前价】\s*[::]?\s*([\d,.]+)', 'pnl': r'【未实现盈亏】\s*[::]?\s*([-\d,.]+)', 'margin': r'【保证金】\s*[::]?\s*\$?\s*([\d,.]+)', } for key, pattern in extractors.items(): m = re.search(pattern, text) if m: fields[key] = m.group(1).replace(',', '') # 清理symbol if 'symbol' in fields: sym = fields['symbol'] sym = re.sub(r'\|.*$', '', sym) # 去掉 |永续|10x sym = sym.replace('USDT', '').strip() fields['symbol'] = sym # 方向转英文 if fields.get('side', '').startswith('做多'): fields['side_en'] = 'long' else: fields['side_en'] = 'short' return fields # ─── 去重 ──────────────────────────────────────────────────────────────── def init_dedup_db(): conn = sqlite3.connect(str(DEDUP_DB)) conn.execute(""" CREATE TABLE IF NOT EXISTS recent_signals ( id TEXT PRIMARY KEY, symbol TEXT, trader TEXT, timestamp REAL, raw_text TEXT ) """) conn.execute(""" CREATE TABLE IF NOT EXISTS processed ( msg_hash TEXT PRIMARY KEY, processed_at REAL ) """) conn.commit() return conn def is_duplicate(conn, text, symbol, trader): """检查是否重复信号(同交易员同币种2分钟内)""" msg_hash = hashlib.md5(text.encode()).hexdigest() # 检查完全相同的消息 row = conn.execute( "SELECT 1 FROM processed WHERE msg_hash = ?", (msg_hash,) ).fetchone() if row: return True # 检查同交易员同币种2分钟内的信号 cutoff = datetime.now().timestamp() - 120 # 2分钟 row = conn.execute( """SELECT 1 FROM recent_signals WHERE symbol = ? AND trader = ? AND timestamp > ? ORDER BY timestamp DESC LIMIT 1""", (symbol, trader, cutoff) ).fetchone() return row is not None def record_signal(conn, text, symbol, trader): """记录信号用于去重""" msg_hash = hashlib.md5(text.encode()).hexdigest() now = datetime.now().timestamp() conn.execute( "INSERT OR REPLACE INTO processed (msg_hash, processed_at) VALUES (?, ?)", (msg_hash, now) ) conn.execute( "INSERT OR REPLACE INTO recent_signals (id, symbol, trader, timestamp, raw_text) VALUES (?, ?, ?, ?, ?)", (msg_hash, symbol, trader, now, text[:500]) ) # 清理1小时前的记录 cutoff = now - 3600 conn.execute("DELETE FROM recent_signals WHERE timestamp < ?", (cutoff,)) conn.execute("DELETE FROM processed WHERE processed_at < ?", (cutoff,)) conn.commit() # ─── Advisor ────────────────────────────────────────────────────────────── def run_advisor(symbol, side, leverage): """调advisor脚本获取正确数据""" cmd = [ 'python3', str(ADVISOR), '--symbol', symbol, '--side', side, '--leverage', str(leverage), '--json' ] try: result = subprocess.run( cmd, capture_output=True, text=True, timeout=30, cwd=str(ADVISOR.parent) ) if result.returncode == 0: return json.loads(result.stdout) else: return {'error': result.stderr.strip()[:200]} except subprocess.TimeoutExpired: return {'error': 'advisor超时'} except json.JSONDecodeError: return {'error': 'advisor输出非JSON'} except Exception as e: return {'error': str(e)} # ─── 分类 ──────────────────────────────────────────────────────────────── def classify_signal(fields): """判断信号类型:加仓/新开仓/减仓/平仓""" text = fields.get('_raw', '') # 平仓信号 if '平仓' in text or '止盈' in text or '止损' in text: return 'close' # 减仓信号 pnl = float(fields.get('pnl', '0').replace('+', '')) if '减仓' in text or (pnl < 0 and '减' in text): return 'reduce' # 默认为新开仓或加仓(由advisor判断) return 'open' # ─── 格式化 ────────────────────────────────────────────────────────────── def format_message(fields, rec, signal_type): """格式化完整推送消息""" if 'error' in rec: return f"⚠️ advisor错误: {rec['error']}" symbol = fields.get('symbol', '?') side_cn = fields.get('side', '做多') emoji = '🟩' if fields.get('side_en') == 'long' else '🟥' leverage = fields.get('leverage', '10') trader = fields.get('trader', '?') size = fields.get('size', '?') value = fields.get('value', '?') entry_price = fields.get('entry', '?') pnl_str = fields.get('pnl', '0') pnl = float(pnl_str.replace('+', '')) if pnl_str else 0 current = rec.get('price', fields.get('current', '?')) pnl_emoji = '🔥' if pnl > 0 else '🔴' pnl_sign = '+' if pnl > 0 else '' # 性价比 cc = rec.get('cost_check', {}) rr = cc.get('rr_ratio', rec.get('rr', 0)) profit = cc.get('profit_amount', rec.get('tp_pnl', 0)) fee = cc.get('fee_cost', 0) fee_pct = cc.get('fee_pct', 0) net = cc.get('net_profit', 0) rating_emoji = cc.get('rating_emoji', '⚠️') rating_text = cc.get('rating_text', '未知') # 信号类型标签 type_labels = { 'open': '新开仓' if not fields.get('_is_add') else 'A类加仓', 'reduce': 'B类减仓', 'close': '平仓', } type_label = type_labels.get(signal_type, signal_type) # 信号源仓位(只展示,不参与计算) src_info = f"📊 {trader} {size} {symbol}(价值${value})← 信号源,非你的仓位" # 仓位变化对比 try: current_size = float(fields.get('size', '0').replace(',', '')) comparison = format_comparison(trader, symbol, current_size) except: comparison = "" # 交易员评分 try: trader_rating = format_trader_rating(trader) except: trader_rating = "" msg = f"""⚡ 跟单建议 | {symbol} {side_cn} {emoji} {leverage}x({type_label}) {src_info} 入场: ${entry_price} | 当前: ${current} 浮盈: {pnl_sign}{pnl:.0f} {pnl_emoji} 📊 仓位变化 {comparison} {trader_rating} 📐 性价比检查(基于你的推荐仓位) • 你的仓位: {rec['contracts']}张(保证金{rec['margin']:.2f} USDT) • 盈亏比: {rr}:1 {'✅' if rr >= 2 else '⚠️' if rr >= 1.5 else '❌'} • 盈利额: +{profit:.2f} USDT {'✅' if profit >= 10 else '❌ <10U保底'} • 手续费: {fee:.2f} USDT ({fee_pct:.1f}%) {'✅' if fee_pct < 5 else '❌'} • 净盈利: {net:.2f} USDT {'✅' if net >= 10 else '❌'} • 评级: {rating_emoji} {rating_text} • SL: ${rec['sl_price']}(-{rec['sl_pct']:.1f}%) • TP: ${rec['tp_price']}(+{rec['tp_pct']:.1f}%) 回复 Y 确认跟单 / N 取消""" # 如果余额不足,替换跟单方案 if rec.get('contracts', 0) == 0: msg = f"""⚡ 跟单建议 | {symbol} {side_cn} {emoji} {leverage}x({type_label}) {src_info} 入场: ${entry_price} | 当前: ${current} 浮盈: {pnl_sign}{pnl:.0f} {pnl_emoji} ⚠️ 余额不足,无法开仓 • 可用: {rec.get('acct_free', 0):.2f} USDT • 需要: ~{rec.get('margin', 0):.2f} USDT 💡 建议:等待其他仓位止盈释放保证金""" return msg # ─── 推送 ──────────────────────────────────────────────────────────────── def push_to_qq(message): """推送到QQ""" try: result = subprocess.run( ['bash', str(QQ_PUSH), message], capture_output=True, text=True, timeout=15 ) return result.returncode == 0 except: return False # ─── 执行订单 ──────────────────────────────────────────────────────────── def execute_order(symbol, side, leverage, rec): """执行开仓订单""" cmd = [ 'python3', str(ADVISOR), '--symbol', symbol, '--side', side, '--leverage', str(leverage), '--execute', '--json', '--rec-json', json.dumps(rec) ] try: result = subprocess.run( cmd, capture_output=True, text=True, timeout=30, cwd=str(ADVISOR.parent) ) if result.returncode == 0: return json.loads(result.stdout) else: return {'error': result.stderr.strip()[:200]} except Exception as e: return {'error': str(e)} def format_execution_result(fields, rec, exec_result): """格式化执行结果""" symbol = fields.get('symbol', '?') side_cn = fields.get('side', '做多') emoji = '🟩' if fields.get('side_en') == 'long' else '🟥' leverage = fields.get('leverage', '10') trader = fields.get('trader', '?') size = fields.get('size', '?') value = fields.get('value', '?') cc = rec.get('cost_check', {}) rr = cc.get('rr_ratio', rec.get('rr', 0)) profit = cc.get('profit_amount', rec.get('tp_pnl', 0)) fee = cc.get('fee_cost', 0) fee_pct = cc.get('fee_pct', 0) net = cc.get('net_profit', 0) rating_emoji = cc.get('rating_emoji', '⚠️') rating_text = cc.get('rating_text', '未知') pos = exec_result.get('position', {}) algo = exec_result.get('algo', {}) msg = f"""✅ {symbol} {side_cn} {emoji} {leverage}x 自动开仓 📊 信号源: {trader} {size} {symbol}(价值${value}) 📐 性价比检查 • 盈亏比: {rr}:1 ✅ • 盈利额: +{profit:.2f} USDT ✅ • 手续费: {fee:.2f} USDT ({fee_pct:.1f}%) ✅ • 净盈利: {net:.2f} USDT ✅ • 评级: {rating_emoji} {rating_text} ✅ 执行结果 • 入场: ${pos.get('entry', rec.get('price', '?'))} • 仓位: {pos.get('contracts', rec.get('contracts', '?'))}张 • TP: ${algo.get('tp', rec.get('tp_price', '?'))} • SL: ${algo.get('sl', rec.get('sl_price', '?'))} • 强平: ${pos.get('liq', '?')} ━━━ 当前全部持仓 ━━━ (查询中...)""" # 尝试获取当前全部持仓 try: acct_cmd = ['python3', '-c', f''' import sys sys.path.insert(0, "{ADVISOR.parent}") from okx_position_advisor import load_credentials, create_exchange, get_account_info creds = load_credentials() exchange = create_exchange(creds) info = get_account_info(exchange) print(f"Free: {{info['usdt_free']:.2f}}") for p in info['positions']: print(f" {{p['symbol']}}: {{p['contracts']}}张 UPL={{p['pnl']:.2f}}") '''] acct_result = subprocess.run(acct_cmd, capture_output=True, text=True, timeout=15) if acct_result.returncode == 0: msg = msg.replace("(查询中...)", f"\n```\n{acct_result.stdout.strip()}\n```") except: pass return msg # ─── 主流程 ────────────────────────────────────────────────────────────── def process_signal(text): """处理一条信号""" # 解析 fields = parse_signal(text) fields['_raw'] = text if not fields.get('symbol') or not fields.get('side'): return "⚠️ 无法解析信号" symbol = fields['symbol'] side = fields['side_en'] leverage = fields.get('leverage', '10') trader = fields.get('trader', '未知') # 去重 dedup_conn = init_dedup_db() if is_duplicate(dedup_conn, text, symbol, trader): dedup_conn.close() return "⏭️ 重复信号,跳过" # 分类 signal_type = classify_signal(fields) # 平仓信号直接推送 if signal_type == 'close': msg = f"""🔔 {trader} {symbol}平仓提醒 {text[text.find("入场"):text.find("回复")].strip() if "入场" in text else "详情见原始信号"} 💡 操作建议 • 若已跟单{symbol},建议同步止盈/止损""" record_signal(dedup_conn, text, symbol, trader) dedup_conn.close() push_to_qq(msg) return "✅ 平仓信号已推送" # 调advisor rec = run_advisor(symbol, side, leverage) if 'error' in rec: record_signal(dedup_conn, text, symbol, trader) dedup_conn.close() return f"⚠️ advisor错误: {rec['error']}" # 性价比检查 cc = rec.get('cost_check', {}) rr = cc.get('rr_ratio', rec.get('rr', 0)) profit = cc.get('profit_amount', rec.get('tp_pnl', 0)) fee_pct = cc.get('fee_pct', 0) auto_execute = cc.get('auto_execute', False) or (rr >= 2 and fee_pct < 5 and profit >= 10) if auto_execute and signal_type == 'open': # 性价比高 + 新开仓 → 自动执行 exec_result = execute_order(symbol, side, leverage, rec) if exec_result and 'error' not in exec_result: msg = format_execution_result(fields, rec, exec_result) _tracker_record(trader=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='auto_executed') else: # 执行失败,降级为确认模式 auto_execute = False msg = format_message(fields, rec, signal_type) _tracker_record(trader=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='pushed') else: # 需要确认或减仓信号 msg = format_message(fields, rec, signal_type) _tracker_record(trader=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='pushed') # 记录去重 record_signal(dedup_conn, text, symbol, trader) dedup_conn.close() # 推送 success = push_to_qq(msg) if success: return f"✅ 已推送 | {symbol} {side} {leverage}x | {rec['contracts']}张 | 性价比{rec.get('cost_check', {}).get('rating_text', '?')}" else: return f"❌ 推送失败" def main(): 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") return result = process_signal(text) print(result) if __name__ == '__main__': main()