fix: hk_backtest用真实净值曲线(8槽资金管理)算窗口收益+加exit_date,修正复利失真

This commit is contained in:
hmo
2026-08-15 03:16:23 +08:00
parent dfb5ead698
commit 024eccd785
+87 -18
View File
@@ -96,8 +96,11 @@ def gen_trades(panel, strat):
break
if exit_p is None:
exit_p, reason, hold = fut.iloc[-1]["close"], "time", maxh
# 退出日期(用该股票日历往后推 hold 个交易日)
exit_date = fut.iloc[min(hold - 1, len(fut) - 1)]["date"] if hold > 0 else s["date"]
trades.append({
"code": s["code"], "name": s["code"], "entry_date": s["date"],
"exit_date": exit_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,
@@ -108,28 +111,94 @@ def gen_trades(panel, strat):
return trades
def portfolio_nav(trades, capital=1000000, slots=8):
"""8槽资金管理净值曲线:每日结算到期→入场(仓位满跳过)→持仓按成本估值。
返回 (nav_series: dict date->nav, stats)"""
if not trades:
return {}, {}
dates = sorted({t["entry_date"] for t in trades} | {t.get("exit_date", t["entry_date"]) for t in trades})
if not dates:
return {}, {}
# 用真实日历(stock_daily 港股日K日期)
conn = sqlite3.connect(DB)
cal = [r[0] for r in conn.execute(
"SELECT DISTINCT date FROM stock_daily WHERE date>=? AND date<=? AND length(code)=5 ORDER BY date",
(dates[0], dates[-1])).fetchall()]
conn.close()
if not cal:
cal = dates
cal_idx = {d: i for i, d in enumerate(cal)}
alloc = capital / slots
open_pos = [] # {exit_date, alloc, pnl}
cash = capital
nav_series = {}
skipped = 0
by_entry = collections.defaultdict(list)
for t in trades:
by_entry[t["entry_date"]].append(t)
for day in cal:
# 结算到期
still = []
for p in open_pos:
if p["exit_date"] <= day:
cash += p["alloc"] * (1 + p["pnl"] / 100)
else:
still.append(p)
open_pos = still
# 入场
for t in by_entry.get(day, []):
if len(open_pos) >= slots or cash < alloc:
skipped += 1
continue
open_pos.append({"exit_date": t.get("exit_date", day), "alloc": alloc,
"pnl": t["profit_pct"]})
cash -= alloc
# 净值
held_val = sum(p["alloc"] * (1 + p["pnl"] / 100) for p in open_pos)
nav_series[day] = cash + held_val
nav_series = {d: v for d, v in sorted(nav_series.items())}
return nav_series, {"skipped": skipped}
def window_returns(nav_series):
"""从净值曲线算窗口收益(近1年/6月/3月,对照最后日期)"""
if not nav_series:
return {"year1": None, "month6": None, "month3": None}
items = sorted(nav_series.items())
last_d, last_v = items[-1]
last_dt = datetime.strptime(last_d, "%Y-%m-%d")
out = {}
for label, days in [("year1", 365), ("month6", 182), ("month3", 91)]:
cutoff = (last_dt - timedelta(days=days)).strftime("%Y-%m-%d")
# 取 cutoff 后最近的净值点
base = None
for d, v in items:
if d >= cutoff:
base = v
break
out[label] = (last_v / base - 1) * 100 if base else None
return out
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
"""资金模拟 + 窗口收益(净值曲线"""
nav, stats = portfolio_nav(trades, capital, slots)
win = window_returns(nav)
# 年化(用首末净值)
cagr = None
if nav:
items = sorted(nav.items())
d0, v0 = items[0]
d1, v1 = items[-1]
yrs = max((datetime.strptime(d1, "%Y-%m-%d") - datetime.strptime(d0, "%Y-%m-%d")).days / 365.0, 0.5)
cagr = (((v1 / v0) ** (1 / yrs)) - 1) * 100 if v0 > 0 else None
return {
"cagr": cagr,
"year1": window_return(12),
"month6": window_return(6),
"month3": window_return(3),
"year1": win.get("year1"),
"month6": win.get("month6"),
"month3": win.get("month3"),
"trades": len(trades),
"skipped": stats.get("skipped", 0),
"win_rate": sum(1 for t in trades if t["profit_pct"] > 0) / len(trades) * 100 if trades else 0,
}