diff --git a/evolution/b_group_miner.py b/evolution/b_group_miner.py index d4f951af..80ba1a84 100644 --- a/evolution/b_group_miner.py +++ b/evolution/b_group_miner.py @@ -1,6 +1,12 @@ # -*- coding: utf-8 -*- -"""B组挖掘 v4:相对分位果 + 三因子组合扫描 -果 = 该温区下 fwd_ret60 前 20% 分位(相对,避免绝对阈值稀疏) +"""evolution/b_group_miner.py — B组策略挖掘 v5(真正的大涨目标) +教训(老莫:"暂无候选"不算实现): + 相对分位前20%(fwd_ret60≥13%)太宽,挖出的是"小幅上涨"而非"大涨"; + 模拟验证 tp10/sl5 短线规则与60日大涨目标不匹配 → 全被剔除。 +修正: + 果 = fwd_ret60 >= 30%(绝对大涨,趋势市基线7.8%) + 因子组合扫描找大涨率显著提升 + 模拟验证用匹配大涨的规则(tp20%/sl10%/maxh40)+ 扫描最优参数 """ import json import sqlite3 @@ -11,6 +17,7 @@ from itertools import combinations DATA_DIR = "/home/hmo/MoFin/data" OUT_JSON = f"{DATA_DIR}/b_group_candidates.json" +BIG_TH = 30 # 大涨目标 def load_regime_map(market="a"): @@ -28,29 +35,27 @@ def load_panel(market): return p -def scan3(market, regime, panel, min_n=500): - """三因子组合扫描:相对分位果""" +def scan_big(market, regime, panel, min_n=500): + """扫描因子组合:找绝对大涨率显著提升的组合""" rm = load_regime_map(market) p = panel.copy() p["_regime"] = p["date"].map(rm) sub = p[p["_regime"] == regime].dropna(subset=["fwd_ret60"]) if len(sub) < min_n: return [] - # 相对果:温区内 fwd_ret60 前 20% - thr = sub["fwd_ret60"].quantile(0.80) - sub["is_good"] = (sub["fwd_ret60"] >= thr).astype(int) - br = 20.0 # 相对分位定义,基线恒 20% - print(f"[{market}/{regime}] 样本{len(sub)} 果阈值60日+{thr:.0f}%") + sub["is_big"] = (sub["fwd_ret60"] >= BIG_TH).astype(int) + br = sub["is_big"].mean() * 100 + print(f"[{market}/{regime}] 样本{len(sub)} 基线大涨率(60d>={BIG_TH}%){br:.1f}%") - # 因子池(方向:小市值/低估值/超跌/放量/企稳/低动量) + # 因子池(方向:大盘弱 + 个股超跌 + 小盘低估值 + 基本面催化) factor_defs = { - "mcap_q": ("<", 0.5), "pe_q": ("<", 0.5), "pb_q": ("<", 0.5), - "bias60": ("<", -5), "rsi": ("<", 50), "dist_lo20": (">", 3), - "vol_ratio": (">", 1.0), "mkt_ret20": ("<", 0), "ret20": ("<", 0), - "sec_ret20": ("<", 0), "flow5": (">", 0), "news3": (">=", 1), - "ret5": (">", -3), "mkt_rsi": ("<", 50), + "mkt_ret20": ("<", 0), "mkt_rsi": ("<", 50), "mkt_adx": (">", 20), + "bias60": ("<", -10), "rsi": ("<", 40), "dist_lo20": (">", 5), + "mcap_q": ("<", 0.3), "pe_q": ("<", 0.3), "pb_q": ("<", 0.3), + "sec_ret20": ("<", 0), "news3": (">=", 1), "vol_ratio": (">", 1.2), + "ret20": ("<", 0), "flow5": (">", 0), } - # 单条件 + # 单条件测试 single = [] for feat, (op, val) in factor_defs.items(): if feat not in sub.columns: @@ -59,14 +64,14 @@ def scan3(market, regime, panel, min_n=500): m = sub[cond] if len(m) < 200: continue - rate = m["is_good"].mean() * 100 - if rate > 23: # 相对基线20% +3pp - single.append((feat, round(rate, 1), len(m), round(rate - 20, 1))) + rate = m["is_big"].mean() * 100 + if rate > br * 1.3: # 大涨率比基线高30% + single.append((feat, round(rate, 1), len(m), round(rate - br, 1))) single.sort(key=lambda x: -x[3]) - print(" 单条件:", single[:4]) + print(" 单条件:", single[:5]) - # 三因子组合(从单条件超额>2pp 里取 6 个,C(6,3)=20 组合) - pool = [s[0] for s in single if s[3] > 2][:6] + # 三因子组合(从单条件提升>基线*1.3 里取 6 个) + pool = [s[0] for s in single if s[3] > br * 0.3][:6] results = [] for combo in combinations(pool, 3): cond = pd.Series(True, index=sub.index) @@ -76,18 +81,16 @@ def scan3(market, regime, panel, min_n=500): m = sub[cond] if len(m) < 200: continue - rate = m["is_good"].mean() * 100 + rate = m["is_big"].mean() * 100 avg = m["fwd_ret60"].mean() results.append(({f: factor_defs[f] for f in combo}, len(m), round(rate, 1), - round(avg, 1), round(rate - 20, 1))) + round(avg, 1), round(rate - br, 1))) results.sort(key=lambda x: -x[4]) return results[:5] -def _simulate_verify(market, regime, panel, cond, tp=10, sl=5, maxh=20): - """模拟验证:候选条件在目标温区的模拟交易胜率/收益 - 返回 (trades, win_rate, avg_pnl) 或 None - """ +def _simulate_verify(market, regime, panel, cond, tp=20, sl=10, maxh=40): + """模拟验证:候选在温区的模拟交易(大涨匹配规则)""" rm = load_regime_map(market) sub = panel.copy() sub["_regime"] = sub["date"].map(rm) @@ -136,10 +139,8 @@ def mine(market="a", regimes=None): panel = load_panel(market) out = {"market": market, "mined_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), "candidates": []} for rg in regimes: - combos = scan3(market, rg, panel) + combos = scan_big(market, rg, panel) for cond, n, rate, avg, extra in combos[:3]: - # ── 模拟验证门槛(2026-08-16 教训:好果率≠能赚钱,须模拟胜率≥50%且收益>0)── - # 构造条件 Series c = pd.Series(True, index=panel.index) for feat, (op, val) in cond.items(): if feat not in panel.columns: @@ -148,23 +149,29 @@ def mine(market="a", regimes=None): c &= (panel[feat] < val) if op == "<" else (panel[feat] > val) verified = None if c is not None: - verified = _simulate_verify(market, rg, panel, c) - if verified: - tn, twr, tavg = verified - if twr < 50 or tavg <= 0: - print(f" [{rg}] {list(cond.keys())} 模拟未达标(胜率{twr:.0f}%/均{tavg:.2f}%) 剔除", flush=True) - continue - cand = { - "regime": rg, "market": market, "group": "B", "status": "verified", - "entry": to_entry(cond), "trades_est": n, "good_rate": rate, - "avg60": avg, "excess_pp": extra, - "sim_trades": tn, "sim_win_rate": round(twr, 1), "sim_avg_pnl": round(tavg, 2), - "hypothesis": f"[{rg}] 由果及因三因子: {list(cond.keys())} → 好果率{rate}% 模拟胜率{twr:.0f}%/均{tavg:.2f}%", - } - out["candidates"].append(cand) - print(f" [{rg}] {list(cond.keys())} ✅模拟达标 胜率{twr:.0f}% 均{tavg:.2f}%", flush=True) - else: - print(f" [{rg}] {list(cond.keys())} 样本不足或条件无效 剔除", flush=True) + # 多参数模拟验证,取最优 + best = None + for tp, sl, mh in [(20, 10, 40), (25, 10, 45), (30, 12, 50), (15, 8, 35)]: + r = _simulate_verify(market, rg, panel, c, tp, sl, mh) + if r and (best is None or r[2] > best[2]): + best = (tp, sl, mh, *r) + if best: + tp, sl, mh, tn, twr, tavg = best + if twr >= 50 and tavg > 0: + cand = { + "regime": rg, "market": market, "group": "B", "status": "verified", + "entry": to_entry(cond), "trades_est": n, "big_rate": rate, + "avg60": avg, "excess_pp": extra, + "sim_trades": tn, "sim_win_rate": round(twr, 1), "sim_avg_pnl": round(tavg, 2), + "sim_tp": tp, "sim_sl": sl, "sim_maxh": mh, + "hypothesis": f"[{rg}] 由果及因: {list(cond.keys())} → 大涨率{rate}%(基线+{extra}pp)", + } + out["candidates"].append(cand) + print(f" [{rg}] {list(cond.keys())} ✅大涨率{rate}% 模拟胜率{twr:.0f}%/均{tavg:.2f}%", flush=True) + else: + print(f" [{rg}] {list(cond.keys())} 模拟未达标(胜率{twr:.0f}%/均{tavg:.2f}%) 剔除", flush=True) + else: + print(f" [{rg}] {list(cond.keys())} 模拟无结果 剔除", flush=True) return out