feat: B组挖掘v5——绝对大涨目标(60d>=30%)+多参数模拟验证(tp20-30/sl10-12),修正相对分位太宽问题

This commit is contained in:
xxm
2026-08-16 17:33:43 +08:00
parent 566c9265c1
commit 3491b84932
+55 -48
View File
@@ -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