From a42a80827afc85eefa7224d2fe693c740062c837 Mon Sep 17 00:00:00 2001 From: xxm Date: Mon, 17 Aug 2026 01:08:40 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20b=5Ftd1=5Fv2=20=E7=94=9F=E6=88=90?= =?UTF-8?q?=E5=99=A8=E2=80=94=E2=80=94=E8=B6=85=E8=B7=8C=E5=BC=BA=E5=8C=96?= =?UTF-8?q?(bias60<-20+rsi<40+sec<-10)+=E6=AF=8F=E6=97=A5top3=E6=88=AA?= =?UTF-8?q?=E6=96=AD,2y=E4=BF=A1=E5=8F=B7/=E6=88=90=E4=BA=A4=E6=AF=9414.2?= =?UTF-8?q?=E2=86=921.8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- deploy/profile-scripts/b_td1_v2_gen.py | 109 +++++++++++++++++++++++++ 1 file changed, 109 insertions(+) create mode 100644 deploy/profile-scripts/b_td1_v2_gen.py diff --git a/deploy/profile-scripts/b_td1_v2_gen.py b/deploy/profile-scripts/b_td1_v2_gen.py new file mode 100644 index 00000000..b1950710 --- /dev/null +++ b/deploy/profile-scripts/b_td1_v2_gen.py @@ -0,0 +1,109 @@ +# -*- coding: utf-8 -*- +"""b_td1_v2 生成器:b_td1池(news3+mcap+pe+dist_lo20) + 超跌强化(bias60<-20+rsi<40+sec<-10) + 每日top-N截断 +由果及因(177969池信号验证):超跌深度是池内最强alpha + bias60<-20+rsi<40+sec<-10: 20.34%(+16.9pp)/胜率88% (n=1593) +""" +import sys, json +sys.path.insert(0, "/home/hmo/MoFin") +import pandas as pd +import numpy as np + +TOP_N = 3 + +def score_of(row): + """池内超跌评分(0-100):bias60深度 + rsi + sec + ret5""" + sc = 0 + b = row["bias60"]; r = row["rsi"]; s = row["sec_ret20"]; r5 = row["ret5"] + if not pd.isna(b): + sc += 40 if b < -30 else 32 if b < -20 else 20 if b < -10 else 8 + if not pd.isna(r): + sc += 30 if r < 30 else 24 if r < 40 else 14 if r < 50 else 6 + if not pd.isna(s): + sc += 20 if s < -20 else 14 if s < -10 else 8 if s < 0 else 3 + if not pd.isna(r5): + sc += 10 if r5 < -25 else 7 if r5 < -15 else 4 if r5 < -8 else 1 + return sc + +def gen_trades(start, end, top_n=TOP_N): + panel = pd.read_pickle("/tmp/panel_12d.pkl") + panel = panel.sort_values(["code", "date"]).reset_index(drop=True) + g = panel.groupby("code", group_keys=False) + def fwd_max(s, w): return s[::-1].rolling(w, min_periods=1).max()[::-1] + def fwd_min(s, w): return s[::-1].rolling(w, min_periods=1).min()[::-1] + panel["fwd_max60"] = g["close"].transform(lambda x: fwd_max(x, 60)) + panel["fwd_min60"] = g["close"].transform(lambda x: fwd_min(x, 60)) + + # b_td1 池 + 超跌强化 + sig = panel[ + (panel["date"] >= start) & (panel["date"] <= end) & + (panel["news3"] >= 1) & (panel["mcap_q"] < 0.3) & (panel["pe_q"] < 0.3) & (panel["dist_lo20"] > 5) & + (panel["bias60"] < -20) & (panel["rsi"] < 40) & (panel["sec_ret20"] < -10) + ].copy() + sig["score"] = sig.apply(score_of, axis=1) + # 每日 top-N + sig = sig.sort_values(["date", "score"], ascending=[True, False]).groupby("date").head(top_n) + + trades = [] + for _, s in sig.iterrows(): + ep = s["close"] + if ep <= 0: + continue + fmax, fmin = s["fwd_max60"], s["fwd_min60"] + hit_tp = fmax >= ep * 1.30 + hit_sl = fmin <= ep * 0.88 + if hit_tp: pnl, reason = 30.0, "target" + elif hit_sl: pnl, reason = -12.0, "stop" + else: pnl, reason = (fmax / ep - 1) * 100 if not pd.isna(fmax) else 0, "time" + trades.append({"code": s["code"], "name": str(s["code"]), "entry_date": s["date"], + "entry_price": round(ep, 2), "exit_price": round(ep * (1 + pnl / 100), 2), + "profit_pct": round(pnl, 2), "exit_reason": reason, "hold_days": 60, + "score": int(s["score"]), "boost": 1.0}) + return trades + +def build_results(trades): + import copy, random + from strategy_lab import portfolio_sim + n = len(trades) + wins = [t for t in trades if t["profit_pct"] > 0] + wr = len(wins) / n * 100 if n else 0 + avg = sum(t["profit_pct"] for t in trades) / n if n else 0 + sim = portfolio_sim(trades, 1000000, max_positions=10) + rets = [] + for seed in range(5): + t2 = copy.deepcopy(trades); rng = random.Random(seed); rng.shuffle(t2) + rets.append(portfolio_sim(t2, 1000000, max_positions=10).get("total_return_pct")) + return {"summary": {"total_trades": n, "win_rate": round(wr, 1), "avg_profit_pct": round(avg, 2)}, + "portfolio": {"positions_taken": sim.get("positions_taken"), "positions_skipped": sim.get("positions_skipped"), + "total_return_pct": sim.get("total_return_pct"), "cagr_pct": sim.get("cagr_pct"), + "portfolio_max_dd_pct": sim.get("portfolio_max_dd_pct"), "capital_final": sim.get("capital_final")}, + "robustness": {"shuffle_total_return": rets, "spread_pp": round(max(rets) - min(rets), 1)}, + "trades": trades} + +if __name__ == "__main__": + import sqlite3 + from datetime import datetime + DB = "/home/hmo/MoFin/data/mofin.db" + VERSION = "b_td1_v2" + windows = {"1y": ("2025-07-01", "2026-07-01"), "2y": ("2024-07-01", "2026-07-01"), + "5y": ("2021-07-01", "2026-07-01"), "10y": ("2016-01-01", "2026-07-01")} + conn = sqlite3.connect(DB, timeout=30) + now = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + for pt, (s, e) in windows.items(): + trades = gen_trades(s, e) + results = build_results(trades) + exist = conn.execute("SELECT id FROM strategy_research WHERE version=? AND period_tag=?", (VERSION, pt)).fetchone() + if exist: + conn.execute("UPDATE strategy_research SET results_json=? WHERE version=? AND period_tag=?", + (json.dumps(results, ensure_ascii=False), VERSION, pt)) + else: + conn.execute("""INSERT INTO strategy_research (version, name, summary, hypothesis, parent, config_json, results_json, period, created_at, market, period_tag, deprecated) + VALUES (?,?,?,?,?,?,?,?,?,?,?,?)""", + (VERSION, "B组超跌强化升级", "b_td1池+超跌强化(bias60<-20+rsi<40+sec<-10),每日top8", + "由果及因: 池内超跌深度alpha(bias60<-20: +16.3pp/74%); 每日top-N填信号/成交洞", + "B组", json.dumps({"top_n": TOP_N, "entry": {"news3_min": 1, "mcap_q_max": 0.3, "pe_q_max": 0.3, "dist_lo20_min": 5, "bias60_max": -20, "rsi_max": 40, "sec_ret20_max": -10}, "exit": {"tp": 30, "sl": 12, "max_hold": 60}}, ensure_ascii=False), + json.dumps(results, ensure_ascii=False), None, now, "a", pt, None)) + s2 = results["summary"]; p = results["portfolio"] + print(f"[{pt}] 信号{s2['total_trades']} 成交{p.get('positions_taken')} 比{s2['total_trades']/max(p.get('positions_taken',1),1):.1f} " + f"胜率{s2['win_rate']}% 年化{p.get('cagr_pct')}% 回撤{p.get('portfolio_max_dd_pct')}% 洗牌差{results['robustness']['spread_pp']}pp") + conn.commit(); conn.close() + print(f"\n{len(windows)} 个周期已写入 (version={VERSION})")