#!/usr/bin/env python3 """ 交易信号处理器 - 一体化脚本 用法: python3 trade_signal_handler.py signal "【币种】BTCUSDT|永续|10x\n【方向】做多\n【仓位】0.5 BTC" python3 trade_signal_handler.py confirm BTC python3 trade_signal_handler.py cancel BTC python3 trade_signal_handler.py status """ import re import os import sys import json import time import glob import subprocess SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) ADVISOR_SCRIPT = os.path.join(SCRIPT_DIR, "okx_position_advisor.py") SIGNAL_DB_SCRIPT = os.path.join(SCRIPT_DIR, "signal_db.py") PENDING_DIR = os.path.expanduser("~/.hermes/trading/pending") os.makedirs(PENDING_DIR, exist_ok=True) def log_signal_to_db(signal_text): """Log signal to history database, return signal_id or None""" try: result = subprocess.run( [sys.executable, SIGNAL_DB_SCRIPT, "log", signal_text], capture_output=True, text=True, timeout=10, ) if result.returncode == 0: return json.loads(result.stdout).get("id") except Exception: pass return None def update_signal_outcome(signal_id, outcome, detail=""): """Update signal outcome in database""" if not signal_id: return try: subprocess.run( [sys.executable, SIGNAL_DB_SCRIPT, "update", str(signal_id), outcome, detail], capture_output=True, text=True, timeout=10, ) except Exception: pass def parse_signal(text): """Parse trading signal text, extract symbol/direction/leverage/size""" result = {} # 币种: BTCUSDT|永续|10x or 【币种】BTCUSDT symbol_match = re.search(r'(?:【币种】|币种[::]\s*)(\w+)', text) if not symbol_match: symbol_match = re.search(r'([A-Z]{2,10})USDT', text) if symbol_match: raw = symbol_match.group(1).upper() raw = raw.replace("USDT", "").replace("/USDT", "").replace(":USDT", "") result["symbol"] = raw else: return None # 方向 if re.search(r'(做空|卖出|short|sell|空单|开空)', text, re.IGNORECASE): result["side"] = "short" elif re.search(r'(做多|买入|long|buy|多单|开多)', text, re.IGNORECASE): result["side"] = "long" else: return None # 杠杆 lev_match = re.search(r'(\d+)\s*[xX倍]', text) result["leverage"] = int(lev_match.group(1)) if lev_match else 10 # 仓位数量 size_match = re.search(r'(?:【仓位】|仓位[::]\s*)([\d,.]+)\s*(\w+)', text) if size_match: result["raw_size"] = float(size_match.group(1).replace(",", "")) result["raw_unit"] = size_match.group(2) # 是否加仓/平仓 result["is_add"] = bool(re.search(r'(加仓|追仓)', text)) result["is_close"] = bool(re.search(r'(平仓|止盈|止损|close|全平)', text, re.IGNORECASE)) return result def save_pending(symbol, rec_json, signal_text, signal_id=None): """Save pending recommendation to file""" path = os.path.join(PENDING_DIR, f"{symbol.upper()}.json") data = { "symbol": symbol.upper(), "rec": rec_json, "signal": signal_text, "signal_id": signal_id, "timestamp": time.time(), "time_str": time.strftime("%Y-%m-%d %H:%M:%S"), } with open(path, "w") as f: json.dump(data, f, ensure_ascii=False, indent=2) return path def load_pending(symbol): """Load pending recommendation""" path = os.path.join(PENDING_DIR, f"{symbol.upper()}.json") if not os.path.exists(path): return None with open(path) as f: return json.load(f) def remove_pending(symbol): """Remove pending recommendation""" path = os.path.join(PENDING_DIR, f"{symbol.upper()}.json") if os.path.exists(path): os.remove(path) def run_advisor(symbol, side, leverage): """Run the advisor script and return JSON result""" cmd = [ sys.executable, ADVISOR_SCRIPT, "--symbol", symbol, "--side", side, "--leverage", str(leverage), "--json", ] env = os.environ.copy() # Source bashrc to get OKX credentials result = subprocess.run( ["bash", "-c", f"source ~/.bashrc && {' '.join(cmd)}"], capture_output=True, text=True, timeout=30, ) if result.returncode != 0: return {"error": result.stderr.strip() or "Advisor script failed"} try: return json.loads(result.stdout) except json.JSONDecodeError: return {"error": f"Invalid JSON output: {result.stdout[:200]}"} def execute_trade(rec_json): """Execute the trade using the advisor script""" import shlex rec_str = json.dumps(rec_json, ensure_ascii=False) symbol = rec_json.get("symbol", "").split("/")[0] side = rec_json.get("side", "") cmd = f"source ~/.bashrc && python3 {ADVISOR_SCRIPT} --symbol {symbol} --side {side} --execute --json --rec-json {shlex.quote(rec_str)}" result = subprocess.run( ["bash", "-c", cmd], capture_output=True, text=True, timeout=60, ) if result.returncode != 0: return {"error": result.stderr.strip() or "Execution failed"} try: return json.loads(result.stdout) except json.JSONDecodeError: return {"raw": result.stdout.strip()} def format_recommendation(rec, signal_text=""): """Format recommendation for user display""" symbol = rec.get("symbol", "?") side = rec.get("side", "?") side_cn = "做多" if side in ("long", "buy") else "做空" leverage = rec.get("leverage", 10) contracts = rec.get("contracts", 0) entry = rec.get("entry_price", 0) tp = rec.get("tp_price", 0) sl = rec.get("sl_price", 0) margin = rec.get("margin_used", 0) balance = rec.get("balance", 0) margin_pct = rec.get("margin_pct", 0) tp_pct = rec.get("tp_pct", 0) # 标的价格变动% sl_pct = rec.get("sl_pct", 0) tp_pnl = rec.get("tp_pnl", 0) sl_pnl = rec.get("sl_pnl", 0) # 保证金收益率 tp_margin_pct = (tp_pnl / margin * 100) if margin > 0 else 0 sl_margin_pct = (sl_pnl / margin * 100) if margin > 0 else 0 liq_price = rec.get("liq_price", 0) liq_pct = rec.get("liq_pct", 0) rr = rec.get("rr_ratio", 0) lines = [ f"📊 **{symbol}USDT {side_cn}** - 仓位推荐", "", f"💰 可用余额: {balance:.2f} USDT", f"📈 当前价: **{entry}**", "", "**开仓方案:**", f"• 方向: {side_cn}", f"• 杠杆: **{leverage}x**", f"• 张数: **{contracts}张**", f"• 保证金: {margin:.2f} USDT ({margin_pct:.0f}%)", "", "**止盈止损:**", f"• 🎯 止盈: **{tp}** (保证金+{tp_margin_pct:.0f}%) → +{tp_pnl:.2f} USDT", f"• 🛑 止损: **{sl}** (保证金-{sl_margin_pct:.0f}%) → -{sl_pnl:.2f} USDT", f"• 📐 盈亏比: **{rr:.1f}:1**", ] if liq_price: lines.append(f"• ⚠️ 清算价: {liq_price} (距离 {liq_pct:.1f}%)") lines.extend([ "", "回复 **Y** 确认下单", "回复 **N** 取消", ]) return "\n".join(lines) def format_execution_result(result, symbol, side): """Format execution result for user display""" if "error" in result: return f"❌ **{symbol}USDT 下单失败**\n\n{result['error']}" side_cn = "做多" if side in ("long", "buy") else "做空" lines = [f"✅ **{symbol}USDT {side_cn} 开仓成功**"] # Parse steps for step in result.get('steps', []): if step['step'] == 'leverage': if step['status'] == 'ok': lines.append("✅ 杠杆设置成功") else: lines.append(f"⚠️ 杠杆: {step.get('msg', '')}") elif step['step'] == 'order': if step['status'] == 'ok': lines.append(f"✅ 下单成功 (ID: {step['order_id']})") else: lines.append(f"❌ 下单失败: {step.get('msg', '')}") return '\n'.join(lines) elif step['step'] == 'tp_sl': if step['status'] == 'ok': lines.append(f"✅ 止盈止损设置成功 (ID: {step['algo_id']})") else: lines.append(f"⚠️ 止盈止损: {step.get('msg', '')}") # Position info pos = result.get('position') if pos: pnl_emoji = "🟢" if pos.get('pnl', 0) >= 0 else "🔴" lines.extend([ "", "📊 **持仓确认:**", f"• 方向: {side_cn}", f"• 数量: {pos.get('contracts', '?')}张", f"• 入场价: **{pos.get('entry', '?')}**", f"• {pnl_emoji} 浮盈: {pos.get('pnl', 0):.2f} USDT", ]) # TP/SL info algo = result.get('algo') if algo: lines.extend([ "", "🎯 **止盈止损:**", f"• 止盈: **{algo.get('tp', '?')}**", f"• 止损: **{algo.get('sl', '?')}**", ]) return "\n".join(lines) def main(): if len(sys.argv) < 2: print("用法: trade_signal_handler.py [args]") sys.exit(1) action = sys.argv[1] if action == "signal": if len(sys.argv) < 3: print("用法: trade_signal_handler.py signal ''") sys.exit(1) signal_text = sys.argv[2] parsed = parse_signal(signal_text) if not parsed: print(json.dumps({"error": "无法解析信号", "raw": signal_text})) sys.exit(1) if parsed.get("is_close"): # 平仓信号 print(json.dumps({"action": "close", "symbol": parsed["symbol"]})) sys.exit(0) # 记录信号到数据库 signal_id = log_signal_to_db(signal_text) # 计算仓位 rec = run_advisor(parsed["symbol"], parsed["side"], parsed["leverage"]) if "error" in rec: if signal_id: update_signal_outcome(signal_id, "error", rec["error"]) print(json.dumps(rec)) sys.exit(1) # 保存待确认 save_pending(parsed["symbol"], rec, signal_text, signal_id) # 输出推荐 output = { "action": "recommend", "symbol": parsed["symbol"], "side": parsed["side"], "recommendation": rec, "display": format_recommendation(rec, signal_text), } print(json.dumps(output, ensure_ascii=False)) elif action == "confirm": if len(sys.argv) < 3: print("用法: trade_signal_handler.py confirm ") sys.exit(1) symbol = sys.argv[2].upper().replace("USDT", "") pending = load_pending(symbol) if not pending: print(json.dumps({"error": f"没有待确认的 {symbol} 交易"})) sys.exit(1) rec = pending["rec"] signal_id = pending.get("signal_id") result = execute_trade(rec) # Only remove pending if execution succeeded if not result.get("error"): remove_pending(symbol) if signal_id: update_signal_outcome(signal_id, "confirmed", json.dumps(result, ensure_ascii=False)[:500]) else: if signal_id: update_signal_outcome(signal_id, "error", result.get("error", "")[:200]) output = { "action": "executed", "symbol": symbol, "side": rec.get("side"), "result": result, "display": format_execution_result(result, symbol, rec.get("side")), } print(json.dumps(output, ensure_ascii=False)) elif action == "cancel": if len(sys.argv) < 3: print("用法: trade_signal_handler.py cancel ") sys.exit(1) symbol = sys.argv[2].upper().replace("USDT", "") pending = load_pending(symbol) signal_id = pending.get("signal_id") if pending else None remove_pending(symbol) if signal_id: update_signal_outcome(signal_id, "cancelled") print(json.dumps({"action": "cancelled", "symbol": symbol})) elif action == "status": pending_files = glob.glob(os.path.join(PENDING_DIR, "*.json")) if not pending_files: print(json.dumps({"pending": []})) else: pending = [] for f in pending_files: with open(f) as fh: d = json.load(fh) pending.append({ "symbol": d["symbol"], "side": d["rec"].get("side"), "time": d.get("time_str", d.get("timestamp", "unknown")), }) print(json.dumps({"pending": pending}, ensure_ascii=False)) elif action == "history": # Forward to signal_db.py result = subprocess.run( [sys.executable, SIGNAL_DB_SCRIPT, "history"] + sys.argv[2:], capture_output=True, text=True, timeout=10, ) print(result.stdout) if result.returncode != 0 and result.stderr: print(result.stderr, file=sys.stderr) elif action == "stats": result = subprocess.run( [sys.executable, SIGNAL_DB_SCRIPT, "stats"], capture_output=True, text=True, timeout=10, ) print(result.stdout) elif action == "traders": result = subprocess.run( [sys.executable, SIGNAL_DB_SCRIPT, "traders"], capture_output=True, text=True, timeout=10, ) print(result.stdout) else: print(f"Unknown action: {action}") sys.exit(1) if __name__ == "__main__": main()