158 lines
6.0 KiB
Python
158 lines
6.0 KiB
Python
#!/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()
|