From 7b26c373d1a05493b1dab75c2b5795794061da0a Mon Sep 17 00:00:00 2001 From: hmo Date: Mon, 20 Jul 2026 22:05:00 +0800 Subject: [PATCH] fix(reassess): stale_detector timeout 60->240s + batch cap 5/run + GATE_ZONE_SANITY Three issues from zhiwei's strategy report: 1. '37 reassess all timed out (subprocess 60s)': real cause is per-call LLM latency exceeding the 60s per-subprocess limit during the key5-dead/ gateway-unstable window. NOT 'no concurrency control' as reported (60s is per stock, not for the batch). Fixes: per-call timeout 60->240s (LLM cold-start is 20-100s), and cap AUTO_REASSESS batch to 5 stocks per run with remainder continuing next run (was unbounded serial calls that also blew the 120s cron script window). 2. '15 stocks entry-zone center wrongly 97.0': quality gates had no zone-sanity-vs-price check, so bad data (bad quote or LLM template output) could be written freely. New GATE_ZONE_SANITY (CRITICAL): zone center must be within 0.3x-3x of current price. Verified: rejects the exact 97-center-vs-5.69-price corruption, passes legit zones. 3. 'reassess overwrites manual SQL fixes': true by design; with GATE_ZONE_SANITY at write time, reassess can no longer overwrite good values with garbage - invalid writes get rejected + flagged instead. --- deploy/profile-scripts/stale_detector.py | 22 +++++++++----- scripts/check_97_bug.py | 37 ++++++++++++++++++++++++ scripts/check_candidates_schema.py | 20 +++++++++++++ scripts/find_97_rows.py | 24 +++++++++++++++ scripts/test_zone_gate.py | 22 ++++++++++++++ scripts/trace_97_source.py | 19 ++++++++++++ strategy_lifecycle.py | 8 +++++ 7 files changed, 145 insertions(+), 7 deletions(-) create mode 100644 scripts/check_97_bug.py create mode 100644 scripts/check_candidates_schema.py create mode 100644 scripts/find_97_rows.py create mode 100644 scripts/test_zone_gate.py create mode 100644 scripts/trace_97_source.py diff --git a/deploy/profile-scripts/stale_detector.py b/deploy/profile-scripts/stale_detector.py index 93412551..dd86e9b2 100644 --- a/deploy/profile-scripts/stale_detector.py +++ b/deploy/profile-scripts/stale_detector.py @@ -142,7 +142,7 @@ def main(): reassess_scripts.append(code) print(f"[AUTO_REASSESS] {name}({code}) 价{cur_price:.2f}偏离买入区中心{center:.2f} {drift:+.0f}% → 触发重评") if reassess_scripts: - # 调用 per_stock_reassess + # 调用 per_stock_reassess(每轮最多5只,防LLM慢导致整批超时;其余下轮继续) reassess_path = None for p in ['/home/hmo/MoFin/scripts/per_stock_reassess.py', '/home/hmo/.hermes/profiles/position-analyst/scripts/per_stock_reassess.py']: @@ -150,12 +150,20 @@ def main(): reassess_path = p break if reassess_path: - for code in reassess_scripts: - r = subprocess.run(['python3', reassess_path, code], - capture_output=True, text=True, timeout=60) - out = r.stdout.strip()[:200] if r.stdout else "" - err = r.stderr.strip()[:200] if r.stderr else "" - print(f" → {code}: exited={r.returncode} {out}") + MAX_PER_RUN = 5 + batch = reassess_scripts[:MAX_PER_RUN] + if len(reassess_scripts) > MAX_PER_RUN: + print(f"[AUTO_REASSESS] 本轮限{MAX_PER_RUN}只,剩余{len(reassess_scripts)-MAX_PER_RUN}只下轮继续") + for code in batch: + try: + # LLM 重评冷启动 20-100s,60s 必死(37只全灭那次的根因)→ 240s + r = subprocess.run(['python3', reassess_path, code], + capture_output=True, text=True, timeout=240) + out = r.stdout.strip()[:200] if r.stdout else "" + err = r.stderr.strip()[:200] if r.stderr else "" + print(f" → {code}: exited={r.returncode} {out}") + except subprocess.TimeoutExpired: + print(f" → {code}: 超时240s(LLM仍慢),下轮重试") except Exception as e: print(f"[AUTO_REASSESS FAIL] {e}") # ----- 结束 自选股重评 ----- diff --git a/scripts/check_97_bug.py b/scripts/check_97_bug.py new file mode 100644 index 00000000..4422d162 --- /dev/null +++ b/scripts/check_97_bug.py @@ -0,0 +1,37 @@ +import sqlite3 +conn = sqlite3.connect('/home/hmo/MoFin/data/mofin.db') +conn.row_factory = sqlite3.Row + +# 1. 找曾被 zhiwei 修过的 15 只(她提到的几只)当前值 +codes = ['000711', '603766', '600617', '688271'] +print('=== holding_strategies 当前值(她修过的几只)===') +for code in codes: + r = conn.execute( + "SELECT code, name, entry_low, entry_high, stop_loss, take_profit, strategy_type, " + "quality_check, created_at, updated_at FROM holding_strategies WHERE code=? AND status='active'", + (code,)).fetchone() + if r: + c = (r['entry_low'] + r['entry_high']) / 2 if r['entry_low'] and r['entry_high'] else 0 + print(f" {r['code']} {r['name']}: 区{r['entry_low']}~{r['entry_high']} 中心{c:.2f} | type={r['strategy_type']} | created={r['created_at']} updated={r['updated_at']}") + +# 2. 找 candidates 里这些股票的评分/买入区(看是不是 promote 写进来的) +print() +print('=== candidates 对应记录 ===') +for code in codes: + r = conn.execute( + "SELECT code, name, score, entry_low, entry_high, stop_loss, take_profit, promoted, created_at " + "FROM candidates WHERE code=?", (code,)).fetchone() + if r: + print(f" {r['code']} {r['name']}: score={r['score']} 区{r['entry_low']}~{r['entry_high']} promoted={r['promoted']} created={r['created_at']}") + +# 3. 还有谁可能是 97 中心:找 entry 中心在 90~105 的活跃自选 +print() +print('=== 当前活跃自选里中心 90~105 的(可能还有漏网的)===') +rows = conn.execute( + "SELECT code, name, entry_low, entry_high FROM holding_strategies " + "WHERE status='active' AND decision_type='自选策略' AND entry_low > 0").fetchall() +for r in rows: + c = (r['entry_low'] + r['entry_high']) / 2 + if 90 <= c <= 105: + print(f" {r['code']} {r['name']}: 区{r['entry_low']}~{r['entry_high']} 中心{c:.2f}") +conn.close() \ No newline at end of file diff --git a/scripts/check_candidates_schema.py b/scripts/check_candidates_schema.py new file mode 100644 index 00000000..21747099 --- /dev/null +++ b/scripts/check_candidates_schema.py @@ -0,0 +1,20 @@ +import sqlite3 +conn = sqlite3.connect('/home/hmo/MoFin/data/mofin.db') +conn.row_factory = sqlite3.Row + +# candidates 表结构 +cols = [r[1] for r in conn.execute("PRAGMA table_info(candidates)")] +print('candidates cols:', cols) +print() + +codes = ['000711', '603766', '600617', '688271'] +print('=== candidates 对应记录 ===') +for code in codes: + r = conn.execute("SELECT * FROM candidates WHERE code=?", (code,)).fetchone() + if r: + d = dict(r) + keys = [k for k in d.keys() if any(s in k.lower() for s in ['score', 'entry', 'stop', 'take', 'promot', 'created', 'name', 'code'])] + print(f" {code}: " + ' | '.join(f'{k}={d[k]}' for k in keys)) + else: + print(f' {code}: 无记录') +conn.close() \ No newline at end of file diff --git a/scripts/find_97_rows.py b/scripts/find_97_rows.py new file mode 100644 index 00000000..efb5c970 --- /dev/null +++ b/scripts/find_97_rows.py @@ -0,0 +1,24 @@ +import sqlite3 +conn = sqlite3.connect('/home/hmo/MoFin/data/mofin.db') +conn.row_factory = sqlite3.Row + +print('=== 中心 85~110 的活跃自选/持仓 ===') +rows = conn.execute( + "SELECT code, name, entry_low, entry_high, decision_type, strategy_type, created_at, updated_at " + "FROM holding_strategies WHERE status='active' AND entry_low > 0").fetchall() +found = 0 +for r in rows: + c = (r['entry_low'] + r['entry_high']) / 2 + if 85 <= c <= 110: + print(f" {r['code']} {r['name']}: 区{r['entry_low']}~{r['entry_high']} 中心{c:.2f} | {r['decision_type']}/{r['strategy_type']} | created={r['created_at']} updated={r['updated_at']}") + found += 1 +print(f'共 {found} 只') +print() + +# holding_strategies 表结构 + 默认值 +print('=== holding_strategies schema ===') +sql = conn.execute("SELECT sql FROM sqlite_master WHERE name='holding_strategies'").fetchone()[0] +for line in sql.split('\n'): + if '97' in line or 'DEFAULT' in line.upper(): + print(' ', line.strip()) +conn.close() \ No newline at end of file diff --git a/scripts/test_zone_gate.py b/scripts/test_zone_gate.py new file mode 100644 index 00000000..70ef36c6 --- /dev/null +++ b/scripts/test_zone_gate.py @@ -0,0 +1,22 @@ +import sys +sys.path.insert(0, '/home/hmo/.hermes/profiles/position-analyst/scripts') +sys.path.insert(0, '/home/hmo/MoFin') +from strategy_lifecycle import validate_strategy + +# 模拟 97 中心坏数据(5元股票被写成中心97) +bad = {'code': '000711', 'price': 5.69, 'entry_low': 94.0, 'entry_high': 100.0, + 'stop_loss': 90.0, 'take_profit': 110.0, 'timing_signal': '买入', + 'rr_ratio': 2.0, 'tech_snapshot': '强撑94 弱撑95 弱压99 强压100', + 'sector_context': '环保', 'signal_factors': ['x'], 'currency': 'CNY'} +passed, failures = validate_strategy(bad) +print('坏数据(中心97 vs 价5.69): passed =', passed) +for f in failures: + print(' FAIL:', f.get('id'), '-', f.get('desc', '')[:60]) + +# 正常数据 +good = dict(bad) +good['entry_low'], good['entry_high'], good['stop_loss'], good['take_profit'] = 5.23, 6.15, 5.0, 7.0 +passed2, failures2 = validate_strategy(good) +print('好数据(区5.23~6.15): passed =', passed2) +for f in failures2: + print(' FAIL:', f.get('id'), '-', f.get('desc', '')[:60]) \ No newline at end of file diff --git a/scripts/trace_97_source.py b/scripts/trace_97_source.py new file mode 100644 index 00000000..1e9d2d4a --- /dev/null +++ b/scripts/trace_97_source.py @@ -0,0 +1,19 @@ +import sqlite3 +conn = sqlite3.connect('/home/hmo/MoFin/data/mofin.db') +conn.row_factory = sqlite3.Row + +codes = ['000711', '603766', '600617', '688271', '688608'] +for code in codes: + r = conn.execute( + "SELECT code, name, entry_low, entry_high, substr(full_analysis,1,1200) as fa, " + "reassessed_at, updated_at FROM holding_strategies WHERE code=? AND status='active'", + (code,)).fetchone() + if r: + print(f"=== {r['code']} {r['name']} 区{r['entry_low']}~{r['entry_high']} updated={r['updated_at']} reassessed={r['reassessed_at']}") + fa = r['fa'] or '' + # 找买入区间相关行 + for line in fa.split('\n'): + if any(k in line for k in ['买入区间', '止损', '止盈', '综合结论']): + print(' ', line[:120]) + print() +conn.close() \ No newline at end of file diff --git a/strategy_lifecycle.py b/strategy_lifecycle.py index 12dce8e9..b0f0867e 100644 --- a/strategy_lifecycle.py +++ b/strategy_lifecycle.py @@ -90,6 +90,14 @@ STRATEGY_QUALITY_GATES = [ "severity": "HIGH", "fix": "设置 d['currency']='HKD'" }, + { + "id": "GATE_ZONE_SANITY", + "desc": "买入区中心不得偏离现价超过3倍(防坏行情/LLM幻觉注入异常值,如15只股被统一写成97的教训)", + "check": lambda d: (lambda p, c: p <= 0 or c <= 0 or (p >= c * 0.3 and p <= c * 3.0))( + d.get("price") or 0, ((d.get("entry_low") or 0) + (d.get("entry_high") or 0)) / 2), + "severity": "CRITICAL", + "fix": "买入区与现价偏离超3倍,疑似坏数据。用 technical_analysis 重算支撑阻力或人工修正" + }, # --- 第4条 CRITICAL 红线:9维交叉验证 (2026-07-02 Dad要求) --- # 策略不能只有价格数字,必须有证据经过了多维分析: # 横切面: 大盘+行业+个股 | 纵切面: 基本面+消息面+技术面+资金流