This commit is contained in:
2026-09-19 19:45:43 +08:00
parent 7183cb45f8
commit 8131b158b4
60 changed files with 7669 additions and 909 deletions

View File

@@ -0,0 +1,130 @@
"""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(" 放开股数后实际能否成交/是否滑点恶化,日线回测无法回答。")