research: ondata 26/07-bis — TP01 su barra parziale + i 2 gate mai costruiti
0 sleeve nuovi. Book, pesi, cron, config INVARIATI.
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, quindi per
tutta la giornata il book vede oggi come 1 barra oraria su 24 (verificato).
Il docstring "ultima barra CHIUSA" era falso: corretto.
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 1h ΔFULL -0.004 (pos 11/24 = moneta)
leva LIVE/MODEL 1.004
-> trascurabile, nessun cambio al live.
All'ancora canonica sembra peggio del vero: a offset 0 la parziale costa -0.230
di hold-out, che e' il MINIMO della banda (mediana +0.118). Speculare alla
lezione del 26/07: li' l'ancora canonica nascondeva un vantaggio, qui inventa
un danno.
REGOLA: la parzialita' dell'ultima barra conta in proporzione a quanto il
segnale pesa la barra piu' recente. Donchian breakout su 230m (la barra corrente
E' il segnale) -> +0.38; TSMOM 30/90/180g -> ±0.03. Non si trasferisce.
T2 — implausible_sharpe e anchor_luck_band codificati in altlib (debito
raccomandato 3 volte e mai scritto), piu' anchor_luck_delta che codifica
l'errore di stamattina (mediana delle differenze appaiate, non differenza
delle mediane).
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. La lezione era gia' cablata il
giorno prima nel monitor DVOLSPREAD ("barre attive, non giorni di calendario").
Applicazione retroattiva 7/7 tutti ok, con controlli positivi obbligatori
superati (firme CC01 e deep-OTM segnalate, rumore Sh 0.62 no).
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,
fortuna +0.182 — contro mediana 0.04 misurata il 02/07 con implementazione
separata. L'hold-out onesto di TP01 e' ~+0.05, non 0.31.
NON fatto: il book ricalcolato sul path live, bloccato da incompatibilita' di
lenti (simulatore per-trade vs sleeve vol-targeted). Follow-up dichiarato.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
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
|
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.
|
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
|
- **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
|
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),
|
`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.
|
||||||
@@ -782,6 +782,148 @@ def _verdict(per_cell: list[dict]) -> dict:
|
|||||||
n_positive_cells=len(ok), n_cells=len(per_cell))
|
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"),
|
def study_weights(name: str, target_fn, tfs=("1d", "12h"),
|
||||||
assets=CERTIFIED, fee_sweep=FEE_SWEEP) -> dict:
|
assets=CERTIFIED, fee_sweep=FEE_SWEEP) -> dict:
|
||||||
"""Run a CONTINUOUS-position hypothesis on each (asset,tf), report robustness.
|
"""Run a CONTINUOUS-position hypothesis on each (asset,tf), report robustness.
|
||||||
|
|||||||
@@ -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()
|
||||||
@@ -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()
|
||||||
@@ -115,7 +115,23 @@ class TrendPortfolio:
|
|||||||
return tgt
|
return tgt
|
||||||
|
|
||||||
def current_target(self, df: pd.DataFrame) -> float:
|
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])
|
return float(self.target_series(df)[-1])
|
||||||
|
|
||||||
def net_returns(self, df: pd.DataFrame) -> tuple[np.ndarray, pd.Series]:
|
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:
|
def resample_tf(df_1h: pd.DataFrame, rule: str) -> pd.DataFrame:
|
||||||
"""Resample 1h -> rule (confini 00:00 UTC). Schema con 'datetime'.
|
"""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
|
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()
|
g = df_1h.copy()
|
||||||
idx = pd.to_datetime(g["timestamp"], unit="ms", utc=True)
|
idx = pd.to_datetime(g["timestamp"], unit="ms", utc=True)
|
||||||
idx.name = "dt"
|
idx.name = "dt"
|
||||||
|
|||||||
@@ -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"]
|
||||||
Reference in New Issue
Block a user