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
|
||||
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.
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
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"
|
||||
|
||||
@@ -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