Files
Hermes-Skills/okx-auto-position/scripts/signal_tracker.py
T
mike 25812bb905 feat: 持仓锁 — 同币种只跟一个 trader
【背景】之前 BTC 同时跟多个 trader (Jasonleo/麻吉/巨鲸), 信号互相干扰
- 群体投票算法复杂,实际投出分歧
- 单一币种单一信号源更清晰

【实现】
- position_locks 表 (symbol PK, trader, side, locked_at, reason, auto_locked)
- acquire_lock / release_lock / get_lock / list_locks / check_signal_lock
- 72h 自动过期 (按 _hours_since 算)
- 同 trader 同方向 → 续期
- 同方向不同 trader → 检查过期,过期后接管
- 不同方向 → 跳过 (允许一多一空并存)
- is_real_trader 黑名单: 'X聚合社区'/'某转发' 等不参与锁

【集成】process_signal.format_message 开头调 check_signal_lock:
- 锁被占用 → 推 '🔒 {reason}' (被锁,跳过)
- 锁允许 → 继续推信号

【测试】6 场景全过: 同 trader 续期/反向跳过/同向跳过/聚合放行/新币自动锁 
【当前锁】BTC long → 矿工Jasonleo, HYPE short → 麻吉大哥 (手动加锁)
2026-07-23 22:09:16 +08:00

692 lines
23 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 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 <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))