Files
MoFin/scripts/research/step46_final_v4.py
T
hmo b9c68a83a7 docs: 预测超跌反弹策略研究成果归档(方法论/策略文档/研究记录/脚本)
- 新增 strategy_research_methodology.md(由果及因/12维/铁律/支撑压力规范)
- 新增 predictive_oversold_strategy.md(v5定稿,年化18.57%)
- 新增 deployment-plan-predictive-oversold.md(整合部署计划)
- 归档 docs/research/(63份研究过程文档)+ scripts/research/(19个研究脚本)
- 更新 docs/README.md 文档中心(策略研究章节)
2026-08-10 14:37:21 +08:00

202 lines
8.0 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.
#!/usr/bin/env python3
"""step46_final_v4.py — 最终模拟 v4(负面过滤 + 限流)
在 step41 基础上,用数据提取的负面因子过滤(step43-45):
- 信号: 预测信号 + 大盘回撤<=-5%
- 负面过滤: 新闻年内>=中位 + 连跌<=2 + flow1>-1e7
- 单日限流5 + 10槽 + 15%仓位
对比: 年化/回撤 vs step37(v3基准)
"""
import numpy as np
import pandas as pd
import sqlite3
import sys
sys.path.insert(0, "/tmp")
from sr_calculator import SRCalculator
print("=== 加载 ===", flush=True)
panel = pd.read_pickle("/tmp/panel_12d.pkl")
panel = panel.sort_values(["code", "date"]).reset_index(drop=True)
panel["_key"] = panel["code"] + "_" + panel["date"]
pos_map = {k: i for i, k in enumerate(panel["_key"])}
dates = sorted(panel["date"].unique())
# 大盘/气氛
conn = sqlite3.connect("file:/home/hmo/MoFin/data/mofin.db?mode=ro", uri=True)
idx_df = pd.read_sql("SELECT date, close, high FROM stock_daily WHERE code='sh000001' ORDER BY date", conn)
idx_df["date"] = idx_df["date"].astype(str)
idx_df["hi60"] = idx_df["high"].rolling(60).max()
idx_df["mkt_dd60"] = (idx_df["close"] / idx_df["hi60"] - 1) * 100
close_arr = idx_df["close"].values
down_days, streak, prev = [], 0, None
for c in close_arr:
streak = streak + 1 if (prev is not None and c < prev) else 0
down_days.append(streak)
prev = c
idx_df["mkt_down_days"] = down_days
dd_map = dict(zip(idx_df["date"], idx_df["mkt_dd60"]))
down_map = dict(zip(idx_df["date"], idx_df["mkt_down_days"]))
panel["mkt_dd60"] = panel["date"].map(dd_map)
panel["mkt_down_days"] = panel["date"].map(down_map)
news_cnt = pd.read_sql("SELECT substr(date,1,10) d, COUNT(*) c FROM stock_news GROUP BY d", conn)
news_cnt["d"] = news_cnt["d"].astype(str)
news_cnt["mkt_news5"] = news_cnt["c"].rolling(5, min_periods=1).mean()
news_map = dict(zip(news_cnt["d"], news_cnt["mkt_news5"]))
panel["mkt_news5"] = panel["date"].map(lambda d: news_map.get(d, np.nan))
# 年内新闻分位
panel["year"] = panel["date"].str[:4]
panel["news_yq"] = panel.groupby("year")["mkt_news5"].transform(lambda x: x.rank(pct=True))
# 信号
sig_cond = (
(panel["mkt_rsi"] < 50) & (panel["mcap_q"] < 0.2) & (panel["pe_q"] < 0.2) &
(panel["news3"] >= 1) & (panel["sec_ret20"] < 0) & (panel["bias60"] < -20) &
(panel["mkt_dd60"] <= -5)
)
# 负面过滤
neg_cond = sig_cond & (panel["news_yq"] >= 0.5) & (panel["mkt_down_days"] <= 2) & (panel["flow1"] > -1e7)
print("基础信号:", sig_cond.sum(), "负面过滤后:", neg_cond.sum(), flush=True)
def build_cand(cond):
cand = panel[cond][["code", "date"]].copy()
cand = cand.sort_values(["code", "date"])
cand["prev"] = cand.groupby("code")["date"].shift(1)
cand["gap"] = (pd.to_datetime(cand["date"]) - pd.to_datetime(cand["prev"])).dt.days
cand = cand[(cand["prev"].isna()) | (cand["gap"] > 30)]
return cand
def prep(sigdf):
codes = sigdf["code"].unique().tolist()
ph = ",".join("?" * len(codes))
df = pd.read_sql("SELECT code, date, close, high, low FROM stock_daily WHERE code IN ({}) ORDER BY code, date".format(ph), conn, params=codes)
df["date"] = df["date"].astype(str)
df["code"] = df["code"].astype(str).str.zfill(6)
df = df.sort_values(["code", "date"]).reset_index(drop=True)
df["_key"] = df["code"] + "_" + df["date"]
dpos = {k: i for i, k in enumerate(df["_key"])}
sigdf["kidx"] = (sigdf["code"] + "_" + sigdf["date"]).map(dpos)
sigdf = sigdf.dropna(subset=["kidx"]).copy()
sigdf["kidx"] = sigdf["kidx"].astype(int)
sr = SRCalculator()
out = []
for r in sigdf.itertuples():
bars_code = sr.get_bars(r.code)
d_idx = bars_code.index[bars_code["date"] == r.date]
if len(d_idx) == 0:
continue
sr_full = sr.sr_full(r.code, d_idx[0])
pv = sr_full["pivot"]
chip = sr_full["chip"]
if not pv:
continue
sig_close = df["close"].values[r.kidx]
support = pv["s2"]
if chip and chip["chip_ss"] < sig_close:
support = max(pv["s2"], chip["chip_ss"])
resist = pv["r2"]
if chip and chip["chip_sr"] > sig_close:
resist = min(pv["r2"], chip["chip_sr"])
if support >= sig_close or resist <= sig_close:
continue
out.append({"code": r.code, "date": r.date, "kidx": r.kidx,
"sig_close": sig_close, "support": support, "resist": resist})
return pd.DataFrame(out), df
def run_sim(sigdf, df, max_daily=5, slots=10, hold=40, pos_frac=0.15, stop_buf=0.05):
dpos = {k: i for i, k in enumerate(df["_key"])}
closes = df["close"].values
highs = df["high"].values
lows = df["low"].values
sig_by_date = {}
for r in sigdf.itertuples():
sig_by_date.setdefault(r.date, []).append(r)
INIT_CAP = 1_000_000
positions = {}
cash = INIT_CAP
navs = []
for di, d in enumerate(dates):
for code in list(positions.keys()):
pos = positions[code]
k = dpos.get(code + "_" + d)
if k is None:
continue
hi, lo, cl = highs[k], lows[k], closes[k]
exited = False
if hi >= pos["tp"]:
sell_qty = pos["qty"] // 2
if sell_qty > 0:
cash += sell_qty * pos["tp"]
pos["qty"] -= sell_qty
if pos["qty"] <= 0:
del positions[code]
exited = True
if not exited and lo <= pos["stop"]:
cash += pos["qty"] * lo
del positions[code]
exited = True
if not exited and di - pos["entry_di"] >= hold:
cash += pos["qty"] * cl
del positions[code]
if d in sig_by_date:
bought = 0
for r in sig_by_date[d]:
if bought >= max_daily or len(positions) >= slots:
break
if r.code in positions:
continue
cur_nav = cash
for c, p in positions.items():
k = dpos.get(c + "_" + d)
px = closes[k] if k is not None else p["avg_cost"]
cur_nav += p["qty"] * px
pos_val = cur_nav * pos_frac
price = r.sig_close
if price <= 0:
continue
qty = int(pos_val / price)
if qty <= 0 or qty * price > cash:
continue
cash -= qty * price
positions[r.code] = {
"entry_di": di, "qty": qty, "avg_cost": price,
"stop": r.support * (1 - stop_buf), "tp": r.resist,
}
bought += 1
nav = cash
for c, p in positions.items():
k = dpos.get(c + "_" + d)
px = closes[k] if k is not None else p["avg_cost"]
nav += p["qty"] * px
navs.append(nav)
nav_arr = np.array(navs)
final = nav_arr[-1]
years = len(navs) / 250
cagr = ((final / INIT_CAP) ** (1 / years) - 1) * 100 if final > 0 else -100
cummax = np.maximum.accumulate(nav_arr)
dd = ((nav_arr - cummax) / cummax).min() * 100
return cagr, dd, nav_arr
print("\n=== 基准(无负面过滤,限流5)===", flush=True)
c0, d0, _ = run_sim(*prep(build_cand(sig_cond)))
print("基准: 年化={:.2f}% 回撤={:.1f}%".format(c0, d0), flush=True)
print("\n=== 负面过滤(限流5===", flush=True)
s1, df1 = prep(build_cand(neg_cond))
c1, d1, nav1 = run_sim(s1, df1)
print("负面过滤: 年化={:.2f}% 回撤={:.1f}% 信号={}".format(c1, d1, len(s1)), flush=True)
print("\n=== 负面过滤 限流参数 ===", flush=True)
for md in [3, 5, 8, 99]:
c, d, _ = run_sim(s1, df1, max_daily=md)
print("限{}: 年化={:.2f}% 回撤={:.1f}%".format(md, c, d), flush=True)
print("\n=== 分年净值(负面过滤+限流5===", flush=True)
nav_df = pd.DataFrame({"date": dates, "nav": nav1})
nav_df["year"] = nav_df["date"].str[:4]
yr = nav_df.groupby("year")["nav"].last()
for y, v in yr.items():
print(" {}: {:.0f} ({}%)".format(y, v, (v/1000000-1)*100))
print("\n=== 完成 ===", flush=True)