Files
MoFin/research/cause_effect_panic_alpha.py
T

77 lines
3.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 -*-
"""由果及因(四):全部恐慌日的 alpha 特征挖掘
方法:合并所有 mkt_rsi<25 的恐慌日信号(全市场),按特征分桶看 fwd_ret20/60 差异
目标:找到能区分"反弹强弱"的特征 → 作为信号 score 依据
关键:所有恐慌日合并(不只2025-04-09),保证样本量和统计意义
"""
import sys
sys.path.insert(0, "/home/hmo/MoFin")
import pandas as pd
import numpy as np
panel = pd.read_pickle("/tmp/panel_12d.pkl")
panel = panel.sort_values(["code", "date"]).reset_index(drop=True)
for w in [10, 20, 60]:
panel[f"fwd_ret{w}"] = panel.groupby("code")["close"].transform(lambda x, ww=w: x.shift(-ww) / x - 1) * 100
# 全部恐慌日(mkt_rsi<25
panic = panel[panel["mkt_rsi"] < 25].dropna(subset=["fwd_ret20"]).copy()
print(f"恐慌日总样本: {len(panic)} (涉及 {panic['date'].nunique()} 天, {panic['code'].nunique()} 只)")
print(f"恐慌日日期: {sorted(panic['date'].unique())}")
base = panic["fwd_ret20"].mean()
base_wr = (panic["fwd_ret20"] > 0).mean() * 100
print(f"恐慌日基线: 20d均值 {base:.2f}% 胜率 {base_wr:.1f}%")
print()
# 特征分桶(重点看哪些特征桶间差异大)
features = {
"ret5(5日跌幅)": ("ret5", [-40, -25, -18, -12, -8, -4, 0]),
"bias60(60日偏离)": ("bias60", [-45, -30, -20, -10, 0, 10, 30]),
"bias20(20日偏离)": ("bias20", [-30, -20, -10, 0, 10, 25]),
"dist_lo20(离低点)": ("dist_lo20", [0, 5, 10, 15, 25, 50]),
"rsi(个股)": ("rsi", [0, 15, 25, 35, 45, 60]),
"ret20(20日跌)": ("ret20", [-40, -25, -15, -5, 0, 10]),
"mcap_q(市值分位)": ("mcap_q", [0, 0.2, 0.4, 0.6, 0.8, 1]),
"pe_q(估值)": ("pe_q", [0, 0.2, 0.4, 0.6, 0.8, 1]),
"pb_q(市净率)": ("pb_q", [0, 0.2, 0.4, 0.6, 0.8, 1]),
"news3(新闻)": ("news3", [0, 1, 2, 4, 8]),
"vol_ratio(量比)": ("vol_ratio", [0, 0.8, 1.2, 1.8, 3, 6]),
"flow5(5日资金)": ("flow5", [-50, -20, 0, 20, 50]),
"mkt_adx(大盘ADX)": ("mkt_adx", [0, 20, 30, 40, 60]),
"mkt_ret20(大盘20d)": ("mkt_ret20", [-30, -20, -10, 0, 10]),
"sec_ret20(行业20d)": ("sec_ret20", [-30, -20, -10, 0, 10]),
"hi20_new(20日新高)": ("hi20_new", [0, 1]),
}
print("=" * 78)
print(f"{'特征':<16} {'桶':<14} {'n':>6} {'20d均值':>8} {'胜率':>7} {'超额':>7}")
print("=" * 78)
for label, (feat, edges) in features.items():
if feat not in panic.columns:
continue
s = panic.dropna(subset=[feat])
if len(s) < 200:
continue
results = []
for i in range(len(edges) - 1):
lo, hi = edges[i], edges[i + 1]
m = s[(s[feat] >= lo) & (s[feat] < hi)]
if len(m) < 50:
continue
wr = (m["fwd_ret20"] > 0).mean() * 100
results.append((f"{lo}~{hi}", len(m), m["fwd_ret20"].mean(), wr))
m = s[s[feat] >= edges[-1]]
if len(m) >= 50:
wr = (m["fwd_ret20"] > 0).mean() * 100
results.append((f">={edges[-1]}", len(m), m["fwd_ret20"].mean(), wr))
# 打印桶间差异(max-min 均值差)
if len(results) >= 2:
means = [r[2] for r in results]
spread = max(means) - min(means)
mark = " ★" if spread > 15 else (" ◈" if spread > 8 else "")
print(f"{label:<16} (桶间差 {spread:.1f}pp){mark}")
for name, n, mean, wr in results:
bar = "█" * int(abs(mean) / 2)
print(f"{'':<16} {name:<14} {n:>6} {mean:>7.2f}% {wr:>6.1f}% {mean-base:>+6.2f} {bar}")
print()