121 lines
5.1 KiB
Python
121 lines
5.1 KiB
Python
"""未了结仓位的浮动亏损轨迹 + "时间止损"反事实对照(只做分析,不改策略代码)。
|
||
|
||
用法: 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(" 不是精确回测(未重算后续轮次与资金复用)。")
|