Files
Hermes-Skills/okx-auto-position/scripts/signal_tracker.py
T
mike b22905a5f3 feat: 多交易员群体决策 multi_trader_decision
【功能】同币种多交易员时,用加权群体决策替代单信号跟随

【算法】
1. 收集币种最近 20 个信号,按 trader 去重
2. 每个交易员按 strength 权重 + size 加权
3. long/short 票数比 > 1.5x 才确认群体方向
4. 反向信号总规模 > 1000 或单交易员 > 1000 触发紧急反向
5. 顺势加 10% / 反向减 50% / 无持仓开仓 1 张

【用法】python3 signal_tracker.py multi <symbol> [our_side] [our_contracts]

【测试】4 场景全过: 顺势加/反向减/无持仓开/分歧观望 
2026-07-23 21:39:22 +08:00

531 lines
18 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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 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);
""")
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)
# 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 <symbol> [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))
# ─── 多交易员群体决策 ─────────────────────────────────────────────────────
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
}
# 1. 计算群体方向 + 加权
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)
side = sig.get('side', '')
if side == 'long':
long_weight += strength
long_size += size
elif side == 'short':
short_weight += strength
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
}
# 2. 判定群体方向 (1.5x 阈值才确认)
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
}
# 3. 紧急反向检测: 大规模反向信号 (>1000 BTC 或总反向 > 1000)
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
# 4. 决定动作
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'
# 顺势加 10% 仓位
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: # our_side != group_side
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