- 新增 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 文档中心(策略研究章节)
226 lines
8.4 KiB
Python
226 lines
8.4 KiB
Python
#!/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)
|