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