#!/usr/bin/env python3 """ 信号历史跟踪DB: 记录每次确认的信号,用于对比加仓/减仓趋势。 表结构: - confirmed_signals: 已确认的信号(用户回复Y后记录) - position_history: 仓位变化历史 """ import sqlite3 from pathlib import Path from datetime import datetime DB_PATH = Path.home() / ".hermes/trading/signal_history.db" def get_conn(): DB_PATH.parent.mkdir(parents=True, exist_ok=True) conn = sqlite3.connect(str(DB_PATH)) conn.row_factory = sqlite3.Row return conn def init_db(): conn = get_conn() conn.executescript(""" CREATE TABLE IF NOT EXISTS confirmed_signals ( id INTEGER PRIMARY KEY AUTOINCREMENT, timestamp TEXT NOT NULL, trader TEXT NOT NULL, symbol TEXT NOT NULL, side TEXT NOT NULL, leverage INTEGER, trader_size REAL, trader_entry REAL, trader_pnl REAL, our_contracts REAL, our_margin REAL, our_entry REAL, outcome TEXT DEFAULT 'confirmed', raw_text TEXT ); CREATE TABLE IF NOT EXISTS position_history ( id INTEGER PRIMARY KEY AUTOINCREMENT, timestamp TEXT NOT NULL, trader TEXT NOT NULL, symbol TEXT NOT NULL, size REAL NOT NULL, entry_price REAL, pnl REAL, signal_type TEXT ); CREATE TABLE IF NOT EXISTS position_locks ( symbol TEXT PRIMARY KEY, trader TEXT NOT NULL, side TEXT NOT NULL, locked_at TEXT NOT NULL, reason TEXT, auto_locked INTEGER DEFAULT 1 ); CREATE INDEX IF NOT EXISTS idx_confirmed_trader_symbol ON confirmed_signals(trader, symbol, timestamp); CREATE INDEX IF NOT EXISTS idx_history_trader_symbol ON position_history(trader, symbol, timestamp); CREATE INDEX IF NOT EXISTS idx_locks_trader ON position_locks(trader); """) conn.commit() return conn def record_confirmed(trader, symbol, side, leverage, trader_size, trader_entry, trader_pnl, our_contracts, our_margin, our_entry, raw_text=""): """记录已确认的信号""" conn = init_db() conn.execute(""" INSERT INTO confirmed_signals (timestamp, trader, symbol, side, leverage, trader_size, trader_entry, trader_pnl, our_contracts, our_margin, our_entry, raw_text) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) """, (datetime.now().isoformat(), trader, symbol, side, leverage, trader_size, trader_entry, trader_pnl, our_contracts, our_margin, our_entry, raw_text[:2000])) conn.execute(""" INSERT INTO position_history (timestamp, trader, symbol, size, entry_price, pnl, signal_type) VALUES (?, ?, ?, ?, ?, ?, ?) """, (datetime.now().isoformat(), trader, symbol, trader_size, trader_entry, trader_pnl, 'confirmed')) conn.commit() conn.close() def record_signal(trader, symbol, side, leverage, trader_size, trader_entry, trader_pnl, raw_text="", outcome="pushed"): """记录推送的信号(不管是否确认)""" conn = init_db() conn.execute(""" INSERT INTO confirmed_signals (timestamp, trader, symbol, side, leverage, trader_size, trader_entry, trader_pnl, outcome, raw_text) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) """, (datetime.now().isoformat(), trader, symbol, side, leverage, trader_size, trader_entry, trader_pnl, outcome, raw_text[:2000])) conn.execute(""" INSERT INTO position_history (timestamp, trader, symbol, size, entry_price, pnl, signal_type) VALUES (?, ?, ?, ?, ?, ?, ?) """, (datetime.now().isoformat(), trader, symbol, trader_size, trader_entry, trader_pnl, outcome)) conn.commit() conn.close() def get_last_confirmed(trader, symbol): """获取上次确认的信号""" conn = init_db() row = conn.execute(""" SELECT * FROM confirmed_signals WHERE trader = ? AND symbol = ? AND outcome = 'confirmed' ORDER BY timestamp DESC LIMIT 1 """, (trader, symbol)).fetchone() conn.close() return dict(row) if row else None def get_last_signal(trader, symbol): """获取上次推送的信号(不管是否确认)""" conn = init_db() row = conn.execute(""" SELECT * FROM confirmed_signals WHERE trader = ? AND symbol = ? ORDER BY timestamp DESC LIMIT 1 """, (trader, symbol)).fetchone() conn.close() return dict(row) if row else None def get_position_trend(trader, symbol, limit=5): """获取仓位变化趋势""" conn = init_db() rows = conn.execute(""" SELECT * FROM position_history WHERE trader = ? AND symbol = ? ORDER BY timestamp DESC LIMIT ? """, (trader, symbol, limit)).fetchall() conn.close() return [dict(r) for r in rows] def compare_position(trader, symbol, current_size): """对比当前仓位与上次,返回变化描述""" last = get_last_signal(trader, symbol) if not last: return None, "首次出现" last_size = last.get('trader_size', 0) if not last_size or last_size == 0: return None, "上次仓位未知" change = current_size - last_size change_pct = (change / last_size) * 100 if abs(change_pct) < 1: return last_size, "仓位不变" elif change > 0: return last_size, f"加仓 +{change_pct:.1f}%" else: return last_size, f"减仓 {change_pct:.1f}%" def format_comparison(trader, symbol, current_size): """格式化对比信息""" last_size, desc = compare_position(trader, symbol, current_size) # 智能显示: 整数直接显示, 小数保留 2-3 位 def _fmt(n): if n is None: return "?" if n == int(n) and abs(n) >= 10: return f"{int(n):,}" return f"{n:,.2f}" if last_size is None: return f"• {trader} {symbol}: 首次出现,仓位 {_fmt(current_size)}" if "不变" in desc: return f"• {trader} {symbol}: 仓位不变 {_fmt(current_size)}" elif "加仓" in desc: return f"• 📈 {trader} {symbol}: {_fmt(last_size)} → {_fmt(current_size)}({desc})" elif "减仓" in desc: delta_pct = 0 if last_size and last_size > 0: delta_pct = (current_size - last_size) / last_size * 100 return f"• 📉 {trader} {symbol}: {_fmt(last_size)} → {_fmt(current_size)}(减幅 {delta_pct:+.1f}%)" else: return f"• {trader} {symbol}: {_fmt(last_size)} → {_fmt(current_size)}({desc})" # ─── 交易员统计 ────────────────────────────────────────────────────────── def get_trader_stats(trader=None): """获取交易员统计数据""" conn = init_db() if trader: rows = conn.execute(""" SELECT trader, symbol, side, outcome, trader_pnl, timestamp FROM confirmed_signals WHERE trader = ? ORDER BY timestamp DESC """, (trader,)).fetchall() else: rows = conn.execute(""" SELECT trader, symbol, side, outcome, trader_pnl, timestamp FROM confirmed_signals ORDER BY trader, timestamp DESC """).fetchall() conn.close() # 按交易员分组 stats = {} for row in rows: r = dict(row) t = r['trader'] if t not in stats: stats[t] = { 'trader': t, 'total': 0, 'pushed': 0, 'confirmed': 0, 'auto_executed': 0, 'cancelled': 0, 'wins': 0, 'losses': 0, 'total_pnl': 0, 'trades': [], } s = stats[t] s['total'] += 1 outcome = r.get('outcome', 'pushed') if outcome in s: s[outcome] += 1 pnl = r.get('trader_pnl', 0) or 0 s['total_pnl'] += pnl if pnl > 0: s['wins'] += 1 elif pnl < 0: s['losses'] += 1 s['trades'].append({ 'symbol': r['symbol'], 'side': r['side'], 'pnl': pnl, 'outcome': outcome, 'time': r['timestamp'], }) # 计算胜率 for t in stats: s = stats[t] decided = s['wins'] + s['losses'] s['win_rate'] = (s['wins'] / decided * 100) if decided > 0 else 0 s['avg_pnl'] = (s['total_pnl'] / s['total']) if s['total'] > 0 else 0 return stats def format_trader_rating(trader): """格式化交易员评分(用于推送模板)""" stats = get_trader_stats(trader) if trader not in stats or stats[trader]['total'] < 2: return f"📊 {trader}: 数据不足(信号<2条)" s = stats[trader] win_rate = s['win_rate'] total = s['total'] total_pnl = s['total_pnl'] # 评分等级 if win_rate >= 70: rating = "⭐⭐⭐⭐⭐ 精准" elif win_rate >= 60: rating = "⭐⭐⭐⭐ 可靠" elif win_rate >= 50: rating = "⭐⭐⭐ 一般" elif win_rate >= 40: rating = "⭐⭐ 谨慎" else: rating = "⭐ 高风险" # 最近3笔 recent = s['trades'][:3] recent_str = " → ".join([ f"{t['symbol']}{'+' if t['pnl']>0 else ''}{t['pnl']:.0f}" for t in recent ]) return f"""📊 {trader} 胜率评级: {rating} • 胜率: {win_rate:.0f}%({s['wins']}胜/{s['losses']}负/{total}总) • 总盈亏: {'+' if total_pnl>0 else ''}{total_pnl:.0f} USDT • 最近: {recent_str}""" def get_all_traders_summary(): """获取所有交易员的汇总表""" stats = get_trader_stats() if not stats: return "暂无交易员数据" lines = ["| 交易员 | 胜率 | 总盈亏 | 信号数 |", "|--------|------|--------|--------|"] for t, s in sorted(stats.items(), key=lambda x: x[1]['win_rate'], reverse=True): win_rate = s['win_rate'] total_pnl = s['total_pnl'] emoji = "⭐" * min(5, max(1, int(win_rate / 20))) lines.append( f"| {t} | {emoji} {win_rate:.0f}% | {'+' if total_pnl>0 else ''}{total_pnl:.0f} | {s['total']} |" ) return "\n".join(lines) # ─── 多交易员群体决策 ───────────────────────────────────────────────────── def get_recent_signals_for_symbol(symbol, limit=20): """获取某币种最近的信号 (含 trader)""" conn = init_db() rows = conn.execute(""" SELECT trader, symbol, side, leverage, trader_size, trader_pnl, timestamp FROM confirmed_signals WHERE symbol = ? ORDER BY timestamp DESC LIMIT ? """, (symbol, limit)).fetchall() conn.close() return [dict(r) for r in rows] def get_trader_strength(trader): """获取交易员强度评分 (基于历史胜率)""" stats = get_trader_stats(trader) if isinstance(stats, list) and stats: s = stats[0] return s.get('strength', 1.0) return 1.0 # 默认中等 def multi_trader_decision(symbol, signals, our_side=None, our_contracts=0): """ 多交易员群体决策 signals: 最近的信号列表 [{trader, side, size, pnl, ...}] our_side: 你当前的持仓方向 (long/short/None) our_contracts: 你的持仓张数 返回: {action, reason, group_side, confidence, urgent_reverse, suggest_amount} """ if not signals: return { 'action': 'wait', 'reason': '无信号', 'group_side': None, 'confidence': 0, 'urgent_reverse': False, 'suggest_amount': 0 } long_weight = 0 short_weight = 0 long_size = 0 short_size = 0 trader_votes = {} for s in signals: t = s.get('trader') if t not in trader_votes: trader_votes[t] = s for trader, sig in trader_votes.items(): strength = get_trader_strength(trader) size = float(sig.get('trader_size', 0) or 0) # 权重 = 强度 × log10(规模+1) — 规模按对数缩放, 避免巨鲸一票定 size_weight = max(1.0, __import__('math').log10(size + 1)) side = sig.get('side', '') if side == 'long': long_weight += strength * size_weight long_size += size elif side == 'short': short_weight += strength * size_weight short_size += size total_weight = long_weight + short_weight if total_weight == 0: return { 'action': 'wait', 'reason': '无有效信号', 'group_side': None, 'confidence': 0, 'urgent_reverse': False, 'suggest_amount': 0 } if long_weight > short_weight * 1.5: group_side = 'long' confidence = long_weight / total_weight elif short_weight > long_weight * 1.5: group_side = 'short' confidence = short_weight / total_weight else: return { 'action': 'wait', 'reason': f'群体分歧 long={long_weight:.1f} short={short_weight:.1f}', 'group_side': None, 'confidence': 0.5, 'urgent_reverse': False, 'suggest_amount': 0 } urgent_reverse = False reverse_size = 0 for trader, sig in trader_votes.items(): if sig.get('side') != group_side: sz = float(sig.get('trader_size', 0) or 0) reverse_size += sz if sz > 1000: urgent_reverse = True if reverse_size > 1000: urgent_reverse = True suggest_amount = 0 action = 'hold' reason_parts = [ f'群体{group_side.upper()} (置信度 {confidence*100:.0f}%)', f'long票数={int(long_weight)}, short票数={int(short_weight)}', f'long总规模={long_size:,.0f}, short总规模={short_size:,.0f}', ] if urgent_reverse and our_contracts > 0: action = 'reduce_50%' suggest_amount = round(our_contracts * 0.5, 2) reason_parts.insert(0, f'⚠️ 紧急反向: 反向总规模 {reverse_size:,.0f}') elif our_side is None: action = 'open' suggest_amount = 1 reason_parts.append('你无持仓,建议开仓') elif our_side == group_side: action = 'add' suggest_amount = round(our_contracts * confidence * 0.1, 2) if suggest_amount < 0.01: suggest_amount = 0.01 reason_parts.append(f'顺势加仓 10% (跟群体置信度 {confidence*100:.0f}%)') else: action = 'reduce_50%' suggest_amount = round(our_contracts * 0.5, 2) reason_parts.append(f'你{our_side}与群体{group_side}反向, 减仓 50%') return { 'action': action, 'reason': ' | '.join(reason_parts), 'group_side': group_side, 'confidence': confidence, 'urgent_reverse': urgent_reverse, 'suggest_amount': suggest_amount, 'long_weight': long_weight, 'short_weight': short_weight, 'long_size': long_size, 'short_size': short_size, 'reverse_size': reverse_size, } def format_multi_decision(result): """格式化群体决策输出""" if result['action'] == 'wait': return f"⏸️ 观望: {result['reason']}" emoji = '🟢' if result['group_side'] == 'long' else '🔴' urgent = '🚨 ' if result['urgent_reverse'] else '' action_emoji = { 'open': '🆕 开仓', 'add': '➕ 加仓', 'reduce_50%': '📉 减仓50%', 'hold': '⏸️ 持有', }.get(result['action'], result['action']) msg = f"""{urgent}{emoji} 群体决策: {action_emoji} {result['suggest_amount']} 张 📊 群体方向: {result['group_side'].upper()} (置信度 {result['confidence']*100:.0f}%) 💡 {result['reason']}""" return msg # ─── 持仓锁 (position lock) ──────────────────────────────────────────── import datetime as _dt LOCK_EXPIRY_HOURS = 72 # 3 自然日 = 72h def _now_iso(): return _dt.datetime.now().isoformat(timespec='seconds') def _hours_since(iso_ts): try: ts = _dt.datetime.fromisoformat(iso_ts) except (ValueError, TypeError): return 999999 delta = _dt.datetime.now() - ts return delta.total_seconds() / 3600 def get_lock(symbol): """获取某 symbol 的锁, 返回 dict 或 None""" conn = init_db() row = conn.execute("SELECT * FROM position_locks WHERE symbol=?", (symbol,)).fetchone() conn.close() return dict(row) if row else None def is_lock_expired(lock): """检查锁是否过期 (>= 72h)""" if not lock: return False return _hours_since(lock.get('locked_at', '')) >= LOCK_EXPIRY_HOURS def acquire_lock(symbol, trader, side, reason="", auto=True): """ 锁定 symbol → trader (同方向) 返回 (ok, msg) """ existing = get_lock(symbol) if existing: if existing['trader'] == trader and existing['side'] == side: conn = init_db() conn.execute("UPDATE position_locks SET locked_at=? WHERE symbol=?", (_now_iso(), symbol)) conn.commit() conn.close() return True, f"续期 {symbol} → {trader} ({side})" elif existing['side'] != side: return False, f"{symbol} 已被 {existing['trader']} 锁 ({existing['side']}), 与 {trader} ({side}) 反向, 跳过" else: if is_lock_expired(existing): release_lock(symbol, existing['trader'], auto=True) else: hours = _hours_since(existing['locked_at']) return False, f"{symbol} 已被 {existing['trader']} 锁 ({hours:.1f}h), 需等 {LOCK_EXPIRY_HOURS}h 或手动解锁" conn = init_db() conn.execute(""" INSERT OR REPLACE INTO position_locks (symbol, trader, side, locked_at, reason, auto_locked) VALUES (?, ?, ?, ?, ?, ?) """, (symbol, trader, side, _now_iso(), reason, 1 if auto else 0)) conn.commit() conn.close() return True, f"🔒 锁定 {symbol} → {trader} ({side})" def release_lock(symbol, trader=None, auto=False): """释放锁""" existing = get_lock(symbol) if not existing: return False, f"{symbol} 未锁定" if trader and existing['trader'] != trader and not auto: return False, f"{symbol} 不是 {trader} 锁的, 当前: {existing['trader']}" conn = init_db() conn.execute("DELETE FROM position_locks WHERE symbol=?", (symbol,)) conn.commit() conn.close() return True, f"🔓 解锁 {symbol} (前锁: {existing['trader']} {existing['side']})" def list_locks(): """列出所有锁""" conn = init_db() rows = conn.execute("SELECT * FROM position_locks ORDER BY locked_at DESC").fetchall() conn.close() if not rows: return "📋 暂无持仓锁" lines = ["| 币种 | 锁交易员 | 方向 | 锁定时间 | 已锁时长 |", "|------|----------|------|----------|----------|"] for r in rows: r = dict(r) hours = _hours_since(r['locked_at']) expired = " (已过期)" if is_lock_expired(r) else "" lines.append(f"| {r['symbol']} | {r['trader']} | {r['side']} | {r['locked_at'][:16]} | {hours:.1f}h{expired} |") return "\n".join(lines) def is_real_trader(trader): """ 判断 trader 名字是不是真实来源 (非聚合转发) 聚合转发的 trader 名字: X聚合社区, 聚合, 转发等 """ if not trader: return False bad_patterns = ['聚合', '转发', 'X社区', 'unknown', 'X 聚合', 'X聚合'] for p in bad_patterns: if p in trader: return False return True def check_signal_lock(symbol, trader, side): """ 检查信号是否被锁允许 返回: - (True, None): 通过 - (False, reason): 被锁, 跳过 - (True, msg): 锁变更 (新锁/续期/过期接管) """ # 聚合转发的 trader 不参与锁 — 直接放行 (不锁也不查锁) if not is_real_trader(trader): return True, None existing = get_lock(symbol) if not existing: ok, msg = acquire_lock(symbol, trader, side, reason="新信号自动锁", auto=True) if ok and msg.startswith("🔒"): return True, msg # 首次锁 return ok, msg if not ok else (True, None) if existing['trader'] == trader and existing['side'] == side: acquire_lock(symbol, trader, side, reason="同 trader 同方向续期", auto=True) return True, None if existing['side'] != side: return False, f"{symbol} 已被 {existing['trader']} 锁 ({existing['side']}), 与 {trader} ({side}) 反向, 跳过" if is_lock_expired(existing): release_lock(symbol, existing['trader'], auto=True) ok, msg = acquire_lock(symbol, trader, side, reason=f"前锁 {existing['trader']} 过期, {trader} 接管", auto=True) if ok: return True, f"🔄 {msg}" return False, msg else: hours = _hours_since(existing['locked_at']) return False, f"{symbol} 被 {existing['trader']} 锁 ({hours:.1f}h), 需等 {LOCK_EXPIRY_HOURS}h 或手动解锁" # CLI if __name__ == '__main__': import sys if len(sys.argv) < 2: print("用法:") print(" python3 signal_tracker.py compare 麻吉大哥 HYPE 12000") print(" python3 signal_tracker.py history 麻吉大哥 HYPE") print(" python3 signal_tracker.py record 麻吉大哥 HYPE long 10 12000 70.8 -3500") print(" python3 signal_tracker.py rating 麻吉大哥") print(" python3 signal_tracker.py summary") sys.exit(0) cmd = sys.argv[1] if cmd == 'compare' and len(sys.argv) >= 5: trader = sys.argv[2] symbol = sys.argv[3] size = float(sys.argv[4]) print(format_comparison(trader, symbol, size)) elif cmd == 'history' and len(sys.argv) >= 4: trader = sys.argv[2] symbol = sys.argv[3] trend = get_position_trend(trader, symbol) for t in trend: print(f" {t['timestamp'][:16]} | {t['size']:,.0f} | {t.get('pnl', 0):+.0f} | {t['signal_type']}") elif cmd == 'record' and len(sys.argv) >= 8: trader = sys.argv[2] symbol = sys.argv[3] side = sys.argv[4] leverage = int(sys.argv[5]) size = float(sys.argv[6]) entry = float(sys.argv[7]) pnl = float(sys.argv[8]) if len(sys.argv) > 8 else 0 record_signal(trader, symbol, side, leverage, size, entry, pnl) print(f"✅ 已记录: {trader} {symbol} {side} {leverage}x {size:,.0f} @{entry}") elif cmd == 'rating' and len(sys.argv) >= 3: trader = sys.argv[2] print(format_trader_rating(trader)) elif cmd == 'summary': print(get_all_traders_summary()) elif cmd == 'multi' and len(sys.argv) >= 3: # 群体决策: multi [our_side] [our_contracts] symbol = sys.argv[2] our_side = sys.argv[3] if len(sys.argv) > 3 else None our_contracts = float(sys.argv[4]) if len(sys.argv) > 4 else 0 signals = get_recent_signals_for_symbol(symbol, limit=20) result = multi_trader_decision(symbol, signals, our_side, our_contracts) print(format_multi_decision(result))