198 lines
8.7 KiB
Python
198 lines
8.7 KiB
Python
#!/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}
|
|
|
|
|
|
# ── 行业新闻情绪分析(2026-07-31:东方财富翻页新闻→500条/股)──
|
|
_POS_KW = ['大涨','涨停','回暖','反弹','回升','领涨','突破','创新高','资金流入','净流入','加仓','密集调研','增持','回购']
|
|
_NEG_KW = ['大跌','跌停','回落','下跌','下挫','走弱','资金流出','净流出','减持','利空','风险','预警','亏损','预警']
|
|
|
|
def sector_news_sentiment(sector, days=3):
|
|
import sqlite3
|
|
conn = sqlite3.connect('/home/hmo/MoFin/data/mofin.db')
|
|
try:
|
|
stocks = conn.execute("SELECT code FROM stock_sectors_em WHERE sector=?", (sector,)).fetchall()
|
|
if not stocks:
|
|
return {'state': 'neutral', 'pos': 0, 'neg': 0, 'total': 0}
|
|
from datetime import datetime, timedelta
|
|
since = (datetime.now() - timedelta(days=days)).strftime('%Y-%m-%d')
|
|
codes = tuple(s[0] for s in stocks)
|
|
placeholders = ','.join('?' for _ in codes)
|
|
arts = conn.execute(f"SELECT title, content FROM stock_news WHERE code IN ({placeholders}) AND date>=?", codes + (since,)).fetchall()
|
|
pos = neg = 0
|
|
for t, c in arts:
|
|
text = (t or '') + ' ' + (c or '')
|
|
for kw in _POS_KW:
|
|
if kw in text: pos += 1; break
|
|
for kw in _NEG_KW:
|
|
if kw in text: neg += 1; break
|
|
total = pos + neg
|
|
state = 'neutral'
|
|
if total > 0 and pos / max(total, 1) > 0.6:
|
|
state = 'positive'
|
|
elif total > 0 and neg / max(total, 1) > 0.6:
|
|
state = 'negative'
|
|
return {'state': state, 'pos': pos, 'neg': neg, 'total': total}
|
|
except Exception:
|
|
return {'state': 'neutral', 'pos': 0, 'neg': 0, 'total': 0}
|
|
finally:
|
|
conn.close()
|
|
|
|
|
|
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 = ''
|
|
|
|
# ── 第四维:行业新闻情绪叠加(2026-07-31) ──
|
|
sns = sector_news_sentiment(sector) if sector else {'state': 'neutral', 'pos': 0, 'neg': 0, 'total': 0}
|
|
if decision == 'pass' and sns['state'] == 'positive' and fs in ('positive', 'strong_positive'):
|
|
decision = 'resonance'
|
|
reason = f"行业新闻确认({sns['pos']}pos/{sns['neg']}neg) + 资金流入 → 共振"
|
|
elif decision == 'pass' and sns['state'] == 'negative' and fs == 'negative':
|
|
decision = 'downgrade'
|
|
reason = f"行业新闻利空({sns['pos']}pos/{sns['neg']}neg) + 资金流出 → 降级"
|
|
return {'decision': decision, 'reason': reason, 'flow': flow, 'news': news, 'sector': sector, 'sector_news': sns}
|
|
|
|
|
|
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']}")
|