Files
MoFin/deploy/profile-scripts/hk_backtest.py
T

159 lines
6.1 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
# -*- 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")
sys.path.insert(0, "/home/hmo/MoFin") # strategy_lab.portfolio_sim(纯函数复用)
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 "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 过滤 trades8槽等权复利净值)
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
nav = 1.0
for t in sorted(wt, key=lambda x: x["entry_date"]):
nav *= (1 + t["profit_pct"] / 100 / slots)
return (nav - 1) * 100
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()