#!/usr/bin/env python3 """resonance.py — 三维共振层(技术×资金×消息) 在 v7.1 闸门通过后,对买入信号做三维合成判断: 否决(veto): 资金持续流出 + 消息利空(双重负面) 降级(downgrade): 任一单维度负面 共振(resonance): 资金强流入 + 消息利好 通过(pass): 其余 所有判断写入 signal_veto_log 供后续回测验证(2026-07-29 老爸批准)""" import sqlite3, json, sys from datetime import datetime, timedelta DB = "/home/hmo/MoFin/data/mofin.db" for p in ("/home/hmo/MoFin", "/home/hmo/web-dashboard"): if p not in sys.path: sys.path.insert(0, p) NEWS_NEGATIVE = {'利空', '偏空', '中性偏空'} NEWS_POSITIVE = {'利好', '偏多', '中性偏利好'} FLOW_NEG_THRESHOLD = -2.5 # v6归因:持续流出胜率仅37.5% FLOW_STRONG_POS_THRESHOLD = 4 # v6归因:加速流入胜率77.8% def _flow_state(code): """资金维度: flow_5d / flow_delta → negative/neutral/positive/strong_positive""" try: conn = sqlite3.connect(DB) rows = conn.execute(""" SELECT date, main_pct FROM stock_capital_flow WHERE code=? ORDER BY date DESC LIMIT 10 """, (code,)).fetchall() conn.close() except sqlite3.OperationalError: return {'state': 'unknown', 'flow_5d': None, 'flow_delta': None} if len(rows) < 5: return {'state': 'unknown', 'flow_5d': None, 'flow_delta': None} recent = [r[1] for r in rows[:5] if r[1] is not None] prior = [r[1] for r in rows[5:10] if r[1] is not None] flow_5d = sum(recent) / len(recent) if recent else None flow_delta = None if recent and prior: flow_delta = flow_5d - sum(prior) / len(prior) state = 'neutral' if flow_5d is not None and flow_5d < FLOW_NEG_THRESHOLD: state = 'negative' elif flow_delta is not None and flow_delta > FLOW_STRONG_POS_THRESHOLD: state = 'strong_positive' elif flow_5d is not None and flow_5d > 1: state = 'positive' return {'state': state, 'flow_5d': round(flow_5d, 2) if flow_5d is not None else None, 'flow_delta': round(flow_delta, 2) if flow_delta is not None else None} def _news_state(code, name=None, sector=None): """消息维度: 3日内该股/该板块最新消息情绪""" since = (datetime.now() - timedelta(days=3)).strftime('%Y-%m-%d') try: conn = sqlite3.connect(DB) # 个股级优先(searched_stocks 或 summary 含代码/名称) rows = conn.execute(""" SELECT overall_sentiment, summary, sector, created_at FROM signal_news WHERE created_at >= ? AND ( searched_stocks LIKE ? OR summary LIKE ? OR sector = ? ) ORDER BY id DESC LIMIT 10 """, (since, f'%{code}%', f'%{name or code}%', sector or '')).fetchall() conn.close() except sqlite3.OperationalError: return {'state': 'unknown', 'sentiment': None, 'summary': None} sentiment = None summary = None for s, sm, sec, ts in rows: if s in NEWS_NEGATIVE or s in NEWS_POSITIVE: sentiment = s summary = (sm or '')[:120] break if sentiment is None: return {'state': 'neutral', 'sentiment': None, 'summary': None} state = 'negative' if sentiment in NEWS_NEGATIVE else 'positive' return {'state': state, 'sentiment': sentiment, 'summary': summary} def evaluate_resonance(code, gate, name=None): """三维合成判断。gate = v71_gate.check_entry_gate 的返回""" flow = _flow_state(code) # 板块名:从 gate factors 拿不到名字,这里用 sector_ctx 的映射 sector = None try: from strategy_lab import _STOCK_SECTOR sector = _STOCK_SECTOR.get(code) except Exception: pass news = _news_state(code, name, sector) fs, ns = flow['state'], news['state'] if fs == 'negative' and ns == 'negative': decision = 'veto' reason = f"资金持续流出({flow['flow_5d']}) + 消息利空({news['sentiment']})" elif fs == 'negative': decision = 'downgrade' reason = f"资金持续流出({flow['flow_5d']})" elif ns == 'negative': decision = 'downgrade' reason = f"消息利空({news['sentiment']})" elif fs == 'strong_positive' and ns == 'positive': decision = 'resonance' reason = f"三维共振: 资金加速流入({flow['flow_delta']}) + 消息{news['sentiment']}" else: decision = 'pass' reason = '' return {'decision': decision, 'reason': reason, 'flow': flow, 'news': news, 'sector': sector} def log_resonance(code, name, price, gate, res, signal_before, signal_after): """完整记录三维状态 → signal_veto_log(后续回测验证的数据资产)""" conn = sqlite3.connect(DB) conn.execute(""" CREATE TABLE IF NOT EXISTS signal_veto_log ( id INTEGER PRIMARY KEY AUTOINCREMENT, code TEXT, name TEXT, price REAL, tech_score INTEGER, tech_summary TEXT, flow_5d REAL, flow_delta REAL, flow_state TEXT, news_sentiment TEXT, news_state TEXT, news_summary TEXT, sector TEXT, decision TEXT, reason TEXT, signal_before TEXT, signal_after TEXT, created_at TEXT ) """) conn.execute(""" INSERT INTO signal_veto_log (code, name, price, tech_score, tech_summary, flow_5d, flow_delta, flow_state, news_sentiment, news_state, news_summary, sector, decision, reason, signal_before, signal_after, created_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) """, (code, name, price, gate.get('score'), gate.get('summary'), res['flow'].get('flow_5d'), res['flow'].get('flow_delta'), res['flow'].get('state'), res['news'].get('sentiment'), res['news'].get('state'), res['news'].get('summary'), res.get('sector'), res['decision'], res['reason'], signal_before, signal_after, datetime.now().strftime('%Y-%m-%d %H:%M:%S'))) conn.commit() conn.close() if __name__ == '__main__': from v71_gate import check_entry_gate for c in sys.argv[1:] or ['603599']: g = check_entry_gate(c) r = evaluate_resonance(c, g) print(f"{c}: gate={g['pass']} | 资金={r['flow']['state']}({r['flow']['flow_5d']}) " f"消息={r['news']['state']}({r['news']['sentiment']}) → {r['decision']} {r['reason']}")