feat: 港股策略独立模块——hk_strategies(三温区策略定义)+hk_backtest(独立回测资金模拟,复用portfolio_sim纯函数,零干扰A股)

This commit is contained in:
hmo
2026-08-15 03:03:42 +08:00
parent 216795708c
commit 57bee18ac4
2 changed files with 231 additions and 0 deletions
+157
View File
@@ -0,0 +1,157 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""hk_backtest.py — 港股策略回测+资金模拟(独立于A股strategy_lab
功能:
1. 读港股12维面板 + 港股策略库(hk_strategies.py)
2. 生成信号 → trades(止盈/止损/最大持有出场)
3. portfolio_sim 资金模拟(复用 strategy_lab.portfolio_sim 纯函数,不改A股框架)
4. 输出组合指标:平均年化/近1年/近6月/近3月(Ralph Loop验收标准)
用法:
python3 hk_backtest.py # 全部策略 + 组合
python3 hk_backtest.py --version hk_pe_mom # 单策略
"""
import sys
import argparse
import collections
from datetime import datetime, timedelta
import pandas as pd
sys.path.insert(0, "/home/hmo/MoFin/deploy/profile-scripts")
from hk_strategies import HK_STRATEGIES, get_hk_strategy
PANEL = "/tmp/panel_12d_hk.pkl"
COST = 0.0015 # 港股往返费率近似(佣金+印花税)
def load_panel():
p = pd.read_pickle(PANEL)
return p.sort_values(["code", "date"]).reset_index(drop=True)
def gen_trades(panel, strat):
"""按策略入场条件生成信号 → trades"""
e = strat["entry"]
cond = pd.Series(True, index=panel.index)
if "pe_q_max" in e:
cond &= panel["pe_q"] < e["pe_q_max"]
if "sec_ret20_min" in e:
cond &= panel["sec_ret20"] > e["sec_ret20_min"]
if "rsi_max" in e:
cond &= panel["rsi"] < e["rsi_max"]
if "bias60_max" in e:
cond &= panel["bias60"] < e["bias60_max"]
if "ret60_max" in e:
cond &= panel["ret60"] < e["ret60_max"]
if "rsi_delta_min" in e:
# 面板无rsi_delta,用rsi与前5日差值近似(面板已有rsi)
cond &= panel["rsi"] - panel.groupby("code")["rsi"].shift(5) >= e["rsi_delta_min"]
if "vol_ratio_min" in e:
cond &= panel["vol_ratio"] > e["vol_ratio_min"]
sig = panel[cond].copy()
sig = sig.dropna(subset=["close"])
print(f" {strat['version']} 信号: {len(sig)}", flush=True)
ex = strat["exit"]
tp, sl, maxh = ex["tp_pct"], ex["sl_pct"], ex["max_hold_days"]
bycode = {c: df for c, df in panel.groupby("code")}
trades = []
for _, s in sig.iterrows():
df = bycode.get(s["code"])
if df is None:
continue
idx = df.index[df["date"] == s["date"]]
if len(idx) == 0:
continue
pos = df.index.get_loc(idx[0])
fut = df.iloc[pos + 1: pos + maxh + 2]
if len(fut) < 2:
continue
ep = s["close"]
if ep <= 0:
continue
exit_p, reason, hold = None, None, 0
for k, fb in enumerate(fut.itertuples()):
if fb.close <= ep * (1 - sl):
exit_p, reason, hold = ep * (1 - sl), "stop", k + 1
break
if fb.close >= ep * (1 + tp):
exit_p, reason, hold = ep * (1 + tp), "target", k + 1
break
if exit_p is None:
exit_p, reason, hold = fut.iloc[-1]["close"], "time", maxh
trades.append({
"code": s["code"], "name": s["code"], "entry_date": s["date"],
"entry_price": round(ep, 2), "exit_price": round(exit_p, 2),
"profit_pct": round((exit_p - ep) / ep * 100, 2),
"exit_reason": reason, "hold_days": hold,
"score": 0, "score_comp": {}, "kelly": 0,
"stop_loss": round(ep * (1 - sl), 2), "target": round(ep * (1 + tp), 2),
"dna": False, "factors": {},
})
return trades
def portfolio_metrics(trades, capital=1000000, slots=8):
"""资金模拟 + 时间窗收益(Ralph Loop验收标准)"""
import strategy_lab as lab
pf = lab.portfolio_sim(trades, capital, max_positions=slots)
years_span = 7.5
cagr = pf.get("cagr_pct")
# 时间窗收益(按 entry_date 过滤 trades 做简单等权组合)
def window_return(months):
cutoff = (datetime(2026, 7, 24) - timedelta(days=int(months * 30.4))).strftime("%Y-%m-%d")
wt = [t for t in trades if t["entry_date"] >= cutoff]
if not wt:
return None
tot = sum(t["profit_pct"] for t in wt) / slots
return tot
return {
"cagr": cagr,
"year1": window_return(12),
"month6": window_return(6),
"month3": window_return(3),
"trades": len(trades),
"win_rate": sum(1 for t in trades if t["profit_pct"] > 0) / len(trades) * 100 if trades else 0,
}
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--version", default=None)
ap.add_argument("--slots", type=int, default=8)
args = ap.parse_args()
panel = load_panel()
print(f"港股面板: {len(panel)}\n", flush=True)
versions = [args.version] if args.version else list(HK_STRATEGIES.keys())
all_trades = []
for v in versions:
strat = get_hk_strategy(v)
if not strat:
print(f"未知策略: {v}")
continue
print(f"=== {v} ({strat['name']}) ===", flush=True)
trades = gen_trades(panel, strat)
if not trades:
print(" 无交易\n", flush=True)
continue
m = portfolio_metrics(trades, slots=args.slots)
print(f" 交易{m['trades']} 胜率{m['win_rate']:.0f}% 组合年化{m['cagr']}% "
f"近1年{m['year1']:+.1f}% 近6月{m['month6']:+.1f}% 近3月{m['month3']:+.1f}%\n", flush=True)
all_trades += trades
if all_trades and not args.version:
print("=== 港股策略组合(全温区)===", flush=True)
m = portfolio_metrics(all_trades, slots=args.slots)
print(f" 组合: 交易{m['trades']} 胜率{m['win_rate']:.0f}% 组合年化{m['cagr']}% "
f"近1年{m['year1']:+.1f}% 近6月{m['month6']:+.1f}% 近3月{m['month3']:+.1f}%", flush=True)
ok = (m['cagr'] or 0) > 10 and (m['year1'] or 0) > 10 and (m['month6'] or 0) > 5 and (m['month3'] or 0) > 0
print(f" 验收: {'✅ 达标' if ok else '❌ 未达标'}(年化>10/近1年>10/近6月>5/近3月>0", flush=True)
if __name__ == "__main__":
main()