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

226 lines
8.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.
#!/usr/bin/env python3
"""step34_sim_support.py — 统一资金模拟 v2(科学支撑压力保护)
在 step33 基础上:
- 信号: 同 step33(预测信号+大盘门控)
- 入场: 信号日收盘买(step30验证最优)
- 止损: 跌破科学支撑位(枢轴S2/筹码支撑下方缓冲)→ 数据验证缓冲
- 止盈: 触及科学压力位(R2/筹码阻力)→ 分批卖
- 兜底: 持有40日强平
- 资金: 8槽 + 现金约束
"""
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"])}
print("面板:", len(panel), flush=True)
dates = sorted(panel["date"].unique())
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)
)
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)]
print("信号:", len(cand), flush=True)
# 加载K线(含high/low
conn = sqlite3.connect("file:/home/hmo/MoFin/data/mofin.db?mode=ro", uri=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"])}
print("K线:", len(df), flush=True)
cand["kloc"] = cand["code"] + "_" + cand["date"]
cand["kidx"] = cand["kloc"].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 = df["close"].iloc[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)
# 测试止损缓冲(数据扫描:支撑下方 0/2/5/8%)
closes = df["close"].values
highs = df["high"].values
lows = df["low"].values
dkey = df["_key"].values
dkeys_set = set(dkey)
print("\n=== 止损缓冲扫描(单信号)===", flush=True)
for buf in [0, 0.02, 0.05, 0.08]:
rets = []
for r in sigdf.itertuples():
stop = r.support * (1 - buf)
tp = r.resist
ret = None
for j in range(r.kidx+1, min(r.kidx+41, len(closes))):
hi, lo = highs[j], lows[j]
if hi >= tp:
ret = (tp / r.sig_close - 1) * 100
break
if lo <= stop:
ret = (lo / r.sig_close - 1) * 100
break
if ret is None:
ret = (closes[min(r.kidx+40, len(closes)-1)] / r.sig_close - 1) * 100
rets.append(ret)
a = np.array(rets)
print("止损缓冲{}%: n={} avg={:.2f}% wr={:.1f}% 止损率={:.1f}%".format(
buf*100, len(a), a.mean(), (a>0).mean()*100, (a<-5).mean()*100), flush=True)
# ── 最终模拟:止损缓冲(选最优)+ 分批止盈 ──
print("\n=== 统一资金模拟(支撑压力保护)===", flush=True)
INIT_CAP = 1_000_000
MAX_SIMULTANEOUS = 8
STOP_BUF = 0.05 # 支撑下方5%(先测,后续可用扫描最优)
# 构建 close_map
close_map = {}
for i in range(len(df)):
close_map[df["_key"].iloc[i]] = (closes[i], highs[i], lows[i])
sig_by_date = {}
for r in sigdf.itertuples():
sig_by_date.setdefault(r.date, []).append(r)
positions = {} # code -> {kidx, entry_di, qty, avg_cost, stop, tp, t1_done}
cash = INIT_CAP
nav_history = []
trades = []
date_index = {d: i for i, d in enumerate(dates)}
for di, d in enumerate(dates):
# 1) 离场:止损/止盈/40日
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
# 40日兜底
if di - pos["entry_di"] >= 40:
cash += pos["qty"] * cl
trades.append({"code": code, "date": d, "ret": (cl/pos["avg_cost"]-1)*100, "reason": "hold40"})
del positions[code]
continue
# 2) 买入
if d in sig_by_date:
for r in sig_by_date[d]:
if len(positions) >= MAX_SIMULTANEOUS:
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 * 0.12
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] = {
"kidx": r.kidx, "entry_di": di, "qty": qty, "avg_cost": price,
"stop": r.support * (1 - STOP_BUF), "tp": r.resist,
}
# 3) 净值
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/step34_nav.csv", index=False)
tr_df = pd.DataFrame(trades)
tr_df.to_csv("/tmp/step34_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("总收益: {:.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)