From 25812bb9057981f1434a71988854e5fc1c39004e Mon Sep 17 00:00:00 2001 From: mike Date: Thu, 23 Jul 2026 22:09:16 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=8C=81=E4=BB=93=E9=94=81=20=E2=80=94?= =?UTF-8?q?=20=E5=90=8C=E5=B8=81=E7=A7=8D=E5=8F=AA=E8=B7=9F=E4=B8=80?= =?UTF-8?q?=E4=B8=AA=20trader?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 【背景】之前 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 → 麻吉大哥 (手动加锁) --- okx-auto-position/scripts/process_signal.py | 24 ++- okx-auto-position/scripts/signal_tracker.py | 164 ++++++++++++++++++++ 2 files changed, 181 insertions(+), 7 deletions(-) diff --git a/okx-auto-position/scripts/process_signal.py b/okx-auto-position/scripts/process_signal.py index 67c46ee..77d781f 100644 --- a/okx-auto-position/scripts/process_signal.py +++ b/okx-auto-position/scripts/process_signal.py @@ -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: diff --git a/okx-auto-position/scripts/signal_tracker.py b/okx-auto-position/scripts/signal_tracker.py index b524d28..3605e56 100644 --- a/okx-auto-position/scripts/signal_tracker.py +++ b/okx-auto-position/scripts/signal_tracker.py @@ -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__':