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 → 麻吉大哥 (手动加锁)
This commit is contained in:
2026-07-23 22:09:16 +08:00
parent efbea5448c
commit 25812bb905
2 changed files with 181 additions and 7 deletions
+17 -7
View File
@@ -30,7 +30,7 @@ DEDUP_DB = Path.home() / ".hermes/trading/signal_dedup.db"
# Import signal tracker
sys.path.insert(0, str(SKILL_DIR / "scripts"))
from signal_tracker import format_comparison, record_signal as _tracker_record, record_confirmed, format_trader_rating, get_last_signal
from signal_tracker import format_comparison, record_signal as _tracker_record, record_confirmed, format_trader_rating, get_last_signal, check_signal_lock
# ─── 解析 ────────────────────────────────────────────────────────────────
@@ -241,6 +241,16 @@ def format_message(fields, rec, signal_type):
if 'error' in rec:
return f"⚠️ advisor错误: {rec['error']}"
symbol = fields.get('symbol', '?')
side_cn = fields.get('side', '做多')
side_en = fields.get('side_en', 'long' if side_cn == '做多' else 'short')
trader = fields.get('trader', '?')
# 锁检查: 同币种只跟一个 trader
allowed, lock_msg = check_signal_lock(symbol, trader, side_en)
if not allowed:
return f"🔒 {lock_msg}"
def _fmt(x, n=4):
"""格式化数字: 字符串保留原样, 数字 round 到 n 位."""
try:
@@ -248,11 +258,8 @@ def format_message(fields, rec, signal_type):
except (ValueError, TypeError):
return str(x)
symbol = fields.get('symbol', '?')
side_cn = fields.get('side', '做多')
emoji = '🟩' if fields.get('side_en') == 'long' else '🟥'
leverage = fields.get('leverage', '10')
trader = fields.get('trader', '?')
size = fields.get('size', '?')
unit = fields.get('unit', '') # 仓位单位: BTC/USDT/张
value = fields.get('value', '?')
@@ -297,10 +304,13 @@ def format_message(fields, rec, signal_type):
our_position = None
our_advice = ""
try:
from okx_position_advisor import get_positions
positions = get_positions(symbol=symbol)
import ccxt
from okx_position_advisor import load_credentials, create_exchange
creds = load_credentials()
ex = create_exchange(creds)
positions = ex.fetch_positions()
for p in positions:
if p.get('symbol') == f"{symbol}/USDT:USDT" or p.get('symbol') == symbol:
if symbol in p.get('symbol', '') and p.get('contracts', 0) != 0:
our_position = p
break
if our_position and current_size > 0:
+164
View File
@@ -50,11 +50,23 @@ def init_db():
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
@@ -471,6 +483,158 @@ def format_multi_decision(result):
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__':