merge: ondata 26/07-bis — TP01 barra parziale + gate implausible_sharpe/anchor_luck

This commit is contained in:
Adriano Dal Pastro
2026-07-25 22:36:40 +00:00
7 changed files with 947 additions and 2 deletions
+52
View File
@@ -557,6 +557,58 @@ Prima ondata di ricerca onesta su BTC/ETH certificati (5 track, harness condivis
ACCOPPIAMENTO al rendimento del giorno** — qui un test "i percentili coincidono" sarebbe PASSATO
con la stima sbagliata di 2-3×. Ogni regola valutata sul minimo (daily-loss, trailing DD, stop di
conto) va misurata su **tuple accoppiate**, mai su un wick estratto a parte.
- **Ondata 2026-07-26-bis (TP01 su barra parziale + i 2 gate mai costruiti) — 0 sleeve nuovi,
1 audit sul libro live, 2 gate codificati, 1 falso positivo MIO su uno sleeve in produzione.**
Script `r0726_tp01_partial_day.py` + `r0726_gates_retro.py`, test
`tests/test_gates_implausible_anchor.py` (13), diario `2026-07-26-wave-tp01-partial-gates.md`.
**Book/pesi/cron/config INVARIATI.**
(1) **T1 — anche TP01 legge una barra giornaliera PARZIALE nel live, ma non conta.** Stesso fatto
strutturale di SKH01: `resample_tf` non scarta il giorno in corso e `current_target` prende `[-1]`;
il feed si ricostruisce alle 00:30 UTC → tutto il giorno il book vede oggi come **1 barra oraria
su 24** (verificato `barre1h=1/24`). Docstring "ultima barra CHIUSA" **falso**, corretto (3ª volta
in 2 giorni che un docstring di produzione dichiara una causalita' che il codice non ha).
Tre path a un grado di liberta' per volta, 24 ancore, **differenze appaiate**: *barra parziale*
ΔFULL **0.031** (pos. 7/24) / ΔHOLD **+0.118** (pos. 19/24); *ritardo d'esecuzione 1h* **0.004**
(11/24 = moneta); leva gonfiata **+0.4%**. → **trascurabile, nessun cambio al live.**
⚠️ All'ancora canonica sembra peggio del vero: a offset 0 la parziale costa **0.230** di hold-out
= **il minimo dell'intera banda** (mediana +0.118) → guardando solo l'ancora canonica avrei
concluso "il live degrada TP01": speculare alla lezione del 26/07 (li' l'ancora canonica
*nascondeva* un vantaggio, qui *inventa* un danno).
**REGOLA NUOVA (il risultato trasferibile):** la parzialita' dell'ultima barra conta **in
proporzione a quanto il segnale pesa la barra piu' recente**. SKH01 = Donchian breakout su 230m,
la barra corrente E' il segnale → **+0.38**; TP01 = TSMOM 30/90/180g, l'ora mancante e' 1/24 di
UNA osservazione su 30-180 → **±0.03**. **La conclusione non si trasferisce fra sleeve.**
(2) **T2 — `implausible_sharpe` e `anchor_luck_band` CODIFICATI in `altlib.py`** (debito
raccomandato 3 volte — 26/06 CC01, 02/07 Albimarini "con piu' forza" — e mai scritto).
`implausible_sharpe`: Sharpe>3, perdite/attive<2%, Calmar>20 o maxDD~0, **regola del tre**
(0 perdite su N → tasso vero fino a 3/N: e' perche' "0/142" non si legge "senza rischio").
`anchor_luck_band`: la griglia d'ancore NON e' una griglia di parametri → **il deflated-Sharpe non
la conta**; riporta stima onesta = **mediana della banda** e fortuna = canonica mediana.
`anchor_luck_delta`: **mediana delle differenze appaiate** (codifica l'errore del 26/07).
⚠️ **Il gate ha segnalato VRP01 e il difetto era MIO:** perdite contate su TUTTE le barre, ma
VRP01 e' settimanale su griglia giornaliera = **94.2% di zeri** → "0.96%, coda assente" su uno
sleeve **in produzione**; sulle barre **ATTIVE** e' **16.5%**, la forma giusta di un credit spread
a rischio definito. **E la lezione era gia' cablata il giorno prima** nel monitor DVOLSPREAD
("barre attive, non giorni di calendario") — imparata in un contesto, non applicata nell'altro.
Zeri per sleeve: VRP01 94.2 / SKH01 87.6 / XS01 39.6 / GTAA01 31.9 / TP01 30.7%.
**Applicazione retroattiva 7/7 — tutti ok:** TP01 1.29, XS01 1.36, VRP01 1.08, SKH01 1.45,
GTAA01 1.01, **XSR01 1.79 (DD 2.5% ma 47.8% di barre attive in perdita → il DD basso e' vol bassa
+ dollar-neutrality, NON una coda mancante: per il gate del 23/10 il rischio #1 resta lo slippage)**,
DVOLSPREAD 0.68. Controlli positivi obbligatori superati (firme CC01 e deep-OTM segnalate, rumore
Sh 0.62 no) — *un gate che non segnala nulla puo' essere semplicemente rotto*.
**Replica indipendente del finding d'ancora del 02/07:** il gate applicato alla cieca a TP01
ritrova canonica **+0.237 = 92° pctl** delle 24, **mediana onesta +0.056**, banda [0.087,+0.247],
fortuna +0.182 — contro mediana 0.04 / banda [0.13,+0.30] misurate il 02/07 con implementazione
SEPARATA. **L'hold-out onesto di TP01 e' ~+0.05, non 0.31** (il valore del sleeve resta il taglio
del DD ~6× vs buy&hold, che non e' ancorato).
(3) **NON fatto e perche':** il book ricalcolato sul **path live** (sintesi naturale: SKH01 live >
modello su entrambi i lati, TP01 live ≈ modello) e' bloccato da una **incompatibilita' di lenti**
il simulatore SKH01 compone per-trade a nozionale unitario, lo sleeve e' vol-targeted; combinarle
darebbe un numero preciso e falso. **Follow-up dichiarato:** serve la versione vol-targeted del
path live di SKH01 prima di toccare il fattore de-luck. Conseguenza gia' solida: il **×0.6** fu
scelto assumendo che il live degradi, ma su SKH01 e' misurato il contrario e su TP01 e' ~zero →
**il ×0.6 e' probabilmente CONSERVATIVO** (la nota "rischia di contare due volte la degradazione
SKH01" e' confermata, non ipotetica).
- **Ondata 2026-07-26 (3 filoni: esecuzione SKH01 / DVOLSPREAD / XSR01 fuori dal crypto) — 0 sleeve
nuovi, 1 raccomandazione operativa aperta, 1 lead promosso, 1 falsificazione.** Script
`r0726_{skh_onbook,dvolspread_gate,xsr_equity}.py`, test `tests/test_wave_0726.py` (11 casi),
@@ -0,0 +1,172 @@
# 2026-07-26-bis — Ondata: TP01 su barra parziale + i due gate mai costruiti
**Richiesta:** *"fai un'altra ondata di ricerca"*.
**Esito: 0 sleeve nuovi, 1 audit sul libro live, 2 gate codificati, 1 falso positivo del mio
stesso gate su uno sleeve in produzione (catturato e corretto), 1 replica indipendente di un
finding di tre settimane fa.**
**Book, pesi, cron, config: INVARIATI.**
Script: `scripts/research/r0726_tp01_partial_day.py`, `scripts/research/r0726_gates_retro.py`.
Test: `tests/test_gates_implausible_anchor.py` (13). Gate in `scripts/research/alt/altlib.py`.
Da dove nasce: il 26/07 ho misurato che il path live di SKH01 divergeva dal modello su entrambi
i lati (uscite e ingressi) e che **il modello era pessimistico**. Due domande restavano aperte, ed
erano piu' interessanti di un'ennesima caccia al segnale: *lo stesso difetto esiste sullo sleeve
che pesa il 75% del book?* e *perche' i gate che il progetto si e' raccomandato tre volte non
esistono ancora?*
---
## T1 — TP01 nel live legge una barra giornaliera fatta di UN'ORA
**Il fatto, verificato non dedotto.** `resample_tf(..., label="left", closed="left")` non scarta il
giorno in corso; `TrendPortfolio.current_target` prende `target_series(df)[-1]`. Il feed certificato
si ricostruisce alle 00:30 UTC, quindi per tutta la giornata il book vede il giorno corrente come
**1 barra oraria su 24** (misurato: `barre1h=1/24`). Il docstring diceva "ultima barra CHIUSA":
**falso nel live**, ed e' la terza volta in due giorni che un docstring di produzione afferma una
causalita' che il codice non ha.
**Due canali con segno atteso opposto**, quindi vanno separati:
- *esecuzione ritardata* — il modello ribilancia al confine 00:00, il live al primo cron dopo il
rebuild (~01:00);
- *barra parziale* — `c[t]` e' il prezzo delle 01:00 e `r[t]` e' un rendimento di **un'ora** contato
come giornaliero nella finestra di vol a 30 barre → vol sottostimata → **sovra-leva sistematica**.
**Tre path, un grado di liberta' per volta**, su griglia oraria (i confini sono diversi: solo
l'orario li rende confrontabili):
| offset 0 (ancora canonica) | FULL | HOLD | maxDD |
|---|---:|---:|---:|
| MODEL — `tgt[t-1]` dal confine 00:00 | 1.30 | 0.24 | 16.7% |
| CLOSED@+1h — scarta la parziale, ora del live | 1.27 | 0.16 | 17.5% |
| LIVE — `tgt[t]` da barra parziale, +1h | 1.25 | 0.07 | 17.5% |
**Banda delle 24 ancore, mediana delle DIFFERENZE APPAIATE:**
| effetto | FULL | HOLD |
|---|---|---|
| barra parziale (LIVE CLOSED) | **0.031** [0.135,+0.065], pos. 7/24 | **+0.118** [0.230,+0.277], pos. 19/24 |
| ritardo 1h (CLOSED MODEL) | 0.004 [0.053,+0.034], pos. 11/24 | 0.007, pos. 9/24 |
| totale (LIVE MODEL) | 0.046, pos. 7/24 | +0.128, pos. 18/24 |
Leva LIVE/MODEL: **1.004** (mediana, invariata su tutta la banda).
**Verdetto: trascurabile.** Il ritardo d'esecuzione e' letteralmente una moneta (11/24). La barra
parziale muove ±0.03 su FULL e la sua "vittoria" su HOLD (+0.118) e' su una finestra sola e con
segno opposto su FULL — cioe' esattamente il profilo che il progetto ha gia' codificato come
sospetto. La sovra-leva prevista esiste ma vale **0.4%**. Nessun cambio al live.
**⚠️ Da notare: all'ancora canonica il quadro sembra peggiore del vero.** A offset 0 la barra
parziale costa **0.230** sull'hold-out, che e' il **minimo dell'intera banda** — mentre la mediana
e' +0.118. Guardando solo l'ancora canonica avrei concluso "il live degrada TP01 di 0.23 sull'hold-out"
e sarebbe stato l'artefatto di un'ancora. E' la stessa lezione del 26/07 in versione speculare
(li' l'ancora canonica *nascondeva* un vantaggio, qui *inventa* un danno).
**Il risultato trasferibile non e' il numero, e' il perche'.** La parzialita' dell'ultima barra conta
in proporzione a **quanto il segnale pesa la barra piu' recente**:
> SKH01 e' un Donchian breakout su barra 230m: **la barra corrente E' il segnale** → stesso difetto,
> **+0.38** di Sharpe mediano. TP01 e' un TSMOM 30/90/180 giorni: l'ora mancante e' 1/24 di **una**
> osservazione su 30-180 → **±0.03**.
Corollario operativo: la conclusione di uno sleeve su questo tema **non si trasferisce** all'altro,
e la domanda "il mio live legge barre parziali?" ha una risposta diversa per ogni sleeve a seconda
della lunghezza del lookback. Cablato nei docstring di `current_target` e `resample_tf`.
---
## T2 — I due gate raccomandati tre volte e mai scritti
Debito dichiarato: `implausible_sharpe` (raccomandato il 26/06 su CC01, ri-raccomandato "con piu'
forza" il 02/07 su Albimarini) e `anchor_luck_band` (dichiarato "candidato gate futuro" il 02/07,
dopo il timing-luck trovato su 4/4 sleeve ancorati). Tre occorrenze, tre diagnosi a mano, zero codice.
### `implausible_sharpe`
Uno Sharpe/track troppo bello significa che **il rischio e' fuori dal dataset**, non che la strategia
sia buona. Trigger: Sharpe > 3, frazione di barre attive in perdita < 2%, Calmar > 20 o maxDD ~0, e
la **regola del tre** (0 perdite su N trade lascia un tasso vero fino a ~3/N — su un payoff deep-OTM
basta a ribaltare l'expectancy: e' il motivo per cui "0/142" non si legge come "senza rischio").
### `anchor_luck_band` + `anchor_luck_delta`
La griglia di ancore **non e' una griglia di parametri**, quindi il deflated-Sharpe non la conta: e'
multiple-testing invisibile. Il gate riporta la stima onesta (**mediana della banda**) e la fortuna
(canonica mediana). `anchor_luck_delta` codifica l'errore che ho commesso stamattina: fra offset
appaiati la statistica e' la **mediana delle differenze**, non la differenza delle mediane — le due
mediane cadono su offset diversi e il verdetto puo' ribaltarsi.
### ⚠️ Il gate ha segnalato VRP01. Il difetto era MIO.
Prima stesura: perdite contate su **tutte** le barre. VRP01 e' settimanale su griglia giornaliera →
**94.2% di zeri** → "0.96% di perdite, coda sinistra assente" su uno sleeve **in produzione al 12%**.
Sulle barre **ATTIVE** le perdite sono **16.5%**, che e' esattamente la forma attesa di un credit
spread a rischio definito.
Verifica: zeri per sleeve — VRP01 94.2%, SKH01 87.6%, XS01 39.6%, GTAA01 31.9%, TP01 30.7%. Un gate
che conta gli zeri come "non perdite" avrebbe finito per segnalare mezzo book.
Ed e' la parte che vale la pena ricordare: **avevo gia' imparato questa lezione ieri**, cablando la
contabilita' a 3 stati del monitor DVOLSPREAD ("finestra misurata in barre attive, non giorni di
calendario"), e l'ho ripetuta il giorno dopo in un contesto diverso. Ora e' un test.
### Esito dell'applicazione retroattiva — copertura 7/7
| | Sharpe | maxDD | perdite/attive | esito |
|---|---:|---:|---:|---|
| TP01 | 1.29 | 14.3% | 48.7% (1865) | ok |
| XS01 | 1.36 | 10.9% | 46.6% (566) | ok |
| VRP01 | 1.08 | 11.9% | 16.5% (109) | ok |
| SKH01 | 1.45 | 18.1% | 55.6% (333) | ok |
| GTAA01 | 1.01 | 8.9% | 45.2% (1831) | ok |
| XSR01 (monitor) | 1.79 | 2.5% | 47.8% (892) | ok |
| DVOLSPREAD (monitor) | 0.68 | 25.5% | 50.8% (1949) | ok |
Controlli positivi (obbligatori: un gate che non segnala nulla puo' essere semplicemente rotto):
firma CC01 e firma deep-OTM **segnalate** con le motivazioni giuste; rumore a Sharpe 0.62 **non**
segnalato.
**Nota che vale per XSR01**, il cui gate di deploy scade il 23/10: il suo maxDD del 2.5% *non* e'
una coda mancante — il **47.8%** delle barre attive e' in perdita. Il DD basso viene da vol bassa
(2.3%) e dollar-neutrality. Il rischio #1 di XSR01 resta lo slippage non modellato, non un modo di
perdita nascosto.
### Replica indipendente del finding d'ancora del 02/07
Il gate, applicato alla cieca a TP01 sulle 24 ancore orarie, ritrova da solo cio' che il 02/07 era
stato diagnosticato a mano:
> canonica (00:00) **+0.237** = **92° percentile** delle 24 | mediana onesta **+0.056** |
> banda [0.087, +0.247] | fortuna **+0.182**
Il 02/07 riportava mediana 0.04 e banda [0.13, +0.30] con un'implementazione **separata**. Accordo
buono. **Il numero onesto dell'hold-out di TP01 e' ~+0.05, non 0.31.** (Il valore del sleeve resta
quello gia' stabilito e non ancorato: il taglio del drawdown ~6× vs buy&hold.)
---
## Cosa NON e' stato fatto, e perche'
**Il book ricalcolato sul path LIVE.** Sarebbe la sintesi naturale (SKH01 live > modello su entrambi
i lati; TP01 live ≈ modello) e cambierebbe la stima de-luckata del book, che oggi applica un ×0.6
assunto. Non l'ho fatto perche' le due misure vivono su **lenti diverse**: il simulatore di SKH01
compone per-trade a nozionale unitario, lo sleeve di `sleeves.py` e' vol-targeted. Combinarle
produrrebbe un numero che sembra preciso e non lo e'. **Follow-up dichiarato con l'ostacolo nominato:**
serve una versione vol-targeted del path live di SKH01 prima di toccare il fattore di de-luck.
Resta pero' una conseguenza qualitativa gia' solida: il ×0.6 fu scelto assumendo che il live
**degradi**. Su SKH01 e' misurato il contrario, su TP01 e' ~zero. **Il ×0.6 e' probabilmente
conservativo**, e la nota gia' presente in CLAUDE.md ("il ×0.6 sopra il path live rischia di contare
due volte la degradazione SKH01") va letta come confermata, non come cautela ipotetica.
---
## Regole nuove
1. **La parzialita' dell'ultima barra conta in proporzione a quanto il segnale pesa la barra piu'
recente.** Breakout/livelli: massima. Momentum a lookback lungo: ~nulla. Non trasferire la
conclusione fra sleeve.
2. **Ogni statistica di "frazione in perdita" si calcola sulle barre ATTIVE.** Uno sleeve flat la
maggior parte del tempo non ha una coda assente, ha meno osservazioni. (E: una lezione imparata
in un contesto non si applica da sola in un altro — questa era gia' cablata nel monitor
DVOLSPREAD il giorno prima.)
3. **Un gate nuovo si valida su un controllo positivo prima di credere ai suoi "ok".** Un gate rotto
e un gate soddisfatto producono lo stesso output.
4. **Anche un DANNO misurato a un'ancora sola e' fortuna d'ancora.** A offset 0 la barra parziale
sembrava costare 0.23 di hold-out: era il minimo della banda, mediana +0.12.
+142
View File
@@ -782,6 +782,148 @@ def _verdict(per_cell: list[dict]) -> dict:
n_positive_cells=len(ok), n_cells=len(per_cell))
def implausible_sharpe(daily, n_trades: int | None = None, n_losing_trades: int | None = None,
sharpe_max: float = 3.0, min_loss_frac: float = 0.02,
calmar_max: float = 20.0) -> dict:
"""GATE: a Sharpe/track too good to be true means the RISK IS OUTSIDE THE DATASET.
Codifies a blind spot the project hit THREE times and each time diagnosed by hand:
- CC01 cash-and-carry: modelled Sharpe 11-13, maxDD 0.3% — it passed EVERY gate of the
marginal scorer. The premium was real; the model simply had no 2022, no liquidation, no
basis blow-out. Real basis trades run Sharpe ~1-3 with abrupt tails.
- Albimarini deep-OTM cells: 0 losses in 142 trades.
- half of the 288-structure VRP grid: same 0-loss signature.
The point is NOT that a high Sharpe is impossible. It is that a track with no left tail is
evidence about the SAMPLE, not about the strategy: the loss mode exists and did not occur.
A flagged candidate is at best STAT-MODE — never a deploy — until the missing tail is priced.
`n_trades` / `n_losing_trades` enable the RULE OF THREE: observing 0 losses in N trades is
consistent with a true loss rate up to ~3/N. On a payoff that loses many multiples of the
premium (deep-OTM short options) that residual rate is enough to flip expectancy — which is
exactly why "0/142" must not read as "riskless".
Returns dict(implausible: bool, reasons: [...], ...). ANY trigger sets implausible=True.
"""
s = pd.Series(daily)
if not isinstance(s.index, pd.DatetimeIndex):
raise TypeError("implausible_sharpe vuole una serie a indice temporale "
"(altrimenti l'annualizzazione e' arbitraria)")
s = _to_daily(s) # normalizza a giornaliero: _sh annualizza a 365.25
r = np.asarray(s.dropna().values, float)
out = dict(implausible=False, reasons=[], n_obs=int(len(r)))
if len(r) < 30:
out["reasons"].append(f"campione troppo corto per giudicare (n={len(r)})")
return out
sh, dd = _sh(s), _dd_ret(s)
ann = float(np.mean(r) * 365.25)
# ⚠️ la frazione di perdite si misura sulle barre ATTIVE, non su tutte. Uno sleeve che sta
# flat la maggior parte del tempo (VRP01 e' settimanale su griglia giornaliera: 94% di zeri;
# SKH01 e' flat l'88% dei giorni) farebbe scattare "coda sinistra assente" per COSTRUZIONE.
# E' lo stesso errore che il monitor DVOLSPREAD evita contando in barre attive: qui l'avevo
# ripetuto, e il gate segnalava VRP01 (perdite 0.96% su tutte -> 16.5% sulle attive).
act = r[r != 0.0]
loss_frac = float((act < 0).mean()) if len(act) else float("nan")
calmar = ann / dd if dd > 1e-9 else float("inf")
out.update(sharpe=sh, maxdd=dd, ann_return=ann, loss_frac=loss_frac, calmar=calmar,
n_active=int(len(act)), active_frac=float(len(act) / len(r)))
if len(act) < 30:
out["reasons"].append(
f"solo {len(act)} barre ATTIVE: la coda non e' misurabile, non 'assente' "
f"(giudizio sospeso, non promozione)")
out["implausible"] = True
return out
if sh > sharpe_max:
out["reasons"].append(
f"Sharpe {sh:.2f} > {sharpe_max} — nessuna strategia eseguibile lo sostiene su anni; "
f"il candidato piu' probabile e' un rischio non nel campione")
if loss_frac < min_loss_frac:
out["reasons"].append(
f"solo il {loss_frac*100:.2f}% delle {len(act)} barre ATTIVE in perdita "
f"(<{min_loss_frac*100:.0f}%) — coda sinistra assente: e' una proprieta' del "
f"CAMPIONE, non della strategia")
if np.isfinite(calmar) and calmar > calmar_max:
out["reasons"].append(
f"Calmar {calmar:.1f} > {calmar_max} (ret {ann*100:.1f}%/a su maxDD {dd*100:.2f}%) — "
f"il drawdown misurato non contiene il modo di perdita")
elif not np.isfinite(calmar):
out["reasons"].append("maxDD ~0: il modo di perdita non si e' MAI manifestato nel campione")
if n_trades and n_trades > 0 and n_losing_trades is not None and n_losing_trades == 0:
ub = 3.0 / n_trades # regola del tre
out["loss_rate_ub95"] = float(ub)
out["reasons"].append(
f"0 perdite su {n_trades} trade: la regola del tre lascia un tasso di perdita VERO "
f"fino a {ub*100:.2f}% — su un payoff asimmetrico basta a ribaltare l'expectancy")
out["implausible"] = bool(out["reasons"])
return out
def anchor_luck_band(fn_by_offset, offsets, canonical=0, metric=_sh,
min_frac_positive: float = 0.5) -> dict:
"""GATE: quanta parte del titolo e' FORTUNA DELL'ANCORA (candidato dichiarato il 02/07).
Il timing-luck d'ancora e' multiple-testing che il deflated-Sharpe NON conta: la griglia di
ancore non e' una griglia di PARAMETRI, quindi nessuno la dichiara come trial, ma scegliere
(anche implicitamente) l'ancora 00:00 e' un max-of-k. Misurato su 4/4 sleeve ancorati:
TP01 hold-out 0.31 = migliore delle 24; SKH01 minHold +1.26 = 93-98° pctl dei 23 offset;
XS01 fase 0 al 15° pctl di DD; VRP01 e' l'unico SENZA firma (la sua fase e' la PEGGIORE).
`fn_by_offset(off)` -> serie di rendimenti (giornalieri o piu' fini). `metric` -> scalare.
La stima ONESTA e' la **mediana della banda**, non il valore canonico.
gate_pass = la metrica e' positiva alla mediana E in almeno `min_frac_positive` delle ancore
(necessario, non sufficiente: dice che il segno non dipende dall'ancora).
"""
vals = {}
for o in offsets:
try:
vals[o] = float(metric(fn_by_offset(o)))
except Exception: # un'ancora rotta non deve uccidere
continue
if len(vals) < 3:
return dict(gate_pass=False, reason=f"solo {len(vals)} ancore valutabili")
arr = np.array(sorted(vals.values()), float)
can = vals.get(canonical, float("nan"))
pctl = float((arr < can).mean()) if np.isfinite(can) else float("nan")
med = float(np.median(arr))
frac_pos = float((arr > 0).mean())
return dict(gate_pass=bool(med > 0 and frac_pos >= min_frac_positive),
canonical=can, canonical_pctl=pctl, median=med,
lo=float(arr.min()), hi=float(arr.max()),
frac_positive=frac_pos, n_anchors=len(vals),
luck=float(can - med) if np.isfinite(can) else float("nan"),
per_offset=vals)
def anchor_luck_delta(fn_a_by_offset, fn_b_by_offset, offsets, metric=_sh) -> dict:
"""Confronto di DUE varianti sulla banda d'ancora: **mediana delle DIFFERENZE APPAIATE**.
Errore catturato in sessione il 26/07 e qui codificato: confrontare `mediana(A) - mediana(B)`
mette a confronto ancore DIVERSE (la mediana di A e quella di B cadono su offset diversi) e
puo' RIBALTARE il verdetto — successe misurando il recupero on-book di SKH01. Gli offset sono
coppie: la statistica corretta e' la mediana di (A_o - B_o).
Corollario della lezione del 26/07: *se si de-lucka una strategia va de-luckato anche il suo
DEGRADO* — ogni Δ fra due varianti misurato a un'ancora sola eredita la fortuna di quell'ancora.
"""
d = {}
for o in offsets:
try:
d[o] = float(metric(fn_a_by_offset(o))) - float(metric(fn_b_by_offset(o)))
except Exception:
continue
if len(d) < 3:
return dict(gate_pass=False, reason=f"solo {len(d)} ancore appaiate")
arr = np.array(list(d.values()), float)
return dict(gate_pass=bool(np.median(arr) > 0 and (arr > 0).mean() >= 0.5),
median_paired=float(np.median(arr)), mean_paired=float(np.mean(arr)),
lo=float(arr.min()), hi=float(arr.max()),
n_positive=int((arr > 0).sum()), n_anchors=len(d), per_offset=d)
def study_weights(name: str, target_fn, tfs=("1d", "12h"),
assets=CERTIFIED, fee_sweep=FEE_SWEEP) -> dict:
"""Run a CONTINUOUS-position hypothesis on each (asset,tf), report robustness.
+134
View File
@@ -0,0 +1,134 @@
"""I due gate MAI COSTRUITI, ora codificati e applicati RETROATTIVAMENTE (ondata 2026-07-26-bis, T2).
Debito dichiarato dal progetto e mai saldato:
`implausible_sharpe` — raccomandato il 26/06 (CC01: Sharpe modellato 11-13, maxDD 0.3%, passa
OGNI gate del marginal scorer -> "punto cieco") e RI-raccomandato "con piu' forza" il 02/07
(celle Albimarini deep-OTM: 0 perdite su 142 trade; meta' della griglia VRP a 288 strutture con
la stessa firma). Tre occorrenze, tre diagnosi a mano, zero codice.
`anchor_luck_band` — dichiarato "candidato gate futuro" il 02/07, dopo che il timing-luck
d'ancora era stato trovato su 4/4 sleeve ancorati. Motivo per cui serve un gate DEDICATO: la
griglia di ancore non e' una griglia di parametri, quindi **il deflated-Sharpe non la conta**.
Questo script (a) prova che i gate FUNZIONANO su controlli positivi noti, poi (b) li passa su
tutto cio' che oggi e' nel book o in forward-monitor. Un gate che non segnala nulla non e' una
buona notizia finche' non si e' dimostrato che sa segnalare.
"""
from __future__ import annotations
import sys
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 al # noqa: E402
from src.portfolio import sleeves as sl # noqa: E402
def line(t: str) -> None:
print("\n" + "-" * 100 + f"\n {t}\n" + "-" * 100)
def show(name: str, rep: dict) -> None:
flag = "*** IMPLAUSIBILE ***" if rep.get("implausible") else "ok"
if "sharpe" in rep:
print(f" {name:<16} Sh {rep['sharpe']:>6.2f} DD {rep['maxdd']*100:>5.2f}% "
f"perdite/attive {rep['loss_frac']*100:>5.1f}% ({rep['n_active']} att.) Calmar {rep['calmar']:>7.2f} {flag}")
else:
print(f" {name:<16} {rep.get('reasons')} {flag}")
for r in rep.get("reasons", []):
print(f" - {r}")
def main() -> None:
print("=" * 100)
print(" T2 — `implausible_sharpe` e `anchor_luck_band`: codificati e applicati retroattivamente")
print("=" * 100)
# ------------------------------------------------------------------ controlli positivi
line("0. CONTROLLI POSITIVI — il gate DEVE segnalare questi, sennò e' rotto")
idx = pd.date_range("2020-01-01", periods=900, freq="1D", tz="UTC")
rng = np.random.default_rng(726)
# (a) firma CC01: cashflow liscio, quasi nessuna perdita, DD ~0
cc = pd.Series(0.0006 + rng.normal(0, 0.00012, len(idx)), index=idx)
show("CC01-like", al.implausible_sharpe(cc))
# (b) firma Albimarini deep-OTM: 0 perdite su 142 trade
alb = pd.Series(np.abs(rng.normal(0.0008, 0.0003, len(idx))), index=idx)
show("deepOTM-like", al.implausible_sharpe(alb, n_trades=142, n_losing_trades=0))
# (c) controllo NEGATIVO: rumore normale a Sharpe ~1 non deve essere segnalato
ok = pd.Series(rng.normal(0.0006, 0.011, len(idx)), index=idx)
show("rumore Sh~1", al.implausible_sharpe(ok))
# ------------------------------------------------------------------ book + monitor
line("1. `implausible_sharpe` su TUTTO cio' che e' nel book o in forward-monitor")
series: dict[str, pd.Series] = {}
for nm, fn in (("TP01", sl._tp01_returns), ("XS01", sl._xsec_returns),
("VRP01", sl._vrp_combo_returns), ("SKH01", sl._skyhook_returns),
("GTAA01", sl._gtaa_daily_returns)):
try:
series[nm] = fn()
except Exception as e: # uno sleeve rotto non ferma l'audit
print(f" {nm:<16} non valutabile: {type(e).__name__}: {e}")
for nm, s in series.items():
show(nm, al.implausible_sharpe(s))
# candidati in forward-monitor, ricostruiti dai rispettivi script di scoperta
line("2. `implausible_sharpe` sui CANDIDATI in forward-monitor")
try:
import r0725_statarb_basket_gate as xg # XSR01 = versione DEMEANATA
P, S = xg.pair_frames()
show("XSR01", al.implausible_sharpe(xg.basket_from_positions(P, S, demean=True)))
except Exception as e:
print(f" XSR01 non ricostruibile qui: {type(e).__name__}: {e}")
try:
import r0726_dvolspread_gate as dg
# config ONESTA congelata nel monitor (`paper_dvolspread.FROZEN`), non la cella pubblicata
show("DVOLSPREAD", al.implausible_sharpe(
dg.daily_returns(zwin=180, tanh_k=2.0, lw=0.6, zw=1.1, tgt=0.17, svw=60)))
except Exception as e:
print(f" DVOLSPREAD non ricostruibile qui: {type(e).__name__}: {e}")
# ------------------------------------------------------------------ anchor band
line("3. `anchor_luck_band` — ri-verifica del finding 02/07 col gate ORA CODIFICATO")
print(" TP01 sulle 24 ancore orarie (metrica: Sharpe hold-out, il numero che il 02/07 era")
print(" risultato il MIGLIORE delle 24). Se il gate e' giusto deve ritrovarlo da solo.")
import r0726_tp01_partial_day as pd_mod
cache: dict[int, dict] = {}
def model_at(off: int) -> pd.Series:
if off not in cache:
cache[off] = pd_mod.evaluate(off)
s = cache[off]["MODEL"]
return s[s.index >= pd_mod.HOLDOUT]
band = al.anchor_luck_band(model_at, range(24), canonical=0,
metric=lambda s: pd_mod.sharpe(s))
print(f"\n canonica (00:00) {band['canonical']:+.3f} | pctl nella banda "
f"{band['canonical_pctl']*100:.0f}° | mediana {band['median']:+.3f} "
f"| banda [{band['lo']:+.3f},{band['hi']:+.3f}]")
print(f" positiva in {band['frac_positive']*100:.0f}% delle ancore | "
f"fortuna = canonica - mediana = {band['luck']:+.3f} | gate_pass={band['gate_pass']}")
print("\n E il confronto APPAIATO fra due varianti (live con barra parziale vs modello):")
delta = al.anchor_luck_delta(
lambda o: cache.setdefault(o, pd_mod.evaluate(o))["LIVE"],
lambda o: cache.setdefault(o, pd_mod.evaluate(o))["MODEL"],
range(24), metric=lambda s: pd_mod.sharpe(s))
print(f" mediana delle DIFFERENZE appaiate {delta['median_paired']:+.3f} "
f"[{delta['lo']:+.3f},{delta['hi']:+.3f}] positiva in "
f"{delta['n_positive']}/{delta['n_anchors']} gate_pass={delta['gate_pass']}")
print()
if __name__ == "__main__":
main()
+254
View File
@@ -0,0 +1,254 @@
"""TP01 nel LIVE: la barra GIORNALIERA e' PARZIALE. Quanto conta? (ondata 2026-07-26-bis, T1)
CONTESTO. Il 26/07 ho misurato che il live di SKH01 valuta il segnale su una barra 230m parziale
(uscite e ingressi) e che il backtest, modellandolo a chiusura di bin, lo sottostimava su entrambi
i lati. Questo script chiede la STESSA cosa allo sleeve che pesa il **75% del book Deribit**.
IL FATTO (verificato, non dedotto):
`resample_tf(..., label="left", closed="left")` NON scarta il giorno in corso, e
`TrendPortfolio.current_target` prende `target_series(df)[-1]` — cioe' **l'ultima barra, che e'
parziale**. Il feed certificato viene ricostruito da `cron_daily` alle 00:30 UTC, quindi per tutta
la giornata il live vede il giorno corrente come una barra da **1 ora su 24** (misurato oggi:
`barre1h=1/24`).
Il docstring di `current_target` dice "ultima barra CHIUSA": e' un'assunzione sul chiamante, e nel
live NON e' verificata.
DUE CANALI, che vanno separati perche' hanno segno atteso opposto:
(a) ESECUZIONE RITARDATA — il modello ribilancia al confine 00:00, il live al primo giro di cron
utile dopo il rebuild (~01:00). Un'ora di ritardo su uno sleeve daily: atteso ~rumore.
(b) BARRA PARZIALE — il target del live usa c[t] = prezzo delle 01:00 al posto della chiusura
piena del giorno, e r[t] = un rendimento di UN'ORA contato come rendimento GIORNALIERO dentro
la finestra di vol a 30 barre. La vol realizzata esce **sottostimata** -> `target_vol/vol`
esce **sovrastimata** -> il live **sovra-leva** in modo sistematico. Direzione attesa: negativa.
TRE PATH, che isolano i due canali (differenza fra due path = un solo grado di liberta'):
MODEL tgt_full[t-1] tenuto in [t 00:00, t+1 00:00) = il backtest canonico
CLOSED@+1h tgt_full[t-1] tenuto in [t 01:00, t+1 01:00) = scarta la parziale, stessa ora del live
LIVE tgt_live[t] tenuto in [t 01:00, t+1 01:00) = cio' che gira oggi
LIVE - CLOSED = effetto puro della BARRA PARZIALE
CLOSED - MODEL = effetto puro del RITARDO d'esecuzione
BANDA D'ANCORA. TP01 e' gia' noto per anchor timing-luck (02/07: l'ancora 00:00 e' la migliore
delle 24). Lezione codificata stamattina: *se si de-lucka una strategia va de-luckato anche il suo
degrado*. Quindi ogni Δ e' riportato sulle 24 ancore, con la **mediana delle DIFFERENZE APPAIATE**
(non la differenza delle mediane: gli offset sono coppie).
Tutto su griglia ORARIA: i tre path hanno confini diversi e solo l'orario li rende confrontabili.
"""
from __future__ import annotations
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.data.downloader import load_data # noqa: E402
from src.strategies.trend_portfolio import CANONICAL, TrendPortfolio # noqa: E402
ASSETS = ("BTC", "ETH")
HOLDOUT = "2025-01-01"
MS_H = 3_600_000
MS_D = 86_400_000
FEE_SIDE = float(CANONICAL.get("fee_side", 0.0005))
EXEC_LAG_H = 1 # il primo giro di cron_book utile dopo il rebuild delle 00:30
# --------------------------------------------------------------------------- dati
def hourly(asset: str) -> tuple[np.ndarray, np.ndarray]:
df = load_data(asset, "1h")
return (df["timestamp"].values.astype(np.int64),
df["close"].values.astype(float))
def daily_from_hourly(ts: np.ndarray, c: np.ndarray, off_h: int) -> dict:
"""Barre giornaliere ancorate a `off_h` (UTC), piu' il prezzo alla PRIMA ora del giorno.
Ritorna: day_start (ms), close pieno, close parziale (dopo 1 ora), n. di barre 1h per giorno.
"""
day = (ts - off_h * MS_H) // MS_D
s = pd.DataFrame({"day": day, "ts": ts, "c": c})
g = s.groupby("day", sort=True)
full = g["c"].last().values
first = g["c"].first().values # close della PRIMA barra 1h -> la "parziale" a +1h
n = g["c"].size().values
start = (g["ts"].min().values // MS_H) * MS_H
return dict(start=start, full=full, partial=first, n=n)
# --------------------------------------------------------------------------- target
def targets_full(cf: np.ndarray, tp: TrendPortfolio) -> np.ndarray:
"""target_series su barre giornaliere PIENE (bpd=1 -> orizzonti in giorni)."""
d = pd.DataFrame({"close": cf,
"datetime": pd.to_datetime(np.arange(len(cf)) * MS_D, unit="ms", utc=True)})
return tp.target_series(d)
def targets_live(cf: np.ndarray, cp: np.ndarray, tp: TrendPortfolio) -> np.ndarray:
"""Ricostruisce cio' che il live calcola: giorni 0..t-1 PIENI + giorno t PARZIALE, indice [-1].
Riproduce a mano la formula di `target_series` sostituendo il SOLO ultimo elemento — che e'
esattamente cio' che cambia fra la vista del live e quella del backtest.
"""
n = len(cf)
vw = tp.vol_win_days # bpd=1 su barre giornaliere
hz = tuple(tp.horizons_days)
bpy = 365.25
r_full = np.zeros(n)
r_full[1:] = cf[1:] / cf[:-1] - 1.0
out = np.zeros(n)
for t in range(n):
# direzione: sign(c[t]/c[t-h]) con c[t] = PARZIALE
acc = cnt = 0.0
for h in hz:
if t - h >= 0:
acc += np.sign(cp[t] / cf[t - h] - 1.0)
cnt += 1
if cnt == 0:
continue
direction = acc / cnt
if tp.long_only:
direction = max(direction, 0.0)
# vol: finestra a vw barre che finisce in t, con r[t] = rendimento di UN'ORA
a = max(0, t - vw + 1)
w = r_full[a:t + 1].copy()
if t >= 1:
w[-1] = cp[t] / cf[t - 1] - 1.0
if len(w) < max(1, vw // 2):
continue
sd = float(np.std(w, ddof=1)) if len(w) > 1 else 0.0
vol = sd * np.sqrt(bpy)
if not np.isfinite(vol) or vol <= 0:
continue
out[t] = float(np.clip(direction * (tp.target_vol / vol), -tp.leverage, tp.leverage))
return out
# --------------------------------------------------------------------------- path orari
def hourly_position(ts: np.ndarray, dstart: np.ndarray, tgt: np.ndarray,
shift_h: int) -> np.ndarray:
"""Posizione oraria: `tgt[k]` in vigore da (dstart[k] + shift_h ore) fino al successivo."""
edges = dstart + shift_h * MS_H
j = np.searchsorted(edges, ts, side="right") - 1
pos = np.where(j >= 0, tgt[np.clip(j, 0, len(tgt) - 1)], 0.0)
return pos
def path_returns(ts: np.ndarray, c: np.ndarray, pos: np.ndarray) -> pd.Series:
"""Rendimenti orari netti fee sul turnover, indicizzati per timestamp."""
r = np.zeros(len(c))
r[1:] = c[1:] / c[:-1] - 1.0
held = np.zeros(len(pos))
held[1:] = pos[:-1] # tenuta durante l'ora i = decisa a i-1
turn = np.abs(np.diff(held, prepend=0.0))
net = held * r - FEE_SIDE * turn
return pd.Series(net, index=pd.to_datetime(ts, unit="ms", utc=True))
def sharpe(s: pd.Series) -> float:
x = s.values[np.isfinite(s.values)]
if len(x) < 50 or np.std(x) == 0:
return 0.0
return float(np.mean(x) / np.std(x) * np.sqrt(24 * 365.25))
def maxdd(s: pd.Series) -> float:
eq = np.cumprod(1.0 + np.clip(s.values, -0.99, None))
return float(np.max((np.maximum.accumulate(eq) - eq) / np.maximum.accumulate(eq)))
# --------------------------------------------------------------------------- valutazione
def evaluate(off_h: int) -> dict:
"""I tre path per una data ancora, combinati 50/50 BTC+ETH come fa lo sleeve."""
tp = TrendPortfolio(**CANONICAL)
legs: dict[str, list] = {"MODEL": [], "CLOSED": [], "LIVE": []}
lev: dict[str, list] = {"MODEL": [], "LIVE": []}
for a in ASSETS:
ts, c = hourly(a)
d = daily_from_hourly(ts, c, off_h)
cf, cp, dstart = d["full"], d["partial"], d["start"]
tf = targets_full(cf, tp)
tl = targets_live(cf, cp, tp)
# MODEL: tgt[t-1] tenuto dal confine del giorno t -> shift di 1 giorno = tgt spostato avanti
tf_prev = np.concatenate([[0.0], tf[:-1]])
legs["MODEL"].append(path_returns(ts, c, hourly_position(ts, dstart, tf_prev, 0)))
legs["CLOSED"].append(path_returns(ts, c, hourly_position(ts, dstart, tf_prev, EXEC_LAG_H)))
legs["LIVE"].append(path_returns(ts, c, hourly_position(ts, dstart, tl, EXEC_LAG_H)))
lev["MODEL"].append(tf_prev)
lev["LIVE"].append(tl)
out = {}
for k, v in legs.items():
J = pd.concat(v, axis=1, join="inner").fillna(0.0)
out[k] = 0.5 * J.iloc[:, 0] + 0.5 * J.iloc[:, 1]
# diagnostica di leva: quanto sovra-leva il live rispetto al modello?
ml = np.concatenate(lev["MODEL"]); ll = np.concatenate(lev["LIVE"])
m = (ml > 0) & (ll > 0)
out["_lev_ratio"] = float(np.median(ll[m] / ml[m])) if m.any() else float("nan")
return out
def report(off_h: int, res: dict) -> dict:
row = dict(off=off_h, lev=res["_lev_ratio"])
for k in ("MODEL", "CLOSED", "LIVE"):
s = res[k]
row[f"{k}_full"] = sharpe(s)
row[f"{k}_hold"] = sharpe(s[s.index >= HOLDOUT])
row[f"{k}_dd"] = maxdd(s)
return row
def main() -> None:
print("=" * 100)
print(" TP01 nel LIVE — la barra giornaliera e' PARZIALE (1h su 24). Quanto conta?")
print("=" * 100)
print("\n MODEL = tgt[t-1] dal confine 00:00 (backtest canonico)")
print(" CLOSED@+1h = tgt[t-1] dal confine +1h (scarta la parziale, ora del live)")
print(" LIVE = tgt[t] da barra PARZIALE, +1h (cio' che gira oggi)")
print(" LIVE-CLOSED = effetto BARRA PARZIALE | CLOSED-MODEL = effetto RITARDO\n")
rows = [report(o, evaluate(o)) for o in range(24)]
df = pd.DataFrame(rows)
print("-" * 100)
print(" ancora canonica (offset 0) — quella su cui sono calcolati TUTTI i numeri di TP01")
print("-" * 100)
r0 = df[df.off == 0].iloc[0]
print(f" {'path':<12}{'FULL':>8}{'HOLD':>8}{'maxDD':>9}")
for k in ("MODEL", "CLOSED", "LIVE"):
print(f" {k:<12}{r0[f'{k}_full']:>8.2f}{r0[f'{k}_hold']:>8.2f}{r0[f'{k}_dd']*100:>8.1f}%")
print(f"\n leva del LIVE / leva del MODEL (mediana sui giorni entrambi investiti): "
f"{r0['lev']:.3f}")
print("\n" + "-" * 100)
print(" BANDA DELLE 24 ANCORE — mediana delle DIFFERENZE APPAIATE (non differenza di mediane)")
print("-" * 100)
for lab, a, b in (("BARRA PARZIALE (LIVE - CLOSED)", "LIVE", "CLOSED"),
("RITARDO 1h (CLOSED - MODEL)", "CLOSED", "MODEL"),
("TOTALE (LIVE - MODEL)", "LIVE", "MODEL")):
for w in ("full", "hold"):
d = df[f"{a}_{w}"] - df[f"{b}_{w}"]
print(f" {lab:<34} {w.upper():<5} mediana {d.median():+.3f} "
f"[{d.min():+.3f},{d.max():+.3f}] positivo in {int((d > 0).sum())}/24")
print(f"\n leva LIVE/MODEL sulla banda: mediana {df['lev'].median():.3f} "
f"[{df['lev'].min():.3f},{df['lev'].max():.3f}]")
print("\n" + "-" * 100)
print(" per-ancora (FULL)")
print("-" * 100)
print(f" {'off':>4}{'MODEL':>8}{'CLOSED':>8}{'LIVE':>8}{'ΔparzialeF':>12}{'ΔparzialeH':>12}{'lev':>7}")
for _, r in df.iterrows():
print(f" {int(r.off):>4}{r.MODEL_full:>8.2f}{r.CLOSED_full:>8.2f}{r.LIVE_full:>8.2f}"
f"{r.LIVE_full - r.CLOSED_full:>+12.3f}{r.LIVE_hold - r.CLOSED_hold:>+12.3f}"
f"{r.lev:>7.3f}")
print()
if __name__ == "__main__":
main()
+21 -2
View File
@@ -115,7 +115,23 @@ class TrendPortfolio:
return tgt
def current_target(self, df: pd.DataFrame) -> float:
"""Posizione-bersaglio decisa all'ultima barra CHIUSA (per il paper/live)."""
"""Posizione-bersaglio all'ULTIMA BARRA DEL df — che nel live e' PARZIALE (per paper/live).
⚠️ Il docstring precedente diceva "ultima barra CHIUSA": e' un'assunzione sul chiamante e
nel live **non e' verificata**. `resample_tf` non scarta la barra in corso e il feed
certificato viene ricostruito alle 00:30 UTC, quindi per tutta la giornata il book vede il
giorno corrente come una barra da **1 ora su 24** (verificato 26/07: `barre1h=1/24`).
MISURATO (`scripts/research/r0726_tp01_partial_day.py`, 24 ancore, differenze appaiate):
l'effetto e' **trascurabile** — barra parziale ΔSharpe FULL 0.031 / HOLD +0.118, ritardo
d'esecuzione di 1h ~0 (11/24 = moneta), leva gonfiata **+0.4%**. Nessun cambio richiesto.
Il perche' e' la regola generale: la parzialita' dell'ultima barra conta in proporzione a
**quanto il segnale pesa la barra piu' recente**. TP01 e' un TSMOM 30/90/180 giorni: l'ora
mancante e' 1/24 di UNA osservazione su 30-180. SKH01 e' un Donchian breakout su barra
230m, dove la barra corrente E' il segnale: li' lo stesso difetto vale +0.38 di Sharpe
(misura del 26/07). Non si trasferisce una conclusione fra i due.
"""
return float(self.target_series(df)[-1])
def net_returns(self, df: pd.DataFrame) -> tuple[np.ndarray, pd.Series]:
@@ -179,7 +195,10 @@ def _bars_per_year(idx: pd.DatetimeIndex) -> float:
def resample_tf(df_1h: pd.DataFrame, rule: str) -> pd.DataFrame:
"""Resample 1h -> rule (confini 00:00 UTC). Schema con 'datetime'.
NB: usare SOLO per-singolo-TF (qui leak-free); MAI ffill/combine mixed-TF su questi
timestamp open-labeled (label='left') -> look-ahead. Deploy a >=12h (vedi docstring modulo)."""
timestamp open-labeled (label='left') -> look-ahead. Deploy a >=12h (vedi docstring modulo).
⚠️ NON scarta la barra IN CORSO: l'ultima riga puo' essere parziale. E' irrilevante nel
backtest (l'ultima barra e' l'ultima) ma NON nel live, dove `current_target` legge proprio
quella — vedi il suo docstring per la misura dell'effetto."""
g = df_1h.copy()
idx = pd.to_datetime(g["timestamp"], unit="ms", utc=True)
idx.name = "dt"
+172
View File
@@ -0,0 +1,172 @@
"""Test dei due gate codificati il 2026-07-26-bis: `implausible_sharpe` e `anchor_luck_band`.
Entrambi erano DEBITO DICHIARATO del progetto (raccomandati piu' volte, mai scritti). Un gate e'
utile solo se ha **potenza** (segnala cio' che deve) E **specificita'** (non segnala il resto):
un gate che non scatta mai passerebbe inosservato per mesi sembrando una buona notizia. Quindi
qui si testano sempre le due facce.
Il caso piu' importante e' `test_sleeve_flat_la_maggior_parte_del_tempo_non_e_implausibile`:
la prima stesura del gate contava le perdite su TUTTE le barre e segnalava VRP01 (settimanale su
griglia giornaliera, 94% di zeri) un falso positivo su uno sleeve IN PRODUZIONE.
"""
from __future__ import annotations
import sys
from pathlib import Path
import numpy as np
import pytest
ROOT = Path(__file__).resolve().parents[1]
for p in (ROOT, ROOT / "scripts" / "research" / "alt"):
sys.path.insert(0, str(p))
pytest.importorskip("pandas")
import pandas as pd # noqa: E402
import altlib as al # noqa: E402
def _idx(n):
return pd.date_range("2020-01-01", periods=n, freq="1D", tz="UTC")
def _noise(n=900, mu=0.0006, sd=0.011, seed=1):
rng = np.random.default_rng(seed)
return pd.Series(rng.normal(mu, sd, n), index=_idx(n))
# --------------------------------------------------------------- implausible_sharpe: POTENZA
def test_firma_cc01_viene_segnalata():
"""CC01 (Sharpe modellato 11-13, maxDD 0.3%) passava OGNI gate del marginal scorer: e' il
punto cieco che questo gate esiste per coprire."""
rng = np.random.default_rng(0)
s = pd.Series(0.0006 + rng.normal(0, 0.00012, 900), index=_idx(900))
r = al.implausible_sharpe(s)
assert r["implausible"]
assert any("Sharpe" in x for x in r["reasons"])
def test_zero_perdite_su_molti_trade_attiva_la_regola_del_tre():
"""0/142 non significa 'senza rischio': lascia un tasso vero fino a ~3/142 = 2.1%, e su un
payoff deep-OTM quel residuo ribalta l'expectancy."""
s = pd.Series(np.abs(_noise(900, 0.0008, 0.0003, seed=2).values), index=_idx(900))
r = al.implausible_sharpe(s, n_trades=142, n_losing_trades=0)
assert r["implausible"]
assert r["loss_rate_ub95"] == pytest.approx(3.0 / 142)
assert any("regola del tre" in x for x in r["reasons"])
def test_calmar_assurdo_viene_segnalato_anche_a_sharpe_moderato():
"""Il modo di perdita puo' mancare senza che lo Sharpe esploda: un DD ~0 basta da solo."""
v = np.full(900, 0.0004)
v[::120] = -0.00005 # perdite presenti ma minuscole -> DD ~0
r = al.implausible_sharpe(pd.Series(v, index=_idx(900)))
assert r["implausible"]
assert any("Calmar" in x or "maxDD" in x for x in r["reasons"])
# --------------------------------------------------------------- implausible_sharpe: SPECIFICITA'
def test_rumore_a_sharpe_uno_non_viene_segnalato():
"""Specificita': senza questo, un gate sempre-acceso sarebbe inutile quanto uno sempre-spento."""
assert not al.implausible_sharpe(_noise())["implausible"]
def test_sleeve_flat_la_maggior_parte_del_tempo_non_e_implausibile():
"""IL falso positivo della prima stesura. VRP01 e' settimanale su griglia giornaliera: 94% di
zeri. Contando le perdite su TUTTE le barre esce 0.96% ('coda assente') invece del 16.5% reale
sulle barre ATTIVE -> il gate segnalava uno sleeve in PRODUZIONE."""
rng = np.random.default_rng(3)
v = np.zeros(900)
act = np.arange(0, 900, 7) # una barra attiva a settimana
v[act] = np.where(rng.random(len(act)) < 0.17,
-rng.uniform(0.02, 0.05, len(act)), # ~17% di settimane in perdita
rng.uniform(0.004, 0.012, len(act)))
r = al.implausible_sharpe(pd.Series(v, index=_idx(900)))
assert r["n_active"] == len(act)
assert 0.05 < r["loss_frac"] < 0.35, r["loss_frac"]
assert not r["implausible"], r["reasons"]
def test_poche_barre_attive_sospende_il_giudizio_invece_di_promuovere():
"""Con pochissime osservazioni attive la coda non e' 'assente', e' NON MISURABILE: il gate
deve bloccare, non assolvere."""
v = np.zeros(900)
v[::100] = 0.01
r = al.implausible_sharpe(pd.Series(v, index=_idx(900)))
assert r["implausible"]
assert any("non e' misurabile" in x for x in r["reasons"])
def test_indice_non_temporale_e_un_errore_esplicito():
"""Senza indice temporale l'annualizzazione sarebbe arbitraria: meglio alzare che indovinare."""
with pytest.raises(TypeError):
al.implausible_sharpe(pd.Series(np.zeros(100) + 0.001))
# --------------------------------------------------------------- anchor_luck_band
def test_banda_dancora_riconosce_il_massimo_come_fortuna():
"""Se l'ancora canonica e' la MIGLIORE della griglia, il gate deve dirlo: pctl alto e
stima onesta = mediana, non il valore canonico."""
vals = {o: 0.05 * o for o in range(10)} # 9 = il migliore
b = al.anchor_luck_band(lambda o: vals[o], range(10), canonical=9, metric=lambda x: x)
assert b["canonical_pctl"] >= 0.85
assert b["median"] == pytest.approx(0.225)
assert b["luck"] > 0
def test_banda_dancora_non_penalizza_chi_e_al_centro():
"""VRP01 e' l'unico sleeve la cui ancora canonica NON e' fortunata: il gate non deve
inventare una penalita' dove non c'e'."""
vals = {o: 1.0 + 0.01 * o for o in range(9)}
b = al.anchor_luck_band(lambda o: vals[o], range(9), canonical=4, metric=lambda x: x)
assert 0.3 <= b["canonical_pctl"] <= 0.7
assert abs(b["luck"]) < 1e-9
assert b["gate_pass"]
def test_segno_che_dipende_dallancora_non_passa():
"""Il senso del gate: se la meta' delle ancore e' negativa, il titolo e' una scelta d'ancora."""
vals = {o: (1.0 if o % 2 else -1.0) for o in range(10)}
b = al.anchor_luck_band(lambda o: vals[o], range(10), canonical=1, metric=lambda x: x)
assert not b["gate_pass"]
def test_ancore_rotte_non_uccidono_la_banda():
"""Un'ancora che solleva (dati mancanti a quell'offset) va saltata, non propagata."""
def f(o):
if o == 3:
raise ValueError("feed mancante")
return 1.0
b = al.anchor_luck_band(f, range(8), canonical=0, metric=lambda x: x)
assert b["n_anchors"] == 7 and b["gate_pass"]
# --------------------------------------------------------------- anchor_luck_delta
def test_delta_appaiato_ribalta_la_differenza_di_mediane():
"""IL motivo per cui questa funzione esiste (errore commesso il 26/07 sul recupero on-book
di SKH01): le mediane di A e B cadono su offset DIVERSI, quindi la loro differenza confronta
ancore diverse e puo' RIBALTARE il verdetto.
NB: la costruzione non e' banale — se A battesse B a OGNI offset le mediane non potrebbero
invertirsi (la mediana e' monotona). Il caso reale, e quello qui: A vince nella MAGGIORANZA
degli offset (4/5) ma la sua mediana cade sotto quella di B."""
A = {0: 1.0, 1: 1.0, 2: 3.0, 3: 5.0, 4: 6.0} # mediana 3
B = {0: 0.0, 1: 0.0, 2: 4.0, 3: 4.5, 4: 5.0} # mediana 4
naive = float(np.median(list(A.values())) - np.median(list(B.values())))
d = al.anchor_luck_delta(lambda o: A[o], lambda o: B[o], range(5), metric=lambda x: x)
assert naive < 0, "il test non direbbe nulla se la statistica ingenua non sbagliasse"
assert d["median_paired"] > 0
assert d["n_positive"] == 4 and d["gate_pass"]
def test_delta_appaiato_non_promuove_una_vittoria_di_una_sola_ancora():
"""Un vantaggio concentrato su un'ancora sola non e' un vantaggio: e' la fortuna di quell'ancora."""
A = {0: 5.0, 1: 0.9, 2: 0.8, 3: 0.7}
B = {0: 1.0, 1: 1.0, 2: 1.0, 3: 1.0}
d = al.anchor_luck_delta(lambda o: A[o], lambda o: B[o], range(4), metric=lambda x: x)
assert d["median_paired"] < 0 and not d["gate_pass"]