82c05f6f81
7 finder paralleli sul diff della giornata (8adf388..HEAD), fix dei confermati: CRITICI (money-path): - close_amount: GUARD stato-stantio sul fallback netting — il residuo non-reduce-only e' consentito solo fino al gap (conto reale - libri degli altri worker - orfani) nella direzione della chiusura (src/live/books.py = fonte unica, usata anche dal reconciler). Senza guard, un close su stato stantio (DSL scattato in outage, flatten manuale) APRIVA una posizione nuda a taglia piena bookata come 'chiusura verificata'. Fail-safe se il gap non e' calcolabile. Check polvere PRIMA di _quantize_step (che clampa al lotto minimo: un residuo 1e-17 diventava un ordine nudo da un lotto). - _real_close: market_amt = filled_amount anche a merged verified=False (i contratti chiusi dal reduce-only non si buttano se il leg netting fallisce); REAL_CLOSE_PARTIAL non piu' gateato su verified (era soppresso proprio nel caso parziale reale). - pairs: verita' per-FRAZIONE di gamba (gross proporzionale al fillato, orfano = solo il residuo — prima falsava reconciler e real_capital della parte gia' chiusa); REAL_OPEN_PAIR booka filled_amount; docstring applied corretta. - open_pair unwind: chiude il FILLATO, non il richiesto (senza il cap silenzioso del reduce-only avrebbe mangiato quota altrui). - place_tp_limit: quantize CONSERVATIVO (sell=floor/buy=ceil) — il rounding al tick piu' vicino poteva mettere il resting oltre il TP sim -> tocco genuino classificato phantom sistematicamente. ROBUSTEZZA/OSSERVABILITA': - runner: WORKER_ERROR_STREAK a 5 e poi ogni 50 poll + recovery RIPRESO + flag real_in_position nell'alert (prima: one-shot a n==5, poi silenzio). - _tp_phantom: TTL 120s sul verdetto (era ~50 HTTP/h per worker a barra fantasma); merge notes con entrambi gli order_id (audit-trail). - reset_flatten: _quantize_step Decimal (round float produceva amount che Deribit rifiuta); hourly_report: book XS01 con gambe SHORT visibili (abs). - validate_xsec_worker: POS/LEV importati (non hardcoded); xs01_tranche: regression-lock ESEGUITO vs xsec_sim; reconcile su src/live/books; drift_monitor: rolling vettoriale + exit code 1 su warn. Test: +4 guard/dust, fixture filled_amount -> 114 passed. Deferiti (TODO): resting esposti al netting, lifecycle orphan_legs, finestra trade-history TP_PHANTOM, validazione feed a monte, dedup minori. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
159 lines
6.5 KiB
Python
159 lines
6.5 KiB
Python
"""XS01 phase-tranching — PRE-REGISTRATO: il roll non-sovrapposto di XS01 (entry a i,
|
|
exit a i+hold, re-entry) ha una FASE arbitraria (dipende dal primo indice valido).
|
|
Se l'esito dipende dalla fase, c'e' timing-luck: dividere il book in K sub-book
|
|
sfasati di hold/K barre (capitale 1/K ciascuno) e' un ensemble di fase che riduce
|
|
la varianza SENZA parametri fittati (K=2 e K=3 riportati entrambi, nessun best-pick).
|
|
|
|
Test (griglia fissata qui, completa):
|
|
[1] SENSIBILITA' DI FASE: xsec_sim base con offset di partenza 0..hold-1
|
|
-> dispersione di Sharpe/ret/DD (FULL e OOS). Se la dispersione e' piccola,
|
|
il tranching non serve (verdetto onesto, fine).
|
|
[2] TRANCHING: K in {2, 3} sub-book sfasati equal-capital -> Sharpe/DD/ret
|
|
FULL e OOS vs la MEDIA e il WORST delle fasi singole.
|
|
Criterio (tutti necessari): il tranched riduce la dispersione (per costruzione) e
|
|
il suo Sharpe OOS >= worst-fase OOS con DD <= mediana fasi; fee identiche (il
|
|
tranching NON cambia il turnover per unita' di capitale).
|
|
|
|
uv run python scripts/analysis/xs01_tranche_research.py
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import numpy as np
|
|
import pandas as pd
|
|
|
|
PROJECT_ROOT = Path(__file__).resolve().parents[2]
|
|
sys.path.insert(0, str(PROJECT_ROOT))
|
|
|
|
from scripts.strategies.XS01_cross_sectional import (
|
|
aligned_panel, UNIVERSE, LB, HOLD, FEE_RT, LEV, POS, OOS_FRAC)
|
|
|
|
|
|
def xsec_trades(phase: int = 0, lb: int = LB, hold: int = HOLD, fee_rt: float = FEE_RT,
|
|
split_frac: float = 0.0, M=None):
|
|
"""Replica ESATTA della logica di xsec_sim ma parte da max(lb, split)+phase e
|
|
ritorna la lista trade (i, j, net) — stessa matematica, fee = 2*fee_rt."""
|
|
C = M[UNIVERSE].values
|
|
n = len(C)
|
|
logC = np.log(C)
|
|
split = int(n * split_frac)
|
|
fee = 2 * fee_rt
|
|
out = []
|
|
last = -1
|
|
i = max(lb, split) + phase
|
|
while i < n - hold:
|
|
if i <= last:
|
|
i += 1
|
|
continue
|
|
dm = logC[i] - logC[i - lb]
|
|
dm = dm - dm.mean()
|
|
gw = np.sum(np.abs(dm))
|
|
if gw < 1e-9:
|
|
i += 1
|
|
continue
|
|
w = -dm / gw
|
|
book = float(np.sum(w * (logC[i + hold] - logC[i])))
|
|
out.append((i, i + hold, book - fee))
|
|
last = i + hold
|
|
i += 1
|
|
return out
|
|
|
|
|
|
def equity_from(trades, ts, pos=POS, lev=LEV, weight=1.0):
|
|
"""Equity compounding a punti-exit (convenzione xsec_sim), peso per tranche."""
|
|
cap = 1000.0
|
|
eq_ts, eq_v = [], []
|
|
for i, j, net in sorted(trades, key=lambda t: t[1]):
|
|
cap = max(cap + cap * pos * lev * net * weight, 10.0)
|
|
eq_ts.append(ts[j])
|
|
eq_v.append(cap)
|
|
return pd.Series(eq_v, index=pd.DatetimeIndex(eq_ts))
|
|
|
|
|
|
def metrics(trades, ts, yrs_span):
|
|
rets = [t[2] * POS for t in trades]
|
|
n = len(rets)
|
|
sharpe = float(np.mean(rets) / np.std(rets) * np.sqrt(n / yrs_span)) if n > 1 and np.std(rets) > 0 else 0.0
|
|
eq = equity_from(trades, ts)
|
|
pk = eq.cummax()
|
|
dd = float(((pk - eq) / pk).max() * 100) if len(eq) else 0.0
|
|
ret = float((eq.iloc[-1] / 1000 - 1) * 100) if len(eq) else 0.0
|
|
return dict(n=n, ret=ret, sharpe=sharpe, dd=dd)
|
|
|
|
|
|
def combined_metrics(branches, ts, yrs_span):
|
|
"""K tranche -> un'unica equity: pesa ogni trade 1/K sul capitale comune."""
|
|
K = len(branches)
|
|
allt = sorted([t for b in branches for t in b], key=lambda t: t[1])
|
|
cap = 1000.0
|
|
eq_ts, eq_v, rets = [], [], []
|
|
for i, j, net in allt:
|
|
rets.append(net * POS / K)
|
|
cap = max(cap + cap * POS * LEV * net / K, 10.0)
|
|
eq_ts.append(ts[j])
|
|
eq_v.append(cap)
|
|
eq = pd.Series(eq_v, index=pd.DatetimeIndex(eq_ts))
|
|
pk = eq.cummax()
|
|
n = len(rets)
|
|
sharpe = float(np.mean(rets) / np.std(rets) * np.sqrt(n / yrs_span)) if n > 1 and np.std(rets) > 0 else 0.0
|
|
return dict(n=n, ret=float((eq.iloc[-1] / 1000 - 1) * 100),
|
|
sharpe=sharpe, dd=float(((pk - eq) / pk).max() * 100))
|
|
|
|
|
|
def regression_lock(M=None):
|
|
"""xsec_trades(phase=0) DEVE riprodurre xsec_sim (n trade e ret totale):
|
|
la copia e' load-bearing (la usano gate e validate_xsec_worker) — senza
|
|
lock eseguito un fix all'engine canonico non si propagherebbe e la
|
|
validazione live certificherebbe un engine stantio (code-review 2026-06-11)."""
|
|
from scripts.strategies.XS01_cross_sectional import xsec_sim
|
|
if M is None:
|
|
M = aligned_panel()
|
|
ts = pd.to_datetime(M.index, unit="ms", utc=True)
|
|
tr = xsec_trades(phase=0, M=M)
|
|
eq = equity_from(tr, ts)
|
|
ref = xsec_sim()
|
|
n_ok = len(tr) == ref["trades"]
|
|
ret = (eq.iloc[-1] / 1000 - 1) * 100 if len(eq) else 0.0
|
|
ret_ok = abs(ret - ref["ret"]) < 1e-6 * max(1.0, abs(ref["ret"]))
|
|
if not (n_ok and ret_ok):
|
|
raise AssertionError(f"REGRESSION-LOCK FALLITO: trades {len(tr)} vs {ref['trades']}, "
|
|
f"ret {ret:.6f} vs {ref['ret']:.6f}")
|
|
print(f" regression-lock OK: {len(tr)} trade, ret {ret:+.2f}% == xsec_sim")
|
|
|
|
|
|
def run():
|
|
M = aligned_panel()
|
|
ts = pd.to_datetime(M.index, unit="ms", utc=True)
|
|
n = len(M)
|
|
print("=" * 96)
|
|
print(" XS01 phase-tranching — sensibilita' di fase + ensemble K=2/3 | griglia pre-registrata")
|
|
print("=" * 96)
|
|
regression_lock(M)
|
|
for tag, split in (("FULL", 0.0), ("OOS", 1 - OOS_FRAC)):
|
|
yrs_span = (ts[-1] - ts[max(int(n * split), LB)]).days / 365.25 or 1
|
|
rows = []
|
|
for ph in range(HOLD):
|
|
tr = xsec_trades(phase=ph, split_frac=split, M=M)
|
|
rows.append(metrics(tr, ts, yrs_span))
|
|
sh = np.array([r["sharpe"] for r in rows])
|
|
dd = np.array([r["dd"] for r in rows])
|
|
rt = np.array([r["ret"] for r in rows])
|
|
print(f"\n [{tag}] sensibilita' di fase (12 fasi):")
|
|
print(f" Sharpe: min {sh.min():.2f} | med {np.median(sh):.2f} | max {sh.max():.2f} | std {sh.std():.2f}")
|
|
print(f" ret%: min {rt.min():+.0f} | med {np.median(rt):+.0f} | max {rt.max():+.0f}")
|
|
print(f" DD%: min {dd.min():.1f} | med {np.median(dd):.1f} | max {dd.max():.1f}")
|
|
for K in (2, 3):
|
|
branches = [xsec_trades(phase=int(p), split_frac=split, M=M)
|
|
for p in np.linspace(0, HOLD, K, endpoint=False)]
|
|
m = combined_metrics(branches, ts, yrs_span)
|
|
print(f" K={K} tranched: Sharpe {m['sharpe']:.2f} | ret {m['ret']:+.0f}% | "
|
|
f"DD {m['dd']:.1f}% | trade {m['n']}")
|
|
print("\n criterio: tranched-Sharpe OOS >= worst-fase OOS e DD <= mediana fasi; "
|
|
"se la dispersione di fase e' gia' piccola -> NON SERVE (verdetto onesto).")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
run()
|