Files
MoFin/scripts/research/step37_final_v3.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

199 lines
7.6 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
"""step37_final_v3.py — 最终模拟 v3(阶段过滤 + 支撑压力 + 最优参数)
规则(全部数据驱动):
- 信号: 预测信号 + 大盘阶段过滤(排除高位/牛市)
保留: mkt_dd60 <= -5(大盘已回撤,非高位)
- 入场: 信号日收盘买
- 止损: 支撑下方5%step34验证)
- 止盈: 压力位分批卖
- 兜底: 40日
- 资金: 10槽 + 15%仓位(step35最优)
"""
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
dd_map = dict(zip(idx_df["date"], idx_df["mkt_dd60"]))
panel["mkt_dd60"] = panel["date"].map(dd_map)
# 信号:预测信号 + 阶段过滤(大盘已回撤,非高位)
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) # 阶段过滤:大盘已回撤(排除高位/牛市)
)
cand = panel[sig_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)]
cand["year"] = cand["date"].str[:4]
print("信号(含阶段过滤):", len(cand), flush=True)
print("月均: {:.1f}".format(len(cand)/max(len(cand["year"].unique()),1)/12), flush=True)
print("分年:", {str(y): int(c) for y, c in cand["year"].value_counts().sort_index().items()}, flush=True)
codes = cand["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"])}
closes = df["close"].values
highs = df["high"].values
lows = df["low"].values
cand["kidx"] = (cand["code"] + "_" + cand["date"]).map(dpos)
cand = cand.dropna(subset=["kidx"]).copy()
cand["kidx"] = cand["kidx"].astype(int)
print("可定位:", len(cand), flush=True)
# 支撑压力
sr = SRCalculator()
print("=== 计算支撑压力 ===", flush=True)
sig_list = []
for r in cand.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 = closes[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
sig_list.append({"code": r.code, "date": r.date, "kidx": r.kidx,
"sig_close": sig_close, "support": support, "resist": resist})
sigdf = pd.DataFrame(sig_list)
print("有支撑压力:", len(sigdf), flush=True)
# ── 模拟(10槽+40日+15%+支撑保护)──
INIT_CAP = 1_000_000
SLOTS = 10
HOLD = 40
POS_FRAC = 0.15
STOP_BUF = 0.05
sig_by_date = {}
for r in sigdf.itertuples():
sig_by_date.setdefault(r.date, []).append(r)
positions = {}
cash = INIT_CAP
nav_history = []
trades = []
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]
if hi >= pos["tp"]:
sell_qty = pos["qty"] // 2
if sell_qty > 0:
cash += sell_qty * pos["tp"]
pos["qty"] -= sell_qty
trades.append({"code": code, "date": d, "ret": (pos["tp"]/pos["avg_cost"]-1)*100, "reason": "tp"})
if pos["qty"] <= 0:
del positions[code]
continue
if lo <= pos["stop"]:
cash += pos["qty"] * lo
trades.append({"code": code, "date": d, "ret": (lo/pos["avg_cost"]-1)*100, "reason": "stop"})
del positions[code]
continue
if di - pos["entry_di"] >= HOLD:
cash += pos["qty"] * cl
trades.append({"code": code, "date": d, "ret": (cl/pos["avg_cost"]-1)*100, "reason": "hold40"})
del positions[code]
continue
if d in sig_by_date:
for r in sig_by_date[d]:
if 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,
}
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
nav_history.append({"date": d, "nav": nav, "n_pos": len(positions), "cash": cash})
nav_df = pd.DataFrame(nav_history)
nav_df.to_csv("/tmp/step37_nav.csv", index=False)
tr_df = pd.DataFrame(trades)
tr_df.to_csv("/tmp/step37_trades.csv", index=False)
final_nav = nav_df["nav"].iloc[-1]
total_ret = (final_nav / INIT_CAP - 1) * 100
years = (pd.to_datetime(nav_df["date"].iloc[-1]) - pd.to_datetime(nav_df["date"].iloc[0])).days / 365
cagr = ((final_nav / INIT_CAP) ** (1 / years) - 1) * 100 if years > 0 else 0
cummax = nav_df["nav"].cummax()
dd = (nav_df["nav"] / cummax - 1) * 100
max_dd = dd.min()
print("\n=== 最终v3(阶段过滤)结果 ===", flush=True)
print("总收益: {:.1f}%".format(total_ret))
print("年化(CAGR): {:.2f}%".format(cagr))
print("最大回撤: {:.1f}%".format(max_dd))
print("持仓峰值: {}".format(nav_df["n_pos"].max()))
print("交易笔数: {}".format(len(tr_df)))
if len(tr_df) > 0:
print("平均收益: {:.2f}% 胜率: {:.1f}%".format(tr_df["ret"].mean(), (tr_df["ret"]>0).mean()*100))
print("卖出原因:", tr_df["reason"].value_counts().to_dict())
nav_df["year"] = nav_df["date"].str[:4]
yr_end = nav_df.groupby("year")["nav"].last()
print("\n分年净值:")
for y, v in yr_end.items():
print(" {}: {:.0f} ({}%)".format(y, v, (v/INIT_CAP-1)*100))
print("现金占比均值: {:.1f}%".format((nav_df["cash"]/nav_df["nav"]).mean()*100))
print("\n=== 完成 ===", flush=True)