From 4fad5696dfe76329b8ab568ec7cfbfc03f638c80 Mon Sep 17 00:00:00 2001 From: dekun Date: Sat, 6 Jun 2026 09:40:14 +0800 Subject: [PATCH] fix(gate_bot): exclude active trend plans from orphan position warning Trend pullback plans manage positions before order_monitors handoff; treat them as covered and add a pre-deploy DB backup script. Co-authored-by: Cursor --- crypto_monitor_gate_bot/app.py | 49 +++++++++++-- .../scripts/backup_db_now.py | 69 +++++++++++++++++++ 2 files changed, 114 insertions(+), 4 deletions(-) create mode 100644 crypto_monitor_gate_bot/scripts/backup_db_now.py diff --git a/crypto_monitor_gate_bot/app.py b/crypto_monitor_gate_bot/app.py index 2af65d5..7d7760c 100644 --- a/crypto_monitor_gate_bot/app.py +++ b/crypto_monitor_gate_bot/app.py @@ -3632,8 +3632,36 @@ def _active_monitor_position_keys(active_orders): return covered -def collect_orphan_exchange_positions(active_orders): - """交易所有持仓但未匹配 order_monitors.status=active(与中控 Agent 持仓对齐提示)。""" +def _active_trend_plan_position_keys(conn): + """运行中的趋势回调计划已开仓时,持仓由计划表管理而非 order_monitors。""" + covered = set() + if conn is None: + return covered + try: + rows = conn.execute( + "SELECT symbol, exchange_symbol, direction FROM trend_pullback_plans " + "WHERE status='active' AND COALESCE(first_order_done, 0) != 0" + ).fetchall() + except Exception: + return covered + for r in rows: + sym = (r["symbol"] or "").strip() + ex = (r["exchange_symbol"] or normalize_exchange_symbol(sym)).strip() + direction = (r["direction"] or "long").lower() + for s in (ex, sym, _unified_symbol_for_match(ex), _unified_symbol_for_match(sym)): + if s: + covered.add((s, direction)) + return covered + + +def _strategy_managed_position_keys(active_orders, conn=None): + covered = _active_monitor_position_keys(active_orders) + covered |= _active_trend_plan_position_keys(conn) + return covered + + +def collect_orphan_exchange_positions(active_orders, conn=None): + """交易所有持仓但未匹配本地策略/监控(order_monitors 或运行中趋势计划)。""" from hub_position_metrics import ( parse_position_mark_price, position_contracts, @@ -3643,7 +3671,7 @@ def collect_orphan_exchange_positions(active_orders): rows = _fetch_all_swap_positions_live() if not rows: return [] - covered = _active_monitor_position_keys(active_orders) + covered = _strategy_managed_position_keys(active_orders, conn) orphans = [] seen = set() for p in rows: @@ -5639,7 +5667,7 @@ def render_main_page(page="trade"): orphan_positions: list = [] if page == "trade": try: - orphan_positions = collect_orphan_exchange_positions(order_list) + orphan_positions = collect_orphan_exchange_positions(order_list, conn) except Exception as exc: print(f"[render_main_page] orphan positions: {exc}") conn.close() @@ -6160,6 +6188,19 @@ def api_order_relink_orphan(): if active: conn.close() return jsonify({"ok": True, "msg": "已有运行中的监控", "order_id": int(active["id"])}) + trend_plan = conn.execute( + "SELECT id FROM trend_pullback_plans WHERE status='active' AND symbol=? AND direction=? " + "AND COALESCE(first_order_done, 0) != 0 LIMIT 1", + (symbol, direction), + ).fetchone() + if trend_plan: + conn.close() + return jsonify( + { + "ok": False, + "msg": f"该持仓由趋势回调计划 #{int(trend_plan['id'])} 管理,请在策略页操作", + } + ), 400 row = conn.execute( """ SELECT * FROM order_monitors diff --git a/crypto_monitor_gate_bot/scripts/backup_db_now.py b/crypto_monitor_gate_bot/scripts/backup_db_now.py new file mode 100644 index 0000000..38c6ccd --- /dev/null +++ b/crypto_monitor_gate_bot/scripts/backup_db_now.py @@ -0,0 +1,69 @@ +#!/usr/bin/env python3 +"""One-shot SQLite backup before code deploy. Reads DB_PATH from .env (default crypto.db).""" +from __future__ import annotations + +import os +import shutil +import sqlite3 +from datetime import datetime +from pathlib import Path + +PROJECT_DIR = Path(__file__).resolve().parent.parent + + +def _read_env_db_path() -> Path: + env_file = PROJECT_DIR / ".env" + default = PROJECT_DIR / "crypto.db" + if not env_file.is_file(): + return default + for line in env_file.read_text(encoding="utf-8", errors="replace").splitlines(): + line = line.strip() + if not line or line.startswith("#") or "=" not in line: + continue + key, val = line.split("=", 1) + if key.strip() != "DB_PATH": + continue + val = val.strip().strip('"').strip("'") + p = Path(val) + return p if p.is_absolute() else PROJECT_DIR / p + return default + + +def main() -> int: + db_path = _read_env_db_path() + if not db_path.is_file(): + print(f"error: database not found: {db_path}") + return 1 + stamp = datetime.now().strftime("%Y%m%d_%H%M%S") + dest_dir = PROJECT_DIR / "backups" / stamp + dest_dir.mkdir(parents=True, exist_ok=True) + dest = dest_dir / db_path.name + try: + src = sqlite3.connect(str(db_path)) + dst = sqlite3.connect(str(dest)) + src.backup(dst) + dst.close() + src.close() + method = "sqlite3 backup" + except Exception: + shutil.copy2(db_path, dest) + method = "file copy" + manifest = dest_dir / "manifest.txt" + manifest.write_text( + "\n".join( + [ + f"project_dir={PROJECT_DIR}", + f"source_db={db_path}", + f"backup_file={dest}", + f"method={method}", + f"created_at={stamp}", + ] + ), + encoding="utf-8", + ) + print(f"ok: {dest} ({method})") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main())