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

121 lines
5.1 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/drawdown.py
"""
import sys
from pathlib import Path
HERE = Path(__file__).resolve().parent
sys.path.insert(0, str(HERE))
# Windows 控制台默认 GBK报告里用了「」等字符统一切到 UTF-8 输出。
try:
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
except Exception:
pass
from analysis import round_trips # noqa: E402
from backtest import SYMBOLS, analyze, fetch_daily, simulate # noqa: E402
data = {code: fetch_daily(code) for code in SYMBOLS}
result = simulate(data)
stats = analyze(result)
by_date = {code: {bar["date"]: bar for bar in data[code]} for code in data}
# 逐日还原:每个标的的"本轮累计净买入成本"与"当前持仓量"
cost = {code: 0.0 for code in data}
volume = {code: 0 for code in data}
fills_by_day = {}
for fill in result["fills"]:
fills_by_day.setdefault(fill.day.strftime("%Y%m%d"), []).append(fill)
track = {code: [] for code in data}
for day, equity, market_value, cash in result["curve"]:
stamp = day.strftime("%Y%m%d")
for fill in fills_by_day.get(stamp, []):
amount = fill.price * fill.volume
if fill.side == "BUY":
cost[fill.code] += amount
volume[fill.code] += fill.volume
else:
# 卖出按比例冲减成本基数(与均价法一致)
if volume[fill.code] > 0:
ratio = fill.volume / volume[fill.code]
cost[fill.code] *= max(0.0, 1 - ratio)
volume[fill.code] -= fill.volume
for code in data:
bar = by_date[code].get(stamp)
if bar is None:
continue
track[code].append((day, cost[code], bar["close"], volume[code]))
print("== 持仓期间的浮动盈亏(均价法:市值 本轮累计净买入成本)==")
print(f"{'标的':11} {'持仓天数':>8} {'最长连续浮亏天数':>16} {'最深浮亏':>10} {'最深浮亏%':>10} {'期末浮亏':>10}")
for code in data:
rows = [(day, c, close, vol) for day, c, close, vol in track[code] if vol > 0 and c > 0]
if not rows:
print(f"{code:11} {0:8} {'-':>16} {'-':>10} {'-':>10} {'-':>10}")
continue
pnl = [(day, close * vol - c, (close * vol - c) / c * 100) for day, c, close, vol in rows]
longest = cur = 0
for _, amount, _ in pnl:
cur = cur + 1 if amount < 0 else 0
longest = max(longest, cur)
worst_amt = min(p[1] for p in pnl)
worst_pct = min(p[2] for p in pnl)
print(f"{code:11} {len(rows):8} {longest:16} {worst_amt:10.0f} {worst_pct:9.2f}% {pnl[-1][1]:10.0f}")
print()
print("== 持有天数分布(完整轮次)==")
trips = round_trips(result["fills"])
buckets = {"≤1天": 0, "2-3天": 0, "4-7天": 0, "8-15天": 0, ">15天": 0}
for t in trips:
d = t.days
key = "≤1天" if d <= 1 else "2-3天" if d <= 3 else "4-7天" if d <= 7 else "8-15天" if d <= 15 else ">15天"
buckets[key] += 1
print(" ", buckets)
print()
print("== 时间止损反事实:把浮亏且持有超过 N 天的仓位按当日收盘价平掉 ==")
print(f"{'规则':22} {'权益变动':>10} {'收益率':>7} {'最大回撤':>8} {'占用ROI':>8} {'佣金':>8} {'强平次数':>8}")
def with_time_stop(limit: int) -> tuple[float, int]:
"""在没有时间止损的结果上,找出"浮亏且连续持有 > limit 天"的仓位并按其后的实际
主出口价格之差估算影响(近似:直接扣掉该仓位到期末的浮亏差额)。"""
stops = 0
delta = 0.0
for code in data:
rows = [(day, c, close, vol) for day, c, close, vol in track[code] if vol > 0 and c > 0]
if not rows:
continue
start = rows[0][0]
for index, (day, c, close, vol) in enumerate(rows):
held = (day - start).days
if held > limit and close * vol - c < 0:
# 反事实:在当天以收盘价平掉,之后不再持有该标的
loss = close * vol - c
final_close = rows[-1][2]
avoid = (final_close - close) * vol
delta += loss * 0 - avoid # 平仓后避免了后续价格变动
stops += 1
break
return delta, stops
for limit in (0, 5, 10, 20):
if limit == 0:
print(f"{'不止损(现配置)':22} {stats['final_equity'] - 500000:10.2f} {stats['return_pct']:6.2f}% "
f"{stats['max_dd_pct']:7.2f}% "
f"{(stats['final_equity'] - 500000) / stats['avg_deployed'] * 100:7.2f}% "
f"{stats['fees']:8.2f} {0:8}")
continue
delta, stops = with_time_stop(limit)
equity = stats["final_equity"] - 500000 + delta
print(f"{'持有>' + str(limit) + '天且浮亏即平':22} {equity:10.2f} {equity / 500000 * 100:6.2f}% "
f"{stats['max_dd_pct']:7.2f}% {equity / stats['avg_deployed'] * 100:7.2f}% "
f"{stats['fees']:8.2f} {stops:8}")
print()
print("注:上表是「平仓后不再持有该标的」的粗略反事实,只用于判断时间止损的方向性收益,")
print(" 不是精确回测(未重算后续轮次与资金复用)。")