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:
@@ -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:
|
||||
|
||||
@@ -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__':
|
||||
|
||||
Reference in New Issue
Block a user