Files
MoFin/deploy/profile-scripts/s2_panic_v2_gen.py
T

153 lines
7.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
"""s2_panic_v2 正式生成器(2026-08-16 由果及因升级版)
洞审计结论:
原 s2_panic 信号无 score → portfolio_sim 按 code 顺序随机成交(洗牌漂移16pp)
s2 业绩 = 大恐慌日 beta2025-04-09 全市场 +19.85%),无选股 alpha
由果及因(72941 恐慌日信号,tp30/sl12/60日 评估):
alpha 入场组合:mkt_rsi<25 + mcap_q<0.4 + rsi>=35 + sec_ret20>=-10 → 胜率60.8% vs 基线30.7%
每日 top-N 截断:信号/成交比可控(top8: 141信号/86成交/比1.6
score 精细排序无效(诚实:top5 11.79% vs 随机 13.33%)——截断是解法,不假装排序有效
参数:top_n=8(每日最多8信号,10仓位槽内可控),出场 tp30/sl12/max60
"""
import sys, json
sys.path.insert(0, "/home/hmo/MoFin")
import pandas as pd
import numpy as np
TOP_N = 8
def alpha_score(mcap_q, rsi, sec_ret20, news3):
"""由果及因 alpha 评分(保留字段供 portfolio_sim 机制,诚实:精细排序无效)"""
sc = 0
if mcap_q is not None and not np.isnan(mcap_q):
sc += 40 if mcap_q < 0.2 else 32 if mcap_q < 0.4 else 24 if mcap_q < 0.6 else 16 if mcap_q < 0.8 else 8
if rsi is not None and not np.isnan(rsi):
sc += 30 if rsi >= 45 else 22 if rsi >= 35 else 12 if rsi >= 25 else 6
if sec_ret20 is not None and not np.isnan(sec_ret20):
sc += 20 if sec_ret20 >= 0 else 16 if sec_ret20 >= -10 else 8 if sec_ret20 >= -20 else 3
if news3 is not None and not np.isnan(news3):
sc += 10 if news3 >= 2 else 7 if news3 >= 1 else 2
return sc
def gen_trades(start, end, top_n=TOP_N):
"""生成 s2_panic_v2 在 [start,end] 窗口的信号 trades"""
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))
panic = panel[(panel["mkt_rsi"] < 25) & (panel["date"] >= start) & (panel["date"] <= end)].copy()
sig = panic[(panic["mcap_q"] < 0.4) & (panic["rsi"] >= 35) & (panic["sec_ret20"] >= -10)].copy()
sig["score"] = sig.apply(lambda r: alpha_score(r["mcap_q"], r["rsi"], r["sec_ret20"], r["news3"]), axis=1)
# 每日 top-N(score 降序,同日择优——虽精细排序无效,但高分不劣于随机,且机制一致)
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):
"""trades → results_jsonsummary + portfolio_sim"""
import copy, random
from strategy_lab import portfolio_sim
n = len(trades)
wins = [t for t in trades if t["profit_pct"] > 0]
losses = [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
avg_w = sum(t["profit_pct"] for t in wins) / len(wins) if wins else 0
avg_l = abs(sum(t["profit_pct"] for t in losses) / len(losses)) if losses else 1
pf = avg_w / avg_l if avg_l else 0
sim = portfolio_sim(trades, 1000000, max_positions=10)
# 洗牌稳健性(5次)
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),
"avg_win_pct": round(avg_w, 2), "avg_loss_pct": round(-avg_l, 2),
"avg_hold_days": round(sum(t["hold_days"] for t in trades) / n, 1) if n else 0,
"sharpe_ratio": round(sim.get("sharpe_ratio", 0) or 0, 2),
"profit_factor": round(pf, 2),
},
"portfolio": {
"capital_final": sim.get("capital_final"), "total_return_pct": sim.get("total_return_pct"),
"cagr_pct": sim.get("cagr_pct"), "portfolio_max_dd_pct": sim.get("portfolio_max_dd_pct"),
"positions_taken": sim.get("positions_taken"), "positions_skipped": sim.get("positions_skipped"),
},
"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 = "s2_panic_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)
# upsert strategy_research
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=?, updated_at=? WHERE version=? AND period_tag=?",
(json.dumps(results, ensure_ascii=False), now, 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, "S2恐慌买alpha升级", "恐慌日+小市值+强势+行业抗跌,每日top8截断",
"由果及因: alpha组合胜率60.8% vs 基线30.7%; 每日top-N截断填'多信号少成交'洞",
"S2家族", json.dumps({"top_n": TOP_N, "entry": {"mkt_rsi_max": 25, "mcap_q_max": 0.4, "rsi_min": 35, "sec_ret20_min": -10}, "exit": {"tp": 30, "sl": 12, "max_hold": 60}}, ensure_ascii=False),
json.dumps(results, ensure_ascii=False), None, now, "a", pt, None))
s = results["summary"]
p = results["portfolio"]
print(f"[{pt}] 信号{s['total_trades']} 成交{p.get('positions_taken')}{s['total_trades']/max(p.get('positions_taken',1),1):.1f} "
f"胜率{s['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)} 个周期已写入 strategy_research (version={VERSION})")