"""ETF 网格策略 vs 同期买入持有(同一区间、同一份日线)。 用法: py -3.14 -B analysis/etf/vs_hold.py """ import math import statistics import sys from pathlib import Path HERE = Path(__file__).resolve().parent sys.path.insert(0, str(HERE)) try: sys.stdout.reconfigure(encoding="utf-8", errors="replace") except Exception: pass from backtest import START_CASH, SYMBOLS, analyze, fetch_daily, simulate # noqa: E402 data = {code: fetch_daily(code) for code in SYMBOLS} result = simulate(data) stats = analyze(result) curve = result["curve"] start_day = curve[0][0] cash0 = result["start_cash"] grid_final = stats["final_equity"] # ---- 买入持有:在回测首日按各标的收盘价等权买入,持到期末 ---- bars_by_date = {code: {b["date"]: b for b in data[code]} for code in SYMBOLS} first_stamp = start_day.strftime("%Y%m%d") entry = {c: bars_by_date[c][first_stamp]["close"] for c in SYMBOLS} last_stamp = curve[-1][0].strftime("%Y%m%d") exit_ = {c: bars_by_date[c][last_stamp]["close"] for c in SYMBOLS} per_symbol_cash = cash0 / len(SYMBOLS) hold_shares = {c: per_symbol_cash / entry[c] for c in SYMBOLS} # 含零股,忽略整手限制 hold_curve = [] for day, _, _, _ in curve: stamp = day.strftime("%Y%m%d") value = sum(hold_shares[c] * bars_by_date[c].get(stamp, {"close": entry[c]})["close"] for c in SYMBOLS) hold_curve.append((day, value)) hold_final = hold_curve[-1][1] def profile(curve_points): """从权益曲线算总收益、最大回撤、日波动、夏普与卡玛。""" values = [v for _, v in curve_points] peak, max_dd = -math.inf, 0.0 for value in values: peak = max(peak, value) max_dd = max(max_dd, (peak - value) / peak) rets = [values[i] / values[i - 1] - 1 for i in range(1, len(values))] mean = statistics.fmean(rets) if rets else 0.0 sd = statistics.pstdev(rets) if len(rets) > 1 else 0.0 total = values[-1] / values[0] - 1 days = len(values) annual = (1 + total) ** (252 / days) - 1 if days else 0.0 return { "total_pct": total * 100, "annual_pct": annual * 100, "max_dd_pct": max_dd * 100, "daily_sd_pct": sd * 100, "sharpe": (mean / sd * math.sqrt(252)) if sd > 0 else float("nan"), "calmar": (total / max_dd) if max_dd > 0 else float("nan"), } grid_curve = [(day, equity) for day, equity, _, _ in curve] print("=" * 108) print(f"区间 {start_day} ~ {curve[-1][0]}({len(curve)} 个交易日),账户 {cash0:,.0f} 元") print("=" * 108) print(f"{'策略':26} {'期末权益':>12} {'总收益':>9} {'年化':>8} {'最大回撤':>9} " f"{'日波动':>8} {'夏普':>7} {'卡玛':>7}") for label, points in (("ETF 网格(现配置)", grid_curve), ("等权买入持有", hold_curve)): p = profile(points) print(f"{label:26} {points[-1][1]:12,.2f} {p['total_pct']:8.2f}% {p['annual_pct']:7.2f}% " f"{p['max_dd_pct']:8.2f}% {p['daily_sd_pct']:7.3f}% {p['sharpe']:7.2f} {p['calmar']:7.2f}") print() print("== 逐标的买入持有(同一区间)==") print(f"{'标的':11} {'期初':>8} {'期末':>8} {'涨跌':>9} {'期间最大回撤':>12}") for code in SYMBOLS: bars = [b for b in data[code] if b["date"] >= first_stamp and b["date"] <= last_stamp] closes = [b["close"] for b in bars] peak, dd = -math.inf, 0.0 for value in closes: peak = max(peak, value) dd = max(dd, (peak - value) / peak) print(f"{code:11} {closes[0]:8.3f} {closes[-1]:8.3f} " f"{(closes[-1] / closes[0] - 1) * 100:8.2f}% {dd * 100:11.2f}%") print() print("== 关键口径 ==") print(f" 网格:平均资金占用 {stats['avg_util_pct']:.2f}%(峰值 {stats['max_deployed'] / cash0 * 100:.2f}%)," f"买入名义 {stats['buy_amount']:,.0f}(换手 {stats['turnover_x']:.2f} 倍)," f"佣金 {stats['fees']:.2f}") print(f" 网格:占用部分的收益率(权益变动 ÷ 平均占用)= " f"{(grid_final - cash0) / stats['avg_deployed'] * 100:.2f}%") print(f" 买入持有:资金 100% 占用(从未空仓),无佣金/无交易") print(f" 网格:完整轮次 {len([f for f in result['fills'] if f.kind == 'exit'])} 次," f"胜率 100%(只在盈利 ≥{stats['params']['min_profit_pct']}% 时才卖)") # ---- 同风险口径:把网格放大到与买入持有相同回撤,比收益 ---- print() print("== 同回撤口径(把网格单档股数放大到回撤≈买入持有)==") from dataclasses import replace # noqa: E402 from backtest import SYMBOL_PARAMS, SYMBOLS as _SYMS # noqa: E402 hold_p = profile(hold_curve) print(f" 目标回撤:买入持有 {hold_p['max_dd_pct']:.2f}%") print(f"{'放大倍数':>8} {'权益变动':>12} {'总收益':>9} {'最大回撤':>9} {'平均占用':>9} {'夏普':>7} {'卡玛':>7}") for factor in (1, 4, 10, 20, 35): params = { c: {**SYMBOL_PARAMS[c], "buy_shares": max(100, int(SYMBOL_PARAMS[c]["buy_shares"] * factor)), "max_shares": max(1000, int(SYMBOL_PARAMS[c]["max_shares"] * factor))} for c in _SYMS } res = simulate(data, symbol_params=params) st = analyze(res) pts = [(day, eq) for day, eq, _, _ in res["curve"]] p = profile(pts) print(f"{factor:8}× {st['final_equity'] - cash0:12,.2f} {p['total_pct']:8.2f}% " f"{p['max_dd_pct']:8.2f}% {st['avg_util_pct']:8.2f}% {p['sharpe']:7.2f} {p['calmar']:7.2f}") print() print("注:放大是外推(同一段历史、同一批机会等比放大),不是新样本的验证;") print(" 放开股数后实际能否成交/是否滑点恶化,日线回测无法回答。")