From 96a1e6b413d33695bcf5038f1be147fa14ea65d2 Mon Sep 17 00:00:00 2001 From: claude_dev Date: Mon, 24 Aug 2026 13:50:40 +0800 Subject: [PATCH] =?UTF-8?q?tools(ops):=20=E5=AD=98=E9=87=8Fmax=5Fpool=2030?= =?UTF-8?q?=E2=86=920=E8=BF=81=E7=A7=BB=E8=84=9A=E6=9C=AC(=E9=BB=98?= =?UTF-8?q?=E8=AE=A4dry-run,--apply=E6=89=8D=E5=8A=A8=E6=89=8B,=E5=B9=82?= =?UTF-8?q?=E7=AD=89)=E2=80=94=E2=80=94=E7=AD=96=E7=95=A5session=2008-24?= =?UTF-8?q?=E7=A7=BB=E4=BA=A4=E9=A1=B9#1=E5=87=86=E5=A4=87=E4=BB=B6=20[nas?= =?UTF-8?q?]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 真实盘点(只读): VPS prod=live_accounts全6行30+paper_accounts.strategies 6处JSON30(=影子实例);NAS=paper_accounts 1处JSON30,live_accounts空。 ⚠️刻意排除backtest_stats.params里的3处30=历史回测运行记录,改写=伪造历史(JSON白名单只放paper_accounts活动配置面)。 ⚠️执行铁律:只与数据session SQL治本(80fc517已落)+f214e2f同车,部署成功后、引擎重启前跑——池放大×慢SQL=更糟;迁移后重启引擎吃新值。 冒烟三态过:dry-run盘点/apply迁移(30→0两行,显式50保留,JSON只动max_pool键)/二次apply零行幂等+历史表原样。 --- scripts/ops/migrate_max_pool_30_to_0.py | 113 ++++++++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100644 scripts/ops/migrate_max_pool_30_to_0.py diff --git a/scripts/ops/migrate_max_pool_30_to_0.py b/scripts/ops/migrate_max_pool_30_to_0.py new file mode 100644 index 0000000..817d158 --- /dev/null +++ b/scripts/ops/migrate_max_pool_30_to_0.py @@ -0,0 +1,113 @@ +#!/usr/bin/env python3 +"""一次性迁移: 存量实例 max_pool=30 → 0(0=不限) — 2026-08-24 选股池失真处置。 + +背景: SANGUO_LIVE_MAX_POOL 默认 30(MVP 验证遗留)把全部实盘/影子/paper 实例的 +选股池截断成「代码序前 30 只」(f214e2f 已改全链默认 30→0)。代码侧默认已翻转, +但存量 DB 行存的是显式 30 → 需本迁移激活。 + +⚠️ 部署铁律(策略 session 移交 #1): 必须与数据 session 的 +get_security_info_batch SQL 治本(冷跑 101s → 亚秒)同车执行—— +池放大 × 慢 SQL = 更糟。 + +覆盖面: + 1. live_accounts.max_pool = 30 → 0 (VPS prod: 全部 6 实例) + 2. paper_accounts.strategies JSON 里的 "max_pool": 30 → 0 + (VPS=6 影子实例,NAS=1 paper 实例) + ⚠️ 刻意排除 backtest_stats.params 里的 30——那是历史回测运行记录, + 当时就是用 30 跑的,改写=伪造历史(2026-08-24 实盘点: VPS 命中 9 处中 + 3 处属 backtest_stats,均已保留)。JSON 白名单只放活动配置面 paper_accounts。 + +用法: + python migrate_max_pool_30_to_0.py # dry-run 只盘点 + python migrate_max_pool_30_to_0.py --apply # 实际执行 +幂等: WHERE max_pool=30 天然幂等; JSON 侧 30→0 后不再命中。 +""" +import re +import sqlite3 +import sys + +# JSON 迁移白名单: 只放「活动实例配置」面。backtest_stats 等历史记录刻意不在内(见 docstring)。 +JSON_TABLES = ("paper_accounts",) + + +def scan(db_path: str) -> dict: + """盘点: live_accounts.max_pool 分布 + params JSON 里的 max_pool=30。""" + con = sqlite3.connect(f"file:{db_path}?mode=ro", uri=True, timeout=30) + con.row_factory = sqlite3.Row + out = {"live_accounts_dist": [], "json_hits": []} + try: + dist = con.execute( + "SELECT max_pool, COUNT(*) n FROM live_accounts GROUP BY max_pool" + ).fetchall() + out["live_accounts_dist"] = [(r["max_pool"], r["n"]) for r in dist] + pat = re.compile(r'"max_pool"\s*:\s*30\b') + for t in JSON_TABLES: + cols = [r[1] for r in con.execute(f"PRAGMA table_info({t})").fetchall()] + for c in cols: + try: + rows = con.execute( + f"SELECT rowid, {c} FROM {t} WHERE {c} LIKE '%max_pool%'" + ).fetchall() + except sqlite3.OperationalError: + continue + for r in rows: + v = r[1] + if isinstance(v, str) and pat.search(v): + out["json_hits"].append((t, c, r[0])) + finally: + con.close() + return out + + +def apply(db_path: str) -> None: + con = sqlite3.connect(db_path, timeout=30) + try: + cur = con.execute("UPDATE live_accounts SET max_pool=0 WHERE max_pool=30") + n_acct = cur.rowcount + n_json = 0 + # JSON 侧: 同 scan 的白名单命中面, 逐条 30→0(正则替换保其余结构不动) + pat = re.compile(r'("max_pool"\s*:\s*)30\b') + for t in JSON_TABLES: + cols = [r[1] for r in con.execute(f"PRAGMA table_info({t})").fetchall()] + for c in cols: + try: + rows = con.execute( + f"SELECT rowid, {c} FROM {t} WHERE {c} LIKE '%max_pool%'" + ).fetchall() + except sqlite3.OperationalError: + continue + for rowid, v in rows: + if isinstance(v, str) and pat.search(v): + nv = pat.sub(r"\g<1>0", v) + con.execute( + f"UPDATE {t} SET {c}=? WHERE rowid=?", (nv, rowid)) + n_json += 1 + con.commit() + print(f"✅ 已迁移: live_accounts {n_acct} 行 30→0; params JSON {n_json} 处 30→0") + finally: + con.close() + + +def main() -> None: + if len(sys.argv) < 2 or sys.argv[1] in ("-h", "--help"): + print(__doc__) + sys.exit(0) + db = sys.argv[1] + do_apply = "--apply" in sys.argv[2:] + s = scan(db) + print(f"DB: {db}") + print(f"live_accounts max_pool 分布: {s['live_accounts_dist']}") + print(f"params JSON 含 max_pool:30 的行: {len(s['json_hits'])} {s['json_hits'][:10]}") + if not do_apply: + print("(dry-run,加 --apply 执行迁移)") + return + if not s["live_accounts_dist"] and not s["json_hits"]: + print("无可迁移项(可能已迁移过)") + return + apply(db) + s2 = scan(db) + print(f"迁移后分布: {s2['live_accounts_dist']}; JSON 残留: {len(s2['json_hits'])}") + + +if __name__ == "__main__": + main()