"""为什么收益率低 / 改进方案量化:同一天数据、同一策略逻辑,只改参数与下单规模。 用法: py -3.14 -B analysis/etf/sweep.py """ from dataclasses import replace import statistics import sys from pathlib import Path HERE = Path(__file__).resolve().parent sys.path.insert(0, str(HERE)) from backtest import ( # noqa: E402 START_CASH, SYMBOLS, SYMBOL_PARAMS, EtfDefaults, EtfSymbolConfig, analyze, fetch_daily, precondition, simulate, ) data = {code: fetch_daily(code) for code in SYMBOLS} BASE = EtfDefaults() _CACHE: dict[tuple, dict] = {} def pre_for(defaults, params): """按"影响指标计算"的参数缓存 precondition 结果。 注意:``channel_pct`` / ``atr_period`` / ``channel_period`` / ``min_grid_pct`` 与逐标的 ``atr_multiplier`` 会改变指标,必须进缓存键;``add_pct`` / ``min_profit_pct`` 只影响 下单判定,不进键。 """ key = ( defaults.channel_pct, defaults.atr_period, defaults.channel_period, defaults.min_grid_pct, tuple(sorted((c, params[c]["atr_multiplier"]) for c in params)), ) if key not in _CACHE: _CACHE[key] = { c: precondition(data[c], EtfSymbolConfig(**params[c]), defaults) for c in data } return _CACHE[key] def run(label, *, defaults=None, params=None, sizer=None, fill_mode="touch"): defaults = defaults or BASE params = params or SYMBOL_PARAMS result = simulate(data, defaults=defaults, symbol_params=params, sizer=sizer, fill_mode=fill_mode, precomputed=pre_for(defaults, params)) stats = analyze(result) adds = sum(1 for f in result["fills"] if f.kind == "add") bases = sum(1 for f in result["fills"] if f.kind == "base") trips = [ (f.price * f.volume if f.side == "SELL" else -f.price * f.volume) - f.fee for f in result["fills"] ] return { "label": label, "equity": stats["final_equity"] - result["start_cash"], "ret_pct": stats["return_pct"], "dd_pct": stats["max_dd_pct"], "bases": bases, "adds": adds, "exits": sum(1 for f in result["fills"] if f.kind == "exit"), "roi_on_deployed_pct": ( (stats["final_equity"] - result["start_cash"]) / stats["avg_deployed"] * 100 if stats["avg_deployed"] else 0.0 ), "util_pct": stats["avg_util_pct"], "max_util_pct": stats["max_deployed"] / result["start_cash"] * 100, "fees": stats["fees"], } def show(rows): print(f"{'场景':46} {'权益变动':>10} {'收益率':>7} {'回撤':>6} {'底仓':>4} {'补仓':>4} " f"{'占用ROI':>8} {'平均占用':>8} {'峰值占用':>8}") for r in rows: print(f"{r['label'][:46]:46} {r['equity']:10.2f} {r['ret_pct']:6.2f}% {r['dd_pct']:5.2f}% " f"{r['bases']:4} {r['adds']:4} {r['roi_on_deployed_pct']:7.2f}% " f"{r['util_pct']:7.2f}% {r['max_util_pct']:7.2f}%") def sized(shares): """把逐标的 buy_shares / max_shares 同步放大,保持 10 档容量不变。""" return {c: {**SYMBOL_PARAMS[c], "buy_shares": shares, "max_shares": shares * 10} for c in SYMBOLS} print("=" * 132) print("A. 只放大单档规模(其余参数一律不动)") print("=" * 132) show([run(f"buy_shares={n}(现值 1000)", params=sized(n)) for n in (1000, 2000, 5000, 10000, 20000)]) print() print("=" * 132) print("B. 按账户资金比例下单(sizer,替代固定股数;每档 = 现金的 x%)") print("=" * 132) rows = [] for pct in (0.02, 0.05, 0.10, 0.20): sizer = (lambda p: (lambda equity, price: int(equity * p / price)))(pct) rows.append(run(f"每档 = 现金 {pct:.0%}", params=sized(20000), sizer=sizer)) show(rows) print() print("=" * 132) print("C. 入场/补仓/止盈参数(规模固定 buy_shares=2000)") print("=" * 132) P2 = sized(2000) show([ run("基准参数(add 3% / profit 1% / channel 15%)", params=P2), run("add_pct=2%", defaults=replace(BASE, add_pct=2.0), params=P2), run("add_pct=1.5%", defaults=replace(BASE, add_pct=1.5), params=P2), run("add_pct=1.0%", defaults=replace(BASE, add_pct=1.0), params=P2), run("min_profit_pct=0.6%", defaults=replace(BASE, min_profit_pct=0.6), params=P2), run("min_profit_pct=3%", defaults=replace(BASE, min_profit_pct=3.0), params=P2), run("channel_pct=10%", defaults=replace(BASE, channel_pct=10.0), params=P2), run("channel_pct=30%", defaults=replace(BASE, channel_pct=30.0), params=P2), run("add 1.5% + profit 0.6%", defaults=replace(BASE, add_pct=1.5, min_profit_pct=0.6), params=P2), run("add 1.5% + profit 0.6% + channel 30%", defaults=replace(BASE, add_pct=1.5, min_profit_pct=0.6, channel_pct=30.0), params=P2), ]) print() print("=" * 132) print("D. 组合方案(把 A/B/C 的结论叠起来)") print("=" * 132) best = [] for shares in (2000, 5000, 10000): for add_pct, profit in ((1.5, 0.6), (1.5, 1.0), (2.0, 0.6), (1.0, 0.6)): label = f"buy={shares} add={add_pct}% profit={profit}%" best.append(run(label, defaults=replace(BASE, add_pct=add_pct, min_profit_pct=profit), params=sized(shares))) best.sort(key=lambda r: -r["equity"]) show(best[:10]) print() print("=" * 132) print("E. 现金利用率天花板:每档 = 现金 10%,同时放开通道与补仓(看能否把 20 万用起来)") print("=" * 132) rows = [] for add_pct, profit, channel in ((3.0, 1.0, 15.0), (1.5, 0.6, 30.0), (1.0, 0.6, 30.0), (1.0, 0.5, 40.0)): sizer = lambda equity, price: int(equity * 0.10 / price) rows.append(run(f"add={add_pct}% profit={profit}% channel={channel}% 每档10%现金", defaults=replace(BASE, add_pct=add_pct, min_profit_pct=profit, channel_pct=channel), params=sized(20000), sizer=sizer)) show(rows)