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