chore(reset): v2.0.0 — storico certificato Deribit mainnet, ripartenza pulita
Reset del progetto su fondamenta verificate dopo la scoperta che l'intera libreria "validata OOS" era artefatto di feed contaminato (print fantasma del feed Cerbero TESTNET + storico Binance/USDT). - Storico ricostruito da Deribit MAINNET (ccxt pubblico, tokenless) e CERTIFICATO (certify_feed.py): BTC/ETH puliti su TUTTA la storia (mediana 2-6 bps vs Coinbase USD), integrita' OHLC + coerenza resample (maxΔ 0.00) + cross-venue OK. Alt esclusi (illiquidi/divergenti: LTC/DOGE 50-82% barre flat; XRP/BNB non certificabili). - Verdetto sul feed pulito: FADE / PAIRS / XS01 / TSM01 morti (ogni portafoglio Sharpe -2.3..-3.0, DD ~40%); solo SH01 e frammenti HONEST con segnale residuo, da ri-validare in isolamento. - Cleanup "restart pulito": strategie, stack live (src/live, src/portfolio, runner/executor, yml, docker), ~100 script ricerca/gate, waste/games/ portfolios, dati non certificati + cache e 60+ diari -> archiviati in Old/ (preservati, non cancellati). Diario consolidato in un unico documento. - Skeleton ricerca tenuto: Strategy ABC + indicatori + src/fractal + src/backtest/engine + load_data; tool dati certificati (rebuild_history, certify_feed, audit_feed, multi_source_check). - Universo dati ATTIVO: solo BTC/ETH (5m/15m/1h); guardrail fisico (load_data su alt -> FileNotFoundError). Esecuzione DISABILITATA, conto flat. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,335 @@
|
||||
"""TAIL-RISK AUDIT of EXIT-22 (no_sl) and EXIT-16 (close_confirm_sl).
|
||||
|
||||
Hypothesis under test (to REFUTE if possible): removing/softening the intrabar SL
|
||||
is an artifact whose hidden cost is catastrophic tail risk in crash regimes.
|
||||
|
||||
Sections:
|
||||
(1) Per-trade MAE (maximum adverse excursion, intrabar, leverage 3, % of notional
|
||||
ret terms == same units as engine `ret`) for base vs no_sl vs close_confirm.
|
||||
Distribution p50/p95/p99/max per sleeve.
|
||||
(2) Crash windows: 2020-03-12, 2021-05-19, 2022-06, 2022-11 (FTX). Trades OPEN in
|
||||
those windows: realized loss + MAE under base / no_sl / close_confirm.
|
||||
(3) Live worker -2% fallback: with no_sl the strategy emits tp but sl=0 -> the
|
||||
worker branch `if self.tp and self.sl` is False, falls to `elif self.max_bars`
|
||||
(fade have max_bars) -> PURE horizon exit, the -2% `else` branch NEVER fires.
|
||||
So no_sl LIVE has NO stop at all. We simulate what a -2% price stop WOULD have
|
||||
capped, to quantify the protection that is in fact ABSENT.
|
||||
(4) Disaster SL at 3x / 4x the base SL distance, intrabar: does it keep almost all
|
||||
the no_sl gain while cutting the tail?
|
||||
|
||||
Run: cd /opt/docker/PythagorasGoal && PYTHONPATH=. uv run python \
|
||||
scripts/analysis/exit_policies/verify_tail_risk.py
|
||||
"""
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
||||
import exit_lab as EL # noqa: E402
|
||||
from exit_lab import ExitPolicy, simulate, load_sleeves, LEV, OOS_START_MS # noqa: E402
|
||||
|
||||
DATA = load_sleeves()
|
||||
SLEEVES = list(DATA.items())
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- helpers
|
||||
|
||||
def _replay_trades(policy_cls, sleeve, params=None, start_ms=None, end_ms=None):
|
||||
"""Re-run the engine logic but COLLECT per-trade detail incl. MAE.
|
||||
|
||||
MAE = worst adverse excursion (in `ret` units == leverage*frac move on notional)
|
||||
measured on the bars the trade is actually OPEN, from entry bar+1 up to and
|
||||
INCLUDING the exit bar (the exit bar's wick counts: it's where SL would trigger).
|
||||
"""
|
||||
params = params or {}
|
||||
h, l, c, ts = sleeve["high"], sleeve["low"], sleeve["close"], sleeve["ts_ms"]
|
||||
n = len(c)
|
||||
ctx = dict(sleeve)
|
||||
policy_cls.prepare(ctx, **params)
|
||||
fee = EL.FEE_RT * LEV
|
||||
last_exit = -1
|
||||
out = []
|
||||
for (i, d, tp0, sl0, mb) in sleeve["signals"]:
|
||||
if start_ms is not None and ts[i] < start_ms:
|
||||
continue
|
||||
if end_ms is not None and ts[i] >= end_ms:
|
||||
continue
|
||||
if i <= last_exit or i + 1 >= n:
|
||||
continue
|
||||
entry = c[i]
|
||||
pol = policy_cls(ctx, i, d, entry, tp0, sl0, mb, **params)
|
||||
horizon = min(int(pol.horizon), EL.HARD_CAP)
|
||||
fills = []
|
||||
remaining = 1.0
|
||||
j = i
|
||||
worst = 0.0 # most negative excursion in ret units
|
||||
for step in range(1, horizon + 1):
|
||||
j = i + step
|
||||
if j >= n:
|
||||
j = n - 1
|
||||
fills.append((remaining, c[j])); remaining = 0.0
|
||||
break
|
||||
# adverse excursion of THIS bar (before any exit): worst intrabar price
|
||||
adverse = l[j] if d == 1 else h[j]
|
||||
exc = (adverse - entry) / entry * d * LEV
|
||||
worst = min(worst, exc)
|
||||
tp, sl, tpfrac = pol.levels(j)
|
||||
hit_sl = sl is not None and ((d == 1 and l[j] <= sl) or (d == -1 and h[j] >= sl))
|
||||
hit_tp = tp is not None and ((d == 1 and h[j] >= tp) or (d == -1 and l[j] <= tp))
|
||||
if hit_sl:
|
||||
fills.append((remaining, sl)); remaining = 0.0
|
||||
break
|
||||
if hit_tp:
|
||||
f = min(max(tpfrac, 0.0), 1.0) * remaining
|
||||
if f > 0:
|
||||
fills.append((f, tp)); remaining -= f
|
||||
if remaining <= 1e-9:
|
||||
break
|
||||
pol.on_partial(j, tp, remaining)
|
||||
if pol.after_bar(j):
|
||||
fills.append((remaining, c[j])); remaining = 0.0
|
||||
break
|
||||
if step == horizon:
|
||||
fills.append((remaining, c[j])); remaining = 0.0
|
||||
if remaining > 1e-9:
|
||||
fills.append((remaining, c[j]))
|
||||
ret = sum(f * (p - entry) for f, p in fills) / entry * d * LEV - fee
|
||||
last_exit = j
|
||||
out.append({
|
||||
"i": i, "j": j, "d": d, "entry": entry,
|
||||
"ts_entry": ts[i], "ts_exit": ts[j],
|
||||
"ret": ret, "mae": worst, "bars": j - i,
|
||||
})
|
||||
return out
|
||||
|
||||
|
||||
def _pct(arr, q):
|
||||
return np.percentile(arr, q) if len(arr) else float("nan")
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- (1) MAE dist
|
||||
|
||||
def section1():
|
||||
print("=" * 100)
|
||||
print("(1) MAE DISTRIBUTION per sleeve (ret units = leverage*move on notional; "
|
||||
"fee NOT included). Negative = adverse.")
|
||||
print(" MAE is the worst intrabar excursion BEFORE exit. For base, SL caps it; "
|
||||
"for no_sl/close_confirm it can run.")
|
||||
print("=" * 100)
|
||||
policies = _load_policies()
|
||||
hdr = f"{'sleeve':<11}{'policy':<16}{'n':>5}{'p50':>9}{'p95':>9}{'p99':>9}{'max':>9}{'realP99':>10}{'realMax':>10}"
|
||||
for (code, asset), sleeve in SLEEVES:
|
||||
key = f"{code.split('_')[0]} {asset}"
|
||||
print(f"\n--- {key}")
|
||||
print(hdr)
|
||||
for pname, (cls, prm) in policies.items():
|
||||
tr = _replay_trades(cls, sleeve, prm)
|
||||
mae = np.array([t["mae"] for t in tr]) * 100 # to %
|
||||
rets = np.array([t["ret"] for t in tr]) * 100
|
||||
print(f"{'':<11}{pname:<16}{len(tr):>5}"
|
||||
f"{_pct(mae,50):>9.2f}{_pct(mae,5):>9.2f}{_pct(mae,1):>9.2f}{mae.min():>9.2f}"
|
||||
f"{_pct(rets,1):>10.2f}{rets.min():>10.2f}")
|
||||
|
||||
|
||||
def _load_policies():
|
||||
"""Return {name: (cls, params)} for base, no_sl, close_confirm(buf0)."""
|
||||
p22 = Path(__file__).resolve().parents[0] / "22_no_sl.py"
|
||||
p16 = Path(__file__).resolve().parents[0] / "16_close_confirm_sl.py"
|
||||
import importlib.util
|
||||
|
||||
def _load(path, attr):
|
||||
spec = importlib.util.spec_from_file_location(path.stem, path)
|
||||
mod = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(mod)
|
||||
return getattr(mod, attr)
|
||||
|
||||
NoSl = _load(p22, "NoSl")
|
||||
CloseConfirm = _load(p16, "CloseConfirmSl")
|
||||
return {
|
||||
"base": (ExitPolicy, {}),
|
||||
"no_sl": (NoSl, {"mode": "none"}),
|
||||
"close_confirm0": (CloseConfirm, {"buffer": 0.0}),
|
||||
}
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- (2) crashes
|
||||
|
||||
CRASHES = {
|
||||
"2020-03-12 covid": ("2020-03-10", "2020-03-16"),
|
||||
"2021-05-19 leverage": ("2021-05-17", "2021-05-23"),
|
||||
"2022-06 3AC/Luna": ("2022-06-10", "2022-06-20"),
|
||||
"2022-11 FTX": ("2022-11-07", "2022-11-12"),
|
||||
}
|
||||
|
||||
|
||||
def _ms(s):
|
||||
return int(pd.Timestamp(s, tz="UTC").value // 1e6)
|
||||
|
||||
|
||||
def section2():
|
||||
print("\n" + "=" * 100)
|
||||
print("(2) CRASH WINDOWS — trades OPEN (entry inside window) per policy: realized "
|
||||
"loss (ret%) and MAE%.")
|
||||
print("=" * 100)
|
||||
policies = _load_policies()
|
||||
for label, (a, b) in CRASHES.items():
|
||||
lo, hi = _ms(a), _ms(b)
|
||||
print(f"\n### {label} [{a} .. {b}]")
|
||||
any_trade = False
|
||||
for (code, asset), sleeve in SLEEVES:
|
||||
key = f"{code.split('_')[0]} {asset}"
|
||||
for pname, (cls, prm) in policies.items():
|
||||
tr = _replay_trades(cls, sleeve, prm)
|
||||
win = [t for t in tr if lo <= t["ts_entry"] <= hi]
|
||||
if not win:
|
||||
continue
|
||||
any_trade = True
|
||||
rets = np.array([t["ret"] for t in win]) * 100
|
||||
mae = np.array([t["mae"] for t in win]) * 100
|
||||
worst = min(win, key=lambda t: t["ret"])
|
||||
print(f" {key:<11}{pname:<16}n{len(win):>3} "
|
||||
f"sumRet{rets.sum():>8.2f}% worstRet{rets.min():>8.2f}% "
|
||||
f"worstMAE{mae.min():>8.2f}% avgBars{np.mean([t['bars'] for t in win]):>5.1f}")
|
||||
if not any_trade:
|
||||
print(" (no trades opened in window across sleeves)")
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- (3) -2% fallback
|
||||
|
||||
def section3():
|
||||
print("\n" + "=" * 100)
|
||||
print("(3) LIVE -2% FALLBACK on no_sl. NOTE: with no_sl the worker has NO stop at "
|
||||
"all (branch analysis in module docstring).")
|
||||
print(" Below = what a -2% PRICE stop (==-6% ret at lev3) WOULD cap if it WERE "
|
||||
"wired. Compares no_sl ret vs a synthetic no_sl+2%stop.")
|
||||
print("=" * 100)
|
||||
NoSl = _load_policies()["no_sl"][0]
|
||||
stop_ret = -0.02 * LEV # -2% price move * leverage = -6% on notional in ret units
|
||||
hdr = f"{'sleeve':<11}{'no_sl ret%':>12}{'+2%stop ret%':>14}{'Δret pp':>10}{'n capped':>10}{'worst no_sl':>13}{'worst +2%':>11}"
|
||||
print(hdr)
|
||||
for (code, asset), sleeve in SLEEVES:
|
||||
key = f"{code.split('_')[0]} {asset}"
|
||||
tr = _replay_trades(NoSl, sleeve, {"mode": "none"})
|
||||
# synthetic: if MAE breaches stop_ret, realize at stop_ret (approx; ignores
|
||||
# that price may recover — that's the point: a -2% stop locks the loss).
|
||||
base_rets = np.array([t["ret"] for t in tr])
|
||||
capped = []
|
||||
n_cap = 0
|
||||
fee = EL.FEE_RT * LEV
|
||||
for t in tr:
|
||||
if t["mae"] <= stop_ret:
|
||||
capped.append(stop_ret - fee) # stopped at -2% price, pay fee
|
||||
n_cap += 1
|
||||
else:
|
||||
capped.append(t["ret"])
|
||||
capped = np.array(capped)
|
||||
|
||||
def _compound(rets):
|
||||
cap = 1000.0
|
||||
for r in rets:
|
||||
cap = max(cap + cap * EL.POS * r, 10.0)
|
||||
return (cap / 1000.0 - 1) * 100
|
||||
|
||||
r_nosl = _compound(base_rets)
|
||||
r_stop = _compound(capped)
|
||||
print(f"{key:<11}{r_nosl:>12.0f}{r_stop:>14.0f}{r_stop - r_nosl:>10.1f}"
|
||||
f"{n_cap:>10}{base_rets.min()*100:>13.2f}{capped.min()*100:>11.2f}")
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- (4) disaster SL
|
||||
|
||||
class DisasterSl(ExitPolicy):
|
||||
"""no_sl behaviour + a FAR intrabar disaster stop at `mult` x base SL distance."""
|
||||
name = "disaster_sl"
|
||||
|
||||
def __init__(self, ctx, i, d, entry, tp0, sl0, mb, **params):
|
||||
super().__init__(ctx, i, d, entry, tp0, sl0, mb, **params)
|
||||
mult = float(params.get("mult", 3.0))
|
||||
self.sl = entry + mult * (sl0 - entry)
|
||||
|
||||
def levels(self, j: int):
|
||||
return self.tp0, self.sl, 1.0
|
||||
|
||||
|
||||
def _full_metrics(cls, sleeve, prm):
|
||||
full = simulate(cls, sleeve, prm)
|
||||
oos = simulate(cls, sleeve, prm, start_ms=OOS_START_MS)
|
||||
return full, oos
|
||||
|
||||
|
||||
def section4():
|
||||
print("\n" + "=" * 100)
|
||||
print("(4) DISASTER SL — no_sl + far intrabar stop at 3x / 4x base SL distance. "
|
||||
"Keep the gain, cut the tail?")
|
||||
print("=" * 100)
|
||||
NoSl = _load_policies()["no_sl"][0]
|
||||
hdr = (f"{'sleeve':<11}{'policy':<13}"
|
||||
f"{'FULLret%':>10}{'FULLdd%':>9}{'FULLsh':>8}"
|
||||
f"{'OOSret%':>9}{'OOSdd%':>8}{'OOSsh':>7}{'worstMAE%':>11}{'nStop':>7}")
|
||||
print(hdr)
|
||||
for (code, asset), sleeve in SLEEVES:
|
||||
key = f"{code.split('_')[0]} {asset}"
|
||||
rows = [
|
||||
("base", ExitPolicy, {}),
|
||||
("no_sl", NoSl, {"mode": "none"}),
|
||||
("disaster3x", DisasterSl, {"mult": 3.0}),
|
||||
("disaster4x", DisasterSl, {"mult": 4.0}),
|
||||
]
|
||||
print(f"--- {key}")
|
||||
for pname, cls, prm in rows:
|
||||
full, oos = _full_metrics(cls, sleeve, prm)
|
||||
tr = _replay_trades(cls, sleeve, prm)
|
||||
mae = np.array([t["mae"] for t in tr]) * 100
|
||||
# count trades that hit the disaster stop (ret near the stop level)
|
||||
n_stop = 0
|
||||
if pname.startswith("disaster"):
|
||||
mult = prm["mult"]
|
||||
for t, raw in zip(tr, sleeve["signals"]):
|
||||
pass
|
||||
# simpler: a stop hit shows up as a large negative ret roughly = stop level
|
||||
n_stop = int(np.sum(mae <= -2.0 * mult * LEV / LEV * 0)) # placeholder
|
||||
print(f"{'':<11}{pname:<13}"
|
||||
f"{full.get('ret_pct',0):>10.0f}{full.get('dd_pct',0):>9.2f}{full.get('sharpe_t',0):>8.2f}"
|
||||
f"{oos.get('ret_pct',0):>9.0f}{oos.get('dd_pct',0):>8.2f}{oos.get('sharpe_t',0):>7.2f}"
|
||||
f"{mae.min():>11.2f}{'':>7}")
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- aggregate
|
||||
|
||||
def section5_aggregate():
|
||||
"""Equal-weight aggregate across the 6 sleeves: does no_sl's tail blow up the
|
||||
PORTFOLIO worst-trade vs disaster3x? Sum of per-sleeve compounded won't aggregate
|
||||
DD honestly, so we report the WORST single-trade ret across all sleeves and the
|
||||
99th pct of the pooled trade distribution."""
|
||||
print("\n" + "=" * 100)
|
||||
print("(5) POOLED TRADE DISTRIBUTION across all 6 sleeves (the real tail metric).")
|
||||
print("=" * 100)
|
||||
NoSl = _load_policies()["no_sl"][0]
|
||||
cfgs = [
|
||||
("base", ExitPolicy, {}),
|
||||
("no_sl", NoSl, {"mode": "none"}),
|
||||
("close_confirm0", _load_policies()["close_confirm0"][0], {"buffer": 0.0}),
|
||||
("disaster3x", DisasterSl, {"mult": 3.0}),
|
||||
("disaster4x", DisasterSl, {"mult": 4.0}),
|
||||
]
|
||||
print(f"{'policy':<16}{'n':>6}{'retP1%':>9}{'retMin%':>9}{'maeP1%':>9}{'maeMin%':>9}{'<-15%cnt':>10}")
|
||||
for pname, cls, prm in cfgs:
|
||||
allret, allmae = [], []
|
||||
for (code, asset), sleeve in SLEEVES:
|
||||
tr = _replay_trades(cls, sleeve, prm)
|
||||
allret += [t["ret"] * 100 for t in tr]
|
||||
allmae += [t["mae"] * 100 for t in tr]
|
||||
ar = np.array(allret); am = np.array(allmae)
|
||||
n_bad = int(np.sum(ar < -15.0))
|
||||
print(f"{pname:<16}{len(ar):>6}{_pct(ar,1):>9.2f}{ar.min():>9.2f}"
|
||||
f"{_pct(am,1):>9.2f}{am.min():>9.2f}{n_bad:>10}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
section1()
|
||||
section2()
|
||||
section3()
|
||||
section4()
|
||||
section5_aggregate()
|
||||
Reference in New Issue
Block a user