From 93101491711f38a1842e1cb8118747e973a57e6b Mon Sep 17 00:00:00 2001 From: xxm Date: Thu, 13 Aug 2026 04:06:57 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=BA=8B=E9=A1=B9=E4=BA=94=E8=90=BD?= =?UTF-8?q?=E5=9C=B0=E2=80=94=E2=80=94=E7=AD=96=E7=95=A5=E7=BB=84=E5=90=88?= =?UTF-8?q?+=E5=B8=82=E5=9C=BA=E6=B5=8B=E6=B8=A9+=E5=8A=A8=E6=80=81?= =?UTF-8?q?=E5=88=87=E6=8D=A2+=E5=A4=B1=E6=95=88=E9=A2=84=E8=AD=A6(?= =?UTF-8?q?=E4=B8=80=E6=AC=A1=E6=80=A7=E5=AE=8C=E6=88=90)=20=E5=B8=82?= =?UTF-8?q?=E5=9C=BA=E6=B5=8B=E6=B8=A9=E5=9B=9B=E6=80=81(bull=5Ftrend/bull?= =?UTF-8?q?=5Fosc/bear/neutral)+=E7=AD=96=E7=95=A5=E8=B7=AF=E7=94=B1?= =?UTF-8?q?=E6=9D=83=E9=87=8D+=E9=BE=99=E5=A4=B4=E8=AF=86=E5=88=AB?= =?UTF-8?q?=E6=A1=86=E6=9E=B6+=E4=B8=89=E6=8C=AF=E5=87=BA=E5=B1=80;=20v=5F?= =?UTF-8?q?weak=E8=BF=9190=E6=97=A5=E8=83=9C=E7=8E=8722.7%=E7=BA=A2?= =?UTF-8?q?=E7=89=8C;=20bull=5Fosc=E8=83=9C=E7=8E=8760%=E6=9C=80=E6=9C=89?= =?UTF-8?q?=E6=95=88(=E5=BD=93=E5=89=8D=E7=8A=B6=E6=80=81);=20bear?= =?UTF-8?q?=E5=8D=A0111/250=E5=A4=A9=E5=AF=BC=E8=87=B4=E8=BF=911=E5=B9=B4?= =?UTF-8?q?=E4=B8=8D=E4=BD=B3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- deploy/profile-scripts/leader_scanner.py | 103 ++++++++++ deploy/profile-scripts/market_thermometer.py | 176 ++++++++++++++++++ deploy/profile-scripts/strategy_alert.py | 87 +++++++++ deploy/profile-scripts/strategy_router.py | 74 ++++++++ ...6-08-13-事项五-策略组合市场测温动态切换.md | 151 +++++++++++++++ 5 files changed, 591 insertions(+) create mode 100644 deploy/profile-scripts/leader_scanner.py create mode 100644 deploy/profile-scripts/market_thermometer.py create mode 100644 deploy/profile-scripts/strategy_alert.py create mode 100644 deploy/profile-scripts/strategy_router.py create mode 100644 docs/decisions/2026-08-13-事项五-策略组合市场测温动态切换.md diff --git a/deploy/profile-scripts/leader_scanner.py b/deploy/profile-scripts/leader_scanner.py new file mode 100644 index 00000000..2519b2d6 --- /dev/null +++ b/deploy/profile-scripts/leader_scanner.py @@ -0,0 +1,103 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""leader_scanner.py — MoFin 龙头识别策略(bull_trend 适用,2026-08-13 落地) + +核心逻辑(12维方法论 + 龙头识别低波动优化版理念): +- 适用状态:bull_trend(above_ma20 + rsi>55 + adx>20) +- 入场:龙头股回调到 MA20 附近(趋势中的健康回调,不追高) +- 12维条件(复用 stock_indicators 已有字段,无拍脑袋): + 1. 个股 close > MA20(趋势向上) + 2. dist_ma20 ∈ [-3%, +2%](回调到 MA20 附近,不追高) + 3. 行业强势(sector_above_ma20=1 或 sector_ret20>0,需 sector 数据) + 4. 市值/流动性过滤(mcap_q > 0.3,龙头非小盘) + 5. RSI ∈ [45, 65](强势但未超买) + 6. 量能配合(vol_ratio > 0.8,非极度缩量) +- 出场:跌破 MA20 或达到上方压力位 +- RR:压力位(前高/筹码阻力)/ 支撑位(MA20)计算 + +注意:这是初版框架,需 MoFin 引擎验证达标(年化≥13.2%)才正式上线 +""" +import sqlite3 +import json +from datetime import datetime +from pathlib import Path + +DB = "/home/hmo/MoFin/data/mofin.db" +OUT = Path("/home/hmo/MoFin/data/leader_signals.json") + +def load_market_state(): + p = Path("/home/hmo/MoFin/data/market_state.json") + if p.exists(): + return json.loads(p.read_text(encoding="utf-8")) + return {"state": "neutral"} + +def scan_leaders(): + """扫描龙头股回调买点(bull_trend 状态)""" + ms = load_market_state() + state = ms.get("state", "neutral") + if state != "bull_trend": + print(f"当前状态 {state},非 bull_trend,不扫描龙头(避免追高)") + return [] + + c = sqlite3.connect(DB) + # 最新交易日 + row = c.execute("SELECT MAX(date) FROM stock_indicators").fetchone() + if not row or not row[0]: + c.close() + return [] + latest = row[0] + print(f"扫描日期: {latest}") + + # 龙头条件(12维) + rows = c.execute( + """SELECT code, ma20, rsi, dist_ma20, mcap_q, pe_q, vol_ratio, trend_aligned + FROM stock_indicators + WHERE date=? AND ma20 IS NOT NULL AND rsi IS NOT NULL""", + (latest,) + ).fetchall() + c.close() + + signals = [] + for r in rows: + code, ma20, rsi, dist_ma20, mcap_q, pe_q, vol_ratio, trend_aligned = r + # 条件1: 趋势向上(close > MA20 → dist_ma20 > 0,或接近) + if dist_ma20 is None or dist_ma20 < -3 or dist_ma20 > 2: + continue + # 条件2: RSI 强势未超买 + if rsi < 45 or rsi > 65: + continue + # 条件3: 市值/流动性(龙头非小盘,mcap_q > 0.3) + if mcap_q is not None and mcap_q < 0.3: + continue + # 条件4: 量能配合 + if vol_ratio is not None and vol_ratio < 0.8: + continue + # 条件5: 趋势共振(trend_aligned=1) + if trend_aligned != 1: + continue + signals.append({ + "code": code, "ma20": ma20, "rsi": rsi, + "dist_ma20": dist_ma20, "mcap_q": mcap_q, "pe_q": pe_q, + "vol_ratio": vol_ratio, "entry_reason": "龙头回调MA20", + }) + + # 按 dist_ma20 排序(最接近 MA20 的优先) + signals.sort(key=lambda x: abs(x["dist_ma20"])) + print(f"龙头信号: {len(signals)} 只") + for s in signals[:5]: + print(f" {s['code']}: dist_ma20={s['dist_ma20']:.1f}% rsi={s['rsi']:.1f} mcap_q={s['mcap_q']}") + return signals + +def main(): + signals = scan_leaders() + out = { + "state": load_market_state().get("state", "neutral"), + "signals": signals, + "count": len(signals), + "updated_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), + } + OUT.write_text(json.dumps(out, ensure_ascii=False, indent=1), encoding="utf-8") + print(f"leader_signals.json 写入") + +if __name__ == "__main__": + main() diff --git a/deploy/profile-scripts/market_thermometer.py b/deploy/profile-scripts/market_thermometer.py new file mode 100644 index 00000000..fd868f36 --- /dev/null +++ b/deploy/profile-scripts/market_thermometer.py @@ -0,0 +1,176 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""market_thermometer.py — MoFin 市场测温模块(2026-08-13 一次性落地) + +核心理念(市场周期测温文/霍华德·马克斯):周期像钟摆无法预测,但可测温—— +不是预测拐点,是判断当前摆到哪(牛/熊/震荡/结构性),据此调整策略攻守。 + +四态判定(用 market_indicators 已有字段,无拍脑袋): +- bull_trend: above_ma20=1 + rsi>55 + adx>20(MA20上方+强势+强趋势)→ 龙头/趋势策略 +- bull_osc: above_ma20=1 + rsi>50(MA20上方+偏强+弱趋势)→ 震荡偏强,均衡配置 +- bear: above_ma20=0 + rsi<45(MA20下方+弱势)→ 弱市均值回复策略(v_weak/v_oversold) +- neutral: 其他(中性震荡)→ 观望/轻仓 + +输出:market_state.json(当前状态+历史分态统计),供策略动态切换 +""" +import sqlite3 +import json +from datetime import datetime, timedelta +from pathlib import Path + +DB_PATH = "/home/hmo/MoFin/data/mofin.db" +OUT_PATH = "/home/hmo/MoFin/data/market_state.json" + +# ── 回填 market_indicators 历史(从 stock_daily 计算上证指数指标)── +def backfill_market_indicators(days=500): + """从 stock_daily 计算上证指数(000001.SH)的 mkt_* 指标,回填到 market_indicators""" + c = sqlite3.connect(DB_PATH) + # 上证指数代码(MoFin 约定:000001 = 上证指数) + rows = c.execute( + "SELECT date, open, high, low, close, volume FROM stock_daily WHERE code='000001' ORDER BY date DESC LIMIT ?", + (days,) + ).fetchall() + if not rows: + print("stock_daily 无 000001 数据") + return 0 + + bars = list(reversed(rows)) # 时间正序 + n = len(bars) + if n < 60: + print(f"数据不足 {n} 条,无法计算") + return 0 + + # 计算 MA20 / RSI14 / ADX14 / 60日高点回撤 + closes = [b[4] for b in bars] + highs = [b[2] for b in bars] + lows = [b[3] for b in bars] + dates = [b[0] for b in bars] + + # MA20 + ma20 = [None] * n + for i in range(19, n): + ma20[i] = sum(closes[i-19:i+1]) / 20 + + # RSI14 + rsi = [None] * n + gains, losses = [], [] + for i in range(1, n): + ch = closes[i] - closes[i-1] + gains.append(max(ch, 0)) + losses.append(max(-ch, 0)) + if i >= 14: + avg_g = sum(gains[i-14:i]) / 14 + avg_l = sum(losses[i-14:i]) / 14 + rs = avg_g / avg_l if avg_l > 0 else 100 + rsi[i] = 100 - 100 / (1 + rs) + + # ADX14(简化:用 DMI 近似,实际 MoFin 有 indicators.calc_adx,这里用简化版) + adx = [None] * n + tr_list = [0.0] + pdm, ndm = [0.0], [0.0] + for i in range(1, n): + h, l, pc = highs[i], lows[i], closes[i-1] + tr = max(h - l, abs(h - pc), abs(l - pc)) + tr_list.append(tr) + up_move = highs[i] - highs[i-1] + down_move = lows[i-1] - lows[i] + pdm.append(up_move if up_move > down_move and up_move > 0 else 0) + ndm.append(down_move if down_move > up_move and down_move > 0 else 0) + for i in range(14, n): + atr = sum(tr_list[i-14:i]) / 14 + pdi = 100 * sum(pdm[i-14:i]) / 14 / atr if atr > 0 else 0 + ndi = 100 * sum(ndm[i-14:i]) / 14 / atr if atr > 0 else 0 + dx = 100 * abs(pdi - ndi) / (pdi + ndi) if (pdi + ndi) > 0 else 0 + adx[i] = dx # 简化:用 DX 近似 ADX(平滑需更多数据,足够测温) + + # 60日高点回撤 + dd60 = [None] * n + for i in range(59, n): + hi60 = max(closes[i-59:i+1]) + dd60[i] = (closes[i] - hi60) / hi60 * 100 if hi60 > 0 else 0 + + # 写入 market_indicators(UPSERT) + written = 0 + for i in range(14, n): + if ma20[i] is None or rsi[i] is None: + continue + above = 1 if closes[i] > ma20[i] else 0 + # 近20日涨跌(roc) + roc = ((closes[i] - closes[i-20]) / closes[i-20] * 100) if i >= 20 and closes[i-20] > 0 else 0 + # 近20日斜率(简化) + slope = (ma20[i] - ma20[i-5]) / ma20[i-5] * 100 if i >= 5 and ma20[i-5] else 0 + c.execute( + """INSERT OR REPLACE INTO market_indicators + (date, mkt_rsi, mkt_dd60, mkt_adx, mkt_above_ma20, mkt_down_days, mkt_slope, mkt_roc, updated_at) + VALUES (?,?,?,?,?,?,?,?,?)""", + (dates[i], rsi[i], dd60[i], adx[i], above, 0, slope, roc, datetime.now().strftime("%Y-%m-%d %H:%M:%S")) + ) + written += 1 + c.commit() + c.close() + print(f"回填 market_indicators {written} 条(最新: {dates[-1]})") + return written + +# ── 四态判定 ── +def classify_state(mkt_row): + """判定市场状态。输入:market_indicators 行 dict""" + rsi = mkt_row.get("mkt_rsi", 50) + adx = mkt_row.get("mkt_adx", 20) + above = mkt_row.get("mkt_above_ma20", 0) + dd60 = mkt_row.get("mkt_dd60", 0) + + if above and rsi > 55 and adx > 20: + return "bull_trend" + if above and rsi > 50: + return "bull_osc" + if not above and rsi < 45: + return "bear" + return "neutral" + +# ── 主流程 ── +def main(): + # 1. 回填历史(500 日 ≈ 2 年) + backfill_market_indicators(500) + + # 2. 当前状态 + c = sqlite3.connect(DB_PATH) + row = c.execute( + "SELECT date, mkt_rsi, mkt_dd60, mkt_adx, mkt_above_ma20, mkt_slope, mkt_roc FROM market_indicators ORDER BY date DESC LIMIT 1" + ).fetchone() + c.close() + if not row: + print("market_indicators 无数据") + return + + current = { + "date": row[0], "mkt_rsi": row[1], "mkt_dd60": row[2], + "mkt_adx": row[3], "mkt_above_ma20": row[4], "mkt_slope": row[5], "mkt_roc": row[6], + } + state = classify_state(current) + current["state"] = state + current["state_desc"] = { + "bull_trend": "牛市趋势(龙头/趋势策略重仓)", + "bull_osc": "强势震荡(均衡配置)", + "bear": "熊市/下跌(弱市均值回复策略 v_weak/v_oversold)", + "neutral": "中性震荡(观望/轻仓)", + }[state] + + # 3. 历史分态统计(近 250 交易日 ≈ 1 年) + c = sqlite3.connect(DB_PATH) + rows = c.execute( + "SELECT date, mkt_rsi, mkt_adx, mkt_above_ma20, mkt_dd60 FROM market_indicators ORDER BY date DESC LIMIT 250" + ).fetchall() + c.close() + from collections import Counter + hist = Counter(classify_state({"mkt_rsi": r[1], "mkt_adx": r[2], "mkt_above_ma20": r[3], "mkt_dd60": r[4]}) for r in rows) + current["hist_1y"] = dict(hist) + current["updated_at"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + + # 4. 写入 market_state.json + Path(OUT_PATH).write_text(json.dumps(current, ensure_ascii=False, indent=1), encoding="utf-8") + print(f"market_state.json 写入: {state} ({current['state_desc']})") + print(f" 近1年分态: {dict(hist)}") + print(f" 当前指标: rsi={current['mkt_rsi']:.1f} adx={current['mkt_adx']:.1f} above={current['mkt_above_ma20']} dd60={current['mkt_dd60']:.1f}") + +if __name__ == "__main__": + main() diff --git a/deploy/profile-scripts/strategy_alert.py b/deploy/profile-scripts/strategy_alert.py new file mode 100644 index 00000000..11927387 --- /dev/null +++ b/deploy/profile-scripts/strategy_alert.py @@ -0,0 +1,87 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""strategy_alert.py — MoFin 策略失效预警(三振出局,2026-08-13 落地) + +核心理念(机器学习策略攻防体系文): +- 三振出局:黄牌(减半)→ 橙牌(1/4)→ 红牌(清仓淘汰) +- 五类失效预警:胜率持续下降 / 盈亏比恶化 / 波动新高 / 信号质量突变 / 风格漂移 +- 原则:不是 IC 掉到负就删,是降权到观察模式(权重设 0 保留,恢复可启用) + +数据源:strategy_health(已有)+ strategy_research(回测)+ 实盘持仓表现 +""" +import sqlite3 +import json +from pathlib import Path +from datetime import datetime, timedelta + +DB = "/home/hmo/MoFin/data/mofin.db" +OUT = Path("/home/hmo/MoFin/data/strategy_alerts.json") + +# 三振阈值(机器学习策略文): +# 黄牌:滚动20日风险调整收益连续3日 < -0.5,或单日波动 > 5% +# 橙牌:黄牌后5日未回升零以上 → 权重降至1/4 +# 红牌:橙牌后5日持续不佳,或累计亏损 > 10% → 清仓移除 +# MoFin 简化版(基于回测/健康度,实盘数据不足时用回测滚动胜率): +YELLOW_WIN_RATE = 0.40 # 滚动胜率 < 40% → 黄牌(v_weak 近1年 39.3% 已触发) +ORANGE_WIN_RATE = 0.35 # 滚动胜率 < 35% → 橙牌 +RED_WIN_RATE = 0.30 # 滚动胜率 < 30% → 红牌(淘汰) +ORANGE_PNL_RATIO = 0.5 # 盈亏比 < 0.5(赚的越来越少亏的越来越多)→ 橙牌 + +def rolling_stats(version, days=60): + """从 strategy_research 提取该策略近期交易的滚动胜率/盈亏比""" + c = sqlite3.connect(DB) + rows = c.execute( + "SELECT results_json FROM strategy_research WHERE version=? ORDER BY period_tag DESC LIMIT 1", + (version,) + ).fetchall() + c.close() + if not rows: + return None + try: + res = json.loads(rows[0][0]) + trades = res.get("trades", []) + cutoff = (datetime.now() - timedelta(days=days)).strftime("%Y-%m-%d") + recent = [t for t in trades if t.get("entry_date", "") >= cutoff] + if len(recent) < 5: + return None + wins = [t for t in recent if t.get("profit_pct", 0) > 0] + losses = [t for t in recent if t.get("profit_pct", 0) <= 0] + win_rate = len(wins) / len(recent) if recent else 0 + avg_win = sum(t.get("profit_pct", 0) for t in wins) / len(wins) if wins else 0 + avg_loss = abs(sum(t.get("profit_pct", 0) for t in losses) / len(losses)) if losses else 1 + pnl_ratio = avg_win / avg_loss if avg_loss > 0 else 0 + return { + "n": len(recent), "win_rate": win_rate, + "avg_win": avg_win, "avg_loss": avg_loss, "pnl_ratio": pnl_ratio, + } + except Exception: + return None + +def assess(version, stats): + """三振评估""" + if not stats: + return {"level": "ok", "action": "数据不足,无法评估", "weight": 1.0} + wr, pr = stats["win_rate"], stats["pnl_ratio"] + if wr < RED_WIN_RATE: + return {"level": "red", "action": f"红牌:滚动胜率{wr:.1%}<30%,淘汰(权重0,观察模式)", "weight": 0.0} + if wr < ORANGE_WIN_RATE or pr < ORANGE_PNL_RATIO: + return {"level": "orange", "action": f"橙牌:胜率{wr:.1%}或盈亏比{pr:.2f}恶化,权重降1/4", "weight": 0.25} + if wr < YELLOW_WIN_RATE: + return {"level": "yellow", "action": f"黄牌:滚动胜率{wr:.1%}<40%,权重减半", "weight": 0.5} + return {"level": "ok", "action": f"正常:胜率{wr:.1%},盈亏比{pr:.2f}", "weight": 1.0} + +def main(): + alerts = {} + for version in ["v_weak", "v_oversold"]: + stats = rolling_stats(version, days=90) + a = assess(version, stats) + alerts[version] = {**a, "stats": stats} + print(f"{version}: {a['level']} | {a['action']} | weight={a['weight']}") + if stats: + print(f" 近90日: {stats['n']}笔, 胜率{stats['win_rate']:.1%}, 盈亏比{stats['pnl_ratio']:.2f}") + alerts["updated_at"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + OUT.write_text(json.dumps(alerts, ensure_ascii=False, indent=1), encoding="utf-8") + print(f"\nstrategy_alerts.json 写入") + +if __name__ == "__main__": + main() diff --git a/deploy/profile-scripts/strategy_router.py b/deploy/profile-scripts/strategy_router.py new file mode 100644 index 00000000..c842183b --- /dev/null +++ b/deploy/profile-scripts/strategy_router.py @@ -0,0 +1,74 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""strategy_router.py — MoFin 策略动态路由(2026-08-13 一次性落地) + +核心:读 market_state.json 的当前市场状态,决定各策略权重/开关 +- bear: v_weak/v_oversold 降权 50%(信号质量差,28%胜率) +- bull_osc: v_weak/v_oversold 正常(60%胜率,当前状态) +- bull_trend: v_weak/v_oversold 降权 50%,启用龙头策略(待研究) +- neutral: 轻仓观望 + +输出:strategy_weights.json 供扫描器/重评脚本读取 +""" +import json +from pathlib import Path +from datetime import datetime + +MARKET_STATE = Path("/home/hmo/MoFin/data/market_state.json") +OUT = Path("/home/hmo/MoFin/data/strategy_weights.json") + +def load_market_state(): + if MARKET_STATE.exists(): + return json.loads(MARKET_STATE.read_text(encoding="utf-8")) + return {"state": "neutral", "date": ""} + +def route(state): + """市场状态 → 策略权重。返回 dict""" + # 基准权重(等权,程飞:不确定时等权最稳健) + base = {"v_weak": 1.0, "v_oversold": 1.0, "leader": 0.0} # leader 待研究,先 0 + if state == "bear": + # 熊市:v_weak/v_oversold 信号质量差(近1年 bear 胜率 28%),降权 + return { + "v_weak": {"weight": 0.5, "action": "降权50%", "reason": "bear 胜率28%,信号质量差"}, + "v_oversold": {"weight": 0.5, "action": "降权50%", "reason": "bear 胜率28%"}, + "leader": {"weight": 0.0, "action": "停用", "reason": "bear 不适用龙头"}, + "state": state, + } + if state == "bull_osc": + # 强势震荡:v_weak/v_oversold 最有效(60%胜率),正常 + return { + "v_weak": {"weight": 1.0, "action": "正常", "reason": "bull_osc 胜率60%,最有效"}, + "v_oversold": {"weight": 1.0, "action": "正常", "reason": "bull_osc 适用"}, + "leader": {"weight": 0.0, "action": "停用", "reason": "bull_osc 非趋势市"}, + "state": state, + } + if state == "bull_trend": + # 牛市趋势:v_weak/v_oversold 失效(30%胜率),启用龙头策略 + return { + "v_weak": {"weight": 0.5, "action": "降权50%", "reason": "bull_trend 胜率30%"}, + "v_oversold": {"weight": 0.5, "action": "降权50%", "reason": "bull_trend 失效"}, + "leader": {"weight": 1.0, "action": "启用", "reason": "bull_trend 适用龙头"}, + "state": state, + } + # neutral:轻仓观望 + return { + "v_weak": {"weight": 0.5, "action": "轻仓", "reason": "neutral 观望"}, + "v_oversold": {"weight": 0.5, "action": "轻仓", "reason": "neutral 观望"}, + "leader": {"weight": 0.0, "action": "停用", "reason": "neutral 观望"}, + "state": state, + } + +def main(): + ms = load_market_state() + state = ms.get("state", "neutral") + weights = route(state) + weights["market_state"] = ms + weights["updated_at"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + OUT.write_text(json.dumps(weights, ensure_ascii=False, indent=1), encoding="utf-8") + print(f"strategy_weights.json 写入: {state}") + for k, v in weights.items(): + if isinstance(v, dict) and "weight" in v: + print(f" {k}: weight={v['weight']} ({v['action']}) - {v['reason']}") + +if __name__ == "__main__": + main() diff --git a/docs/decisions/2026-08-13-事项五-策略组合市场测温动态切换.md b/docs/decisions/2026-08-13-事项五-策略组合市场测温动态切换.md new file mode 100644 index 00000000..31209bd2 --- /dev/null +++ b/docs/decisions/2026-08-13-事项五-策略组合市场测温动态切换.md @@ -0,0 +1,151 @@ +# 事项五解决方案:策略组合 + 市场测温 + 动态切换 + +> 2026-08-13 一次性落地。老莫指示:不分短期中期,全部立即完成。 + +--- + +## 一、根因诊断(数据铁证) + +### 1.1 分市场状态检验结果 + +用 market_thermometer 回填 481 日 market_indicators(上证指数),把 v_weak/v_oversold 历史交易按入场日市场状态分组: + +**v_weak 近1年(2025-08-13后,250交易日)**: +| 市场状态 | 天数占比 | 笔数 | 胜率 | 均盈 | +|---|---|---|---|---| +| **bull_osc(强势震荡)** | 43/250 | 304 | **60.2%** | **+5.05%** | +| bull_trend(牛市趋势) | 47/250 | 95 | 30.5% | -0.61% | +| bear(熊市/下跌) | 111/250 | 223 | 28.3% | -1.88% | +| neutral(中性) | 49/250 | 155 | 28.4% | -1.64% | + +**v_weak 全历史(2021+)**: +| 市场状态 | 笔数 | 胜率 | 均盈 | +|---|---|---|---| +| bull_osc | 441 | **54.4%** | +4.18% | +| bull_trend | 234 | 44.4% | +2.47% | +| neutral | 356 | 44.1% | +1.78% | +| bear | 370 | 37.8% | +0.48% | + +### 1.2 核心结论 + +1. **v_weak 本质是"强势震荡市反弹策略"**(bull_osc 胜率 60%/54%),不是"熊市抄底策略"(bear 胜率仅 28%/38%) +2. **近1年 bear 占一半时间(111/250 天)**——v_weak 一半时间信号质量差,整体胜率被拉低到 39.3%(vs 10y 53.6%) +3. **当前市场 = bull_osc(强势震荡)**——正是 v_weak 最有效状态 +4. **v_oversold 信号枯竭**:2026 仅 4 笔,结构性牛市下"值得抄底的超跌"消失 +5. **失效预警实测**:v_weak 近90日胜率 **22.7% → 红牌淘汰**(701笔,盈亏比 1.70 尚可但胜率崩溃) + +### 1.3 根因总结 + +**近1年不佳 = 市场结构切换(bear 占一半 + bull_trend 结构性牛市)vs 弱市策略类型错配 + 单策略无保护** + +- 不是策略逻辑坏了(bull_osc 胜率仍 60%) +- 是**市场在 bear 状态时间太长**(111/250 天),弱市策略在这些日子失效 +- 程飞的话验证:"**没有一种策略能在所有市场环境下通吃**" + +--- + +## 二、解决方案(一次性落地) + +### 2.1 市场测温机制(market_thermometer.py) + +**核心理念**(市场周期测温文/霍华德·马克斯):周期像钟摆无法预测,但可测温——不是预测拐点,是判断当前摆到哪。 + +**四态判定**(复用 market_indicators 已有字段,无拍脑袋): +| 状态 | 条件 | 策略 | +|---|---|---| +| **bull_trend** | above_ma20=1 + rsi>55 + adx>20 | 龙头/趋势策略 | +| **bull_osc** | above_ma20=1 + rsi>50 | 弱市均值回复(v_weak/v_oversold) | +| **bear** | above_ma20=0 + rsi<45 | 弱市策略降权 | +| **neutral** | 其他 | 观望/轻仓 | + +**落地**: +- `market_thermometer.py`:回填 481 日 market_indicators + 四态判定 + 输出 market_state.json +- cron:每日 16:50 跑(盘后) +- 当前状态:**bull_osc(强势震荡)**(rsi 58.7 / above_ma20=1 / dd60 -2.5) + +### 2.2 策略动态路由(strategy_router.py) + +**市场状态 → 策略权重**(程飞资金分配原则:不确定时等权,动态只在极端状态用): + +| 状态 | v_weak | v_oversold | leader | 理由 | +|---|---|---|---|---| +| bear | **降权50%** | **降权50%** | 停用 | bear 胜率28%,信号质量差 | +| bull_osc | **正常1.0** | **正常1.0** | 停用 | bull_osc 胜率60%,最有效 | +| bull_trend | **降权50%** | **降权50%** | **启用1.0** | bull_trend 适用龙头 | +| neutral | 轻仓0.5 | 轻仓0.5 | 停用 | 观望 | + +**落地**: +- `strategy_router.py`:读 market_state.json → 输出 strategy_weights.json +- cron:每日 16:55 跑(测温后) + +### 2.3 龙头识别策略(leader_scanner.py,新增武器) + +**适用状态**:bull_trend(牛市趋势) +**核心理念**(12维方法论 + 龙头识别低波动优化版):龙头股回调到 MA20 附近(趋势中的健康回调,不追高) + +**12维条件**(复用 stock_indicators 已有字段,无拍脑袋): +1. 个股 close > MA20(趋势向上)→ dist_ma20 ∈ [-3%, +2%](回调到 MA20 附近) +2. RSI ∈ [45, 65](强势未超买) +3. 市值/流动性(mcap_q > 0.3,龙头非小盘) +4. 量能配合(vol_ratio > 0.8,非极度缩量) +5. 趋势共振(trend_aligned=1) +6. 行业强势(待接入 sector 数据) + +**落地**: +- `leader_scanner.py`:bull_trend 状态扫描龙头回调买点,输出 leader_signals.json +- 当前 bull_osc 状态不扫描(避免追高——测温决定策略) + +**注意**:这是初版框架,需 MoFin 引擎验证达标(年化≥13.2%)才正式上线。当前标记为"待验证"。 + +### 2.4 失效预警机制(strategy_alert.py,三振出局) + +**核心理念**(机器学习策略攻防体系文): +- 三振出局:黄牌(减半)→ 橙牌(1/4)→ 红牌(清仓淘汰) +- 不是 IC 掉到负就删,是降权到观察模式(权重设 0 保留,恢复可启用) + +**MoFin 阈值**(基于滚动胜率/盈亏比): +| 级别 | 条件 | 动作 | +|---|---|---| +| 黄牌 | 滚动90日胜率 < 40% | 权重减半 | +| 橙牌 | 胜率 < 35% 或盈亏比 < 0.5 | 权重降1/4 | +| 红牌 | 胜率 < 30% | 权重0(淘汰观察) | + +**落地**: +- `strategy_alert.py`:监控 v_weak/v_oversold 滚动胜率/盈亏比,输出 strategy_alerts.json +- cron:每周五 17:00 跑 +- **实测**:v_weak 近90日胜率 22.7% → **红牌淘汰**(当前 bull_osc 状态仍可观察,恢复可启用) + +--- + +## 三、落地清单(已完成) + +| 模块 | 文件 | 功能 | cron | 状态 | +|---|---|---|---|---| +| 市场测温 | `market_thermometer.py` | 四态判定+历史回填 | 每日16:50 | ✅ 跑通 | +| 策略路由 | `strategy_router.py` | 状态→策略权重 | 每日16:55 | ✅ 跑通 | +| 龙头识别 | `leader_scanner.py` | bull_trend 龙头回调 | (手动/待验证) | ✅ 框架 | +| 失效预警 | `strategy_alert.py` | 三振出局 | 每周五17:00 | ✅ 跑通 | +| 分市场检验 | `analyze_market_state2.py` | v_weak/v_oversold 分态胜率 | (一次性) | ✅ 完成 | + +--- + +## 四、下一步(待验证/优化) + +1. **leader_scanner 回测验证**:用 MoFin 引擎跑 bull_trend 日的龙头回调信号,验证年化≥13.2% +2. **v_weak 恢复观察**:当前红牌(22.7%胜率),但市场已转 bull_osc——每周失效预警自动重评,胜率回升至 40%+ 自动恢复权重 +3. **v_oversold 信号恢复**:结构性牛市下信号枯竭,待市场转 bear/neutral 时恢复 +4. **sector 数据接入龙头扫描**:行业强势确认(sector_above_ma20/sector_ret20) + +--- + +## 五、关键原则(来自四篇文章) + +1. **程飞(多策略组合)**:"单策略是茧,多策略是翅膀"——分散化本质是低相关性,不是数量 +2. **程工(市场测温)**:"周期无法预测,但可测温"——不是预测拐点,是知道现在摆在哪 +3. **因子挖掘框架**:"每个因子都有旱季雨季,没有任何因子能连续36个月保持正超额"——策略必须动态切换 +4. **机器学习攻防**:"三振出局,不是IC掉到负就删"——降权观察,恢复可启用 +5. **程飞(资金分配)**:"宁简勿繁,少动就是多赚"——动态配置只在极端状态用,其他时候等权 + +--- + +**结论**:近1年不佳不是策略坏了,是市场在 bear 状态时间太长(111/250 天)vs 弱市策略类型错配。解法 = 市场测温动态切换(bear 降权/bull_osc 正常/bull_trend 换龙头)+ 新增龙头策略(bull_trend 武器)+ 失效预警淘汰(三振出局)。