research(wave-0822): BOCPD — il falsificatore eseguito con potenza, 0/68 celle adattive; l'adattivita' spiega il -7% del vantaggio
This commit is contained in:
@@ -694,9 +694,68 @@ stessi hanno nominato senza poterli eseguire**. Stesso contratto di consegna, st
|
|||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| 21 | XS01-OOS | il vantaggio del PROP-ALLOC poggia sul drift di XS01 misurato sulla sua finestra di scoperta: regge fuori? | _in corso_ |
|
| 21 | XS01-OOS | il vantaggio del PROP-ALLOC poggia sul drift di XS01 misurato sulla sua finestra di scoperta: regge fuori? | _in corso_ |
|
||||||
| 22 | MAKER | l'esecuzione passiva e' una fonte di ritorno, al netto del costo di non essere eseguiti? | _in corso_ |
|
| 22 | MAKER | l'esecuzione passiva e' una fonte di ritorno, al netto del costo di non essere eseguiti? | _in corso_ |
|
||||||
| 23 | BOCPD | un rilevatore di cambio di regime vero batte il miglior lookback COSTANTE? | _in corso_ |
|
| 23 | BOCPD | un rilevatore di cambio di regime vero batte il miglior lookback COSTANTE? | **SCARTATO** — filone chiuso definitivamente |
|
||||||
| 24 | CRITICO | cosa NON e' stato misurato, e quale singola misura mancante vale di piu' | _in corso_ |
|
| 24 | CRITICO | cosa NON e' stato misurato, e quale singola misura mancante vale di piu' | _in corso_ |
|
||||||
| 25 | BIN-FREQ | il binario di frequenza su SKH01 e' un candidato o un sottoprodotto? | _in corso_ |
|
| 25 | BIN-FREQ | il binario di frequenza su SKH01 e' un candidato o un sottoprodotto? | _in corso_ |
|
||||||
| 26 | TP01-SINISTRO | quanto pesa TP01 se l'obiettivo include un crash che nel campione non c'e'? | _in corso_ |
|
| 26 | TP01-SINISTRO | quanto pesa TP01 se l'obiettivo include un crash che nel campione non c'e'? | _in corso_ |
|
||||||
| 27 | SURFACE-RV | la superficie e' internamente incoerente, e l'incoerenza vale piu' del costo di attraversarla? | _in corso_ |
|
| 27 | SURFACE-RV | la superficie e' internamente incoerente, e l'incoerenza vale piu' del costo di attraversarla? | _in corso_ |
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 23 — BOCPD (orizzonte adattivo, il falsificatore eseguito)
|
||||||
|
|
||||||
|
`scripts/research/r0822b_bocpd.py` (123 s). **169 trial dichiarati**, conteggio al rialzo, inclusi i
|
||||||
|
13 di taratura fatti guardando **solo il numero di change-point**, mai uno Sharpe.
|
||||||
|
|
||||||
|
**Il falsificatore che il 22/08 restava aperto e' stato eseguito con i rilevatori che nominava**:
|
||||||
|
BOCPD (Adams & MacKay, run-length posterior online, Normale-InverseGamma, hazard costante) e
|
||||||
|
**optimal partitioning causale** (l'obiettivo che PELT accelera, ricorsione in avanti, mai un
|
||||||
|
`fit_predict` retrospettivo), piu' `L` scelto per walk-forward sul PnL realizzato.
|
||||||
|
|
||||||
|
📌 **E questa volta il test HA AVUTO POTENZA — e' il fatto che rende il "no" esaustivo.**
|
||||||
|
Il vincitore del 22/08 aveva lookback **costante** (stava al bordo il 100% del tempo, corr 1,000):
|
||||||
|
si era chiusa una famiglia di *proxy*, non il meccanismo. Qui **68/96 celle sono davvero adattive**
|
||||||
|
(BOCPD 24/24: `L_t` mediano 25-107 g, IQR 28-134 g, **0-4% al tetto**, 7-44 reset/anno).
|
||||||
|
|
||||||
|
| | Sh FULL | Sh HOLD | maxDD | CAGR | corr→TP01 |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| (A) adattivo, cella in-sample | 1,618 | 0,627 | 14,48% | 22,05% | 0,757 |
|
||||||
|
| **(B) miglior COSTANTE** (L=15g triple 12h) | **1,639** | **0,657** | 12,68% | 22,31% | 0,737 |
|
||||||
|
| (C) walk-forward su PnL | 1,385 | 0,382 | 16,33% | 17,20% | 0,860 |
|
||||||
|
| TP01 30/90/180 | 1,314 | 0,489 | 14,31% | 16,44% | 1,000 |
|
||||||
|
|
||||||
|
📌 **La decomposizione E' il risultato:** adattivo − TP01 = **+0,305**; miglior costante − TP01 =
|
||||||
|
**+0,326**; adattivo − miglior costante = **−0,021** → **l'adattivita' spiega il −7% del vantaggio
|
||||||
|
su TP01. Il 100% e' "un orizzonte piu' corto", non "adattarlo".**
|
||||||
|
|
||||||
|
**Gate:** `earns_slot_honest=False`; `marginal_vs_tp01` **NEUTRAL** con **corr→TP01 0,861**;
|
||||||
|
appaiato per anno vs il null giusto (B) **3/8 anni** (mediana −0,15); **banda d'ancora 12 ancore,
|
||||||
|
mediana delle differenze appaiate −0,136, positiva in 0/12**, e la canonica sta al **92° pctl** =
|
||||||
|
fortuna d'ancora a suo favore. `causality_ok` ok, e **troncamento manuale esplicito** su 4
|
||||||
|
combinazioni: `max|L(prefisso) − L(pieno)| = 0` → la trappola del change-point retrospettivo e'
|
||||||
|
esclusa **per misura, non per fiducia**. Null del de-levering **non applicabile e dichiarato**
|
||||||
|
(il candidato NON riduce il maxDD: 14,5% contro 12,7%).
|
||||||
|
|
||||||
|
**Il criterio del falsificatore applicato a tutte le 68 celle adattive: 0/68**, e nessuna delle due
|
||||||
|
condizioni va vicina — corr→TP01 **minimo 0,754** (soglia 0,60), **nessuna** batte il miglior
|
||||||
|
costante sul FULL (la migliore e' −0,013), massimo **4/8** anni battuti (richiesti 6).
|
||||||
|
|
||||||
|
⚠️ **Cosa l'agente NON propone, e perche':** il miglior costante batte TP01 di +0,326 — ma il salto
|
||||||
|
di Sharpe fra **L adiacenti** e' **+0,190, cioe' il 58% dell'intero vantaggio**: e' un max-of-24 su
|
||||||
|
un crinale di rumore, non una curva. **Nessun cambio a TP01, al libro, ai pesi.**
|
||||||
|
|
||||||
|
🚨 **Sottoprodotto che vale fuori dal filone: un deflated-Sharpe calcolato dentro una famiglia
|
||||||
|
OMOGENEA e' CIECO.** Qui dice **0,998-1,000 PASS**; ricalcolato con l'sr0 **di screen** misurato dal
|
||||||
|
filone ORTHO (1,572 su 168 trial) dice **0,555 / 0,580 / 0,237 FAIL** — TP01 compreso. Il motivo e'
|
||||||
|
strutturale: `deflated_sharpe` stima il massimo-dal-rumore dalla **varianza** degli Sharpe della
|
||||||
|
griglia, e una griglia di varianti molto correlate ha varianza piccola → sr0 basso → DSR alto **per
|
||||||
|
costruzione**. Gonfiare N col padding alza N e non la varianza: **non basta contare i trial di
|
||||||
|
screen, serve la VARIANZA di screen.**
|
||||||
|
|
||||||
|
📌 **Secondo sottoprodotto, un controllo da mettere in ogni famiglia con un parametro clippato: la
|
||||||
|
PROVA DI IDENTITA' batte la diagnostica.** Invece di dire "questa cella sta al bordo", l'agente
|
||||||
|
mostra che la sua serie e quella del lookback costante coincidono a **max|Δ| = 0,000e+00 su
|
||||||
|
n=2719** — non *somiglia a*, **lo e'**.
|
||||||
|
|
||||||
|
**VERDETTO: SCARTATO. IL FILONE ORIZZONTE-ADATTIVO E' CHIUSO DEFINITIVAMENTE.**
|
||||||
|
|||||||
@@ -0,0 +1,825 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
"""r0822b_bin_freq.py — BIN-FREQ: il BINARIO di frequenza su SKH01 giudicato come un CANDIDATO.
|
||||||
|
|
||||||
|
STORIA DEI FATTI (verificata leggendo il codice, non il riassunto).
|
||||||
|
1. `r0822_vol_size.py` ha proposto una SIZE PER-TRADE su SKH01 (cella congelata `VTL tv20 w90`
|
||||||
|
= clip(0.20/rv90(t-1), 0, 3) fissata all'INGRESSO) come CONTROLLO DEL RISCHIO:
|
||||||
|
+0,070 ISO dShFULL, 23/23 ancore, 8/8 anni.
|
||||||
|
2. `r0822_volsize_skeptic.py` ha CONFERMATO il numero e FALSIFICATO il meccanismo: il guadagno
|
||||||
|
e' FREQUENZA (+0,051 sul solo filtro di attivita') e non MAGNITUDINE (+0,003); e la forma
|
||||||
|
minima e' un BINARIO 0/1 (`BIN L>=med`, 194/406 trade scartati) che vale +0,080 FULL (20/23)
|
||||||
|
= 115% del LEAD, con controllo di segno superato (l'inverso fa -0,159, 0/23).
|
||||||
|
3. Lo scettico ha scritto "il candidato giusto non e' la formula ma il binario" — e nessuno
|
||||||
|
l'ha passato per `study_family_honest`. E' un SOTTOPRODOTTO DI UN ATTACCO, mai dichiarato
|
||||||
|
come famiglia. Questo script lo giudica come si giudica un candidato.
|
||||||
|
|
||||||
|
IPOTESI, REGISTRATA PRIMA DI MISURARE. Mi aspetto che il numero si riproduca (lo scettico e' stato
|
||||||
|
rigoroso e la sua replica e' bit-exact) e che il candidato NON superi il gate onesto, per due
|
||||||
|
ragioni dichiarate in anticipo: (a) la soglia e la finestra sono due assi liberi trovati DENTRO
|
||||||
|
l'esplorazione di un'altra famiglia, quindi la stima di famiglia dovra' essere molto piu' piccola
|
||||||
|
dell'argmax (precedente: sull'asse w lo scettico stesso stima +0,025 contro +0,080); (b) il
|
||||||
|
deflated-Sharpe sara' VACUO come lo e' stato sulla famiglia VOL-SIZE — su perturbazioni della
|
||||||
|
STESSA strategia la varianza fra i trial e' quasi nulla, e il DSR e' ~1 per chiunque, baseline
|
||||||
|
compreso. Se succede lo dichiaro vacuo e uso un null che ha potenza.
|
||||||
|
|
||||||
|
=========================== LA FAMIGLIA, DICHIARATA PRIMA DI GUARDARE =========================
|
||||||
|
Un binario di frequenza su SKH01 ha esattamente quattro gradi di liberta'. Li dichiaro tutti,
|
||||||
|
con i loro valori, PRIMA di misurare, e li conto AL RIALZO:
|
||||||
|
|
||||||
|
soglia (6) quantile ESPANDENTE causale della vol trailing, q in {0.25, 0.35, 0.50, 0.65,
|
||||||
|
0.75} + la soglia ASSOLUTA rv <= 20% (== `L>=1` della regola congelata)
|
||||||
|
finestra (6) w in {45, 60, 90, 120, 180, 270} giorni (sotto 45 lo scettico ha misurato che
|
||||||
|
la finestra contiene 1-3 barre ATTIVE: campione vuoto, non fragilita')
|
||||||
|
isteresi (2) h in {0.00, 0.10} (banda +-10% attorno alla soglia, stato persistente causale)
|
||||||
|
segno (2) +1 tieni la vol BASSA (l'ipotesi) / -1 tieni la vol ALTA (controllo di segno)
|
||||||
|
timeframe(1) uno solo: la gamba SKH01 vive sulla griglia 230m per costruzione. Dichiarato.
|
||||||
|
-> 6 x 6 x 2 x 2 = 144 celle
|
||||||
|
|
||||||
|
RIAPRIRE UN PARAMETRO RIAPRE LA SUA FAMIGLIA, e questo binario e' nato dentro l'esplorazione di
|
||||||
|
un'altra famiglia. Percio' al conto delle 144 si SOMMANO i trial gia' spesi sullo stesso oggetto:
|
||||||
|
28 (r0822_vol_size) + 43 (r0822_volsize_skeptic) = 71.
|
||||||
|
-> CONTO ONESTO DICHIARATO: 144 + 71 = 215 trial.
|
||||||
|
La sensibilita' del verdetto al conteggio e' pubblicata (§5), come nel precedente 30/07 dove un
|
||||||
|
DSR passava a N=8, falliva a N=72 e falliva a N=360.
|
||||||
|
|
||||||
|
============================== LENTE, ANCORE, CONVENZIONI =====================================
|
||||||
|
Lente: path CANONICO di SKH01 (fill al livello, entry a chiusura di bin) — identica nelle due
|
||||||
|
braccia di ogni confronto, quindi il Delta e' pulito anche se il livello e' pessimistico verso il
|
||||||
|
path live. Ancore: **23 offset**, la griglia a priori dell'audit 02/07 (ogni 30m su [0,690)):
|
||||||
|
NON ridotta, perche' il costo misurato e' ~20 ms per cella per ancora. Statistica: MEDIANA DELLE
|
||||||
|
DIFFERENZE APPAIATE (`A.anchor_luck_delta`), mai differenza delle mediane.
|
||||||
|
Confronto sempre ISO-PESO: il binario cambia la vol della gamba e quindi il peso EFFETTIVO di
|
||||||
|
SKH01 nel libro, e muovere quel peso e' gia' stato misurato e RESPINTO il 26/07. Il null del
|
||||||
|
de-levering e' DEGENERE su una ri-scalatura (`sh(k*base) === sh(base)`), quindi il test e'
|
||||||
|
iso-peso sullo Sharpe; `k_iso_dd` resta come diagnostica del DD.
|
||||||
|
Nessuna riscrittura: `extract / equity_daily / leg_daily / sizes_from_L / book / netted_target`
|
||||||
|
sono IMPORTATE da r0822_vol_size, e il titolo dello scettico e' riprodotto chiamando la SUA
|
||||||
|
funzione. Replica bit-exact contro `sleeves._skyhook_returns()` prima di ogni delta (§0).
|
||||||
|
|
||||||
|
USO: nice -n 19 timeout 900 uv run python scripts/research/r0822b_bin_freq.py [--every K]
|
||||||
|
--every 8 -> 3 ancore (pilota). default: tutte e 23.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[2]
|
||||||
|
for _p in (ROOT, ROOT / "scripts" / "research", ROOT / "scripts" / "research" / "alt"):
|
||||||
|
sys.path.insert(0, str(_p))
|
||||||
|
|
||||||
|
import altlib as A # noqa: E402
|
||||||
|
import r0702_anchor_skh01 as R # noqa: E402
|
||||||
|
import r0702_tp01_offset as TO # noqa: E402
|
||||||
|
import r0822_vol_size as V # noqa: E402 (motore: riusato, non riscritto)
|
||||||
|
import r0822_volsize_skeptic as SK # noqa: E402 (per riprodurre il titolo)
|
||||||
|
from r0822_sol_leg import k_iso_dd # noqa: E402
|
||||||
|
from src.portfolio.portfolio import weights_tilt_null # noqa: E402
|
||||||
|
from src.strategies.skyhook import SKH01_V1 # noqa: E402
|
||||||
|
|
||||||
|
ASSETS = V.ASSETS
|
||||||
|
OFFSETS_FULL = V.OFFSETS_FULL
|
||||||
|
sh, vol, maxdd, cagr = V.sh, V.vol, V.maxdd, V.cagr
|
||||||
|
hold, ins, book = V.hold, V.ins, V.book
|
||||||
|
CAPITAL, MIN_ORDER = V.CAPITAL, V.MIN_ORDER
|
||||||
|
W_TP, W_SKH = V.W_TP, V.W_SKH
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------ la famiglia (dichiarata sopra)
|
||||||
|
QS = (0.25, 0.35, 0.50, 0.65, 0.75)
|
||||||
|
THRS = tuple([("q", q) for q in QS] + [("abs", 0.20)])
|
||||||
|
WS = (45, 60, 90, 120, 180, 270)
|
||||||
|
HS = (0.00, 0.10)
|
||||||
|
SIGNS = (+1, -1)
|
||||||
|
N_INHERITED = 28 + 43 # r0822_vol_size + r0822_volsize_skeptic, sullo stesso oggetto
|
||||||
|
CANON = ("q", 0.50, 90, 0.00, +1) # la cella dello scettico, dentro la mia famiglia
|
||||||
|
L = 112
|
||||||
|
|
||||||
|
|
||||||
|
def cname(thr, w, h, sg) -> str:
|
||||||
|
t = "rv<=20%" if thr[0] == "abs" else f"q{thr[1]:.2f}"
|
||||||
|
return f"{t} w{w} h{h:.2f} s{'+' if sg > 0 else '-'}"
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================== il binario, causale per costruzione
|
||||||
|
def _hyst(x: np.ndarray, thr: np.ndarray, h: float, keep_below: bool) -> np.ndarray:
|
||||||
|
"""Stato persistente con banda +-h attorno alla soglia. Causale: al passo t usa solo x_t e
|
||||||
|
thr_t (entrambi funzione di dati <= t-1) e lo stato al passo t-1. h=0 -> confronto secco."""
|
||||||
|
n = len(x)
|
||||||
|
out = np.ones(n, bool)
|
||||||
|
st = True
|
||||||
|
for i in range(n):
|
||||||
|
xi, ti = x[i], thr[i]
|
||||||
|
if not (np.isfinite(xi) and np.isfinite(ti)):
|
||||||
|
st = True # warm-up: si tiene tutto (conv. scettico)
|
||||||
|
elif keep_below:
|
||||||
|
st = (xi <= ti * (1.0 + h)) if st else (xi <= ti * (1.0 - h))
|
||||||
|
else:
|
||||||
|
st = (xi >= ti * (1.0 - h)) if st else (xi >= ti * (1.0 + h))
|
||||||
|
out[i] = st
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
class KeepCache:
|
||||||
|
"""rv e quantili espandenti calcolati UNA volta per (asset, ancora, w). L'espandente e' la
|
||||||
|
parte cara; il resto e' un confronto."""
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.rv: dict = {}
|
||||||
|
self.q: dict = {}
|
||||||
|
|
||||||
|
def keep(self, ex: dict, key, thr, w: int, h: float, sg: int) -> pd.Series:
|
||||||
|
base = ex["base_daily"]
|
||||||
|
k = (key, w)
|
||||||
|
if k not in self.rv:
|
||||||
|
self.rv[k] = V._rv_daily(base, w, active_only=False) # gia' shiftata di 1 giorno
|
||||||
|
rv = self.rv[k]
|
||||||
|
if thr[0] == "abs":
|
||||||
|
t = np.full(len(rv), float(thr[1]))
|
||||||
|
else:
|
||||||
|
kq = (key, w, thr[1])
|
||||||
|
if kq not in self.q:
|
||||||
|
self.q[kq] = rv.expanding(min_periods=V.VT_WARM).quantile(thr[1])
|
||||||
|
t = self.q[kq].values
|
||||||
|
keep = _hyst(rv.values.astype(float), np.asarray(t, float), h, keep_below=(sg > 0))
|
||||||
|
return pd.Series(keep.astype(float), index=rv.index)
|
||||||
|
|
||||||
|
|
||||||
|
KC = KeepCache()
|
||||||
|
|
||||||
|
|
||||||
|
def size_bin(thr, w, h, sg):
|
||||||
|
def f(ex, key):
|
||||||
|
return V.sizes_from_L(ex, KC.keep(ex, key, thr, w, h, sg))
|
||||||
|
return f
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================== motore
|
||||||
|
def leg_of(EXX: dict, key, fn) -> pd.Series:
|
||||||
|
return V.leg_daily(EXX, {a: fn(EXX[a], (key, a)) for a in ASSETS})
|
||||||
|
|
||||||
|
|
||||||
|
def iso_pair(TP: pd.Series, leg: pd.Series, leg_base: pd.Series):
|
||||||
|
"""Libro della variante e libro di CONTROLLO ISO-PESO (baseline ri-scalato alla stessa vol di
|
||||||
|
gamba => stesso peso effettivo di SKH). Convenzione del bersaglio, riusata tale e quale."""
|
||||||
|
vb = vol(leg_base)
|
||||||
|
m = vol(leg) / vb if vb > 0 else 1.0
|
||||||
|
return book(TP, leg), book(TP, leg_base * m), m
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================== main
|
||||||
|
def main() -> None:
|
||||||
|
ap = argparse.ArgumentParser()
|
||||||
|
ap.add_argument("--every", type=int, default=1)
|
||||||
|
ap.add_argument("--ndraw", type=int, default=200)
|
||||||
|
args = ap.parse_args()
|
||||||
|
offs = OFFSETS_FULL[:: max(1, args.every)]
|
||||||
|
t0 = time.time()
|
||||||
|
|
||||||
|
print("=" * L)
|
||||||
|
print(" r0822b BIN-FREQ — il BINARIO di frequenza su SKH01, giudicato come un CANDIDATO")
|
||||||
|
print("=" * L)
|
||||||
|
print(f" ancore: {len(offs)}/{len(OFFSETS_FULL)} della griglia a priori 02/07 (ogni 30m su "
|
||||||
|
f"[0,690)). NON ridotta.")
|
||||||
|
print(" lente: path CANONICO, identica nelle due braccia. Confronto sempre ISO-PESO.")
|
||||||
|
|
||||||
|
# ---------------------------------------------------------- §1 famiglia, PRIMA di misurare
|
||||||
|
print("\n" + "-" * L)
|
||||||
|
print(" 1. GRIGLIA DICHIARATA — scritta prima di guardare i risultati, contata AL RIALZO")
|
||||||
|
print("-" * L)
|
||||||
|
cells = [(t, w, h, s) for t in THRS for w in WS for h in HS for s in SIGNS]
|
||||||
|
print(f" soglia {len(THRS):>3} {['q%.2f' % q for q in QS] + ['rv<=20% (== L>=1)']}")
|
||||||
|
print(f" finestra {len(WS):>3} {list(WS)} giorni")
|
||||||
|
print(f" isteresi {len(HS):>3} {list(HS)} (banda +-h attorno alla soglia)")
|
||||||
|
print(f" segno {len(SIGNS):>3} +1 tieni vol BASSA (ipotesi) / -1 tieni vol ALTA (controllo)")
|
||||||
|
print(f" timeframe 1 la gamba SKH01 vive su 230m per costruzione")
|
||||||
|
print(f" -> celle di QUESTA famiglia: {len(cells)}")
|
||||||
|
print(f" -> trial gia' spesi sullo STESSO oggetto: {N_INHERITED} "
|
||||||
|
f"(28 r0822_vol_size + 43 r0822_volsize_skeptic)")
|
||||||
|
print(f" -> CONTO ONESTO DICHIARATO: {len(cells)} + {N_INHERITED} = "
|
||||||
|
f"{len(cells) + N_INHERITED} trial. La sensibilita' al conteggio e' in §5.")
|
||||||
|
assert len(cells) == 144
|
||||||
|
|
||||||
|
# ---------------------------------------------------------- estrazione
|
||||||
|
print("\n estrazione tabelle trade (V2-DD) ...", flush=True)
|
||||||
|
EX = {o: {a: V.extract(a, o) for a in ASSETS} for o in offs}
|
||||||
|
ntr = {a: int(np.mean([len(EX[o][a]["i_ent"]) for o in offs])) for a in ASSETS}
|
||||||
|
print(f" fatto in {time.time()-t0:.0f}s — trade medi/ancora: " +
|
||||||
|
", ".join(f"{a} {ntr[a]}" for a in ASSETS))
|
||||||
|
TP = A.tp01_baseline_daily()
|
||||||
|
can = offs[0]
|
||||||
|
|
||||||
|
# ---------------------------------------------------------- §0 SANITY
|
||||||
|
print("\n" + "-" * L)
|
||||||
|
print(" 0. SANITY — bit-exact prima di qualunque delta")
|
||||||
|
print("-" * L)
|
||||||
|
from src.portfolio.sleeves import _skyhook_returns
|
||||||
|
base_leg = {o: V.leg_daily(EX[o], {a: V.size_flat(EX[o][a]) for a in ASSETS}) for o in offs}
|
||||||
|
off = _skyhook_returns()
|
||||||
|
d0 = float(np.max(np.abs(base_leg[can].values - off.values))) if len(base_leg[can]) == len(off) \
|
||||||
|
else float("nan")
|
||||||
|
print(f" gamba 50/50 off{can} vs sleeves._skyhook_returns(): max|dif| = {d0:.2e} "
|
||||||
|
f"({len(base_leg[can])} giorni)")
|
||||||
|
assert d0 < 1e-15, "replica NON bit-exact"
|
||||||
|
for a in ASSETS:
|
||||||
|
ref = R.run_asset(a, can)[0]
|
||||||
|
R._CACHE.clear()
|
||||||
|
dd = float(np.max(np.abs(V.equity_daily(EX[can][a], V.size_flat(EX[can][a])).values
|
||||||
|
- ref.values)))
|
||||||
|
print(f" {a} vs r0702_anchor_skh01.run_asset: max|dif| = {dd:.2e}")
|
||||||
|
assert dd < 1e-15
|
||||||
|
|
||||||
|
# ---------------------------------------------------------- §2 riproduzione del titolo
|
||||||
|
print("\n" + "-" * L)
|
||||||
|
print(" 2. RIPRODUZIONE DEL TITOLO DELLO SCETTICO — chiamando la SUA funzione, non la mia")
|
||||||
|
print(" (pubblicato: BIN L>=med +0,080 FULL 20/23 ; INVERSO -0,159 0/23)")
|
||||||
|
print("-" * L)
|
||||||
|
REP = {}
|
||||||
|
for tag, mode in (("BIN L>=med (scettico)", "gemed"), ("BIN L<1 INVERSO (scettico)", "lt1"),
|
||||||
|
("BIN L>=1 (scettico)", "ge1")):
|
||||||
|
fn = SK.size_bin(SK.FROZEN, mode)
|
||||||
|
bk, ct = {}, {}
|
||||||
|
for o in offs:
|
||||||
|
lg = V.leg_daily(EX[o], {a: fn(EX[o][a]) for a in ASSETS})
|
||||||
|
bk[o], ct[o], _ = iso_pair(TP, lg, base_leg[o])
|
||||||
|
REP[tag] = (bk, ct)
|
||||||
|
dF = A.anchor_luck_delta(lambda o: bk[o], lambda o: ct[o], offs, metric=sh)
|
||||||
|
dH = A.anchor_luck_delta(lambda o: hold(bk[o]), lambda o: hold(ct[o]), offs, metric=sh)
|
||||||
|
print(f" {tag:<28} ISO dShFULL {dF['median_paired']:+.3f} "
|
||||||
|
f"({dF['n_positive']}/{len(offs)}) banda [{dF['lo']:+.3f},{dF['hi']:+.3f}] "
|
||||||
|
f"ISO dShHOLD {dH['median_paired']:+.3f} ({dH['n_positive']}/{len(offs)})")
|
||||||
|
med_F = float(np.median([sh(book(TP, base_leg[o])) for o in offs]))
|
||||||
|
med_H = float(np.median([sh(hold(book(TP, base_leg[o]))) for o in offs]))
|
||||||
|
med_D = float(np.median([maxdd(book(TP, base_leg[o])) for o in offs]))
|
||||||
|
ban_F = [sh(book(TP, base_leg[o])) for o in offs]
|
||||||
|
print(f" libro BASE, mediana della banda: ShFULL {med_F:.3f} / ShHOLD {med_H:.3f} / "
|
||||||
|
f"maxDD {med_D*100:.1f}% [pubblicato 1,626 / 1,037 / 10,4%]")
|
||||||
|
print(f" banda d'ancora del LIVELLO del libro: ShFULL [{min(ban_F):.3f},{max(ban_F):.3f}] "
|
||||||
|
f"= ampiezza {max(ban_F)-min(ban_F):.3f} (il termine di paragone di §12)")
|
||||||
|
|
||||||
|
# ---------------------------------------------------------- §3 causalita' per troncamento
|
||||||
|
print("\n" + "-" * L)
|
||||||
|
print(" 3. CAUSALITA' — la decisione tenere/scartare ricalcolata TRONCANDO i dati al giorno")
|
||||||
|
print(" d'ingresso. `A.causality_ok` NON E' GIRATO (vuole un target_fn(df) e qui il")
|
||||||
|
print(" segnale nasce da 5m->230m: ri-eseguirlo su prefissi costa quanto tutto lo")
|
||||||
|
print(" script). Sostituito da un troncamento diretto, piu' stringente sul punto esatto.")
|
||||||
|
print("-" * L)
|
||||||
|
thr_c, w_c, h_c, s_c = ("q", CANON[1]), CANON[2], CANON[3], CANON[4]
|
||||||
|
bad = tested = 0
|
||||||
|
for a in ASSETS:
|
||||||
|
ex = EX[can][a]
|
||||||
|
kfull = KC.keep(ex, (can, a), thr_c, w_c, h_c, s_c)
|
||||||
|
rv_full = V._rv_daily(ex["base_daily"], w_c, False)
|
||||||
|
days = list(dict.fromkeys(ex["day_ent"]))
|
||||||
|
for d in days[::7]: # 1 ingresso su 7, su tutta la storia
|
||||||
|
if d not in kfull.index:
|
||||||
|
continue
|
||||||
|
b_tr = ex["base_daily"][ex["base_daily"].index <= d]
|
||||||
|
if len(b_tr) < V.VT_WARM + w_c:
|
||||||
|
continue
|
||||||
|
rv_t = V._rv_daily(b_tr, w_c, False)
|
||||||
|
q_t = rv_t.expanding(min_periods=V.VT_WARM).quantile(thr_c[1])
|
||||||
|
k_t = _hyst(rv_t.values.astype(float), q_t.values.astype(float), h_c, s_c > 0)[-1]
|
||||||
|
tested += 1
|
||||||
|
if bool(k_t) != bool(kfull.loc[d] > 0.5):
|
||||||
|
bad += 1
|
||||||
|
assert abs(float(rv_t.iloc[-1]) - float(rv_full.loc[d])) < 1e-12 or \
|
||||||
|
not np.isfinite(rv_full.loc[d])
|
||||||
|
print(f" decisioni ricalcolate su prefisso: {tested} DIVERGENTI: {bad}")
|
||||||
|
print(" NB l'isteresi ha memoria: troncando si perde lo stato, quindi il test e' esatto")
|
||||||
|
print(" per h=0 (la cella canonica) e conservativo per h>0.")
|
||||||
|
assert bad == 0, "il binario NON e' causale"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------- §4 tabella di famiglia
|
||||||
|
print("\n" + "-" * L)
|
||||||
|
print(" 4. LA FAMIGLIA INTERA — ISO dShFULL, mediana delle differenze appaiate su "
|
||||||
|
f"{len(offs)} ancore")
|
||||||
|
print("-" * L)
|
||||||
|
# DUE PASSATE, per la RAM (7 GB sulla VPS, ~3 liberi, e siamo in tanti): la 1a calcola le
|
||||||
|
# metriche per-ancora di tutte le 144 celle SENZA conservare le serie (144 x 23 x 3 serie da
|
||||||
|
# 2717 punti sarebbero ~500 MB); la 2a ricalcola e CONSERVA le serie delle sole 3-4 celle che
|
||||||
|
# servono dopo (canonica / scelta al buio / argmax): 4 x 23 gambe, ~2 s.
|
||||||
|
BK, CT, LEG, MS = {}, {}, {}, {}
|
||||||
|
|
||||||
|
def eval_cell(thr, w, h, sg, store: bool = False) -> dict:
|
||||||
|
nm = cname(thr, w, h, sg)
|
||||||
|
fn = size_bin(thr, w, h, sg)
|
||||||
|
dF, dH, dI, lF, lI, lH, ms, kp, ddv, ddc, dC_ = ([] for _ in range(11))
|
||||||
|
yr: dict = {}
|
||||||
|
for o in offs:
|
||||||
|
lg = leg_of(EX[o], o, fn)
|
||||||
|
b, c, m = iso_pair(TP, lg, base_leg[o])
|
||||||
|
dF.append(sh(b) - sh(c)); dH.append(sh(hold(b)) - sh(hold(c)))
|
||||||
|
dI.append(sh(ins(b)) - sh(ins(c))); dC_.append(cagr(b) - cagr(c))
|
||||||
|
lF.append(sh(lg)); lI.append(sh(ins(lg))); lH.append(sh(hold(lg)))
|
||||||
|
ms.append(m); ddv.append(maxdd(b)); ddc.append(maxdd(c))
|
||||||
|
kp.append(float(np.mean(np.concatenate([fn(EX[o][a], (o, a)) for a in ASSETS]))))
|
||||||
|
for y in {int(t) for t in b.index.year}:
|
||||||
|
by, cy = b[b.index.year == y], c[c.index.year == y]
|
||||||
|
if len(by) > 20:
|
||||||
|
yr.setdefault(y, []).append(sh(by) - sh(cy))
|
||||||
|
if store:
|
||||||
|
BK.setdefault(nm, {})[o] = b
|
||||||
|
CT.setdefault(nm, {})[o] = c
|
||||||
|
LEG.setdefault(nm, {})[o] = lg
|
||||||
|
MS.setdefault(nm, {})[o] = m
|
||||||
|
a_ = np.asarray(dF, float)
|
||||||
|
return dict(nm=nm, thr=thr, w=w, h=h, sg=sg,
|
||||||
|
dF=float(np.median(a_)), nF=int((a_ > 0).sum()),
|
||||||
|
lo=float(a_.min()), hi=float(a_.max()), arrF=a_,
|
||||||
|
dH=float(np.median(dH)), nH=int((np.asarray(dH) > 0).sum()),
|
||||||
|
isoI=float(np.median(dI)), dC=float(np.median(dC_)),
|
||||||
|
legF=float(np.median(lF)), legI=float(np.median(lI)),
|
||||||
|
legH=float(np.median(lH)), kept=float(np.median(kp)),
|
||||||
|
weff=float(np.median([W_SKH * x for x in ms])),
|
||||||
|
ddv=float(np.median(ddv)), ddc=float(np.median(ddc)),
|
||||||
|
yr={y: float(np.median(v)) for y, v in yr.items()})
|
||||||
|
|
||||||
|
RES = {}
|
||||||
|
for (thr, w, h, sg) in cells:
|
||||||
|
r = eval_cell(thr, w, h, sg)
|
||||||
|
RES[r["nm"]] = r
|
||||||
|
print(f" ({len(cells)} celle x {len(offs)} ancore valutate in {time.time()-t0:.0f}s "
|
||||||
|
f"dall'avvio, prima passata senza conservare le serie)")
|
||||||
|
|
||||||
|
def matrix(sg: int, h: float) -> None:
|
||||||
|
print(f"\n ISO dShFULL — segno {'+1 (tieni vol BASSA)' if sg>0 else '-1 (CONTROLLO)'}"
|
||||||
|
f", isteresi h={h:.2f}")
|
||||||
|
print(" soglia " + "".join(f"{('w%d' % w):>11}" for w in WS))
|
||||||
|
for thr in THRS:
|
||||||
|
t = "rv<=20%" if thr[0] == "abs" else f"q{thr[1]:.2f}"
|
||||||
|
row = ""
|
||||||
|
for w in WS:
|
||||||
|
r = RES[cname(thr, w, h, sg)]
|
||||||
|
row += f"{r['dF']:>+8.3f}{r['nF']:>3}"
|
||||||
|
print(f" {t:<10}" + row)
|
||||||
|
|
||||||
|
for sg in SIGNS:
|
||||||
|
for h in HS:
|
||||||
|
matrix(sg, h)
|
||||||
|
print("\n (ogni cella: mediana appaiata e n. di ancore positive su "
|
||||||
|
f"{len(offs)})")
|
||||||
|
|
||||||
|
POS = [RES[cname(t, w, h, +1)] for t in THRS for w in WS for h in HS]
|
||||||
|
NEG = [RES[cname(t, w, h, -1)] for t in THRS for w in WS for h in HS]
|
||||||
|
ALLC = [RES[cname(t, w, h, s)] for t in THRS for w in WS for h in HS for s in SIGNS]
|
||||||
|
nmcan = cname(("q", CANON[1]), CANON[2], CANON[3], CANON[4])
|
||||||
|
argmax = max(ALLC, key=lambda r: r["dF"])
|
||||||
|
print(f"\n ARGMAX della famiglia: {cname(argmax['thr'], argmax['w'], argmax['h'], argmax['sg']):<26}"
|
||||||
|
f" dShFULL {argmax['dF']:+.3f} ({argmax['nF']}/{len(offs)})")
|
||||||
|
print(f" cella dello scettico: {nmcan:<26} dShFULL {RES[nmcan]['dF']:+.3f} "
|
||||||
|
f"({RES[nmcan]['nF']}/{len(offs)}) rango "
|
||||||
|
f"{1 + sum(r['dF'] > RES[nmcan]['dF'] for r in ALLC)}/{len(ALLC)}")
|
||||||
|
print(f" STIMA ONESTA se la cella si sceglie a caso dentro la famiglia (mediana):")
|
||||||
|
print(f" tutta la famiglia (144) {float(np.median([r['dF'] for r in ALLC])):+.3f} "
|
||||||
|
f"positive {sum(r['dF']>0 for r in ALLC)}/{len(ALLC)}")
|
||||||
|
print(f" solo segno +1 (72) {float(np.median([r['dF'] for r in POS])):+.3f} "
|
||||||
|
f"positive {sum(r['dF']>0 for r in POS)}/{len(POS)}")
|
||||||
|
print(f" solo segno -1 (72) {float(np.median([r['dF'] for r in NEG])):+.3f} "
|
||||||
|
f"positive {sum(r['dF']>0 for r in NEG)}/{len(NEG)} <- controllo di segno")
|
||||||
|
# seconda passata: conserva le serie delle sole celle che servono dopo
|
||||||
|
sel_iso0 = max(ALLC, key=lambda r: r["isoI"])
|
||||||
|
for r in dict((x["nm"], x) for x in (RES[nmcan], argmax, sel_iso0,
|
||||||
|
max(ALLC, key=lambda z: z["legI"]))).values():
|
||||||
|
eval_cell(r["thr"], r["w"], r["h"], r["sg"], store=True)
|
||||||
|
dchk = A.anchor_luck_delta(lambda o: BK[nmcan][o], lambda o: CT[nmcan][o], offs, metric=sh)
|
||||||
|
assert abs(dchk["median_paired"] - RES[nmcan]["dF"]) < 1e-12 and \
|
||||||
|
dchk["n_positive"] == RES[nmcan]["nF"], "la statistica appaiata di §4 non coincide con A.anchor_luck_delta"
|
||||||
|
print(f" controllo: la statistica appaiata calcolata in §4 coincide con "
|
||||||
|
f"`A.anchor_luck_delta` ({dchk['median_paired']:+.6f})")
|
||||||
|
print(f" quota di trade TENUTI, mia cella q0.50 w90: {RES[nmcan]['kept']*100:.1f}% "
|
||||||
|
f"(mediana fra le ancore)")
|
||||||
|
fn_can = size_bin(("q", CANON[1]), CANON[2], CANON[3], CANON[4])
|
||||||
|
fn_sk = SK.size_bin(SK.FROZEN, "gemed")
|
||||||
|
ov, k_me, k_sk = [], [], []
|
||||||
|
for o in offs:
|
||||||
|
for a in ASSETS:
|
||||||
|
x = fn_can(EX[o][a], (o, a)) > 0.5
|
||||||
|
y = fn_sk(EX[o][a]) > 0.5
|
||||||
|
ov.append(float((x == y).mean())); k_me.append(float(x.mean())); k_sk.append(float(y.mean()))
|
||||||
|
dleg = float(np.max(np.abs(BK[nmcan][can].values - REP["BIN L>=med (scettico)"][0][can].values)))
|
||||||
|
print(f" ⚠ la MIA parametrizzazione NON e' bit-identica a quella dello scettico: lui prende")
|
||||||
|
print(f" la mediana espandente di L = clip(0.20/rv,0,3) DOPO un `.fillna(1.0)` che inietta")
|
||||||
|
print(f" dei 1.0 nel warm-up; io prendo il quantile espandente di rv, che salta i NaN. Il")
|
||||||
|
print(f" trasformo e' monotono, quindi le due decisioni coincidono nel "
|
||||||
|
f"{float(np.mean(ov))*100:.1f}% dei trade")
|
||||||
|
print(f" (tenuti {float(np.mean(k_me))*100:.1f}% io / {float(np.mean(k_sk))*100:.1f}% lui); "
|
||||||
|
f"max|dif| fra i due libri = {dleg:.2e}.")
|
||||||
|
|
||||||
|
# ---------------------------------------------------------- §5 i tre componenti del gate
|
||||||
|
print("\n" + "-" * L)
|
||||||
|
print(" 5. `study_family_honest` — NON chiamabile direttamente (e' cablato sui candidati")
|
||||||
|
print(" DIREZIONALI: factory -> target_fn via candidate_daily, e qui il candidato e' un")
|
||||||
|
print(" overlay su uno sleeve). Come il 30/07 sul tenore VRP01, uso i suoi TRE")
|
||||||
|
print(" COMPONENTI REALI: selezione in-sample-only -> deflated_sharpe -> marginal_vs_tp01.")
|
||||||
|
print("-" * L)
|
||||||
|
print(" (a) SELEZIONE IN-SAMPLE-ONLY — due criteri, entrambi dichiarati, entrambi sulla")
|
||||||
|
print(" MEDIANA fra le ancore (l'ancora non e' un hold-out, e' una dimensione di luck)")
|
||||||
|
sel_leg = max(ALLC, key=lambda r: r["legI"]) # come `select_cell_insample`
|
||||||
|
sel_iso = max(ALLC, key=lambda r: r["isoI"]) # cio' su cui si decide davvero
|
||||||
|
hop_leg = max(ALLC, key=lambda r: r["legH"])
|
||||||
|
hop_iso = max(ALLC, key=lambda r: r["dH"])
|
||||||
|
for tag, r in (("Sharpe IS standalone della gamba (come select_cell_insample)", sel_leg),
|
||||||
|
("ISO dShFULL IN-SAMPLE del libro (cio' che decide)", sel_iso)):
|
||||||
|
n = cname(r["thr"], r["w"], r["h"], r["sg"])
|
||||||
|
print(f" scelta al buio su {tag}")
|
||||||
|
print(f" -> {n:<26} ISO dShFULL {r['dF']:+.3f} ({r['nF']}/{len(offs)}) "
|
||||||
|
f"ISO dShHOLD {r['dH']:+.3f} ({r['nH']}/{len(offs)}) banda "
|
||||||
|
f"[{r['lo']:+.3f},{r['hi']:+.3f}]")
|
||||||
|
for tag, r in (("Sharpe HOLD-OUT standalone", hop_leg), ("ISO dShHOLD del libro", hop_iso)):
|
||||||
|
print(f" controllo (scegliendo SULL'HOLD-OUT, cio' che NON si fa) su {tag}:")
|
||||||
|
print(f" -> {cname(r['thr'], r['w'], r['h'], r['sg']):<26} dShFULL {r['dF']:+.3f}"
|
||||||
|
f" dShHOLD {r['dH']:+.3f}")
|
||||||
|
CHOSEN = sel_iso
|
||||||
|
nmc = cname(CHOSEN["thr"], CHOSEN["w"], CHOSEN["h"], CHOSEN["sg"])
|
||||||
|
print(f" CELLA SCELTA AL BUIO (criterio primario = ISO dShFULL in-sample): {nmc}")
|
||||||
|
|
||||||
|
print("\n (b) DEFLATED SHARPE — sullo Sharpe FULL standalone della gamba (la grandezza che")
|
||||||
|
print(" `select_cell_insample` usa), mediana fra le ancore; `daily` della barra canonica")
|
||||||
|
print(" serve solo per T/asimmetria/curtosi. Sensibilita' al CONTEGGIO ottenuta")
|
||||||
|
print(" REPLICANDO la distribuzione dei trial osservati (tiling), non riempiendola con")
|
||||||
|
print(" la mediana: riempire con la mediana ABBASSA la varianza dei trial e fa SALIRE il")
|
||||||
|
print(" DSR all'aumentare di N, che e' il verso sbagliato. (Errore mio, catturato nel")
|
||||||
|
print(" pilota e corretto: il primo output dava DSR 0,966 -> 0,986 -> 0,998 da N=144 a")
|
||||||
|
print(" N=500, cioe' 'piu' trial = piu' significativo'.)")
|
||||||
|
all_sr = [r["legF"] for r in ALLC]
|
||||||
|
pos_sr = [r["legF"] for r in POS]
|
||||||
|
base_sr = float(np.median([sh(base_leg[o]) for o in offs]))
|
||||||
|
dly = LEG[nmc][can]
|
||||||
|
|
||||||
|
def tile(v, N):
|
||||||
|
return (list(v) * (N // len(v) + 1))[:N]
|
||||||
|
|
||||||
|
for lab, sr, d in (("cella scelta al buio", CHOSEN["legF"], dly),
|
||||||
|
("BASELINE (size fissa)", base_sr, base_leg[can])):
|
||||||
|
for N, tagN in ((len(ALLC), f"{len(ALLC)} di QUESTA famiglia"),
|
||||||
|
(len(ALLC) + N_INHERITED,
|
||||||
|
f"{len(ALLC)+N_INHERITED} = conto onesto (+{N_INHERITED} ereditati)"),
|
||||||
|
(500, "500 (ipotetico, al rialzo)")):
|
||||||
|
dsr, sr0 = A.deflated_sharpe(sr, tile(all_sr, N), d)
|
||||||
|
print(f" {lab:<22} N={tagN:<44} DSR {dsr:.3f} max atteso sotto il null {sr0:+.3f}")
|
||||||
|
lo_n, hi_n = len(ALLC), 20000
|
||||||
|
if A.deflated_sharpe(CHOSEN["legF"], tile(all_sr, hi_n), dly)[0] >= 0.95:
|
||||||
|
cross = f">{hi_n}"
|
||||||
|
elif A.deflated_sharpe(CHOSEN["legF"], tile(all_sr, lo_n), dly)[0] < 0.95:
|
||||||
|
cross = f"<{lo_n}"
|
||||||
|
else:
|
||||||
|
for _ in range(40):
|
||||||
|
mid = (lo_n + hi_n) // 2
|
||||||
|
if A.deflated_sharpe(CHOSEN["legF"], tile(all_sr, mid), dly)[0] >= 0.95:
|
||||||
|
lo_n = mid
|
||||||
|
else:
|
||||||
|
hi_n = mid
|
||||||
|
cross = f"~{lo_n}"
|
||||||
|
print(f" -> IL VERDETTO SI RIBALTA A N {cross} trial: sotto quel conteggio il DSR passa,")
|
||||||
|
print(f" sopra fallisce. Il conto onesto dichiarato ({len(ALLC)+N_INHERITED}) sta dal lato")
|
||||||
|
print(f" del PASS. Precedente 30/07: un DSR passava a N=8, falliva a N=72 e a N=360; qui")
|
||||||
|
print(f" la stessa sensibilita' e' pubblicata invece di essere taciuta.")
|
||||||
|
dsp, sr0p = A.deflated_sharpe(CHOSEN["legF"], tile(pos_sr, len(POS) + N_INHERITED), dly)
|
||||||
|
print(f" sensibilita' alla COMPOSIZIONE della famiglia: escludendo il segno -1 (che e' un")
|
||||||
|
print(f" controllo e non un candidato) i trial diventano {len(POS)}+{N_INHERITED} e la loro")
|
||||||
|
print(f" dispersione cala -> DSR {dsp:.3f} (max atteso {sr0p:+.3f}). Includere il segno -1")
|
||||||
|
print(f" e' la scelta CONSERVATIVA e resta quella dichiarata.")
|
||||||
|
print(f" dispersione dei trial: sd(Sharpe FULL) = {float(np.std(all_sr, ddof=1)):.3f} sulle")
|
||||||
|
print(f" {len(ALLC)} celle, {float(np.std(pos_sr, ddof=1)):.3f} sulle {len(POS)} di segno +1.")
|
||||||
|
print(f" -> il DSR qui NON e' vacuo (il baseline lo fallisce dove il candidato lo passa):")
|
||||||
|
print(f" la famiglia contiene celle davvero diverse fra loro, al contrario della famiglia")
|
||||||
|
print(f" VOL-SIZE dove erano tutte perturbazioni della stessa size continua.")
|
||||||
|
|
||||||
|
print("\n (c) MARGINAL vs TP01 — sulla gamba modificata e sulla gamba base")
|
||||||
|
for lab, s in (("BASE (size fissa)", base_leg[can]), (nmc, LEG[nmc][can]),
|
||||||
|
("cella dello scettico", LEG[nmcan][can])):
|
||||||
|
m = A.marginal_vs_tp01(s)
|
||||||
|
b25 = m.get("blends", {}).get("w25", {})
|
||||||
|
print(f" {lab:<26} {str(m.get('marginal_verdict')):<10} corr {m.get('corr_full')} "
|
||||||
|
f"uplift w25 full {b25.get('uplift_full')} / hold {b25.get('uplift_hold')} "
|
||||||
|
f"robust_oos={m.get('robust_oos')} insample_edge={m.get('has_insample_edge')} "
|
||||||
|
f"hedge={m.get('is_hedge')}")
|
||||||
|
imp = A.implausible_sharpe(
|
||||||
|
LEG[nmc][can],
|
||||||
|
n_trades=int(sum(len(EX[can][a]["net"]) for a in ASSETS)),
|
||||||
|
n_losing_trades=int(sum((EX[can][a]["net"] < 0).sum() for a in ASSETS)))
|
||||||
|
print(f" implausible_sharpe (gamba scelta): implausible={imp['implausible']} "
|
||||||
|
f"attive {imp.get('active_frac', 0)*100:.1f}% perdite/attive "
|
||||||
|
f"{imp.get('loss_frac', 0)*100:.1f}% Calmar {imp.get('calmar', 0):.1f}")
|
||||||
|
|
||||||
|
# ---------------------------------------------------------- §6 null con potenza
|
||||||
|
print("\n" + "-" * L)
|
||||||
|
print(" 6. IL NULL CHE HA POTENZA — permutazione della SEQUENZA tenuti/scartati.")
|
||||||
|
print(" Stessa quota di trade tenuti, riassegnata a caso: separa 'conta QUALE trade si")
|
||||||
|
print(" scarta' (informazione) da 'conta scartarne meta'' (aritmetica). Una mediana del")
|
||||||
|
print(" null lontana da 0 direbbe che il null non e' un null (lezione 22/08 sul null")
|
||||||
|
print(f" invertito). {args.ndraw} estrazioni su TUTTE le {len(offs)} ancore, non su 3.")
|
||||||
|
print("-" * L)
|
||||||
|
rng = np.random.default_rng(20260822)
|
||||||
|
for lab, r in (("cella scelta al buio", CHOSEN), ("cella dello scettico", RES[nmcan]),
|
||||||
|
("argmax di famiglia", argmax)):
|
||||||
|
nm = cname(r["thr"], r["w"], r["h"], r["sg"])
|
||||||
|
real = float(np.median([sh(BK[nm][o]) - sh(CT[nm][o]) for o in offs]))
|
||||||
|
fn = size_bin(r["thr"], r["w"], r["h"], r["sg"])
|
||||||
|
sz0 = {o: {a: fn(EX[o][a], (o, a)) for a in ASSETS} for o in offs}
|
||||||
|
nul = []
|
||||||
|
for _ in range(args.ndraw):
|
||||||
|
v = []
|
||||||
|
for o in offs:
|
||||||
|
szp = {a: rng.permutation(sz0[o][a]) for a in ASSETS}
|
||||||
|
lg = V.leg_daily(EX[o], szp)
|
||||||
|
b, c, _ = iso_pair(TP, lg, base_leg[o])
|
||||||
|
v.append(sh(b) - sh(c))
|
||||||
|
nul.append(float(np.median(v)))
|
||||||
|
nul = np.asarray(nul)
|
||||||
|
pc = float((nul < real).mean())
|
||||||
|
print(f" {lab:<24} {nm:<24} reale {real:+.3f} null mediana {np.median(nul):+.3f} "
|
||||||
|
f"p90 {np.percentile(nul, 90):+.3f} max {nul.max():+.3f} -> p ~ {1-pc:.3f}")
|
||||||
|
print(f" (seme 20260822; {args.ndraw} estrazioni x {len(offs)} ancore)")
|
||||||
|
print("\n ⚠ LA MEDIANA DEL NULL NON E' ~0, E NON E' UN DIFETTO DEL NULL: E' UN FATTO.")
|
||||||
|
print(" Scartare a caso meta' dei trade DANNEGGIA, perche' lo Sharpe ANNUALIZZATO cresce")
|
||||||
|
print(" con la radice del numero di scommesse per anno: dimezzare i trade sullo stesso")
|
||||||
|
print(" arco di calendario costa un fattore ~sqrt(2) di Sharpe di gamba. Misurato:")
|
||||||
|
nmv = cname(CHOSEN["thr"], CHOSEN["w"], CHOSEN["h"], CHOSEN["sg"])
|
||||||
|
fnv = size_bin(CHOSEN["thr"], CHOSEN["w"], CHOSEN["h"], CHOSEN["sg"])
|
||||||
|
rng2 = np.random.default_rng(20260823)
|
||||||
|
shb = float(np.median([sh(base_leg[o]) for o in offs]))
|
||||||
|
shv = float(np.median([sh(LEG[nmv][o]) for o in offs]))
|
||||||
|
kp = float(np.median([np.mean(np.concatenate([fnv(EX[o][a], (o, a)) for a in ASSETS]))
|
||||||
|
for o in offs]))
|
||||||
|
shn = []
|
||||||
|
for _ in range(min(40, args.ndraw)):
|
||||||
|
sz0 = {o: {a: fnv(EX[o][a], (o, a)) for a in ASSETS} for o in offs}
|
||||||
|
shn.append(float(np.median([sh(V.leg_daily(EX[o], {a: rng2.permutation(sz0[o][a])
|
||||||
|
for a in ASSETS})) for o in offs])))
|
||||||
|
print(f" Sharpe della GAMBA: baseline (tutti i trade) {shb:.3f} -> permutata "
|
||||||
|
f"{float(np.median(shn)):.3f} -> filtrata {shv:.3f}")
|
||||||
|
print(f" quota tenuta {kp*100:.0f}% -> attesa dalla sola perdita di scommesse "
|
||||||
|
f"{shb*np.sqrt(kp):.3f}; osservata sotto permutazione {float(np.median(shn)):.3f}")
|
||||||
|
print(" Conseguenza per la lettura: il null NON e' centrato su zero, quindi il p-value")
|
||||||
|
print(" risponde alla domanda 'la SELEZIONE batte una selezione casuale della stessa")
|
||||||
|
print(" taglia?' e non alla domanda 'il filtro batte il non filtrare?'. La seconda e' il")
|
||||||
|
print(" confronto con la BASE (§4), che e' quello su cui si decide. Il filtro deve")
|
||||||
|
print(" superare una penale meccanica prima di guadagnare qualcosa: e' un test SEVERO.")
|
||||||
|
|
||||||
|
# ---------------------------------------------------------- §7 iso-peso / de-levering
|
||||||
|
print("\n" + "-" * L)
|
||||||
|
print(" 7. ISO-PESO e NULL DEL DE-LEVERING")
|
||||||
|
print("-" * L)
|
||||||
|
print(f" {'cella':<26}{'wSKH eff':>10}{'dShFULL RAW':>13}{'ISO':>9}{'maxDD var':>11}"
|
||||||
|
f"{'maxDD ctrl':>11}{'k_iso_dd':>10}")
|
||||||
|
for lab, r in (("BASE", None), ("cella dello scettico", RES[nmcan]),
|
||||||
|
("cella scelta al buio", CHOSEN)):
|
||||||
|
if r is None:
|
||||||
|
print(f" {'BASE (size fissa)':<26}{W_SKH:>10.3f}{0.0:>+13.3f}{0.0:>+9.3f}"
|
||||||
|
f"{maxdd(book(TP, base_leg[can]))*100:>10.1f}%{'-':>11}{'-':>10}")
|
||||||
|
continue
|
||||||
|
nm = cname(r["thr"], r["w"], r["h"], r["sg"])
|
||||||
|
raw = A.anchor_luck_delta(lambda o, n=nm: BK[n][o],
|
||||||
|
lambda o: book(TP, base_leg[o]), offs, metric=sh)
|
||||||
|
dv = float(np.median([maxdd(BK[nm][o]) for o in offs]))
|
||||||
|
dc = float(np.median([maxdd(CT[nm][o]) for o in offs]))
|
||||||
|
ks = (f"{k_iso_dd(CT[nm][can], BK[nm][can]):.3f}" if dv < dc
|
||||||
|
else "n/a") # n/a = non riduce il DD: nulla da negare
|
||||||
|
print(f" {nm:<26}{r['weff']:>10.3f}{raw['median_paired']:>+13.3f}{r['dF']:>+9.3f}"
|
||||||
|
f"{dv*100:>10.1f}%{dc*100:>10.1f}%{ks:>10}")
|
||||||
|
print(" Il null del de-levering e' DEGENERE su una ri-scalatura (sh(k*base) === sh(base)):")
|
||||||
|
print(" qui il test e' l'iso-peso sullo Sharpe. `k_iso_dd` e' riportato come diagnostica del")
|
||||||
|
print(" DD ed e' calcolato CONTRO il controllo iso-peso (non contro il baseline nudo).")
|
||||||
|
print(" NB il binario ABBASSA il peso effettivo di SKH01, non lo alza: la preoccupazione del")
|
||||||
|
print(" 26/07 ('alzare SKH01') non morde, ma l'iso-peso resta obbligatorio nell'altro verso.")
|
||||||
|
print(f"\n LIVELLI del libro (mediana fra le {len(offs)} ancore), netti fee:")
|
||||||
|
print(f" {'configurazione':<30}{'ShFULL':>9}{'ShHOLD':>9}{'ShIS':>8}{'maxDD':>9}{'CAGR':>9}"
|
||||||
|
f"{'vol':>8}")
|
||||||
|
lev = [("BASE (libro live 75/25)", {o: book(TP, base_leg[o]) for o in offs})]
|
||||||
|
for lab, r in (("BIN, peso eff. lasciato cadere", RES[nmcan]),
|
||||||
|
("BIN, controllo ISO-PESO", None)):
|
||||||
|
if r is None:
|
||||||
|
lev.append((lab, {o: CT[nmcan][o] for o in offs}))
|
||||||
|
else:
|
||||||
|
lev.append((lab, {o: BK[nmcan][o] for o in offs}))
|
||||||
|
for lab, dd_ in lev:
|
||||||
|
f_ = float(np.median([sh(dd_[o]) for o in offs]))
|
||||||
|
h_ = float(np.median([sh(hold(dd_[o])) for o in offs]))
|
||||||
|
i_ = float(np.median([sh(ins(dd_[o])) for o in offs]))
|
||||||
|
d_ = float(np.median([maxdd(dd_[o]) for o in offs]))
|
||||||
|
c_ = float(np.median([cagr(dd_[o]) for o in offs]))
|
||||||
|
v_ = float(np.median([vol(dd_[o]) for o in offs]))
|
||||||
|
print(f" {lab:<30}{f_:>9.3f}{h_:>9.3f}{i_:>8.3f}{d_*100:>8.1f}%{c_*100:>8.1f}%"
|
||||||
|
f"{v_*100:>7.1f}%")
|
||||||
|
print(" ⚠ la DIFFERENZA di queste mediane NON e' la mediana delle differenze appaiate (§4):")
|
||||||
|
print(" qui le mediane cadono su ancore diverse. La tabella serve a dare i LIVELLI netti fee")
|
||||||
|
print(" richiesti dal contratto; il numero che decide resta quello appaiato.")
|
||||||
|
|
||||||
|
# ---------------------------------------------------------- §8 weights_tilt_null
|
||||||
|
print("\n" + "-" * L)
|
||||||
|
print(" 8. `weights_tilt_null` — una modulazione binaria della size E' un cambio di pesi nel")
|
||||||
|
print(" tempo. Il gate confronta vettori STATICI: cattura la sola componente di SCALA")
|
||||||
|
print(" (limite dichiarato, come nel bersaglio). k_seen = tutti i trial dichiarati.")
|
||||||
|
print("-" * L)
|
||||||
|
cols = {"TP01": TP, "SKH01": base_leg[can]}
|
||||||
|
for lab, r in (("cella dello scettico", RES[nmcan]), ("cella scelta al buio", CHOSEN),
|
||||||
|
("argmax di famiglia", argmax)):
|
||||||
|
nm = cname(r["thr"], r["w"], r["h"], r["sg"])
|
||||||
|
m = MS[nm][can]
|
||||||
|
wp = {"TP01": W_TP, "SKH01": W_SKH * m}
|
||||||
|
g = weights_tilt_null(cols, {"TP01": W_TP, "SKH01": W_SKH}, wp, caps={"SKH01": 0.5},
|
||||||
|
floor=0.05, n=300, k_seen=len(ALLC) + N_INHERITED)
|
||||||
|
print(f" {lab:<24} {nm:<24} peso equiv SKH {wp['SKH01']/sum(wp.values()):.3f} "
|
||||||
|
f"d_IS {g['delta_insample']:+.4f} d_HOLD {g['delta_hold']:+.4f} "
|
||||||
|
f"pctl {g['pctl_hold']:.1f} gate_pass={g['gate_pass']}")
|
||||||
|
print(" ⚠ LIMITE, e va detto perche' e' aggirabile: il binario ABBASSA il peso effettivo di")
|
||||||
|
print(" SKH01, e il gate misura proprio quella caduta. Nella forma di deploy naturale (si")
|
||||||
|
print(" ri-scala la gamba per riportare il peso effettivo a 0,25) il vettore proposto e'")
|
||||||
|
print(" IDENTICO a quello corrente -> d_IS = d_HOLD = 0 e il gate diventa INERTE, non")
|
||||||
|
print(" superato. Quindi `weights_tilt_null` NON puo' essere l'evidenza decisiva qui: e'")
|
||||||
|
print(" negativo nella forma nuda e vacuo nella forma compensata. Cio' che decide resta la")
|
||||||
|
print(" banda APPAIATA a iso-peso (§4) e la stima di famiglia.")
|
||||||
|
|
||||||
|
# ---------------------------------------------------------- §9 eseguibilita'
|
||||||
|
print("\n" + "-" * L)
|
||||||
|
print(f" 9. ESEGUIBILITA' a ${CAPITAL:.0f} — il vantaggio DICHIARATO dal filone (dimezza gli")
|
||||||
|
print(" ordini). Target NETTATO 0.75*TP01 + 0.25*SKH sul grid 1h = cio' che il cron manda.")
|
||||||
|
print("-" * L)
|
||||||
|
print(f" {'configurazione':<26}{'Sh model':>10}{'Sh reale':>10}{'haircut':>9}"
|
||||||
|
f"{'ordini eseg':>13}{'sotto-min':>11}{'turnover/a':>12}")
|
||||||
|
EXEC = {}
|
||||||
|
for lab, r in (("BASE (size fissa)", None), ("cella dello scettico", RES[nmcan]),
|
||||||
|
("cella scelta al buio", CHOSEN)):
|
||||||
|
nm = None if r is None else "SEL"
|
||||||
|
tm = tr = tu = 0.0
|
||||||
|
no = ns = 0
|
||||||
|
for a in ASSETS:
|
||||||
|
df1h = TO.get1h(a)
|
||||||
|
SZa = {} if r is None else {
|
||||||
|
"SEL": (lambda ex, rr=r, aa=a: size_bin(rr["thr"], rr["w"], rr["h"],
|
||||||
|
rr["sg"])(ex, (can, aa)))}
|
||||||
|
tgt = V.netted_target(a, EX[can][a], nm, SZa, {}, {}, {}, {}, TP, can, df1h)
|
||||||
|
ev = A.eval_weights_smallcap(df1h, tgt, capital=CAPITAL, min_order=MIN_ORDER)
|
||||||
|
tm += 0.5 * ev["modeled"]["sharpe"]
|
||||||
|
tr += 0.5 * ev["realistic"]["sharpe"]
|
||||||
|
tu += ev["executed_turnover_per_year"]
|
||||||
|
no += ev["n_executed_trades"]
|
||||||
|
dw = np.abs(np.diff(np.nan_to_num(tgt), prepend=0.0))
|
||||||
|
ns += int(((dw > 1e-12) & (dw * CAPITAL < MIN_ORDER)).sum())
|
||||||
|
EXEC[lab] = (tm, tr, no, ns, tu)
|
||||||
|
print(f" {lab:<26}{tm:>10.3f}{tr:>10.3f}{tm-tr:>9.3f}{no:>13}{ns:>11}{tu:>12.1f}")
|
||||||
|
print(" 'ordini eseg' e 'sotto-min' sono sui due asset sommati, su tutta la storia 1h.")
|
||||||
|
ntr_base = int(sum(len(EX[can][a]['i_ent']) for a in ASSETS))
|
||||||
|
ntr_sel = int(sum((size_bin(CHOSEN['thr'], CHOSEN['w'], CHOSEN['h'], CHOSEN['sg'])
|
||||||
|
(EX[can][a], (can, a)) > 0).sum() for a in ASSETS))
|
||||||
|
print(f" trade SKH eseguiti (off{can}): BASE {ntr_base} -> cella scelta {ntr_sel} "
|
||||||
|
f"({ntr_sel/ntr_base*100:.0f}%)")
|
||||||
|
print(" ⚠ IL RISPARMIO DI FEE E' GIA' DENTRO i numeri sopra: `extract` netta ogni trade di")
|
||||||
|
print(f" FEE_RT={V.FEE_RT:.4f} (10 bps RT) all'ingresso, quindi un trade scartato non paga.")
|
||||||
|
print(" Convertirlo di nuovo con la curva del 26/07 (-0,017 Sh/bps di libro, SKH 4x TP01)")
|
||||||
|
print(" sarebbe un DOPPIO CONTEGGIO. Cio' che NON e' dentro e' (a) il min-order a $635,")
|
||||||
|
print(" misurato qui sopra come haircut, e (b) il fatto che la fee reale e' 3,5 bps/lato =")
|
||||||
|
print(" 7 bps RT contro i 10 modellati -> i backtest sono CONSERVATIVI, in entrambe le braccia.")
|
||||||
|
|
||||||
|
# ---------------------------------------------------------- §9b fee vs informazione
|
||||||
|
print("\n" + "-" * L)
|
||||||
|
print(" 9b. QUANTO DELL'EFFETTO E' INFORMAZIONE E QUANTO E' SOLO FEE RISPARMIATA.")
|
||||||
|
print(" Scartare meta' dei trade risparmia meta' delle fee: parte del +0,080 e' quella,")
|
||||||
|
print(" ed e' un guadagno vero ma NON e' un'ipotesi sulla vol. Lo stesso confronto,")
|
||||||
|
print(" identico in tutto, ricalcolato a fee diverse (le size si ricalcolano da zero,")
|
||||||
|
print(" perche' la vol trailing della gamba dipende anche dalle fee).")
|
||||||
|
print("-" * L)
|
||||||
|
print(f" {'lente':<40}{'cella scettico':>17}{'pos/n':>8}{'cella al buio':>17}{'pos/n':>8}")
|
||||||
|
FEEDEC = {}
|
||||||
|
keep_fee = V.FEE_RT
|
||||||
|
for fee, tag in ((keep_fee, f"MODELLATA {keep_fee*1e4:.0f} bps RT (i backtest)"),
|
||||||
|
(0.0007, "REALE 3,5 bps/lato = 7 bps RT"),
|
||||||
|
(0.0, "fee ZERO (informazione pura)")):
|
||||||
|
V.FEE_RT = fee
|
||||||
|
if fee == keep_fee:
|
||||||
|
EXf, basef = EX, base_leg
|
||||||
|
else:
|
||||||
|
EXf = {o: {a: V.extract(a, o) for a in ASSETS} for o in offs}
|
||||||
|
basef = {o: V.leg_daily(EXf[o], {a: V.size_flat(EXf[o][a]) for a in ASSETS})
|
||||||
|
for o in offs}
|
||||||
|
row = ""
|
||||||
|
for r in (RES[nmcan], CHOSEN):
|
||||||
|
fn = size_bin(r["thr"], r["w"], r["h"], r["sg"])
|
||||||
|
bk, ct = {}, {}
|
||||||
|
for o in offs:
|
||||||
|
lg = V.leg_daily(EXf[o], {a: fn(EXf[o][a], (tag, o, a)) for a in ASSETS})
|
||||||
|
bk[o], ct[o], _ = iso_pair(TP, lg, basef[o])
|
||||||
|
d = A.anchor_luck_delta(lambda o: bk[o], lambda o: ct[o], offs, metric=sh)
|
||||||
|
FEEDEC[(tag, cname(r["thr"], r["w"], r["h"], r["sg"]))] = d["median_paired"]
|
||||||
|
row += f"{d['median_paired']:>+17.3f}{d['n_positive']:>5}/{len(offs):<3}"
|
||||||
|
print(f" {tag:<40}{row}")
|
||||||
|
if fee != keep_fee:
|
||||||
|
del EXf, basef
|
||||||
|
V.FEE_RT = keep_fee
|
||||||
|
assert V.FEE_RT == 0.001
|
||||||
|
f_mod = FEEDEC[(f"MODELLATA {keep_fee*1e4:.0f} bps RT (i backtest)", nmcan)]
|
||||||
|
f_zero = FEEDEC[("fee ZERO (informazione pura)", nmcan)]
|
||||||
|
print(f" -> a fee ZERO resta {f_zero:+.3f} di {f_mod:+.3f} = "
|
||||||
|
f"{100*f_zero/f_mod if f_mod else float('nan'):.0f}% dell'effetto: la parte restante")
|
||||||
|
print(" e' fee risparmiata. La fee vera oggi e' 3,5 bps/lato, meno di quella modellata,")
|
||||||
|
print(" quindi nel LIVE la componente-fee del vantaggio e' PIU' PICCOLA di quella misurata")
|
||||||
|
print(" sui backtest — il verso in cui il numero pubblicato e' ottimista per il deploy.")
|
||||||
|
|
||||||
|
# ---------------------------------------------------------- §10 trasferimento su V1
|
||||||
|
print("\n" + "-" * L)
|
||||||
|
print(" 10. TRASFERIMENTO su SKH01_V1 — seconda sequenza di trade, geometria d'uscita")
|
||||||
|
print(" DIVERSA (stop/target in ATR invece che a percentuale fissa). Non e' un hold-out")
|
||||||
|
print(" temporale, ma e' l'unico campione indipendente disponibile a questo costo.")
|
||||||
|
print("-" * L)
|
||||||
|
EX1 = {o: {a: V.extract(a, o, SKH01_V1) for a in ASSETS} for o in offs}
|
||||||
|
base1 = {o: V.leg_daily(EX1[o], {a: V.size_flat(EX1[o][a]) for a in ASSETS}) for o in offs}
|
||||||
|
print(f" {'cella':<26}{'ISO dShFULL':>13}{'pos/n':>8}{'ISO dShHOLD':>14}{'pos/n':>8}"
|
||||||
|
f"{'tenuti':>9}")
|
||||||
|
for lab, r in (("cella dello scettico", RES[nmcan]), ("cella scelta al buio", CHOSEN)):
|
||||||
|
fn = size_bin(r["thr"], r["w"], r["h"], r["sg"])
|
||||||
|
b1, c1, kp = {}, {}, []
|
||||||
|
for o in offs:
|
||||||
|
lg = V.leg_daily(EX1[o], {a: fn(EX1[o][a], ("V1", o, a)) for a in ASSETS})
|
||||||
|
b1[o], c1[o], _ = iso_pair(TP, lg, base1[o])
|
||||||
|
kp.append(float(np.mean(np.concatenate(
|
||||||
|
[fn(EX1[o][a], ("V1", o, a)) for a in ASSETS]))))
|
||||||
|
dF = A.anchor_luck_delta(lambda o: b1[o], lambda o: c1[o], offs, metric=sh)
|
||||||
|
dH = A.anchor_luck_delta(lambda o: hold(b1[o]), lambda o: hold(c1[o]), offs, metric=sh)
|
||||||
|
print(f" {cname(r['thr'], r['w'], r['h'], r['sg']):<26}{dF['median_paired']:>+13.3f}"
|
||||||
|
f"{dF['n_positive']:>5}/{len(offs):<3}{dH['median_paired']:>+14.3f}"
|
||||||
|
f"{dH['n_positive']:>5}/{len(offs):<3}{float(np.median(kp))*100:>8.0f}%")
|
||||||
|
|
||||||
|
# ---------------------------------------------------------- §11 per anno
|
||||||
|
print("\n" + "-" * L)
|
||||||
|
print(" 11. SCOMPOSIZIONE PER ANNO (regola 22/08 dopo SOL: un contributo positivo si")
|
||||||
|
print(" scompone per ANNO prima di crederci — 24/24 ancore possono essere un anno solo)")
|
||||||
|
print("-" * L)
|
||||||
|
yrs = sorted({int(y) for y in BK[nmcan][can].index.year})
|
||||||
|
print(f" {'cella':<26}" + "".join(f"{y:>8}" for y in yrs))
|
||||||
|
for lab, r in (("cella dello scettico", RES[nmcan]), ("cella scelta al buio", CHOSEN)):
|
||||||
|
nm = cname(r["thr"], r["w"], r["h"], r["sg"])
|
||||||
|
cc = []
|
||||||
|
for y in yrs:
|
||||||
|
v = [sh(BK[nm][o][BK[nm][o].index.year == y]) - sh(CT[nm][o][CT[nm][o].index.year == y])
|
||||||
|
for o in offs if (BK[nm][o].index.year == y).sum() > 20]
|
||||||
|
cc.append(float(np.median(v)) if v else float("nan"))
|
||||||
|
print(f" {nm:<26}" + "".join(f"{c:>+8.2f}" for c in cc) +
|
||||||
|
f" positivi {int(np.nansum(np.asarray(cc) > 0))}/{len(yrs)}")
|
||||||
|
|
||||||
|
# ---------------------------------------------------------- §12 la domanda che decide
|
||||||
|
print("\n" + "-" * L)
|
||||||
|
print(" 12. LA DOMANDA CHE DECIDE — un effetto che vale un terzo della propria incertezza")
|
||||||
|
print(" merita di cambiare un sistema che gira con soldi veri?")
|
||||||
|
print("-" * L)
|
||||||
|
amp = max(ban_F) - min(ban_F)
|
||||||
|
nm = nmcan
|
||||||
|
dC = float(np.median([cagr(BK[nm][o]) - cagr(CT[nm][o]) for o in offs]))
|
||||||
|
cag_band = [cagr(book(TP, base_leg[o])) for o in offs]
|
||||||
|
print(f" (i) IL CONFRONTO GIUSTO NON E' QUELLO DELLA DOMANDA. La banda d'ancora del")
|
||||||
|
print(f" LIVELLO ({amp:.3f} di ampiezza, +-{amp/2:.3f}) e' incertezza sul livello, e")
|
||||||
|
print(f" si CANCELLA in un confronto APPAIATO: le due braccia girano sulla STESSA")
|
||||||
|
print(f" ancora. Cio' che va confrontato con l'effetto e' la dispersione della")
|
||||||
|
print(f" DIFFERENZA appaiata, che e' un ordine di grandezza piu' piccola.")
|
||||||
|
print(f" cella scettico: effetto {RES[nm]['dF']:+.3f}, banda appaiata "
|
||||||
|
f"[{RES[nm]['lo']:+.3f},{RES[nm]['hi']:+.3f}], "
|
||||||
|
f"{RES[nm]['nF']}/{len(offs)} ancore positive.")
|
||||||
|
print(f" (ii) MA la banda del livello torna a mordere DUE volte, e in modo decisivo:")
|
||||||
|
print(f" (a) la SCELTA della cella e' fatta guardando quella banda -> la stima onesta")
|
||||||
|
print(f" non e' l'argmax ma la MEDIANA DI FAMIGLIA "
|
||||||
|
f"({float(np.median([r['dF'] for r in POS])):+.3f} sul segno +1);")
|
||||||
|
print(f" (b) l'effetto e' misurato sugli STESSI 8 anni su cui SKH01 e' stato")
|
||||||
|
print(f" selezionato, e non esiste un forward capace di misurarlo (SE(Sharpe)")
|
||||||
|
print(f" ~1,4 su 6 mesi contro un effetto di 0,08).")
|
||||||
|
print(f" (iii) IL CRITERIO, dichiarato: si tocca il libro live solo se TUTTE E QUATTRO —")
|
||||||
|
print(f" (1) l'effetto sopravvive alla stima ONESTA di famiglia (mediana, non argmax)")
|
||||||
|
print(f" (2) `weights_tilt_null` passa (e' il gate codificato per i cambi di peso)")
|
||||||
|
print(f" (3) il gate marginale + un null CON POTENZA lo sostengono")
|
||||||
|
print(f" (4) l'effetto e' grande abbastanza da pagare un modo di fallire in piu'.")
|
||||||
|
print(f" (iv) ORDINI DI GRANDEZZA a ${CAPITAL:.0f} (regola 26/07: quando le leve hanno")
|
||||||
|
print(f" ordini di grandezza diversi, DIRLO):")
|
||||||
|
print(f" effetto della cella (ISO, mediana appaiata): dCAGR {dC*100:+.2f}%/anno")
|
||||||
|
print(f" = ${dC*CAPITAL:+.2f}/anno = EUR {dC*CAPITAL/365*0.86:+.4f}/giorno")
|
||||||
|
print(f" banda d'ancora del CAGR del libro: "
|
||||||
|
f"[{min(cag_band)*100:.1f}%, {max(cag_band)*100:.1f}%] "
|
||||||
|
f"= ${(max(cag_band)-min(cag_band))*CAPITAL:.0f}/anno di incertezza")
|
||||||
|
print(f" versare EUR 500/mese (il piano dell'operatore) = EUR 16,44/giorno di capitale")
|
||||||
|
print(f" nuovo, cioe' ~{16.44/max(1e-9, abs(dC*CAPITAL/365*0.86)):.0f}x l'effetto misurato qui.")
|
||||||
|
print(f" (v) IL CRITERIO APPLICATO, riga per riga:")
|
||||||
|
med_pos = float(np.median([r['dF'] for r in POS]))
|
||||||
|
c1 = med_pos > 0
|
||||||
|
c3 = True
|
||||||
|
print(f" (1) stima ONESTA di famiglia (mediana segno +1) = {med_pos:+.3f} "
|
||||||
|
f"contro argmax {argmax['dF']:+.3f} e cella pubblicata {RES[nmcan]['dF']:+.3f}"
|
||||||
|
f" -> {'PASS' if c1 else 'FAIL'}")
|
||||||
|
print(f" (2) `weights_tilt_null`: gate_pass=False nella forma nuda, INERTE nella forma")
|
||||||
|
print(f" compensata -> il gate non lo sostiene in nessuna delle due -> FAIL/vacuo")
|
||||||
|
print(f" (3) marginale ADDS + null di permutazione con potenza (§6) -> vedi sopra")
|
||||||
|
print(f" (4) taglia contro il modo di fallire in piu': "
|
||||||
|
f"EUR {dC*CAPITAL/365*0.86:+.4f}/giorno a ${CAPITAL:.0f} -> FAIL")
|
||||||
|
print(f" Di cui, per giunta, solo il {100*f_zero/f_mod if f_mod else float('nan'):.0f}% "
|
||||||
|
f"e' informazione e il resto e' fee risparmiata (§9b),")
|
||||||
|
print(f" e la fee vera e' piu' bassa di quella modellata.")
|
||||||
|
|
||||||
|
print("\n" + "=" * L)
|
||||||
|
print(f" fatto in {time.time()-t0:.0f}s")
|
||||||
|
print("=" * L)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,957 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
"""r0822b_bocpd.py — IL FALSIFICATORE NOMINATO E MAI ESEGUITO del filone ADAPTIVE-HORIZON.
|
||||||
|
|
||||||
|
CONTESTO (perche' questo file esiste)
|
||||||
|
-------------------------------------
|
||||||
|
Il 22/08 `r0822_adaptive_horizon.py` ha scartato il TSMOM a orizzonte adattivo con una diagnosi
|
||||||
|
netta: il "vincitore adattivo" aveva L_t **incollato al bordo il 100% del tempo** = un TSMOM a
|
||||||
|
orizzonte COSTANTE di 20 giorni travestito (corr 1.000, dSh 0.00 in 8/8 anni e 0/12 ancore);
|
||||||
|
isolando le 52 celle davvero adattive -> NEUTRAL, corr->TP01 0.90 = TP01 travestito.
|
||||||
|
Ma quell'agente ha dichiarato **cosa lo smentirebbe** e non ha potuto eseguirlo:
|
||||||
|
|
||||||
|
"Un rilevatore il cui L_t si muove DAVVERO e batte il miglior lookback COSTANTE (non TP01) in
|
||||||
|
>=6/8 anni appaiati con corr->TP01 < 0.6. La mia famiglia copre 3 rilevatori a soglia/finestra
|
||||||
|
fissa — un run-length posterior bayesiano (BOCPD/PELT vero) o un L scelto per walk-forward sul
|
||||||
|
PnL realizzato non sono stati provati."
|
||||||
|
|
||||||
|
Questo file esegue **quel** test. Oggi il filone e' chiuso su UNA famiglia (3 proxy a soglia
|
||||||
|
fissa), non sul MECCANISMO. Una seconda chiusura, con rilevatori veri, rende il "no" definitivo.
|
||||||
|
|
||||||
|
PERCHE' BOCPD E' STRUTTURALMENTE DIVERSO DAI 3 PROXY DEL PRIMO TENTATIVO
|
||||||
|
-----------------------------------------------------------------------
|
||||||
|
I 3 proxy (CUSUM a soglia k, variance-ratio, Hurst) producevano una STATISTICA che veniva poi
|
||||||
|
MAPPATA linearmente dentro [Lmin, Lmax]: la mappatura e' arbitraria, e infatti la statistica
|
||||||
|
saturava contro il clip (bordo 100%). Un run-length posterior NON ha bisogno di mappatura:
|
||||||
|
la lunghezza di run **e' gia' un lookback**, nelle stesse unita' (barre). "Quanto indietro posso
|
||||||
|
guardare" = "da quanto tempo non cambia il regime" e' un'identita', non una scelta di scala.
|
||||||
|
* BOCPD (Adams & MacKay 2007): posterior online su r_t con hazard costante e verosimiglianza
|
||||||
|
Normale-InverseGamma coniugata (predittiva Student-t). Causale PER COSTRUZIONE: e' un filtro
|
||||||
|
in avanti, ogni P(r_t | x_{1:t}) usa solo x_{1:t}.
|
||||||
|
* OP/PELT causale: PELT e' un'accelerazione (pruning) dell'**optimal partitioning**
|
||||||
|
F(t) = min_{s<t} [F(s) + C(y_{s+1:t}) + beta]. La ricorsione E' online. ⚠️ La maggior parte
|
||||||
|
delle implementazioni di change-point (ruptures.Pelt().fit_predict(intera_serie)) e'
|
||||||
|
**retrospettiva** e restituisce la segmentazione dell'INTERO campione = look-ahead colossale.
|
||||||
|
Qui F(t) e la sua backtracking-partition sono calcolati SOLO su y[0:t] per ogni t.
|
||||||
|
Verificato con `A.causality_ok` **e** con un troncamento manuale esplicito (sezione 0-bis).
|
||||||
|
* WF-PnL: L scelto per walk-forward sullo Sharpe NETTO realizzato dei candidati costanti nella
|
||||||
|
finestra all'indietro. Non usa alcun modello di regime: e' il "cosa avrebbe funzionato finora".
|
||||||
|
|
||||||
|
IL NULL GIUSTO NON E' TP01 — E' IL MIGLIOR LOOKBACK COSTANTE
|
||||||
|
------------------------------------------------------------
|
||||||
|
E' il controllo che ha ucciso il primo tentativo, e va rimesso PRIMA, non dopo: senza di esso
|
||||||
|
il null confonde "adattare paga" con "un orizzonte piu' corto paga su questo campione".
|
||||||
|
Percio' il confronto principale e': adattivo (cella scelta in-sample) vs miglior L COSTANTE
|
||||||
|
(cella scelta in-sample, stessa geometria, stesso TF, stesse fee), **appaiato per ANNO e per
|
||||||
|
ANCORA** (mediana delle DIFFERENZE APPAIATE, mai differenza delle mediane — lezione 26/07).
|
||||||
|
|
||||||
|
FAMIGLIA DICHIARATA **PRIMA** DI GUARDARE (conteggio al RIALZO)
|
||||||
|
--------------------------------------------------------------
|
||||||
|
A) rilevatori veri 2 (bocpd | op)
|
||||||
|
parametro 3 (bocpd: hazard lambda 50/100/250 g ; op: penalita' k 2/5/10)
|
||||||
|
statistica 2 (bocpd: E[r_t] | argmax r_t ; op: eta' dall'ultimo cp | mediana
|
||||||
|
delle lunghezze di segmento gia' concluse)
|
||||||
|
mappatura 2 (direct | inverse) [il verso non e' ovvio a priori => e' un asse]
|
||||||
|
blend 2 (single L | triple L/3,L,2L — con L=90 il triple E' TP01)
|
||||||
|
timeframe 2 (1d | 12h) [>=12h: sotto, costi+overfit dominano, 19/06]
|
||||||
|
= 2*3*2*2*2*2 = **96 trial**
|
||||||
|
B) controllo L COSTANTE 12 L x 2 blend x 2 TF = **48 trial**
|
||||||
|
C) WF-PnL (L da walk-forward) 3 finestre x 2 blend x 2 TF = **12 trial**
|
||||||
|
D) taratura della SCALA 8 penalita' OP + 5 hazard BOCPD = **13** (sezione 1-pre)
|
||||||
|
---------------------------------------------------------------- TOTALE DICHIARATO **169**
|
||||||
|
⚠️ (D) e' una scansione fatta guardando SOLO il numero di change-point rilevati — una proprieta'
|
||||||
|
del RILEVATORE — e mai uno Sharpe: nessuna di quelle 13 configurazioni produce un rendimento. Si
|
||||||
|
dichiara lo stesso, al rialzo, perche' ha ristretto la griglia di (A). Il suo esito e' gia' un
|
||||||
|
risultato: a penalita' OP >= 5 l'eta' resta incollata al tetto della finestra il 44% del tempo,
|
||||||
|
cioe' **l'artefatto del 22/08 riprodotto su un rilevatore vero** — e la griglia (A) usa solo la
|
||||||
|
regione in cui il rilevatore rileva davvero (0.5/1.0/2.0 = 18-40 change-point l'anno).
|
||||||
|
Costanti FISSATE A PRIORI (gradi di liberta' non cercati, dichiarati perche' esistono):
|
||||||
|
Lmin=20g, Lmax=200g (contengono 30..180 del canonico e sono gli stessi del primo tentativo);
|
||||||
|
BOCPD su rendimenti log STANDARDIZZATI da vol rolling causale 30g (il bersaglio e' il DRIFT, non
|
||||||
|
la vol); prior NIG mu0=0, kappa0=1, alpha0=1, beta0=1; troncamento del posterior a Lmax barre;
|
||||||
|
costo OP = mean-shift gaussiano su dati standardizzati, penalita' beta = k*log(t) causale.
|
||||||
|
Il deflated-Sharpe e' riportato a N=169 (mia famiglia), N=281 (mia + le 112 del primo tentativo:
|
||||||
|
**stessa domanda**, i trial si sommano) e N=449 (di SCREEN — lezione 22/08 del filone ORTHO:
|
||||||
|
su BTC/ETH direzionale il massimo atteso dal rumore su 168 trial e' Sharpe 1.572, SOPRA il
|
||||||
|
soffitto misurato ~1.3, quindi il conteggio di screen e' quello che decide).
|
||||||
|
|
||||||
|
ORDINE DI ESECUZIONE (imposto dalla missione, ed e' l'ordine giusto)
|
||||||
|
--------------------------------------------------------------------
|
||||||
|
La diagnostica che ha smascherato il primo tentativo va PER PRIMA: quanto si muove davvero L_t?
|
||||||
|
Se sta al bordo, l'artefatto e' riprodotto e il filone si chiude di nuovo senza guardare i
|
||||||
|
rendimenti. Solo se L_t si muove ha senso misurare se il movimento PAGA.
|
||||||
|
|
||||||
|
Runtime atteso ~6-10 min su 2 core. Nessun file toccato fuori da questo.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import pandas as pd
|
||||||
|
from scipy.special import gammaln
|
||||||
|
from scipy.stats import norm
|
||||||
|
|
||||||
|
ROOT = Path("/opt/docker/PythagorasGoal")
|
||||||
|
sys.path.insert(0, str(ROOT / "scripts" / "research" / "alt"))
|
||||||
|
sys.path.insert(0, str(ROOT))
|
||||||
|
|
||||||
|
import altlib as A # noqa: E402
|
||||||
|
from src.strategies.trend_portfolio import CANONICAL, TrendPortfolio # noqa: E402
|
||||||
|
|
||||||
|
ASSETS = A.CERTIFIED
|
||||||
|
HOLDOUT = A.HOLDOUT
|
||||||
|
|
||||||
|
# ---- costanti FISSATE A PRIORI (dichiarate sopra, non cercate) -------------------------
|
||||||
|
LMIN_D, LMAX_D = 20, 200 # banda dell'orizzonte, IDENTICA al primo tentativo
|
||||||
|
FIXED_H = (30, 90, 180) # == (L/3, L, 2L) con L=90 -> TP01 canonico
|
||||||
|
TGT_VOL, LEV, VOLWIN = 0.20, 2.0, 30
|
||||||
|
NIG_MU0, NIG_KAP0, NIG_AL0, NIG_BE0 = 0.0, 1.0, 1.0, 1.0
|
||||||
|
STD_WIN_D = 30 # finestra della standardizzazione causale dei rendimenti
|
||||||
|
|
||||||
|
TFS = ("1d", "12h")
|
||||||
|
DETECTORS = ("bocpd", "op")
|
||||||
|
PARAMS = {"bocpd": (50, 100, 250), "op": (0.5, 1.0, 2.0)}
|
||||||
|
# TARATURA DELLA SCALA (sezione 1-pre): scansione ESPLORATIVA fatta guardando SOLO il numero di
|
||||||
|
# change-point rilevati — una proprieta' del RILEVATORE — e mai lo Sharpe. Un rilevatore che non
|
||||||
|
# rileva nulla non e' un rilevatore: a pen_k>=5 l'eta' resta incollata al tetto della finestra
|
||||||
|
# (44% del tempo) = ESATTAMENTE l'artefatto del 22/08, riprodotto qui su un rilevatore vero.
|
||||||
|
CAL_PEN = (0.02, 0.05, 0.1, 0.2, 0.5, 1.0, 2.0, 5.0)
|
||||||
|
CAL_LAM = (25, 50, 100, 250, 500)
|
||||||
|
STATS = {"bocpd": ("mean", "map"), "op": ("age", "segmed")}
|
||||||
|
MAPPINGS = ("direct", "inverse")
|
||||||
|
BLENDS = ("single", "triple")
|
||||||
|
|
||||||
|
CONST_L = (5, 7, 10, 15, 20, 30, 45, 60, 90, 120, 150, 200)
|
||||||
|
WF_WIN_D = (180, 365, 730)
|
||||||
|
|
||||||
|
N_DECLARED_A = len(DETECTORS) * 3 * 2 * len(MAPPINGS) * len(BLENDS) * len(TFS) # 96
|
||||||
|
N_DECLARED_B = len(CONST_L) * len(BLENDS) * len(TFS) # 48
|
||||||
|
N_DECLARED_C = len(WF_WIN_D) * len(BLENDS) * len(TFS) # 12
|
||||||
|
N_DECLARED_CAL = len(CAL_PEN) + len(CAL_LAM) # 13 (taratura)
|
||||||
|
N_DECLARED = N_DECLARED_A + N_DECLARED_B + N_DECLARED_C + N_DECLARED_CAL # 169
|
||||||
|
N_PRIOR_FAMILY = 112 # trial dichiarati da r0822_adaptive_horizon (stessa domanda)
|
||||||
|
N_SCREEN = 168 # pool di screen direzionale BTC/ETH dell'ondata (filone 12)
|
||||||
|
|
||||||
|
|
||||||
|
# =======================================================================================
|
||||||
|
# RILEVATORI VERI
|
||||||
|
# =======================================================================================
|
||||||
|
def _logret(c: np.ndarray) -> np.ndarray:
|
||||||
|
r = np.zeros(len(c))
|
||||||
|
r[1:] = np.log(c[1:] / c[:-1])
|
||||||
|
return r
|
||||||
|
|
||||||
|
|
||||||
|
def _std_returns(c: np.ndarray, bpd: int) -> np.ndarray:
|
||||||
|
"""Rendimenti log standardizzati da vol rolling CAUSALE (min_periods -> warmup a 0).
|
||||||
|
Il bersaglio del rilevatore e' il cambio di DRIFT: senza standardizzare, un cambio di
|
||||||
|
regime di VOLATILITA' dominerebbe la verosimiglianza e il rilevatore misurerebbe la vol."""
|
||||||
|
lr = _logret(c)
|
||||||
|
w = max(5, STD_WIN_D * bpd)
|
||||||
|
sig = pd.Series(lr).rolling(w, min_periods=max(5, w // 4)).std().values
|
||||||
|
y = np.where((sig > 0) & np.isfinite(sig), lr / np.where(sig > 0, sig, 1.0), 0.0)
|
||||||
|
return np.nan_to_num(np.clip(y, -8.0, 8.0))
|
||||||
|
|
||||||
|
|
||||||
|
def _student_logpdf(x: float, mu: np.ndarray, kap: np.ndarray,
|
||||||
|
al: np.ndarray, be: np.ndarray) -> np.ndarray:
|
||||||
|
"""Predittiva Student-t della Normale-InverseGamma coniugata (Murphy 2007, eq. 99-100):
|
||||||
|
x | mu,kappa,alpha,beta ~ t_{2 alpha}(mu, beta(kappa+1)/(alpha kappa))."""
|
||||||
|
nu = 2.0 * al
|
||||||
|
s2 = be * (kap + 1.0) / (al * kap)
|
||||||
|
z = (x - mu) ** 2 / (nu * s2)
|
||||||
|
return (gammaln(0.5 * (nu + 1.0)) - gammaln(0.5 * nu)
|
||||||
|
- 0.5 * np.log(nu * np.pi * s2) - 0.5 * (nu + 1.0) * np.log1p(z))
|
||||||
|
|
||||||
|
|
||||||
|
def _bocpd(y: np.ndarray, lam_bars: float, rmax: int) -> tuple[np.ndarray, np.ndarray]:
|
||||||
|
"""BOCPD (Adams & MacKay 2007) con hazard COSTANTE H = 1/lam_bars e verosimiglianza NIG.
|
||||||
|
Ritorna (E[r_t], argmax_r P(r_t)) in BARRE. Filtro in avanti: ogni valore usa solo il
|
||||||
|
passato -> causale per costruzione, e stabile su prefisso (nessuna statistica full-sample).
|
||||||
|
Il posterior e' troncato a rmax barre e rinormalizzato (il troncamento e' un limite di
|
||||||
|
memoria, NON un clip sul valore: se la massa si accumula a rmax lo si vede in diagnostica)."""
|
||||||
|
n = len(y)
|
||||||
|
haz = 1.0 / max(2.0, float(lam_bars))
|
||||||
|
R = np.array([1.0])
|
||||||
|
mu = np.array([NIG_MU0]); kap = np.array([NIG_KAP0])
|
||||||
|
al = np.array([NIG_AL0]); be = np.array([NIG_BE0])
|
||||||
|
rl = np.arange(1)
|
||||||
|
mean_rl = np.zeros(n); map_rl = np.zeros(n)
|
||||||
|
for t in range(n):
|
||||||
|
x = float(y[t])
|
||||||
|
lp = _student_logpdf(x, mu, kap, al, be)
|
||||||
|
lp -= lp.max()
|
||||||
|
pred = np.exp(lp)
|
||||||
|
gr = R * pred * (1.0 - haz)
|
||||||
|
cp = float(np.sum(R * pred * haz))
|
||||||
|
m_new = min(len(R) + 1, rmax + 1)
|
||||||
|
newR = np.empty(m_new)
|
||||||
|
newR[0] = cp
|
||||||
|
newR[1:] = gr[:m_new - 1]
|
||||||
|
s = newR.sum()
|
||||||
|
if not np.isfinite(s) or s <= 0:
|
||||||
|
newR = np.zeros(m_new); newR[0] = 1.0; s = 1.0
|
||||||
|
newR /= s
|
||||||
|
# aggiornamento delle statistiche sufficienti (run r -> run r+1), prior in testa
|
||||||
|
mu_f = np.concatenate(([NIG_MU0], (kap * mu + x) / (kap + 1.0)))[:m_new]
|
||||||
|
kap_f = np.concatenate(([NIG_KAP0], kap + 1.0))[:m_new]
|
||||||
|
al_f = np.concatenate(([NIG_AL0], al + 0.5))[:m_new]
|
||||||
|
be_f = np.concatenate(([NIG_BE0], be + kap * (x - mu) ** 2 / (2.0 * (kap + 1.0))))[:m_new]
|
||||||
|
R, mu, kap, al, be = newR, mu_f, kap_f, al_f, be_f
|
||||||
|
if len(rl) != m_new:
|
||||||
|
rl = np.arange(m_new, dtype=float)
|
||||||
|
mean_rl[t] = float(np.dot(R, rl))
|
||||||
|
map_rl[t] = float(rl[int(np.argmax(R))])
|
||||||
|
return mean_rl, map_rl
|
||||||
|
|
||||||
|
|
||||||
|
def _op_causal(y: np.ndarray, pen_k: float, smax: int) -> tuple[np.ndarray, np.ndarray]:
|
||||||
|
"""OPTIMAL PARTITIONING **online** (l'obiettivo esatto che PELT accelera con il pruning):
|
||||||
|
F(t) = min_{s in [t-smax, t-1]} [ F(s) + C(y[s:t]) + beta(t) ]
|
||||||
|
con C = costo gaussiano di mean-shift su dati standardizzati e beta(t) = pen_k*log(t).
|
||||||
|
⚠️ Il termine sum(y^2) del costo gaussiano e' INVARIANTE alla partizione (ogni punto sta in
|
||||||
|
esattamente un segmento) -> si omette dall'argmin. C(s:t) = -(S[t]-S[s])^2/(t-s).
|
||||||
|
Ritorna (eta' dall'ultimo change-point, mediana delle lunghezze dei segmenti CONCLUSI),
|
||||||
|
entrambe in barre. Per ogni t si usa SOLO y[0:t]: e' la differenza fra questo e un
|
||||||
|
`ruptures.Pelt().fit_predict(y)` retrospettivo, che guarda tutto il campione."""
|
||||||
|
n = len(y)
|
||||||
|
S = np.concatenate(([0.0], np.cumsum(y)))
|
||||||
|
F = np.full(n + 1, np.inf)
|
||||||
|
F[0] = 0.0
|
||||||
|
prev = np.zeros(n + 1, dtype=int)
|
||||||
|
age = np.zeros(n); segmed = np.zeros(n)
|
||||||
|
for t in range(1, n + 1):
|
||||||
|
a = max(0, t - smax)
|
||||||
|
s_idx = np.arange(a, t)
|
||||||
|
seg = (S[t] - S[a:t])
|
||||||
|
ln = (t - s_idx).astype(float)
|
||||||
|
cand = F[a:t] - seg * seg / ln
|
||||||
|
j = int(np.argmin(cand))
|
||||||
|
beta = pen_k * np.log(max(2.0, float(t)))
|
||||||
|
F[t] = cand[j] + beta
|
||||||
|
prev[t] = int(s_idx[j])
|
||||||
|
# backtracking della partizione VALIDA A t (cap a 40 segmenti: ci serve la mediana)
|
||||||
|
last = prev[t]
|
||||||
|
age[t - 1] = t - last
|
||||||
|
lens = []
|
||||||
|
u = last
|
||||||
|
while u > 0 and len(lens) < 40:
|
||||||
|
p = prev[u]
|
||||||
|
lens.append(u - p)
|
||||||
|
u = p
|
||||||
|
segmed[t - 1] = float(np.median(lens)) if lens else float(t)
|
||||||
|
return age, segmed
|
||||||
|
|
||||||
|
|
||||||
|
_STAT_CACHE: dict = {}
|
||||||
|
|
||||||
|
|
||||||
|
def _stat_key(df: pd.DataFrame, det: str, par) -> tuple:
|
||||||
|
"""Chiave chiusa sul CONTENUTO del frame (lunghezza + primo/ultimo timestamp): un PREFISSO
|
||||||
|
non deve prendere la cache del campione pieno, altrimenti `causality_ok` verrebbe ingannata
|
||||||
|
dalla cache invece che dal codice."""
|
||||||
|
ts = df["timestamp"].values
|
||||||
|
return (det, par, len(df), int(ts[0]), int(ts[-1]))
|
||||||
|
|
||||||
|
|
||||||
|
def detector_stats(df: pd.DataFrame, det: str, par) -> dict:
|
||||||
|
key = _stat_key(df, det, par)
|
||||||
|
if key in _STAT_CACHE:
|
||||||
|
return _STAT_CACHE[key]
|
||||||
|
bpd = A.bars_per_day(df)
|
||||||
|
c = df["close"].values.astype(float)
|
||||||
|
y = _std_returns(c, bpd)
|
||||||
|
rmax = max(4, LMAX_D * bpd)
|
||||||
|
if det == "bocpd":
|
||||||
|
m, mp = _bocpd(y, float(par) * bpd, rmax)
|
||||||
|
out = {"mean": m, "map": mp}
|
||||||
|
elif det == "op":
|
||||||
|
ag, sm = _op_causal(y, float(par), rmax)
|
||||||
|
out = {"age": ag, "segmed": sm}
|
||||||
|
else:
|
||||||
|
raise ValueError(det)
|
||||||
|
_STAT_CACHE[key] = out
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def lookback_bars(df: pd.DataFrame, det: str, par, stat: str, mapping: str) -> np.ndarray:
|
||||||
|
"""L_t in BARRE dentro [Lmin, Lmax] giorni. `direct` = la lunghezza di run E' il lookback
|
||||||
|
(dopo un change-point si puo' guardare indietro solo fin la'); `inverse` e' lo specchio,
|
||||||
|
dichiarato come asse perche' il verso non e' ovvio a priori."""
|
||||||
|
bpd = A.bars_per_day(df)
|
||||||
|
lo, hi = max(2, LMIN_D * bpd), max(3, LMAX_D * bpd)
|
||||||
|
v = detector_stats(df, det, par)[stat]
|
||||||
|
base = np.clip(np.nan_to_num(v, nan=float(lo)), lo, hi)
|
||||||
|
L = base if mapping == "direct" else (lo + hi - base)
|
||||||
|
return np.clip(np.round(L), 2, hi).astype(int)
|
||||||
|
|
||||||
|
|
||||||
|
# =======================================================================================
|
||||||
|
# DIREZIONE TSMOM (orizzonte costante o variabile) + vol-target — IDENTICA al primo tentativo
|
||||||
|
# =======================================================================================
|
||||||
|
def _sign_at(c: np.ndarray, h: np.ndarray) -> tuple[np.ndarray, np.ndarray]:
|
||||||
|
n = len(c)
|
||||||
|
i = np.arange(n)
|
||||||
|
j = i - h
|
||||||
|
ok = j >= 0
|
||||||
|
s = np.zeros(n)
|
||||||
|
jj = np.where(ok, j, 0)
|
||||||
|
s[ok] = np.sign(c[i[ok]] / c[jj[ok]] - 1.0)
|
||||||
|
return s, ok
|
||||||
|
|
||||||
|
|
||||||
|
def _dir_from_L(c: np.ndarray, L: np.ndarray, blend: str) -> np.ndarray:
|
||||||
|
hs = [L] if blend == "single" else [
|
||||||
|
np.maximum(2, np.round(L / 3.0).astype(int)), L, np.round(2.0 * L).astype(int)]
|
||||||
|
acc = np.zeros(len(c)); cnt = np.zeros(len(c))
|
||||||
|
for h in hs:
|
||||||
|
s, ok = _sign_at(c, np.asarray(h, int))
|
||||||
|
acc[ok] += s[ok]; cnt[ok] += 1
|
||||||
|
out = np.zeros(len(c)); nz = cnt > 0
|
||||||
|
out[nz] = acc[nz] / cnt[nz]
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def fixed_target(df: pd.DataFrame) -> np.ndarray:
|
||||||
|
"""BASELINE: TSMOM fisso 30/90/180, long-flat, vol-target 20%, leva 2x = TP01 canonico."""
|
||||||
|
c = df["close"].values.astype(float)
|
||||||
|
bpd = A.bars_per_day(df)
|
||||||
|
acc = np.zeros(len(c)); cnt = np.zeros(len(c))
|
||||||
|
for hd in FIXED_H:
|
||||||
|
s, ok = _sign_at(c, np.full(len(c), hd * bpd, int))
|
||||||
|
acc[ok] += s[ok]; cnt[ok] += 1
|
||||||
|
d = np.zeros(len(c)); nz = cnt > 0
|
||||||
|
d[nz] = acc[nz] / cnt[nz]
|
||||||
|
return A.vol_target(np.clip(d, 0, None), df, TGT_VOL, VOLWIN, LEV)
|
||||||
|
|
||||||
|
|
||||||
|
def const_factory(tf: str, L: int, blend: str):
|
||||||
|
def fn(df: pd.DataFrame) -> np.ndarray:
|
||||||
|
c = df["close"].values.astype(float)
|
||||||
|
bpd = A.bars_per_day(df)
|
||||||
|
Lb = np.full(len(c), max(2, int(round(L * bpd))), int)
|
||||||
|
return A.vol_target(np.clip(_dir_from_L(c, Lb, blend), 0, None), df, TGT_VOL, VOLWIN, LEV)
|
||||||
|
fn.__name__ = f"const_L{L}_{blend}"
|
||||||
|
return fn
|
||||||
|
|
||||||
|
|
||||||
|
def factory(tf: str, det: str, par, stat: str, mapping: str, blend: str):
|
||||||
|
def fn(df: pd.DataFrame) -> np.ndarray:
|
||||||
|
c = df["close"].values.astype(float)
|
||||||
|
L = lookback_bars(df, det, par, stat, mapping)
|
||||||
|
return A.vol_target(np.clip(_dir_from_L(c, L, blend), 0, None), df, TGT_VOL, VOLWIN, LEV)
|
||||||
|
fn.__name__ = f"adapt_{det}{par}_{stat}_{mapping}_{blend}"
|
||||||
|
return fn
|
||||||
|
|
||||||
|
|
||||||
|
_WF_CACHE: dict = {}
|
||||||
|
|
||||||
|
|
||||||
|
def wf_picks(df: pd.DataFrame, win_d: int, blend: str):
|
||||||
|
"""(C) L scelto per WALK-FORWARD sul PnL NETTO realizzato: a ogni barra si prende l'L che,
|
||||||
|
fra i candidati COSTANTI, ha lo Sharpe netto migliore nella finestra all'indietro `win_d`.
|
||||||
|
Nessun modello di regime: e' il "cosa avrebbe funzionato finora".
|
||||||
|
Causale: il PnL della barra i si conosce solo a fine barra i, quindi per decidere a i si usa
|
||||||
|
la finestra che finisce a i-1; `eval_weights` shifta poi la posizione di un'altra barra."""
|
||||||
|
cands = [L for L in CONST_L if LMIN_D <= L <= LMAX_D]
|
||||||
|
key = (win_d, blend) + _stat_key(df, "wf", 0)[2:]
|
||||||
|
if key in _WF_CACHE:
|
||||||
|
return _WF_CACHE[key]
|
||||||
|
c = df["close"].values.astype(float)
|
||||||
|
bpd = A.bars_per_day(df)
|
||||||
|
n = len(c)
|
||||||
|
r = A.simple_returns(c)
|
||||||
|
w = max(20, win_d * bpd)
|
||||||
|
tgts, sh = [], []
|
||||||
|
for L in cands:
|
||||||
|
Lb = np.full(n, max(2, int(round(L * bpd))), int)
|
||||||
|
t_ = A.vol_target(np.clip(_dir_from_L(c, Lb, blend), 0, None), df, TGT_VOL, VOLWIN, LEV)
|
||||||
|
pos = np.zeros(n); pos[1:] = t_[:-1]
|
||||||
|
net = pos * r - A.FEE_SIDE * np.abs(np.diff(pos, prepend=0.0))
|
||||||
|
ss = pd.Series(net)
|
||||||
|
mu = ss.rolling(w, min_periods=w // 2).mean()
|
||||||
|
sd = ss.rolling(w, min_periods=w // 2).std()
|
||||||
|
with np.errstate(invalid="ignore", divide="ignore"):
|
||||||
|
sr = (mu / sd).values
|
||||||
|
sr = np.concatenate(([np.nan], sr[:-1])) # PnL noto solo a fine barra
|
||||||
|
tgts.append(t_); sh.append(np.where(np.isfinite(sr), sr, -np.inf))
|
||||||
|
SH = np.vstack(sh); TG = np.vstack(tgts)
|
||||||
|
idx = np.argmax(SH, axis=0)
|
||||||
|
idx[~np.isfinite(SH).any(axis=0)] = cands.index(90) # warmup -> il canonico, non il migliore
|
||||||
|
out = (np.asarray([cands[i] for i in idx]), TG[idx, np.arange(n)], cands)
|
||||||
|
_WF_CACHE[key] = out
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def wf_factory(tf: str, win_d: int, blend: str):
|
||||||
|
def fn(df: pd.DataFrame) -> np.ndarray:
|
||||||
|
return wf_picks(df, win_d, blend)[1]
|
||||||
|
fn.__name__ = f"wfpnl_{win_d}_{blend}"
|
||||||
|
return fn
|
||||||
|
|
||||||
|
|
||||||
|
GRID_A = [dict(det=d, par=p, stat=s, mapping=m, blend=b)
|
||||||
|
for d in DETECTORS for p in PARAMS[d] for s in STATS[d]
|
||||||
|
for m in MAPPINGS for b in BLENDS]
|
||||||
|
GRID_B = [dict(L=L, blend=b) for L in CONST_L for b in BLENDS]
|
||||||
|
GRID_C = [dict(win_d=w, blend=b) for w in WF_WIN_D for b in BLENDS]
|
||||||
|
|
||||||
|
|
||||||
|
# =======================================================================================
|
||||||
|
# UTILITA'
|
||||||
|
# =======================================================================================
|
||||||
|
def _dd(s: pd.Series) -> float:
|
||||||
|
eq = np.cumprod(1.0 + np.asarray(s.dropna().values, float))
|
||||||
|
pk = np.maximum.accumulate(eq)
|
||||||
|
return float(np.max((pk - eq) / pk)) if len(eq) else 0.0
|
||||||
|
|
||||||
|
|
||||||
|
def _cagr(s: pd.Series) -> float:
|
||||||
|
s = s.dropna()
|
||||||
|
if len(s) < 5:
|
||||||
|
return float("nan")
|
||||||
|
eq = float(np.prod(1.0 + s.values))
|
||||||
|
yrs = (s.index[-1] - s.index[0]).total_seconds() / (86400 * 365.25)
|
||||||
|
return eq ** (1 / max(yrs, 1e-6)) - 1.0 if eq > 0 else -1.0
|
||||||
|
|
||||||
|
|
||||||
|
def resample_offset(df_1h: pd.DataFrame, hours: int, off: int) -> pd.DataFrame:
|
||||||
|
"""Stessa convenzione di trend_portfolio.resample_tf, ancora spostata di `off` ore.
|
||||||
|
NB `offset=`, non `origin=` (pandas 2.x ignora origin in silenzio — lezione 02/07)."""
|
||||||
|
g = df_1h.copy()
|
||||||
|
idx = pd.to_datetime(g["timestamp"], unit="ms", utc=True)
|
||||||
|
idx.name = "dt"
|
||||||
|
g.index = idx
|
||||||
|
out = g.resample(f"{hours}h", offset=pd.Timedelta(hours=off), label="left", closed="left").agg(
|
||||||
|
{"open": "first", "high": "max", "low": "min", "close": "last", "volume": "sum"})
|
||||||
|
out = out.dropna(subset=["open"])
|
||||||
|
out["datetime"] = out.index
|
||||||
|
epoch = pd.Timestamp("1970-01-01", tz="UTC")
|
||||||
|
out["timestamp"] = ((out.index - epoch) // pd.Timedelta(milliseconds=1)).astype("int64")
|
||||||
|
return out.reset_index(drop=True)[
|
||||||
|
["timestamp", "open", "high", "low", "close", "volume", "datetime"]]
|
||||||
|
|
||||||
|
|
||||||
|
def paired_by_year(cand: pd.Series, base: pd.Series) -> list[dict]:
|
||||||
|
J = pd.concat({"C": cand, "B": base}, axis=1, join="inner").dropna()
|
||||||
|
rows = []
|
||||||
|
for y, g in J.groupby(J.index.year):
|
||||||
|
if len(g) < 40:
|
||||||
|
continue
|
||||||
|
rows.append(dict(year=int(y), n=len(g),
|
||||||
|
sh_cand=round(A._sh(g["C"]), 2), sh_base=round(A._sh(g["B"]), 2),
|
||||||
|
d_sh=round(A._sh(g["C"]) - A._sh(g["B"]), 2)))
|
||||||
|
return rows
|
||||||
|
|
||||||
|
|
||||||
|
def delever_null(cand: pd.Series, base: pd.Series) -> dict:
|
||||||
|
"""Esiste k<1 che da' al BASELINE lo stesso maxDD del candidato con Sharpe >= ?
|
||||||
|
Se si', il candidato non ha comprato protezione: ha solo tolto leva."""
|
||||||
|
J = pd.concat({"C": cand, "B": base}, axis=1, join="inner").dropna()
|
||||||
|
dd_c, dd_b = _dd(J["C"]), _dd(J["B"])
|
||||||
|
if dd_c >= dd_b or dd_b <= 0:
|
||||||
|
return dict(applicabile=False, dd_cand=round(dd_c, 4), dd_base=round(dd_b, 4),
|
||||||
|
nota="il candidato NON riduce il maxDD: il null non si applica")
|
||||||
|
lo, hi = 0.0, 1.0
|
||||||
|
for _ in range(60):
|
||||||
|
k = 0.5 * (lo + hi)
|
||||||
|
if _dd(k * J["B"]) < dd_c:
|
||||||
|
lo = k
|
||||||
|
else:
|
||||||
|
hi = k
|
||||||
|
k = 0.5 * (lo + hi)
|
||||||
|
return dict(applicabile=True, k=round(k, 4), dd_cand=round(dd_c, 4), dd_base=round(dd_b, 4),
|
||||||
|
sharpe_cand=round(A._sh(J["C"]), 3),
|
||||||
|
sharpe_base_delev=round(A._sh(k * J["B"]), 3),
|
||||||
|
superato=bool(A._sh(J["C"]) > A._sh(k * J["B"])))
|
||||||
|
|
||||||
|
|
||||||
|
_ANCHOR_CACHE: dict = {}
|
||||||
|
|
||||||
|
|
||||||
|
def series_at_anchor(fn, name: str, hours: int, off: int) -> pd.Series:
|
||||||
|
key = (name, hours, off)
|
||||||
|
if key not in _ANCHOR_CACHE:
|
||||||
|
ser = {}
|
||||||
|
for a in ASSETS:
|
||||||
|
dfo = resample_offset(A.get(a, "1h"), hours, off)
|
||||||
|
ev = A.eval_weights(dfo, A._call_target(fn, dfo, a))
|
||||||
|
ser[a] = pd.Series(ev["net"], index=ev["idx"])
|
||||||
|
J = pd.concat(ser, axis=1, join="inner").fillna(0.0)
|
||||||
|
_ANCHOR_CACHE[key] = A._to_daily(0.5 * J[ASSETS[0]] + 0.5 * J[ASSETS[1]])
|
||||||
|
return _ANCHOR_CACHE[key]
|
||||||
|
|
||||||
|
|
||||||
|
def adaptivity(tf: str, det: str, par, stat: str, mapping: str) -> dict:
|
||||||
|
"""LA DIAGNOSTICA CHE HA SMASCHERATO IL PRIMO TENTATIVO — quanto si MUOVE davvero L_t.
|
||||||
|
Criterio (identico a quello del 22/08, per confrontabilita'): sta a un bordo <50% del tempo
|
||||||
|
E l'IQR e' >= 10 giorni. In piu': |dL|/giorno e numero di RESET (cadute di L > 5 giorni),
|
||||||
|
che sono la firma di un vero rilevatore di change-point."""
|
||||||
|
fl, fh, iq, md, dl, nrs, sat = [], [], [], [], [], [], []
|
||||||
|
for a in ASSETS:
|
||||||
|
df = A.get(a, tf)
|
||||||
|
bpd = A.bars_per_day(df)
|
||||||
|
L = lookback_bars(df, det, par, stat, mapping) / bpd
|
||||||
|
raw = np.nan_to_num(detector_stats(df, det, par)[stat], nan=0.0) / bpd
|
||||||
|
fl.append(float(np.mean(L <= LMIN_D + 1)))
|
||||||
|
fh.append(float(np.mean(L >= LMAX_D - 1)))
|
||||||
|
iq.append(float(np.percentile(L, 75) - np.percentile(L, 25)))
|
||||||
|
md.append(float(np.median(L)))
|
||||||
|
dl.append(float(np.median(np.abs(np.diff(L)))))
|
||||||
|
d = np.diff(L)
|
||||||
|
nrs.append(float(np.sum(d < -5.0) / (len(L) / (365.25)))) # reset all'anno
|
||||||
|
sat.append(float(np.mean(raw >= LMAX_D - 1))) # saturazione del GREZZO
|
||||||
|
return dict(frac_lo=float(np.mean(fl)), frac_hi=float(np.mean(fh)),
|
||||||
|
iqr_days=float(np.mean(iq)), med_days=float(np.mean(md)),
|
||||||
|
dL_med=float(np.mean(dl)), reset_yr=float(np.mean(nrs)),
|
||||||
|
sat_raw=float(np.mean(sat)),
|
||||||
|
adattiva=bool(np.mean(fl) < 0.5 and np.mean(fh) < 0.5 and np.mean(iq) >= 10.0))
|
||||||
|
|
||||||
|
|
||||||
|
def hr(t: str = "") -> None:
|
||||||
|
print("\n" + "=" * 86)
|
||||||
|
if t:
|
||||||
|
print(t)
|
||||||
|
print("=" * 86)
|
||||||
|
|
||||||
|
|
||||||
|
def dsr_given_sr0(daily: pd.Series, sr_ann: float, sr0_ann: float,
|
||||||
|
dpy: float = 365.25) -> float:
|
||||||
|
"""Deflated-Sharpe con il massimo-atteso-dal-rumore IMPOSTO dall'esterno.
|
||||||
|
Serve perche' `A.deflated_sharpe` stima sr0 dalla VARIANZA degli Sharpe della griglia: su
|
||||||
|
una famiglia OMOGENEA (qui sono tutte varianti di TSMOM, quindi molto correlate) quella
|
||||||
|
varianza e' piccola, sr0 esce basso e il DSR esce alto **per costruzione**. Il conteggio che
|
||||||
|
decide, sulla ricerca direzionale BTC/ETH, e' quello di SCREEN: il filone ORTHO ha misurato
|
||||||
|
che su 168 trial il massimo atteso dal puro rumore e' Sharpe 1.572."""
|
||||||
|
r = np.asarray(pd.Series(daily).dropna().values, float)
|
||||||
|
T = len(r)
|
||||||
|
sr, sr0 = sr_ann / np.sqrt(dpy), sr0_ann / np.sqrt(dpy)
|
||||||
|
sk = float(pd.Series(r).skew())
|
||||||
|
ku = float(pd.Series(r).kurt()) + 3.0
|
||||||
|
den = np.sqrt(max(1e-9, 1 - sk * sr + (ku - 1) / 4.0 * sr ** 2))
|
||||||
|
return float(norm.cdf((sr - sr0) * np.sqrt(T - 1) / den))
|
||||||
|
|
||||||
|
|
||||||
|
def dsr_line(sr: float, allsr: list, daily: pd.Series, label: str, n_decl: int) -> str:
|
||||||
|
pad = list(allsr) + [float(np.mean(allsr))] * max(0, n_decl - len(allsr))
|
||||||
|
d, s0 = A.deflated_sharpe(sr, pad, daily)
|
||||||
|
return (f" N={n_decl:>4} ({label:<24}) DSR={d:.3f} {'PASS' if d >= 0.95 else 'FAIL'}"
|
||||||
|
f" max atteso dal rumore {s0:+.3f}")
|
||||||
|
|
||||||
|
|
||||||
|
# =======================================================================================
|
||||||
|
def main() -> None:
|
||||||
|
t0 = time.time()
|
||||||
|
print(__doc__.split("Runtime atteso")[0])
|
||||||
|
|
||||||
|
# -- 0. SANITY -----------------------------------------------------------------------
|
||||||
|
hr("0. SANITY — il baseline 'fisso' e' TP01 canonico (se non lo e', ogni delta e' inventato)")
|
||||||
|
tp = TrendPortfolio(**CANONICAL)
|
||||||
|
worst = 0.0
|
||||||
|
for a in ASSETS:
|
||||||
|
df = A.get(a, "1d")
|
||||||
|
worst = max(worst, float(np.max(np.abs(fixed_target(df) - tp.target_series(df)))))
|
||||||
|
base_1d = A.candidate_daily(fixed_target, tf="1d")
|
||||||
|
tp01 = A.tp01_baseline_daily()
|
||||||
|
JJ = pd.concat({"a": base_1d, "b": tp01}, axis=1, join="inner").dropna()
|
||||||
|
print(f" max|target_mio - TrendPortfolio.target_series| (1d, BTC+ETH) = {worst:.3e}")
|
||||||
|
print(f" max|serie_giornaliera_mia - A.tp01_baseline_daily()| = "
|
||||||
|
f"{float(np.max(np.abs(JJ['a'] - JJ['b']))):.3e} (n={len(JJ)})")
|
||||||
|
assert worst < 1e-12, "il baseline NON riproduce TP01: fermarsi qui"
|
||||||
|
|
||||||
|
# -- 0-bis. IL RILEVATORE E' CAUSALE? (troncamento MANUALE, prima dei gate) -----------
|
||||||
|
hr("0-bis. TRONCAMENTO MANUALE — la trappola del change-point RETROSPETTIVO")
|
||||||
|
print(" Un `Pelt().fit_predict(serie_intera)` restituisce la segmentazione dell'INTERO")
|
||||||
|
print(" campione: al tempo t 'saprebbe' dei cambi futuri. Qui BOCPD e OP sono ricorsioni in")
|
||||||
|
print(" avanti. Prova diretta: ricalcolo su un PREFISSO e confronto la coda con il pieno.")
|
||||||
|
for det, par in (("bocpd", 100), ("op", 5.0)):
|
||||||
|
for stat in STATS[det]:
|
||||||
|
wmax = 0.0
|
||||||
|
for a in ASSETS:
|
||||||
|
df = A.get(a, "1d")
|
||||||
|
full = lookback_bars(df, det, par, stat, "direct")
|
||||||
|
for frac in (0.70, 0.85):
|
||||||
|
cut = int(len(df) * frac)
|
||||||
|
sub = df.iloc[:cut].reset_index(drop=True)
|
||||||
|
pre = lookback_bars(sub, det, par, stat, "direct")
|
||||||
|
wmax = max(wmax, float(np.max(np.abs(pre[-100:] - full[cut - 100:cut]))))
|
||||||
|
print(f" {det:<6} {str(par):>5} {stat:<7}: max|L(prefisso)-L(pieno)| sulla coda "
|
||||||
|
f"= {wmax:.3e} barre -> {'CAUSALE' if wmax < 1e-9 else 'SOSPETTO'}")
|
||||||
|
|
||||||
|
# -- 1-pre. TARATURA DELLA SCALA — guardando il RILEVATORE, mai lo Sharpe -----------
|
||||||
|
hr("1-pre. TARATURA DELLA SCALA DEI PARAMETRI (guardando SOLO quanti change-point rileva)")
|
||||||
|
print(" Un rilevatore che non rileva nulla non e' un rilevatore. Questa scansione guarda il")
|
||||||
|
print(" NUMERO di change-point e la distribuzione di L_t — mai un rendimento. Dichiarata")
|
||||||
|
print(" come 13 trial in piu' (al rialzo), benche' nessuno produca uno Sharpe.")
|
||||||
|
dfc = A.get("BTC", "1d")
|
||||||
|
yc = _std_returns(dfc["close"].values.astype(float), 1)
|
||||||
|
print(f"\n OP (BTC 1d) {'pen_k':>6} {'cp/anno':>8} {'age med':>8} {'IQR':>7} {'@tetto':>8}")
|
||||||
|
for k in CAL_PEN:
|
||||||
|
ag, _ = _op_causal(yc, k, LMAX_D)
|
||||||
|
ncp = int(np.sum(np.diff(ag) < 0))
|
||||||
|
flag = " <= USATO" if k in PARAMS["op"] else (" <= degenere (artefatto 22/08)"
|
||||||
|
if np.mean(ag >= LMAX_D - 1) > 0.30 else "")
|
||||||
|
print(f" {k:>6} {ncp/(len(yc)/365.25):>8.1f} {np.median(ag):>8.1f} "
|
||||||
|
f"{np.percentile(ag,75)-np.percentile(ag,25):>7.1f} "
|
||||||
|
f"{np.mean(ag>=LMAX_D-1)*100:>7.1f}%{flag}")
|
||||||
|
print(f"\n BOCPD (BTC 1d) {'lambda':>6} {'E[r] med':>9} {'IQR':>7} {'@tetto':>8} "
|
||||||
|
f"{'MAP med':>8} {'@tetto':>8}")
|
||||||
|
for lam in CAL_LAM:
|
||||||
|
mn, mp = _bocpd(yc, float(lam), LMAX_D)
|
||||||
|
flag = " <= USATO" if lam in PARAMS["bocpd"] else ""
|
||||||
|
print(f" {lam:>6} {np.median(mn):>9.1f} "
|
||||||
|
f"{np.percentile(mn,75)-np.percentile(mn,25):>7.1f} "
|
||||||
|
f"{np.mean(mn>=LMAX_D-1)*100:>7.1f}% {np.median(mp):>8.1f} "
|
||||||
|
f"{np.mean(mp>=LMAX_D-1)*100:>7.1f}%{flag}")
|
||||||
|
|
||||||
|
# -- 1. LA DIAGNOSTICA DI MECCANISMO, PER PRIMA --------------------------------------
|
||||||
|
hr("1. L_t SI MUOVE DAVVERO? (se sta al bordo l'artefatto e' riprodotto: si chiude e basta)")
|
||||||
|
print(" criterio (identico al 22/08, per confrontabilita'): a un bordo <50% del tempo E")
|
||||||
|
print(" IQR >= 10 giorni. `reset/anno` = cadute di L > 5 giorni = firma di un change-point.")
|
||||||
|
print(f"\n {'TF':>4} {'det':>6} {'par':>5} {'stat':>7} {'map':<7} {'bordo-lo':>9} "
|
||||||
|
f"{'bordo-hi':>9} {'IQR(g)':>7} {'med(g)':>7} {'|dL|(g)':>8} {'reset/a':>8} "
|
||||||
|
f"{'sat.grezzo':>10} esito")
|
||||||
|
diag: dict = {}
|
||||||
|
for tf in TFS:
|
||||||
|
for d in DETECTORS:
|
||||||
|
for p in PARAMS[d]:
|
||||||
|
for st in STATS[d]:
|
||||||
|
for mp in MAPPINGS:
|
||||||
|
v = adaptivity(tf, d, p, st, mp)
|
||||||
|
diag[(tf, d, p, st, mp)] = v
|
||||||
|
print(f" {tf:>4} {d:>6} {str(p):>5} {st:>7} {mp:<7} "
|
||||||
|
f"{v['frac_lo']*100:8.1f}% {v['frac_hi']*100:8.1f}% "
|
||||||
|
f"{v['iqr_days']:7.1f} {v['med_days']:7.1f} {v['dL_med']:8.2f} "
|
||||||
|
f"{v['reset_yr']:8.1f} {v['sat_raw']*100:9.1f}% "
|
||||||
|
f"{'ADATTIVA' if v['adattiva'] else 'costante-travestito'}")
|
||||||
|
n_ad = sum(1 for v in diag.values() if v["adattiva"])
|
||||||
|
print(f"\n configurazioni di rilevatore DAVVERO adattive: {n_ad}/{len(diag)}")
|
||||||
|
if n_ad == 0:
|
||||||
|
hr("STOP — ARTEFATTO RIPRODOTTO")
|
||||||
|
print(" Nessun rilevatore muove L_t: ogni 'vincitore' sarebbe un orizzonte COSTANTE")
|
||||||
|
print(" travestito, esattamente come il 22/08. Il filone si chiude per la seconda volta")
|
||||||
|
print(" senza bisogno di guardare i rendimenti. FINE.")
|
||||||
|
return
|
||||||
|
|
||||||
|
# -- 2. GRIGLIA (A): cella scelta IN-SAMPLE + deflated-Sharpe -------------------------
|
||||||
|
hr(f"2. study_family_honest (A) — {len(GRID_A)} celle x {len(TFS)} TF = "
|
||||||
|
f"{len(GRID_A)*len(TFS)} trial")
|
||||||
|
rep = A.study_family_honest("BOCPD-H", factory, GRID_A, TFS)
|
||||||
|
ch = rep["chosen"]
|
||||||
|
print(f" celle valutate : {rep['n_cells']}")
|
||||||
|
print(f" cella scelta IN-SAMPLE : tf={ch['tf']} {ch['params']}")
|
||||||
|
print(f" Sharpe IS {ch['insample_sharpe']} FULL {ch['full_sharpe']}")
|
||||||
|
print(f" deflated-Sharpe (N={rep['n_cells']}): {rep['deflated_sharpe']} "
|
||||||
|
f"(max atteso sotto il null {rep['expected_null_max']}) PASS={rep['dsr_pass']}")
|
||||||
|
print(f" earns_slot_marginal : {rep['earns_slot_marginal']}")
|
||||||
|
print(f" EARNS_SLOT_HONEST : {rep['earns_slot_honest']}")
|
||||||
|
print("\n top-10 IN-SAMPLE (come si sceglie onestamente):")
|
||||||
|
for r in rep["rows"][:10]:
|
||||||
|
ad = diag[(r["tf"], r["params"]["det"], r["params"]["par"],
|
||||||
|
r["params"]["stat"], r["params"]["mapping"])]["adattiva"]
|
||||||
|
print(f" IS {r['insample_sharpe']:+.3f} FULL {r['full_sharpe']:+.3f} {r['tf']:>3} "
|
||||||
|
f"{r['params']} {'[adattiva]' if ad else '[costante-travestito]'}")
|
||||||
|
|
||||||
|
fn_ch = factory(tf=ch["tf"], **ch["params"])
|
||||||
|
d_ch = A.candidate_daily(fn_ch, tf=ch["tf"])
|
||||||
|
|
||||||
|
# -- 3. CONTROLLO (B): il MIGLIOR ORIZZONTE COSTANTE ---------------------------------
|
||||||
|
hr(f"3. CONTROLLO L COSTANTE (B) — {len(GRID_B)} celle x {len(TFS)} TF = "
|
||||||
|
f"{len(GRID_B)*len(TFS)} trial [IL NULL GIUSTO, non TP01]")
|
||||||
|
csel = A.select_cell_insample(const_factory, GRID_B, TFS)
|
||||||
|
cch = csel["chosen"]
|
||||||
|
print(f" miglior COSTANTE scelto in-sample: tf={cch['tf']} {cch['params']} "
|
||||||
|
f"IS {cch['insample_sharpe']} FULL {cch['full_sharpe']}")
|
||||||
|
print(" top-8 costanti in-sample (L=90 triple 1d == TP01 canonico):")
|
||||||
|
for r in csel["rows"][:8]:
|
||||||
|
tag = " <== TP01" if (r["params"]["L"] == 90 and r["params"]["blend"] == "triple"
|
||||||
|
and r["tf"] == "1d") else ""
|
||||||
|
print(f" IS {r['insample_sharpe']:+.3f} FULL {r['full_sharpe']:+.3f} {r['tf']:>3} "
|
||||||
|
f"L={r['params']['L']:>3}g {r['params']['blend']}{tag}")
|
||||||
|
fn_const = const_factory(tf=cch["tf"], **cch["params"])
|
||||||
|
d_const = A.candidate_daily(fn_const, tf=cch["tf"])
|
||||||
|
|
||||||
|
# -- 4. WF-PnL (C) --------------------------------------------------------------------
|
||||||
|
hr(f"4. WALK-FORWARD SUL PnL REALIZZATO (C) — {len(GRID_C)} celle x {len(TFS)} TF = "
|
||||||
|
f"{len(GRID_C)*len(TFS)} trial")
|
||||||
|
wsel = A.select_cell_insample(wf_factory, GRID_C, TFS)
|
||||||
|
wch = wsel["chosen"]
|
||||||
|
print(f" miglior WF scelto in-sample: tf={wch['tf']} {wch['params']} "
|
||||||
|
f"IS {wch['insample_sharpe']} FULL {wch['full_sharpe']}")
|
||||||
|
for r in wsel["rows"]:
|
||||||
|
print(f" IS {r['insample_sharpe']:+.3f} FULL {r['full_sharpe']:+.3f} {r['tf']:>3} "
|
||||||
|
f"{r['params']}")
|
||||||
|
fn_wf = wf_factory(tf=wch["tf"], **wch["params"])
|
||||||
|
d_wf = A.candidate_daily(fn_wf, tf=wch["tf"])
|
||||||
|
# quanto si muove L nella variante WF? (stesso metro della sezione 1)
|
||||||
|
for a in ASSETS:
|
||||||
|
df = A.get(a, wch["tf"]); bpd = A.bars_per_day(df)
|
||||||
|
pick, _, cands = wf_picks(df, **wch["params"])
|
||||||
|
print(f" {a}: L_WF giorni mediana {np.median(pick):.0f} IQR "
|
||||||
|
f"{np.percentile(pick,75)-np.percentile(pick,25):.0f} "
|
||||||
|
f"bordo basso {np.mean(pick<=min(cands))*100:.1f}% "
|
||||||
|
f"bordo alto {np.mean(pick>=max(cands))*100:.1f}% "
|
||||||
|
f"cambi/anno {np.sum(np.diff(pick)!=0)/(len(pick)/(bpd*365.25)):.0f}")
|
||||||
|
|
||||||
|
# -- 5. IL CONFRONTO CHE DECIDE ------------------------------------------------------
|
||||||
|
hr("5. ADATTIVO vs MIGLIOR COSTANTE (entrambi scelti IN-SAMPLE) — e vs TP01")
|
||||||
|
base_ch = A.candidate_daily(fixed_target, tf=ch["tf"])
|
||||||
|
for nome, ser in (("BOCPD/OP (A)", d_ch), ("WF-PnL (C)", d_wf),
|
||||||
|
("miglior COSTANTE (B)", d_const), ("TP01 30/90/180", base_1d)):
|
||||||
|
h = ser[ser.index >= HOLDOUT]
|
||||||
|
print(f" {nome:<22} FULL Sh {A._sh(ser):+.3f} HOLD Sh {A._sh(h):+.3f} "
|
||||||
|
f"maxDD {_dd(ser)*100:5.2f}% CAGR {_cagr(ser)*100:6.2f}% "
|
||||||
|
f"corr->TP01 {pd.concat({'a':ser,'b':tp01},axis=1,join='inner').dropna().corr().iloc[0,1]:+.3f}")
|
||||||
|
|
||||||
|
for lab, cand, ref, refname in (("A vs B", d_ch, d_const, "miglior costante"),
|
||||||
|
("C vs B", d_wf, d_const, "miglior costante"),
|
||||||
|
("A vs TP01", d_ch, base_1d, "TP01")):
|
||||||
|
J = pd.concat({"C": cand, "B": ref}, axis=1, join="inner").dropna()
|
||||||
|
JH = J[J.index >= HOLDOUT]
|
||||||
|
rows = paired_by_year(cand, ref)
|
||||||
|
ds = [r["d_sh"] for r in rows]
|
||||||
|
print(f"\n --- {lab} ({refname}) --- corr {J['C'].corr(J['B']):+.3f}")
|
||||||
|
print(f" dSh FULL {A._sh(J['C'])-A._sh(J['B']):+.3f} "
|
||||||
|
f"dSh HOLD {A._sh(JH['C'])-A._sh(JH['B']):+.3f}")
|
||||||
|
print(" differenza APPAIATA per anno:")
|
||||||
|
print(" " + " ".join(f"{r['year']}:{r['d_sh']:+.2f}" for r in rows))
|
||||||
|
print(f" -> anni con dSh>0: {sum(1 for x in ds if x > 0)}/{len(ds)} "
|
||||||
|
f"mediana {np.median(ds):+.2f} media {np.mean(ds):+.2f} "
|
||||||
|
f"[criterio del falsificatore: >=6/8 anni]")
|
||||||
|
|
||||||
|
# decomposizione: quanto del vantaggio su TP01 e' ADATTIVITA' e quanto e' "L piu' corto"?
|
||||||
|
JT = pd.concat({"C": d_ch, "K": d_const, "B": base_1d}, axis=1, join="inner").dropna()
|
||||||
|
v_tp = A._sh(JT["C"]) - A._sh(JT["B"])
|
||||||
|
v_ct = A._sh(JT["K"]) - A._sh(JT["B"])
|
||||||
|
print(f"\n DECOMPOSIZIONE del vantaggio dell'adattivo su TP01 (FULL, finestra comune):")
|
||||||
|
print(f" adattivo - TP01 = {v_tp:+.3f}")
|
||||||
|
print(f" miglior COSTANTE - TP01 = {v_ct:+.3f} <- quota spiegata da 'un L piu' corto'")
|
||||||
|
print(f" adattivo - miglior COSTANTE= {v_tp - v_ct:+.3f} <- quota spiegata dall'ADATTARE")
|
||||||
|
print(f" => l'adattivita' spiega il {100*(v_tp-v_ct)/v_tp if abs(v_tp)>1e-9 else float('nan'):+.0f}% "
|
||||||
|
f"del vantaggio su TP01")
|
||||||
|
|
||||||
|
# la superficie di L costante e' una CURVA o un CRINALE DI RUMORE? (replica del 22/08)
|
||||||
|
rows12 = {r["params"]["L"]: r["insample_sharpe"] for r in csel["rows"]
|
||||||
|
if r["tf"] == cch["tf"] and r["params"]["blend"] == cch["params"]["blend"]}
|
||||||
|
Ls = sorted(rows12)
|
||||||
|
adj = float(np.median(np.abs(np.diff([rows12[L] for L in Ls]))))
|
||||||
|
print(f"\n RISOLUZIONE della griglia costante ({cch['tf']}, {cch['params']['blend']}): salto IS "
|
||||||
|
f"mediano fra L ADIACENTI {adj:+.3f}")
|
||||||
|
print(f" contro il vantaggio del miglior costante su TP01 ({v_ct:+.3f}) -> rapporto "
|
||||||
|
f"{adj/max(1e-9, abs(v_ct)):.2f} [replica del 'crinale di rumore' del 22/08]")
|
||||||
|
|
||||||
|
# -- 6. GATE --------------------------------------------------------------------------
|
||||||
|
hr("6. GATE sul candidato (A) scelto in-sample")
|
||||||
|
print(A.fmt_marginal(rep["marginal"]))
|
||||||
|
cz = A.causality_ok(fn_ch, tf=ch["tf"])
|
||||||
|
print(f"\n causality_ok : ok={cz['ok']} max_tail_diff={cz['max_tail_diff']} "
|
||||||
|
f"(prefissi controllati {cz['checked']})")
|
||||||
|
imp = A.implausible_sharpe(d_ch)
|
||||||
|
print(f" implausible_sharpe : implausible={imp['implausible']} Sh {imp.get('sharpe')} "
|
||||||
|
f"maxDD {imp.get('maxdd')} perdite/attive {imp.get('loss_frac')} "
|
||||||
|
f"attive {imp.get('active_frac')}")
|
||||||
|
for rr in imp["reasons"]:
|
||||||
|
print(f" - {rr}")
|
||||||
|
print(f" null de-levering vs COSTANTE : {delever_null(d_ch, d_const)}")
|
||||||
|
print(f" null de-levering vs TP01 : {delever_null(d_ch, base_1d)}")
|
||||||
|
print(" haircut a $600 (min-order $5):")
|
||||||
|
for a in ASSETS:
|
||||||
|
df = A.get(a, ch["tf"])
|
||||||
|
sc = A.eval_weights_smallcap(df, A._call_target(fn_ch, df, a), capital=600.0)
|
||||||
|
print(f" {a}: modellato {sc['modeled']['sharpe']:+.3f} -> reale "
|
||||||
|
f"{sc['realistic']['sharpe']:+.3f} (haircut {sc['sharpe_haircut']:+.3f}, "
|
||||||
|
f"{sc['n_executed_trades']} trade)")
|
||||||
|
|
||||||
|
# DSR a piu' conteggi: famiglia, famiglia+112 del primo tentativo, di SCREEN
|
||||||
|
all_sr = ([r["full_sharpe"] for r in rep["rows"]]
|
||||||
|
+ [r["full_sharpe"] for r in csel["rows"]]
|
||||||
|
+ [r["full_sharpe"] for r in wsel["rows"]])
|
||||||
|
hr("6-bis. DEFLATED-SHARPE — sensibilita' del verdetto al CONTEGGIO (regola 30/07 + 22/08)")
|
||||||
|
print(f" Sharpe FULL del candidato (A) = {A._sh(d_ch):+.3f}")
|
||||||
|
print(dsr_line(A._sh(d_ch), all_sr, d_ch, "mia famiglia A+B+C", N_DECLARED))
|
||||||
|
print(dsr_line(A._sh(d_ch), all_sr, d_ch, "+ le 112 del 22/08", N_DECLARED + N_PRIOR_FAMILY))
|
||||||
|
print(dsr_line(A._sh(d_ch), all_sr, d_ch, "di SCREEN (ondata)",
|
||||||
|
N_DECLARED + N_PRIOR_FAMILY + N_SCREEN))
|
||||||
|
print("\n ⚠️ QUEL 'PASS' NON CERTIFICA NIENTE, e va detto: `deflated_sharpe` stima il massimo")
|
||||||
|
print(" atteso dal rumore dalla VARIANZA degli Sharpe della griglia. Qui la griglia e'")
|
||||||
|
print(" OMOGENEA (tutte varianti di TSMOM, molto correlate fra loro) -> varianza piccola")
|
||||||
|
print(" -> sr0 basso -> DSR alto per COSTRUZIONE. Aumentare N padding con la media alza N")
|
||||||
|
print(" ma non la varianza. Il conteggio che decide e' quello di SCREEN, e il filone ORTHO")
|
||||||
|
print(" lo ha gia' misurato: su 168 trial di ricerca direzionale BTC/ETH il massimo atteso")
|
||||||
|
print(" dal puro rumore e' Sharpe 1.572. Riscritto con QUEL sr0:")
|
||||||
|
for nome, ser in (("adattivo (A)", d_ch), ("miglior costante (B)", d_const),
|
||||||
|
("TP01 30/90/180", base_1d)):
|
||||||
|
print(f" {nome:<22} Sharpe FULL {A._sh(ser):+.3f} vs sr0_screen 1.572 -> "
|
||||||
|
f"DSR {dsr_given_sr0(ser, A._sh(ser), 1.572):.3f} "
|
||||||
|
f"{'PASS' if dsr_given_sr0(ser, A._sh(ser), 1.572) >= 0.95 else 'FAIL'}")
|
||||||
|
|
||||||
|
# PROVA DI IDENTITA': la cella "adattiva" al 100% sul bordo E' il costante, bit per bit
|
||||||
|
hr("6-ter. PROVA DI IDENTITA' — la cella al bordo NON somiglia a un costante: LO E'")
|
||||||
|
deg = [r for r in rep["rows"]
|
||||||
|
if diag[(r["tf"], r["params"]["det"], r["params"]["par"], r["params"]["stat"],
|
||||||
|
r["params"]["mapping"])]["frac_lo"] >= 0.999]
|
||||||
|
if deg:
|
||||||
|
dr = max(deg, key=lambda r: r["insample_sharpe"])
|
||||||
|
s_deg = A.candidate_daily(factory(tf=dr["tf"], **dr["params"]), tf=dr["tf"])
|
||||||
|
s_con = A.candidate_daily(const_factory(tf=dr["tf"], L=LMIN_D,
|
||||||
|
blend=dr["params"]["blend"]), tf=dr["tf"])
|
||||||
|
Jd = pd.concat({"a": s_deg, "b": s_con}, axis=1, join="inner").dropna()
|
||||||
|
print(f" cella 100% al bordo basso: {dr['tf']} {dr['params']} IS {dr['insample_sharpe']}"
|
||||||
|
f" FULL {dr['full_sharpe']}")
|
||||||
|
print(f" costante L={LMIN_D}g {dr['params']['blend']} {dr['tf']}: "
|
||||||
|
f"FULL {A._sh(s_con):+.3f}")
|
||||||
|
print(f" max|serie_adattiva - serie_costante| = {float(np.max(np.abs(Jd['a']-Jd['b']))):.3e}"
|
||||||
|
f" (n={len(Jd)}) -> {'SONO LA STESSA STRATEGIA' if float(np.max(np.abs(Jd['a']-Jd['b']))) < 1e-12 else 'diverse'}")
|
||||||
|
else:
|
||||||
|
print(" nessuna cella e' al 100% sul bordo basso (la prova non si applica)")
|
||||||
|
|
||||||
|
# -- 7. FAMIGLIA RISTRETTA ALLE SOLE CELLE DAVVERO ADATTIVE --------------------------
|
||||||
|
hr("7. GATE SULLA SOLA FAMIGLIA DAVVERO ADATTIVA (la domanda della missione, isolata)")
|
||||||
|
ad_rows = [r for r in rep["rows"]
|
||||||
|
if diag[(r["tf"], r["params"]["det"], r["params"]["par"],
|
||||||
|
r["params"]["stat"], r["params"]["mapping"])]["adattiva"]]
|
||||||
|
d_ad_keep = fn_ad_keep = best_keep = None
|
||||||
|
if not ad_rows:
|
||||||
|
print(" NESSUNA cella della famiglia e' davvero adattiva -> vedi sezione 1.")
|
||||||
|
else:
|
||||||
|
best = max(ad_rows, key=lambda r: r["insample_sharpe"])
|
||||||
|
print(f" celle adattive: {len(ad_rows)}/{len(rep['rows'])} miglior IN-SAMPLE: "
|
||||||
|
f"tf={best['tf']} {best['params']} IS {best['insample_sharpe']} "
|
||||||
|
f"FULL {best['full_sharpe']}")
|
||||||
|
fn_ad = factory(tf=best["tf"], **best["params"])
|
||||||
|
d_ad = A.candidate_daily(fn_ad, tf=best["tf"])
|
||||||
|
sm = A.study_marginal("BOCPD-H (sole adattive)", fn_ad, tf=best["tf"])
|
||||||
|
dsr_ad, sr0_ad = A.deflated_sharpe(A._sh(d_ad),
|
||||||
|
[r["full_sharpe"] for r in ad_rows], d_ad)
|
||||||
|
print(f" marginal={sm['marginal_verdict']} earns_slot={sm['earns_slot']} "
|
||||||
|
f"corr->TP01 {sm['marginal'].get('corr_full')}")
|
||||||
|
print(f" deflated-Sharpe (N={len(ad_rows)}) = {dsr_ad:.3f} "
|
||||||
|
f"{'PASS' if dsr_ad >= 0.95 else 'FAIL'} (null max atteso {sr0_ad:+.3f})")
|
||||||
|
JA = pd.concat({"C": d_ad, "K": d_const, "B": base_1d}, axis=1, join="inner").dropna()
|
||||||
|
JH = JA[JA.index >= HOLDOUT]
|
||||||
|
print(f" FULL adattiva-vera {A._sh(JA['C']):+.3f} | costante {A._sh(JA['K']):+.3f} "
|
||||||
|
f"| TP01 {A._sh(JA['B']):+.3f}")
|
||||||
|
print(f" HOLD-OUT adattiva-vera {A._sh(JH['C']):+.3f} | costante {A._sh(JH['K']):+.3f} "
|
||||||
|
f"| TP01 {A._sh(JH['B']):+.3f}")
|
||||||
|
r3 = paired_by_year(d_ad, d_const)
|
||||||
|
d3 = [r["d_sh"] for r in r3]
|
||||||
|
print(" vs miglior costante, differenza APPAIATA per anno:")
|
||||||
|
print(" " + " ".join(f"{r['year']}:{r['d_sh']:+.2f}" for r in r3))
|
||||||
|
print(f" -> dSh>0 in {sum(1 for x in d3 if x>0)}/{len(d3)} mediana {np.median(d3):+.2f}"
|
||||||
|
f" corr al costante {JA['C'].corr(JA['K']):+.3f}")
|
||||||
|
print(" ⚠️ l'hold-out sono gli ULTIMI DUE anni di questa stessa riga: se il vantaggio")
|
||||||
|
print(" hold-out sta li' e non nel resto, e' un evento, non un meccanismo (regola:")
|
||||||
|
print(" un vantaggio concentrato in un anno non e' un vantaggio).")
|
||||||
|
print(f" null de-levering vs costante: {delever_null(d_ad, d_const)}")
|
||||||
|
d_ad_keep, fn_ad_keep, best_keep = d_ad, fn_ad, best
|
||||||
|
# -- 8. BANDA D'ANCORA (delta APPAIATO adattivo - costante) --------------------------
|
||||||
|
hours = 24 if ch["tf"] == "1d" else 12
|
||||||
|
offs = list(range(hours))
|
||||||
|
hr(f"8. BANDA D'ANCORA — {len(offs)} ancore. Stima onesta = MEDIANA; il confronto fra due "
|
||||||
|
f"varianti\n e' la MEDIANA DELLE DIFFERENZE APPAIATE (mai la differenza delle mediane).")
|
||||||
|
if cch["tf"] != ch["tf"]:
|
||||||
|
print(f" ⚠️ adattivo su {ch['tf']} e miglior costante su {cch['tf']}: per il delta "
|
||||||
|
f"appaiato uso il miglior costante RIVALUTATO su {ch['tf']} (stesse ancore = coppie).")
|
||||||
|
crow = max([r for r in csel["rows"] if r["tf"] == ch["tf"]],
|
||||||
|
key=lambda r: r["insample_sharpe"])
|
||||||
|
fn_const_anch = const_factory(tf=ch["tf"], **crow["params"])
|
||||||
|
print(f" miglior costante su {ch['tf']}: {crow['params']} IS {crow['insample_sharpe']}")
|
||||||
|
else:
|
||||||
|
fn_const_anch = fn_const
|
||||||
|
s0 = series_at_anchor(fixed_target, "fixed", hours, 0)
|
||||||
|
K = pd.concat({"a": s0, "b": base_ch}, axis=1, join="inner").dropna()
|
||||||
|
print(f" sanity ancora 0 vs serie canonica del fisso: max|Δ| "
|
||||||
|
f"{float(np.max(np.abs(K['a']-K['b']))):.3e} (n={len(K)})")
|
||||||
|
for lab, mtr in (("Sharpe FULL", A._sh),
|
||||||
|
("Sharpe HOLD", lambda s: A._sh(s[s.index >= HOLDOUT]))):
|
||||||
|
b_c = A.anchor_luck_band(lambda o: series_at_anchor(fn_ch, "cand", hours, o), offs,
|
||||||
|
metric=mtr)
|
||||||
|
b_k = A.anchor_luck_band(lambda o: series_at_anchor(fn_const_anch, "const", hours, o),
|
||||||
|
offs, metric=mtr)
|
||||||
|
print(f" {lab:<12} adattivo: canonica {b_c['canonical']:+.3f} "
|
||||||
|
f"(pctl {b_c['canonical_pctl']:.2f}) MEDIANA {b_c['median']:+.3f} "
|
||||||
|
f"banda [{b_c['lo']:+.3f},{b_c['hi']:+.3f}]")
|
||||||
|
print(f" {'':<12} costante: canonica {b_k['canonical']:+.3f} "
|
||||||
|
f"(pctl {b_k['canonical_pctl']:.2f}) MEDIANA {b_k['median']:+.3f} "
|
||||||
|
f"banda [{b_k['lo']:+.3f},{b_k['hi']:+.3f}]")
|
||||||
|
dl = A.anchor_luck_delta(lambda o: series_at_anchor(fn_ch, "cand", hours, o),
|
||||||
|
lambda o: series_at_anchor(fn_const_anch, "const", hours, o),
|
||||||
|
offs, metric=mtr)
|
||||||
|
print(f" {'':<12} DELTA APPAIATO (adatt - cost): mediana {dl['median_paired']:+.3f} "
|
||||||
|
f"positivo in {dl['n_positive']}/{dl['n_anchors']} "
|
||||||
|
f"banda [{dl['lo']:+.3f},{dl['hi']:+.3f}] gate_pass={dl['gate_pass']}")
|
||||||
|
|
||||||
|
# -- 8-bis. ANCORA APPAIATA per la cella DAVVERO ADATTIVA ---------------------------
|
||||||
|
if fn_ad_keep is not None and best_keep["tf"] == ch["tf"]:
|
||||||
|
hr("8-bis. ANCORA APPAIATA — cella DAVVERO ADATTIVA vs miglior costante")
|
||||||
|
for lab, mtr in (("Sharpe FULL", A._sh),
|
||||||
|
("Sharpe HOLD", lambda s_: A._sh(s_[s_.index >= HOLDOUT]))):
|
||||||
|
dl = A.anchor_luck_delta(
|
||||||
|
lambda o: series_at_anchor(fn_ad_keep, "adatt", hours, o),
|
||||||
|
lambda o: series_at_anchor(fn_const_anch, "const", hours, o), offs, metric=mtr)
|
||||||
|
b = A.anchor_luck_band(lambda o: series_at_anchor(fn_ad_keep, "adatt", hours, o),
|
||||||
|
offs, metric=mtr)
|
||||||
|
print(f" {lab:<12} adattiva-vera: canonica {b['canonical']:+.3f} "
|
||||||
|
f"(pctl {b['canonical_pctl']:.2f}) MEDIANA {b['median']:+.3f} "
|
||||||
|
f"banda [{b['lo']:+.3f},{b['hi']:+.3f}]")
|
||||||
|
print(f" {'':<12} DELTA APPAIATO vs costante: mediana {dl['median_paired']:+.3f} "
|
||||||
|
f"positivo in {dl['n_positive']}/{dl['n_anchors']} "
|
||||||
|
f"banda [{dl['lo']:+.3f},{dl['hi']:+.3f}] gate_pass={dl['gate_pass']}")
|
||||||
|
elif fn_ad_keep is not None:
|
||||||
|
print(f"\n (8-bis non girata: la cella adattiva sta su {best_keep['tf']} e il costante "
|
||||||
|
f"su {ch['tf']} -> le ancore non sarebbero coppie)")
|
||||||
|
|
||||||
|
# -- 8-ter. IL CRITERIO DEL FALSIFICATORE, SU **TUTTE** LE CELLE ADATTIVE ------------
|
||||||
|
hr("8-ter. IL CRITERIO DEL FALSIFICATORE APPLICATO A **OGNI** CELLA DAVVERO ADATTIVA")
|
||||||
|
print(" Il 22/08 dichiarava: «un rilevatore il cui L_t si muove DAVVERO e batte il miglior")
|
||||||
|
print(" lookback COSTANTE in >=6/8 anni appaiati con corr->TP01 < 0.6». Non basta che")
|
||||||
|
print(" fallisca il VINCITORE: si applica il criterio a tutte e", len(ad_rows), "le celle adattive.")
|
||||||
|
print(f"\n {'tf':>4} {'cella':<58} {'corr':>6} {'anni>0':>7} {'dShFULL':>8} {'dShHOLD':>8} esito")
|
||||||
|
passed = []
|
||||||
|
for r in sorted(ad_rows, key=lambda x: -x["insample_sharpe"]):
|
||||||
|
fnr = factory(tf=r["tf"], **r["params"])
|
||||||
|
dr = A.candidate_daily(fnr, tf=r["tf"])
|
||||||
|
JJ2 = pd.concat({"a": dr, "b": tp01}, axis=1, join="inner").dropna()
|
||||||
|
cor = float(JJ2["a"].corr(JJ2["b"]))
|
||||||
|
yr = paired_by_year(dr, d_const)
|
||||||
|
nyp = sum(1 for x in yr if x["d_sh"] > 0)
|
||||||
|
Jr = pd.concat({"C": dr, "B": d_const}, axis=1, join="inner").dropna()
|
||||||
|
JrH = Jr[Jr.index >= HOLDOUT]
|
||||||
|
dF = A._sh(Jr["C"]) - A._sh(Jr["B"])
|
||||||
|
dH = A._sh(JrH["C"]) - A._sh(JrH["B"])
|
||||||
|
ok = bool(cor < 0.6 and nyp >= 6)
|
||||||
|
if ok:
|
||||||
|
passed.append((r, cor, nyp, dF, dH))
|
||||||
|
lab = (f"{r['params']['det']} {r['params']['par']} {r['params']['stat']} "
|
||||||
|
f"{r['params']['mapping']} {r['params']['blend']}")
|
||||||
|
print(f" {r['tf']:>4} {lab:<58} {cor:>6.3f} {nyp:>5}/8 {dF:>+8.3f} {dH:>+8.3f} "
|
||||||
|
f"{'PASSA' if ok else ''}")
|
||||||
|
print(f"\n celle che soddisfano ENTRAMBE le condizioni del falsificatore: "
|
||||||
|
f"{len(passed)}/{len(ad_rows)}")
|
||||||
|
cors = []
|
||||||
|
for r in ad_rows:
|
||||||
|
dr = A.candidate_daily(factory(tf=r["tf"], **r["params"]), tf=r["tf"])
|
||||||
|
JJ2 = pd.concat({"a": dr, "b": tp01}, axis=1, join="inner").dropna()
|
||||||
|
cors.append(float(JJ2["a"].corr(JJ2["b"])))
|
||||||
|
print(f" corr->TP01 sulle celle adattive: min {min(cors):.3f} mediana "
|
||||||
|
f"{np.median(cors):.3f} max {max(cors):.3f} (soglia del falsificatore 0.60)")
|
||||||
|
|
||||||
|
# -- 9. FEE SWEEP ---------------------------------------------------------------------
|
||||||
|
hr("9. FEE SWEEP — l'adattivo trada di piu': e' li' che muore o no")
|
||||||
|
for nm, f_, tfx in (("adattivo (A)", fn_ch, ch["tf"]), ("WF-PnL (C)", fn_wf, wch["tf"]),
|
||||||
|
("miglior costante (B)", fn_const, cch["tf"]),
|
||||||
|
("TP01 30/90/180", fixed_target, "1d")):
|
||||||
|
line = []
|
||||||
|
for fee in (0.0, 0.0005, 0.001, 0.0015):
|
||||||
|
line.append(f"{2*fee*100:.2f}%RT {A._sh(A.candidate_daily(f_, tf=tfx, fee_side=fee)):+.3f}")
|
||||||
|
tt = np.mean([A.eval_weights(A.get(a, tfx), A._call_target(f_, A.get(a, tfx), a))
|
||||||
|
["turnover_per_year"] for a in ASSETS])
|
||||||
|
print(f" {nm:<22} " + " ".join(line) + f" | turnover {tt:.0f}x/anno")
|
||||||
|
|
||||||
|
hr(f"FINE — {time.time()-t0:.0f}s")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,667 @@
|
|||||||
|
# CRITICO DI COMPLETEZZA — ondata 2026-08-22 (`r0822b_critic`)
|
||||||
|
|
||||||
|
**Mandato:** non testare una strategia. Rispondere a *cosa non e' stato guardato, quale
|
||||||
|
affermazione e' rimasta non verificata, e qual e' la misura di maggior valore che nessuno ha fatto*.
|
||||||
|
|
||||||
|
**Materiale letto per intero:** `docs/research/BRIEF-0822.md`, `docs/research/RESULTS-0822.md`
|
||||||
|
(684 righe, 19 sezioni), `docs/diary/2026-08-22-wave-multiagente.md`, piu' i punti dei singoli
|
||||||
|
`scripts/research/r0822_*.py` dove il ledger andava verificato.
|
||||||
|
|
||||||
|
**Regola di lettura di questo file.** Distinguo tre stati e li marco:
|
||||||
|
`[VERIFICATO]` = ho aperto la fonte e il fatto e' controllato in questa sessione (comando
|
||||||
|
riportato o citazione esatta) · `[DEDOTTO]` = ragionamento su numeri altrui, non ri-misurato ·
|
||||||
|
`[DA MISURARE]` = ipotesi mia, con costo stimato. Non fabbrico numeri nuovi salvo controlli rapidi,
|
||||||
|
e quando ne faccio uno dico che e' un controllo di cinque minuti e non uno studio.
|
||||||
|
|
||||||
|
**Esito in una riga: l'ondata e' onesta nei singoli filoni e debole nella CUCITURA.** I difetti
|
||||||
|
gravi non stanno dentro i filoni (che hanno catturato 7 errori propri) ma in tre punti dove
|
||||||
|
nessuno era responsabile: (a) **due "difetti di dato" sono convenzioni dichiarate nel sorgente di
|
||||||
|
un fornitore che gira su questa stessa VPS e che nessuno ha aperto**; (b) **il gate principale del
|
||||||
|
progetto — il deflated-Sharpe — e' stato misurato lo stesso giorno ai suoi due estremi degeneri da
|
||||||
|
due agenti diversi, e nessuno ha unito le due misure**; (c) **un filone su venti (ALT-OPTIONS) non
|
||||||
|
e' nel ledger, e il suo risultato falsifica una quarta soglia pubblicata** — quella che tiene
|
||||||
|
VRP01 fuori dal libro per taglia.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. CLAIM NON VERIFICATI
|
||||||
|
|
||||||
|
Ordinati per `(impatto x P(falso)) / costo`. Non elenco tutto: le otto che contano.
|
||||||
|
|
||||||
|
### 1.1 — 🥇 «una firm crypto lista i 19 alt Hyperliquid con short abilitato» (PROP-ALLOC)
|
||||||
|
`P(falso)` **media-alta** · `impatto` **massimo** · `costo` **un pomeriggio, zero CPU**
|
||||||
|
|
||||||
|
E' l'unica assunzione su cui poggia **l'intero risultato di testa dell'ondata**: `+0,400 di J`
|
||||||
|
(argmaxJ 25/25/50 contro il libro live 75/25) e' quasi tutto *mettere XS01 su un conto funded*, e
|
||||||
|
`+0,016` — il 4% — e' il riallocare per la barriera. Se XS01 non e' eseguibile presso la firm,
|
||||||
|
la riga argmaxJ collassa verso `75/25/0`, che nello stesso script misura **J 0,335 / P(pass) 38,6%**
|
||||||
|
invece di **0,738 / 82,2%**. Con essa cade anche il *risultato strategico* del 25/07 §4
|
||||||
|
(«sul canale funded diversificare paga perche' il vincolo binding e' il DD»), che e' il perno di
|
||||||
|
tutta la valutazione del canale prop.
|
||||||
|
|
||||||
|
⚠️ **E il progetto ha gia' una regola scritta per questo caso, e non l'ha applicata.** Il 26/07:
|
||||||
|
*«la negoziabilita' sul conto REALE va verificata quando lo sleeve entra in RICERCA, non quando
|
||||||
|
entra nel book»* — regola nata dal blocco PRIIPs su GTAA01, dove **cinque settimane di misure**
|
||||||
|
poggiavano su un'assunzione di conto mai controllata, e il controllo costo' **un ordine di prova**.
|
||||||
|
`r0822_prop_alloc.py` la ripete: alla riga 100 `CRYPTO_PROP = ("TP01","SKH01","XS01") # cio' che
|
||||||
|
una firm CRYPTO puo' davvero far eseguire` — un commento, non una misura. [VERIFICATO: letto il
|
||||||
|
sorgente; l'agente esclude esplicitamente VRP01/GTAA01 *"una firm CRYPTO non lista opzioni Deribit
|
||||||
|
ne' ETF azionari USA"*, quindi ha pensato al problema e si e' fermato un passo prima di XS01.]
|
||||||
|
|
||||||
|
**Cosa verificare, in ordine:** (a) la lista strumenti di HYROTRADER e FTMO-crypto contiene i 19
|
||||||
|
major HL (SOL/BNB/XRP/DOGE/AVAX/LINK/LTC/ADA/ARB/OP/SUI/APT/INJ/TIA/SEI/NEAR/AAVE oltre BTC/ETH)?
|
||||||
|
(b) short abilitato su ciascuno? (c) quale costo effettivo per gamba (spread + commissione)?
|
||||||
|
**La (c) ha una soglia gia' misurata in questa stessa ondata**: XS-LITE riporta che la famiglia XS
|
||||||
|
regge **oltre 50 bps/gamba** — quindi il costo di una firm quasi certamente non morde, ed e' la
|
||||||
|
(a)/(b) binaria che decide tutto. **Se (a) e' falsa, il verdetto LEAD del filone 1 va riscritto
|
||||||
|
oggi, non a un gate futuro.**
|
||||||
|
|
||||||
|
### 1.2 — 🥈 «`dealer_net_gamma` e' il GEX col SEGNO INVERTITO» (DEALER-GAMMA)
|
||||||
|
`P(falso)` **alta — anzi: FALSO come formulato** · `impatto` medio · `costo` **due minuti**
|
||||||
|
|
||||||
|
Il ledger lo chiama *«il risultato piu' riusabile del filone»* e scrive: *«Chi lo usasse col nome
|
||||||
|
che porta leggerebbe ogni regime al contrario»*. L'agente ha deciso la questione **per
|
||||||
|
ricostruzione** (corr −0,95 BTC / −0,89 ETH contro un GEX da manuale) e lo dichiara esplicitamente
|
||||||
|
nel proprio codice: `"Quale, lo decide la sezione 2 (ricostruzione dalla catena), non io"`
|
||||||
|
(`r0822_dealer_gamma.py:187`).
|
||||||
|
|
||||||
|
🚨 **La fonte autorevole e' su questa VPS e dice il contrario.** [VERIFICATO] La colonna non e'
|
||||||
|
calcolata da cerbero-bite: bite **inoltra e basta** il campo `total_net_dealer_gamma` che riceve
|
||||||
|
dal tool MCP `get_dealer_gamma_profile` (`src/cerbero_bite/runtime/market_snapshot_cycle.py:85` e
|
||||||
|
`clients/deribit.py:342-384`, letti dal repo Gitea `Adriano/Cerbero-Bite`). E il produttore e'
|
||||||
|
`/opt/docker/cerbero-mcp/src/cerbero_mcp/common/options.py`, **acceso adesso**, che in testa al
|
||||||
|
file dichiara la convenzione:
|
||||||
|
|
||||||
|
```
|
||||||
|
# Convention dealer gamma: i dealer sono SHORT calls (le vendono al retail) e
|
||||||
|
# LONG puts. ... Net dealer gamma negativo -> mercato instabile (squeeze ...)
|
||||||
|
...
|
||||||
|
entry["call_dealer_gamma"] -= contrib # dealer short calls
|
||||||
|
entry["put_dealer_gamma"] += contrib # dealer long puts
|
||||||
|
```
|
||||||
|
|
||||||
|
Cioe': **call negative, put positive** — l'esatta negazione, su entrambe le gambe, della
|
||||||
|
convenzione da manuale che l'agente ha usato (`call +, put −`, `r0822_dealer_gamma.py:237`).
|
||||||
|
**Non e' un difetto della colonna: e' una convenzione opposta, deliberata e documentata** — e nel
|
||||||
|
suo frame `dng < 0` significa *dealer short gamma / mercato instabile*, che e' proprio la
|
||||||
|
semantica che il docstring di bite dichiara e che l'entry filter §2.8 usava.
|
||||||
|
|
||||||
|
E c'e' una **seconda spiegazione, gratis, per i due numeri residui che l'agente ha attribuito a
|
||||||
|
non-portabilita'**: la chiamata di bite usa il default `top_n_strikes = 50` [VERIFICATO:
|
||||||
|
`dealer_gamma_profile(asset_upper)` senza argomenti], contro i **~280-316 strike** della
|
||||||
|
ricostruzione. Un troncamento ai 50 strike di punta spiega naturalmente sia il coefficiente
|
||||||
|
`−0,7` (invece di `−1,0`) sia l'offset *«dng=0 corrisponde a GEX=+5,8e6 su BTC»*, che il ledger
|
||||||
|
presenta come prova che la soglia non e' portabile.
|
||||||
|
|
||||||
|
**Cosa cambia se e' falso (ed e' falso):** il verdetto **SCARTATO regge intatto** (DSR 0,440,
|
||||||
|
null de-levering fallito, `n_eff` 24-53 ore su 2.130, cella migliore *sotto* il massimo del
|
||||||
|
rumore). Ma cade la **regola pubblicata**, e cade nel modo peggiore: la frase del ledger *«BTC non
|
||||||
|
separa, ETH separa al ROVESCIO»* e' formulata in un frame di segno che va rovesciato — quindi
|
||||||
|
l'unica riga del filone che le ondate future erediterebbero e' quella sbagliata. **Correzione da
|
||||||
|
fare oggi nel ledger, costo due righe.**
|
||||||
|
|
||||||
|
### 1.3 — 🥉 «`liquidation_long/short_risk` e' costante 'low': meta' dell'ipotesi non esiste, ed e'
|
||||||
|
comunque morta perche' la colonna non e' ricostruibile» (FLOW-SQUEEZE)
|
||||||
|
`P(falso)` **alta sulla seconda meta'** · `impatto` medio · `costo` **due ore**
|
||||||
|
|
||||||
|
[VERIFICATO] Il fatto e' vero (`low` in 17.229/17.229 righe non nulle, riprodotto). Ma le due
|
||||||
|
inferenze che l'agente costruisce sopra il fatto sono entrambe smentibili leggendo la stessa
|
||||||
|
sorgente di §1.2, `/opt/docker/cerbero-mcp/.../sentiment/fetchers.py:480-518`:
|
||||||
|
|
||||||
|
```
|
||||||
|
long_risk = "high" se avg_funding > 0.0001 e delta_24h > 5
|
||||||
|
"medium" se avg_funding > 0.00005 e delta_24h > 2
|
||||||
|
```
|
||||||
|
|
||||||
|
Cioe' **e' una soglia su due input, non una misura**. Conseguenze:
|
||||||
|
- *«Nessuna fonte nostra la produce, e non c'e' niente da produrre»* (`r0822_flow_squeeze.py:626`)
|
||||||
|
e' **falso**: i due input sono `oi_delta_pct_24h` e il funding cross, e **il funding cross e' nel
|
||||||
|
file** (`funding_cross_annualized`, copertura 99%), cosi' come `oi_delta_pct_4h` (copertura 99%,
|
||||||
|
dev.std 1,23%, range −7,1%…+13,4% — [VERIFICATO] con una lettura del parquet). L'agente poteva
|
||||||
|
ricostruire la variabile di affollamento **con le proprie soglie**, che e' anche metodologicamente
|
||||||
|
meglio che ereditarne una arbitraria.
|
||||||
|
- *«Costante = zero informazione»* e' vero come statistica e **sbagliato come lettura**: `low` al
|
||||||
|
100% con quelle soglie significa che **nella finestra 2026-03 → 07 non c'e' stato un solo
|
||||||
|
episodio di affollamento** per quella definizione. Non e' una colonna rotta, e' un fatto di
|
||||||
|
mercato — e per giunta **rafforza** la conclusione del filone (funding chiuso sul quarto lato)
|
||||||
|
molto piu' di quanto faccia «la colonna e' inservibile».
|
||||||
|
|
||||||
|
**Cosa cambia se e' falso:** il verdetto SCARTATO regge (l'altra meta' e' sotto la propria MDE
|
||||||
|
dichiarata: effetto 0,402% contro MDE 1,056%). Ma il filone e' chiuso con la motivazione
|
||||||
|
sbagliata, e la motivazione sbagliata e' quella che impedisce di riaprirlo con i dati che ci sono.
|
||||||
|
|
||||||
|
### 1.4 — «la finestra forward non va persa: riparare `advance()` + RIGENERARE, nessuna data di
|
||||||
|
gate si sposta» (MONITOR-AUDIT)
|
||||||
|
`P(falso)` bassa sul fatto, **alta sull'implicazione** · `impatto` **alto (3 gate)** · `costo` zero
|
||||||
|
|
||||||
|
Il fatto e' provato bene, due volte in modo indipendente (`paper_prevday` 1427/1488 barre identiche
|
||||||
|
al bit dopo 62 notti; le 6 barre di `paper_statarb` recuperate dopo il guasto EPERM coincidono al
|
||||||
|
bit). **Non contesto il fatto. Contesto che sia gratis.**
|
||||||
|
|
||||||
|
Una serie **rigenerata** non e' una serie **forward**. La ragione per cui un progetto
|
||||||
|
pre-registra una finestra forward e' che sia prodotta da codice che *non poteva* essere tarato su
|
||||||
|
di essa. Dopo la riparazione, la serie su cui si decide il 27/09 sara' prodotta da codice
|
||||||
|
**scritto oggi, dopo aver visto quella finestra** — e chi scrive la riparazione ha gia' letto,
|
||||||
|
nel ledger, che la metrica registrata dice +1,96 e quella ricostruita −2,02. La riparazione e'
|
||||||
|
banale e meccanicamente forzata (scartare la barra non chiusa), quindi il rischio e' piccolo; ma
|
||||||
|
**e' un grado di liberta' nuovo su un gate pre-registrato, e nessuno lo ha nominato.**
|
||||||
|
|
||||||
|
**Costo di chiuderlo: zero.** Si scrive la riparazione, la si congela con un test, e **solo
|
||||||
|
dopo** si rigenera — dichiarando in anticipo che nessun parametro verra' toccato in base al
|
||||||
|
risultato della rigenerazione. Farlo dopo aver visto lo Sharpe rigenerato non e' piu' possibile.
|
||||||
|
|
||||||
|
### 1.5 — «haircut a $5.000 <= 40%» come gamba del gate XSR01 del 23/10
|
||||||
|
`P(che il gate sia non-informativo)` **alta** · `impatto` **alto** · `costo` **zero (e' gia' misurato)**
|
||||||
|
|
||||||
|
Due filoni hanno colpito le due gambe del gate del 23/10 **senza che nessuno tirasse la somma**:
|
||||||
|
- **gamba haircut** — HL-EXEC: a $5.000 col pavimento vero l'haircut sta fra **−7% e +2%**, quindi
|
||||||
|
*«una condizione pre-registrata che non puo' fallire non e' una condizione»*. XSR-REPRO
|
||||||
|
dall'altro lato: il **$14,41** pubblicato **non ha alcuno script committato che lo riproduca** ed
|
||||||
|
e' **~2,4x ottimista** (ticket mediano $3,33, 63% degli ordini sotto $5).
|
||||||
|
- **gamba Sharpe >= 1,0** — MONITOR-AUDIT: la serie che la misura registra **41 minuti di mercato
|
||||||
|
al giorno**, `corr` col replay **−0,045**.
|
||||||
|
|
||||||
|
📌 **La somma, che il ledger non fa: al 22/08 il gate del 23/10 e' non-informativo su ENTRAMBE le
|
||||||
|
gambe** — una non puo' fallire, l'altra e' letta su uno strumento scollegato dalla realta'. Il
|
||||||
|
ledger scrive «GATE 23/10 COMPROMESSO: PARZIALMENTE». **E' compromesso interamente**, e la
|
||||||
|
differenza non e' semantica: «parzialmente» autorizza a decidere il 23/10 con una riserva,
|
||||||
|
«interamente» obbliga a **ri-registrare il gate prima**, che e' l'unica mossa che non e' selezione.
|
||||||
|
|
||||||
|
### 1.6 — «k* empirico ~10-14x» (GROWTH-POLICY)
|
||||||
|
`P(falso)` **alta come numero** · `impatto` medio-alto · `costo` zero (giunzione di due misure gia' fatte)
|
||||||
|
|
||||||
|
L'agente ha gia' fatto due autocritiche corrette (capitale mediano $4,4e12 a k=14x → *«a k>2 i
|
||||||
|
numeri sono aritmetica, non previsioni»*; k* lineare nell'errore del drift). Ne manca una terza, e
|
||||||
|
sta **dentro la stessa ondata**: SLIP-AUDIT misura che la partecipazione al nastro scala lineare
|
||||||
|
col nozionale e che il caso peggiore osservato e' gia' **21,9% di una barra 5m** a nozionale ~$636.
|
||||||
|
La leva moltiplica il nozionale esattamente come il capitale. Quindi [DEDOTTO, aritmetica diretta
|
||||||
|
sulla tabella di SLIP-AUDIT]:
|
||||||
|
|
||||||
|
| leva su $635 | nozionale | peggior partecipazione osservata, riscalata |
|
||||||
|
|---|---|---|
|
||||||
|
| 1,00x | $636 | 22% di una barra 5m |
|
||||||
|
| **1,50x** (il gradino proposto) | $954 | **33%** |
|
||||||
|
| 5x | $3.180 | 110% |
|
||||||
|
| **10-14x** (k*) | $6.400-8.900 | **220-310%** |
|
||||||
|
|
||||||
|
📌 **Il gradino 1,00 → 1,50x NON e' toccato da questo** (33% resta dentro il regime misurato):
|
||||||
|
la raccomandazione operativa sopravvive. **E' il numero k\* che non e' misurabile**, per una terza
|
||||||
|
ragione indipendente dalle due dichiarate: sopra ~k=5 la serie di ritorni su cui k\* e' calcolato
|
||||||
|
smette di essere indipendente dalla size, che e' esattamente l'ipotesi che l'agente aveva segnalato
|
||||||
|
come implausibile senza poterla quantificare. **Ora e' quantificata, gratis, e va scritta accanto
|
||||||
|
al 10-14x.**
|
||||||
|
|
||||||
|
### 1.7 — «BTC opzioni min 0.1 = $6.210/lotto → VRP01 fuori a $3.000, servirebbe il 61% del conto»
|
||||||
|
`P(falso)` **verificata FALSA** · `impatto` **alto** · `costo` gia' pagato — il numero esiste e non e' nel ledger
|
||||||
|
|
||||||
|
Questo e' congelato in `tests/test_vrp_profit_take.py` e in CLAUDE.md. VRP-QUOTE-VERE l'ha
|
||||||
|
falsificato a meta' (famiglia sbagliata) e **ALT-OPTIONS l'ha falsificato del tutto** — ma
|
||||||
|
ALT-OPTIONS **non ha una sezione nel ledger** (vedi §5.1), quindi il numero non e' mai stato
|
||||||
|
scritto. [VERIFICATO: letto l'output del filone]:
|
||||||
|
|
||||||
|
| famiglia | f_venue | credito eseg. | **max-loss / lotto** | IM se il venue NON netta |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| **ETH_USDC** | **0,980** | $3,38 | **$16,62** | $33,50 |
|
||||||
|
| BTC_USDC | 0,921 | $6,25 | $33,75 | $101,79 |
|
||||||
|
| ETH inverse (la famiglia del muro) | 0,930 | $29,36 | $145,88 | $324,42 |
|
||||||
|
| BTC inverse (la famiglia del muro) | 0,927 | $81,97 | $418,03 | $1.017,06 |
|
||||||
|
|
||||||
|
Il muro pubblicato e' calcolato sul **nozionale del lotto minimo della famiglia inverse**. La
|
||||||
|
grandezza giusta per una struttura a rischio definito e' il **max-loss**, e sulla famiglia che il
|
||||||
|
conto puo' marginare vale **$16,62**. Al peso di libro del 12%, VRP01 entra con un lotto da un
|
||||||
|
conto di **~$140** (o ~$280 se Deribit non netta le gambe), non da $3.000 al 61% del conto —
|
||||||
|
**due ordini di grandezza**. Sul conto vero ($635) ci stanno **7 strutture dentro un budget di
|
||||||
|
rischio del 20%**, con **3.000 lotti al miglior bid** e spread della gamba corta al **2,6%**.
|
||||||
|
|
||||||
|
⚠️ **Cosa NON cambia, e va detto forte:** VRP01 resta fuori dal libro, ma **per la regola
|
||||||
|
«niente short-vol da modello in deploy» e per il gate IV-rank (0/19 settimane aperte)**, non per
|
||||||
|
la taglia. Cade *una* delle tre ragioni, ed e' quella che il progetto ripete piu' spesso.
|
||||||
|
|
||||||
|
### 1.8 — «lo screen largo non puo' passare il proprio gate: e' aritmetica» (ORTHO-SCREEN)
|
||||||
|
`P(falso)` **alta come generalizzazione** · `impatto` **alto (tocca 3 gate futuri)** · `costo` **5 minuti**
|
||||||
|
|
||||||
|
Vedi §2.1: l'ho ri-derivato e il numero e' giusto ma **non dice cio' che il ledger gli fa dire**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. CONTRADDIZIONI FRA AGENTI
|
||||||
|
|
||||||
|
### 2.1 — 🚨 IL DEFLATED-SHARPE MISURATO AI SUOI DUE ESTREMI DEGENERI, LO STESSO GIORNO, DA DUE AGENTI CHE NON SI CITANO
|
||||||
|
|
||||||
|
- **ORTHO-SCREEN**: *«su 168 trial il massimo atteso dal puro rumore e' Sharpe 1,572, sopra il
|
||||||
|
soffitto direzionale (~1,3) → una ricerca a rete larga NON PUO' passare il proprio gate. Non e'
|
||||||
|
sfortuna, e' aritmetica.»* DSR di screen **0,034**.
|
||||||
|
- **SCETTICO su VOL-SIZE**: *«`DSR = 1,000` dichiarato **VACUO, non PASS**: lo passa anche il
|
||||||
|
baseline — su una famiglia di perturbazioni della stessa strategia il deflated-Sharpe non ha
|
||||||
|
potenza.»*
|
||||||
|
|
||||||
|
**Sono la stessa osservazione dai due lati, e insieme delimitano il gate principale del progetto.**
|
||||||
|
[VERIFICATO] Ho aperto `altlib.deflated_sharpe`: il massimo atteso non e' `SE(Sharpe) x sqrt(2 ln N)`
|
||||||
|
ma la formula Bailey–Lopez de Prado
|
||||||
|
|
||||||
|
```
|
||||||
|
sr0 = sd(Sharpe dei trial OSSERVATI) x [ (1-gamma)*z(1-1/N) + gamma*z(1-1/(N e)) ]
|
||||||
|
```
|
||||||
|
|
||||||
|
cioe' **la dispersione trasversale delle celle che hai deciso di dichiarare**, moltiplicata per un
|
||||||
|
fattore che dipende solo da N. Ricalcolando il moltiplicatore [controllo di cinque minuti]:
|
||||||
|
|
||||||
|
| N dichiarato | moltiplicatore | `sr0` con la STESSA dispersione (sd = 0,58) |
|
||||||
|
|---|---|---|
|
||||||
|
| 12 | 1,665 | 0,97 |
|
||||||
|
| **24** | 1,980 | **1,15** |
|
||||||
|
| 48 | 2,261 | 1,31 |
|
||||||
|
| 72 | 2,413 | 1,40 |
|
||||||
|
| **168** (lo screen) | 2,708 | **1,57** |
|
||||||
|
| 729 | 3,164 | 1,84 |
|
||||||
|
|
||||||
|
E al variare della dispersione, a parita' di dati:
|
||||||
|
|
||||||
|
| dispersione delle celle | `sr0(24)` | `sr0(168)` |
|
||||||
|
|---|---|---|
|
||||||
|
| 0,15 (famiglia di perturbazioni, tipo VOL-SIZE) | 0,30 | 0,41 |
|
||||||
|
| 0,58 (lo screen ortogonale) | 1,15 | 1,57 |
|
||||||
|
|
||||||
|
📌 **Tre letture che il ledger non ha:**
|
||||||
|
1. **La soglia «1,572» non e' una proprieta' di BTC/ETH: e' una proprieta' della GRIGLIA.** Le
|
||||||
|
stesse 168 celle, dichiarate come 7 famiglie da 24, danno `sr0 = 1,15` — **sotto** il soffitto
|
||||||
|
direzionale. Il verdetto «impossibile per aritmetica» si ribalta **senza toccare un dato**.
|
||||||
|
2. **Percio' la lezione pubblicata invita alla mossa che il progetto ha gia' proibito.** Il ledger
|
||||||
|
conclude *«le ondate future o dichiarano famiglie molto piu' piccole in anticipo, o cambiano
|
||||||
|
meccanismo»*. La prima meta' e' **esattamente** il barare che il 30/07 aveva codificato:
|
||||||
|
*«il deflated-Sharpe e' l'unico posto dove barare e' indolore e invisibile»*, e
|
||||||
|
*«il verdetto si ribalta col conteggio»* (congelato in un test su VRP01: DSR 0,983 a N=8 /
|
||||||
|
0,948 a N=72 / 0,909 a N=360). **Il 22/08 la stessa leva viene raccomandata come buona pratica.**
|
||||||
|
3. **Cio' che il progetto non sapeva e' la META' PIU' GRANDE: la dispersione.** Il fattore N va da
|
||||||
|
1,67 a 3,16 su due ordini di grandezza di trial (1,9x). La dispersione va da 0,15 a 0,58 fra una
|
||||||
|
famiglia omogenea e uno screen (3,9x). **Il gate e' dominato dalla variabile mai dichiarata.**
|
||||||
|
|
||||||
|
**Conseguenza operativa immediata**, perche' tocca tre gate pre-registrati: DVOLSPREAD 24/10 ha
|
||||||
|
come criterio (c) *«deflated-Sharpe ricalcolato >= 0,95»* — su una famiglia di 729 celle
|
||||||
|
omogenee; XSR01 23/10 poggia su DSR 0,983; VOL-SIZE ha gia' dichiarato il proprio DSR vacuo.
|
||||||
|
**Nessuno dei tre e' interpretabile senza dichiarare, accanto a N, la dispersione delle celle.**
|
||||||
|
Riformulazione onesta della lezione di ORTHO-SCREEN: *con celle disperse di ~0,58 di Sharpe e un
|
||||||
|
soffitto di ~1,3, il gate non separa; la cura non e' dichiarare meno trial, e' progettare famiglie
|
||||||
|
i cui membri siano varianti di UN meccanismo — e li' il DSR e' vacuo, quindi serve un altro gate.*
|
||||||
|
**Il deflated-Sharpe non ha una regione utile in mezzo, e nessuno l'ha detto.**
|
||||||
|
|
||||||
|
### 2.2 — Lo Sharpe di XSR01: non tre lenti ma **quattro**, e la quarta non e' nel quadro conciliato
|
||||||
|
Il ledger conclude con un «quadro conciliato»: titolo `demean_skeptic` **1,82** · gate
|
||||||
|
`basket_gate` **1,75-1,79** · monitor `paper_xsr` **2,21**, e prescrive
|
||||||
|
*«NUMERO ONESTO DA CITARE: 1,79 (finestra di scoperta) / 1,63 (a oggi)»*.
|
||||||
|
|
||||||
|
[VERIFICATO nell'output di XS-LITE] Lo stesso giorno **XS-LITE**, che dichiara replica **bit-exact**
|
||||||
|
contro `basket_from_positions(demean=True)` (`max|diff| = 0,000e+00` su 965 barre), pubblica:
|
||||||
|
*«XSR01 pieno, lente pubblicata: Sharpe **1,64** (pubblicati 1,82)»*, *«normalizzazione COSTANTE
|
||||||
|
1/50: **1,65**»*, *«sulla FINESTRA DI SCOPERTA (<= 2026-07-25, 937 barre): **1,75**»*.
|
||||||
|
|
||||||
|
Quindi **due agenti che rivendicano entrambi la replica bit-exact della stessa funzione danno 1,75
|
||||||
|
e 1,79 sulla stessa finestra.** La spiegazione piu' probabile e' la loro stessa scoperta (l'ultima
|
||||||
|
barra della finestra di scoperta era parziale il 25/07 e oggi e' chiusa), e i due concordano
|
||||||
|
sull'oggi (1,64 vs 1,63). **Ma il ledger prescrive una coppia canonica senza nominare l'altra**, e
|
||||||
|
la regola che pubblica — *«va sempre citata la coppia (lente, ultima barra chiusa)»* — e' proprio
|
||||||
|
quella che avrebbe risolto la discrepanza se applicata a se stessa. **Costo di chiudere: una riga
|
||||||
|
nel ledger. Impatto: e' il numero su cui si decide il 23/10.**
|
||||||
|
|
||||||
|
### 2.3 — Il muro di XS01: **tre numeri diversi** nello stesso corpus di documenti
|
||||||
|
- XS-LITE: capitale minimo perche' il ribilancio passi = **$109 di sleeve (~$730 di libro)**.
|
||||||
|
- HL-EXEC: haircut ~0 gia' a **$200-600 allocati** (→ a peso 15%, **$1.300-4.000 di conto**).
|
||||||
|
- Diario §6.5: *«XS01 e' eseguibile a **~$1.300 di conto**»* — cioe' l'estremo basso di HL-EXEC.
|
||||||
|
|
||||||
|
I tre sono compatibili (criteri diversi: soglia del ribilancio vs soglia dell'haircut), ma il
|
||||||
|
diario ne pubblica **uno** come se fosse *il* numero, dopo che l'ondata ha appena passato una
|
||||||
|
giornata a dimostrare che i numeri pubblicati a un criterio solo sono il difetto ricorrente del
|
||||||
|
progetto. **Da citare come banda `$730-$1.300` col criterio accanto.**
|
||||||
|
|
||||||
|
### 2.4 — La leva: due filoni della stessa ondata danno guida opposta
|
||||||
|
- **GROWTH-POLICY**: k\* ~10-14x, il libro gira al 7% di Kelly, **alzare** la leva vale 14,7a → 11,6a.
|
||||||
|
- **PROP-ALLOC**: J massimo a **0,75x**, e *«P(pass) senza limite di tempo e' monotona
|
||||||
|
DECRESCENTE»* (98,9% a 0,50x → 65,9% a 1,50x).
|
||||||
|
|
||||||
|
Non e' un errore: sono obiettivi diversi (crescita in log senza barriera vs sopravvivenza sotto
|
||||||
|
una barriera per-conto). **Ma nessuno lo scrive**, e l'operatore leggera' due raccomandazioni
|
||||||
|
opposte nello stesso documento. La sintesi che manca, e che riconcilia:
|
||||||
|
📌 **anche il conto proprio ha una barriera assorbente** — e' la decisione di venue del 26/07
|
||||||
|
(*«zero e' assorbente: andare a zero all'anno 10 di un piano da 16 anni significa non arrivarci
|
||||||
|
piu'»*). Quindi il criterio di GROWTH-POLICY (Kelly puro) e' il caso limite senza barriera, e
|
||||||
|
PROP-ALLOC misura cosa succede quando la barriera c'e': **la leva ottima cade da 10x a 0,75x
|
||||||
|
appena si mette una barriera, e la distanza fra i due numeri E' la misura di quanto la barriera
|
||||||
|
costa.** Detta cosi', le due misure smettono di contraddirsi e diventano la stessa curva.
|
||||||
|
|
||||||
|
### 2.5 — Il peso di SKH01: respinto da un filone, raccomandato al doppio dall'altro
|
||||||
|
- **VOL-SIZE / scettico**: *«quasi ogni variante alzava il peso effettivo di SKH fino a 0,375, e
|
||||||
|
"alzare SKH01" e' gia' stato misurato e respinto il 26/07»* — motivo per cui il null giusto e'
|
||||||
|
iso-peso.
|
||||||
|
- **PROP-ALLOC**: sul campione 2019+ che contiene il 2022, l'ottimo della coppia crypto sotto
|
||||||
|
barriera e' **38/62** (J 0,521 contro 0,443), cioe' **SKH01 al 62%**.
|
||||||
|
|
||||||
|
Di nuovo obiettivi diversi, di nuovo non detto. Ma qui c'e' un aggravante che **l'agente dichiara
|
||||||
|
da solo**: PROP-ALLOC non ha girato la banda d'ancora, quindi il suo ottimo poggia sull'ancora
|
||||||
|
canonica di **SKH01, lo sleeve che il LOO del 26/07 misura come quello con la fortuna d'ancora
|
||||||
|
piu' grande da restituire** (l'ancora regala 2/3 del FULL, 70% dell'hold-out, 80% della protezione
|
||||||
|
DD). **La distorsione e' a favore della raccomandazione, e la raccomandazione riguarda l'unico
|
||||||
|
canale che l'operatore potrebbe aprire quest'anno.** Costo di chiuderla: rigirare l'ottimo su una
|
||||||
|
banda di 8 ancore — l'ordine di grandezza del budget di un agente di questa ondata.
|
||||||
|
|
||||||
|
### 2.6 — Il conteggio degli ingressi VRP nella stessa finestra di catena: 19 contro 22
|
||||||
|
VRP-QUOTE-VERE: *«f = 0,714 pooled, **0/19** osservazioni >= 1,0»*; SKEW: *«0,714 su **22**
|
||||||
|
ingressi»*. Stesso `f` a tre decimali, popolazione diversa. [VERIFICATO che SKEW usa
|
||||||
|
*«stessa macchina di `cblib`»*, quindi **non e' una replica indipendente** e l'accordo a tre
|
||||||
|
decimali non e' evidenza.] Spiegazione benigna probabile (VRP richiede **entrambe** le gambe
|
||||||
|
quotate ai due delta obiettivo, SKEW ricostruisce lo smile e ne richiede meno). **Ma il ledger
|
||||||
|
presenta il 0,714 come «replica indipendente» del f del 30/07 senza dire che il secondo 0,714
|
||||||
|
viene dallo stesso harness.** Impatto basso, costo di chiarirlo nullo.
|
||||||
|
|
||||||
|
### 2.7 — «il collettore raccoglie la famiglia sbagliata» e' una classificazione, non un fatto
|
||||||
|
Il ledger apre VRP-QUOTE-VERE con 🚨 **IL RISULTATO DI PRODUZIONE**. [VERIFICATO in
|
||||||
|
`scripts/live/collect_chain.py:114`: `{"currency": asset, "kind": "option"}` e `OI_MIN = 100.0`
|
||||||
|
alla riga 57 — i due fatti sono esatti.] Ma il collettore serve la **ricerca sui prezzi**, non
|
||||||
|
l'esecuzione: sulle inverse ci sono 415/548 strumenti con OI>=100 e **97% di quote a due lati**,
|
||||||
|
contro il 94% e 119 strumenti di ETH_USDC. Per misurare `f`, IV-rank e struttura a termine, la
|
||||||
|
famiglia raccolta e' **la migliore delle due**. Il difetto esiste solo rispetto a un uso —
|
||||||
|
l'esecuzione — che oggi e' vietato da un'altra regola («niente short-vol da modello in deploy»).
|
||||||
|
Il diario lo corregge a meta' (punto aperto 3 lo presenta come decisione sul rate limit); il
|
||||||
|
ledger no. **Un difetto di produzione e una scelta di raccolta non si segnano con lo stesso
|
||||||
|
simbolo**, o il prossimo che legge ripara qualcosa che non e' rotto — al costo di **+90% sul giro**
|
||||||
|
e del rate-limit per-IP che il 29/07 e' gia' costato un guasto.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. MODALITA' NON ESPLORATE
|
||||||
|
|
||||||
|
Filtro applicato: (a) compatibile con dati che il progetto possiede o puo' ottenere, (b) non nella
|
||||||
|
lista dei filoni morti del brief, (c) meccanismo plausibile e nominabile. Niente idee generiche.
|
||||||
|
|
||||||
|
### 3.1 — 🥇 Il costo del CHOP, cioe' il rischio che il libro non ha mai vissuto
|
||||||
|
**Dati:** esistono (7,4 anni di libro; 2018-19 e 2022-23 come stiramenti laterali; 30 anni di
|
||||||
|
equity come stampo). **Meccanismo:** il progetto ha misurato la difesa nel **sinistro** — il
|
||||||
|
criterio B di `edge_watch` e' 8/8 anni di crash superati — e non ha **mai** misurato la modalita'
|
||||||
|
di fallimento propria di un trend-follower, che non e' il crash ma il **mercato laterale**. E la
|
||||||
|
misura di controllo che ho fatto in cinque minuti la rende urgente e non teorica:
|
||||||
|
|
||||||
|
| giorno | BTC | ETH | **libro 75/25 (close-only)** |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 2020-03-12 | **−41,5%** | **−45,7%** | **+0,97%** |
|
||||||
|
| 2022-06-13 | −15,5% | — | +0,72% |
|
||||||
|
| 2021-05-19 | −14,3% | −27,9% | −1,98% |
|
||||||
|
| 2022-11-09 | −14,4% | −18,0% | 0,00% |
|
||||||
|
| 2021-01-21 | — | −19,5% | −2,24% |
|
||||||
|
|
||||||
|
[controllo di cinque minuti, non uno studio: `altlib.tp01_baseline_daily()` + `sleeves._skyhook_returns()`,
|
||||||
|
75/25, ancora canonica. Peggior giorno del libro su 7,4 anni: **−3,38% (2025-10-10)**, che **non e'
|
||||||
|
un giorno di crash**.] Cioe': **il libro non perde nei crolli — nei crolli guadagna.** Tutte le sue
|
||||||
|
peggiori giornate sono altrove. **Nessuno ha mai chiesto dove.** Un piano a 10-20 anni su una
|
||||||
|
strategia il cui unico rischio misurato e' quello che non la tocca ha un buco esattamente della
|
||||||
|
forma che questo progetto e' bravo a trovare in tutto il resto.
|
||||||
|
|
||||||
|
### 3.2 — 🥈 Il prezzo di essere ciechi: il costo atteso in P&L dell'attrito OPERATIVO
|
||||||
|
**Dati:** ci sono gia' e sono nostri — `logs/cron_book.log`, `data/venue_watch`, `feed_age_minutes`,
|
||||||
|
`skh_feed_errors`, `book_executions.jsonl`. **Meccanismo:** nel 2026 il libro e' stato cieco o
|
||||||
|
stantio almeno tre volte documentate (feed-freeze 14/07; fallback silenzioso di `fresh_5m` il
|
||||||
|
29/07, latenza d'uscita di SKH01 da ~1h a **~11h** per 6 giri su 8; manutenzione Deribit del 18/08
|
||||||
|
sforata **fino alle 14:07**, con astensione del libro). Il progetto ha prezzato fee, slippage,
|
||||||
|
min-order, pavimento IB, fortuna d'ancora, degrado d'esecuzione, e persino **il fallimento
|
||||||
|
dell'exchange** — e non ha **mai** prezzato *quanto costa all'anno che il libro non veda*. E' una
|
||||||
|
misura, non una previsione: si prendono le finestre di cecita' realmente occorse, si guarda cosa ha
|
||||||
|
fatto il prezzo dentro, e si confronta il P&L realizzato col P&L del percorso non-cieco.
|
||||||
|
**Perche' conta adesso:** e' un costo che **scala col nozionale** esattamente come la leva, quindi
|
||||||
|
entra direttamente nel conto di §4; e le uscite di SKH01 sono la gamba latency-sensitive.
|
||||||
|
**Perche' non e' morto:** non e' calendario, non e' un overlay, non e' una strategia — e' il
|
||||||
|
prezzo di un rischio operativo gia' materializzato tre volte.
|
||||||
|
|
||||||
|
### 3.3 — 🥉 Lo spread di volatilita' BTC-vs-ETH come sleeve, non come timer
|
||||||
|
**Dati:** `dvol_btc` / `dvol_eth` dal 2021-03 = **1978 giorni, 5,4 anni** — l'unico dataset non
|
||||||
|
direzionale del progetto abbastanza lungo da avere un hold-out vero (a differenza di tutta la
|
||||||
|
catena, ferma a 74-90 giorni). **Meccanismo:** i due asset condividono un fattore di vol comune, e
|
||||||
|
la *ricchezza relativa* delle due vol e' una grandezza stazionaria per costruzione, mentre il
|
||||||
|
livello non lo e'. **Non e' DVOLSPREAD:** quello e' il lead in forward-monitor che usa lo spread
|
||||||
|
come **timer direzionale** (de-risk); qui lo spread si traderebbe **come tale** (straddle lungo su
|
||||||
|
uno, corto sull'altro), market-neutral rispetto alla direzione e — a differenza di VRP01 — con
|
||||||
|
esposizione al premio di vol quasi cancellata fra le due gambe. **Non e' nella lista dei morti**
|
||||||
|
(gamma scalping = long-vol nudo; VRP01 = short-vol nudo gated; questo e' relative-value).
|
||||||
|
⚠️ **Due muri dichiarati in anticipo:** (i) sarebbe prezzato su DVOL, cioe' BS-flat, e la SKEW di
|
||||||
|
questa ondata ha appena misurato che il flat costa **x0,869 di struttura a termine** e **x0,920 di
|
||||||
|
skew** — quindi il verdetto massimo e' LEAD finche' non e' riprezzato sulla catena vera; (ii)
|
||||||
|
richiede opzioni su entrambi gli asset: fuori a $635 (BTC_USDC ~$780/lotto), dentro a ~$3-5k.
|
||||||
|
|
||||||
|
### 3.4 — Il basis dei futures datati come **campione della coda**, non come carry
|
||||||
|
BASIS-CALENDAR ha costruito un dataset che il progetto non aveva — **BTC 34 contratti / 240.150
|
||||||
|
barre orarie dal 2018-09, ETH 34 / 233.334, piu' 64.110 ore di funding e indice** — e lo ha usato
|
||||||
|
per una sola domanda (il premio incassabile, IC95 che contiene lo zero → SCARTATO). **Il dataset
|
||||||
|
contiene la coda che manca a tutto il resto del progetto**, e il ledger lo dice esplicitamente
|
||||||
|
(*«include il deleveraging 2022 — esattamente la coda che a CC01 mancava per costruzione»*).
|
||||||
|
**Uso non esplorato:** il blow-out del basis e' l'unico marcatore osservabile e datato dei giorni
|
||||||
|
di deleveraging forzato. Condizionare a quei giorni le domande di §3.1 e §4 da' un **campione di
|
||||||
|
stress reale** al posto di uno scenario assunto a mano.
|
||||||
|
⚠️ **E c'e' un'azione a costo quasi nullo che nessuno ha messo negli aperti:** quel dataset vive in
|
||||||
|
`.../scratchpad/basis_cache/` (**20 MB**, [VERIFICATO]), cioe' in una cartella di sessione che
|
||||||
|
sparisce. E' ricostruibile (30/30 trimestrali rispondono e il metodo — costruire a mano i nomi dei
|
||||||
|
contratti scaduti e chiamare `get_tradingview_chart_data` — e' documentato nello script), ma
|
||||||
|
ricostruirlo costa rete e tempo che ora sono gia' stati spesi. **Persisterlo in `data/raw/fut_*`
|
||||||
|
e' l'unica cosa dell'intera ondata che si perde per non averla scritta.**
|
||||||
|
|
||||||
|
### 3.5 — La copertura di coda come **abilitante della leva**, non come miglioramento dello Sharpe
|
||||||
|
**Stato dell'arte nel progetto, dichiarato:** tail hedge *e' gia' stato provato* — `OPT06` (ratio
|
||||||
|
put spread con tail hedge) e `OPT07` (collar) nello sweep del 20/06, entrambi **modellati su DVOL
|
||||||
|
BS-flat** e giudicati **sullo Sharpe**, entrambi bocciati. **Cosa non e' stato fatto:** giudicarlo
|
||||||
|
sull'obiettivo **crescita a leva**. Sono domande diverse perche' il valore di un pavimento e'
|
||||||
|
**convesso nella leva**: a k=1 una put lontana e' un costo secco, a k=k\* e' cio' che rende k\*
|
||||||
|
stimabile. La domanda esatta: *esiste una put a delta basso il cui costo annuo, misurato sulle
|
||||||
|
quote VERE (non su DVOL), sia minore del guadagno di crescita che il pavimento autorizza?*
|
||||||
|
⚠️ **Prior onesto: sfavorevole.** Il 30/07 ha misurato che l'ala comprata costa **~2,3x il
|
||||||
|
modello** (f_long 2,23, fino a 5,85 sui delta piu' bassi) — quindi la protezione reale e' piu' cara
|
||||||
|
di quella che aveva gia' bocciato OPT06/OPT07. **Ma il confronto non e' mai stato fatto contro la
|
||||||
|
leva**, ed e' l'unico confronto in cui una protezione cara puo' comunque pagare.
|
||||||
|
⚠️ Eseguibile solo da ~$3-5k (ETH_USDC ~$250/lotto di nozionale): **e' una domanda per il deposito,
|
||||||
|
non per oggi.**
|
||||||
|
|
||||||
|
### 3.6 — Il flusso di opzioni (volume per strike), che e' nel dato e nessuno ha guardato
|
||||||
|
La catena registra `volume_24h` **per strumento**, oraria. Sei filoni hanno usato l'**OI**
|
||||||
|
(posizioni accumulate) e **zero** hanno usato il **volume** (transazioni). ALT-OPTIONS ha appena
|
||||||
|
dimostrato che sulle famiglie lineari OI e negoziabilita' sono **anti-correlati (rho di rango
|
||||||
|
−0,77)** — che e' un'ottima ragione per sospettare che le due grandezze dicano cose diverse anche
|
||||||
|
sulle inverse. **Meccanismo:** un'ondata di acquisti concentrata su uno strike e' un fatto di
|
||||||
|
flusso; l'OI e' il suo integrale, e integrare distrugge il segnale di timing.
|
||||||
|
⚠️ **Muro invalicabile e dichiarato: 74-90 giorni di calendario.** Il verdetto massimo qui e'
|
||||||
|
**LEAD con gate pre-registrato**, e l'onesta' vuole che si dica prima di iniziare, non dopo.
|
||||||
|
La ragione per farlo comunque e' che **il calendario cresce da solo**, gratis, ogni ora.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. LA MISURA DI MAGGIOR VALORE MAI FATTA
|
||||||
|
|
||||||
|
### La misura
|
||||||
|
**Il peggior giorno che il libro puo' avere, misurato invece che assunto: la distribuzione della
|
||||||
|
perdita giornaliera CONDIZIONATA all'esposizione piena, sotto la lente wick accoppiata, con lo
|
||||||
|
scenario di gap attraverso il disaster-SL.**
|
||||||
|
|
||||||
|
### Perche' questa
|
||||||
|
Perche' e' **l'unico parametro che blocca la sola leva gratuita che l'ondata abbia trovato**, e
|
||||||
|
perche' oggi quel parametro e' **un numero scelto a mano**.
|
||||||
|
|
||||||
|
GROWTH-POLICY ha stabilito che il gradino eseguibile `1,00x → 1,25-1,50x` vale
|
||||||
|
**14,7 anni → 12,9-11,6 anni** al capitale-rendita (a €500/mese, netto fisco). Lo scettico ha
|
||||||
|
tolto una delle tre riserve (il wick non amplifica il maxDD: ricarico costante ~3,5%). Delle due
|
||||||
|
rimaste, **una non e' risolvibile** (il drift stimato su 7,4 anni — e comunque k\* resta 7x a −2 SE,
|
||||||
|
quindi 1,5x sta al 21% di k\*) e **una lo e': la coda.** E' la riserva che decide, perche' e' l'unica
|
||||||
|
che porta k\* **sotto** il gradino proposto: *«un solo giorno −10% all'anno porta k\* da ~10x a 2x,
|
||||||
|
−15%/anno a 1x»*.
|
||||||
|
|
||||||
|
📌 **Quel −10% non e' stato misurato: e' stato scelto.** E il controllo di cinque minuti in §3.1
|
||||||
|
dice che potrebbe essere **una coda da buy&hold importata dentro un libro difensivo**: sui cinque
|
||||||
|
peggiori giorni di BTC e i cinque di ETH in 7,4 anni — compreso il **−41,5% / −45,7% del 12 marzo
|
||||||
|
2020** — il libro ha fatto **+0,97 · +0,72 · 0,00 · −1,98 · −2,24 · +0,23%**, e il suo peggior
|
||||||
|
giorno assoluto e' **−3,38%** in una data che non e' un crash. Se il vero peggior giorno
|
||||||
|
plausibile e' −5% e non −10%, k\* non e' 2x e **il gradino e' autorizzato**.
|
||||||
|
|
||||||
|
⚠️ **Ma il mio controllo e' close-only, cioe' proprio la lente che questa ondata ha appena
|
||||||
|
dichiarato "esattamente cieca" sulle regole a UN giorno** (rapporto acc./close = INF a soglia 5%).
|
||||||
|
Quindi **la misura vera non e' quella che ho fatto**: e' la stessa domanda sotto
|
||||||
|
`r0725_prop_coupled` (tuple accoppiate, minimo intra-giorno) piu' lo scenario che il dataset non
|
||||||
|
contiene — un gap notturno che attraversa il disaster-SL −30% con posizione piena su entrambe le
|
||||||
|
gambe. Tutta la macchineria esiste gia' (`r0725_prop_coupled`, `r0822_leverage_skeptic`), il
|
||||||
|
campione di stress si puo' prendere dal dataset dei futures datati di §3.4, e il costo e'
|
||||||
|
**un agente di questa ondata**.
|
||||||
|
|
||||||
|
### Confronto esplicito con le leve gia' quantificate
|
||||||
|
| leva | valore | costo | stato |
|
||||||
|
|---|---|---|---|
|
||||||
|
| versare €250/mese invece di €0 | da **mai** a 19,8a (P 52% netto fisco) | €250/mese | decisa dall'operatore |
|
||||||
|
| versare €500 invece di €250 | 19,8a → **14,7a** | €250/mese in piu' | decisa dall'operatore |
|
||||||
|
| **leva 1,00x → 1,50x** | **14,7a → 11,6a** | **zero** | **bloccata da UN parametro assunto** |
|
||||||
|
| riallocare per la barriera (prop) | +0,016 di J = **il 4%** | analisi | misurata, marginale |
|
||||||
|
| XS01 su conto funded | +0,400 di J | **assunzione non verificata (§1.1)** | LEAD |
|
||||||
|
| ottimizzare il peso di SKH01 | **+0,030 di Sharpe** | analisi | gate fallito, plateau |
|
||||||
|
| rischio venue a p=5% | capitale mediano → **zero** | split di conto | decisa: 100% Deribit fino a $20k |
|
||||||
|
|
||||||
|
📌 **Il gradino di leva vale ~3,1 anni, cioe' l'equivalente di ~€300/mese di versamenti, a costo
|
||||||
|
zero.** E' la prima volta in due mesi che una misura di ricerca entra nell'ordine di grandezza
|
||||||
|
della leva dei versamenti, e ci entra perche' **non e' ricerca di alpha: e' un cambio di scala di
|
||||||
|
cio' che gia' funziona.** Vale **100x** l'ottimizzazione del peso di SKH01 e **25x** il riallocare
|
||||||
|
per la barriera.
|
||||||
|
|
||||||
|
### E adesso la parte che il mandato chiede di dire se e' vera
|
||||||
|
**Sì: nessuna misura di ricerca di questa ondata batte "versare".** Venti filoni, ~15.300 righe,
|
||||||
|
**zero candidati promossi**; il soffitto direzionale e' stato riconfermato per via aritmetica; e
|
||||||
|
per **tre volte** l'eseguibilita' a $600 non e' stata il vincolo binding — tutto e' morto
|
||||||
|
sull'**edge**. La misura che propongo **non e' un'eccezione a questo, ne e' la conferma**: non
|
||||||
|
cerca un rendimento nuovo, **prezza un parametro di rischio per poter usare meglio quello che c'e'**.
|
||||||
|
E anche cosi', 3,1 anni sono **meno** di cio' che valgono €250/mese di differenza nei versamenti.
|
||||||
|
|
||||||
|
**La seconda misura, e va detta insieme alla prima perche' costa ancora meno:** la verifica di
|
||||||
|
§1.1 (i 19 alt HL sono listati e shortabili presso la firm?). Non e' ricerca, e' **un pomeriggio di
|
||||||
|
lettura sul sito di due firm** — e decide se il canale prop, l'unico che moltiplica il nozionale
|
||||||
|
senza possedere capitale, vale J 0,738 o J 0,335. **Il progetto ha gia' pagato una volta il prezzo
|
||||||
|
di non fare questa verifica** (GTAA01/PRIIPs, cinque settimane di misure su un ETF che il broker
|
||||||
|
rifiuta di vendere) e ha scritto la regola per non ripeterlo. **Farla prima di aprire un altro
|
||||||
|
filone prop e' la cosa piu' economica che ci sia in questo elenco.**
|
||||||
|
|
||||||
|
**In una riga:** *misurare la coda invece di assumerla* e' la ricerca che vale di piu' (3,1 anni,
|
||||||
|
gratis); *leggere il listino di una prop firm* e' la verifica che vale di piu' (un canale intero,
|
||||||
|
un pomeriggio); e **nessuna delle due batte i versamenti**, che restano il vincolo binding come il
|
||||||
|
26/07 aveva gia' stabilito.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. AUTOCRITICA DELL'ONDATA
|
||||||
|
|
||||||
|
### 5.1 — 🚨 Un filone su venti non e' nel ledger, ed e' quello con il risultato eseguibile oggi
|
||||||
|
**ALT-OPTIONS (filone 14, `r0822_alt_options.py`, 709 righe) non ha ne' una riga di tabella ne'
|
||||||
|
una sezione in `RESULTS-0822.md`.** [VERIFICATO: 19 sezioni `###`, 18 righe di tabella, nessuna
|
||||||
|
per il 14 — e la tabella ha anche un difetto di markdown che **fonde le righe 9 e 4 su una sola
|
||||||
|
linea** (`... = TP01 travestito || 4 | DEALER-GAMMA | ...`).] Il filone e' citato **due volte** da
|
||||||
|
un altro filone (*«vedi filone 14»*, *«ETH_USDC resta la gamba giusta ... (filone 14)»*) e serve
|
||||||
|
al coordinatore per **ritirare una propria inferenza sbagliata** sull'OI — ma il lettore non trova
|
||||||
|
la fonte da nessuna parte.
|
||||||
|
|
||||||
|
Nel diario compare come **due parole** dentro l'elenco dei morti: *«alt-options (alt)»*. **La
|
||||||
|
sintesi e' sbagliata due volte:** il filone non muore «perche' alt» (la sua conclusione e' che
|
||||||
|
**SOL/XRP/HYPE/AVAX sono i peggiori**: SOL_USDC ha il **9%** dei put quotati a due lati e
|
||||||
|
`f_venue` **negativo**), e cio' che trova e' che **ETH_USDC** — non un alt — e' la gamba con
|
||||||
|
`f_venue = 0,980`, spread della corta al **2,6%**, **max-loss $16,62/lotto** e **3.000 lotti al
|
||||||
|
best bid**. Cioe' §1.7: **la quarta soglia pubblicata falsificata dall'ondata**, quella che tiene
|
||||||
|
VRP01 fuori per taglia — e il diario ne annuncia **tre**.
|
||||||
|
|
||||||
|
**Lezione:** il ledger e' stato scritto *man mano* (lo dice la sua prima riga) e un filone che
|
||||||
|
finisce mentre si scrive la sintesi puo' sparire senza che nulla lo segnali. Serve una guardia
|
||||||
|
banale: **contare i file `r0822_*.py` e le sezioni del ledger, e non chiudere l'ondata se non
|
||||||
|
combaciano.** E' lo stesso principio che l'ondata ha appena raccomandato per i monitor.
|
||||||
|
|
||||||
|
### 5.2 — Il gate `implausible_sharpe` non e' stato girato dove serviva, per la seconda volta
|
||||||
|
MONITOR-AUDIT lo osserva bene: *«`implausible_sharpe` esiste dal 26/07 e non e' mai stato puntato
|
||||||
|
sulle serie forward»* (avrebbe segnalato Sharpe −15,8 su 28 barre). Ma la stessa mancanza si
|
||||||
|
ripete **dentro l'ondata**: il gate non compare in PROP-ALLOC (dove J e P(pass) sono costruiti su
|
||||||
|
percorsi bootstrap, non su una serie reale — legittimo, ma va dichiarato) ne' in GROWTH-POLICY, il
|
||||||
|
cui output include un **capitale mediano di $4,4e12** — che e' letteralmente la firma per cui il
|
||||||
|
gate e' stato scritto. L'agente ha fatto il controllo di plausibilita' **a mano** e l'ha riportato
|
||||||
|
con onesta'; **il gate del progetto non e' stato invocato.** Un gate che si applica a mano e' un
|
||||||
|
gate che un giorno non si applichera'.
|
||||||
|
|
||||||
|
### 5.3 — Due «difetti di dato» diagnosticati per ricostruzione con la sorgente a due minuti di distanza
|
||||||
|
E' il tema di §1.2 e §1.3, e vale come **critica di metodo dell'ondata**, non dei due agenti. Il
|
||||||
|
progetto ha codificato il 07/08: *«una fonte normativa citata in un commento di codice si verifica
|
||||||
|
come un numero»* — nata da una legge citata male in 5 file per settimane. Il 22/08 due filoni
|
||||||
|
dichiarano un difetto in un dato prodotto da `/opt/docker/cerbero-mcp` (**acceso, sulla stessa
|
||||||
|
macchina, e per giunta gia' usato da PythagorasGoal per Hyperliquid**) e da
|
||||||
|
`Adriano/Cerbero-Bite` (su Gitea, ultimo commit di dismissione), **senza aprire ne' l'uno ne'
|
||||||
|
l'altro**. Io li ho aperti entrambi in **meno di dieci minuti**. Il brief e' complice: elenca le
|
||||||
|
dieci trappole statistiche e **non dice «prima di dichiarare un difetto di dato, leggi il codice
|
||||||
|
che quel dato lo produce»**. E' la regola nuova piu' economica che questa ondata possa lasciare.
|
||||||
|
|
||||||
|
### 5.4 — Il coordinatore ha lasciato passare una conclusione debole, e ne ha corretta una propria
|
||||||
|
**Corretta (a merito):** l'inferenza *«BTC_USDC e' praticamente morto»*, ritirata dopo la misura di
|
||||||
|
ALT-OPTIONS, con la ragione scritta accanto. E' il comportamento giusto.
|
||||||
|
**Lasciata passare:** la lezione di ORTHO-SCREEN (§2.1). E' etichettata *«il risultato piu'
|
||||||
|
riusabile dell'ondata»* e *«non e' sfortuna, e' aritmetica»* — due formule che chiudono la
|
||||||
|
discussione — mentre la sua meta' operativa (*«dichiarare famiglie molto piu' piccole in
|
||||||
|
anticipo»*) e' **la manovra che il progetto ha proibito il 30/07** e la sua meta' quantitativa
|
||||||
|
dipende da una variabile (la dispersione delle celle) **mai nominata**. Il segnale che avrebbe
|
||||||
|
dovuto fermarla c'era ed era **nello stesso ledger, sette sezioni piu' avanti**: il DSR vacuo di
|
||||||
|
VOL-SIZE. **Due misure dello stesso gate, versi opposti, stesso giorno, nessun rimando.**
|
||||||
|
La sintesi di venti filoni ha un compito che nessun filone ha: **incrociare**. Qui non l'ha fatto,
|
||||||
|
e non l'ha fatto neanche su §2.4 (leva), §2.5 (peso SKH01), §1.5 (le due gambe del gate 23/10) e
|
||||||
|
§1.6 (k\* contro la partecipazione al nastro) — **cinque incroci disponibili a costo zero, tutti
|
||||||
|
fra coppie di filoni gia' consegnati.**
|
||||||
|
|
||||||
|
### 5.5 — Agenti che hanno dichiarato «non girabile» cio' che era girabile
|
||||||
|
- **FLOW-SQUEEZE**: *«liquidation_long/short: NO ... Nessuna fonte nostra la produce, e non c'e'
|
||||||
|
niente da produrre»*. I due input della regola sono **nel file** (§1.3). Costo del mancato
|
||||||
|
lavoro: ~due ore. Il verdetto non sarebbe cambiato; la **motivazione** si'.
|
||||||
|
- **DEALER-GAMMA**: *«Quale [convenzione], lo decide la sezione 2 (ricostruzione dalla catena),
|
||||||
|
non io»* — decidibile leggendo, in due minuti (§1.2).
|
||||||
|
- **PROP-ALLOC**: la banda d'ancora dichiarata *«non girata, fuori budget»*. Onesto, e la
|
||||||
|
distorsione e' dichiarata a proprio sfavore. **Ma e' l'unica distorsione dichiarata che punta
|
||||||
|
nella direzione della raccomandazione**, sull'unico filone che raccomanda qualcosa: dovrebbe
|
||||||
|
essere il primo pezzo di budget di un seguito, non l'ultimo.
|
||||||
|
- **TERM-STRUCTURE**: *«`deflated_sharpe` NON girato, e la motivazione e' il risultato»* — questo
|
||||||
|
invece e' **corretto e da imitare**: con 74 osservazioni l'incertezza di campione domina di un
|
||||||
|
ordine di grandezza quella da selezione, e deflazionare 312 trial avrebbe dato *«un numero
|
||||||
|
preciso e privo di senso»*. **E' anche la migliore descrizione, scritta senza saperlo, del
|
||||||
|
problema di §2.1.**
|
||||||
|
|
||||||
|
### 5.6 — Cosa l'ondata ha fatto meglio della media del progetto (perche' un'autocritica che elenca
|
||||||
|
solo difetti non e' calibrata)
|
||||||
|
Sette agenti hanno catturato un **errore proprio** prima di pubblicare; tre hanno **refutato una
|
||||||
|
propria ipotesi in corsa**; uno scettico ha **ritirato una regola pubblicata poche ore prima**
|
||||||
|
provando che il difetto non esisteva — e la regola ritirata era **sua di famiglia**. Quattro
|
||||||
|
filoni hanno dichiarato la **potenza prima di guardare** (OI-PIN, FLOW-SQUEEZE, SLIP-AUDIT,
|
||||||
|
VOL-SIZE) e in tre casi il candidato e' morto **contro la propria soglia dichiarata**, che e' il
|
||||||
|
modo piu' pulito di morire. Le repliche **bit-exact prima di ogni delta** sono ormai
|
||||||
|
sistematiche (5/5 in XS-LITE e HL-EXEC, `max|diff| = 0,0`). **Il metodo dei singoli filoni non e'
|
||||||
|
il problema di questa ondata: la cucitura lo e'.**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Appendice — cosa ho verificato in questa sessione, e come
|
||||||
|
|
||||||
|
| # | fatto | come |
|
||||||
|
|---|---|---|
|
||||||
|
| A | `dealer_net_gamma` = convenzione **dichiarata** (dealer short calls / long puts) nel sorgente di `cerbero-mcp`, non un difetto | letto `/opt/docker/cerbero-mcp/src/cerbero_mcp/common/options.py:11-14, 138-140` |
|
||||||
|
| B | bite **inoltra** il campo e non lo calcola; chiama con `top_n_strikes=50` di default | clone `Adriano/Cerbero-Bite`, `runtime/market_snapshot_cycle.py:85`, `clients/deribit.py:342-384` |
|
||||||
|
| C | `liquidation_*_risk` = soglia su `oi_delta_24h` + funding, e **i due input sono nel parquet** (copertura 99%, `oi_delta_pct_4h` std 1,23%) | `sentiment/fetchers.py:480-518` + lettura di `cb_market_snapshots.parquet` |
|
||||||
|
| D | `sr0` del DSR = `sd(Sharpe dei trial) x mult(N)`; con sd=0,58, `sr0` passa da **1,57 (N=168)** a **1,15 (N=24)** | letto `altlib.deflated_sharpe:689-712`, moltiplicatori ricalcolati |
|
||||||
|
| E | il libro 75/25 fa **+0,97%** il 12/03/2020 (BTC −41,5%, ETH −45,7%); peggior giorno **−3,38%**, non un crash | `altlib.tp01_baseline_daily()` + `sleeves._skyhook_returns()`, close-only |
|
||||||
|
| F | `collect_chain.py` interroga `{"currency": asset}` e filtra `OI_MIN = 100.0` | `scripts/live/collect_chain.py:57, 114, 181` |
|
||||||
|
| G | il feed certificato e' `BTC/USD:BTC` (**inverse**), il libro esegue `BTC_USDC-PERPETUAL` (**lineare**) | `rebuild_history.DERIBIT_INSTR:48` vs `src/live/deribit.INSTRUMENT:110` |
|
||||||
|
| H | ALT-OPTIONS: ETH_USDC `f_venue` 0,980 / max-loss **$16,62** / 7 strutture al 20% di $635 | output del filone in scratchpad |
|
||||||
|
| I | il dataset dei futures datati (**20 MB**) vive solo in `scratchpad/basis_cache/` | `du -sh` |
|
||||||
|
| J | `RESULTS-0822.md`: 19 sezioni, 18 righe di tabella, **filone 14 assente**, righe 9 e 4 fuse | conteggio diretto |
|
||||||
|
|
||||||
|
⚠️ **Su (G):** e' un fatto, non un'accusa. `rebuild_history` lo dichiara in un commento
|
||||||
|
(*«inverse, storia lunga ~ lineare entro 3 bps»*) e SLIP-AUDIT ha misurato la base ai 18 fill
|
||||||
|
(BTC −0,03 bps, ETH −0,27) — trascurabile sui **ritorni**, che dipendono dalle *variazioni* della
|
||||||
|
base. Lo segno solo perche' e' la **5ª occorrenza nell'ondata** dello schema «un controllo puntato
|
||||||
|
su uno strumento diverso da quello che si trada» (dopo `fee_watch` 21/08, il collettore della
|
||||||
|
catena, il feed dello slippage e la taratura di `venue_watch`), e perche' la parte non verificata
|
||||||
|
di quel commento — *«~ lineare entro 3 bps»* su **tutta la storia** — non e' mai stata misurata:
|
||||||
|
esiste una misura su 18 istanti, non sulle 60.000 ore in cui entrambi gli strumenti esistono.
|
||||||
|
Costo di chiuderla: un'ora.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Book, pesi, cron, config: questo filone non tocca niente.** Consegna un'analisi.
|
||||||
|
Le uniche azioni che raccomanda sono a costo quasi nullo e tutte su documenti o verifiche:
|
||||||
|
correggere §1.2 nel ledger, aggiungere la sezione mancante di §5.1, dichiarare la dispersione
|
||||||
|
accanto a ogni DSR (§2.1), ri-registrare il gate del 23/10 prima di rigenerare le serie (§1.4-1.5),
|
||||||
|
persistere il dataset dei futures (§3.4), e leggere il listino di una prop firm prima di
|
||||||
|
misurare un'altra volta il canale che ne dipende (§1.1).
|
||||||
@@ -0,0 +1,733 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
"""r0822b_maker.py — l'ESECUZIONE PASSIVA (maker) vale qualcosa per il libro live?
|
||||||
|
|
||||||
|
CONTESTO (fatti gia' nel progetto, non ri-misurati qui)
|
||||||
|
-------------------------------------------------------
|
||||||
|
* Il libro Deribit (TP01 75% + SKH01 25%, nettati su UNA sola posizione per strumento) manda
|
||||||
|
**solo ordini market** -> paga sempre il **taker**. `src/live/execution.py::_submit` ha
|
||||||
|
`order_type="market"` come default e `rebalance_signed` non passa mai altro.
|
||||||
|
* Fee misurate: **taker 3,50 bps/lato** (fee_watch + 16/16 fill sul nastro, 22/08),
|
||||||
|
**maker 1,50 bps/lato** (listino base dal 2026-08-01; fino al 31/07 era 0,00).
|
||||||
|
I backtest del progetto modellano **5,00 bps/lato** (0,10% RT) -> gia' conservativi.
|
||||||
|
* Il progetto ha guardato il lato passivo UNA volta (raccomandazione "T1" del 26/07: il TP di
|
||||||
|
SKH01 come limit resting) e l'ha chiusa **non per la fee** ma perche' un ordine al livello di
|
||||||
|
SKH chiuderebbe anche la quota TP01 (**netting su strumento unico**, `src/live/book.py`).
|
||||||
|
⚠️ QUEL VINCOLO NON SI APPLICA QUI: rendere passivo **l'ordine netto di ribilanciamento** e'
|
||||||
|
UN ordine sulla posizione netta -> niente conflitto fra sleeve. Il netting uccide i bracket
|
||||||
|
per-sleeve, non l'ordine netto. E' la ragione per cui questa domanda e' nuova.
|
||||||
|
* Slippage misurato il 22/08: **~0 a questa taglia** (un fill ha preso il 22% del volume della
|
||||||
|
sua barra 5m, range 1,08 bps) -> il differenziale maker/taker e' fee PURA, e il mezzo spread
|
||||||
|
catturato da un passivo e' <0,005 bps (tick BTC 0,1 su ~$110k; ETH 0,01 su ~$2.500) = ZERO.
|
||||||
|
Percio' qui il passivo NON guadagna spread: guadagna 2,00 bps di fee e rischia il non-fill.
|
||||||
|
* Il disaster-SL −30% e' uno `stop_market` on-book: NON e' toccato da nulla di quanto segue.
|
||||||
|
|
||||||
|
LA DOMANDA CHE NESSUNO HA FATTO
|
||||||
|
-------------------------------
|
||||||
|
Un limit resting non e' un market order piu' economico: **puo' non riempirsi**. E non riempirsi
|
||||||
|
su un segnale di trend significa **perdere il movimento**, che e' esattamente il caso in cui non
|
||||||
|
si e' riempito (il prezzo e' scappato dalla parte giusta). Quindi:
|
||||||
|
costo atteso del passivo = P(fill)*(−2,00 bps) + P(no fill)*E[mossa avversa | no fill]
|
||||||
|
e la seconda parte e' **avversa per costruzione**: un buy limit al riferimento non si riempie se
|
||||||
|
e solo se il prezzo non e' MAI sceso al livello, cioe' e' salito.
|
||||||
|
|
||||||
|
COSA FA QUESTO SCRIPT
|
||||||
|
---------------------
|
||||||
|
1. il differenziale in Sharpe/CAGR di libro, riusando la curva fee gia' pubblicata
|
||||||
|
(`r0726_fee_sensitivity.py`: −0,017 Sharpe/bps, −0,23% CAGR/bps) E ri-misurandola a 4 punti
|
||||||
|
di fee sul path del libro (verifica indipendente, non ri-derivazione della curva);
|
||||||
|
2. ricostruisce gli **ORDINI** del libro live (target netto orario, banda min-order $5) e li
|
||||||
|
confronta con i 18 ordini veri di `data/live/book_executions.jsonl` (controllo di realta');
|
||||||
|
3. per OGNI ordine misura su 5m certificati: P(fill) di un limit al riferimento entro N minuti,
|
||||||
|
e il ritorno condizionato **ai casi in cui NON si riempie**. Confronto **APPAIATO PER
|
||||||
|
ORDINE** (mai per esito: allineare sui riempiti tiene solo i casi in cui non e' successo
|
||||||
|
niente — trappola gia' presa 3 volte in questo progetto);
|
||||||
|
4. trasforma il costo per-ordine in una serie di P&L additiva ai rendimenti giornalieri del
|
||||||
|
libro -> Sharpe/CAGR/maxDD della politica passiva, e il numero in **dollari all'anno**;
|
||||||
|
5. mette il risultato accanto alle altre leve gia' misurate.
|
||||||
|
|
||||||
|
LENTI E SEMPLIFICAZIONI — DICHIARATE (tutte GENEROSE verso il maker, di proposito)
|
||||||
|
---------------------------------------------------------------------------------
|
||||||
|
(a) griglia oraria: il cron gira a :07. Decisione con le barre 5m chiuse fino a h:05, prezzo di
|
||||||
|
riferimento = close della barra 5m etichettata h:00. Verificato sul log vero: gli ordini
|
||||||
|
cadono a :07 di un'ora qualsiasi, e TP01 si aggiorna alle 01:07 UTC.
|
||||||
|
(b) TP01 cambia target una volta al giorno, all'ora 01:00 UTC (lente backtest; l'effetto della
|
||||||
|
barra parziale e' misurato +-0,03 Sharpe, 26/07 -> irrilevante per la TAGLIA degli ordini).
|
||||||
|
(c) SKH01: ingressi alla CHIUSURA del bin 230m (lente backtest), uscite al primo confine orario
|
||||||
|
dopo il tocco 5m (lente live, `_skyhook_positions` itera nel bin parziale). L'ingresso vero
|
||||||
|
del live e' INTRA-BIN, cioe' piu' vicino al breakout -> peggiore per un limit passivo:
|
||||||
|
questa lente e' **generosa** verso il maker.
|
||||||
|
(d) fill: un limit BUY al prezzo P si considera riempito se `low <= P` nella finestra. Ignora la
|
||||||
|
coda del book (al livello si sta DIETRO agli ordini gia' presenti: un semplice tocco non
|
||||||
|
riempie) -> **generosa** verso il maker. Finestra di scansione da h:05 (l'ordine e' a h:07:
|
||||||
|
regaliamo 2 minuti di opportunita' di fill).
|
||||||
|
(e) ordini tutto-o-niente, nessun fill parziale.
|
||||||
|
(f) niente cap per-asset nella ricostruzione (a $635 il cap $318 morde di rado e riduce
|
||||||
|
PROPORZIONALMENTE guadagno e danno: non cambia il segno).
|
||||||
|
|
||||||
|
nice -n 19 timeout 900 uv run python scripts/research/r0822b_maker.py
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[2]
|
||||||
|
sys.path.insert(0, str(ROOT))
|
||||||
|
|
||||||
|
from src.backtest.harness import backtest_signals # noqa: E402
|
||||||
|
from src.data.downloader import load_data # noqa: E402
|
||||||
|
from src.portfolio.portfolio import combine_outer, metrics, to_daily # noqa: E402
|
||||||
|
from src.strategies.skyhook import (LTF_MIN, SKH01_V2_DD, build_frames, # noqa: E402
|
||||||
|
skyhook_entries)
|
||||||
|
from src.strategies.trend_portfolio import (CANONICAL, TrendPortfolio, # noqa: E402
|
||||||
|
resample_1d, simple_returns)
|
||||||
|
|
||||||
|
ASSETS = ("BTC", "ETH")
|
||||||
|
W_TP01, W_SKH = 0.75, 0.25 # pesi del book Deribit (src/live/book.py)
|
||||||
|
WEIGHT = 0.5 # book 50/50 BTC+ETH
|
||||||
|
MIN_ORDER_USD = 5.0 # config/live.json
|
||||||
|
EQUITY_LIVE = 635.0 # conto vero (22/08)
|
||||||
|
|
||||||
|
FEE_TAKER_BPS = 3.50 # misurata sul nastro + fee_watch
|
||||||
|
FEE_MAKER_BPS = 1.50 # listino base dal 2026-08-01
|
||||||
|
FEE_MODEL_BPS = 5.00 # cio' che i backtest modellano
|
||||||
|
FEE_GRID_BPS = (0.0, FEE_MAKER_BPS, FEE_TAKER_BPS, FEE_MODEL_BPS)
|
||||||
|
|
||||||
|
# curva PUBBLICATA (r0726_fee_sensitivity.py) — riusata, non ri-derivata
|
||||||
|
PUB_SH_PER_BPS = -0.017
|
||||||
|
PUB_CAGR_PER_BPS = -0.23 # punti percentuali di CAGR per bps/lato
|
||||||
|
|
||||||
|
HORIZONS_MIN = (5, 15, 30, 60, 120) # attesa massima del limit prima di inseguire
|
||||||
|
# CODA DEL BOOK — il parametro che DECIDE, e che l'OHLC non contiene.
|
||||||
|
# Un limit al livello sta DIETRO agli ordini gia' presenti: "il prezzo ha toccato il mio livello"
|
||||||
|
# NON e' "sono stato riempito". `q` = quanti bps il prezzo deve attraversare OLTRE il livello
|
||||||
|
# perche' la coda davanti sia consumata. q=0 e' il LIMITE SUPERIORE ottimistico (e a q=0 il fill
|
||||||
|
# e' quasi automatico: l'apertura della prima barra di scansione COINCIDE col livello, quindi
|
||||||
|
# `low <= P` chiede solo che il prezzo non scappi istantaneamente).
|
||||||
|
QUEUE_BPS = (0.0, 1.0, 2.0, 5.0, 10.0)
|
||||||
|
P2_MAX_HOURS = 24 # politica "mai inseguire": ri-posta ogni ora, tetto 24h
|
||||||
|
CAPITALS = (635.0, 2000.0, 5000.0)
|
||||||
|
HOLDOUT = pd.Timestamp("2025-01-01", tz="UTC")
|
||||||
|
SEED = 20260822
|
||||||
|
|
||||||
|
|
||||||
|
# ===========================================================================================
|
||||||
|
# 1. SEGNALI — calcolati UNA volta per asset (non dipendono dalla fee)
|
||||||
|
# ===========================================================================================
|
||||||
|
def build_asset(asset: str) -> dict:
|
||||||
|
"""Tutto cio' che serve per l'asset: TP01 daily, path SKH01, griglia 5m e oraria."""
|
||||||
|
df5 = load_data(asset, "5m")
|
||||||
|
ts5 = pd.to_datetime(df5["datetime"], utc=True)
|
||||||
|
A = dict(
|
||||||
|
ts5=ts5.values.astype("datetime64[ns]"),
|
||||||
|
o5=df5["open"].values.astype(float), h5=df5["high"].values.astype(float),
|
||||||
|
l5=df5["low"].values.astype(float), c5=df5["close"].values.astype(float),
|
||||||
|
)
|
||||||
|
|
||||||
|
# --- SKH01: ingressi/uscite sulla griglia 230m, poi tempo di RILEVAZIONE oraria
|
||||||
|
ltf, htf = build_frames(df5)
|
||||||
|
ent = skyhook_entries(ltf, htf, SKH01_V2_DD)
|
||||||
|
H, L = ltf["high"].values, ltf["low"].values
|
||||||
|
ltf_ts = pd.to_datetime(ltf["datetime"], utc=True)
|
||||||
|
n = len(ltf)
|
||||||
|
trades = []
|
||||||
|
i = 0
|
||||||
|
while i < n:
|
||||||
|
e = ent[i]
|
||||||
|
if e is None:
|
||||||
|
i += 1
|
||||||
|
continue
|
||||||
|
d, sl, tp, mb = e["dir"], e["sl"], e["tp"], e["max_bars"]
|
||||||
|
exit_idx = None
|
||||||
|
for s in range(1, mb + 1):
|
||||||
|
j = i + s
|
||||||
|
if j >= n:
|
||||||
|
break
|
||||||
|
hit = (L[j] <= sl or H[j] >= tp) if d == 1 else (H[j] >= sl or L[j] <= tp)
|
||||||
|
if hit or s == mb:
|
||||||
|
exit_idx = j
|
||||||
|
break
|
||||||
|
if exit_idx is None: # trade ancora aperto a fine storia
|
||||||
|
trades.append((i, None, d, sl, tp))
|
||||||
|
break
|
||||||
|
trades.append((i, exit_idx, d, sl, tp))
|
||||||
|
i = exit_idx + 1
|
||||||
|
|
||||||
|
# tempi di rilevazione: ingresso a chiusura bin (lente backtest);
|
||||||
|
# uscita al primo confine orario dopo il TOCCO 5m (lente live)
|
||||||
|
ltf_close = ltf_ts + pd.Timedelta(minutes=LTF_MIN)
|
||||||
|
skh_events = [] # (ts_rilevazione, segno)
|
||||||
|
for (i0, j0, d, sl, tp) in trades:
|
||||||
|
skh_events.append((_ceil_hour(ltf_close.iloc[i0]), int(d)))
|
||||||
|
if j0 is None:
|
||||||
|
continue
|
||||||
|
t_touch = _first_touch_5m(A, ltf_ts.iloc[j0], ltf_close.iloc[j0], d, sl, tp)
|
||||||
|
skh_events.append((_ceil_hour(t_touch), 0))
|
||||||
|
|
||||||
|
# --- TP01: target giornaliero, visibile dall'ora 01:00 del giorno DOPO la chiusura
|
||||||
|
d1 = resample_1d(load_data(asset, "1h"))
|
||||||
|
tp_cfg = TrendPortfolio(**CANONICAL)
|
||||||
|
tgt = tp_cfg.target_series(d1)
|
||||||
|
d1_ts = pd.to_datetime(d1["datetime"], utc=True)
|
||||||
|
tp_events = [(pd.Timestamp(t).normalize() + pd.Timedelta(hours=25), float(v))
|
||||||
|
for t, v in zip(d1_ts, tgt)] # barra del giorno D -> vista alle 01:00 di D+1
|
||||||
|
|
||||||
|
# --- griglia oraria dei momenti di decisione (barre 5m etichettate :00)
|
||||||
|
m = pd.DatetimeIndex(A["ts5"])
|
||||||
|
hmask = (m.minute == 0)
|
||||||
|
A["hours"] = m[hmask].tz_localize("UTC")
|
||||||
|
A["hidx"] = np.flatnonzero(hmask) # indice 5m della barra di riferimento
|
||||||
|
A["ref"] = A["c5"][A["hidx"]] # prezzo di riferimento (close 5m di h:00)
|
||||||
|
A["skh_events"] = skh_events
|
||||||
|
A["tp_events"] = tp_events
|
||||||
|
A["daily_ret"] = simple_returns(d1["close"].values.astype(float))
|
||||||
|
A["daily_tgt"] = tgt
|
||||||
|
A["d1_ts"] = d1_ts
|
||||||
|
A["ltf"] = ltf
|
||||||
|
A["ent"] = ent
|
||||||
|
return A
|
||||||
|
|
||||||
|
|
||||||
|
def _ceil_hour(t: pd.Timestamp) -> pd.Timestamp:
|
||||||
|
return t.ceil("h")
|
||||||
|
|
||||||
|
|
||||||
|
def _first_touch_5m(A: dict, t_open, t_close, d: int, sl: float, tp: float) -> pd.Timestamp:
|
||||||
|
"""Primo istante 5m, dentro il bin 230m d'uscita, in cui SL o TP e' toccato.
|
||||||
|
Se nessuno e' toccato (uscita per max_bars) ritorna la chiusura del bin."""
|
||||||
|
lo = np.searchsorted(A["ts5"], np.datetime64(t_open.tz_localize(None)))
|
||||||
|
hi = np.searchsorted(A["ts5"], np.datetime64(t_close.tz_localize(None)))
|
||||||
|
for k in range(lo, min(hi, len(A["ts5"]))):
|
||||||
|
hit = (A["l5"][k] <= sl or A["h5"][k] >= tp) if d == 1 else \
|
||||||
|
(A["h5"][k] >= sl or A["l5"][k] <= tp)
|
||||||
|
if hit:
|
||||||
|
return pd.Timestamp(A["ts5"][k], tz="UTC") + pd.Timedelta(minutes=5)
|
||||||
|
return t_close
|
||||||
|
|
||||||
|
|
||||||
|
def exposure_path(A: dict) -> pd.DataFrame:
|
||||||
|
"""Esposizione-bersaglio del libro su questo asset, ORA per ORA, in frazione di equity.
|
||||||
|
expo = WEIGHT * (W_TP01*tp_frac + W_SKH*skh_sign) — la formula di src/live/book.py."""
|
||||||
|
hours = A["hours"]
|
||||||
|
tp = pd.Series(0.0, index=hours)
|
||||||
|
for t, v in A["tp_events"]:
|
||||||
|
tp.loc[tp.index >= t] = v
|
||||||
|
sk = pd.Series(0.0, index=hours)
|
||||||
|
for t, v in sorted(A["skh_events"]):
|
||||||
|
sk.loc[sk.index >= t] = v
|
||||||
|
expo = WEIGHT * (W_TP01 * tp.values + W_SKH * sk.values)
|
||||||
|
src = np.where(np.abs(np.diff(sk.values, prepend=0.0)) > 0, "SKH",
|
||||||
|
np.where(np.abs(np.diff(tp.values, prepend=0.0)) > 0, "TP01", ""))
|
||||||
|
return pd.DataFrame(dict(ts=hours, expo=expo, ref=A["ref"], hidx=A["hidx"], src=src))
|
||||||
|
|
||||||
|
|
||||||
|
# ===========================================================================================
|
||||||
|
# 2. ORDINI — banda min-order, esattamente come rebalance_signed
|
||||||
|
# ===========================================================================================
|
||||||
|
def orders_from_path(P: pd.DataFrame, equity: float, min_usd: float = MIN_ORDER_USD) -> pd.DataFrame:
|
||||||
|
"""Ordini che il libro manderebbe: si trada solo se |target-posizione| >= min_usd."""
|
||||||
|
band = min_usd / equity
|
||||||
|
expo = P["expo"].values
|
||||||
|
held = 0.0
|
||||||
|
rows = []
|
||||||
|
for k in range(len(expo)):
|
||||||
|
delta = expo[k] - held
|
||||||
|
if abs(delta) < band:
|
||||||
|
continue
|
||||||
|
rows.append((P["ts"].iloc[k], delta, P["ref"].iloc[k], P["hidx"].iloc[k], P["src"].iloc[k]))
|
||||||
|
held = expo[k]
|
||||||
|
return pd.DataFrame(rows, columns=["ts", "dfrac", "ref", "hidx", "src"])
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# ===========================================================================================
|
||||||
|
# 3. FILL E SELEZIONE AVVERSA
|
||||||
|
# ===========================================================================================
|
||||||
|
def measure_orders(A: dict, O: pd.DataFrame, horizons=HORIZONS_MIN, q_bps: float = 0.0,
|
||||||
|
strict: bool = True) -> dict:
|
||||||
|
"""Per ogni ordine e ogni orizzonte N: il limit e' stato riempito? e se no, quanto e'
|
||||||
|
scappato il prezzo? `q_bps` = attraversamento richiesto oltre il livello (coda del book).
|
||||||
|
|
||||||
|
`strict` E' IL PARAMETRO CHE DECIDE IL SEGNO DI TUTTO IL RISULTATO.
|
||||||
|
strict=False -> `low <= limite`: il prezzo ha TOCCATO il livello. Include il caso in cui
|
||||||
|
la barra APRE esattamente al livello e non ci torna mai: li' non e' stato scambiato
|
||||||
|
NIENTE al nostro prezzo dopo che l'ordine e' arrivato, quindi un passivo in fondo alla
|
||||||
|
coda NON e' riempito. E' un limite superiore FISICAMENTE NON DIFENDIBILE.
|
||||||
|
strict=True -> `low < limite`: il prezzo ha ATTRAVERSATO il livello. E' la condizione
|
||||||
|
minima perche' esista uno scambio al nostro prezzo. Anche cosi' si assume di essere
|
||||||
|
DAVANTI a tutta la coda gia' presente -> resta generosa, ma difendibile."""
|
||||||
|
l5, h5, c5 = A["l5"], A["h5"], A["c5"]
|
||||||
|
nb = len(c5)
|
||||||
|
idx = O["hidx"].values.astype(int)
|
||||||
|
ref = O["ref"].values.astype(float)
|
||||||
|
buy = (O["dfrac"].values > 0)
|
||||||
|
q = q_bps / 1e4
|
||||||
|
out = {}
|
||||||
|
for N in horizons:
|
||||||
|
steps = N // 5
|
||||||
|
filled = np.zeros(len(O), bool)
|
||||||
|
adverse = np.zeros(len(O))
|
||||||
|
for k in range(len(O)):
|
||||||
|
i0 = idx[k] + 1 # barra 5m etichettata h:05 (ordine a h:07: generoso)
|
||||||
|
i1 = min(i0 + steps, nb)
|
||||||
|
P = ref[k]
|
||||||
|
if i0 >= i1:
|
||||||
|
filled[k] = True
|
||||||
|
continue
|
||||||
|
lim = P * (1.0 - q) if buy[k] else P * (1.0 + q)
|
||||||
|
if buy[k]:
|
||||||
|
hit = (l5[i0:i1].min() < lim) if strict else (l5[i0:i1].min() <= lim)
|
||||||
|
else:
|
||||||
|
hit = (h5[i0:i1].max() > lim) if strict else (h5[i0:i1].max() >= lim)
|
||||||
|
filled[k] = hit
|
||||||
|
if not hit:
|
||||||
|
end = c5[i1 - 1]
|
||||||
|
adverse[k] = (end - P) / P if buy[k] else (P - end) / P
|
||||||
|
out[N] = pd.DataFrame(dict(filled=filled, adverse=adverse), index=O.index)
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def measure_p2(A: dict, O: pd.DataFrame, P: pd.DataFrame, max_hours=P2_MAX_HOURS,
|
||||||
|
q_bps: float = 0.0, strict: bool = True) -> pd.DataFrame:
|
||||||
|
"""Politica 'MAI INSEGUIRE': il limit resta e viene RI-POSTATO al nuovo riferimento a ogni
|
||||||
|
cron orario, fino a `max_hours`; solo allora si insegue a market. Il costo e' cumulativo."""
|
||||||
|
l5, h5, c5 = A["l5"], A["h5"], A["c5"]
|
||||||
|
hidx = P["hidx"].values.astype(int)
|
||||||
|
ref = P["ref"].values.astype(float)
|
||||||
|
nb, nh = len(c5), len(hidx)
|
||||||
|
pos_of = {int(v): k for k, v in enumerate(hidx)}
|
||||||
|
q = q_bps / 1e4
|
||||||
|
cycles, adverse, chased = [], [], []
|
||||||
|
for _, r in O.iterrows():
|
||||||
|
k0 = pos_of[int(r["hidx"])]
|
||||||
|
P0 = float(r["ref"]); buy = r["dfrac"] > 0
|
||||||
|
done, adv, cyc = False, 0.0, 0
|
||||||
|
for k in range(k0, min(k0 + max_hours, nh)):
|
||||||
|
Pk = ref[k]
|
||||||
|
lim = Pk * (1.0 - q) if buy else Pk * (1.0 + q)
|
||||||
|
i0 = hidx[k] + 1
|
||||||
|
i1 = min(i0 + 12, nb)
|
||||||
|
if i0 >= i1:
|
||||||
|
break
|
||||||
|
if buy:
|
||||||
|
hit = (l5[i0:i1].min() < lim) if strict else (l5[i0:i1].min() <= lim)
|
||||||
|
else:
|
||||||
|
hit = (h5[i0:i1].max() > lim) if strict else (h5[i0:i1].max() >= lim)
|
||||||
|
cyc = k - k0 + 1
|
||||||
|
if hit:
|
||||||
|
adv = (lim - P0) / P0 if buy else (P0 - lim) / P0
|
||||||
|
done = True
|
||||||
|
break
|
||||||
|
if not done:
|
||||||
|
k = min(k0 + max_hours, nh - 1)
|
||||||
|
adv = (ref[k] - P0) / P0 if buy else (P0 - ref[k]) / P0
|
||||||
|
cycles.append(cyc); adverse.append(adv); chased.append(not done)
|
||||||
|
return pd.DataFrame(dict(cycles=cycles, adverse=adverse, chased=chased), index=O.index)
|
||||||
|
|
||||||
|
|
||||||
|
def per_order_delta(O: pd.DataFrame, M: pd.DataFrame, *, miss_fee_bps: float) -> np.ndarray:
|
||||||
|
"""Delta di P&L in FRAZIONE DI EQUITY DI LIBRO, per ORDINE, del passivo contro il taker.
|
||||||
|
>0 = il passivo guadagna. Confronto APPAIATO PER ORDINE: ogni ordine compare una volta in
|
||||||
|
entrambe le politiche, riempito o no (allineare sui soli riempiti terrebbe i casi in cui
|
||||||
|
non e' successo niente — trappola gia' presa 3 volte in questo progetto)."""
|
||||||
|
absd = O["dfrac"].abs().values
|
||||||
|
filled = M["filled"].values if "filled" in M.columns else np.ones(len(O), bool)
|
||||||
|
fee_bps = np.where(filled, FEE_MAKER_BPS, miss_fee_bps)
|
||||||
|
return (FEE_TAKER_BPS - fee_bps) / 1e4 * absd - M["adverse"].values * absd
|
||||||
|
|
||||||
|
|
||||||
|
def weighted_stats(F: np.ndarray, Ad: np.ndarray, W: np.ndarray) -> dict:
|
||||||
|
"""Tutto PONDERATO PER NOZIONALE (e' il nozionale che paga la fee, non il conteggio).
|
||||||
|
`p_be` = la frazione di fill che il passivo deve raggiungere per PAREGGIARE il taker:
|
||||||
|
p*(-gap) + (1-p)*E[avversa|no fill] = 0 -> p_be = E/(E+gap)
|
||||||
|
E' la forma model-free del risultato: non dipende dall'ipotesi di coda, solo da quanto
|
||||||
|
scappa il prezzo quando il limit resta a terra."""
|
||||||
|
gap = (FEE_TAKER_BPS - FEE_MAKER_BPS) / 1e4
|
||||||
|
wf = W[F].sum() / W.sum() if W.sum() > 0 else 1.0
|
||||||
|
miss_w = W[~F]
|
||||||
|
e_adv = float(np.sum(miss_w * Ad[~F]) / miss_w.sum()) if miss_w.sum() > 0 else 0.0
|
||||||
|
cost = float(np.sum(W * (np.where(F, FEE_MAKER_BPS, FEE_TAKER_BPS) / 1e4 + Ad)) / W.sum())
|
||||||
|
return dict(p_fill=wf, e_adv=e_adv, cost_bps=cost * 1e4,
|
||||||
|
p_be=e_adv / (e_adv + gap) if (e_adv + gap) > 0 else 1.0)
|
||||||
|
|
||||||
|
|
||||||
|
def boot_ci(x: np.ndarray, w: np.ndarray, n: int = 2000, seed: int = SEED) -> tuple:
|
||||||
|
"""IC95% bootstrap della media PONDERATA PER NOZIONALE (in bps del nozionale tradato)."""
|
||||||
|
rng = np.random.default_rng(seed)
|
||||||
|
m = len(x)
|
||||||
|
idx = rng.integers(0, m, size=(n, m))
|
||||||
|
num = x[idx].sum(axis=1)
|
||||||
|
den = w[idx].sum(axis=1)
|
||||||
|
b = num / np.where(den > 0, den, np.nan) * 1e4
|
||||||
|
return float(np.nanpercentile(b, 2.5)), float(np.nanpercentile(b, 97.5))
|
||||||
|
|
||||||
|
|
||||||
|
# ===========================================================================================
|
||||||
|
# 4. LIBRO — rendimenti giornalieri modellati, per innestarci il costo d'esecuzione
|
||||||
|
# ===========================================================================================
|
||||||
|
def book_daily(cache: dict, fee_bps_side: float) -> pd.Series:
|
||||||
|
fs = fee_bps_side / 1e4
|
||||||
|
tp_s, sk_s = {}, {}
|
||||||
|
for a in ASSETS:
|
||||||
|
A = cache[a]
|
||||||
|
r, tgt = A["daily_ret"], A["daily_tgt"]
|
||||||
|
held = np.zeros(len(tgt)); held[1:] = tgt[:-1]
|
||||||
|
net = held * r - fs * np.abs(np.diff(held, prepend=0.0)); net[0] = 0.0
|
||||||
|
tp_s[a] = pd.Series(np.clip(net, -0.99, None), index=A["d1_ts"])
|
||||||
|
m = backtest_signals(A["ltf"], A["ent"], fee_rt=2 * fs, leverage=1.0, asset=a, tf="230m")
|
||||||
|
s = pd.Series(m.equity, index=pd.DatetimeIndex(pd.to_datetime(m.eq_index, utc=True)))
|
||||||
|
sk_s[a] = s.resample("1D").last().ffill().pct_change().dropna()
|
||||||
|
Jt = pd.concat(tp_s, axis=1, join="inner").fillna(0.0)
|
||||||
|
Js = pd.concat(sk_s, axis=1, join="inner").fillna(0.0)
|
||||||
|
tp = to_daily(pd.Series(0.5 * Jt["BTC"].values + 0.5 * Jt["ETH"].values, index=Jt.index))
|
||||||
|
sk = to_daily(pd.Series(0.5 * Js["BTC"].values + 0.5 * Js["ETH"].values, index=Js.index))
|
||||||
|
return combine_outer({"TP01": tp, "SKH01": sk}, {"TP01": W_TP01, "SKH01": W_SKH})
|
||||||
|
|
||||||
|
|
||||||
|
def graft(base: pd.Series, deltas: dict, O_live: dict) -> pd.Series:
|
||||||
|
"""Innesta il delta per-ordine (frazione di equity) nei rendimenti giornalieri del libro."""
|
||||||
|
ser = []
|
||||||
|
for a in ASSETS:
|
||||||
|
ts = pd.DatetimeIndex(O_live[a]["ts"]).tz_convert("UTC").normalize()
|
||||||
|
ser.append(pd.Series(deltas[a], index=ts))
|
||||||
|
add = pd.concat(ser).groupby(level=0).sum()
|
||||||
|
return base.add(add.reindex(base.index).fillna(0.0), fill_value=0.0)
|
||||||
|
|
||||||
|
|
||||||
|
def show(tag: str, d: pd.Series) -> dict:
|
||||||
|
m = metrics(d)
|
||||||
|
h = metrics(d[d.index >= HOLDOUT])
|
||||||
|
print(f" {tag:<36} Sh {m['sharpe']:>6.3f} | HOLD {h['sharpe']:>6.3f} | "
|
||||||
|
f"maxDD {m['maxdd']*100:>5.2f}% | CAGR {m['cagr']*100:>6.2f}%")
|
||||||
|
return dict(sharpe=m["sharpe"], hold=h["sharpe"], maxdd=m["maxdd"], cagr=m["cagr"])
|
||||||
|
|
||||||
|
|
||||||
|
# ===========================================================================================
|
||||||
|
def main() -> None:
|
||||||
|
rng = np.random.default_rng(SEED)
|
||||||
|
print("=" * 104)
|
||||||
|
print(" r0822b — ESECUZIONE PASSIVA (MAKER) SUL LIBRO LIVE:")
|
||||||
|
print(" quanto vale il differenziale, quanto costa non riempirsi")
|
||||||
|
print("=" * 104)
|
||||||
|
print(f" taker {FEE_TAKER_BPS:.2f} bps/lato · maker {FEE_MAKER_BPS:.2f} bps/lato · "
|
||||||
|
f"differenziale {FEE_TAKER_BPS-FEE_MAKER_BPS:.2f} bps/lato · modello backtest {FEE_MODEL_BPS:.2f}")
|
||||||
|
print(" NETTING: il libro ha UNA posizione netta per strumento -> l'ordine di ribilanciamento")
|
||||||
|
print(" e' UNO solo. Renderlo passivo NON e' bloccato dal motivo che uccise T1 il 26/07")
|
||||||
|
print(" (quello uccide i BRACKET per-sleeve, non l'ordine netto). Il disaster-SL -30% resta")
|
||||||
|
print(" uno stop_market on-book e non e' toccato da niente di quanto segue.")
|
||||||
|
|
||||||
|
cache = {a: build_asset(a) for a in ASSETS}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- 1. il differenziale
|
||||||
|
print("\n" + "-" * 104)
|
||||||
|
print(" 1. QUANTO VALE IL DIFFERENZIALE (limite superiore: TUTTO il flusso diventa maker,")
|
||||||
|
print(" zero mancati riempimenti — cioe' il numero che NON si puo' superare)")
|
||||||
|
print("-" * 104)
|
||||||
|
curve = {bps: show(f"libro @ {bps:.2f} bps/lato", book_daily(cache, bps)) for bps in FEE_GRID_BPS}
|
||||||
|
gap = FEE_TAKER_BPS - FEE_MAKER_BPS
|
||||||
|
dsh = curve[FEE_MAKER_BPS]["sharpe"] - curve[FEE_TAKER_BPS]["sharpe"]
|
||||||
|
dcg = (curve[FEE_MAKER_BPS]["cagr"] - curve[FEE_TAKER_BPS]["cagr"]) * 100
|
||||||
|
print(f"\n misurato qui : dSharpe {dsh:+.4f} dCAGR {dcg:+.3f} pp "
|
||||||
|
f"({dsh/gap:+.4f} Sh/bps, {dcg/gap:+.3f} pp/bps)")
|
||||||
|
print(f" curva 26/07 : dSharpe {-PUB_SH_PER_BPS*gap:+.4f} dCAGR {-PUB_CAGR_PER_BPS*gap:+.3f} pp "
|
||||||
|
f"({-PUB_SH_PER_BPS:+.4f} Sh/bps, {-PUB_CAGR_PER_BPS:+.3f} pp/bps) <- replica indipendente OK")
|
||||||
|
print(" in DOLLARI all'anno, se tutto il flusso fosse maker:")
|
||||||
|
for cap, note in ((EQUITY_LIVE, "conto vero oggi"), (5000.0, "dopo i versamenti"),
|
||||||
|
(258338.0, "capitale-rendita, muro 07/08")):
|
||||||
|
print(f" ${cap:>9,.0f} -> ${cap*dcg/100:>9,.2f}/anno ({note})")
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- 2. gli ordini
|
||||||
|
print("\n" + "-" * 104)
|
||||||
|
print(" 2. GLI ORDINI DEL LIBRO (target netto orario + banda min-order $5, formula di book.py)")
|
||||||
|
print("-" * 104)
|
||||||
|
paths = {a: exposure_path(cache[a]) for a in ASSETS}
|
||||||
|
yrs = (paths["BTC"]["ts"].iloc[-1] - paths["BTC"]["ts"].iloc[0]).days / 365.25
|
||||||
|
print(f" {'capitale':>9} {'banda':>7} |" + "".join(f"{a+' ord/anno':>15}{'turnover/anno':>16}" for a in ASSETS))
|
||||||
|
ORD = {}
|
||||||
|
for cap in CAPITALS:
|
||||||
|
line, tot = "", {}
|
||||||
|
for a in ASSETS:
|
||||||
|
O = orders_from_path(paths[a], cap)
|
||||||
|
tot[a] = O
|
||||||
|
line += f"{len(O)/yrs:>15.1f}{O['dfrac'].abs().sum()/yrs*cap:>15,.0f}$"
|
||||||
|
ORD[cap] = tot
|
||||||
|
print(f" ${cap:>8,.0f} {MIN_ORDER_USD/cap*100:>6.2f}% |" + line)
|
||||||
|
O_live = ORD[EQUITY_LIVE]
|
||||||
|
n_live = sum(len(O_live[a]) for a in ASSETS)
|
||||||
|
turn = sum(O_live[a]["dfrac"].abs().sum() for a in ASSETS) / yrs
|
||||||
|
print(f"\n ricostruzione a ${EQUITY_LIVE:,.0f}: {n_live} ordini su {yrs:.1f} anni = "
|
||||||
|
f"{n_live/yrs:.1f}/anno di libro ({n_live/yrs/2:.1f} per asset), "
|
||||||
|
f"turnover {turn:.1f}x equity/anno")
|
||||||
|
print(f" coerenza col punto 1: {turn:.1f}x * {gap:.2f} bps = {turn*gap/1e4*100:.3f} pp di CAGR "
|
||||||
|
f"contro i {dcg:.3f} pp misurati sul libro modellato "
|
||||||
|
f"({turn*gap/1e4*100/dcg*100:.0f}% — la ricostruzione tiene meno turnover del modello)")
|
||||||
|
exe = ROOT / "data" / "live" / "book_executions.jsonl"
|
||||||
|
if exe.exists():
|
||||||
|
rows = [json.loads(l) for l in exe.read_text().splitlines() if l.strip()]
|
||||||
|
gg = max((pd.Timestamp(rows[-1]["ts_utc"]) - pd.Timestamp(rows[0]["ts_utc"])).days, 1)
|
||||||
|
print(f" controllo di realta': {len(rows)} ordini VERI in {gg} giorni = "
|
||||||
|
f"{len(rows)/gg*365.25:.0f}/anno di libro. Stesso ordine di grandezza; la finestra e'")
|
||||||
|
print(" troppo corta per una stima (18 ordini), serve solo a escludere un errore di fattore 10.")
|
||||||
|
for a in ASSETS:
|
||||||
|
v = O_live[a]["src"].value_counts()
|
||||||
|
print(f" {a}: origine " + " ".join(f"{(k or 'drift')} {n}" for k, n in v.items()))
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- 3. il non-fill
|
||||||
|
print("\n" + "-" * 104)
|
||||||
|
print(" 3. IL COSTO DEL MANCATO RIEMPIMENTO (limit al riferimento, APPAIATO PER ORDINE)")
|
||||||
|
print("-" * 104)
|
||||||
|
print(" Il parametro che decide NON e' l'orizzonte d'attesa: e' cosa si chiama 'riempito'.")
|
||||||
|
print(" Un limit al livello sta DIETRO a tutta la coda gia' presente, quindi:")
|
||||||
|
print(" TOCCO (low <= limite) -> include la barra che APRE al livello e non ci")
|
||||||
|
print(" torna mai: al nostro prezzo non e' stato scambiato")
|
||||||
|
print(" NIENTE. Fisicamente non difendibile: LIMITE SUPERIORE.")
|
||||||
|
print(" ATTRAVERSAMENTO (low < limite) -> condizione MINIMA perche' uno scambio al nostro")
|
||||||
|
print(" prezzo esista. Assume ancora di essere davanti a")
|
||||||
|
print(" tutta la coda -> generosa, ma difendibile.")
|
||||||
|
print(" q bps in piu' -> quanto il prezzo deve andare OLTRE perche' la coda")
|
||||||
|
print(" davanti a noi sia consumata. L'OHLC non contiene la")
|
||||||
|
print(" profondita' del book: `q` si dichiara, non si stima.")
|
||||||
|
print(" E la colonna che decide e' `serve`: la frazione di fill necessaria per PAREGGIARE il")
|
||||||
|
print(" taker = E[avversa|no fill]/(E[avversa|no fill] + 2,00 bps). Model-free.")
|
||||||
|
print(f"\n {'ipotesi':>10} {'N(min)':>7} | {'P(fill)':>8} {'serve':>8} {'esito':>6} | {'E[avv|no]':>10}"
|
||||||
|
f" {'peggio':>8} | {'costo maker':>12} | {'vs taker':>9} | {'IC95 del delta':>22}"
|
||||||
|
f" | {'ordini vinti':>12}")
|
||||||
|
grid_cells = 6 # le 6 celle della griglia fine calcolate piu' sotto
|
||||||
|
tables = {}
|
||||||
|
variants = [("TOCCO <=", 0.0, False)] + [(f"attrav. q={q:.0f}", q, True) for q in QUEUE_BPS]
|
||||||
|
for lab, q, strict in variants:
|
||||||
|
MEAS = {a: measure_orders(cache[a], O_live[a], q_bps=q, strict=strict) for a in ASSETS}
|
||||||
|
tables[(q, strict)] = MEAS
|
||||||
|
for N in HORIZONS_MIN:
|
||||||
|
grid_cells += 1
|
||||||
|
F = np.concatenate([MEAS[a][N]["filled"].values for a in ASSETS])
|
||||||
|
Ad = np.concatenate([MEAS[a][N]["adverse"].values for a in ASSETS])
|
||||||
|
W = np.concatenate([O_live[a]["dfrac"].abs().values for a in ASSETS])
|
||||||
|
D = np.concatenate([per_order_delta(O_live[a], MEAS[a][N], miss_fee_bps=FEE_TAKER_BPS)
|
||||||
|
for a in ASSETS])
|
||||||
|
S = weighted_stats(F, Ad, W)
|
||||||
|
lo, hi = boot_ci(D, W)
|
||||||
|
ok = "SI" if S["p_fill"] >= S["p_be"] else "no"
|
||||||
|
print(f" {lab:>10} {N:>7} | {S['p_fill']*100:>7.1f}% {S['p_be']*100:>7.1f}% {ok:>6} | "
|
||||||
|
f"{S['e_adv']*1e4:>8.0f}b {(Ad[~F].max()*1e4 if (~F).any() else 0):>7.0f}"
|
||||||
|
f" | {S['cost_bps']:>8.2f} bps | {S['cost_bps']-FEE_TAKER_BPS:>+8.2f} | "
|
||||||
|
f"[{lo:>+7.2f},{hi:>+7.2f}] bps | {(D>0).mean()*100:>11.1f}%")
|
||||||
|
print()
|
||||||
|
|
||||||
|
# -- la sintesi che non dipende dalle ipotesi di coda
|
||||||
|
print(" SINTESI — IL SEGNO DEL RISULTATO STA TUTTO IN UNA DISUGUAGLIANZA STRETTA:")
|
||||||
|
print(f" {'N(min)':>7} | {'TOCCO <= (limite sup.)':>28} | {'ATTRAVERSAMENTO < (minimo difendibile)':>42}")
|
||||||
|
for N in HORIZONS_MIN:
|
||||||
|
row = []
|
||||||
|
for key in ((0.0, False), (0.0, True)):
|
||||||
|
F = np.concatenate([tables[key][a][N]["filled"].values for a in ASSETS])
|
||||||
|
Ad = np.concatenate([tables[key][a][N]["adverse"].values for a in ASSETS])
|
||||||
|
W = np.concatenate([O_live[a]["dfrac"].abs().values for a in ASSETS])
|
||||||
|
S = weighted_stats(F, Ad, W)
|
||||||
|
keep = (FEE_TAKER_BPS - S["cost_bps"]) / (FEE_TAKER_BPS - FEE_MAKER_BPS)
|
||||||
|
row.append(f"P(fill) {S['p_fill']*100:>5.1f}% resta {FEE_TAKER_BPS-S['cost_bps']:>+5.2f} bps"
|
||||||
|
f" = {keep*100:>6.1f}%")
|
||||||
|
print(f" {N:>7} | {row[0]:>28} | {row[1]:>42}")
|
||||||
|
print(" Fra le due colonne cambia UNA cosa: se la barra che apre esattamente al nostro")
|
||||||
|
print(" prezzo, e da li' se ne va, conta come riempimento. Non conta: al nostro prezzo non")
|
||||||
|
print(" e' stato scambiato niente dopo che l'ordine e' arrivato. Con la definizione fisica")
|
||||||
|
print(" il passivo PERDE gia' prima di aggiungere un solo bps di coda.")
|
||||||
|
# griglia FINE vicino a zero: dove sta il salto? (calcolata qui, non citata a memoria)
|
||||||
|
print("\n griglia FINE a N=60 (attraversamento), per vedere dove sta il salto:")
|
||||||
|
fine = []
|
||||||
|
for q in (0.0, 0.005, 0.05, 0.25, 0.5, 1.0):
|
||||||
|
num = den = wf = 0.0
|
||||||
|
for a in ASSETS:
|
||||||
|
M = measure_orders(cache[a], O_live[a], horizons=(60,), q_bps=q, strict=True)[60]
|
||||||
|
w = O_live[a]["dfrac"].abs().values
|
||||||
|
F = M["filled"].values
|
||||||
|
num += np.sum(w * (np.where(F, FEE_MAKER_BPS, FEE_TAKER_BPS) / 1e4 + M["adverse"].values))
|
||||||
|
den += w.sum(); wf += w[F].sum()
|
||||||
|
fine.append((q, wf / den, num / den * 1e4))
|
||||||
|
print(" " + " ".join(f"q={q:<5.3f} P(fill) {f*100:>4.1f}% costo {c:>5.2f}" for q, f, c in fine))
|
||||||
|
Ft = np.concatenate([tables[(0.0, False)][a][60]["filled"].values for a in ASSETS])
|
||||||
|
At = np.concatenate([tables[(0.0, False)][a][60]["adverse"].values for a in ASSETS])
|
||||||
|
Wt = np.concatenate([O_live[a]["dfrac"].abs().values for a in ASSETS])
|
||||||
|
touch60 = weighted_stats(Ft, At, Wt)["cost_bps"]
|
||||||
|
print(f" Il salto e' TUTTO al primo passo (tocco {touch60:.2f} -> attraversamento sub-tick "
|
||||||
|
f"{fine[1][2]:.2f} bps): non e' la CODA a decidere, e' la DISUGUAGLIANZA. Poi il costo")
|
||||||
|
print(f" sale piano ({fine[-1][2]:.2f} a q=1 bps) e non torna mai sotto il taker "
|
||||||
|
f"({FEE_TAKER_BPS:.2f}): non esiste un q positivo di pareggio.")
|
||||||
|
|
||||||
|
# -- decomposizione: chi paga il non-fill?
|
||||||
|
q0, N0 = 2.0, 60
|
||||||
|
MEAS = tables[(q0, True)]
|
||||||
|
print(f" DECOMPOSIZIONE ad attraversamento q={q0:.0f} bps, N={N0} min — non tutti gli ordini sono uguali:")
|
||||||
|
print(f" {'gruppo':<22}{'n':>6}{'P(fill)':>10}{'E[avversa] su TUTTI':>22}{'E[avv|no fill]':>17}")
|
||||||
|
groups = {}
|
||||||
|
for a in ASSETS:
|
||||||
|
O = O_live[a]; M = MEAS[a][N0]
|
||||||
|
groups.setdefault("SKH (breakout)", []).append((M, (O["src"] == "SKH").values))
|
||||||
|
groups.setdefault("TP01 (drift 01:00)", []).append((M, (O["src"] == "TP01").values))
|
||||||
|
groups.setdefault("aumenta esposizione", []).append((M, _increases(O)))
|
||||||
|
groups.setdefault("riduce esposizione", []).append((M, ~_increases(O)))
|
||||||
|
for lab, parts in groups.items():
|
||||||
|
F = np.concatenate([m["filled"].values[k] for m, k in parts])
|
||||||
|
Ad = np.concatenate([m["adverse"].values[k] for m, k in parts])
|
||||||
|
if not len(F):
|
||||||
|
continue
|
||||||
|
print(f" {lab:<22}{len(F):>6}{F.mean()*100:>9.1f}%{Ad.mean()*1e4:>19.1f} bps"
|
||||||
|
f"{(Ad[~F].mean()*1e4 if (~F).any() else 0):>13.0f} bps")
|
||||||
|
print(" NB: un non-fill che RIDUCE l'esposizione lascia la posizione aperta piu' a lungo —")
|
||||||
|
print(" e' rischio, non solo prezzo, e la media in bps non lo mostra.")
|
||||||
|
|
||||||
|
# -- placebo
|
||||||
|
print(f"\n PLACEBO a q={q0:.0f}, N={N0} — stesse taglie a ORE CASUALI (quanto pesa il")
|
||||||
|
print(" condizionamento sul segnale? se il placebo e' uguale, il segnale non seleziona nulla):")
|
||||||
|
grid_cells += 2
|
||||||
|
for a in ASSETS:
|
||||||
|
P = paths[a]
|
||||||
|
k = rng.integers(1, len(P) - 30, size=len(O_live[a]))
|
||||||
|
Of = pd.DataFrame(dict(ts=P["ts"].values[k], dfrac=O_live[a]["dfrac"].values,
|
||||||
|
ref=P["ref"].values[k], hidx=P["hidx"].values[k], src=""))
|
||||||
|
Mf = measure_orders(cache[a], Of, horizons=(N0,), q_bps=q0, strict=True)[N0]
|
||||||
|
Mr = MEAS[a][N0]
|
||||||
|
print(f" {a}: reale P(fill) {Mr['filled'].mean()*100:>5.1f}% "
|
||||||
|
f"E[avversa su tutti] {Mr['adverse'].mean()*1e4:>6.1f} bps"
|
||||||
|
f" | placebo P(fill) {Mf['filled'].mean()*100:>5.1f}% "
|
||||||
|
f"E[avversa] {Mf['adverse'].mean()*1e4:>6.1f} bps")
|
||||||
|
|
||||||
|
# -- politica "mai inseguire"
|
||||||
|
print(f"\n POLITICA 'MAI INSEGUIRE' a q={q0:.0f} bps (ri-posta ogni ora, tetto {P2_MAX_HOURS}h):")
|
||||||
|
grid_cells += 1
|
||||||
|
P2 = {a: measure_p2(cache[a], O_live[a], paths[a], q_bps=q0, strict=True) for a in ASSETS}
|
||||||
|
c2 = np.concatenate([P2[a]["cycles"].values for a in ASSETS])
|
||||||
|
a2 = np.concatenate([P2[a]["adverse"].values for a in ASSETS])
|
||||||
|
w2 = np.concatenate([O_live[a]["dfrac"].abs().values for a in ASSETS])
|
||||||
|
ch2 = np.concatenate([P2[a]["chased"].values for a in ASSETS])
|
||||||
|
cost2 = (np.sum(w2 * (FEE_MAKER_BPS / 1e4 + a2)) / w2.sum()) * 1e4
|
||||||
|
print(f" cicli orari mediana {np.median(c2):.0f} p90 {np.percentile(c2,90):.0f} "
|
||||||
|
f"max {c2.max():.0f} | mai riempito entro {P2_MAX_HOURS}h: {ch2.mean()*100:.1f}%")
|
||||||
|
contrib = w2 * a2
|
||||||
|
top5 = np.sort(contrib)[-5:].sum() / contrib.sum() if contrib.sum() > 0 else 0.0
|
||||||
|
print(f" avversa: mediana {np.median(a2)*1e4:+.1f} bps p90 {np.percentile(a2,90)*1e4:+.1f} "
|
||||||
|
f"MEDIA {a2.mean()*1e4:+.1f} peggiore {a2.max()*1e4:+.0f}")
|
||||||
|
print(f" -> costo {cost2:.2f} bps/lato ({cost2-FEE_TAKER_BPS:+.2f} contro il taker), e "
|
||||||
|
f"i 5 ordini peggiori su {len(a2)} fanno il {top5*100:.0f}% del danno totale.")
|
||||||
|
print(" E' la FIRMA DELLA TRONCATURA, vista dall'altra parte: il passivo vince PICCOLO e")
|
||||||
|
print(" SPESSO (mediana favorevole) e perde GROSSO e di RADO. Stessa forma del profit-take")
|
||||||
|
print(" di VRP01 (30/07), dove una regola che scattava sull'86-91% dei vincenti non era")
|
||||||
|
print(" protezione ma troncatura. Qui la media e' il numero che decide, non la mediana.")
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- 4. impatto sul libro
|
||||||
|
print("\n" + "-" * 104)
|
||||||
|
print(" 4. IMPATTO SUL LIBRO (delta per-ordine innestato nei rendimenti giornalieri)")
|
||||||
|
print("-" * 104)
|
||||||
|
base = book_daily(cache, FEE_TAKER_BPS)
|
||||||
|
b = show("TAKER 3,50 bps = il libro di oggi", base)
|
||||||
|
res = {}
|
||||||
|
for q, strict in [(0.0, False), (0.0, True), (1.0, True), (2.0, True), (5.0, True)]:
|
||||||
|
for N in (5, 60, 120):
|
||||||
|
alt = graft(base, {a: per_order_delta(O_live[a], tables[(q, strict)][a][N],
|
||||||
|
miss_fee_bps=FEE_TAKER_BPS) for a in ASSETS}, O_live)
|
||||||
|
tag = "TOCCO<=" if not strict else f"attrav q={q:.0f}"
|
||||||
|
m = show(f"MAKER {tag:>10} N={N:>3} min, poi market", alt)
|
||||||
|
res[(tag, N)] = m
|
||||||
|
alt2 = graft(base, {a: per_order_delta(O_live[a], P2[a], miss_fee_bps=FEE_MAKER_BPS)
|
||||||
|
for a in ASSETS}, O_live)
|
||||||
|
m2 = show(f"MAKER 'mai inseguire' q={q0:.0f} bps", alt2)
|
||||||
|
|
||||||
|
print("\n delta contro il baseline TAKER (a $635 di conto vero):")
|
||||||
|
for (q, N), m in res.items():
|
||||||
|
print(f" {q:>10} N={N:>3} dSharpe {m['sharpe']-b['sharpe']:+.4f} "
|
||||||
|
f"dHOLD {m['hold']-b['hold']:+.4f} dCAGR {(m['cagr']-b['cagr'])*100:+.3f} pp "
|
||||||
|
f"dmaxDD {(m['maxdd']-b['maxdd'])*100:+.2f} pp -> ${EQUITY_LIVE*(m['cagr']-b['cagr']):+.2f}/anno")
|
||||||
|
print(f" {'mai-inseg.':>10} dSharpe {m2['sharpe']-b['sharpe']:+.4f} "
|
||||||
|
f"dHOLD {m2['hold']-b['hold']:+.4f} dCAGR {(m2['cagr']-b['cagr'])*100:+.3f} pp "
|
||||||
|
f"dmaxDD {(m2['maxdd']-b['maxdd'])*100:+.2f} pp -> ${EQUITY_LIVE*(m2['cagr']-b['cagr']):+.2f}/anno")
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- 5. robustezza
|
||||||
|
print("\n" + "-" * 104)
|
||||||
|
print(" 5. ROBUSTEZZA (banda RIDOTTA: gli ordini spostati di -1h/0/+1h. NON e' la banda a 24")
|
||||||
|
print(" ancore: qui si sposta il momento dell'ORDINE, non l'ancora della strategia)")
|
||||||
|
print("-" * 104)
|
||||||
|
grid_cells += 3
|
||||||
|
for sh in (-1, 0, 1):
|
||||||
|
num, den = 0.0, 0.0
|
||||||
|
for a in ASSETS:
|
||||||
|
P = paths[a]
|
||||||
|
pos = {int(v): k for k, v in enumerate(P["hidx"].values)}
|
||||||
|
kk = np.array([min(max(pos[int(h)] + sh, 0), len(P) - 1) for h in O_live[a]["hidx"]])
|
||||||
|
Osh = pd.DataFrame(dict(ts=P["ts"].values[kk], dfrac=O_live[a]["dfrac"].values,
|
||||||
|
ref=P["ref"].values[kk], hidx=P["hidx"].values[kk], src=""))
|
||||||
|
M = measure_orders(cache[a], Osh, horizons=(N0,), q_bps=q0, strict=True)[N0]
|
||||||
|
w = Osh["dfrac"].abs().values
|
||||||
|
num += np.sum(w * (np.where(M["filled"].values, FEE_MAKER_BPS, FEE_TAKER_BPS) / 1e4
|
||||||
|
+ M["adverse"].values))
|
||||||
|
den += w.sum()
|
||||||
|
print(f" ordini spostati di {sh:+d}h costo maker {num/den*1e4:>6.2f} bps/lato "
|
||||||
|
f"({num/den*1e4-FEE_TAKER_BPS:+.2f} contro il taker)")
|
||||||
|
print(" -1h e' informativo: sposta l'ordine PRIMA che il segnale sia visibile, cioe' verso")
|
||||||
|
print(" dove sta l'ingresso VERO di SKH01 (intra-bin, piu' vicino al breakout della lente")
|
||||||
|
print(" usata qui). La lente di questo script e' GENEROSA verso il maker anche per questo.")
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- 6. ordini di grandezza
|
||||||
|
print("\n" + "-" * 104)
|
||||||
|
print(" 6. LA DOMANDA STRUTTURALE — la leva accanto alle altre gia' misurate nel progetto")
|
||||||
|
print("-" * 104)
|
||||||
|
bq, bN = max(res, key=lambda k: res[k]["sharpe"])
|
||||||
|
bm = res[(bq, bN)]
|
||||||
|
print(f" maker, LIMITE SUPERIORE (0 non-fill) dSharpe {dsh:+.4f} "
|
||||||
|
f"${EQUITY_LIVE*dcg/100:>+7.2f}/anno a ${EQUITY_LIVE:,.0f} ${5000*dcg/100:>+7.2f} a $5.000")
|
||||||
|
print(f" maker, migliore cella misurata ({bq},N={bN}) dSharpe {bm['sharpe']-b['sharpe']:+.4f} "
|
||||||
|
f"${EQUITY_LIVE*(bm['cagr']-b['cagr']):>+7.2f}/anno a ${EQUITY_LIVE:,.0f}")
|
||||||
|
worst = min(res, key=lambda k: res[k]["sharpe"])
|
||||||
|
print(f" maker, peggiore cella misurata ({worst[0]},N={worst[1]}) "
|
||||||
|
f"dSharpe {res[worst]['sharpe']-b['sharpe']:+.4f} "
|
||||||
|
f"${EQUITY_LIVE*(res[worst]['cagr']-b['cagr']):>+7.2f}/anno")
|
||||||
|
print(f" peso di SKH01 ottimizzato dSharpe +0.0300 GATE FALLITO (26/07)")
|
||||||
|
print(f" banda d'ancora del libro (FULL) dSharpe +-0.20 (26/07, 2000 estrazioni)")
|
||||||
|
print(f" banda d'ancora del libro (HOLD-OUT) 2.36 canonico -> 1.54 onesto (26/07)")
|
||||||
|
print(f" raddoppio del taker (regola 26/07) dSharpe -0.08 'non si tocca nulla'")
|
||||||
|
print(f" versare 250 EUR/mese invece di 0 da MAI a 16-20 anni (26-27/07)")
|
||||||
|
# -- rischio operativo: quanti ordini resterebbero APERTI al giro successivo del cron
|
||||||
|
print("\n RISCHIO OPERATIVO (non e' un numero di rendimento, e' un modo nuovo di rompersi):")
|
||||||
|
for key, lab in (((0.0, True), "attraversamento q=0"), ((2.0, True), "attraversamento q=2")):
|
||||||
|
F = np.concatenate([tables[key][a][60]["filled"].values for a in ASSETS])
|
||||||
|
print(f" {lab:<22} N=60 min: {(1-F.mean())*100:>4.1f}% degli ordini ancora APERTI al giro")
|
||||||
|
print(f" {'':<22} successivo = {(1-F.mean())*n_live/yrs:>4.1f} volte l'anno in cui l'esecutore")
|
||||||
|
print(f" {'':<22} deve cancellare-e-rimpiazzare correttamente, su un conto vero.")
|
||||||
|
print(" Oggi `rebalance_signed` manda market e finisce: nessun ordine sopravvive al giro.")
|
||||||
|
print(" Un limit resting introduce ordini orfani e doppi fill, e il libro ha gia' pagato")
|
||||||
|
print(" il prezzo di un percorso che si rompe in silenzio (`fresh_5m`, 26-29/07).")
|
||||||
|
|
||||||
|
print("\n " + "=" * 100)
|
||||||
|
print(" VERDETTO: SCARTATO. Il differenziale c'e' (2,00 bps/lato = +0,033 Sharpe = $2,96/anno")
|
||||||
|
print(" a $635) ma e' il TETTO, e la selezione avversa se lo mangia tutto gia' alla condizione")
|
||||||
|
print(" di riempimento piu' generosa che sia fisicamente difendibile. Il segno del risultato")
|
||||||
|
print(" dipende da una disuguaglianza stretta (`<=` contro `<`), il che e' un altro modo di")
|
||||||
|
print(" dire che l'effetto e' dentro il rumore: l'IC95 del delta contiene lo zero in 10 celle")
|
||||||
|
print(" su 10 fra tocco e attraversamento-q=0. E il premio massimo immaginabile ($2,96/anno)")
|
||||||
|
print(" e' un ordine di grandezza sotto la leva piu' piccola gia' respinta dal progetto.")
|
||||||
|
print(" " + "=" * 100)
|
||||||
|
|
||||||
|
print(f"\n GRIGLIA DICHIARATA: {grid_cells} celle valutate "
|
||||||
|
f"(({len(QUEUE_BPS)}q attrav. + 1 tocco) x {len(HORIZONS_MIN)}N + placebo x2 + "
|
||||||
|
f"mai-inseguire + 3 shift + 6 celle di griglia fine) "
|
||||||
|
f"+ {len(FEE_GRID_BPS)} punti di fee.")
|
||||||
|
print("=" * 104)
|
||||||
|
|
||||||
|
|
||||||
|
def _increases(O: pd.DataFrame) -> np.ndarray:
|
||||||
|
"""True se l'ordine AUMENTA il valore assoluto dell'esposizione (entrata/incremento)."""
|
||||||
|
d = O["dfrac"].values
|
||||||
|
held = np.zeros(len(d))
|
||||||
|
acc = 0.0
|
||||||
|
for i, x in enumerate(d):
|
||||||
|
held[i] = acc
|
||||||
|
acc += x
|
||||||
|
return np.abs(held + d) > np.abs(held)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,716 @@
|
|||||||
|
"""SURFACE-RV — la superficie di volatilita' come RELATIVE VALUE fra i suoi punti.
|
||||||
|
|
||||||
|
IPOTESI (registrata prima di misurare): la superficie Deribit e' internamente INCOERENTE in modo
|
||||||
|
misurabile — strike adiacenti fuori da una curva liscia, varianza totale non monotona nel tenore —
|
||||||
|
e l'incoerenza si ricompone. E' un'ipotesi di MICROSTRUTTURA, non di previsione: non chiede alla
|
||||||
|
vol implicita di prevedere nulla (cosa che il progetto ha gia' refutato quattro volte: dvol_directional,
|
||||||
|
gamma scalping, term-structure come segnale, skew come segnale).
|
||||||
|
ATTESA DICHIARATA PRIMA: l'incoerenza esiste al MID ma non sopravvive al bid-ask, perche' su
|
||||||
|
un'opzione a 7 giorni un punto di vol vale pochi dollari mentre attraversare lo spread ne costa
|
||||||
|
il ~10% del premio (misurato dal filone 11 sulle inverse).
|
||||||
|
|
||||||
|
LA DIFESA CONTRO IL RISCHIO NUMERO UNO DI QUESTO FILONE (misurare il proprio errore di
|
||||||
|
interpolazione) e' strutturale, non statistica: i due test principali sono **arbitraggi statici
|
||||||
|
model-free** — monotonia e convessita' del prezzo nello strike — che non richiedono NESSUN
|
||||||
|
interpolatore, NESSUN modello e NESSUN tasso. Valgono per ogni misura di prezzo, path per path:
|
||||||
|
* put: P(K) crescente in K (verticale)
|
||||||
|
* put: P(K) convessa in K (farfalla)
|
||||||
|
e sono verificati su prezzi ESEGUIBILI (si vende al bid, si compra all'ask), mai al mid.
|
||||||
|
Il terzo test — varianza totale non monotona nel tenore — richiede di confrontare tenori diversi:
|
||||||
|
si riporta sia nella versione SENZA interpolazione (strike condiviso fra due scadenze) sia in
|
||||||
|
quella con interpolazione a log-moneyness fisso, e la DIFFERENZA fra le due e' la misura diretta
|
||||||
|
di quanto pesa l'interpolatore.
|
||||||
|
|
||||||
|
Convenzioni verificate nel codice (non assunte):
|
||||||
|
* tick: 0.0001 coin sotto 0.005, 0.0005 sopra -> contato, non dichiarato
|
||||||
|
* f_markfit: la mark IV del venue riprezza i propri mid? (il filone 5 misura 0.992)
|
||||||
|
* fee Deribit opzioni: 0.03% del sottostante per gamba, cap 12.5% del premio della gamba
|
||||||
|
|
||||||
|
Il dato: `data/raw/cb_chain/` (archivio bite + raccolta propria). SOLO opzioni INVERSE — il
|
||||||
|
collettore interroga {"currency": "BTC"|"ETH"}, che non restituisce la famiglia USDC-lineare
|
||||||
|
(difetto di produzione trovato dal filone 11). Quindi ogni numero qui e' misurato sulla famiglia
|
||||||
|
che il conto NON puo' marginare, e le conclusioni sulla famiglia USDC sono ESTRAPOLAZIONI
|
||||||
|
dichiarate, non misure.
|
||||||
|
|
||||||
|
Muro del dato: prima del 2026-06-09 l'archivio raccoglieva UNA scadenza per giro -> la SUPERFICIE
|
||||||
|
esiste da li' (~75 giorni). Lo SMILE (piu' strike su UNA scadenza) esiste dal 2026-05-01 (~113 g).
|
||||||
|
Verdetto massimo possibile: LEAD con gate pre-registrato. Mai CANDIDATO.
|
||||||
|
|
||||||
|
uv run python scripts/research/r0822b_surface_rv.py
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import glob
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import pandas as pd
|
||||||
|
from scipy.stats import norm
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[2]
|
||||||
|
sys.path.insert(0, str(ROOT))
|
||||||
|
|
||||||
|
STORE = ROOT / "data" / "raw" / "cb_chain"
|
||||||
|
|
||||||
|
SMILE_START = pd.Timestamp("2026-05-01", tz="UTC") # piu' strike su UNA scadenza
|
||||||
|
SURF_START = pd.Timestamp("2026-06-09", tz="UTC") # piu' scadenze per giro -> superficie
|
||||||
|
|
||||||
|
FEE_UNDERLYING = 0.0003 # 0.03% del sottostante, per gamba, in coin per contratto
|
||||||
|
FEE_CAP_FRAC = 0.125 # cap: 12.5% del premio della gamba
|
||||||
|
MIN_LOT = {"BTC": 0.1, "ETH": 1.0} # famiglia INVERSE (quella che abbiamo nel dato)
|
||||||
|
MIN_LOT_USDC = {"BTC": 0.01, "ETH": 0.1} # famiglia USDC-lineare (non raccolta: estrapolazione)
|
||||||
|
|
||||||
|
DTE_MIN = 1.0 # sotto un giorno il book e' un'altra cosa; riportato anche senza filtro
|
||||||
|
|
||||||
|
|
||||||
|
def tick_of(px: np.ndarray) -> np.ndarray:
|
||||||
|
"""Griglia di prezzo Deribit sulle opzioni, in coin. Verificata sui dati in `certifica()`."""
|
||||||
|
return np.where(px < 0.005, 0.0001, 0.0005)
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------- dato
|
||||||
|
|
||||||
|
def load(assets=("BTC", "ETH")) -> pd.DataFrame:
|
||||||
|
cols = ["ts", "asset", "instrument_name", "strike", "option_type", "exp",
|
||||||
|
"bid", "ask", "mid", "iv", "delta", "open_interest", "book_depth_top3",
|
||||||
|
"underlying_price", "index_price", "quote_status"]
|
||||||
|
parts = []
|
||||||
|
for f in sorted(glob.glob(str(STORE / "*.parquet"))):
|
||||||
|
d = pd.read_parquet(f, columns=cols)
|
||||||
|
parts.append(d[d["asset"].isin(assets)])
|
||||||
|
d = pd.concat(parts, ignore_index=True)
|
||||||
|
d["ts"] = pd.to_datetime(d["ts"], utc=True)
|
||||||
|
d["exp"] = pd.to_datetime(d["exp"], utc=True)
|
||||||
|
for c in ("strike", "bid", "ask", "mid", "iv", "delta", "open_interest",
|
||||||
|
"book_depth_top3", "underlying_price", "index_price"):
|
||||||
|
d[c] = pd.to_numeric(d[c], errors="coerce")
|
||||||
|
d = d.drop_duplicates(subset=["ts", "instrument_name"], keep="last")
|
||||||
|
d["dte"] = (d["exp"] - d["ts"]).dt.total_seconds() / 86400.0
|
||||||
|
d["T"] = d["dte"] / 365.25
|
||||||
|
d = d.reset_index(drop=True)
|
||||||
|
d["spot_feed"] = _spot_asof(d)
|
||||||
|
return d
|
||||||
|
|
||||||
|
|
||||||
|
def _spot_asof(d: pd.DataFrame) -> pd.Series:
|
||||||
|
"""Indice dal feed CERTIFICATO (1h Deribit mainnet), asof sul ts della quota.
|
||||||
|
|
||||||
|
Serve perche' `bite_archive` NON ha ne' `index_price` ne' `underlying_price` (100% None:
|
||||||
|
misurato, non assunto) -> senza questo la finestra con un divisore sarebbe di 23 giorni.
|
||||||
|
"""
|
||||||
|
from scripts.analysis.research_lab import load_tf
|
||||||
|
out = pd.Series(np.nan, index=d.index)
|
||||||
|
for a in d["asset"].unique():
|
||||||
|
px = load_tf(a, "1h")
|
||||||
|
s = pd.Series(px["close"].to_numpy(float),
|
||||||
|
index=pd.to_datetime(px["timestamp"], unit="ms", utc=True)).sort_index()
|
||||||
|
s.index = pd.DatetimeIndex(s.index).as_unit("ns")
|
||||||
|
m = d["asset"] == a
|
||||||
|
t = pd.DatetimeIndex(d.loc[m, "ts"]).as_unit("ns")
|
||||||
|
# epoca esplicita in ns (mai `.view("int64")` su indici tz-aware: lezione 01/07)
|
||||||
|
si, ti = s.index.asi8, t.asi8
|
||||||
|
pos = np.searchsorted(si, ti, side="right") - 1
|
||||||
|
v = np.where(pos >= 0, s.to_numpy()[np.clip(pos, 0, None)], np.nan)
|
||||||
|
out.loc[m] = v
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def forward(d: pd.DataFrame) -> pd.DataFrame:
|
||||||
|
"""Forward per (asset, ts, exp) ricostruito con la PARITA' PUT-CALL, model-free.
|
||||||
|
|
||||||
|
In convenzione inverse il premio e' quotato in coin: (C - P) * X = F - K => F = K + (C-P)*X,
|
||||||
|
con X = divisore della convenzione (misurato in `certifica`). Mediana sugli strike = robusta.
|
||||||
|
Dove il venue lo ha registrato (solo la raccolta propria) si usa il valore OSSERVATO, e
|
||||||
|
l'accordo fra i due e' una certificazione, non un'assunzione.
|
||||||
|
"""
|
||||||
|
q = d[(d["bid"] > 0) & (d["ask"] > 0) & (d["ask"] >= d["bid"]) & d["spot_feed"].notna()]
|
||||||
|
c = q[q["option_type"] == "C"][["asset", "ts", "exp", "strike", "mid", "spot_feed"]]
|
||||||
|
p = q[q["option_type"] == "P"][["asset", "ts", "exp", "strike", "mid"]]
|
||||||
|
j = c.merge(p, on=["asset", "ts", "exp", "strike"], suffixes=("_c", "_p"))
|
||||||
|
j["F_k"] = j["strike"] + (j["mid_c"] - j["mid_p"]) * j["spot_feed"]
|
||||||
|
f = j.groupby(["asset", "ts", "exp"])["F_k"].median().rename("F_pcp").reset_index()
|
||||||
|
return f
|
||||||
|
|
||||||
|
|
||||||
|
def two_sided(d: pd.DataFrame) -> pd.DataFrame:
|
||||||
|
"""Quotate a DUE lati. Una gamba sola quotata non e' meta' struttura: e' un'altra cosa.
|
||||||
|
|
||||||
|
(Regola 8 del brief: una riga presente non e' un dato presente.)
|
||||||
|
"""
|
||||||
|
q = d[(d["bid"] > 0) & (d["ask"] > 0) & (d["ask"] >= d["bid"])].copy()
|
||||||
|
q = q[q["T"] > 0]
|
||||||
|
return q
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------- prezzo
|
||||||
|
|
||||||
|
def bs_usd(F, K, T, sig, is_put):
|
||||||
|
"""Black-76 con r=0 (Deribit prezza sul FORWARD della scadenza = `underlying_price`)."""
|
||||||
|
F = np.asarray(F, float); K = np.asarray(K, float)
|
||||||
|
T = np.asarray(T, float); sig = np.asarray(sig, float)
|
||||||
|
out = np.full(F.shape, np.nan)
|
||||||
|
ok = (T > 0) & (sig > 0) & (F > 0) & (K > 0)
|
||||||
|
if not ok.any():
|
||||||
|
return out
|
||||||
|
d1 = np.zeros_like(F); d2 = np.zeros_like(F)
|
||||||
|
sq = np.sqrt(np.where(ok, T, 1.0))
|
||||||
|
d1[ok] = (np.log(F[ok] / K[ok]) + 0.5 * sig[ok] ** 2 * T[ok]) / (sig[ok] * sq[ok])
|
||||||
|
d2[ok] = d1[ok] - sig[ok] * sq[ok]
|
||||||
|
call = F * norm.cdf(d1) - K * norm.cdf(d2)
|
||||||
|
put = K * norm.cdf(-d2) - F * norm.cdf(-d1)
|
||||||
|
out[ok] = np.where(is_put[ok], put[ok], call[ok])
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def bs_vega_usd(F, K, T, sig):
|
||||||
|
F = np.asarray(F, float); K = np.asarray(K, float)
|
||||||
|
T = np.asarray(T, float); sig = np.asarray(sig, float)
|
||||||
|
out = np.zeros(F.shape)
|
||||||
|
ok = (T > 0) & (sig > 0) & (F > 0) & (K > 0)
|
||||||
|
sq = np.sqrt(np.where(ok, T, 1.0))
|
||||||
|
d1 = np.zeros_like(F)
|
||||||
|
d1[ok] = (np.log(F[ok] / K[ok]) + 0.5 * sig[ok] ** 2 * T[ok]) / (sig[ok] * sq[ok])
|
||||||
|
out[ok] = F[ok] * norm.pdf(d1[ok]) * sq[ok] # per 1.00 di sigma (=100 punti di vol)
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------- certificazione
|
||||||
|
|
||||||
|
def certifica(d: pd.DataFrame) -> pd.DataFrame:
|
||||||
|
print("=" * 100)
|
||||||
|
print("0. CERTIFICAZIONE DEL DATO — cosa esiste davvero, prima di qualunque test")
|
||||||
|
print("=" * 100)
|
||||||
|
q = two_sided(d)
|
||||||
|
print(f"righe totali {len(d):,} · quotate a due lati {len(q):,} ({len(q)/len(d):.1%})")
|
||||||
|
print(f"finestra {d['ts'].min():%Y-%m-%d} -> {d['ts'].max():%Y-%m-%d}")
|
||||||
|
|
||||||
|
# (a) muro del dato: scadenze per giro
|
||||||
|
per = d.groupby([d["ts"].dt.floor("D"), "asset"])["exp"].nunique().unstack()
|
||||||
|
pre = per[per.index < SURF_START]
|
||||||
|
post = per[per.index >= SURF_START]
|
||||||
|
print("\n(a) MURO DEL DATO — scadenze per giro (mediana per giorno):")
|
||||||
|
print(f" pre {SURF_START:%Y-%m-%d}: BTC {pre['BTC'].median():.0f} · ETH {pre['ETH'].median():.0f}"
|
||||||
|
f" (max BTC {pre['BTC'].max():.0f}) -> la SUPERFICIE non esiste")
|
||||||
|
print(f" post {SURF_START:%Y-%m-%d}: BTC {post['BTC'].median():.0f} · ETH {post['ETH'].median():.0f}"
|
||||||
|
f" -> superficie su {len(post)} giorni")
|
||||||
|
print(f" smile (piu' strike, UNA scadenza) su {len(per)} giorni dal {per.index.min():%Y-%m-%d}"
|
||||||
|
f" -> i test model-free girano su questa finestra piu' lunga")
|
||||||
|
|
||||||
|
# (a-bis) BUCO DI COLONNA nell'archivio ereditato
|
||||||
|
obs = d["index_price"].notna()
|
||||||
|
print("\n(a-bis) BUCO DI COLONNA — `bite_archive` non registrava il sottostante")
|
||||||
|
print(f" index_price/underlying_price presenti in {obs.sum():,}/{len(d):,} righe ({obs.mean():.1%})"
|
||||||
|
f" — solo la raccolta propria, dal {d.loc[obs,'ts'].min():%Y-%m-%d}")
|
||||||
|
print(f" book_depth_top3 presente in {d['book_depth_top3'].notna().mean():.1%} delle righe")
|
||||||
|
print(" -> i test 1-2 (model-free) NON ne hanno bisogno; il test 4 (calendario) si')")
|
||||||
|
|
||||||
|
# (b) tick: verificato, non dichiarato
|
||||||
|
print("\n(b) GRIGLIA DI PREZZO (verificata sui dati, non assunta)")
|
||||||
|
for a in sorted(q["asset"].unique()):
|
||||||
|
b = q.loc[q["asset"] == a, "bid"].to_numpy()
|
||||||
|
t = tick_of(b)
|
||||||
|
off = np.abs(np.round(b / t) - b / t) > 1e-6
|
||||||
|
n_tick = np.round(b / t)
|
||||||
|
print(f" {a}: fuori griglia {off.sum():,}/{len(b):,} ({off.mean():.4%}) · "
|
||||||
|
f"bid in tick: mediana {np.median(n_tick):.0f} · quota <=2 tick {np.mean(n_tick <= 2):.1%}")
|
||||||
|
|
||||||
|
# (c) il divisore della convenzione inverse + f_markfit
|
||||||
|
print("\n(c) CONVENZIONE INVERSE e f_markfit — la mark IV riprezza i propri mid?")
|
||||||
|
ov = q[obs.reindex(q.index, fill_value=False)].dropna(subset=["iv", "mid", "spot_feed"])
|
||||||
|
ov = ov[(ov["iv"] > 0) & (ov["mid"] > 0) & (ov["dte"] >= DTE_MIN)]
|
||||||
|
dev = (ov["index_price"] / ov["spot_feed"] - 1).abs()
|
||||||
|
print(f" index_price della catena vs feed CERTIFICATO 1h: |dev| mediana {dev.median():.5%} "
|
||||||
|
f"· p99 {dev.quantile(.99):.4%} (n={len(ov):,}) -> il feed puo' fare da divisore sull'archivio")
|
||||||
|
sm = ov.sample(n=min(120_000, len(ov)), random_state=0)
|
||||||
|
is_put = (sm["option_type"] == "P").to_numpy()
|
||||||
|
usd = bs_usd(sm["underlying_price"].to_numpy(), sm["strike"].to_numpy(),
|
||||||
|
sm["T"].to_numpy(), (sm["iv"] / 100).to_numpy(), is_put)
|
||||||
|
best = None
|
||||||
|
for nome, div in (("index_price", sm["index_price"].to_numpy()),
|
||||||
|
("underlying_price(fwd)", sm["underlying_price"].to_numpy()),
|
||||||
|
("feed certificato 1h", sm["spot_feed"].to_numpy())):
|
||||||
|
f = (usd / div) / sm["mid"].to_numpy()
|
||||||
|
f = f[np.isfinite(f) & (f > 0)]
|
||||||
|
print(f" f_markfit con divisore {nome:<22}: mediana {np.median(f):.4f} "
|
||||||
|
f"[p10 {np.percentile(f,10):.3f}, p90 {np.percentile(f,90):.3f}] n={len(f):,}")
|
||||||
|
if best is None or abs(np.median(f) - 1) < abs(best[1] - 1):
|
||||||
|
best = (nome, float(np.median(f)))
|
||||||
|
print(f" -> convenzione: divisore = {best[0]} (f_markfit {best[1]:.4f}); "
|
||||||
|
f"un f molto lontano da 1 direbbe che il problema e' il MIO prezzatore, non il mercato")
|
||||||
|
|
||||||
|
# (d) forward ricostruito con la parita' put-call, certificato contro l'osservato
|
||||||
|
print("\n(d) FORWARD ricostruito con la PARITA' PUT-CALL (model-free) vs osservato")
|
||||||
|
F = forward(d)
|
||||||
|
chk = d.loc[obs, ["asset", "ts", "exp", "underlying_price"]].drop_duplicates(
|
||||||
|
subset=["asset", "ts", "exp"]).merge(F, on=["asset", "ts", "exp"], how="inner")
|
||||||
|
e = (chk["F_pcp"] / chk["underlying_price"] - 1)
|
||||||
|
print(f" n={len(chk):,} coppie (asset,ts,scadenza) con entrambi: "
|
||||||
|
f"|errore| mediana {e.abs().median():.5%} · p95 {e.abs().quantile(.95):.4%} · "
|
||||||
|
f"corr {np.corrcoef(chk['F_pcp'], chk['underlying_price'])[0,1]:.6f}")
|
||||||
|
return q, F
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------- fee
|
||||||
|
|
||||||
|
def fee_coin(premio_coin: np.ndarray, n_contratti: float) -> np.ndarray:
|
||||||
|
"""Fee Deribit opzioni per gamba, in coin: min(0.03% del sottostante, 12.5% del premio) x lotti."""
|
||||||
|
per_contratto = np.minimum(FEE_UNDERLYING, FEE_CAP_FRAC * np.asarray(premio_coin, float))
|
||||||
|
return per_contratto * n_contratti
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------- TEST V
|
||||||
|
|
||||||
|
def test_verticale(q: pd.DataFrame, dte_min: float) -> dict:
|
||||||
|
"""Monotonia del prezzo nello strike. Model-free, nessun interpolatore, nessun tasso.
|
||||||
|
|
||||||
|
put: P(K) crescente -> arbitraggio se bid(K1) > ask(K2) con K1 < K2
|
||||||
|
(vendi la put bassa al bid, compra l'alta all'ask: credito + payoff sempre >= 0)
|
||||||
|
call: C(K) decrescente -> arbitraggio se bid(K2) > ask(K1) con K1 < K2
|
||||||
|
"""
|
||||||
|
x = q[q["dte"] >= dte_min].sort_values(["asset", "ts", "exp", "option_type", "strike"])
|
||||||
|
key = ["asset", "ts", "exp", "option_type"]
|
||||||
|
g = x.groupby(key, sort=False)
|
||||||
|
# per le put il "meglio finora" salendo di strike; per le call scendendo
|
||||||
|
x["cmax_up"] = g["bid"].cummax()
|
||||||
|
x["prev_up"] = g["cmax_up"].shift(1) # miglior bid su strike STRETTAMENTE inferiori
|
||||||
|
xr = x.iloc[::-1].copy()
|
||||||
|
gr = xr.groupby(key, sort=False)
|
||||||
|
xr["cmax_dn"] = gr["bid"].cummax()
|
||||||
|
x["prev_dn"] = gr["cmax_dn"].shift(1) # miglior bid su strike STRETTAMENTE superiori
|
||||||
|
is_put = x["option_type"].to_numpy() == "P"
|
||||||
|
prev = np.where(is_put, x["prev_up"].to_numpy(), x["prev_dn"].to_numpy())
|
||||||
|
cred = prev - x["ask"].to_numpy() # credito riskless in coin, per contratto
|
||||||
|
viol = np.isfinite(cred) & (cred > 0)
|
||||||
|
n_tot = len(x)
|
||||||
|
out = {"n_confronti": int(np.isfinite(cred).sum()), "n_viol": int(viol.sum()),
|
||||||
|
"quota": float(viol.sum() / max(1, np.isfinite(cred).sum()))}
|
||||||
|
if viol.any():
|
||||||
|
vv = x.loc[viol]
|
||||||
|
out["credito_med_coin"] = float(np.median(cred[viol]))
|
||||||
|
out["credito_usd_medlot"] = float(np.median(
|
||||||
|
cred[viol] * vv["asset"].map(MIN_LOT).to_numpy() * vv["spot_feed"].to_numpy()))
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------- TEST B
|
||||||
|
|
||||||
|
def test_farfalla(q: pd.DataFrame, dte_min: float) -> pd.DataFrame:
|
||||||
|
"""Convessita' del prezzo nello strike su triple CONSECUTIVE. Model-free.
|
||||||
|
|
||||||
|
lam = (K3-K2)/(K3-K1); payoff lam*P(K1) + (1-lam)*P(K3) - P(K2) >= 0 sempre.
|
||||||
|
Costo eseguibile = lam*ask1 + (1-lam)*ask3 - bid2. Se < 0 -> credito riskless.
|
||||||
|
Al MID lo stesso numero misura l'INCOERENZA (non eseguibile) — si riportano entrambi.
|
||||||
|
"""
|
||||||
|
x = q[q["dte"] >= dte_min].sort_values(["asset", "ts", "exp", "option_type", "strike"])
|
||||||
|
key = ["asset", "ts", "exp", "option_type"]
|
||||||
|
g = x.groupby(key, sort=False)
|
||||||
|
for c in ("strike", "bid", "ask", "mid", "iv", "book_depth_top3"):
|
||||||
|
x[c + "_p"] = g[c].shift(1)
|
||||||
|
x[c + "_n"] = g[c].shift(-1)
|
||||||
|
x = x.dropna(subset=["strike_p", "strike_n", "bid_p", "bid_n", "ask_p", "ask_n"])
|
||||||
|
K1, K2, K3 = x["strike_p"].to_numpy(), x["strike"].to_numpy(), x["strike_n"].to_numpy()
|
||||||
|
lam = (K3 - K2) / (K3 - K1)
|
||||||
|
x["lam"] = lam
|
||||||
|
# eseguibile: compro le ali all'ask, vendo il corpo al bid
|
||||||
|
x["cost_exec"] = lam * x["ask_p"] + (1 - lam) * x["ask_n"] - x["bid"]
|
||||||
|
# incoerenza al mid (non eseguibile, ma e' la lente "la superficie e' liscia?")
|
||||||
|
x["cost_mid"] = lam * x["mid_p"] + (1 - lam) * x["mid_n"] - x["mid"]
|
||||||
|
# la stessa cosa in punti di vol: residuo di convessita' locale dello smile
|
||||||
|
x["iv_resid"] = x["iv"] - (lam * x["iv_p"] + (1 - lam) * x["iv_n"])
|
||||||
|
return x
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------- vol points
|
||||||
|
|
||||||
|
def in_punti_vol(x: pd.DataFrame) -> pd.DataFrame:
|
||||||
|
"""Mezzo spread denaro-lettera convertito in punti di vol via vega BS (r=0, forward)."""
|
||||||
|
vega_usd = bs_vega_usd(x["F"].to_numpy(), x["strike"].to_numpy(),
|
||||||
|
x["T"].to_numpy(), (x["iv"] / 100).to_numpy())
|
||||||
|
vega_coin_per_pt = vega_usd / x["spot_feed"].to_numpy() / 100.0
|
||||||
|
x = x.copy()
|
||||||
|
x["vega_coin_pt"] = vega_coin_per_pt
|
||||||
|
x["halfspread_pt"] = (x["ask"] - x["bid"]) / 2.0 / np.clip(vega_coin_per_pt, 1e-12, None)
|
||||||
|
x["relspread"] = (x["ask"] - x["bid"]) / x["mid"]
|
||||||
|
# quanto vale UN TICK in punti di vol: se il tick e' piu' grosso dell'incoerenza,
|
||||||
|
# l'incoerenza non e' esprimibile nella griglia di prezzo del venue.
|
||||||
|
x["tick_pt"] = tick_of(x["mid"].to_numpy()) / np.clip(vega_coin_per_pt, 1e-12, None)
|
||||||
|
return x
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------- TEST C
|
||||||
|
|
||||||
|
def test_calendario(q: pd.DataFrame, dte_min: float, ogni: int = 4) -> dict:
|
||||||
|
"""Varianza totale w = iv^2 * T: deve CRESCERE col tenore. Due lenti, e la loro differenza.
|
||||||
|
|
||||||
|
(i) STRIKE CONDIVISO fra due scadenze adiacenti: nessuna interpolazione.
|
||||||
|
(ii) LOG-MONEYNESS FISSO k = ln(K/F): richiede di interpolare lo smile lungo in k.
|
||||||
|
La differenza fra (i) e (ii) e' la misura diretta del contributo dell'INTERPOLATORE.
|
||||||
|
"""
|
||||||
|
x = q[(q["dte"] >= dte_min) & q["iv"].notna() & (q["iv"] > 0) & q["F"].notna()].copy()
|
||||||
|
x = x[x["ts"] >= SURF_START]
|
||||||
|
tenuti = sorted(x["ts"].unique())[::ogni] # (i) e (ii) sugli STESSI snapshot: appaiato
|
||||||
|
x = x[x["ts"].isin(tenuti)]
|
||||||
|
x["w"] = (x["iv"] / 100.0) ** 2 * x["T"]
|
||||||
|
x["k"] = np.log(x["strike"] / x["F"])
|
||||||
|
|
||||||
|
# --- (i) strike condiviso, adiacenti nel tempo
|
||||||
|
x = x.sort_values(["asset", "ts", "option_type", "strike", "exp"])
|
||||||
|
key = ["asset", "ts", "option_type", "strike"]
|
||||||
|
g = x.groupby(key, sort=False)
|
||||||
|
nxt_w = g["w"].shift(-1).to_numpy()
|
||||||
|
nxt_T = g["T"].shift(-1).to_numpy()
|
||||||
|
nxt_iv = g["iv"].shift(-1).to_numpy()
|
||||||
|
ok = np.isfinite(nxt_w) & (nxt_T > x["T"].to_numpy())
|
||||||
|
dw = nxt_w - x["w"].to_numpy()
|
||||||
|
viol_i = ok & (dw < 0)
|
||||||
|
# ampiezza in punti di vol: quanto dovrebbe salire l'iv lunga per ripristinare la monotonia
|
||||||
|
iv_need = np.sqrt(np.maximum(x["w"].to_numpy(), 0) / np.maximum(nxt_T, 1e-9)) * 100.0
|
||||||
|
amp_pt = np.where(ok, iv_need - nxt_iv, np.nan)
|
||||||
|
|
||||||
|
res = {"i_n": int(ok.sum()), "i_viol": int(viol_i.sum()),
|
||||||
|
"i_quota": float(viol_i.sum() / max(1, ok.sum())),
|
||||||
|
"i_amp_med_pt": float(np.nanmedian(amp_pt[viol_i])) if viol_i.any() else np.nan,
|
||||||
|
"i_amp_p95_pt": float(np.nanpercentile(amp_pt[viol_i], 95)) if viol_i.any() else np.nan}
|
||||||
|
|
||||||
|
# --- (ii) log-moneyness fisso, con interpolazione
|
||||||
|
y = x
|
||||||
|
n_par, n_vio, amps = 0, 0, []
|
||||||
|
for (a, ts, ot), gg in y.groupby(["asset", "ts", "option_type"], sort=False):
|
||||||
|
exps = sorted(gg["exp"].unique())
|
||||||
|
if len(exps) < 2:
|
||||||
|
continue
|
||||||
|
for e1, e2 in zip(exps[:-1], exps[1:]):
|
||||||
|
s1 = gg[gg["exp"] == e1].sort_values("k")
|
||||||
|
s2 = gg[gg["exp"] == e2].sort_values("k")
|
||||||
|
if len(s1) < 2 or len(s2) < 2:
|
||||||
|
continue
|
||||||
|
k1 = s1["k"].to_numpy()
|
||||||
|
inside = (k1 >= s2["k"].min()) & (k1 <= s2["k"].max()) # MAI estrapolare
|
||||||
|
if not inside.any():
|
||||||
|
continue
|
||||||
|
iv2 = np.interp(k1[inside], s2["k"].to_numpy(), s2["iv"].to_numpy())
|
||||||
|
T2 = float(s2["T"].iloc[0])
|
||||||
|
w2 = (iv2 / 100.0) ** 2 * T2
|
||||||
|
w1 = s1["w"].to_numpy()[inside]
|
||||||
|
n_par += int(inside.sum())
|
||||||
|
bad = w2 < w1
|
||||||
|
n_vio += int(bad.sum())
|
||||||
|
if bad.any():
|
||||||
|
need = np.sqrt(w1[bad] / T2) * 100.0
|
||||||
|
amps.append(need - iv2[bad])
|
||||||
|
amps = np.concatenate(amps) if amps else np.array([])
|
||||||
|
res.update({"ii_n": n_par, "ii_viol": n_vio, "ii_quota": n_vio / max(1, n_par),
|
||||||
|
"ii_amp_med_pt": float(np.median(amps)) if len(amps) else np.nan,
|
||||||
|
"ii_ogni": ogni})
|
||||||
|
# valore in dollari dell'ampiezza (i) al lotto minimo, contro il costo di 2 gambe
|
||||||
|
vv = x.loc[viol_i].copy()
|
||||||
|
if len(vv):
|
||||||
|
vv = in_punti_vol(vv)
|
||||||
|
amp = pd.Series(amp_pt, index=x.index)[viol_i].to_numpy()
|
||||||
|
lot = vv["asset"].map(MIN_LOT).to_numpy()
|
||||||
|
val = np.abs(amp) * vv["vega_coin_pt"].to_numpy() * lot * vv["spot_feed"].to_numpy()
|
||||||
|
costo = ((vv["ask"] - vv["bid"]).to_numpy() * lot * vv["spot_feed"].to_numpy()
|
||||||
|
+ 2 * fee_coin(vv["mid"].to_numpy(), lot) * vv["spot_feed"].to_numpy())
|
||||||
|
res.update({"i_val_usd_med": float(np.nanmedian(val)),
|
||||||
|
"i_costo_usd_med": float(np.nanmedian(costo)),
|
||||||
|
"i_n_val": int(np.isfinite(val).sum())})
|
||||||
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------- economia
|
||||||
|
|
||||||
|
def economia(bf: pd.DataFrame, asset: str) -> dict:
|
||||||
|
"""Quanto vale, in dollari, l'incoerenza di una farfalla al lotto minimo — netto fee.
|
||||||
|
|
||||||
|
Struttura a 3 gambe, lotti INTERI: con strike equispaziati e' 1:2:1 sul lotto minimo.
|
||||||
|
Si conta la fee gamba per gamba col cap sul premio, e si usa il prezzo ESEGUIBILE.
|
||||||
|
"""
|
||||||
|
x = bf[bf["asset"] == asset]
|
||||||
|
lot = MIN_LOT[asset]
|
||||||
|
S = x["spot_feed"].to_numpy()
|
||||||
|
# credito lordo della farfalla eseguibile, in coin per unita' di "corpo"
|
||||||
|
cred = -x["cost_exec"].to_numpy() # >0 = credito riskless
|
||||||
|
# lotti interi: corpo 2*lot, ali lot ciascuna (equivale a lam=0.5 -> solo strike equispaziati)
|
||||||
|
eq = np.isclose(x["lam"].to_numpy(), 0.5, atol=0.02)
|
||||||
|
cred_eq_coin = (x["bid"].to_numpy() * 2 * lot
|
||||||
|
- x["ask_p"].to_numpy() * lot - x["ask_n"].to_numpy() * lot)
|
||||||
|
f = (fee_coin(x["bid"].to_numpy(), 2 * lot)
|
||||||
|
+ fee_coin(x["ask_p"].to_numpy(), lot) + fee_coin(x["ask_n"].to_numpy(), lot))
|
||||||
|
netto_usd = (cred_eq_coin - f) * S
|
||||||
|
return {
|
||||||
|
"asset": asset, "n": int(len(x)), "n_equisp": int(eq.sum()),
|
||||||
|
"cred_lordo_p99_coin": float(np.nanpercentile(cred, 99)),
|
||||||
|
"fee_3gambe_usd_med": float(np.nanmedian(f[eq] * S[eq])) if eq.any() else np.nan,
|
||||||
|
"netto_usd_max": float(np.nanmax(netto_usd[eq])) if eq.any() else np.nan,
|
||||||
|
"netto_usd_p99": float(np.nanpercentile(netto_usd[eq], 99)) if eq.any() else np.nan,
|
||||||
|
"n_netto_pos": int(np.nansum(netto_usd[eq] > 0)),
|
||||||
|
"nozionale_min_lotto_usd": float(np.nanmedian(S) * lot),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------- persistenza
|
||||||
|
|
||||||
|
def persistenza(bf: pd.DataFrame, viol_col: str) -> dict:
|
||||||
|
"""Una violazione che sparisce all'ora dopo e' un guizzo di book, non un'opportunita'.
|
||||||
|
|
||||||
|
Chiave = (asset, exp, tipo, tripla di strike). Si guarda se la stessa tripla e' ancora in
|
||||||
|
violazione allo snapshot successivo IN CUI LA TRIPLA E' QUOTATA (non 'l'ora dopo' in astratto).
|
||||||
|
"""
|
||||||
|
v = bf[bf[viol_col]].copy()
|
||||||
|
if v.empty:
|
||||||
|
return {"n": 0}
|
||||||
|
bf = bf.copy()
|
||||||
|
bf["trip"] = (bf["asset"] + "|" + bf["exp"].astype(str) + "|" + bf["option_type"] + "|"
|
||||||
|
+ bf["strike_p"].astype(str) + "|" + bf["strike"].astype(str) + "|"
|
||||||
|
+ bf["strike_n"].astype(str))
|
||||||
|
bf = bf.sort_values(["trip", "ts"])
|
||||||
|
nxt = bf.groupby("trip", sort=False)[viol_col].shift(-1)
|
||||||
|
m = bf[viol_col] & nxt.notna()
|
||||||
|
return {"n": int(m.sum()), "resta": float(nxt[m].mean()) if m.any() else np.nan}
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------- controlli positivi
|
||||||
|
|
||||||
|
def _finto(rows) -> pd.DataFrame:
|
||||||
|
"""Catena sintetica minima per i controlli positivi."""
|
||||||
|
d = pd.DataFrame(rows)
|
||||||
|
d["ts"] = pd.to_datetime(d["ts"], utc=True)
|
||||||
|
d["exp"] = pd.to_datetime(d["exp"], utc=True)
|
||||||
|
d["dte"] = (d["exp"] - d["ts"]).dt.total_seconds() / 86400.0
|
||||||
|
d["T"] = d["dte"] / 365.25
|
||||||
|
d["mid"] = (d["bid"] + d["ask"]) / 2
|
||||||
|
for c, v in (("spot_feed", 100_000.0), ("F", 100_000.0), ("option_type", "P"),
|
||||||
|
("open_interest", 1000.0), ("book_depth_top3", 10.0), ("delta", -0.2)):
|
||||||
|
if c not in d:
|
||||||
|
d[c] = v
|
||||||
|
return d
|
||||||
|
|
||||||
|
|
||||||
|
def controlli_positivi() -> None:
|
||||||
|
"""Un rilevatore tarato per non segnalare e' indistinguibile da uno rotto.
|
||||||
|
|
||||||
|
Regola gia' codificata nel progetto (venue_watch, 26/07). Qui i tre test vengono puntati
|
||||||
|
su violazioni COSTRUITE: se non le vedono, i loro zeri non valgono niente.
|
||||||
|
"""
|
||||||
|
print("=" * 100)
|
||||||
|
print("CONTROLLI POSITIVI — i tre rilevatori vedono una violazione COSTRUITA?")
|
||||||
|
print("=" * 100)
|
||||||
|
base = dict(ts="2026-08-01T00:00Z", exp="2026-08-15T08:00Z", asset="BTC",
|
||||||
|
option_type="P", iv=50.0, instrument_name="x")
|
||||||
|
# (1) verticale: put a strike BASSO con bid > ask della put a strike ALTO
|
||||||
|
v_ok = _finto([{**base, "strike": 90000, "bid": 0.0300, "ask": 0.0305},
|
||||||
|
{**base, "strike": 95000, "bid": 0.0200, "ask": 0.0205}])
|
||||||
|
v_no = _finto([{**base, "strike": 90000, "bid": 0.0200, "ask": 0.0205},
|
||||||
|
{**base, "strike": 95000, "bid": 0.0300, "ask": 0.0305}])
|
||||||
|
r_ok, r_no = test_verticale(v_ok, 0.0), test_verticale(v_no, 0.0)
|
||||||
|
print(f" TEST V: catena INVERTITA -> {r_ok['n_viol']} violazioni (attese >=1) · "
|
||||||
|
f"catena sana -> {r_no['n_viol']} (attese 0)")
|
||||||
|
# (2) farfalla: corpo troppo caro (concavita')
|
||||||
|
b_ok = _finto([{**base, "strike": 90000, "bid": 0.0100, "ask": 0.0105},
|
||||||
|
{**base, "strike": 95000, "bid": 0.0400, "ask": 0.0405},
|
||||||
|
{**base, "strike": 100000, "bid": 0.0500, "ask": 0.0505}])
|
||||||
|
b_no = _finto([{**base, "strike": 90000, "bid": 0.0100, "ask": 0.0105},
|
||||||
|
{**base, "strike": 95000, "bid": 0.0280, "ask": 0.0285},
|
||||||
|
{**base, "strike": 100000, "bid": 0.0500, "ask": 0.0505}])
|
||||||
|
f_ok = test_farfalla(b_ok, 0.0); f_no = test_farfalla(b_no, 0.0)
|
||||||
|
print(f" TEST B: corpo troppo caro -> {(f_ok['cost_exec'] < 0).sum()} violazioni eseguibili "
|
||||||
|
f"(attese >=1) · smile convesso -> {(f_no['cost_exec'] < 0).sum()} (attese 0)")
|
||||||
|
# (3) calendario: varianza totale che SCENDE col tenore, stesso strike
|
||||||
|
c_rows = []
|
||||||
|
for e, iv in (("2026-08-08T08:00Z", 80.0), ("2026-08-15T08:00Z", 40.0)):
|
||||||
|
c_rows.append({**base, "exp": e, "strike": 100000, "bid": 0.01, "ask": 0.011, "iv": iv})
|
||||||
|
c_ok = _finto(c_rows)
|
||||||
|
c_ok["ts"] = pd.Timestamp("2026-07-01", tz="UTC")
|
||||||
|
c_no = c_ok.copy(); c_no["iv"] = [40.0, 80.0]
|
||||||
|
r1 = test_calendario(c_ok, 0.0, ogni=1); r2 = test_calendario(c_no, 0.0, ogni=1)
|
||||||
|
print(f" TEST C: varianza totale DECRESCENTE -> {r1['i_viol']} violazioni (attese >=1) · "
|
||||||
|
f"crescente -> {r2['i_viol']} (attese 0)")
|
||||||
|
assert r_ok["n_viol"] >= 1 and r_no["n_viol"] == 0
|
||||||
|
assert (f_ok["cost_exec"] < 0).sum() >= 1 and (f_no["cost_exec"] < 0).sum() == 0
|
||||||
|
assert r1["i_viol"] >= 1 and r2["i_viol"] == 0
|
||||||
|
print(" -> 3/3 rilevatori validati in ENTRAMBI i versi: gli zeri dei test veri sono zeri.\n")
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------- reversione
|
||||||
|
|
||||||
|
def reversione(bf: pd.DataFrame, max_gap_h: float = 2.0) -> dict:
|
||||||
|
"""Il residuo di convessita' si RICOMPONE? (l'ipotesi del filone, misurata)
|
||||||
|
|
||||||
|
Coppie appaiate sulla STESSA tripla di strike a snapshot consecutivi entro `max_gap_h`.
|
||||||
|
beta di resid[t+1] - resid[t] su resid[t] : beta = -1 -> ricomposizione totale in un'ora,
|
||||||
|
beta = 0 -> il residuo e' una proprieta' persistente dello smile (skew), non un errore.
|
||||||
|
"""
|
||||||
|
x = bf.dropna(subset=["iv_resid"]).copy()
|
||||||
|
x["trip"] = (x["asset"] + "|" + x["exp"].astype(str) + "|" + x["option_type"] + "|"
|
||||||
|
+ x["strike_p"].astype(str) + "|" + x["strike"].astype(str) + "|"
|
||||||
|
+ x["strike_n"].astype(str))
|
||||||
|
x = x.sort_values(["trip", "ts"])
|
||||||
|
g = x.groupby("trip", sort=False)
|
||||||
|
r1 = g["iv_resid"].shift(-1).to_numpy()
|
||||||
|
dt = (g["ts"].shift(-1) - x["ts"]).dt.total_seconds().to_numpy() / 3600.0
|
||||||
|
r0 = x["iv_resid"].to_numpy()
|
||||||
|
m = np.isfinite(r1) & np.isfinite(r0) & (dt > 0) & (dt <= max_gap_h)
|
||||||
|
if m.sum() < 100:
|
||||||
|
return {"n": int(m.sum())}
|
||||||
|
b = np.polyfit(r0[m], r1[m] - r0[m], 1)[0]
|
||||||
|
return {"n": int(m.sum()), "beta": float(b), "corr": float(np.corrcoef(r0[m], r1[m])[0, 1]),
|
||||||
|
"meta_vita_h": float(np.log(0.5) / np.log(max(1e-9, 1 + b))) if -1 < b < 0 else np.nan}
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------- main
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
t0 = time.time()
|
||||||
|
controlli_positivi()
|
||||||
|
d = load()
|
||||||
|
q, F = certifica(d)
|
||||||
|
q = q.merge(F, on=["asset", "ts", "exp"], how="left")
|
||||||
|
q["F"] = q["underlying_price"].fillna(q["F_pcp"])
|
||||||
|
print(f"\n forward disponibile su {q['F'].notna().mean():.1%} delle quote a due lati "
|
||||||
|
f"(osservato dove c'e', altrimenti parita' put-call)")
|
||||||
|
|
||||||
|
for dte_min, etichetta in ((0.0, "SENZA filtro DTE"), (DTE_MIN, f"DTE >= {DTE_MIN:g}g")):
|
||||||
|
print("\n" + "=" * 100)
|
||||||
|
print(f"1-2. ARBITRAGGI STATICI MODEL-FREE su prezzi ESEGUIBILI — {etichetta}")
|
||||||
|
print("=" * 100)
|
||||||
|
v = test_verticale(q, dte_min)
|
||||||
|
print(f" TEST V (monotonia nello strike): {v['n_viol']:,} violazioni su {v['n_confronti']:,} "
|
||||||
|
f"confronti = {v['quota']:.4%}")
|
||||||
|
if v["n_viol"]:
|
||||||
|
print(f" credito mediano {v['credito_med_coin']:.5f} coin = "
|
||||||
|
f"${v['credito_usd_medlot']:.2f} al lotto minimo (LORDO fee)")
|
||||||
|
|
||||||
|
bf = test_farfalla(q, dte_min)
|
||||||
|
bf = in_punti_vol(bf)
|
||||||
|
bf["viol_exec"] = bf["cost_exec"] < 0
|
||||||
|
bf["viol_mid"] = bf["cost_mid"] < 0
|
||||||
|
print(f" TEST B (convessita' nello strike): {len(bf):,} triple consecutive")
|
||||||
|
print(f" al MID (incoerenza, non eseguibile): {bf['viol_mid'].sum():,} = {bf['viol_mid'].mean():.3%}")
|
||||||
|
print(f" ESEGUIBILE (compra ali all'ask, vendi corpo al bid): "
|
||||||
|
f"{bf['viol_exec'].sum():,} = {bf['viol_exec'].mean():.4%}")
|
||||||
|
if dte_min == DTE_MIN:
|
||||||
|
bf_keep = bf
|
||||||
|
|
||||||
|
bf = bf_keep
|
||||||
|
print("\n" + "=" * 100)
|
||||||
|
print("3. QUANTO E' GRANDE L'INCOERENZA, E QUANTO COSTA ATTRAVERSARLA")
|
||||||
|
print("=" * 100)
|
||||||
|
for a in sorted(bf["asset"].unique()):
|
||||||
|
x = bf[bf["asset"] == a]
|
||||||
|
x = x[np.isfinite(x["halfspread_pt"]) & np.isfinite(x["tick_pt"])]
|
||||||
|
r = np.abs(x["iv_resid"].dropna())
|
||||||
|
hs, tk = x["halfspread_pt"], x["tick_pt"]
|
||||||
|
print(f" {a} (n={len(x):,} triple con vega misurabile)")
|
||||||
|
print(f" |residuo di convessita' dello smile| mediana {r.median():.3f} pt-vol · "
|
||||||
|
f"p90 {r.quantile(.9):.3f} · p99 {r.quantile(.99):.3f}")
|
||||||
|
print(f" mezzo spread denaro-lettera mediana {hs.median():.3f} pt-vol · "
|
||||||
|
f"p25 {hs.quantile(.25):.3f} · p75 {hs.quantile(.75):.3f}")
|
||||||
|
print(f" UN TICK della griglia di prezzo mediana {tk.median():.3f} pt-vol "
|
||||||
|
f"-> l'incoerenza e' sotto il tick nel {np.mean(r.reindex(x.index) < tk):.1%} dei casi")
|
||||||
|
both = x.dropna(subset=["iv_resid"])
|
||||||
|
rap = np.abs(both["iv_resid"]) / (2 * both["halfspread_pt"])
|
||||||
|
print(f" |residuo| / (spread PIENO delle gambe): mediana {rap.median():.3f} · "
|
||||||
|
f"p99 {rap.quantile(.99):.3f} · quota > 1 = {np.mean(rap > 1):.3%}")
|
||||||
|
print(f" spread relativo (ask-bid)/mid del venue: mediana {x['relspread'].median():.1%} "
|
||||||
|
f"· p25 {x['relspread'].quantile(.25):.1%}")
|
||||||
|
# valore in dollari dell'incoerenza al MID (il caso migliore possibile: fee 0, spread 0)
|
||||||
|
eq = np.isclose(x["lam"], 0.5, atol=0.02)
|
||||||
|
lot = MIN_LOT[a]
|
||||||
|
val = -x.loc[eq, "cost_mid"].to_numpy() * 2 * lot * x.loc[eq, "spot_feed"].to_numpy()
|
||||||
|
pos = val[val > 0]
|
||||||
|
print(f" valore LORDO al mid della farfalla incoerente, lotto minimo: "
|
||||||
|
f"mediana ${np.median(pos):.2f} · p99 ${np.percentile(pos,99):.2f} · max ${pos.max():.2f}"
|
||||||
|
f" (n={len(pos):,})")
|
||||||
|
|
||||||
|
print("\n economia della farfalla al LOTTO MINIMO (famiglia INVERSE, quella nel dato):")
|
||||||
|
for a in sorted(bf["asset"].unique()):
|
||||||
|
e = economia(bf, a)
|
||||||
|
print(f" {a}: nozionale/lotto ${e['nozionale_min_lotto_usd']:,.0f} · "
|
||||||
|
f"fee 3 gambe mediana ${e['fee_3gambe_usd_med']:.2f} · "
|
||||||
|
f"triple equispaziate {e['n_equisp']:,}")
|
||||||
|
print(f" credito netto: max ${e['netto_usd_max']:.2f} · p99 ${e['netto_usd_p99']:.2f} · "
|
||||||
|
f"positivi {e['n_netto_pos']:,}/{e['n_equisp']:,}")
|
||||||
|
|
||||||
|
print("\n ESTRAPOLAZIONE DICHIARATA alla famiglia USDC (NON nel dato: il collettore raccoglie")
|
||||||
|
print(" le sole inverse — difetto di produzione del filone 11). Il lotto minimo e' 10x piu'")
|
||||||
|
print(" piccolo, ma il muro NON e' il ticket: e' lo spread in punti di vol, che e' una")
|
||||||
|
print(" QUOTA e non un livello -> ridurre il lotto non cambia |residuo|/spread.")
|
||||||
|
for a in sorted(bf["asset"].unique()):
|
||||||
|
x = bf[bf["asset"] == a]
|
||||||
|
x = x[np.isfinite(x["halfspread_pt"])].dropna(subset=["iv_resid"])
|
||||||
|
rap = np.abs(x["iv_resid"]) / (2 * x["halfspread_pt"])
|
||||||
|
rel_inv = x["relspread"].median()
|
||||||
|
for nome, rel_usdc in ((f"{a}_USDC", 0.082 if a == "BTC" else 0.026),):
|
||||||
|
k = rel_inv / rel_usdc # di quanto lo spread sarebbe piu' stretto
|
||||||
|
print(f" {a}: spread relativo inverse {rel_inv:.1%} vs {nome} {rel_usdc:.1%} "
|
||||||
|
f"-> fattore {k:.1f}x; rapporto mediano {rap.median():.3f} -> {rap.median()*k:.3f}, "
|
||||||
|
f"quota > 1 {np.mean(rap > 1):.2%} -> {np.mean(rap * k > 1):.2%}")
|
||||||
|
|
||||||
|
print("\n" + "=" * 100)
|
||||||
|
print("4. TEST C — varianza totale nel tenore (l'unico che tocca un interpolatore)")
|
||||||
|
print("=" * 100)
|
||||||
|
c = test_calendario(q, DTE_MIN)
|
||||||
|
print(f" (i) strike CONDIVISO, nessuna interpolazione: {c['i_viol']:,}/{c['i_n']:,} = {c['i_quota']:.3%}"
|
||||||
|
f" · ampiezza mediana {c['i_amp_med_pt']:.3f} pt-vol (p95 {c['i_amp_p95_pt']:.3f})")
|
||||||
|
print(f" (ii) log-moneyness FISSO, con interpolazione (1 snapshot ogni {c['ii_ogni']}): "
|
||||||
|
f"{c['ii_viol']:,}/{c['ii_n']:,} = {c['ii_quota']:.3%} · ampiezza mediana {c['ii_amp_med_pt']:.3f} pt-vol")
|
||||||
|
print(f" -> contributo dell'INTERPOLATORE = {c['ii_quota'] - c['i_quota']:+.3%} di quota di violazione")
|
||||||
|
|
||||||
|
print("\n" + "=" * 100)
|
||||||
|
print("5. TICK E PERSISTENZA — le due difese contro 'la mia incoerenza e' un artefatto'")
|
||||||
|
print("=" * 100)
|
||||||
|
for col, nome in (("viol_mid", "al MID"), ("viol_exec", "ESEGUIBILE")):
|
||||||
|
v = bf[bf[col]]
|
||||||
|
if len(v) == 0:
|
||||||
|
print(f" {nome}: nessuna violazione")
|
||||||
|
continue
|
||||||
|
legs = np.concatenate([v["bid"].to_numpy(), v["bid_p"].to_numpy(), v["bid_n"].to_numpy()])
|
||||||
|
legs = legs[np.isfinite(legs) & (legs > 0)]
|
||||||
|
nt = legs / tick_of(legs)
|
||||||
|
p = persistenza(bf, col)
|
||||||
|
print(f" {nome}: n={len(v):,} · gambe a <=2 tick {np.mean(nt <= 2):.1%} "
|
||||||
|
f"(mediana {np.median(nt):.0f} tick) · resta in violazione allo snapshot dopo "
|
||||||
|
f"{p.get('resta', float('nan')):.1%} (n={p['n']:,})")
|
||||||
|
|
||||||
|
print("\n" + "=" * 100)
|
||||||
|
print("6. IL RESIDUO SI RICOMPONE? (l'ipotesi del filone, misurata invece che assunta)")
|
||||||
|
print("=" * 100)
|
||||||
|
for a in sorted(bf["asset"].unique()):
|
||||||
|
rv = reversione(bf[bf["asset"] == a])
|
||||||
|
if "beta" not in rv:
|
||||||
|
print(f" {a}: coppie insufficienti (n={rv['n']})")
|
||||||
|
continue
|
||||||
|
print(f" {a}: n={rv['n']:,} coppie appaiate (stessa tripla, snapshot consecutivo <=2h) · "
|
||||||
|
f"beta di ricomposizione {rv['beta']:+.3f} · corr(resid_t, resid_t+1) {rv['corr']:+.3f}")
|
||||||
|
if np.isfinite(rv["meta_vita_h"]):
|
||||||
|
print(f" -> meta-vita {rv['meta_vita_h']:.1f} h (beta=-1 sarebbe ricomposizione totale "
|
||||||
|
f"in un'ora, beta=0 residuo persistente = forma dello smile, non errore)")
|
||||||
|
|
||||||
|
print("\n" + "=" * 100)
|
||||||
|
print("7. ROBUSTEZZA D'ANCORA — il risultato dipende dall'ora in cui guardo?")
|
||||||
|
print("=" * 100)
|
||||||
|
h = bf.assign(ora=bf["ts"].dt.hour).groupby("ora").agg(
|
||||||
|
mid=("viol_mid", "mean"), ex=("viol_exec", "mean"), n=("viol_mid", "size"))
|
||||||
|
print(f" quota di incoerenza al MID per ora UTC: mediana {h['mid'].median():.3%} · "
|
||||||
|
f"min {h['mid'].min():.3%} (h{h['mid'].idxmin():02d}) · max {h['mid'].max():.3%} (h{h['mid'].idxmax():02d})")
|
||||||
|
print(f" quota ESEGUIBILE per ora UTC: max {h['ex'].max():.5%} su {len(h)} ore -> "
|
||||||
|
f"nessuna ora del giorno apre una finestra")
|
||||||
|
|
||||||
|
print("\n" + "=" * 100)
|
||||||
|
print("SINTESI")
|
||||||
|
print("=" * 100)
|
||||||
|
nv = int(bf["viol_exec"].sum()); nt = len(bf)
|
||||||
|
print(f" * arbitraggio statico ESEGUIBILE sullo strike: {nv} su {nt:,} triple = {nv/nt:.6%}")
|
||||||
|
print(f" * incoerenza al MID: {bf['viol_mid'].mean():.2%} delle triple, "
|
||||||
|
f"ma mediana {np.abs(bf['iv_resid']).median():.3f} punti di vol")
|
||||||
|
print(f" * mezzo spread mediano: BTC {bf[bf.asset=='BTC']['halfspread_pt'].median():.3f} pt-vol, "
|
||||||
|
f"ETH {bf[bf.asset=='ETH']['halfspread_pt'].median():.3f} pt-vol")
|
||||||
|
print(" * il credito netto della migliore farfalla eseguibile in 1,03M osservazioni e' NEGATIVO")
|
||||||
|
print(" -> l'incoerenza ESISTE e vale meno del costo di attraversarla: verdetto SCARTATO.")
|
||||||
|
print(f"\n[tempo {time.time()-t0:.1f}s]")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,873 @@
|
|||||||
|
"""r0822b_tp01_sinistro — quanto costa l'ASSICURAZIONE TP01 su un conto a barriera (2026-08-22).
|
||||||
|
|
||||||
|
IL BUCO CHE CHIUDE
|
||||||
|
------------------
|
||||||
|
`r0822_prop_alloc.py` (stessa ondata) ha ottimizzato l'allocazione di un conto **funded** sotto
|
||||||
|
barriera di drawdown e ha trovato l'ottimo a **TP01 25 / SKH01 25 / XS01 50**, cioe' TP01 pesato
|
||||||
|
**tre volte meno** del libro live (75%). Ha anche dichiarato il proprio limite:
|
||||||
|
|
||||||
|
"la sotto-pesatura di TP01, lo sleeve DIFENSIVO, e' misurata su un campione SENZA SINISTRO
|
||||||
|
(2024-2026 non contiene un crash): su una barriera assorbente quella scommessa si paga una
|
||||||
|
volta sola."
|
||||||
|
|
||||||
|
E questo si scontra con due misure del progetto:
|
||||||
|
(a) il **leave-one-out de-luckato** del 26/07: TP01 e' il maggior contributore al Sharpe FULL
|
||||||
|
(+0.390, positivo nel 100% delle estrazioni) e il suo hold-out negativo (-0.200, negativo
|
||||||
|
nel 99.1%) e' *la firma dell'assicurazione, non della morte* — "uno sleeve difensivo si
|
||||||
|
giudica sul SINISTRO, non sul premio";
|
||||||
|
(b) `edge_watch` criterio B: negli **8 anni su 8** con drawdown buy&hold > 10% TP01 ha protetto
|
||||||
|
da **1.8x a 34.4x**.
|
||||||
|
|
||||||
|
DOMANDA: qual e' il peso giusto di TP01 se l'obiettivo include sopravvivere a un crash che nel
|
||||||
|
campione di ottimizzazione NON c'e'?
|
||||||
|
|
||||||
|
IPOTESI REGISTRATA PRIMA DI MISURARE
|
||||||
|
------------------------------------
|
||||||
|
Mi aspettavo che (1) la sotto-pesatura di TP01 costasse molto **nel sinistro** e poco fuori, e che
|
||||||
|
(2) esistesse un peso di compromesso (~50%) dentro la regione robusta del funded e non
|
||||||
|
catastrofico al crash. In altre parole mi aspettavo di **confermare** il caveat di PROP-ALLOC.
|
||||||
|
Se la misura dice il contrario, e' un risultato e va scritto come tale.
|
||||||
|
|
||||||
|
CIO' CHE E' UN'ASSUNZIONE E CIO' CHE E' UN DATO
|
||||||
|
-----------------------------------------------
|
||||||
|
* Lo **scenario di stress e' uno SCENARIO, non una previsione.** Due costruzioni, entrambe
|
||||||
|
dichiarate: (S1) bootstrap MISTO — ogni blocco ha probabilita' `p` di essere estratto dal pool
|
||||||
|
dei sinistri 2019-2023 invece che dalla finestra normale; (S2) **replay** — un episodio di
|
||||||
|
crash VERO innestato per intero, in ordine, a una data casuale dentro l'anno. In piu' il
|
||||||
|
replay deterministico del **2022 intero** (aprile-dicembre), che non e' un modello: e' successo.
|
||||||
|
* Il vincolo binding di TUTTO il filone e' che i sinistri osservati sono **QUATTRO**. Nessun
|
||||||
|
bootstrap crea informazione: l'ampiezza effettiva della coda resta 4 episodi, e ogni numero qui
|
||||||
|
eredita quell'errore. Detto una volta, vale per ogni tabella.
|
||||||
|
* Lente **ACCOPPIATA** (`r0725_prop_coupled`): il minimo intraday si compone ESATTO sul path
|
||||||
|
orario condiviso TP01/SKH01. Non e' un dettaglio: su una regola a UN giorno (daily-loss) la
|
||||||
|
lente close-only e' misurata **esattamente cieca** (0.00 breach contro 0.40-1.21 veri), mentre
|
||||||
|
su una statistica multi-giorno l'errore vale il 3.5%. Il vincolo funded E' una regola a un giorno.
|
||||||
|
* de-luck sul drift **x0.89** (fattore MISURATO il 26/07). ⚠️ L'offset di de-luck e' calcolato UNA
|
||||||
|
VOLTA sulla finestra 2019+ e sottratto ovunque in valore ASSOLUTO: de-luckare un pool di crash
|
||||||
|
sulla sua PROPRIA media (negativa) *aggiungerebbe* drift proprio dove non deve.
|
||||||
|
* macchineria RIUSATA, non riscritta: `r0725_prop_coupled` (tuple accoppiate) e `r0822_prop_alloc`
|
||||||
|
(`eval_sim`, `funded_sim`, `boot_idx`, `FIRMS`). Qui si aggiungono solo i pool e gli innesti.
|
||||||
|
|
||||||
|
Uso: `nice -n 19 timeout 900 uv run python scripts/research/r0822b_tp01_sinistro.py`
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[2]
|
||||||
|
sys.path.insert(0, str(ROOT))
|
||||||
|
sys.path.insert(0, str(ROOT / "scripts" / "research"))
|
||||||
|
|
||||||
|
from src.data.downloader import load_data # noqa: E402
|
||||||
|
from src.portfolio.sleeves import _tp01_returns, _skyhook_returns # noqa: E402
|
||||||
|
|
||||||
|
import r0725_prop_coupled as pc # noqa: E402
|
||||||
|
import r0822_prop_alloc as PA # noqa: E402
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------ costanti DICHIARATE
|
||||||
|
DELUCK = PA.DELUCK # 0.89, misurato il 26/07
|
||||||
|
FULL_START = "2019-03-14" # inizio del recon crypto (TP01+SKH01)
|
||||||
|
RECENT_START = "2024-01-01" # finestra di ottimizzazione di PROP-ALLOC (nasce XS01)
|
||||||
|
BLOCK = 20 # blocco del bootstrap, in giorni
|
||||||
|
N_PATHS = 2500
|
||||||
|
EPI_LEN = 30 # lunghezza di un episodio di sinistro, in giorni
|
||||||
|
HORIZON = 365
|
||||||
|
LEVS = (0.25, 0.375, 0.50, 0.625, 0.75, 1.00, 1.25, 1.50)
|
||||||
|
W_GRID = tuple(i / 8 for i in range(9)) # peso di TP01 nella coppia crypto, 0 -> 1
|
||||||
|
SURV_TARGET = 0.90 # livello di sopravvivenza della frontiera ISO-SOPRAVVIVENZA
|
||||||
|
|
||||||
|
N_EVAL = 0 # contatore onesto delle valutazioni (griglia dichiarata)
|
||||||
|
|
||||||
|
|
||||||
|
def hr(t: str = "") -> None:
|
||||||
|
print("\n" + "-" * 106)
|
||||||
|
if t:
|
||||||
|
print(f" {t}")
|
||||||
|
print("-" * 106)
|
||||||
|
|
||||||
|
|
||||||
|
# ================================================================== §A. PANNELLO
|
||||||
|
|
||||||
|
_TUP: dict = {}
|
||||||
|
|
||||||
|
|
||||||
|
def crypto(w_tp: float) -> tuple[np.ndarray, np.ndarray, pd.DatetimeIndex]:
|
||||||
|
"""(R, gap) giornalieri della coppia crypto a peso TP01 = w_tp, SKH01 = 1-w_tp.
|
||||||
|
gap = minimo_intraday - chiusura (<=0). Storia piena dal 2019-03."""
|
||||||
|
k = round(float(w_tp), 6)
|
||||||
|
if k not in _TUP:
|
||||||
|
c = pc.crypto_daily_tuples(k, 1.0 - k)
|
||||||
|
R = c["R"].values.astype(float)
|
||||||
|
G = np.minimum(c["m"].values, R) - R
|
||||||
|
_TUP[k] = (R, G, c.index)
|
||||||
|
R, G, I = _TUP[k]
|
||||||
|
return R.copy(), G.copy(), I
|
||||||
|
|
||||||
|
|
||||||
|
_OFF: dict = {}
|
||||||
|
|
||||||
|
|
||||||
|
def deluck_offset(w_tp: float, factor: float = DELUCK) -> float:
|
||||||
|
"""Offset ASSOLUTO di de-luck, calcolato UNA VOLTA sulla finestra 2019+ e riusato ovunque.
|
||||||
|
Calcolarlo sulla media di un sotto-campione di crash (negativa) *aggiungerebbe* drift."""
|
||||||
|
key = (round(float(w_tp), 6), round(float(factor), 4))
|
||||||
|
if key not in _OFF:
|
||||||
|
R, _, I = crypto(w_tp)
|
||||||
|
_OFF[key] = float((1.0 - factor) * R[I >= pd.Timestamp(FULL_START, tz="UTC")].mean())
|
||||||
|
return _OFF[key]
|
||||||
|
|
||||||
|
|
||||||
|
def buyhold_daily(idx: pd.DatetimeIndex) -> pd.Series:
|
||||||
|
"""Buy&hold 50/50 BTC+ETH sulla stessa griglia giornaliera del libro."""
|
||||||
|
bh = None
|
||||||
|
for a in ("BTC", "ETH"):
|
||||||
|
d = load_data(a, "1h")
|
||||||
|
i = pd.DatetimeIndex(pd.to_datetime(d["datetime"], utc=True))
|
||||||
|
s = pd.Series(d["close"].astype(float).values, index=i).resample("1D").last()
|
||||||
|
r = s.pct_change().fillna(0.0) * 0.5
|
||||||
|
bh = r if bh is None else bh.add(r, fill_value=0.0)
|
||||||
|
return bh.reindex(idx).fillna(0.0)
|
||||||
|
|
||||||
|
|
||||||
|
def worst_windows(lbh: np.ndarray, idx: pd.DatetimeIndex, K: int, L: int,
|
||||||
|
hi: int | None = None) -> list[int]:
|
||||||
|
"""Le K finestre di L giorni NON sovrapposte col peggior rendimento buy&hold, greedy sul
|
||||||
|
minimo. `hi` limita i possibili inizi (per restare dentro 2019-2023)."""
|
||||||
|
cs = np.concatenate(([0.0], np.cumsum(lbh)))
|
||||||
|
win = np.expm1(cs[L:] - cs[:-L])
|
||||||
|
cand = np.arange(len(win)) if hi is None else np.arange(min(hi, len(win)))
|
||||||
|
order = cand[np.argsort(win[cand])]
|
||||||
|
out: list[int] = []
|
||||||
|
for t in order:
|
||||||
|
if all(abs(int(t) - q) >= L for q in out):
|
||||||
|
out.append(int(t))
|
||||||
|
if len(out) == K:
|
||||||
|
break
|
||||||
|
return sorted(out)
|
||||||
|
|
||||||
|
|
||||||
|
def binom_two_sided(k: int, n: int) -> float:
|
||||||
|
"""p-value esatto a due code di un test dei segni con p0=0.5 (n piccolo: si conta)."""
|
||||||
|
from math import comb
|
||||||
|
tot = 2 ** n
|
||||||
|
kk = max(k, n - k)
|
||||||
|
tail = sum(comb(n, j) for j in range(kk, n + 1))
|
||||||
|
return min(1.0, 2.0 * tail / tot)
|
||||||
|
|
||||||
|
|
||||||
|
def maxdd(r: np.ndarray) -> float:
|
||||||
|
eq = np.cumprod(1.0 + r)
|
||||||
|
return float((eq / np.maximum.accumulate(eq) - 1.0).min())
|
||||||
|
|
||||||
|
|
||||||
|
# ================================================================== §B. SIMULAZIONE
|
||||||
|
|
||||||
|
def J_of(R: np.ndarray, G: np.ndarray, ev_idx: np.ndarray, fu_idx: np.ndarray,
|
||||||
|
lev: float, firm: str = "HYRO") -> dict:
|
||||||
|
"""J = P(passare l'eval) x P(essere vivo a 12 mesi da funded), lente ACCOPPIATA."""
|
||||||
|
global N_EVAL
|
||||||
|
N_EVAL += 1
|
||||||
|
e = PA.eval_sim(R, G, ev_idx, lev, firm, "coupled")
|
||||||
|
f = PA.funded_sim(R, G, fu_idx, lev, firm, 100_000.0, "coupled")
|
||||||
|
return dict(J=e["p_pass"] * f["p_alive"], p_pass=e["p_pass"], p_alive=f["p_alive"],
|
||||||
|
e_payout=f["e_payout"], jvec=(e["passed"] & f["alive"]).astype(float),
|
||||||
|
alive=f["alive"])
|
||||||
|
|
||||||
|
|
||||||
|
def best_lev(R: np.ndarray, G: np.ndarray, ev_idx, fu_idx, firm: str = "HYRO") -> tuple:
|
||||||
|
rows = [(lv, J_of(R, G, ev_idx, fu_idx, lv, firm)) for lv in LEVS]
|
||||||
|
return max(rows, key=lambda x: x[1]["J"])
|
||||||
|
|
||||||
|
|
||||||
|
def pool_paths(seg_bounds: list[tuple[int, int]], n_days: int, n_paths: int, seed: int,
|
||||||
|
block: int = BLOCK) -> np.ndarray:
|
||||||
|
"""Indici di bootstrap a blocchi ristretti a segmenti CONTIGUI (ogni blocco vive dentro un
|
||||||
|
solo segmento -> non si incollano giorni di episodi diversi)."""
|
||||||
|
rng = np.random.default_rng(seed)
|
||||||
|
starts = []
|
||||||
|
for a, b in seg_bounds:
|
||||||
|
if b - a >= block:
|
||||||
|
starts.extend(range(a, b - block + 1))
|
||||||
|
starts = np.array(starts)
|
||||||
|
nb = int(np.ceil(n_days / block))
|
||||||
|
st = starts[rng.integers(0, len(starts), size=(n_paths, nb))]
|
||||||
|
return (st[:, :, None] + np.arange(block)[None, None, :]).reshape(n_paths, -1)[:, :n_days]
|
||||||
|
|
||||||
|
|
||||||
|
def mix_paths(base_idx: np.ndarray, crash_starts: np.ndarray, p: float, seed: int,
|
||||||
|
block: int = BLOCK) -> np.ndarray:
|
||||||
|
"""S1 — bootstrap MISTO: ogni blocco della path normale ha probabilita' `p` di essere
|
||||||
|
sostituito da un blocco estratto dal pool dei SINISTRI."""
|
||||||
|
rng = np.random.default_rng(seed)
|
||||||
|
out = base_idx.copy()
|
||||||
|
P, D = out.shape
|
||||||
|
nb = int(np.ceil(D / block))
|
||||||
|
hit = rng.random((P, nb)) < p
|
||||||
|
for b in range(nb):
|
||||||
|
sl = slice(b * block, min((b + 1) * block, D))
|
||||||
|
L = sl.stop - sl.start
|
||||||
|
rows = np.where(hit[:, b])[0]
|
||||||
|
if len(rows) == 0:
|
||||||
|
continue
|
||||||
|
st = crash_starts[rng.integers(0, len(crash_starts), size=len(rows))]
|
||||||
|
out[rows, sl] = st[:, None] + np.arange(L)[None, :]
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def splice_paths(base_idx: np.ndarray, epi_idx: np.ndarray, seed: int) -> np.ndarray:
|
||||||
|
"""S2 — REPLAY: l'episodio VERO (contiguo, in ordine) innestato una volta sola a una data
|
||||||
|
casuale dentro l'orizzonte. E' la domanda operativa: *se il sinistro arriva*."""
|
||||||
|
rng = np.random.default_rng(seed)
|
||||||
|
out = base_idx.copy()
|
||||||
|
P, D = out.shape
|
||||||
|
L = len(epi_idx)
|
||||||
|
st = rng.integers(0, max(D - L, 1), size=P)
|
||||||
|
for i in range(P):
|
||||||
|
a = int(st[i])
|
||||||
|
out[i, a:a + L] = epi_idx[:min(L, D - a)]
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
# ================================================================== main
|
||||||
|
|
||||||
|
def main() -> None: # noqa: C901
|
||||||
|
try:
|
||||||
|
sys.stdout.reconfigure(line_buffering=True)
|
||||||
|
except Exception: # pragma: no cover
|
||||||
|
pass
|
||||||
|
t0 = time.time()
|
||||||
|
|
||||||
|
def tick(lab: str) -> None:
|
||||||
|
print(f" [{time.time()-t0:6.0f}s] {lab} (valutazioni finora: {N_EVAL})")
|
||||||
|
|
||||||
|
print("=" * 106)
|
||||||
|
print(" TP01-SINISTRO — quanto costa l'assicurazione su un conto a barriera, e quanto rende")
|
||||||
|
print(" quando il sinistro arriva. Lente ACCOPPIATA, firm HYRO.")
|
||||||
|
print("=" * 106)
|
||||||
|
|
||||||
|
R75, G75, IDX = crypto(0.75)
|
||||||
|
n_full = len(IDX)
|
||||||
|
i_rec = int(np.searchsorted(IDX, pd.Timestamp(RECENT_START, tz="UTC")))
|
||||||
|
print(f"\n finestra crypto piena: {n_full} giorni {IDX[0].date()} -> {IDX[-1].date()}")
|
||||||
|
print(f" finestra di ottimizzazione di PROP-ALLOC: {n_full - i_rec} giorni dal "
|
||||||
|
f"{IDX[i_rec].date()} (e' anche la finestra di SCOPERTA di XS01)")
|
||||||
|
|
||||||
|
# --------------------------------------------------------- sanity: recon vs sleeve ufficiale
|
||||||
|
print("\n SANITY — il recon orario contro gli sleeve ufficiali. TP01/SKH01 NON coincidono")
|
||||||
|
print(" barra per barra per costruzione (il recon e' un mark-to-market ORARIO, lo sleeve")
|
||||||
|
print(" marca a fine-trade): il controllo giusto e' il log-totale.")
|
||||||
|
for nm, off_s, w in (("TP01", _tp01_returns(), 1.0), ("SKH01", _skyhook_returns(), 0.0)):
|
||||||
|
o = off_s.dropna().sort_index()
|
||||||
|
if o.index.tz is None:
|
||||||
|
o.index = o.index.tz_localize("UTC")
|
||||||
|
Rn, _, In = crypto(w)
|
||||||
|
com = o.index.intersection(In)
|
||||||
|
a = o.reindex(com).values
|
||||||
|
b = pd.Series(Rn, index=In).reindex(com).values
|
||||||
|
print(f" {nm:>6} log-tot sleeve {float(np.log1p(a).sum()):+8.4f} "
|
||||||
|
f"recon {float(np.log1p(b).sum()):+8.4f} scarto "
|
||||||
|
f"{float(np.log1p(a).sum()-np.log1p(b).sum()):+7.4f}")
|
||||||
|
|
||||||
|
# ============================================================ §1. CHE COS'E' UN SINISTRO
|
||||||
|
hr("§1. CHE COS'E' UN SINISTRO — e la frase '2024-2026 non contiene un crash' e' FALSA")
|
||||||
|
bh = buyhold_daily(IDX)
|
||||||
|
lbh = np.log1p(bh.values)
|
||||||
|
print(" Peggior rendimento buy&hold 50/50 su finestra mobile, per periodo:")
|
||||||
|
print(f" {'W':>4} {'2019-2023 min':>15} {'2024+ min':>12} {'rapporto':>10} "
|
||||||
|
f"{'2019-23 p1':>11} {'2024+ p1':>10} {'data del min 2024+':>20}")
|
||||||
|
ratios = []
|
||||||
|
for W in (1, 5, 10, 20, 60):
|
||||||
|
cs = np.concatenate(([0.0], np.cumsum(lbh)))
|
||||||
|
roll = np.expm1(cs[W:] - cs[:-W]) # finestra che FINISCE in t=W-1+k
|
||||||
|
tt = IDX[W - 1:]
|
||||||
|
pre = roll[tt < pd.Timestamp(RECENT_START, tz="UTC")]
|
||||||
|
post = roll[tt >= pd.Timestamp(RECENT_START, tz="UTC")]
|
||||||
|
tpost = tt[tt >= pd.Timestamp(RECENT_START, tz="UTC")]
|
||||||
|
rr = pre.min() / post.min()
|
||||||
|
ratios.append(rr)
|
||||||
|
print(f" {W:>4} {pre.min():>15.1%} {post.min():>12.1%} {rr:>10.2f}x "
|
||||||
|
f"{np.percentile(pre, 1):>11.1%} {np.percentile(post, 1):>10.1%} "
|
||||||
|
f"{str(tpost[int(np.argmin(post))].date()):>20}")
|
||||||
|
print(f"\n -> il 2024+ **contiene** un crash: peggior giorno -14.5%, peggiori 20g -39.6%,")
|
||||||
|
print(f" drawdown buy&hold fino a -60%. Cio' che NON contiene e' un crash della TAGLIA")
|
||||||
|
print(f" dei precedenti: il rapporto e' {np.mean(ratios[:3]):.1f}x sulla coda veloce (1-10g)")
|
||||||
|
print(f" e {ratios[-1]:.1f}x a 60 giorni. **La frase giusta non e' 'senza sinistro' ma")
|
||||||
|
print(f" 'con un sinistro di taglia dimezzata'** — e cambia la domanda: non 'cosa succede")
|
||||||
|
print(f" se arriva un crash' ma 'cosa succede se ne arriva uno DUE VOLTE piu' grande'.")
|
||||||
|
|
||||||
|
# ----- replica del criterio B di edge_watch (protezione per anno)
|
||||||
|
print("\n Criterio B di `edge_watch` replicato qui (anni con DD buy&hold > 10%):")
|
||||||
|
tp_only, _, _ = crypto(1.0)
|
||||||
|
ser_bh = pd.Series(bh.values, index=IDX)
|
||||||
|
ser_tp = pd.Series(tp_only, index=IDX)
|
||||||
|
print(f" {'anno':>6} {'DD b&h':>9} {'DD TP01':>9} {'protezione':>12} {'DD SKH01':>10} "
|
||||||
|
f"{'DD 75/25':>10}")
|
||||||
|
skh_only, _, _ = crypto(0.0)
|
||||||
|
ser_sk = pd.Series(skh_only, index=IDX)
|
||||||
|
ser_75 = pd.Series(R75, index=IDX)
|
||||||
|
n_sin = n_ok = 0
|
||||||
|
for y, g in ser_bh.groupby(ser_bh.index.year):
|
||||||
|
d_bh = maxdd(g.values)
|
||||||
|
if d_bh > -0.10:
|
||||||
|
continue
|
||||||
|
n_sin += 1
|
||||||
|
d_tp = maxdd(ser_tp.loc[g.index].values)
|
||||||
|
d_sk = maxdd(ser_sk.loc[g.index].values)
|
||||||
|
d_75 = maxdd(ser_75.loc[g.index].values)
|
||||||
|
ok = (d_tp / d_bh) <= 0.75
|
||||||
|
n_ok += ok
|
||||||
|
print(f" {y:>6} {d_bh:>9.1%} {d_tp:>9.1%} {d_bh/d_tp:>11.1f}x {d_sk:>10.1%} "
|
||||||
|
f"{d_75:>10.1%} {'ok' if ok else 'FAIL'}")
|
||||||
|
print(f" -> anni di sinistro {n_sin}, superati {n_ok}. **Ogni anno e' un anno di sinistro")
|
||||||
|
print(f" per il buy&hold**: la 'frequenza del sinistro' non e' un evento raro, e' lo")
|
||||||
|
print(f" stato normale dell'asset. Cio' che e' raro e' la TAGLIA (riga sopra).")
|
||||||
|
|
||||||
|
# ----- definizione degli episodi
|
||||||
|
hr("§1-bis. GLI EPISODI DI SINISTRO — regola di selezione DICHIARATA PRIMA")
|
||||||
|
print(f" Regola: le {3} finestre di {EPI_LEN} giorni NON sovrapposte con il peggior rendimento")
|
||||||
|
print(f" buy&hold nel 2019-2023 (greedy sul minimo), PIU' l'FTX 2022-11-05 aggiunto **per")
|
||||||
|
print(f" NOME** (evento noto, non selezionato dal minimo: e' un controllo, non una scelta).")
|
||||||
|
cs = np.concatenate(([0.0], np.cumsum(lbh)))
|
||||||
|
win = np.expm1(cs[EPI_LEN:] - cs[:-EPI_LEN]) # finestra che INIZIA in t
|
||||||
|
pre_mask = IDX[:len(win)] < pd.Timestamp(RECENT_START, tz="UTC")
|
||||||
|
cand = np.where(pre_mask)[0]
|
||||||
|
order = cand[np.argsort(win[cand])]
|
||||||
|
picked = []
|
||||||
|
for t in order:
|
||||||
|
if all(abs(int(t) - p) >= EPI_LEN for p in picked):
|
||||||
|
picked.append(int(t))
|
||||||
|
if len(picked) == 3:
|
||||||
|
break
|
||||||
|
t_ftx = int(np.searchsorted(IDX, pd.Timestamp("2022-11-05", tz="UTC")))
|
||||||
|
epis = sorted(picked + [t_ftx])
|
||||||
|
NAMES = {}
|
||||||
|
print(f"\n {'episodio':>12} {'dal':>12} {'al':>12} {'b&h 30g':>10} {'peggior 1g b&h':>16}")
|
||||||
|
for t in epis:
|
||||||
|
lab = ("COVID" if IDX[t].year == 2020 else
|
||||||
|
"MAG-2021" if (IDX[t].year, IDX[t].month) == (2021, 5) else
|
||||||
|
"LUNA" if (IDX[t].year, IDX[t].month) in ((2022, 5), (2022, 6)) else
|
||||||
|
"FTX" if (IDX[t].year, IDX[t].month) == (2022, 11) else f"EPI-{IDX[t].date()}")
|
||||||
|
NAMES[t] = lab
|
||||||
|
seg = bh.values[t:t + EPI_LEN]
|
||||||
|
print(f" {lab:>12} {str(IDX[t].date()):>12} {str(IDX[t+EPI_LEN-1].date()):>12} "
|
||||||
|
f"{win[t]:>10.1%} {seg.min():>16.1%}")
|
||||||
|
crash_days = np.concatenate([np.arange(t, t + EPI_LEN) for t in epis])
|
||||||
|
freq_day = len(crash_days) / n_full
|
||||||
|
print(f"\n giorni di sinistro {len(crash_days)} su {n_full} = **{freq_day:.1%} del tempo**")
|
||||||
|
print(f" ⚠️ AMPIEZZA EFFETTIVA DELLA CODA = **4 EPISODI**. Nessun bootstrap la aumenta.")
|
||||||
|
print(f" Ogni numero di questo file eredita l'errore di un campione di quattro eventi.")
|
||||||
|
|
||||||
|
tick("§1 sinistri")
|
||||||
|
|
||||||
|
# ============================================================ §2. IL CONTO DELL'ASSICURAZIONE
|
||||||
|
hr("§2. IL CONTO DELL'ASSICURAZIONE — premio fuori dal sinistro, indennizzo dentro")
|
||||||
|
print(" Confronto DUE ripartizioni della coppia crypto, appaiato giorno per giorno:")
|
||||||
|
print(" LIVE = TP01 75 / SKH01 25 (il libro che gira)")
|
||||||
|
print(" OPT = TP01 38 / SKH01 62 (l'ottimo di PROP-ALLOC sul campione 2019+)")
|
||||||
|
in_sin = np.zeros(n_full, bool)
|
||||||
|
in_sin[crash_days] = True
|
||||||
|
print(f"\n {'config':>10} {'drift fuori':>12} {'drift dentro':>13} {'Sharpe full':>12} "
|
||||||
|
f"{'maxDD':>8} {'peggior 1g':>11} {'peggior min 1g':>15}")
|
||||||
|
prof = {}
|
||||||
|
for lab, w in (("LIVE 75/25", 0.75), ("OPT 38/62", 0.375), ("TP01 puro", 1.0),
|
||||||
|
("SKH01 puro", 0.0)):
|
||||||
|
R, G, _ = crypto(w)
|
||||||
|
R = R - deluck_offset(w)
|
||||||
|
out_d = R[~in_sin].mean() * 365.0
|
||||||
|
in_d = R[in_sin].mean() * 365.0
|
||||||
|
vol = R.std() * np.sqrt(365.0)
|
||||||
|
prof[lab] = dict(out=out_d, ind=in_d, sh=R.mean() * 365.0 / vol, dd=maxdd(R),
|
||||||
|
w1=R.min(), wm=(R + G).min())
|
||||||
|
p = prof[lab]
|
||||||
|
print(f" {lab:>10} {p['out']:>12.1%} {p['ind']:>13.1%} {p['sh']:>12.2f} {p['dd']:>8.1%} "
|
||||||
|
f"{p['w1']:>11.2%} {p['wm']:>15.2%}")
|
||||||
|
prem = prof["LIVE 75/25"]["out"] - prof["OPT 38/62"]["out"]
|
||||||
|
ind = prof["LIVE 75/25"]["ind"] - prof["OPT 38/62"]["ind"]
|
||||||
|
R_live = crypto(0.75)[0] - deluck_offset(0.75)
|
||||||
|
R_opt = crypto(0.375)[0] - deluck_offset(0.375)
|
||||||
|
cum_live = float(np.expm1(np.log1p(R_live[in_sin]).sum()))
|
||||||
|
cum_opt = float(np.expm1(np.log1p(R_opt[in_sin]).sum()))
|
||||||
|
print(f"\n PREMIO (LIVE - OPT) fuori dal sinistro : {prem:+.2%}/anno = "
|
||||||
|
f"{prem*100_000:+,.0f} $/anno su un funded da $100k")
|
||||||
|
print(f" INDENNIZZO (LIVE - OPT) sui 120 giorni di sinistro, CUMULATO (non annualizzato,")
|
||||||
|
print(f" che su 120 giorni sarebbe una finta precisione): LIVE {cum_live:+.1%} contro "
|
||||||
|
f"OPT {cum_opt:+.1%} = {cum_live-cum_opt:+.1%}")
|
||||||
|
print(f" su un funded da $100k: {(cum_live-cum_opt)*100_000:+,.0f} $ per l'INTERO "
|
||||||
|
f"monte-sinistri di {n_full/365.0:.1f} anni")
|
||||||
|
print(f" frequenza: {freq_day:.1%} del tempo dentro un sinistro (4 episodi in "
|
||||||
|
f"{n_full/365.0:.1f} anni = uno ogni {n_full/365.0/4:.1f} anni)")
|
||||||
|
if ind < 0:
|
||||||
|
print("\n 🚨 **IL SEGNO E' ROVESCIATO RISPETTO ALLA MIA IPOTESI.** Dentro il sinistro il")
|
||||||
|
print(" libro con MENO TP01 fa MEGLIO, non peggio. Il meccanismo si legge in una riga:")
|
||||||
|
print(" TP01 nel crash va FLAT (non perde, non guadagna: e' assicurazione), SKH01 va")
|
||||||
|
print(" SHORT (guadagna). Su un conto a barriera *guadagnare batte non-perdere*.")
|
||||||
|
print("\n Rendimento dei 4 episodi, per peso di TP01 (30 giorni ciascuno, chiusura e MINIMO):")
|
||||||
|
print(f" {'w TP01':>8} " + " ".join(f"{NAMES[t]:>18}" for t in epis) + f" {'peggior min':>13}")
|
||||||
|
for w in (1.0, 0.75, 0.5, 0.375, 0.25, 0.0):
|
||||||
|
R, G, _ = crypto(w)
|
||||||
|
cells = []
|
||||||
|
wm = 0.0
|
||||||
|
for t in epis:
|
||||||
|
seg = R[t:t + EPI_LEN]
|
||||||
|
gp = G[t:t + EPI_LEN]
|
||||||
|
eq = np.cumprod(1.0 + seg)
|
||||||
|
mn = float(np.minimum(eq - 1.0, np.concatenate(([1.0], eq[:-1])) * (1 + seg + gp) - 1.0).min())
|
||||||
|
wm = min(wm, mn)
|
||||||
|
cells.append(f"{float(eq[-1]-1.0):+7.1%}/{mn:+7.1%}")
|
||||||
|
print(f" {w:>8.3f} " + " ".join(f"{c:>18}" for c in cells) + f" {wm:>13.1%}")
|
||||||
|
print(" (formato: chiusura a 30g / minimo intraday cumulato dell'episodio)")
|
||||||
|
|
||||||
|
print("\n ⚠️ QUANTA POTENZA HA QUESTO SEGNO? Con 4 episodi, '4 su 4 a favore' ha un test dei")
|
||||||
|
print(" segni con p = 0.125 a due code: **non e' significativo al 5%**. Il modo onesto di")
|
||||||
|
print(" guadagnare potenza non e' ripetere il bootstrap (non crea eventi) ma ESTENDERE la")
|
||||||
|
print(" stessa regola di selezione a piu' finestre. Le 12 peggiori finestre di 30g NON")
|
||||||
|
print(" sovrapposte su TUTTO il 2019+ (stessa regola greedy, K da 3 a 12):")
|
||||||
|
w12 = worst_windows(lbh, IDX, 12, EPI_LEN)
|
||||||
|
print(f"\n {'finestra':>12} {'b&h 30g':>9} {'LIVE 75/25':>11} {'OPT 38/62':>11} "
|
||||||
|
f"{'TP01 puro':>10} {'SKH01 puro':>11} {'OPT>LIVE':>9}")
|
||||||
|
wins = 0
|
||||||
|
wins_sk = 0
|
||||||
|
lr_live, lr_opt = np.log1p(R_live), np.log1p(R_opt)
|
||||||
|
R_tp = crypto(1.0)[0] - deluck_offset(1.0)
|
||||||
|
R_sk = crypto(0.0)[0] - deluck_offset(0.0)
|
||||||
|
for t in w12:
|
||||||
|
a = float(np.expm1(lr_live[t:t + EPI_LEN].sum()))
|
||||||
|
b = float(np.expm1(lr_opt[t:t + EPI_LEN].sum()))
|
||||||
|
c = float(np.expm1(np.log1p(R_tp[t:t + EPI_LEN]).sum()))
|
||||||
|
d = float(np.expm1(np.log1p(R_sk[t:t + EPI_LEN]).sum()))
|
||||||
|
wins += b > a
|
||||||
|
wins_sk += d > c
|
||||||
|
print(f" {str(IDX[t].date()):>12} {win[t]:>9.1%} {a:>11.1%} {b:>11.1%} "
|
||||||
|
f"{c:>10.1%} {d:>11.1%} {('si' if b > a else 'no'):>9}")
|
||||||
|
print(f"\n OPT batte LIVE in **{wins}/12** finestre -> test dei segni "
|
||||||
|
f"p = {binom_two_sided(wins, 12):.4f}")
|
||||||
|
print(f" SKH01 puro batte TP01 puro in **{wins_sk}/12** -> p = "
|
||||||
|
f"{binom_two_sided(wins_sk, 12):.4f}")
|
||||||
|
print(" ⚠️ 5 di queste 12 finestre stanno nel 2024+, cioe' DENTRO la finestra su cui")
|
||||||
|
print(" SKH01 e' stato tarato e su cui PROP-ALLOC ha ottimizzato: la potenza sale ma")
|
||||||
|
print(" l'indipendenza scende. Il test va letto come **una diagnostica, non una prova**.")
|
||||||
|
|
||||||
|
tick("§2 conto assicurazione")
|
||||||
|
|
||||||
|
# ============================================================ §3. SOTTO BARRIERA
|
||||||
|
hr("§3. SOTTO BARRIERA — griglia peso TP01 x leva su TRE lenti di campione")
|
||||||
|
print(f" {len(W_GRID)} pesi x {len(LEVS)} leve x 3 finestre. J = P(pass) x P(vivo 12m), HYRO")
|
||||||
|
print(f" (eval +10%/-6% statico/-4% giornaliero; funded -6% statico/-4% giornaliero).")
|
||||||
|
ev_f = PA.boot_idx(n_full, HORIZON, N_PATHS, seed=8230)
|
||||||
|
fu_f = PA.boot_idx(n_full, HORIZON, N_PATHS, seed=8231)
|
||||||
|
n_rec = n_full - i_rec
|
||||||
|
ev_r = PA.boot_idx(n_rec, HORIZON, N_PATHS, seed=8240)
|
||||||
|
fu_r = PA.boot_idx(n_rec, HORIZON, N_PATHS, seed=8241)
|
||||||
|
seg_sin = [(t, t + EPI_LEN) for t in epis]
|
||||||
|
ev_s = pool_paths(seg_sin, HORIZON, N_PATHS, seed=8250)
|
||||||
|
fu_s = pool_paths(seg_sin, HORIZON, N_PATHS, seed=8251)
|
||||||
|
|
||||||
|
print(f"\n {'w TP01':>7} | {'2019+ (col 2022)':>26} | {'2024+ (PROP-ALLOC)':>26} | "
|
||||||
|
f"{'SOLO-SINISTRO':>26}")
|
||||||
|
print(f" {'':>7} | {'leva':>6} {'J':>7} {'P(viv)':>7} | {'leva':>6} {'J':>7} {'P(viv)':>7} | "
|
||||||
|
f"{'leva':>6} {'J':>7} {'P(viv)':>7}")
|
||||||
|
tab = {}
|
||||||
|
for w in W_GRID:
|
||||||
|
R, G, _ = crypto(w)
|
||||||
|
off = deluck_offset(w)
|
||||||
|
Rf, Gf = R - off, G
|
||||||
|
Rr, Gr = Rf[i_rec:], Gf[i_rec:]
|
||||||
|
lv1, o1 = best_lev(Rf, Gf, ev_f, fu_f)
|
||||||
|
lv2, o2 = best_lev(Rr, Gr, ev_r, fu_r)
|
||||||
|
lv3, o3 = best_lev(Rf, Gf, ev_s, fu_s)
|
||||||
|
tab[w] = dict(full=(lv1, o1), rec=(lv2, o2), sin=(lv3, o3))
|
||||||
|
print(f" {w:>7.3f} | {lv1:>6.3f} {o1['J']:>7.3f} {o1['p_alive']:>7.1%} | "
|
||||||
|
f"{lv2:>6.3f} {o2['J']:>7.3f} {o2['p_alive']:>7.1%} | "
|
||||||
|
f"{lv3:>6.3f} {o3['J']:>7.3f} {o3['p_alive']:>7.1%}")
|
||||||
|
a_full = max(tab, key=lambda w: tab[w]["full"][1]["J"])
|
||||||
|
a_rec = max(tab, key=lambda w: tab[w]["rec"][1]["J"])
|
||||||
|
a_sin = max(tab, key=lambda w: tab[w]["sin"][1]["J"])
|
||||||
|
print(f"\n argmax: 2019+ -> TP01 {a_full:.3f} | 2024+ -> TP01 {a_rec:.3f} | "
|
||||||
|
f"SOLO-SINISTRO -> TP01 {a_sin:.3f}")
|
||||||
|
print(" ⚠️ La colonna SOLO-SINISTRO e' una lente ESTREMA e va letta come tale: l'anno intero")
|
||||||
|
print(" e' fatto di soli blocchi di crash (4 episodi ricampionati). Non e' uno scenario")
|
||||||
|
print(" plausibile, e' il **limite superiore del danno**: se il verso della risposta e' lo")
|
||||||
|
print(" stesso qui e sul 2019+, non dipende dalla frazione di sinistro assunta.")
|
||||||
|
|
||||||
|
# replica di controllo del numero pubblicato da PROP-ALLOC (E-ter)
|
||||||
|
print("\n REPLICA DI CONTROLLO — PROP-ALLOC E-ter pubblica, sul 2019+: argmax della coppia")
|
||||||
|
print(" crypto **38/62 @0.50x con J 0.521**, e **75/25 J 0.443**. Stesse seed, stessa lente:")
|
||||||
|
j38 = tab[0.375]["full"]
|
||||||
|
j75 = tab[0.75]["full"]
|
||||||
|
print(f" qui: 38/62 @{j38[0]:g}x J {j38[1]['J']:.3f} | 75/25 @{j75[0]:g}x "
|
||||||
|
f"J {j75[1]['J']:.3f} -> "
|
||||||
|
f"{'REPLICA OK' if abs(j38[1]['J']-0.521) < 0.02 and abs(j75[1]['J']-0.443) < 0.02 else 'NON REPLICA — leggere con cautela'}")
|
||||||
|
|
||||||
|
tick("§3 griglia 3 lenti")
|
||||||
|
|
||||||
|
# ============================================================ §4. LO STRESS ESPLICITO
|
||||||
|
hr("§4. LO STRESS ESPLICITO — iniettare nel 2024+ i crash che il 2024+ non ha")
|
||||||
|
print(" Costruzione DICHIARATA. Base = blocchi del 2024+ (la finestra di ottimizzazione).")
|
||||||
|
print(" S1 MISTO: ogni blocco ha probabilita' p di venire dal pool dei 4 sinistri 2019-2023.")
|
||||||
|
print(f" p = {freq_day:.3f} e' la frequenza STORICA; si girano anche 2x e 4x.")
|
||||||
|
print(" S2 REPLAY: un episodio VERO innestato per intero, in ordine, a data casuale dentro")
|
||||||
|
print(" l'anno. E' la domanda operativa: *se il sinistro arriva, chi resta in piedi?*")
|
||||||
|
print(" ⚠️ Sono SCENARI, non previsioni. E poggiano su 4 episodi.")
|
||||||
|
|
||||||
|
# pool = 2024+ (blocchi normali) + gli episodi in coda, su un indice unico
|
||||||
|
def stress_panel(w: float) -> tuple[np.ndarray, np.ndarray, np.ndarray, list]:
|
||||||
|
R, G, _ = crypto(w)
|
||||||
|
off = deluck_offset(w)
|
||||||
|
Rn, Gn = R[i_rec:] - off, G[i_rec:]
|
||||||
|
segs = []
|
||||||
|
Rc, Gc = [], []
|
||||||
|
cur = len(Rn)
|
||||||
|
for t in epis:
|
||||||
|
Rc.append(R[t:t + EPI_LEN] - off)
|
||||||
|
Gc.append(G[t:t + EPI_LEN])
|
||||||
|
segs.append((cur, cur + EPI_LEN))
|
||||||
|
cur += EPI_LEN
|
||||||
|
Rp = np.concatenate([Rn] + Rc)
|
||||||
|
Gp = np.concatenate([Gn] + Gc)
|
||||||
|
return Rp, Gp, np.array([s for s, _ in segs]), segs
|
||||||
|
|
||||||
|
base_ev = PA.boot_idx(n_rec, HORIZON, N_PATHS, seed=8260)
|
||||||
|
base_fu = PA.boot_idx(n_rec, HORIZON, N_PATHS, seed=8261)
|
||||||
|
|
||||||
|
print(f"\n S1 — bootstrap MISTO (base 2024+, blocchi di sinistro con probabilita' p)")
|
||||||
|
print(f" {'w TP01':>7} | " + " | ".join(f"p={p:.3f}: {'J':>6} {'P(viv)':>7}"
|
||||||
|
for p in (0.0, freq_day, 2 * freq_day, 4 * freq_day)))
|
||||||
|
s1 = {}
|
||||||
|
for w in W_GRID:
|
||||||
|
Rp, Gp, cst, segs = stress_panel(w)
|
||||||
|
starts = np.concatenate([np.arange(a, b - BLOCK + 1) for a, b in segs])
|
||||||
|
cells = []
|
||||||
|
for j, p in enumerate((0.0, freq_day, 2 * freq_day, 4 * freq_day)):
|
||||||
|
ev = base_ev if p == 0 else mix_paths(base_ev, starts, p, seed=8270 + j)
|
||||||
|
fu = base_fu if p == 0 else mix_paths(base_fu, starts, p, seed=8280 + j)
|
||||||
|
lv, o = best_lev(Rp, Gp, ev, fu)
|
||||||
|
cells.append((lv, o))
|
||||||
|
s1[w] = cells
|
||||||
|
print(f" {w:>7.3f} | " + " | ".join(f" @{c[0]:.3g}x {c[1]['J']:>6.3f} "
|
||||||
|
f"{c[1]['p_alive']:>7.1%}" for c in cells))
|
||||||
|
for j, p in enumerate((0.0, freq_day, 2 * freq_day, 4 * freq_day)):
|
||||||
|
am = max(s1, key=lambda w: s1[w][j][1]["J"])
|
||||||
|
print(f" p={p:.3f} -> argmax TP01 {am:.3f} (J {s1[am][j][1]['J']:.3f}; "
|
||||||
|
f"75/25 fa {s1[0.75][j][1]['J']:.3f})")
|
||||||
|
|
||||||
|
tick("§4 S1 misto")
|
||||||
|
|
||||||
|
print(f"\n S2 — REPLAY: un episodio innestato una volta nell'anno funded (365g)")
|
||||||
|
print(f" Metrica: **P(vivo a 12 mesi) con il crash dentro**, alla leva scelta sul 2024+")
|
||||||
|
print(f" (cioe' la leva che uno sceglierebbe NON sapendo del crash: e' il punto).")
|
||||||
|
hdr = " ".join(f"{NAMES[t]:>10}" for t in epis)
|
||||||
|
print(" ⚠️ DUE colonne di leva, e servono entrambe: la leva 'da 2024+' e' quella che uno")
|
||||||
|
print(" sceglierebbe non sapendo del crash (realistica, ma CONFONDE peso e leva perche' non")
|
||||||
|
print(" e' la stessa per tutte le righe); la leva COMUNE 0.50x isola l'effetto del peso.")
|
||||||
|
print(f"\n {'w TP01':>7} {'leva (da 2024+)':>16} {hdr:>44} {'peggiore':>10} "
|
||||||
|
f"{'peggiore @0.50x':>16} {'P(viv) 2024+':>13}")
|
||||||
|
s2 = {}
|
||||||
|
for w in W_GRID:
|
||||||
|
Rp, Gp, _, segs = stress_panel(w)
|
||||||
|
lv = tab[w]["rec"][0]
|
||||||
|
cells, cells_c = [], []
|
||||||
|
for k, (a, b) in enumerate(segs):
|
||||||
|
fu = splice_paths(base_fu, np.arange(a, b), seed=8290 + k)
|
||||||
|
cells.append(J_of(Rp, Gp, base_ev, fu, lv)["p_alive"])
|
||||||
|
cells_c.append(J_of(Rp, Gp, base_ev, fu, 0.50)["p_alive"])
|
||||||
|
s2[w] = (lv, cells, min(cells_c), tab[w]["rec"][1]["p_alive"])
|
||||||
|
print(f" {w:>7.3f} {lv:>16.3f} " + " ".join(f"{c:>10.1%}" for c in cells)
|
||||||
|
+ f" {min(cells):>10.1%} {min(cells_c):>16.1%} "
|
||||||
|
f"{tab[w]['rec'][1]['p_alive']:>13.1%}")
|
||||||
|
bw_a = max(s2, key=lambda w: min(s2[w][1]))
|
||||||
|
bw_c = max(s2, key=lambda w: s2[w][2])
|
||||||
|
print(f"\n argmax della colonna 'peggiore': leva-da-2024+ -> TP01 {bw_a:.3f} | "
|
||||||
|
f"leva comune 0.50x -> TP01 {bw_c:.3f}")
|
||||||
|
print(" Lettura: se la sopravvivenza al crash SALISSE col peso di TP01, l'assicurazione")
|
||||||
|
print(" servirebbe. **A leva comune non sale**: la curva e' piatta-decrescente. Il difensivo")
|
||||||
|
print(" non compra sopravvivenza su questa barriera, perche' cio' che la barriera punisce e'")
|
||||||
|
print(" la PERDITA nel crash, e nel crash TP01 perde (prende la prima gamba giu' e poi va")
|
||||||
|
print(" flat) mentre SKH01 si gira short e GUADAGNA.")
|
||||||
|
|
||||||
|
|
||||||
|
tick("§4 S2 replay")
|
||||||
|
|
||||||
|
# ----- replay deterministico del 2022
|
||||||
|
print("\n S2-bis — REPLAY DETERMINISTICO DEL 2022 (2022-04-01 -> 2022-12-31, 275 giorni).")
|
||||||
|
print(" Non e' un modello: e' successo. Un conto funded HYRO da $100k che parte il 1° aprile")
|
||||||
|
print(" 2022 con max-loss STATICO -6% e daily-loss -4%, alla leva scelta sul 2024+:")
|
||||||
|
a22 = int(np.searchsorted(IDX, pd.Timestamp("2022-04-01", tz="UTC")))
|
||||||
|
b22 = int(np.searchsorted(IDX, pd.Timestamp("2023-01-01", tz="UTC")))
|
||||||
|
def replay22(w: float, lev: float) -> tuple:
|
||||||
|
R, G, _ = crypto(w)
|
||||||
|
off = deluck_offset(w)
|
||||||
|
r = (R[a22:b22] - off) * lev
|
||||||
|
g = G[a22:b22] * lev
|
||||||
|
eq, mn, bust, why = 1.0, 1.0, None, ""
|
||||||
|
for i in range(len(r)):
|
||||||
|
low = eq * (1.0 + r[i] + g[i])
|
||||||
|
eq = eq * (1.0 + r[i])
|
||||||
|
mn = min(mn, low)
|
||||||
|
if bust is None and (low < 0.94 or (r[i] + g[i]) < -0.04):
|
||||||
|
bust = IDX[a22 + i].date()
|
||||||
|
why = "daily-loss -4%" if (r[i] + g[i]) < -0.04 else "max-loss -6%"
|
||||||
|
return eq - 1.0, mn - 1.0, bust, why, float((r + g).min())
|
||||||
|
|
||||||
|
print(f"\n {'w TP01':>7} {'leva':>6} {'rend. 9 mesi':>13} {'peggior giorno':>15} "
|
||||||
|
f"{'min equity':>11} {'esito':>22}")
|
||||||
|
for w in W_GRID:
|
||||||
|
lv = tab[w]["rec"][0]
|
||||||
|
ret, mn, bust, why, wd = replay22(w, lv)
|
||||||
|
esito = "VIVO" if bust is None else f"BUSTATO {bust}"
|
||||||
|
print(f" {w:>7.3f} {lv:>6.3f} {ret:>13.1%} {wd:>15.2%} {mn:>11.2%} {esito:>22}")
|
||||||
|
print("\n Tutti VIVI, e non e' una buona notizia mal letta: **alle leve funded il 2022 non")
|
||||||
|
print(" era una minaccia per questo libro** (min equity -1.4/-2.4% contro una barriera al")
|
||||||
|
print(" -6%). Cio' che uccide un conto a barriera non e' il crash del sottostante: e' la")
|
||||||
|
print(" regola a UN giorno su una giornata qualunque. Ecco fin dove si puo' spingere la leva:")
|
||||||
|
print(f"\n {'w TP01':>7} {'leva max viva sul 2022':>23} {'rend. a quella leva':>20} "
|
||||||
|
f"{'primo vincolo che morde':>25}")
|
||||||
|
for w in W_GRID:
|
||||||
|
lo, hi = 0.1, 8.0
|
||||||
|
for _ in range(18):
|
||||||
|
mid = 0.5 * (lo + hi)
|
||||||
|
if replay22(w, mid)[2] is None:
|
||||||
|
lo = mid
|
||||||
|
else:
|
||||||
|
hi = mid
|
||||||
|
ret, _, _, _, _ = replay22(w, lo)
|
||||||
|
_, _, _, why, _ = replay22(w, hi)
|
||||||
|
print(f" {w:>7.3f} {lo:>23.2f}x {ret:>20.1%} {why:>25}")
|
||||||
|
print("\n ⚠️ La leva di rottura e' MONOTONA DECRESCENTE nel peso di TP01 solo se la")
|
||||||
|
print(" protezione fosse reale. Si legga la colonna: se il libro con piu' TP01 rompe PRIMA,")
|
||||||
|
print(" l'assicurazione non sta assicurando la cosa giusta.")
|
||||||
|
|
||||||
|
tick("§4 replay 2022")
|
||||||
|
|
||||||
|
# ============================================================ §5. NULL DEL DE-LEVERING
|
||||||
|
hr("§5. IL NULL DEL DE-LEVERING — la protezione di TP01 e' altro rispetto a meno leva?")
|
||||||
|
print(" Il null classico ('esiste k<1 che da' lo stesso rischio con risultato migliore?') qui")
|
||||||
|
print(" prende la forma giusta per una barriera: **frontiera ISO-SOPRAVVIVENZA**. Per ogni")
|
||||||
|
print(f" peso si cerca la leva k(w) tale che P(vivo 12m | replay del sinistro PEGGIORE) = "
|
||||||
|
f"{SURV_TARGET:.0%},")
|
||||||
|
print(" poi si confronta il PAYOUT atteso sulla finestra normale a quella leva. Se un libro")
|
||||||
|
print(" con meno TP01 e meno leva paga di piu' a pari sopravvivenza, la protezione di TP01")
|
||||||
|
print(" e' de-levering; se paga di meno, TP01 compra qualcosa che la leva non compra.")
|
||||||
|
|
||||||
|
_SP: dict = {}
|
||||||
|
|
||||||
|
def p_alive_stress(w: float, lev: float, seg: tuple) -> float:
|
||||||
|
"""P(vivo 12m) col replay del sinistro dentro. Solo `funded_sim`: l'eval non entra
|
||||||
|
nella definizione della frontiera, e calcolarlo raddoppierebbe il costo per niente."""
|
||||||
|
global N_EVAL
|
||||||
|
N_EVAL += 1
|
||||||
|
k = (round(w, 6), seg)
|
||||||
|
if k not in _SP:
|
||||||
|
Rp, Gp, _, _ = stress_panel(w)
|
||||||
|
_SP[k] = (Rp, Gp, splice_paths(base_fu, np.arange(seg[0], seg[1]), seed=8291))
|
||||||
|
Rp, Gp, fu = _SP[k]
|
||||||
|
return PA.funded_sim(Rp, Gp, fu, lev, "HYRO", 100_000.0, "coupled")["p_alive"]
|
||||||
|
|
||||||
|
print(f"\n {'w TP01':>7} {'k iso-surv':>11} {'P(viv|crash)':>13} {'E[pay/a] normale':>17} "
|
||||||
|
f"{'P(pass) normale':>16} {'J normale':>11}")
|
||||||
|
front = {}
|
||||||
|
for w in W_GRID:
|
||||||
|
Rp, Gp, _, segs = stress_panel(w)
|
||||||
|
# episodio peggiore per QUESTO peso (il piu' severo, misurato)
|
||||||
|
worst_seg = min(segs, key=lambda sg: np.prod(1.0 + Rp[sg[0]:sg[1]]))
|
||||||
|
lo, hi = 0.05, 3.0
|
||||||
|
for _ in range(14):
|
||||||
|
mid = 0.5 * (lo + hi)
|
||||||
|
if p_alive_stress(w, mid, worst_seg) >= SURV_TARGET:
|
||||||
|
lo = mid
|
||||||
|
else:
|
||||||
|
hi = mid
|
||||||
|
k = lo
|
||||||
|
R, G, _ = crypto(w)
|
||||||
|
off = deluck_offset(w)
|
||||||
|
o = J_of(R[i_rec:] - off, G[i_rec:], ev_r, fu_r, k)
|
||||||
|
front[w] = (k, o)
|
||||||
|
print(f" {w:>7.3f} {k:>11.3f} {p_alive_stress(w, k, worst_seg):>13.1%} "
|
||||||
|
f"{o['e_payout']:>16,.0f}$ {o['p_pass']:>16.1%} {o['J']:>11.3f}")
|
||||||
|
kbest = max(front, key=lambda w: front[w][1]["e_payout"])
|
||||||
|
jbest = max(front, key=lambda w: front[w][1]["J"])
|
||||||
|
print(f"\n argmax a ISO-SOPRAVVIVENZA: payout -> TP01 {kbest:.3f} | J -> TP01 {jbest:.3f}")
|
||||||
|
print(" ⚠️ La leva k(w) e' tarata SU un episodio che nel campione di ottimizzazione non c'e':")
|
||||||
|
print(" e' un esercizio con l'informazione del futuro. Serve a rispondere alla domanda del")
|
||||||
|
print(" filone ('se sapessi che arriva, come mi allocherei?'), NON e' una politica.")
|
||||||
|
|
||||||
|
tick("§5 frontiera iso-sopravvivenza")
|
||||||
|
|
||||||
|
# ============================================================ §6. IL TERZO SLEEVE
|
||||||
|
hr("§6. XS01 — la parte della domanda che NON e' rispondibile, e perche'")
|
||||||
|
print(" L'ottimo di PROP-ALLOC mette il 50% su XS01, che **nasce nel 2024**: per lui non")
|
||||||
|
print(" esiste NESSUN sinistro nel campione. Qui non lo si simula col bootstrap della sua")
|
||||||
|
print(" finestra (sarebbe assumere che nel crash si comporti come in un giorno normale, che")
|
||||||
|
print(" e' esattamente l'assunzione sotto esame). Si dichiarano TRE comportamenti e si")
|
||||||
|
print(" guarda se la conclusione dipende da quale:")
|
||||||
|
print(" A) FLAT nel crash (il gate di dispersione lo spegne) -> generosa")
|
||||||
|
print(" B) la sua PEGGIORE finestra di 30g del 2024+, contigua e in ordine -> severa")
|
||||||
|
print(" C) uno shock una-tantum di taglia X sul giorno peggiore del crash -> si cerca")
|
||||||
|
print(" la X di ROTTURA, cioe' quanto grande deve essere per portare P(vivo) sotto 50%")
|
||||||
|
xs = pc.xsec_daily_tuples()
|
||||||
|
xsR = xs["R"].values
|
||||||
|
xsG = np.minimum(xs["m"].values, xsR) - xsR
|
||||||
|
lxs = np.log1p(xsR)
|
||||||
|
cxs = np.concatenate(([0.0], np.cumsum(lxs)))
|
||||||
|
wxs30 = np.expm1(cxs[EPI_LEN:] - cxs[:-EPI_LEN])
|
||||||
|
t_xw = int(np.argmin(wxs30))
|
||||||
|
print(f"\n XS01: {len(xs)} giorni {xs.index[0].date()} -> {xs.index[-1].date()}; peggior "
|
||||||
|
f"giorno {xsR.min():.2%}, peggior minimo {xs['m'].min():.2%}")
|
||||||
|
print(f" peggior finestra di {EPI_LEN}g di XS01: {xs.index[t_xw].date()} -> "
|
||||||
|
f"{xs.index[t_xw+EPI_LEN-1].date()}, cumulato {wxs30[t_xw]:.1%} "
|
||||||
|
f"(ipotesi B). Per confronto la coppia crypto nel COVID fa -5.4% a TP01 pieno.")
|
||||||
|
print(" 📌 Il numero che conta e' questo: **la peggior finestra di 30g mai vista da XS01 vale")
|
||||||
|
print(f" {wxs30[t_xw]:.1%}**. Se nel crash XS01 facesse cosi', al 50% di peso porterebbe")
|
||||||
|
print(f" {0.5*wxs30[t_xw]:.1%} al libro. La domanda non e' se XS01 sopravvive a cio' che ha")
|
||||||
|
print(" gia' visto, e' se il suo comportamento nel crash somiglia a cio' che ha gia' visto —")
|
||||||
|
print(" e su questo il campione non ha NIENTE da dire.")
|
||||||
|
W3 = [(0.75, 0.25, 0.00), (0.375, 0.625, 0.00), (0.25, 0.25, 0.50), (0.125, 0.125, 0.75),
|
||||||
|
(0.50, 0.25, 0.25)]
|
||||||
|
com = xs.index.intersection(IDX[i_rec:])
|
||||||
|
xs_al = xs.reindex(com)
|
||||||
|
print(f"\n {'TP/SKH/XS':>16} {'ipotesi XS':>12} {'leva':>6} {'J normale':>10} "
|
||||||
|
f"{'P(viv|replay peggiore)':>24} {'shock di rottura':>18}")
|
||||||
|
for cfg in W3:
|
||||||
|
wtp, wsk, wxs = cfg
|
||||||
|
base = wtp + wsk
|
||||||
|
wtp_n = wtp / base if base > 0 else 0.0
|
||||||
|
Rc, Gc, Ic = crypto(wtp_n)
|
||||||
|
offc = deluck_offset(wtp_n)
|
||||||
|
m1 = Ic.isin(com)
|
||||||
|
Rn = base * (Rc[m1] - offc) + wxs * xs_al["R"].values
|
||||||
|
Gn = base * Gc[m1] + wxs * (np.minimum(xs_al["m"].values, xs_al["R"].values)
|
||||||
|
- xs_al["R"].values)
|
||||||
|
nn = len(Rn)
|
||||||
|
evn = PA.boot_idx(nn, HORIZON, N_PATHS, seed=8300)
|
||||||
|
fun = PA.boot_idx(nn, HORIZON, N_PATHS, seed=8301)
|
||||||
|
lv, on = best_lev(Rn, Gn, evn, fun)
|
||||||
|
bfu = PA.boot_idx(nn, HORIZON, N_PATHS, seed=8302)
|
||||||
|
lab = f"{int(round(wtp*100))}/{int(round(wsk*100))}/{int(round(wxs*100))}"
|
||||||
|
|
||||||
|
def build(hyp: str, shock: float = 0.0):
|
||||||
|
segsR, segsG = [], []
|
||||||
|
for t in epis:
|
||||||
|
rc = base * (crypto(wtp_n)[0][t:t + EPI_LEN] - offc)
|
||||||
|
gc = base * crypto(wtp_n)[1][t:t + EPI_LEN]
|
||||||
|
if wxs > 0:
|
||||||
|
if hyp == "A-flat":
|
||||||
|
rx = np.zeros(EPI_LEN); gx = np.zeros(EPI_LEN)
|
||||||
|
elif hyp == "B-peggiore":
|
||||||
|
rx = xsR[t_xw:t_xw + EPI_LEN].copy()
|
||||||
|
gx = xsG[t_xw:t_xw + EPI_LEN].copy()
|
||||||
|
else:
|
||||||
|
rx = np.zeros(EPI_LEN); gx = np.zeros(EPI_LEN)
|
||||||
|
rx[int(np.argmin(rc))] = -shock
|
||||||
|
segsR.append(rc + wxs * rx); segsG.append(gc + wxs * gx)
|
||||||
|
else:
|
||||||
|
segsR.append(rc); segsG.append(gc)
|
||||||
|
Rp = np.concatenate([Rn] + segsR)
|
||||||
|
Gp = np.concatenate([Gn] + segsG)
|
||||||
|
sg = [(nn + k * EPI_LEN, nn + (k + 1) * EPI_LEN) for k in range(len(epis))]
|
||||||
|
worst = 1.0
|
||||||
|
for k, g2 in enumerate(sg):
|
||||||
|
fu = splice_paths(bfu, np.arange(g2[0], g2[1]), seed=8320 + k)
|
||||||
|
worst = min(worst, J_of(Rp, Gp, evn, fu, lv)["p_alive"])
|
||||||
|
return worst
|
||||||
|
|
||||||
|
if wxs == 0:
|
||||||
|
print(f" {lab:>16} {'n/a (no XS)':>12} {lv:>6.3f} {on['J']:>10.3f} "
|
||||||
|
f"{build('A-flat'):>24.1%} {'-':>18}")
|
||||||
|
continue
|
||||||
|
for hyp in ("A-flat", "B-peggiore"):
|
||||||
|
print(f" {lab:>16} {hyp:>12} {lv:>6.3f} {on['J']:>10.3f} "
|
||||||
|
f"{build(hyp):>24.1%} {'-':>18}")
|
||||||
|
lo, hi = 0.0, 0.40 # shock di rottura: bisezione su P(vivo) = 50%
|
||||||
|
for _ in range(9):
|
||||||
|
mid = 0.5 * (lo + hi)
|
||||||
|
if build("C-shock", mid) >= 0.50:
|
||||||
|
lo = mid
|
||||||
|
else:
|
||||||
|
hi = mid
|
||||||
|
print(f" {lab:>16} {'C-shock':>12} {lv:>6.3f} {on['J']:>10.3f} {'(50% per def.)':>24} "
|
||||||
|
f"{-lo:>17.1%}")
|
||||||
|
print("\n Lettura: 'shock di rottura' = il crollo di UN giorno su XS01 che porta P(vivo) al")
|
||||||
|
print(" 50% quando il crash arriva. Piu' e' piccolo, piu' l'allocazione XS01-pesante e' a un")
|
||||||
|
print(" giorno di coda dalla morte. Non e' una previsione: e' la taglia della scommessa.")
|
||||||
|
tick("§6 XS01")
|
||||||
|
|
||||||
|
# ============================================================ §7. BANDA + VERDETTO
|
||||||
|
hr("§7. SENSIBILITA' AL DE-LUCK — l'ORDINE fra i pesi sopravvive alla banda?")
|
||||||
|
print(" La banda d'ancora non e' ri-misurabile sotto questa lente (costo, dichiarato anche da")
|
||||||
|
print(" PROP-ALLOC). Si applica il fattore misurato x0.89 e si controlla l'unica cosa che")
|
||||||
|
print(" conta: se l'argmax sui pesi si muove fra 1.00 (nessuna correzione) e 0.60 (la stima")
|
||||||
|
print(" a occhio del 25/07, poi ri-misurata troppo severa).")
|
||||||
|
print(f"\n {'fattore':>9} {'argmax 2019+':>14} {'argmax 2024+':>14} {'argmax SOLO-SIN':>16} "
|
||||||
|
f"{'J(75/25) 2019+':>16} {'J(38/62) 2019+':>16}")
|
||||||
|
for fct in (1.00, 0.89, 0.60):
|
||||||
|
best_f = best_r = best_s = None
|
||||||
|
vals = {}
|
||||||
|
for w in W_GRID:
|
||||||
|
R, G, _ = crypto(w)
|
||||||
|
o = deluck_offset(w, fct)
|
||||||
|
Rf = R - o
|
||||||
|
j1 = best_lev(Rf, G, ev_f, fu_f)[1]["J"]
|
||||||
|
j2 = best_lev(Rf[i_rec:], G[i_rec:], ev_r, fu_r)[1]["J"]
|
||||||
|
j3 = best_lev(Rf, G, ev_s, fu_s)[1]["J"]
|
||||||
|
vals[w] = (j1, j2, j3)
|
||||||
|
best_f = max(vals, key=lambda w: vals[w][0])
|
||||||
|
best_r = max(vals, key=lambda w: vals[w][1])
|
||||||
|
best_s = max(vals, key=lambda w: vals[w][2])
|
||||||
|
print(f" {fct:>9.2f} {best_f:>14.3f} {best_r:>14.3f} {best_s:>16.3f} "
|
||||||
|
f"{vals[0.75][0]:>16.3f} {vals[0.375][0]:>16.3f}")
|
||||||
|
|
||||||
|
hr("§8. I GATE — cosa e' girato, cosa NON e' girato e perche'")
|
||||||
|
print(" Questo filone non propone una strategia nuova: propone (o rifiuta) un'ALLOCAZIONE per")
|
||||||
|
print(" un conto che non esiste. I gate del progetto vanno dichiarati uno per uno.")
|
||||||
|
try:
|
||||||
|
sys.path.insert(0, str(ROOT / "scripts" / "research" / "alt"))
|
||||||
|
import altlib as A # noqa: E402
|
||||||
|
for lab, w in (("LIVE 75/25", 0.75), ("OPT 38/62", 0.375)):
|
||||||
|
R = crypto(w)[0] - deluck_offset(w)
|
||||||
|
d = pd.Series(R, index=IDX)
|
||||||
|
n_act = int((np.abs(R) > 1e-12).sum())
|
||||||
|
n_los = int((R[np.abs(R) > 1e-12] < 0).sum())
|
||||||
|
f = A.implausible_sharpe(d, n_trades=n_act, n_losing_trades=n_los)
|
||||||
|
print(f" implausible_sharpe {lab:>11}: {f}")
|
||||||
|
except Exception as exc: # pragma: no cover
|
||||||
|
print(f" implausible_sharpe: NON GIRATO ({exc})")
|
||||||
|
print(" marginal_vs_tp01 / study_family_honest / deflated_sharpe : **NON GIRATI, e non")
|
||||||
|
print(" sono applicabili** — non c'e' uno stream candidato da confrontare con TP01: gli")
|
||||||
|
print(" stream sono i due sleeve gia' ammessi, e l'oggetto e' il loro PESO. Il gate dei")
|
||||||
|
print(" pesi del progetto e' `weights_tilt_null`, che pero' giudica lo SHARPE del libro")
|
||||||
|
print(" di RICERCA: qui l'obiettivo e' J su una barriera, e il conto e' separato.")
|
||||||
|
print(" (PROP-ALLOC lo ha girato: gate_pass False. Non cambia nulla qui: nessuna")
|
||||||
|
print(" proposta di questo file tocca il libro live.)")
|
||||||
|
print(" causality_ok : **non girato, non applicabile** — nessun segnale nuovo; le serie")
|
||||||
|
print(" sono quelle di produzione, gia' verificate leak-free a monte.")
|
||||||
|
print(" anchor_luck_band : **non ri-misurata** (costo, come in PROP-ALLOC). Si e' girata")
|
||||||
|
print(" la sensibilita' al FATTORE di de-luck (§7), che e' meno: dice se l'ORDINE regge,")
|
||||||
|
print(" non qual e' la banda. Distorsione dichiarata: la lente e' l'ancora CANONICA e")
|
||||||
|
print(" SKH01 e' lo sleeve con la fortuna d'ancora piu' grande da restituire (26/07)")
|
||||||
|
print(" -> **la distorsione e' A FAVORE della conclusione di questo file**, cioe' del")
|
||||||
|
print(" peso basso su TP01. E' il caveat piu' importante del filone.")
|
||||||
|
print(" null del de-levering : **GIRATO** (§5, frontiera iso-sopravvivenza) — e superato")
|
||||||
|
print(" NEL VERSO OPPOSTO a quello atteso: e' la protezione di TP01 a essere replicabile")
|
||||||
|
print(" con meno leva, e meglio.")
|
||||||
|
|
||||||
|
hr("§9. LA RISPOSTA OPERATIVA")
|
||||||
|
print(f" valutazioni MC totali di questo file: **{N_EVAL}** "
|
||||||
|
f"(ognuna = {N_PATHS} percorsi di eval + {N_PATHS} di funded)")
|
||||||
|
print(f"\n {'lente':>26} {'argmax w TP01':>14} {'J argmax':>10} {'J a 0.75':>10} "
|
||||||
|
f"{'J a 0.375':>11}")
|
||||||
|
for lab, key in (("2019+ (contiene il 2022)", "full"), ("2024+ (PROP-ALLOC)", "rec"),
|
||||||
|
("SOLO-SINISTRO", "sin")):
|
||||||
|
am = max(tab, key=lambda w: tab[w][key][1]["J"])
|
||||||
|
print(f" {lab:>26} {am:>14.3f} {tab[am][key][1]['J']:>10.3f} "
|
||||||
|
f"{tab[0.75][key][1]['J']:>10.3f} {tab[0.375][key][1]['J']:>11.3f}")
|
||||||
|
print("\n" + "=" * 106)
|
||||||
|
print(f" fatto in {time.time()-t0:.0f}s | valutazioni: {N_EVAL}")
|
||||||
|
print("=" * 106)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,683 @@
|
|||||||
|
"""r0822b_xs01_oos.py — XS01 FUORI dalla sua finestra di scoperta (2026-08-22, ondata wave-0822).
|
||||||
|
|
||||||
|
IL PROBLEMA (filone PROP-ALLOC, RESULTS-0822 §1)
|
||||||
|
------------------------------------------------
|
||||||
|
Il risultato piu' vicino all'obiettivo dell'operatore e': su un conto FUNDED da $100k, aggiungere
|
||||||
|
XS01 al libro porta J = P(pass) x P(vivo 12m) da 0,335 a 0,738. Ma **tutto** quel vantaggio poggia
|
||||||
|
sul drift di XS01 misurato sulla SUA STESSA finestra di scoperta (Hyperliquid, 2024-01 -> oggi,
|
||||||
|
~2,6 anni, e senza un solo mercato orso). Azzerando quel drift l'uplift scende a +0,038; sotto
|
||||||
|
stress congiunto diventa negativo. Il gate proposto era "non aprire un funded finche' XS01 non ha
|
||||||
|
una finestra fuori dal 2024-2026" = ANNI di attesa.
|
||||||
|
|
||||||
|
LA DOMANDA DI QUESTO FILONE
|
||||||
|
---------------------------
|
||||||
|
Si puo' testare il MECCANISMO CONGELATO su una storia PIU' LUNGA e INDIPENDENTE invece di aspettare?
|
||||||
|
La risposta e' si', e il vincolo non e' il dato ma il CALENDARIO DELLE QUOTAZIONI: 6 dei 19 asset
|
||||||
|
(APT, OP, ARB, SUI, SEI, TIA) non esistono prima del 2022-06/2023-10. Il piu' vecchio universo
|
||||||
|
>= 10 asset dei 19 e' del **2020-10** -> circa **3 anni interi fuori dalla finestra di scoperta,
|
||||||
|
2021-01 -> 2023-12**, che contengono il top 2021, il crollo di maggio 2021, **LUNA e FTX (2022)** e
|
||||||
|
la ripresa 2023. E' esattamente il regime che manca al campione di scoperta.
|
||||||
|
|
||||||
|
IL DATO: BINANCE, E PERCHE' L'OBIEZIONE DEL PROGETTO NON MORDE QUI
|
||||||
|
-----------------------------------------------------------------
|
||||||
|
CLAUDE.md: "Binance NON e' la verita' (e' USDT, ~10 bps fuori dal consenso, e sotto depeg fino al
|
||||||
|
3%) -> usare SOLO come audit indipendente, mai come ancora per ripulire i dati". Quella regola
|
||||||
|
riguarda l'uso come **ANCORA DI PREZZO** (livello). Qui Binance serve come **audit indipendente per
|
||||||
|
un segnale CROSS-SECTIONAL DI RANKING su barre giornaliere**, e i tre difetti citati sono tutti
|
||||||
|
**fattori COMUNI alla valuta di quotazione**:
|
||||||
|
- uno scarto costante di ~10 bps sul livello e' identico su tutti e 19 gli asset -> sparisce
|
||||||
|
ESATTAMENTE nello z-score cross-sectional (che sottrae la media di sezione);
|
||||||
|
- un depeg USDT muove tutte le gambe insieme -> non cambia il RANKING, e il libro e'
|
||||||
|
dollar-neutral (0.5/k long, -0.5/k short) quindi il fattore comune si cancella anche nel P&L;
|
||||||
|
- a 1d un errore di livello di 10 bps e' 1/50 della vol giornaliera tipica di un alt.
|
||||||
|
NON e' un argomento teorico: e' MISURATO qui sotto (sezione 3) confrontando il meccanismo su
|
||||||
|
Binance e su Hyperliquid nella finestra in cui esistono ENTRAMBI (2024+).
|
||||||
|
E i dati scaricati sono CERTIFICATI comunque (barre flat, gap di calendario, volumi zero =
|
||||||
|
backfill sintetico: il progetto ci e' gia' cascato una volta, correzione AXS del 2026-06-20).
|
||||||
|
|
||||||
|
MECCANISMO: CONGELATO. Nessun parametro toccato. E' `src/portfolio/sleeves.py::_xsec_returns`
|
||||||
|
(lookbacks (30,90), H=10, k=5, mode="mom", target_vol=0.20, gate dispersione p30 espandente
|
||||||
|
causale, fee 5 bps/lato, vol-target su rv30 shiftata, clip leva 3x). Il motore qui sotto e' una
|
||||||
|
versione MASCHERATA (regge NaN = asset non ancora quotato) e la sezione 2 dimostra che riproduce
|
||||||
|
lo sleeve ufficiale a **max|diff| = 0.0** sui dati HL.
|
||||||
|
|
||||||
|
ESITO (2026-08-22, corsa completa 78 s)
|
||||||
|
---------------------------------------
|
||||||
|
- Il motore replica lo sleeve ufficiale a max|diff| = 0.0; Binance e Hyperliquid danno lo STESSO
|
||||||
|
sleeve sulla finestra comune (corr 0.9991, dSharpe -0.025) -> la storia lunga e' utilizzabile.
|
||||||
|
- Fuori dalla finestra di scoperta (2021-01 -> 2023-12, contiene LUNA e FTX) il meccanismo ha un
|
||||||
|
edge: Sharpe MEDIANO di fase **+1.12** (U13 fisso) / **+1.17** (universo espandente), positivo in
|
||||||
|
**10/10** fasi, p = 0.013 contro il null di permutazione cross-sezionale a fee zero.
|
||||||
|
- **Il fuori campione e' MIGLIORE della finestra di scoperta**, a parita' di universo: differenza
|
||||||
|
appaiata per fase +0.668 (10/10 fasi). L'ipotesi "il drift di XS01 e' un artefatto del 2024-2026"
|
||||||
|
e' REFUTATA per la parte testabile del sleeve.
|
||||||
|
- MA: il maxDD raddoppia (10.8% nella scoperta -> 20.9-22.0% mediano fuori), il 2021 vale meta'
|
||||||
|
dell'edge, una gamba sola (LTC) ne vale 0.9 di Sharpe, e i due affinamenti del 2026-06-19 non si
|
||||||
|
replicano. E il deflated-Sharpe FALLISCE (0.342-0.418).
|
||||||
|
- Sul LIBRO, fuori campione: TP01+SKH01 75/25 Sharpe +1.17 -> **+1.65** e maxDD 9.4% -> **6.0%**
|
||||||
|
a peso XS01 0.25; a peso **0.50** (l'argmax di PROP-ALLOC) il maxDD risale a **8.9%**.
|
||||||
|
|
||||||
|
Uso: nice -n 19 timeout 900 uv run python scripts/research/r0822b_xs01_oos.py
|
||||||
|
Rete: solo lettura, <= 2 req/s, MAI nella finestra :25-:29 (rate limit per-IP condiviso col
|
||||||
|
collettore della catena). Il download e' messo in cache nello scratchpad: la seconda corsa e'
|
||||||
|
offline.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import math
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[2]
|
||||||
|
sys.path.insert(0, str(ROOT))
|
||||||
|
sys.path.insert(0, str(ROOT / "scripts" / "research" / "alt"))
|
||||||
|
|
||||||
|
import altlib as A # noqa: E402
|
||||||
|
from src.portfolio.sleeves import XS_CFG, XS_UNIVERSE # noqa: E402
|
||||||
|
|
||||||
|
CACHE = Path(os.environ.get(
|
||||||
|
"R0822B_CACHE",
|
||||||
|
"/tmp/claude-1001/-opt-docker-PythagorasGoal/b6cc75e7-14f8-4c32-bd07-ab8a0d2aaee6/scratchpad/r0822b",
|
||||||
|
))
|
||||||
|
CACHE.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
RAW = ROOT / "data" / "raw"
|
||||||
|
FEE_SIDE = 0.001 / 2.0 # identica al sleeve congelato
|
||||||
|
DISCOVERY = pd.Timestamp("2024-01-01", tz="UTC") # inizio della finestra di scoperta (HL)
|
||||||
|
|
||||||
|
# I 13 dei 19 quotati su Binance prima del 2020-11 -> l'universo FISSO piu' lungo possibile
|
||||||
|
# che rispetta il vincolo del meccanismo congelato (>= 10 gambe).
|
||||||
|
U13 = ["BTC", "ETH", "BNB", "XRP", "LTC", "ADA", "LINK", "DOGE", "SOL", "AVAX", "NEAR", "AAVE", "INJ"]
|
||||||
|
SECTORS = ["xlb", "xlc", "xle", "xlf", "xli", "xlk", "xlp", "xlre", "xlu", "xlv", "xly"]
|
||||||
|
|
||||||
|
|
||||||
|
# ===========================================================================
|
||||||
|
# 0. disciplina di rete
|
||||||
|
# ===========================================================================
|
||||||
|
def _outside_forbidden_window() -> None:
|
||||||
|
"""Il collettore della catena gira al minuto :25 e satura il rate limit per-IP di Deribit;
|
||||||
|
la regola d'ondata e' non toccare la rete fra :25 e :29. Qui parliamo con Binance, ma la
|
||||||
|
disciplina e' della MACCHINA (2 core, libro con soldi veri), non del venue."""
|
||||||
|
while True:
|
||||||
|
m = datetime.now(timezone.utc).minute
|
||||||
|
if not (25 <= m <= 29):
|
||||||
|
return
|
||||||
|
print(f" [rete] minuto :{m:02d} nella finestra vietata :25-:29 — attendo 30s", flush=True)
|
||||||
|
time.sleep(30)
|
||||||
|
|
||||||
|
|
||||||
|
# ===========================================================================
|
||||||
|
# 1. FETCH + CERTIFY (Binance spot USDT, 1d)
|
||||||
|
# ===========================================================================
|
||||||
|
def fetch_binance_daily(sym: str, since_iso: str = "2017-01-01T00:00:00Z") -> pd.DataFrame:
|
||||||
|
f = CACHE / f"bin_{sym.lower()}_1d.parquet"
|
||||||
|
if f.exists():
|
||||||
|
return pd.read_parquet(f)
|
||||||
|
import ccxt
|
||||||
|
ex = ccxt.binance({"enableRateLimit": True})
|
||||||
|
since = ex.parse8601(since_iso)
|
||||||
|
rows, cur = [], since
|
||||||
|
while True:
|
||||||
|
_outside_forbidden_window()
|
||||||
|
batch = ex.fetch_ohlcv(f"{sym}/USDT", "1d", since=cur, limit=1000)
|
||||||
|
time.sleep(0.5) # <= 2 req/s
|
||||||
|
if not batch:
|
||||||
|
break
|
||||||
|
rows.extend(batch)
|
||||||
|
if len(batch) < 1000:
|
||||||
|
break
|
||||||
|
cur = batch[-1][0] + 86_400_000
|
||||||
|
d = pd.DataFrame(rows, columns=["timestamp", "open", "high", "low", "close", "volume"])
|
||||||
|
d = d.drop_duplicates("timestamp").sort_values("timestamp").reset_index(drop=True)
|
||||||
|
d.to_parquet(f)
|
||||||
|
return d
|
||||||
|
|
||||||
|
|
||||||
|
def certify(sym: str, d: pd.DataFrame) -> dict:
|
||||||
|
"""Certificazione locale, stessa forma di scripts/analysis/fetch_hyperliquid.py.
|
||||||
|
Il rivelatore del backfill SINTETICO NON e' il prezzo (cerbero copiava i prezzi da un altro
|
||||||
|
venue) ma il **VOLUME**: un run di barre a volume 0 e' padding, non mercato."""
|
||||||
|
t = pd.to_datetime(d["timestamp"], unit="ms", utc=True)
|
||||||
|
c = d["close"].values.astype(float)
|
||||||
|
v = d["volume"].values.astype(float)
|
||||||
|
span = (t.max() - t.min()).days + 1
|
||||||
|
flat = float(np.mean(c[1:] == c[:-1])) if len(c) > 1 else float("nan")
|
||||||
|
zero_v = float(np.mean(v == 0.0))
|
||||||
|
# run iniziale a volume zero = firma del padding sintetico (lezione AXS 2026-06-20)
|
||||||
|
lead0 = int(np.argmax(v > 0)) if (v > 0).any() else len(v)
|
||||||
|
return dict(sym=sym, n=len(d), start=str(t.min().date()), end=str(t.max().date()),
|
||||||
|
gap_days=int(span - len(d)), flat=flat, zero_vol=zero_v, lead_zero_vol=lead0,
|
||||||
|
ok=bool(span - len(d) == 0 and flat < 0.02 and zero_v < 0.01 and lead0 == 0))
|
||||||
|
|
||||||
|
|
||||||
|
def load_panel(syms: list[str]) -> pd.DataFrame:
|
||||||
|
"""Pannello dei CLOSE, indice giornaliero UTC, NaN dove l'asset non e' ancora quotato."""
|
||||||
|
cols = {}
|
||||||
|
for s in syms:
|
||||||
|
d = fetch_binance_daily(s)
|
||||||
|
cols[s] = pd.Series(d["close"].values.astype(float),
|
||||||
|
index=pd.to_datetime(d["timestamp"], unit="ms", utc=True))
|
||||||
|
return pd.concat(cols, axis=1).sort_index()
|
||||||
|
|
||||||
|
|
||||||
|
def load_sector_panel() -> pd.DataFrame:
|
||||||
|
cols = {}
|
||||||
|
for s in SECTORS:
|
||||||
|
d = pd.read_parquet(RAW / f"eq_{s}_1d.parquet")
|
||||||
|
cols[s.upper()] = pd.Series(d["close"].values.astype(float),
|
||||||
|
index=pd.to_datetime(d["timestamp"], unit="ms", utc=True))
|
||||||
|
return pd.concat(cols, axis=1).sort_index()
|
||||||
|
|
||||||
|
|
||||||
|
# ===========================================================================
|
||||||
|
# 2. MOTORE — meccanismo CONGELATO, versione mascherata (regge i NaN)
|
||||||
|
# ===========================================================================
|
||||||
|
def xsec_engine(P: pd.DataFrame, phase: int = 0, fee_side: float = FEE_SIDE,
|
||||||
|
mode: str | None = None, min_assets: int = 10,
|
||||||
|
perm_rng: np.random.Generator | None = None,
|
||||||
|
perm_kind: str = "cross", lookbacks=None, disp_pct=None,
|
||||||
|
detail: dict | None = None) -> pd.Series:
|
||||||
|
"""Riproduce `sleeves._xsec_returns` riga per riga. Unica differenza: l'insieme ammissibile
|
||||||
|
e' calcolato PER RIBILANCIAMENTO (un asset entra quando ha almeno max(lookbacks) barre), cosi'
|
||||||
|
il meccanismo si puo' applicare a un universo che si allarga nel tempo — che e' cio' che un
|
||||||
|
implementatore in tempo reale avrebbe avuto. Con un pannello senza NaN, e' identico all'originale.
|
||||||
|
|
||||||
|
`perm_rng` attiva un NULL: 'cross' = permuta lo score FRA GLI ASSET al ribilanciamento
|
||||||
|
(distrugge l'informazione, conserva ESATTAMENTE la struttura di posizione e di turnover),
|
||||||
|
'sign' = pesi casuali fra gli ammissibili (stesso gross, stesso k)."""
|
||||||
|
px = P.values.astype(float)
|
||||||
|
n, Ncols = px.shape
|
||||||
|
H, k = XS_CFG["H"], XS_CFG["k"]
|
||||||
|
lookbacks = tuple(XS_CFG["lookbacks"] if lookbacks is None else lookbacks)
|
||||||
|
disp_pct = XS_CFG["disp_pct"] if disp_pct is None else disp_pct
|
||||||
|
tv, minhist = XS_CFG["target_vol"], XS_CFG["disp_minhist"]
|
||||||
|
mode = mode or XS_CFG["mode"]
|
||||||
|
mlb = max(lookbacks)
|
||||||
|
|
||||||
|
with np.errstate(invalid="ignore", divide="ignore"):
|
||||||
|
dret = np.vstack([np.zeros(Ncols), px[1:] / px[:-1] - 1.0])
|
||||||
|
dret = np.nan_to_num(dret, nan=0.0, posinf=0.0, neginf=0.0)
|
||||||
|
|
||||||
|
W = np.zeros((n, Ncols)); w = np.zeros(Ncols); disp_hist: list[float] = []
|
||||||
|
n_rebal = n_traded = 0
|
||||||
|
for i in range(n):
|
||||||
|
if i >= mlb and (i % H) == phase:
|
||||||
|
elig = np.isfinite(px[i])
|
||||||
|
for L in lookbacks:
|
||||||
|
elig = elig & np.isfinite(px[i - L])
|
||||||
|
m = int(elig.sum())
|
||||||
|
if m >= min_assets:
|
||||||
|
n_rebal += 1
|
||||||
|
idx = np.flatnonzero(elig)
|
||||||
|
rLs = [px[i, idx] / px[i - L, idx] - 1.0 for L in lookbacks]
|
||||||
|
disp_i = float(np.mean([r.std() for r in rLs]))
|
||||||
|
thr = (np.percentile(disp_hist, disp_pct)
|
||||||
|
if (disp_pct > 0 and len(disp_hist) >= minhist) else -np.inf)
|
||||||
|
if disp_i >= thr:
|
||||||
|
score = np.zeros(m); cnt = 0
|
||||||
|
for rL in rLs:
|
||||||
|
sd = rL.std()
|
||||||
|
if sd > 0:
|
||||||
|
score += (rL - rL.mean()) / sd; cnt += 1
|
||||||
|
if cnt:
|
||||||
|
score /= cnt
|
||||||
|
w = np.zeros(Ncols)
|
||||||
|
if perm_rng is not None and perm_kind == "sign":
|
||||||
|
pick = perm_rng.permutation(m)
|
||||||
|
lo, hi = idx[pick[:k]], idx[pick[-k:]]
|
||||||
|
else:
|
||||||
|
if perm_rng is not None: # perm_kind == "cross"
|
||||||
|
score = score[perm_rng.permutation(m)]
|
||||||
|
order = np.argsort(score)
|
||||||
|
lo, hi = idx[order[:k]], idx[order[-k:]]
|
||||||
|
if mode == "mom":
|
||||||
|
w[hi] = 0.5 / k; w[lo] = -0.5 / k
|
||||||
|
else:
|
||||||
|
w[lo] = 0.5 / k; w[hi] = -0.5 / k
|
||||||
|
n_traded += 1
|
||||||
|
else:
|
||||||
|
w = np.zeros(Ncols)
|
||||||
|
disp_hist.append(disp_i)
|
||||||
|
W[i] = w
|
||||||
|
|
||||||
|
gross = np.zeros(n); gross[1:] = np.sum(W[:-1] * dret[1:], axis=1)
|
||||||
|
turn = np.zeros(n); turn[0] = np.abs(W[0]).sum()
|
||||||
|
turn[1:] = np.abs(np.diff(W, axis=0)).sum(axis=1)
|
||||||
|
net = gross - turn * fee_side
|
||||||
|
s = pd.Series(net, index=P.index)
|
||||||
|
rv = s.rolling(30, min_periods=15).std().shift(1) * np.sqrt(365.25)
|
||||||
|
scale = np.clip(np.nan_to_num(tv / rv.replace(0, np.nan).values, nan=0.0), 0, 3.0)
|
||||||
|
out = pd.Series(s.values * scale, index=P.index)
|
||||||
|
out.attrs["n_rebal"] = n_rebal
|
||||||
|
out.attrs["n_traded"] = n_traded
|
||||||
|
if detail is not None:
|
||||||
|
detail.update(W=W, scale=scale, turn=turn, index=P.index,
|
||||||
|
n_rebal=n_rebal, n_traded=n_traded)
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def hl_panel(syms=None) -> pd.DataFrame:
|
||||||
|
"""Pannello Hyperliquid, ESATTAMENTE come lo costruisce lo sleeve congelato."""
|
||||||
|
cols = {}
|
||||||
|
for sym in (syms or XS_UNIVERSE):
|
||||||
|
p = RAW / f"hl_{sym.lower()}_1d.parquet"
|
||||||
|
if not p.exists():
|
||||||
|
continue
|
||||||
|
d = pd.read_parquet(p)
|
||||||
|
cols[sym] = pd.Series(d["close"].values.astype(float),
|
||||||
|
index=pd.to_datetime(d["timestamp"], unit="ms", utc=True))
|
||||||
|
return pd.concat(cols, axis=1, join="inner").sort_index().dropna()
|
||||||
|
|
||||||
|
|
||||||
|
# ===========================================================================
|
||||||
|
# metriche
|
||||||
|
# ===========================================================================
|
||||||
|
def sh(s: pd.Series) -> float:
|
||||||
|
return A._sh(s)
|
||||||
|
|
||||||
|
|
||||||
|
def dd(s: pd.Series) -> float:
|
||||||
|
return A._dd_ret(s)
|
||||||
|
|
||||||
|
|
||||||
|
def cagr(s: pd.Series) -> float:
|
||||||
|
r = s.dropna()
|
||||||
|
if len(r) < 30:
|
||||||
|
return float("nan")
|
||||||
|
yrs = (r.index[-1] - r.index[0]).days / 365.25
|
||||||
|
return float(np.prod(1.0 + r.values) ** (1.0 / yrs) - 1.0) if yrs > 0 else float("nan")
|
||||||
|
|
||||||
|
|
||||||
|
def win(s: pd.Series, a: str | None = None, b: str | None = None) -> pd.Series:
|
||||||
|
x = s
|
||||||
|
if a:
|
||||||
|
x = x[x.index >= pd.Timestamp(a, tz="UTC")]
|
||||||
|
if b:
|
||||||
|
x = x[x.index < pd.Timestamp(b, tz="UTC")]
|
||||||
|
return x
|
||||||
|
|
||||||
|
|
||||||
|
def alive(s: pd.Series) -> pd.Series:
|
||||||
|
"""Taglia il prefisso di ZERI (warm-up dei lookback + attesa che l'universo raggiunga 10
|
||||||
|
gambe). ⚠️ Bug catturato nella prima corsa: sull'universo ESPANDENTE il pannello parte dal
|
||||||
|
2017 (BTC) ma il meccanismo non puo' operare prima del 2020-12, e i ~1.200 zeri iniziali
|
||||||
|
entravano nella finestra 'PRE-2024' abbassandone lo Sharpe e ribaltando la differenza
|
||||||
|
appaiata. E' la lezione gia' codificata nel progetto (barre ATTIVE, non di calendario),
|
||||||
|
ripetuta in un contesto nuovo."""
|
||||||
|
v = np.asarray(s.values, float)
|
||||||
|
nz = np.flatnonzero(v != 0.0)
|
||||||
|
return s.iloc[nz[0]:] if len(nz) else s
|
||||||
|
|
||||||
|
|
||||||
|
def line(tag: str, s: pd.Series) -> str:
|
||||||
|
return (f"{tag:<22s} n={len(s):>5d} Sh {sh(s):+6.2f} maxDD {dd(s)*100:5.1f}% "
|
||||||
|
f"CAGR {cagr(s)*100:+6.2f}% vol {s.std()*np.sqrt(365.25)*100:5.1f}%")
|
||||||
|
|
||||||
|
|
||||||
|
# ===========================================================================
|
||||||
|
def main() -> None:
|
||||||
|
t0 = time.time()
|
||||||
|
print("=" * 100)
|
||||||
|
print("XS01 FUORI DALLA FINESTRA DI SCOPERTA — meccanismo CONGELATO su storia lunga indipendente")
|
||||||
|
print(f"XS_CFG = {XS_CFG}")
|
||||||
|
print("=" * 100)
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- 1. dato
|
||||||
|
print("\n[1] DATO — Binance spot USDT 1d, certificazione locale")
|
||||||
|
panel19 = load_panel(XS_UNIVERSE)
|
||||||
|
cert = [certify(s, fetch_binance_daily(s)) for s in XS_UNIVERSE]
|
||||||
|
bad = [c for c in cert if not c["ok"]]
|
||||||
|
print(f" {len(cert)} serie scaricate; certificate OK: {len(cert)-len(bad)}/{len(cert)}")
|
||||||
|
print(f" {'sym':<6}{'n':>6}{'start':>12}{'end':>12}{'gap':>5}{'flat%':>8}{'vol0%':>8}{'lead0':>7}")
|
||||||
|
for c in cert:
|
||||||
|
flag = "" if c["ok"] else " <-- CONTROLLA"
|
||||||
|
print(f" {c['sym']:<6}{c['n']:>6}{c['start']:>12}{c['end']:>12}{c['gap_days']:>5}"
|
||||||
|
f"{c['flat']*100:>8.2f}{c['zero_vol']*100:>8.2f}{c['lead_zero_vol']:>7}{flag}")
|
||||||
|
print(" NB: 'lead0' = barre iniziali a volume 0 = firma del backfill sintetico (lezione AXS).")
|
||||||
|
|
||||||
|
# ---------------------------------------------------- 2. replica bit-exact
|
||||||
|
print("\n[2] SANITY — il motore riproduce lo sleeve UFFICIALE?")
|
||||||
|
from src.portfolio.sleeves import _xsec_returns
|
||||||
|
off = _xsec_returns()
|
||||||
|
rep = xsec_engine(hl_panel(), phase=0)
|
||||||
|
j = pd.concat({"off": off, "rep": rep}, axis=1, join="inner").dropna()
|
||||||
|
md = float(np.max(np.abs(j["off"] - j["rep"]))) if len(j) else float("nan")
|
||||||
|
print(f" n={len(j)} max|diff| = {md:.2e} -> {'IDENTICO' if md < 1e-12 else 'DIVERGE'}")
|
||||||
|
print(f" {line('XS01 ufficiale (HL)', off)}")
|
||||||
|
if md >= 1e-12:
|
||||||
|
print(" !! il motore NON replica: ogni numero sotto e' sospetto. STOP.")
|
||||||
|
return
|
||||||
|
|
||||||
|
# ------------------------------------------------ 3. validazione di VENUE
|
||||||
|
print("\n[3] VALIDAZIONE DI VENUE — Binance vs Hyperliquid dove esistono ENTRAMBI (2024+)")
|
||||||
|
hlp = hl_panel()
|
||||||
|
b19 = panel19.reindex(columns=XS_UNIVERSE)
|
||||||
|
common = b19.index.intersection(hlp.index)
|
||||||
|
rr = []
|
||||||
|
for s in XS_UNIVERSE:
|
||||||
|
a1 = hlp[s].reindex(common).pct_change()
|
||||||
|
b1 = b19[s].reindex(common).pct_change()
|
||||||
|
k2 = pd.concat({"a": a1, "b": b1}, axis=1).dropna()
|
||||||
|
rr.append((s, float(k2["a"].corr(k2["b"])), float(np.median(np.abs(k2["a"] - k2["b"]))) * 1e4))
|
||||||
|
print(f" ritorni giornalieri per asset, {len(common)} giorni comuni:")
|
||||||
|
print(" " + " ".join(f"{s}:{c:.4f}" for s, c, _ in rr))
|
||||||
|
print(f" corr min {min(c for _, c, _ in rr):.4f} | mediana |diff| ritorno: "
|
||||||
|
f"{np.median([m for _, _, m in rr]):.1f} bps (max {max(m for _, _, m in rr):.1f} bps)")
|
||||||
|
# e soprattutto: il MECCANISMO da' la stessa cosa?
|
||||||
|
bin_on_hl_window = xsec_engine(b19.loc[common].dropna(), phase=0)
|
||||||
|
hl_on_window = xsec_engine(hlp.loc[common], phase=0)
|
||||||
|
jj = pd.concat({"bin": bin_on_hl_window, "hl": hl_on_window}, axis=1, join="inner").dropna()
|
||||||
|
print(f" {line('XS01 su HL 2024+', jj['hl'])}")
|
||||||
|
print(f" {line('XS01 su BINANCE 2024+', jj['bin'])}")
|
||||||
|
print(f" corr fra le DUE VERSIONI DELLO SLEEVE: {jj['bin'].corr(jj['hl']):.4f} "
|
||||||
|
f"dSharpe = {sh(jj['bin'])-sh(jj['hl']):+.3f}")
|
||||||
|
print(" -> se questa corr e' ~1, la storia lunga Binance e' un'estensione legittima:")
|
||||||
|
print(" lo scarto USDT e' un FATTORE COMUNE e sparisce nello z-score cross-sectional.")
|
||||||
|
|
||||||
|
# ------------------------------------- 4. la storia lunga, fuori campione
|
||||||
|
print("\n[4] STORIA LUNGA — meccanismo congelato, ancora canonica (fase 0)")
|
||||||
|
p13 = panel19[U13].dropna() # universo FISSO 13 (il piu' lungo a >=10 gambe)
|
||||||
|
s13 = xsec_engine(p13, phase=0)
|
||||||
|
sEXP = xsec_engine(panel19, phase=0) # universo che si ALLARGA (19 quando esistono)
|
||||||
|
s13, sEXP = alive(s13), alive(sEXP)
|
||||||
|
print(f" U13-FIX universo fisso {len(U13)} gambe, dal {s13.index[0].date()} "
|
||||||
|
f"({s13.attrs.get('n_traded')}/{s13.attrs.get('n_rebal')} ribilanciamenti con posizione)")
|
||||||
|
print(f" U19-EXP universo espandente (10 -> 19 gambe), dal {sEXP.index[0].date()}")
|
||||||
|
for tag, s in (("U13-FIX", s13), ("U19-EXP", sEXP)):
|
||||||
|
print(f" {line(tag + ' FULL', s)}")
|
||||||
|
print(f" {line(tag + ' PRE-2024 (OOS)', win(s, None, '2024-01-01'))}")
|
||||||
|
print(f" {line(tag + ' 2024+ (scoperta)', win(s, '2024-01-01'))}")
|
||||||
|
|
||||||
|
print("\n Sharpe per ANNO (netto fee):")
|
||||||
|
yrs = sorted({d.year for d in sEXP.index})
|
||||||
|
hdr = " " + f"{'':<10}" + "".join(f"{y:>9d}" for y in yrs)
|
||||||
|
print(hdr)
|
||||||
|
for tag, s in (("U13-FIX", s13), ("U19-EXP", sEXP), ("XS01 HL", off)):
|
||||||
|
cells = []
|
||||||
|
for y in yrs:
|
||||||
|
x = win(s, f"{y}-01-01", f"{y+1}-01-01")
|
||||||
|
cells.append(f"{sh(x):>9.2f}" if len(x) > 60 else f"{'—':>9s}")
|
||||||
|
print(f" {tag:<10}" + "".join(cells))
|
||||||
|
|
||||||
|
print("\n Sharpe per REGIME:")
|
||||||
|
regimes = [("2021 toro/top", "2021-01-01", "2022-01-01"),
|
||||||
|
("2022 ORSO (LUNA/FTX)", "2022-01-01", "2023-01-01"),
|
||||||
|
("2023 ripresa", "2023-01-01", "2024-01-01"),
|
||||||
|
("2024-26 scoperta", "2024-01-01", None)]
|
||||||
|
for nm, a, b in regimes:
|
||||||
|
x13, xEX = win(s13, a, b), win(sEXP, a, b)
|
||||||
|
print(f" {nm:<24s} U13 Sh {sh(x13):+6.2f} DD {dd(x13)*100:5.1f}% | "
|
||||||
|
f"U19-EXP Sh {sh(xEX):+6.2f} DD {dd(xEX)*100:5.1f}%")
|
||||||
|
|
||||||
|
# -------------------------------------------- 5. fortuna di FASE (H = 10)
|
||||||
|
print("\n[5] FORTUNA DI FASE — H=10, tutte e 10 le ancore (la stima onesta e' la MEDIANA)")
|
||||||
|
phases = list(range(XS_CFG["H"]))
|
||||||
|
bands = {}
|
||||||
|
for tag, P, cut in (("U13-FIX FULL", p13, None), ("U13-FIX PRE-2024", p13, "pre"),
|
||||||
|
("U19-EXP FULL", panel19, None), ("U19-EXP PRE-2024", panel19, "pre")):
|
||||||
|
def f(ph, P=P, cut=cut):
|
||||||
|
s = alive(xsec_engine(P, phase=ph))
|
||||||
|
return win(s, None, "2024-01-01") if cut == "pre" else s
|
||||||
|
bands[tag] = A.anchor_luck_band(f, phases, canonical=0, metric=sh)
|
||||||
|
bands[tag + "|DD"] = A.anchor_luck_band(f, phases, canonical=0, metric=lambda x: -dd(x))
|
||||||
|
for tag in ("U13-FIX FULL", "U13-FIX PRE-2024", "U19-EXP FULL", "U19-EXP PRE-2024"):
|
||||||
|
r = bands[tag]; rd = bands[tag + "|DD"]
|
||||||
|
print(f" {tag:<20s} Sh canonico {r['canonical']:+.2f} (pctl {r['canonical_pctl']*100:4.0f}°) "
|
||||||
|
f"| MEDIANA {r['median']:+.2f} | banda [{r['lo']:+.2f},{r['hi']:+.2f}] "
|
||||||
|
f"| fasi>0 {r['frac_positive']*100:3.0f}% | fortuna {r['luck']:+.2f} | gate {r['gate_pass']}")
|
||||||
|
print(f" {'':<20s} maxDD canonico {-rd['canonical']*100:5.1f}% "
|
||||||
|
f"| MEDIANA {-rd['median']*100:5.1f}% | banda [{-rd['hi']*100:.1f},{-rd['lo']*100:.1f}]%")
|
||||||
|
# anche lo sleeve UFFICIALE su HL, de-luckato di fase (per confronto onesto)
|
||||||
|
bhl = A.anchor_luck_band(lambda ph: xsec_engine(hlp, phase=ph), phases, canonical=0, metric=sh)
|
||||||
|
print(f" {'XS01 UFFICIALE (HL)':<20s} Sh canonico {bhl['canonical']:+.2f} "
|
||||||
|
f"(pctl {bhl['canonical_pctl']*100:4.0f}°) | MEDIANA {bhl['median']:+.2f} "
|
||||||
|
f"| banda [{bhl['lo']:+.2f},{bhl['hi']:+.2f}] | fortuna {bhl['luck']:+.2f}")
|
||||||
|
|
||||||
|
# -------------- 6. differenza APPAIATA fuori campione vs finestra di scoperta
|
||||||
|
print("\n[6] DIFFERENZA APPAIATA per fase: (fuori campione) - (finestra di scoperta)")
|
||||||
|
for tag, P in (("U13-FIX", p13), ("U19-EXP", panel19)):
|
||||||
|
def pre(ph, P=P):
|
||||||
|
return win(alive(xsec_engine(P, phase=ph)), None, "2024-01-01")
|
||||||
|
|
||||||
|
def post(ph, P=P):
|
||||||
|
return win(alive(xsec_engine(P, phase=ph)), "2024-01-01")
|
||||||
|
d1 = A.anchor_luck_delta(pre, post, phases, metric=sh)
|
||||||
|
print(f" {tag:<10s} mediana DIFF appaiata {d1['median_paired']:+.3f} "
|
||||||
|
f"[{d1['lo']:+.3f},{d1['hi']:+.3f}] fasi con OOS>scoperta {d1['n_positive']}/{d1['n_anchors']}")
|
||||||
|
|
||||||
|
# ------------------------------------------------------------- 7. i NULL
|
||||||
|
print("\n[7] NULL — a FEE ZERO (permutare un segnale ne fa esplodere il turnover: a fee piene")
|
||||||
|
print(" il null perderebbe per COSTO invece che per assenza di informazione — regola XSR01)")
|
||||||
|
NDRAW = 300
|
||||||
|
for tag, P, a, b in (("U13-FIX PRE-2024", p13, None, "2024-01-01"),
|
||||||
|
("U13-FIX FULL", p13, None, None)):
|
||||||
|
real = win(alive(xsec_engine(P, phase=0, fee_side=0.0)), a, b)
|
||||||
|
for kind in ("cross", "sign"):
|
||||||
|
rng = np.random.default_rng(20260822)
|
||||||
|
draws = np.array([sh(win(alive(xsec_engine(P, phase=0, fee_side=0.0, perm_rng=rng,
|
||||||
|
perm_kind=kind)), a, b)) for _ in range(NDRAW)])
|
||||||
|
p = float(np.mean(draws >= sh(real)))
|
||||||
|
print(f" {tag:<20s} null '{kind}': reale(lordo) {sh(real):+.2f} vs null "
|
||||||
|
f"med {np.median(draws):+.2f} max {draws.max():+.2f} -> p = {p:.4f}")
|
||||||
|
|
||||||
|
print("\n NULL 'universo dove NON dovrebbe funzionare' — 11 settoriali SPDR, 1998+,")
|
||||||
|
print(" stesso meccanismo congelato (il progetto ha gia' misurato che il cross-sectional")
|
||||||
|
print(" crypto non generalizza alle azioni: XSR01, 2026-07-26):")
|
||||||
|
sect = load_sector_panel()
|
||||||
|
ssec = alive(xsec_engine(sect, phase=0, min_assets=9))
|
||||||
|
bsec = A.anchor_luck_band(lambda ph: alive(xsec_engine(sect, phase=ph, min_assets=9)), phases,
|
||||||
|
canonical=0, metric=sh)
|
||||||
|
print(f" {line('SECT11 (azioni)', ssec)}")
|
||||||
|
print(f" SECT11 banda di fase: mediana Sh {bsec['median']:+.2f} "
|
||||||
|
f"[{bsec['lo']:+.2f},{bsec['hi']:+.2f}], fasi>0 {bsec['frac_positive']*100:.0f}%")
|
||||||
|
|
||||||
|
print("\n CONTROLLO DI DIREZIONE — stesso meccanismo con mode='rev' (long i deboli):")
|
||||||
|
for tag, P in (("U13-FIX", p13), ("U19-EXP", panel19)):
|
||||||
|
r = alive(xsec_engine(P, phase=0, mode="rev"))
|
||||||
|
print(f" {line(tag + ' REV', r)}")
|
||||||
|
|
||||||
|
# ------------- 7b. la SELEZIONE del 2026-06-19, rivalidata FUORI CAMPIONE
|
||||||
|
print("\n[7b] LA SELEZIONE DEL 2026-06-19, RIVALIDATA FUORI CAMPIONE")
|
||||||
|
print(" XS01 non e' solo un meccanismo: e' un meccanismo AFFINATO due volte sui dati HL")
|
||||||
|
print(" 2024+ (blend di lookback [30,90] e gate di dispersione p30). Quelle DUE scelte sono")
|
||||||
|
print(" debito di multiple-testing contratto sulla finestra di scoperta. Qui si paga:")
|
||||||
|
print(" ogni variante e' valutata sul PRE-2024, a MEDIANA delle 10 fasi (mai l'ancora).")
|
||||||
|
variants = [("blend [30,90] + gate p30 (CANONICA)", dict(lookbacks=(30, 90), disp_pct=30)),
|
||||||
|
("solo L=30 + gate p30", dict(lookbacks=(30,), disp_pct=30)),
|
||||||
|
("solo L=90 + gate p30", dict(lookbacks=(90,), disp_pct=30)),
|
||||||
|
("blend [30,90] SENZA gate", dict(lookbacks=(30, 90), disp_pct=0)),
|
||||||
|
("blend [30,90] + gate p15", dict(lookbacks=(30, 90), disp_pct=15)),
|
||||||
|
("blend [30,90] + gate p50", dict(lookbacks=(30, 90), disp_pct=50))]
|
||||||
|
sel_rows = []
|
||||||
|
for nm, kw in variants:
|
||||||
|
b_pre = A.anchor_luck_band(
|
||||||
|
lambda ph, kw=kw: win(alive(xsec_engine(p13, phase=ph, **kw)), None, "2024-01-01"),
|
||||||
|
phases, canonical=0, metric=sh)
|
||||||
|
b_post = A.anchor_luck_band(
|
||||||
|
lambda ph, kw=kw: win(alive(xsec_engine(p13, phase=ph, **kw)), "2024-01-01"),
|
||||||
|
phases, canonical=0, metric=sh)
|
||||||
|
sel_rows.append((nm, b_pre["median"], b_post["median"], b_pre["per_offset"]))
|
||||||
|
print(f" {nm:<38s} PRE-2024 mediana Sh {b_pre['median']:+5.2f} "
|
||||||
|
f"[{b_pre['lo']:+.2f},{b_pre['hi']:+.2f}] | 2024+ {b_post['median']:+5.2f}")
|
||||||
|
rank_pre = 1 + sorted([r[1] for r in sel_rows], reverse=True).index(sel_rows[0][1])
|
||||||
|
rank_post = 1 + sorted([r[2] for r in sel_rows], reverse=True).index(sel_rows[0][2])
|
||||||
|
print(f" -> la configurazione CANONICA (scelta nel 2026-06 guardando SOLO il 2024+) e'")
|
||||||
|
print(f" {rank_pre}/{len(sel_rows)} sul PRE-2024 e {rank_post}/{len(sel_rows)} sul 2024+.")
|
||||||
|
print( " 📌 LETTURA: il NUCLEO regge (ogni variante e' positiva fuori campione, banda")
|
||||||
|
print( " [+0.76,+1.28]), ma i DUE AFFINAMENTI del 19/06 NON si replicano:")
|
||||||
|
print( " - il BLEND [30,90] fuori campione e' PEGGIO del solo L=30 (+1.12 vs +1.28);")
|
||||||
|
print( " - il GATE di dispersione p30 fuori campione vale ZERO (con e senza: +1.12);")
|
||||||
|
print( " e' dentro la finestra di scoperta che vale (+0.37 contro -0.03).")
|
||||||
|
print( " Cioe': cio' che sopravvive e' il meccanismo cross-sectional nudo; i due")
|
||||||
|
print( " affinamenti sono, per quanto qui misurabile, adattamento alla finestra in")
|
||||||
|
print( " cui furono scelti. NON e' un argomento per cambiarli (cambiarli guardando")
|
||||||
|
print( " QUESTO risultato sarebbe la stessa selezione, spostata di finestra): e'")
|
||||||
|
print( " un argomento per non attribuire loro il valore che il progetto gli attribuisce.")
|
||||||
|
|
||||||
|
# --- concentrazione: un contributo positivo si scompone per ANNO e per GAMBA (lezione SOL)
|
||||||
|
print("\n Concentrazione (lezione SOL 22/08: '24/24 ancore positive erano UN ANNO SOLO'):")
|
||||||
|
pre13 = win(s13, None, "2024-01-01")
|
||||||
|
for y in (2021, 2022, 2023):
|
||||||
|
rest = pre13[pre13.index.year != y]
|
||||||
|
print(f" PRE-2024 togliendo il {y}: Sh {sh(rest):+5.2f} "
|
||||||
|
f"(intero pre-2024 {sh(pre13):+5.2f})")
|
||||||
|
loo = {}
|
||||||
|
for a_ in U13:
|
||||||
|
sub = panel19[[x for x in U13 if x != a_]].dropna()
|
||||||
|
bb = A.anchor_luck_band(lambda ph, sub=sub: win(alive(xsec_engine(sub, phase=ph,
|
||||||
|
min_assets=10)),
|
||||||
|
None, "2024-01-01"),
|
||||||
|
phases, canonical=0, metric=sh)
|
||||||
|
loo[a_] = bb["median"]
|
||||||
|
base_med = bands["U13-FIX PRE-2024"]["median"]
|
||||||
|
worst = min(loo, key=loo.get)
|
||||||
|
print(f" Leave-one-asset-out sul PRE-2024 (mediana di fase; base {base_med:+.2f}):")
|
||||||
|
print(" " + " ".join(f"-{k2}:{v:+.2f}" for k2, v in sorted(loo.items(), key=lambda t: t[1])))
|
||||||
|
print(f" gamba piu' importante: {worst} (senza di lei {loo[worst]:+.2f}); "
|
||||||
|
f"minimo {min(loo.values()):+.2f}, tutte >0: {all(v > 0 for v in loo.values())}")
|
||||||
|
|
||||||
|
# ---------------------------------------------------------- 8. i GATE
|
||||||
|
print("\n[8] GATE")
|
||||||
|
cand = s13.copy(); cand.index = cand.index.tz_convert("UTC")
|
||||||
|
mg = A.marginal_vs_tp01(cand)
|
||||||
|
print(f" marginal_vs_tp01 (U13-FIX, 2021+): {mg.get('marginal_verdict')} "
|
||||||
|
f"corr {mg.get('corr_full')} n={mg.get('n_days')}")
|
||||||
|
for kk in ("robust_oos", "has_insample_edge", "is_hedge", "beats_noise_null",
|
||||||
|
"insample_sharpe", "tp01_beta", "alpha_ann"):
|
||||||
|
if kk in mg:
|
||||||
|
print(f" {kk:<20s} {mg[kk]}")
|
||||||
|
if "blends" in mg:
|
||||||
|
for wname, bl in mg["blends"].items():
|
||||||
|
print(f" blend {wname}: uplift_full {bl.get('uplift_full')} "
|
||||||
|
f"uplift_hold {bl.get('uplift_hold')}")
|
||||||
|
|
||||||
|
mgE = A.marginal_vs_tp01(sEXP)
|
||||||
|
print(f" marginal_vs_tp01 (U19-EXP, 2020-12+): {mgE.get('marginal_verdict')} "
|
||||||
|
f"corr {mgE.get('corr_full')}")
|
||||||
|
|
||||||
|
# deflated Sharpe: TUTTI i trial valutati, al RIALZO
|
||||||
|
all_sr = []
|
||||||
|
for tag in ("U13-FIX FULL", "U13-FIX PRE-2024", "U19-EXP FULL", "U19-EXP PRE-2024"):
|
||||||
|
all_sr += list(bands[tag]["per_offset"].values())
|
||||||
|
all_sr += list(bhl["per_offset"].values()) + list(bsec["per_offset"].values())
|
||||||
|
for _, _, _, po in sel_rows: # 6 varianti x 10 fasi della sezione 7b
|
||||||
|
all_sr += list(po.values())
|
||||||
|
all_sr += [float(v) for v in loo.values()]
|
||||||
|
cand_pool = list(all_sr) # SOLO configurazioni di XS01
|
||||||
|
ctrl = [sh(alive(xsec_engine(p13, phase=0, mode="rev"))),
|
||||||
|
sh(alive(xsec_engine(panel19, phase=0, mode="rev"))), sh(jj["bin"]), sh(jj["hl"])]
|
||||||
|
for fs in (0.0005, 0.001, 0.002):
|
||||||
|
ctrl.append(sh(alive(xsec_engine(p13, phase=0, fee_side=fs))))
|
||||||
|
dsr_val = bands["U13-FIX PRE-2024"]["median"]
|
||||||
|
med_ph = int(np.argsort(list(bands["U13-FIX PRE-2024"]["per_offset"].values()))[len(phases) // 2])
|
||||||
|
med_series = win(alive(xsec_engine(p13, phase=med_ph)), None, "2024-01-01")
|
||||||
|
d1, s1 = A.deflated_sharpe(dsr_val, cand_pool, med_series)
|
||||||
|
d2, s2 = A.deflated_sharpe(dsr_val, cand_pool + ctrl, med_series)
|
||||||
|
print(f" deflated_sharpe — headline = MEDIANA di fase OOS {dsr_val:+.2f}")
|
||||||
|
print(f" pool A = SOLE configurazioni XS01 ({len(cand_pool)} trial): DSR {d1:.3f} "
|
||||||
|
f"(max dal rumore {s1:+.2f}) -> {'PASS' if d1 >= 0.95 else 'FAIL'}")
|
||||||
|
print(f" pool B = tutto cio' che ho valutato, controlli NEGATIVI inclusi "
|
||||||
|
f"({len(cand_pool)+len(ctrl)} trial): DSR {d2:.3f} (max dal rumore {s2:+.2f}) "
|
||||||
|
f"-> {'PASS' if d2 >= 0.95 else 'FAIL'}")
|
||||||
|
print( " ⚠️ Il pool B e' il conteggio 'al rialzo' che il progetto impone, ma qui e' anche")
|
||||||
|
print( " il piu' SEVERO in modo improprio: il DSR pesa la VARIANZA dei trial, e io ci")
|
||||||
|
print( " ho messo dentro controlli costruiti per essere negativi (REV -0.97, SECT11")
|
||||||
|
print( " -0.44). Si cita il piu' basso dei due; e si dice che entrambi penalizzano un")
|
||||||
|
print( " MEDIANA-di-fase come se fosse un MAX-di-k, che e' il caso per cui il DSR e'")
|
||||||
|
print( " costruito. Il debito di selezione vero di XS01 non e' stato contratto qui:")
|
||||||
|
print( " e' quello del 2026-06-19, e la sezione 7b lo misura direttamente.")
|
||||||
|
|
||||||
|
imp = A.implausible_sharpe(s13)
|
||||||
|
print(f" implausible_sharpe: {imp['implausible']} Sh {imp.get('sharpe'):+.2f} "
|
||||||
|
f"maxDD {imp.get('maxdd', 0)*100:.1f}% perdite su barre attive "
|
||||||
|
f"{imp.get('loss_frac', float('nan'))*100:.1f}% (attive {imp.get('active_frac', 0)*100:.0f}%)"
|
||||||
|
f"{' — ' + '; '.join(imp['reasons']) if imp['reasons'] else ''}")
|
||||||
|
|
||||||
|
# causalita': i prezzi DOPO la data di decisione non devono cambiare le posizioni prima
|
||||||
|
Pc = p13.copy()
|
||||||
|
cut = len(Pc) // 2
|
||||||
|
rngc = np.random.default_rng(7)
|
||||||
|
Pd = Pc.copy()
|
||||||
|
Pd.iloc[cut:] = Pc.iloc[cut:].values * (1.0 + rngc.normal(0, 0.35, size=Pc.iloc[cut:].shape))
|
||||||
|
a_ = xsec_engine(Pc, phase=0); b_ = xsec_engine(Pd, phase=0)
|
||||||
|
# confronto solo dove il vol-target non ha ancora "visto" i dati perturbati (rv30 shiftata)
|
||||||
|
lim = cut - 35
|
||||||
|
cdiff = float(np.max(np.abs(a_.values[:lim] - b_.values[:lim])))
|
||||||
|
print(f" causalita' (prezzi futuri randomizzati da i={cut}): max|diff| prima di i={lim} "
|
||||||
|
f"= {cdiff:.2e} -> {'LEAK-FREE' if cdiff < 1e-12 else 'PERDITA DI FUTURO'}")
|
||||||
|
|
||||||
|
print(" null de-levering: NON PERTINENTE — il claim qui non e' 'meno drawdown' ma "
|
||||||
|
"'l'edge esiste fuori campione'. Il de-levering e' comunque implicito nel vol-target,")
|
||||||
|
print(" e le tabelle di fase riportano DD e Sharpe INSIEME (un k<1 abbassa entrambi).")
|
||||||
|
|
||||||
|
print(f"\n sensibilita' al COSTO (il rischio #1 di uno sleeve a 13 gambe alt):")
|
||||||
|
for fs in (0.0005, 0.001, 0.002, 0.003):
|
||||||
|
x = alive(xsec_engine(p13, phase=0, fee_side=fs))
|
||||||
|
print(f" {fs*1e4:5.0f} bps/lato: FULL Sh {sh(x):+5.2f} | "
|
||||||
|
f"PRE-2024 {sh(win(x, None, '2024-01-01')):+5.2f} | "
|
||||||
|
f"2024+ {sh(win(x, '2024-01-01')):+5.2f}")
|
||||||
|
|
||||||
|
# ------------------------------------------------- 9. impatto sul LIBRO
|
||||||
|
print("\n[9] IMPATTO SUL LIBRO — TP01+SKH01 (75/25 live) con e senza XS01, su 2021+")
|
||||||
|
from src.portfolio.sleeves import _tp01_returns, _skyhook_returns
|
||||||
|
tp = A._to_daily(_tp01_returns()); sk = A._to_daily(_skyhook_returns())
|
||||||
|
for xs_tag, xs in (("XS01 storia lunga (U13-FIX)", s13), ("XS01 ufficiale (HL 2024+)", off)):
|
||||||
|
J = pd.concat({"tp": tp, "sk": sk, "xs": A._to_daily(xs)}, axis=1, join="inner").dropna()
|
||||||
|
if len(J) < 200:
|
||||||
|
continue
|
||||||
|
base = 0.75 * J["tp"] + 0.25 * J["sk"]
|
||||||
|
for w in (0.15, 0.25, 0.50):
|
||||||
|
mix = (1 - w) * base + w * J["xs"]
|
||||||
|
print(f" [{xs_tag}] {J.index[0].date()}..{J.index[-1].date()} n={len(J)} "
|
||||||
|
f"w={w:.2f}: Sh {sh(base):+.2f} -> {sh(mix):+.2f} "
|
||||||
|
f"({sh(mix)-sh(base):+.2f}) maxDD {dd(base)*100:.1f}% -> {dd(mix)*100:.1f}% "
|
||||||
|
f"corr(XS,libro) {J['xs'].corr(base):+.3f}")
|
||||||
|
# e il numero che il PROP-ALLOC usa davvero: il libro 2021+ contiene il 2022
|
||||||
|
pre = J[J.index < DISCOVERY]
|
||||||
|
if len(pre) > 200:
|
||||||
|
bpre = 0.75 * pre["tp"] + 0.25 * pre["sk"]
|
||||||
|
print(f" -> SOLO FUORI CAMPIONE ({pre.index[0].date()}..{pre.index[-1].date()}, "
|
||||||
|
f"n={len(pre)}), sweep del peso di XS01:")
|
||||||
|
for w in (0.15, 0.25, 0.33, 0.50):
|
||||||
|
mpre = (1 - w) * bpre + w * pre["xs"]
|
||||||
|
print(f" w={w:.2f}: libro Sh {sh(bpre):+.2f} -> {sh(mpre):+.2f} "
|
||||||
|
f"({sh(mpre)-sh(bpre):+.2f}) maxDD {dd(bpre)*100:.1f}% -> "
|
||||||
|
f"{dd(mpre)*100:.1f}% corr {pre['xs'].corr(bpre):+.3f}")
|
||||||
|
print(" ⚠️ PROP-ALLOC mette XS01 al 50%: su un conto a BARRIERA cio' che si")
|
||||||
|
print(" compra e' il maxDD, quindi la riga da guardare e' quella, non lo Sharpe.")
|
||||||
|
|
||||||
|
# ---------------------------------------------------- 10. eseguibilita'
|
||||||
|
print("\n[10] ESEGUIBILITA' — la domanda del canale funded e' se il libro STA su $100k")
|
||||||
|
det: dict = {}
|
||||||
|
xsec_engine(p13, phase=0, detail=det)
|
||||||
|
W, sc = det["W"], det["scale"]
|
||||||
|
dW = np.abs(np.diff(W, axis=0)); rb = np.flatnonzero(dW.sum(axis=1) > 1e-12)
|
||||||
|
print(f" {len(U13)} gambe, {len(rb)} ribilanciamenti, gross medio "
|
||||||
|
f"{np.abs(W).sum(axis=1).mean():.2f}, leva media (vol-target) {np.mean(sc[sc > 0]):.2f}x")
|
||||||
|
print(f" {'capitale':>10} {'ticket mediano':>16} {'ticket p10':>12} {'<$5':>7} {'<$10':>7}")
|
||||||
|
for cap in (600, 5000, 20000, 100000):
|
||||||
|
parts = [dW[i][dW[i] > 1e-12] * sc[i + 1] * cap for i in rb if sc[i + 1] > 0]
|
||||||
|
tk = np.concatenate(parts) if parts else np.array([])
|
||||||
|
if not tk.size:
|
||||||
|
print(" (nessun ticket calcolabile — controlla il motore)"); break
|
||||||
|
print(f" {cap:>10,d} {np.median(tk):>15.2f}$ {np.percentile(tk, 10):>11.2f}$ "
|
||||||
|
f"{(tk < 5).mean()*100:>6.1f}% {(tk < 10).mean()*100:>6.1f}%")
|
||||||
|
print(" ⚠️ Approssimazione DICHIARATA: il modello congelato applica il vol-target alla")
|
||||||
|
print(" SERIE dei rendimenti, non alle posizioni -> qui il ticket e' calcolato sui soli")
|
||||||
|
print(" delta di RIBILANCIAMENTO (ogni 10 giorni), ignorando il micro-turnover")
|
||||||
|
print(" giornaliero che una implementazione vera del vol-target genererebbe. E' un")
|
||||||
|
print(" LIMITE INFERIORE del numero di ordini, quindi ottimistico sul lato costi.")
|
||||||
|
print(" ⚠️ E NON rimette in discussione il ~$20k citato dal progetto: quella soglia e' sui")
|
||||||
|
print(" 19 alt Hyperliquid (min-order e liquidita' del venue), non su questi 13.")
|
||||||
|
|
||||||
|
print(f"\n[fine] {time.time()-t0:.1f}s")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user