#!/usr/bin/env python3 """ 信号历史数据库 - 记录所有交易信号 用法: python3 signal_db.py log '<原始信号文本>' python3 signal_db.py history [--trader NAME] [--symbol BTC] [--days 7] [--limit 20] python3 signal_db.py stats python3 signal_db.py traders """ import sqlite3 import os import re import json import sys import time from datetime import datetime, timedelta DB_PATH = os.path.expanduser("~/.hermes/trading/signal_history.db") def get_conn(): os.makedirs(os.path.dirname(DB_PATH), exist_ok=True) conn = sqlite3.connect(DB_PATH) conn.row_factory = sqlite3.Row return conn def init_db(): conn = get_conn() conn.execute(""" CREATE TABLE IF NOT EXISTS signals ( id INTEGER PRIMARY KEY AUTOINCREMENT, timestamp REAL NOT NULL, time_str TEXT NOT NULL, trader TEXT, symbol TEXT, side TEXT, leverage INTEGER, raw_size REAL, raw_unit TEXT, entry_price REAL, current_price REAL, margin REAL, margin_unit TEXT, margin_mode TEXT, pnl REAL, pnl_pct REAL, leverage_change TEXT, raw_text TEXT NOT NULL, outcome TEXT DEFAULT 'pending', outcome_time REAL, outcome_detail TEXT ) """) conn.execute(""" CREATE INDEX IF NOT EXISTS idx_signals_time ON signals(timestamp DESC) """) conn.execute(""" CREATE INDEX IF NOT EXISTS idx_signals_trader ON signals(trader) """) conn.execute(""" CREATE INDEX IF NOT EXISTS idx_signals_symbol ON signals(symbol) """) conn.commit() conn.close() def extract_trader(text): """Extract trader name from signal text. Common patterns: - 【熬鹰资本】 (standalone 【name】 on its own line, no colon) - 【交易员】xxx - 【老师】xxx - 交易员: xxx - 来自xxx: - [xxx] at the beginning - @username - Name followed by colon (e.g. "张三: BTC做多") - Name followed by signal keywords """ # Priority 1: Standalone 【name】 on its own line (no colon after) # This matches 【熬鹰资本】 but NOT 【币种】: xxx standalone = re.search(r'^【([^】]{1,20})】\s*$', text, re.MULTILINE) if standalone: return standalone.group(1).strip() patterns = [ r'【交易员】\s*(.+?)(?:\n|$|【)', r'【老师】\s*(.+?)(?:\n|$|【)', r'【来源】\s*(.+?)(?:\n|$|【)', r'【策略】\s*(.+?)(?:\n|$|【)', r'交易员[::]\s*(.+?)(?:\n|$)', r'老师[::]\s*(.+?)(?:\n|$)', r'来源[::]\s*(.+?)(?:\n|$)', r'策略师[::]\s*(.+?)(?:\n|$)', r'^\[([^\]]+)\]', # [TraderName] at start r'^(@\w+)', # @username at start r'^(\S+?)\s*[::]\s*(?:【|BTC|ETH|做多|做空|开多|开空)', # Name: signal r'^(\S{2,10})\s+(?:【|BTC|ETH|做多|做空|开多|开空)', # Name signal (no colon) ] for p in patterns: m = re.search(p, text, re.MULTILINE) if m: name = m.group(1).strip() # Filter out non-name matches if len(name) > 1 and len(name) < 30 and not re.match(r'^[\d.]+$', name): return name return None def extract_signal_fields(text): """Parse signal text for key fields.""" result = {'trader': extract_trader(text)} # Symbol - multiple patterns m = re.search(r'(?:【币种】|币种[::]\s*)(\w+)', text) if not m: m = re.search(r'([A-Z]{2,10})USDT', text) if not m: # Bare symbol before direction keywords (e.g. "ETH做空", "BTC 开多") m = re.search(r'\b([A-Z]{2,10})\s*(?:做多|做空|开多|开空|做多|做空|long|short)', text, re.IGNORECASE) if m: raw = m.group(1).upper().replace("USDT", "").replace("/USDT", "").replace(":USDT", "") if len(raw) >= 2: result['symbol'] = raw # Side if re.search(r'(做空|卖出|short|sell|空单|开空)', text, re.IGNORECASE): result['side'] = 'short' elif re.search(r'(做多|买入|long|buy|多单|开多)', text, re.IGNORECASE): result['side'] = 'long' # Leverage from field m = re.search(r'(?:【币种】|币种[::]\s*)[^\n]*?(\d+)\s*[xX倍]', text) if not m: m = re.search(r'(\d+)\s*[xX倍]', text) result['leverage'] = int(m.group(1)) if m else None # Size m = re.search(r'(?:【仓位】|仓位[::]\s*)([\d,.]+)\s*(\w+)', text) if m: result['raw_size'] = float(m.group(1).replace(",", "")) result['raw_unit'] = m.group(2) # Entry price m = re.search(r'【开仓价】\s*[::]?\s*([\d,.]+)', text) if m: result['entry_price'] = float(m.group(1).replace(",", "")) # Current price m = re.search(r'【当前价】\s*[::]?\s*([\d,.]+)', text) if m: result['current_price'] = float(m.group(1).replace(",", "")) # Margin m = re.search(r'【保证金】\s*[::]?\s*([\d,.]+)\s*(\w+)', text) if m: result['margin'] = float(m.group(1).replace(",", "")) result['margin_unit'] = m.group(2) # Margin mode (全仓/逐仓) m = re.search(r'(全仓|逐仓)', text) if m: result['margin_mode'] = m.group(1) # PnL m = re.search(r'【收益额】\s*[::]?\s*([-\d,.]+)\s*(\w+)', text) if m: result['pnl'] = float(m.group(1).replace(",", "")) m = re.search(r'【收益额】\s*[::]?\s*[-\d,.]+\s*\w+\(([-\d.]+)%\)', text) if m: result['pnl_pct'] = float(m.group(1)) # Leverage change (e.g. "5→10") m = re.search(r'修改了杠杆\s*(\d+)\s*[→>→]\s*(\d+)', text) if m: result['leverage_change'] = f"{m.group(1)}→{m.group(2)}" # Is close signal result['is_close'] = bool(re.search(r'(平仓|止盈|止损|close|全平)', text, re.IGNORECASE)) return result def log_signal(raw_text): """Log a signal to the database.""" init_db() fields = extract_signal_fields(raw_text) conn = get_conn() now = time.time() time_str = datetime.now().strftime("%Y-%m-%d %H:%M:%S") conn.execute(""" INSERT INTO signals (timestamp, time_str, trader, symbol, side, leverage, raw_size, raw_unit, entry_price, current_price, margin, margin_unit, margin_mode, pnl, pnl_pct, leverage_change, raw_text) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) """, ( now, time_str, fields.get('trader'), fields.get('symbol'), fields.get('side'), fields.get('leverage'), fields.get('raw_size'), fields.get('raw_unit'), fields.get('entry_price'), fields.get('current_price'), fields.get('margin'), fields.get('margin_unit'), fields.get('margin_mode'), fields.get('pnl'), fields.get('pnl_pct'), fields.get('leverage_change'), raw_text, )) signal_id = conn.execute("SELECT last_insert_rowid()").fetchone()[0] conn.commit() conn.close() return { 'id': signal_id, 'time': time_str, 'trader': fields.get('trader'), 'symbol': fields.get('symbol'), 'side': fields.get('side'), 'leverage': fields.get('leverage'), } def update_outcome(signal_id, outcome, detail=""): """Update signal outcome (confirmed/cancelled/expired).""" conn = get_conn() conn.execute(""" UPDATE signals SET outcome=?, outcome_time=?, outcome_detail=? WHERE id=? """, (outcome, time.time(), detail, signal_id)) conn.commit() conn.close() def find_latest_signal_id(symbol): """Find the most recent pending signal ID for a symbol.""" conn = get_conn() row = conn.execute(""" SELECT id FROM signals WHERE symbol=? AND outcome='pending' ORDER BY timestamp DESC LIMIT 1 """, (symbol,)).fetchone() conn.close() return row['id'] if row else None def query_history(trader=None, symbol=None, days=7, limit=20): """Query signal history with filters.""" init_db() conn = get_conn() conditions = ["timestamp > ?"] params = [time.time() - days * 86400] if trader: conditions.append("trader LIKE ?") params.append(f"%{trader}%") if symbol: conditions.append("symbol LIKE ?") params.append(f"%{symbol}%") where = " AND ".join(conditions) rows = conn.execute(f""" SELECT * FROM signals WHERE {where} ORDER BY timestamp DESC LIMIT ? """, params + [limit]).fetchall() conn.close() return [dict(r) for r in rows] def get_trader_stats(): """Get stats per trader.""" init_db() conn = get_conn() rows = conn.execute(""" SELECT trader, COUNT(*) as total, SUM(CASE WHEN outcome='confirmed' THEN 1 ELSE 0 END) as confirmed, SUM(CASE WHEN outcome='cancelled' THEN 1 ELSE 0 END) as cancelled, SUM(CASE WHEN outcome='pending' THEN 1 ELSE 0 END) as pending, SUM(CASE WHEN outcome='expired' THEN 1 ELSE 0 END) as expired, GROUP_CONCAT(DISTINCT symbol) as symbols FROM signals GROUP BY trader ORDER BY total DESC """).fetchall() conn.close() return [dict(r) for r in rows] def get_summary_stats(): """Get overall summary stats.""" init_db() conn = get_conn() total = conn.execute("SELECT COUNT(*) as c FROM signals").fetchone()['c'] today = conn.execute( "SELECT COUNT(*) as c FROM signals WHERE timestamp > ?", (time.time() - 86400,) ).fetchone()['c'] by_outcome = conn.execute(""" SELECT outcome, COUNT(*) as c FROM signals GROUP BY outcome """).fetchall() by_side = conn.execute(""" SELECT side, COUNT(*) as c FROM signals WHERE side IS NOT NULL GROUP BY side """).fetchall() top_symbols = conn.execute(""" SELECT symbol, COUNT(*) as c FROM signals WHERE symbol IS NOT NULL GROUP BY symbol ORDER BY c DESC LIMIT 5 """).fetchall() conn.close() return { 'total': total, 'today': today, 'by_outcome': {r['outcome']: r['c'] for r in by_outcome}, 'by_side': {r['side']: r['c'] for r in by_side}, 'top_symbols': [(r['symbol'], r['c']) for r in top_symbols], } def format_history(signals): """Format history for display.""" if not signals: return "📭 暂无信号记录" lines = ["📋 **信号历史记录**\n"] for s in signals: side_cn = "做多" if s['side'] == 'long' else ("做空" if s['side'] == 'short' else "?") outcome_emoji = { 'confirmed': '✅', 'cancelled': '❌', 'pending': '⏳', 'expired': '⏰' }.get(s['outcome'], '❓') trader = s['trader'] or '未知' lev = f"{s['leverage']}x" if s['leverage'] else '?x' # Extra info extra = [] if s.get('entry_price'): extra.append(f"入场{s['entry_price']}") if s.get('pnl'): pnl_str = f"{s['pnl']:+,.0f}" if s.get('pnl_pct'): pnl_str += f"({s['pnl_pct']:+.1f}%)" extra.append(f"盈亏{pnl_str}") if s.get('margin'): extra.append(f"保证金{s['margin']:,.0f}") if s.get('margin_mode'): extra.append(s['margin_mode']) if s.get('leverage_change'): extra.append(f"杠杆{s['leverage_change']}") extra_str = " | " + " ".join(extra) if extra else "" lines.append( f"{outcome_emoji} #{s['id']} | {s['time_str']} | " f"👤{trader} | {s['symbol'] or '?'} {side_cn} | " f"{lev}{extra_str}" ) return "\n".join(lines) def format_stats(stats): """Format stats for display.""" lines = ["📊 **信号统计**\n"] lines.append(f"总计: {stats['total']} 条") lines.append(f"今日: {stats['today']} 条\n") if stats['by_outcome']: lines.append("**按结果:**") for k, v in stats['by_outcome'].items(): emoji = {'confirmed': '✅', 'cancelled': '❌', 'pending': '⏳', 'expired': '⏰'}.get(k, '❓') lines.append(f" {emoji} {k}: {v}") if stats['by_side']: lines.append("\n**按方向:**") for k, v in stats['by_side'].items(): cn = "做多" if k == 'long' else "做空" lines.append(f" {cn}: {v}") if stats['top_symbols']: lines.append("\n**热门币种:**") for sym, cnt in stats['top_symbols']: lines.append(f" {sym}: {cnt}次") return "\n".join(lines) def format_traders(traders): """Format trader stats for display.""" if not traders: return "📭 暂无交易员数据" lines = ["👤 **交易员统计**\n"] for t in traders: name = t['trader'] or '未知' lines.append( f"**{name}**: {t['total']}条信号 | " f"✅{t['confirmed']} ❌{t['cancelled']} ⏳{t['pending']} | " f"币种: {t['symbols'] or '-'}" ) return "\n".join(lines) def main(): if len(sys.argv) < 2: print("用法: signal_db.py [args]") sys.exit(1) action = sys.argv[1] if action == "log": if len(sys.argv) < 3: print("用法: signal_db.py log ''") sys.exit(1) raw_text = sys.argv[2] result = log_signal(raw_text) print(json.dumps(result, ensure_ascii=False)) elif action == "history": import argparse # Simple arg parsing trader = symbol = None days = 7 limit = 20 for i in range(2, len(sys.argv)): if sys.argv[i] == "--trader" and i + 1 < len(sys.argv): trader = sys.argv[i + 1] elif sys.argv[i] == "--symbol" and i + 1 < len(sys.argv): symbol = sys.argv[i + 1] elif sys.argv[i] == "--days" and i + 1 < len(sys.argv): days = int(sys.argv[i + 1]) elif sys.argv[i] == "--limit" and i + 1 < len(sys.argv): limit = int(sys.argv[i + 1]) signals = query_history(trader, symbol, days, limit) print(format_history(signals)) elif action == "stats": stats = get_summary_stats() print(format_stats(stats)) elif action == "traders": traders = get_trader_stats() print(format_traders(traders)) elif action == "update": if len(sys.argv) < 4: print("用法: signal_db.py update [detail]") sys.exit(1) signal_id = int(sys.argv[2]) outcome = sys.argv[3] detail = sys.argv[4] if len(sys.argv) > 4 else "" update_outcome(signal_id, outcome, detail) print(f"✅ Updated signal #{signal_id} → {outcome}") else: print(f"Unknown action: {action}") sys.exit(1) if __name__ == "__main__": main()