diff --git a/deploy/profile-scripts/setup_strategy_defs.py b/deploy/profile-scripts/setup_strategy_defs.py new file mode 100644 index 00000000..fbfa8e5b --- /dev/null +++ b/deploy/profile-scripts/setup_strategy_defs.py @@ -0,0 +1,234 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""MoFin 策略定义库落地脚本(2026-08-18) +1. 建 strategy_defs 表 +2. holding_strategies 加列 strategy_provenance/strategy_state/strategy_attributed +3. 种子卡:8 个策略(accumulation/b_td1_v3/v_mr/hk_pe_mom/hk_pe_oversold/p_oversold/s2_panic/leader) +执行:/home/hmo/MoFin/venv/bin/python setup_strategy_defs.py +""" +import sqlite3, json, hashlib, sys + +DB = "/home/hmo/MoFin/data/mofin.db" + +def get_conn(): + conn = sqlite3.connect(DB, timeout=30) + conn.execute("PRAGMA busy_timeout=30000") + return conn + +def create_tables(conn): + # 策略定义表 + conn.execute(""" + CREATE TABLE IF NOT EXISTS strategy_defs ( + strategy_name TEXT PRIMARY KEY, + display_name TEXT NOT NULL, + aliases TEXT, + market TEXT NOT NULL DEFAULT 'A', + regime TEXT, + family TEXT, + summary TEXT NOT NULL, + entry_logic TEXT NOT NULL, + exit_logic TEXT NOT NULL, + key_params TEXT, + review_focus TEXT NOT NULL, + holding_style TEXT, + status TEXT NOT NULL DEFAULT 'active', + retired_reason TEXT, + superseded_by TEXT, + params_hash TEXT, + needs_review INTEGER DEFAULT 0, + doc_ref TEXT, + scanner_file TEXT, + version TEXT, + updated_at TEXT, + updated_by TEXT + )""") + # holding_strategies 加列(幂等) + cols = [c[1] for c in conn.execute("PRAGMA table_info(holding_strategies)").fetchall()] + for col, ddl in [ + ("strategy_provenance", "TEXT"), + ("strategy_state", "TEXT DEFAULT 'active'"), + ("strategy_attributed", "TEXT"), + ]: + if col not in cols: + conn.execute(f"ALTER TABLE holding_strategies ADD COLUMN {col} {ddl}") + print(f" + holding_strategies.{col} 已加") + else: + print(f" = holding_strategies.{col} 已存在") + +# 策略种子卡(从 scanner 头部 + docs + 实盘参数提炼,2026-08-18) +SEEDS = [ + dict( + strategy_name="accumulation", + display_name="主力建仓期捕捉", + aliases=json.dumps(["accumulation", "acc"]), + market="A", regime="all", + family="accumulation", + summary="捕捉主力资金建仓期的股票:价格20日区间中下段、量能放大、连续增量,基本面安全(PB<2或PE合理)。", + entry_logic="价格处于20日中下段;成交量放大>50%且连续多日增量;小涨或平盘吸筹形态;基本面PB<2或PE合理(防暴雷)。", + exit_logic="建仓结束信号(放量滞涨/主力流出)或价格突破主升浪后分批止盈;跌破建仓成本区下沿止损。", + key_params=json.dumps({"vol_ratio": 1.5, "pos": "20d_mid_low", "pb_max": 2.0}), + review_focus="①主力是否仍在吸筹(量能/资金流)②是否已进入拉升阶段(建仓逻辑可能已结束)③放量滞涨风险④止损位是否仍合理", + holding_style="波段/中期(建仓期到拉升期)", + status="active", version="v1", + scanner_file="accumulation_scanner.py", + doc_ref="docs/strategy-current-state-20260811.md", + ), + dict( + strategy_name="b_td1_v3", + display_name="B组超跌·原池优选", + aliases=json.dumps(["b_td1_v3", "b_td1"]), + market="A", regime="trend_down,choppy", + family="oversold", + summary="弱市/震荡市买入小市值+低估值+深超跌股(mcap_q<0.3、pe_q<0.3、bias60<-20、rsi<40、距20日低点>5%),等反弹至+15%。", + entry_logic="mcap_q<0.3 且 pe_q<0.3(小市值低估值);bias60<-20(比MA60低20%+);rsi<40(超卖);dist_lo20>5(距20日低点>5%);评分=超跌深度+超卖程度+5日急跌。", + exit_logic="止盈 +15%(target=close×1.15);止损 -8%(stop=close×0.92);温区 trend_up 时不再开新仓。", + key_params=json.dumps({"mcap_q_max": 0.3, "pe_q_max": 0.3, "bias60_max": -20, "rsi_max": 40, "dist_lo20_min": 5, "tp_pct": 0.15, "sl_pct": 0.08, "regime": ["trend_down","choppy"]}), + review_focus="①超卖是否修复(RSI/bias60 回到区间外=反弹逻辑兑现)②反弹动能是否还在(量价)③距+15%止盈的空间④原入场条件是否仍成立(若已反弹出超跌区→策略失效)", + holding_style="波段(超跌反弹,快进快出)", + status="active", version="v4", + scanner_file="b_td1_v3_scanner.py", + doc_ref="", + ), + dict( + strategy_name="v_mr", + display_name="弱市深超跌均值回复", + aliases=json.dumps(["v_mr", "v_weak"]), + market="A", regime="trend_down,choppy", + family="mean_reversion", + summary="弱市(大盘MA20下)+ADX甜区时买入深超跌(bias60∈[-35,-20))、极度超卖(RSI≤25)、5日急跌、距20日低点<5%的票,等均值回复。", + entry_logic="bias60∈[-35,-20)(深超跌不过深);RSI≤25(极度超卖);r5f≤-3%(5日急跌);dist_lo20<5%(下方有承接);信号日收阳+缩量。", + exit_logic="止盈 +30%;止损 -12%;最长持有 40 交易日(RR 2.5:1)。", + key_params=json.dumps({"bias60_range": [-35,-20], "rsi_max": 25, "r5f_max": -3, "dist_lo20_max": 5, "tp_pct": 0.30, "sl_pct": 0.12, "max_hold_days": 40}), + review_focus="①超卖是否修复②反弹动能③距止损/止盈空间④原入场条件是否仍成立(反弹出区间→失效)⑤是否超最长持有期", + holding_style="波段(弱市均值回复,最长40交易日)", + status="active", version="v_weak", + scanner_file="mr_scanner.py", + doc_ref="docs/v_mr_strategy.md", + ), + dict( + strategy_name="hk_pe_mom", + display_name="港股低PE+小市值+行业动量", + aliases=json.dumps(["hk_pe_mom"]), + market="HK", regime="trend_up", + family="momentum", + summary="港股通 trend_up 温区:买入低PE+小市值+行业动量强势的港股,追涨不追高。", + entry_logic="低PE(估值便宜);小市值;行业动量强(sec_ret20>0);价格处于上升趋势。", + exit_logic="动量衰减/跌破趋势线止损;止盈按行业压力位分批。", + key_params=json.dumps({"regime": "trend_up"}), + review_focus="①动量是否延续(行业/个股趋势)②温区是否从 trend_up 切换(动量策略主战场变了)③估值是否仍低④止损位合理性", + holding_style="趋势跟踪(动量延续期)", + status="active", version="v2", + scanner_file="hk_scanner.py", + doc_ref="", + ), + dict( + strategy_name="hk_pe_oversold", + display_name="港股低PE超卖反弹", + aliases=json.dumps(["hk_pe_oversold", "hk_mr1"]), + market="HK", regime="choppy", + family="oversold", + summary="港股通 choppy 温区:买入低PE+超卖(RSI低)的港股,等超卖修复反弹。", + entry_logic="低PE;超卖(RSI低/深回调);choppy 温区(震荡市超卖反弹逻辑)。", + exit_logic="超卖修复/反弹至压力位分批止盈;跌破支撑止损。", + key_params=json.dumps({"regime": "choppy"}), + review_focus="①超卖是否修复②温区是否仍 choppy③反弹空间④止损合理性", + holding_style="波段(超卖修复)", + status="active", version="v2", + scanner_file="hk_scanner.py", + doc_ref="", + ), + dict( + strategy_name="p_oversold", + display_name="预测超跌反弹", + aliases=json.dumps(["p_oversold"]), + market="A", regime="trend_down,choppy", + family="oversold", + summary="弱市/恐慌期买入深度超跌+低估值+小市值+有新闻的票(mcap_q<0.2、pe_q<0.2、bias60<-20、行业20日动量弱),等反弹。", + entry_logic="mcap_q<0.2(小市值);pe_q<0.2(低估值);bias60<-20(深超跌);sec_ret20<0(行业弱);news3≥1(有新闻催化)。", + exit_logic="支撑位下方5%止损;压力位分批止盈;40交易日强平。", + key_params=json.dumps({"mcap_q_max": 0.2, "pe_q_max": 0.2, "bias60_max": -20, "sec_ret20_max": 0, "news_min": 1, "max_hold_days": 40}), + review_focus="①超跌是否修复②消息催化是否兑现③行业20日动量是否转强④40日强平窗口⑤止损/止盈空间", + holding_style="波段(超跌+催化反弹)", + status="active", version="v5", + scanner_file="predictive_oversold_scanner.py", + doc_ref="docs/predictive_oversold_strategy.md", + ), + dict( + strategy_name="s2_panic", + display_name="恐慌日买强势", + aliases=json.dumps(["s2_panic", "s2_panic_v2"]), + market="A", regime="trend_down", + family="panic", + summary="恐慌日(大盘RSI超卖)买入小市值+高RSI+行业抗跌的强势股(mcap_q<0.4、rsi≥35、sec_ret20≥-10),恐慌错杀中的强势alpha。", + entry_logic="大盘 RSI<25(恐慌日门控);mcap_q<0.4(小市值);rsi≥35(个股强势);sec_ret20≥-10(行业抗跌)。", + exit_logic="止盈 +30%;止损 -12%;最长持有 60 交易日。", + key_params=json.dumps({"mkt_rsi_max": 25, "mcap_q_max": 0.4, "rsi_min": 35, "sec_ret20_min": -10, "tp_pct": 0.30, "sl_pct": 0.12, "max_hold_days": 60}), + review_focus="①恐慌情绪是否缓解(大盘RSI修复→策略逻辑失效)②个股强势是否延续③行业抗跌性④止损/止盈空间⑤是否超60日持有期", + holding_style="波段(恐慌错杀修复)", + status="active", version="v2", + scanner_file="s2_panic_v2_scanner.py", + doc_ref="", + ), + dict( + strategy_name="leader", + display_name="龙头回调买点", + aliases=json.dumps(["leader", "bull_trend"]), + market="A", regime="trend_up", + family="momentum", + summary="牛市(trend_up)买入龙头股回调到 MA20 附近的健康回调买点(close>MA20、dist_ma20∈[-3%,+2%]、行业强势、市值>0.3分位),不追高。", + entry_logic="close>MA20(趋势向上);dist_ma20∈[-3%,+2%](回调到MA20附近);sector_above_ma20=1 或 sector_ret20>0(行业强势);mcap_q>0.3(龙头非小盘)。", + exit_logic="突破前高后趋势延续持有;跌破MA20/趋势线止损;行业转弱退出。", + key_params=json.dumps({"regime": "trend_up", "dist_ma20_range": [-3,2], "mcap_q_min": 0.3}), + review_focus="①牛市温区是否延续(trend_up→其他=龙头策略失效)②回调买点是否仍成立③行业强度④止损位(跌破MA20)合理性", + holding_style="趋势持有(龙头主升)", + status="active", version="v1", + scanner_file="leader_scanner.py", + doc_ref="", + ), +] + +def seed(conn): + for s in SEEDS: + hp = hashlib.md5((s.get("key_params") or "").encode()).hexdigest()[:12] + old = conn.execute("SELECT params_hash FROM strategy_defs WHERE strategy_name=?", (s["strategy_name"],)).fetchone() + if old and old[0] == hp: + print(f" = {s['strategy_name']} 定义未变,跳过") + continue + s2 = dict(s) + s2["params_hash"] = hp + s2["updated_at"] = "2026-08-18 17:30:00" + s2["updated_by"] = "setup_strategy_defs.py" + cols = ",".join(s2.keys()) + ph = ",".join("?" * len(s2)) + conn.execute( + f"INSERT INTO strategy_defs ({cols}) VALUES ({ph}) " + f"ON CONFLICT(strategy_name) DO UPDATE SET " + f"display_name=excluded.display_name, aliases=excluded.aliases, market=excluded.market, " + f"regime=excluded.regime, family=excluded.family, summary=excluded.summary, " + f"entry_logic=excluded.entry_logic, exit_logic=excluded.exit_logic, " + f"key_params=excluded.key_params, review_focus=excluded.review_focus, " + f"holding_style=excluded.holding_style, status=excluded.status, " + f"retired_reason=excluded.retired_reason, superseded_by=excluded.superseded_by, " + f"params_hash=excluded.params_hash, needs_review=excluded.needs_review, " + f"doc_ref=excluded.doc_ref, scanner_file=excluded.scanner_file, " + f"version=excluded.version, updated_at=excluded.updated_at, updated_by=excluded.updated_by", + [s2[k] for k in s2]) + print(f" + {s['strategy_name']} 种子卡已写入") + +def main(): + conn = get_conn() + print("[1/3] 建表...") + create_tables(conn) + print("[2/3] 种子卡...") + seed(conn) + conn.commit() + print("[3/3] 校验...") + n = conn.execute("SELECT COUNT(*) FROM strategy_defs").fetchone()[0] + print(f" strategy_defs 共 {n} 条") + for r in conn.execute("SELECT strategy_name, display_name, status FROM strategy_defs ORDER BY strategy_name").fetchall(): + print(f" - {r[0]} | {r[1]} | {r[2]}") + conn.close() + print("DONE") + +if __name__ == "__main__": + main()