Files
big-qmt/labs/analysis/etf/sweep.py
2026-09-19 19:45:43 +08:00

145 lines
5.8 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.
"""为什么收益率低 / 改进方案量化:同一天数据、同一策略逻辑,只改参数与下单规模。
用法: 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)