diff --git a/deploy/profile-scripts/resonance.py b/deploy/profile-scripts/resonance.py new file mode 100644 index 00000000..6106ddbb --- /dev/null +++ b/deploy/profile-scripts/resonance.py @@ -0,0 +1,153 @@ +#!/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']}") diff --git a/deploy/profile-scripts/strategy_lifecycle.py b/deploy/profile-scripts/strategy_lifecycle.py index 562cb782..b90ffe56 100644 --- a/deploy/profile-scripts/strategy_lifecycle.py +++ b/deploy/profile-scripts/strategy_lifecycle.py @@ -1474,6 +1474,30 @@ def reassess_strategy(code, name, price, cost, shares, current_action, except Exception as _e: print(f" [v7.1闸门] 评估异常(放行): {_e}", flush=True) + # ----- 【三维共振层】技术×资金×消息合成判断(2026-07-29 老爸批准,全程记录) ----- + _res_decision = None + if is_new_entry and any(s in timing_signal for s in ("买入", "加仓", "可追")): + try: + from resonance import evaluate_resonance, log_resonance + _res = evaluate_resonance(code, _gate if '_gate' in dir() and _gate else {'score': 0, 'summary': ''}, name) + _res_decision = _res["decision"] + _sig_before = timing_signal + if _res_decision == "veto": + timing_signal = "观望" + action_note = (action_note + " | 三维否决: " + _res["reason"]) if action_note else ("三维否决: " + _res["reason"]) + elif _res_decision == "downgrade": + timing_signal = "关注" + action_note = (action_note + " | 三维降级: " + _res["reason"]) if action_note else ("三维降级: " + _res["reason"]) + elif _res_decision == "resonance": + action_note = (action_note + " | " + _res["reason"]) if action_note else _res["reason"] + log_resonance(code, name, price, + _gate if '_gate' in dir() and _gate else {'score': 0, 'summary': ''}, + _res, _sig_before, timing_signal) + if _res_decision != "pass": + print(f" [三维共振] {_sig_before}→{timing_signal}: {_res['reason']}", flush=True) + except Exception as _e: + print(f" [三维共振] 评估异常(放行): {_e}", flush=True) + # ----- 构造 action 描述(供 cron prompt 使用) ----- action_parts = [] # 非持仓(自选股/未持有,shares=0):盈亏标签无意义——cost=0 → profit_pct=0 → diff --git a/static/index.html b/static/index.html index a2bccd07..535c7eb9 100644 --- a/static/index.html +++ b/static/index.html @@ -1969,6 +1969,17 @@ function renderStrategyTable(strategies) { if (pf.cagr_pct != null) best.cagr_pct = Math.max(best.cagr_pct, pf.cagr_pct); if (pf.portfolio_max_dd_pct != null) best.portfolio_max_dd_pct = Math.min(best.portfolio_max_dd_pct, pf.portfolio_max_dd_pct); } + // 综合评分:总收益30% + 胜率20% + 夏普20% + 盈亏比15% + 资产回撤15%(反向) + for (const s of strategies) { + const st = s.summary_stats || {}; const pf = st.portfolio || {}; + if (st.total_trades == null) { s._composite = null; continue; } + const ret = Math.min(pf.total_return_pct || 0, 100) / 100 * 30; + const wr = (st.win_rate || 0) / 100 * 20; + const sh = Math.min(Math.max(st.sharpe_ratio || 0, 0), 20) / 20 * 20; + const pfc = Math.min(st.profit_factor || 0, 5) / 5 * 15; + const dd = (1 - Math.min(pf.portfolio_max_dd_pct || 0, 50) / 50) * 15; + s._composite = Math.round(ret + wr + sh + pfc + dd); + } const hl = (val, bestVal, invert) => { if (val == null) return ''; const isBest = invert ? (val === bestVal && bestVal !== 999) : (val === bestVal && bestVal !== -999); @@ -1977,12 +1988,12 @@ function renderStrategyTable(strategies) { // 条件格式色条:计算每列 min/max,单元格背景按相对大小画色条 const CURRENT_STRATEGY = 'v7.1'; const ranges = {}; - const cols = ['total_return_pct','capital_final','cagr_pct','total_trades','avg_hold_days','win_rate','avg_profit_pct','sharpe_ratio','profit_factor','portfolio_max_dd_pct']; + const cols = ['composite','universality_score','total_return_pct','capital_final','cagr_pct','total_trades','avg_hold_days','win_rate','avg_profit_pct','sharpe_ratio','profit_factor','portfolio_max_dd_pct']; for (const c of cols) { let mn = Infinity, mx = -Infinity; for (const s of strategies) { const st = s.summary_stats || {}; const pf = st.portfolio || {}; - const v = (c in pf) ? pf[c] : st[c]; + const v = c === 'composite' ? s._composite : (c === 'universality_score' ? (st.universality || {}).score : ((c in pf) ? pf[c] : st[c])); if (v != null) { mn = Math.min(mn, v); mx = Math.max(mx, v); } } ranges[c] = { mn: mn === Infinity ? 0 : mn, mx: mx === -Infinity ? 1 : mx }; @@ -2004,6 +2015,8 @@ function renderStrategyTable(strategies) { }; let html = '
| 版本 | 名称 | ' + + '综合 | ' + + '普适 | ' + '总收益 | ' + '最终资产 | ' + '年化 | ' + @@ -2024,6 +2037,8 @@ function renderStrategyTable(strategies) { html += '
|---|---|---|---|---|---|---|
| ' + s.version + (isCurrent ? ' 当前' : '') + ' | ' + '' + (s.name || '') + (smallSample ? ' ⚠️' : '') + ' | ' + + cell('composite', s._composite, v => v, 'font-bold text-amber-300') + + cell('universality_score', (st.universality || {}).score, v => v + '/' + (st.universality || {}).months + '月') + cell('total_return_pct', pf.total_return_pct, v => v + '%', 'font-bold ' + retCls + ' ' + hl(pf.total_return_pct, best.total_return_pct)) + cell('capital_final', pf.capital_final, v => '¥' + (v/10000).toFixed(0) + '万') + cell('cagr_pct', pf.cagr_pct, v => v + '%', hl(pf.cagr_pct, best.cagr_pct)) + diff --git a/strategy_lab.py b/strategy_lab.py index 582b4b0b..839ce2e3 100644 --- a/strategy_lab.py +++ b/strategy_lab.py @@ -925,6 +925,23 @@ def calc_summary(trades, capital): for c in curve: peak = max(peak, c) max_dd = max(max_dd, (peak - c) / peak * 100) + # 普适性:信号月份分布(分散度越高越普适) + from collections import Counter + months = Counter(t['entry_date'][:7] for t in trades if t.get('entry_date')) + n_months = len(months) + peak_pct = round(max(months.values()) / len(trades) * 100) if trades else 0 + # 香农熵归一化 0-100(分布越均匀越高) + entropy = 0.0 + if n_months > 1: + for c in months.values(): + p = c / len(trades) + entropy -= p * math.log(p) + entropy = entropy / math.log(n_months) * 100 + universality = { + 'months': n_months, + 'peak_pct': peak_pct, + 'score': round(entropy, 0), + } return { 'total_trades': len(trades), 'win_rate': round(win_rate, 1), @@ -937,6 +954,7 @@ def calc_summary(trades, capital): 'profit_factor': round(abs(avg_w/avg_l), 2) if avg_l != 0 else None, 'wins': len(wins), 'losses': len(losses), 'capital_end': round(curve[-1], 2), + 'universality': universality, }