research(vrp): buco del tenore chiuso col gate onesto — earns_slot_honest = False
Book, pesi, cron, config INVARIATI. La griglia strutture del 03/07 si fermava a 10 giorni. Famiglia dichiarata nel docstring PRIMA di guardare i numeri: 8 tenori (5-35g) x 3 delta corti x 3 lunghi = 72 celle, perche' riaprire il tenore riapre la struttura e i trial si contano al rialzo. study_family_honest e' cablato sui candidati direzionali (factory -> target_fn via candidate_daily) e VRP01 non lo e': usati i suoi tre componenti reali — selezione in-sample-only, altlib.deflated_sharpe, altlib.marginal_vs_tp01 — importati e non riscritti (c'e' un test d'identita'). ESITO. Cella scelta al buio 10g -0.28/-0.05 (Sh IS 1.75 / FULL 1.55 / HOLD 1.01) contro il canonico 7g (1.50/1.32/0.82; rango 17/72 in-sample). Batte il canonico ma DSR 0.948 < 0.95 FAIL. marginal_vs_tp01 = ADDS per entrambe: il verdetto non e' 'VRP01 e' rotto', e' che il vantaggio non sopravvive al conto dei trial. IL BUCO SI CHIUDE SUL CONTENUTO, non sul gate: la regione mai esplorata PERDE. Miglior cella >10g = 18g, rango 8/72, e solo 3/10 della top-10 sta oltre i 10 giorni -> lo studio conferma il 03/07 invece di ribaltarlo. IL VINCITORE STA DOVE IL MODELLO SBAGLIA DI PIU': compra l'ala piu' lontana (delta lungo -0.05), come 5/10 della top-10, cioe' la gamba che il 30/07 ha misurato sottoprezzata ~2.3x. Sospetto motivato, non dimostrazione: il mediano non separa -0.10 da -0.05, si separa la coda alta. Ma f non e' misurato fuori dalla struttura canonica, e la sensibilita' a f uniforme e' la lente sbagliata per una struttura il cui errore e' concentrato in una gamba sola. ROBUSTEZZA DEL VERDETTO, PUBBLICATA: DSR 0.983 PASS a N=8, 0.948 FAIL a N=72, 0.909 a N=360. Il verdetto si ribalta col conteggio. La griglia era dichiarata in anticipo e il conto fatto al rialzo, ma fallisce per 0.002: si cita come tale, non come refutazione netta. Congelato in un test. Seguito giusto = una MISURA, non un altro backtest: quanto vale f sul 10g/-0.05 sulle quote vere, ora che la catena la raccogliamo noi ogni ora. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -194,6 +194,40 @@ Prima ondata di ricerca onesta su BTC/ETH certificati (5 track, harness condivis
|
|||||||
`fee_frac` **NON cambiato**: il forfait e' conservativo e questo progetto tiene i numeri di
|
`fee_frac` **NON cambiato**: il forfait e' conservativo e questo progetto tiene i numeri di
|
||||||
ammissione conservativi. **Rientro immediato** dopo l'uscita: Sh 1.19 < 1.32 canonico; il suo
|
ammissione conservativi. **Rientro immediato** dopo l'uscita: Sh 1.19 < 1.32 canonico; il suo
|
||||||
ShHOLD 2.42 non e' selezionabile in-sample → **non e' un lead**.
|
ShHOLD 2.42 non e' selezionabile in-sample → **non e' un lead**.
|
||||||
|
✅ **BUCO DEL TENORE CHIUSO col gate onesto (2026-07-30, 4° filone) — nessun cambio.**
|
||||||
|
`scripts/research/r0730_vrp_tenor_gate.py`, test `tests/test_vrp_tenor_gate.py` (12), diario
|
||||||
|
`2026-07-30-vrp-tenor-gate.md`. La griglia 03/07 si fermava a **10g**; qui la famiglia e' stata
|
||||||
|
**dichiarata prima di guardare**: 8 tenori (5-35g) x 3 delta corti x 3 lunghi = **72 celle**
|
||||||
|
(riaprire il tenore riapre la struttura → i trial si contano al RIALZO). ⚠️ `study_family_honest`
|
||||||
|
e' cablato sui candidati DIREZIONALI (`factory -> target_fn` via `candidate_daily`), quindi sono
|
||||||
|
stati usati i suoi **tre componenti reali**: selezione in-sample-only → `altlib.deflated_sharpe`
|
||||||
|
→ `altlib.marginal_vs_tp01` (importati, non riscritti; c'e' un test d'identita').
|
||||||
|
**Esito: `earns_slot_honest = False`.** Cella scelta al buio **10g −0.28/−0.05** (Sh IS 1.75 /
|
||||||
|
FULL 1.55 / HOLD 1.01 / DD 9.6%) contro il canonico 7g (1.50/1.32/0.82/10.5%; rango **17/72**
|
||||||
|
in-sample, 26/72 full) → batte il canonico, ma **DSR 0.948 < 0.95 FAIL** (il canonico stesso fa
|
||||||
|
0.885 dentro questa famiglia). `marginal_vs_tp01` = **ADDS** per entrambe → il verdetto non e'
|
||||||
|
"VRP01 e' rotto", e' che il vantaggio non sopravvive al conto dei trial.
|
||||||
|
📌 **Il buco si chiude meglio sul contenuto che sul gate: la regione MAI esplorata PERDE.**
|
||||||
|
Miglior cella >10g = 18g −0.28/−0.05, **rango 8/72**, e solo **3/10** della top-10 stanno oltre i
|
||||||
|
10 giorni → lo studio **conferma** il 03/07 invece di ribaltarlo.
|
||||||
|
⚠️ **Il vincitore sta dove il modello sbaglia di piu':** compra l'ala piu' lontana (δ lungo
|
||||||
|
−0.05), come 5/10 della top-10, cioe' esattamente la gamba che il 30/07 ha MISURATO
|
||||||
|
sottoprezzata (~2.3x, IV−DVOL +7.5pp). Sospetto motivato, **non dimostrazione**: il mediano non
|
||||||
|
separa −0.10 da −0.05 (1.37 vs 1.37), si separa la coda alta. Ma `f` **non e' misurato** fuori
|
||||||
|
dalla struttura canonica, e la sensibilita' a f uniforme (cella scelta 0.84 vs canonico 0.47 a
|
||||||
|
f=0.73) **e' la lente sbagliata** per una struttura il cui errore e' concentrato in una gamba.
|
||||||
|
⚠️ **Robustezza del verdetto, pubblicata:** DSR **0.983 PASS a N=8** / **0.948 FAIL a N=72** /
|
||||||
|
0.909 a N=360 → **il verdetto si ribalta col conteggio**. La griglia era dichiarata in anticipo e
|
||||||
|
il conto fatto al rialzo, ma **fallisce per 0.002**: si cita come tale, non come refutazione
|
||||||
|
netta. Congelato in `test_il_verdetto_dipende_dal_numero_di_trial_dichiarati`.
|
||||||
|
**Seguito giusto = una MISURA, non un altro backtest:** quanto vale f sul 10g/−0.05 sulle quote
|
||||||
|
vere (la catena ora la raccogliamo noi ogni ora; oggi 15 osservazioni sul solo canonico).
|
||||||
|
**REGOLE:** (a) quando si riapre un parametro si riapre la sua **famiglia**, e il deflated-Sharpe
|
||||||
|
e' l'unico posto dove barare e' indolore e invisibile; (b) dichiarare la griglia **prima** e
|
||||||
|
pubblicare la **sensibilita' del verdetto al conteggio**; (c) un buco si chiude meglio mostrando
|
||||||
|
che dentro **non c'e' niente** che bocciando il candidato; (d) se la selezione converge dove un
|
||||||
|
difetto di modello e' gia' misurato, **il sospetto vale piu' del numero** — e si dice che e' un
|
||||||
|
sospetto.
|
||||||
💰 **A $3.000 la domanda non e' il rendimento** (`min_trade_amount` letti dal venue il 30/07):
|
💰 **A $3.000 la domanda non e' il rendimento** (`min_trade_amount` letti dal venue il 30/07):
|
||||||
**BTC min 0.1 = $6.210 di collaterale/lotto → FUORI**; ETH min 1 contratto = $1.832 → **1 lotto**.
|
**BTC min 0.1 = $6.210 di collaterale/lotto → FUORI**; ETH min 1 contratto = $1.832 → **1 lotto**.
|
||||||
Il sleeve 50/50 **diventa ETH-only**, e **al peso di book (12% = $360) sono 0 lotti** — servirebbe
|
Il sleeve 50/50 **diventa ETH-only**, e **al peso di book (12% = $360) sono 0 lotti** — servirebbe
|
||||||
|
|||||||
@@ -0,0 +1,124 @@
|
|||||||
|
# 2026-07-30 (4º filone) — il buco del tenore chiuso col gate onesto: nessun cambio a VRP01
|
||||||
|
|
||||||
|
**Richiesta dell'operatore:** *«chiudi il buco del tenore con study_family_honest».*
|
||||||
|
**Esito: `earns_slot_honest = False`. Book, pesi, cron, config INVARIATI.**
|
||||||
|
|
||||||
|
Script `scripts/research/r0730_vrp_tenor_gate.py`, test `tests/test_vrp_tenor_gate.py` (12 casi).
|
||||||
|
|
||||||
|
## Il buco, e perché era reale
|
||||||
|
|
||||||
|
La griglia strutture del 03/07 (`r0703_vrpimp_structgrid`) cercava su `TENOR_GRID = (3, 5, 7, 10)`:
|
||||||
|
**oltre i 10 giorni non è mai stato guardato.** Lo sweep del 30/07 sul profit-take aveva toccato
|
||||||
|
14/18/21/28g di passaggio e mostrato che il 18g *sembrava* battere il 7g in hold-to-expiry
|
||||||
|
(1.51 vs 1.32) — sei celle scelte sul campione pieno, cioè esattamente la selezione che questo
|
||||||
|
gate esiste per uccidere.
|
||||||
|
|
||||||
|
## Perché il gate è composto a mano, e non è una scorciatoia
|
||||||
|
|
||||||
|
`altlib.study_family_honest` è cablato sui candidati **direzionali**: vuole
|
||||||
|
`factory(tf=..., **params) -> target_fn` e passa da `candidate_daily`, che costruisce i rendimenti
|
||||||
|
da una serie di *posizioni* su BTC/ETH. VRP01 non è direzionale. Si usano quindi i suoi **tre
|
||||||
|
componenti reali**, nello stesso ordine e con le stesse soglie: selezione in-sample-only →
|
||||||
|
`altlib.deflated_sharpe` (importato, non riscritto — c'è un test che lo verifica) →
|
||||||
|
`altlib.marginal_vs_tp01` (importato).
|
||||||
|
|
||||||
|
**Famiglia dichiarata nel docstring PRIMA di guardare i numeri:** 8 tenori × 3 delta corti ×
|
||||||
|
3 delta lunghi = **72 celle**. Riaprire il tenore riapre la struttura, e il conteggio dei trial si
|
||||||
|
fa **al rialzo** — contarne meno gonfia il deflated-Sharpe.
|
||||||
|
|
||||||
|
## Il risultato
|
||||||
|
|
||||||
|
| | tenore | δ corto | δ lungo | Sh IS | Sh FULL | Sh HOLD | maxDD | CAGR |
|
||||||
|
|---|---|---|---|---|---|---|---|---|
|
||||||
|
| **scelta al buio** | 10g | −0.28 | −0.05 | **1.75** | 1.55 | 1.01 | 9.6% | 13.5% |
|
||||||
|
| VRP01 canonico | 7g | −0.28 | −0.10 | 1.50 | 1.32 | 0.82 | 10.5% | 10.5% |
|
||||||
|
|
||||||
|
Rango del canonico: **17/72 in-sample, 26/72 sul campione pieno**. La selezione al buio sceglie
|
||||||
|
un'altra cella, e quella cella batte il canonico sia in-sample sia full.
|
||||||
|
|
||||||
|
**Ma il gate si chiude sul deflated-Sharpe:**
|
||||||
|
|
||||||
|
| | DSR su 72 trial | esito |
|
||||||
|
|---|---|---|
|
||||||
|
| cella scelta | **0.948** | **FAIL** (soglia 0.95) |
|
||||||
|
| VRP01 canonico | 0.885 | FAIL |
|
||||||
|
|
||||||
|
`marginal_vs_tp01` dà **ADDS** a entrambe (corr +0.016, `robust_oos` True, `has_insample_edge`
|
||||||
|
True, `is_hedge` False). Quindi il verdetto **non** è «VRP01 è rotto» né «il tenore non conta»:
|
||||||
|
è che il vantaggio apparente non sopravvive al conto dei trial.
|
||||||
|
|
||||||
|
## Come il buco si chiude davvero: la regione nuova perde
|
||||||
|
|
||||||
|
Questo è il punto che rende la chiusura pulita, più del DSR:
|
||||||
|
|
||||||
|
| | cella | Sh IS | rango |
|
||||||
|
|---|---|---|---|
|
||||||
|
| miglior cella **≤10g** (già coperta dal 03/07) | 10g −0.28/−0.05 | 1.75 | 1/72 |
|
||||||
|
| miglior cella **>10g** (territorio mai guardato) | 18g −0.28/−0.05 | 1.56 | **8/72** |
|
||||||
|
|
||||||
|
Solo **3 celle su 10** nella top-10 in-sample stanno oltre i 10 giorni. **Si è aperta la regione
|
||||||
|
mai esplorata e la regione mai esplorata ha perso** contro celle che il 03/07 aveva già visto e
|
||||||
|
non promosso. Il buco non è "chiuso perché il gate boccia": è chiuso perché **guardandoci dentro
|
||||||
|
non c'è niente**.
|
||||||
|
|
||||||
|
## ⚠️ Il vincitore sta dove il modello sbaglia di più
|
||||||
|
|
||||||
|
La cella scelta compra l'ala **più lontana** (δ lungo −0.05), e 5 delle 10 celle migliori fanno lo
|
||||||
|
stesso. Il 30/07 ha **misurato** che il modello piatto sottoprezza proprio l'ala che si *compra*
|
||||||
|
(IV(lunga)−DVOL **+7.5pp** contro +0.8pp della corta, ~**2.3×** il modello), e che l'errore cresce
|
||||||
|
con la distanza dallo strike. Una selezione che premia il δ lungo più piccolo sta plausibilmente
|
||||||
|
massimizzando **l'errore di modello**, non l'edge.
|
||||||
|
|
||||||
|
Onestà su questo punto: è un **sospetto motivato, non una dimostrazione**. Il *mediano* in-sample
|
||||||
|
non separa −0.10 da −0.05 (1.37 contro 1.37); a separarsi è la **coda alta** della griglia. Ma
|
||||||
|
basta a spostare l'onere della prova, perché `f` **non è misurato** fuori dalla struttura canonica.
|
||||||
|
|
||||||
|
⚠️ E la sensibilità a `f` non risolve: applicandolo uniformemente la cella scelta degrada *meno*
|
||||||
|
(f=0.73 → 0.84 contro 0.47 del canonico), ma **f uniforme è la lente sbagliata proprio per questa
|
||||||
|
struttura**, il cui errore è concentrato in una gamba sola. Un numero rassicurante ottenuto con lo
|
||||||
|
strumento sbagliato non è rassicurazione.
|
||||||
|
|
||||||
|
## ⚠️ Quanto è robusto il mio stesso verdetto
|
||||||
|
|
||||||
|
Il DSR dipende da quanti trial si dichiarano, e qui il verdetto **si ribalta**:
|
||||||
|
|
||||||
|
| conteggio | N | DSR | esito |
|
||||||
|
|---|---|---|---|
|
||||||
|
| solo gli 8 tenori | 8 | 0.983 | **PASS** |
|
||||||
|
| **la griglia dichiarata** | **72** | **0.948** | **FAIL** |
|
||||||
|
| 72 + 288 simulate del 03/07 | 360 | 0.909 | FAIL |
|
||||||
|
|
||||||
|
La griglia è stata dichiarata **prima** di guardare i numeri e il conteggio fatto al rialzo — ma il
|
||||||
|
verdetto va citato per quello che è: **fallisce per 0.002**, non è una refutazione netta. (L'ultima
|
||||||
|
riga usa trial *simulati* dalla distribuzione osservata, non le Sharpe vere del 03/07, che vengono
|
||||||
|
da un motore diverso: indicazione, non misura.) Congelato in
|
||||||
|
`test_il_verdetto_dipende_dal_numero_di_trial_dichiarati`, così che nessuno possa farlo passare
|
||||||
|
un domani restringendo il conteggio.
|
||||||
|
|
||||||
|
## Decisione
|
||||||
|
|
||||||
|
**Nessun cambio a VRP01**, per quattro ragioni in ordine di peso:
|
||||||
|
|
||||||
|
1. **Il gate pre-registrato fallisce.** Una soglia dichiarata in anticipo che poi si aggira non è
|
||||||
|
una soglia.
|
||||||
|
2. **La regione davvero nuova non vince** — quindi questo studio non ribalta il 03/07, lo conferma.
|
||||||
|
3. **Il vincitore sta nell'angolo dove abbiamo misurato che il modello è meno affidabile**, e lì
|
||||||
|
`f` non è misurato.
|
||||||
|
4. Niente di tutto questo è eseguibile prima di ~$2.6k, e a $3.000 VRP01 è **0 lotti** al peso di
|
||||||
|
book.
|
||||||
|
|
||||||
|
**Il seguito giusto non è un altro backtest, è una misura.** Se il 10g/−0.28/−0.05 interessa, la
|
||||||
|
domanda che decide è *quanto vale f su quella struttura sulle quote vere* — e da oggi la catena la
|
||||||
|
raccogliamo noi ogni ora. Serve però che il collettore accumuli abbastanza scadenze a 10 giorni:
|
||||||
|
oggi ci sono 15 osservazioni sul solo canonico.
|
||||||
|
|
||||||
|
## Regole
|
||||||
|
|
||||||
|
- **Quando si riapre un parametro si riapre la sua famiglia**, e i trial si contano al rialzo: il
|
||||||
|
deflated-Sharpe è l'unico posto dove barare è indolore e invisibile.
|
||||||
|
- **Dichiarare la griglia prima di guardare i numeri**, e pubblicare la sensibilità del verdetto al
|
||||||
|
conteggio — un gate che passa solo con la propria griglia preferita non è un gate.
|
||||||
|
- **Un buco si chiude meglio mostrando che dentro non c'è niente che bocciando il candidato**: qui
|
||||||
|
la frase che conta non è «DSR 0.948» ma «la regione mai esplorata è ottava».
|
||||||
|
- **Se la selezione converge dove un difetto di modello è già misurato, il sospetto vale più del
|
||||||
|
numero** — e si dice che è un sospetto.
|
||||||
@@ -0,0 +1,281 @@
|
|||||||
|
"""R0730 VRP-TENOR — chiude col gate onesto il buco lasciato aperto sul TENORE di VRP01.
|
||||||
|
|
||||||
|
IL BUCO. La griglia strutture del 03/07 (`r0703_vrpimp_structgrid`) cercava su
|
||||||
|
`TENOR_GRID = (3, 5, 7, 10)`: **oltre i 10 giorni non e' mai stato guardato**. Lo sweep del
|
||||||
|
30/07 (`r0730_vrp_profit_take`, sezione 4-ter) ha toccato 14/18/21/28g per testare un'ipotesi
|
||||||
|
sul profit-take e ha mostrato di sfuggita che il tenore 18 sembra battere il 7 in
|
||||||
|
hold-to-expiry (1.51 vs 1.32) — ma **sei celle scelte sul campione pieno non sono un
|
||||||
|
risultato**, sono la selezione che questo gate esiste per uccidere. Qui si decide.
|
||||||
|
|
||||||
|
PERCHE' IL GATE E' COMPOSTO A MANO. `altlib.study_family_honest` e' cablato sui candidati
|
||||||
|
DIREZIONALI: vuole `factory(tf=..., **params) -> target_fn` e passa per `candidate_daily`,
|
||||||
|
che costruisce i rendimenti da una serie di POSIZIONI su BTC/ETH. VRP01 non e' direzionale.
|
||||||
|
Si usano quindi i suoi tre componenti REALI, nello stesso ordine e con le stesse soglie:
|
||||||
|
|
||||||
|
1. `select_cell_insample` -> qui replicato sulla serie VRP: cella scelta col SOLO Sharpe
|
||||||
|
pre-HOLDOUT (2025-01-01). Nessuna sbirciata all'hold-out.
|
||||||
|
2. `altlib.deflated_sharpe(sr, all_sr, daily)` -> importato, non riscritto. PASS >= 0.95.
|
||||||
|
3. `altlib.marginal_vs_tp01(daily)` -> importato. Serve ADDS + robust_oos +
|
||||||
|
has_insample_edge + non-hedge.
|
||||||
|
|
||||||
|
LA FAMIGLIA. Riaprire il tenore significa riaprire la STRUTTURA, quindi il conteggio dei
|
||||||
|
trial include anche i delta: 8 tenori x 3 delta corti x 3 delta lunghi = **72 celle**. Contarne
|
||||||
|
meno gonfierebbe il deflated-Sharpe (piu' trial = soglia piu' severa: si conta al rialzo).
|
||||||
|
|
||||||
|
⚠️ CAVEAT SU f, DICHIARATO PRIMA DI GUARDARE I NUMERI. Il fattore f=0.73 e' misurato
|
||||||
|
(2026-07-30) SOLO sulla struttura canonica: settimanale, delta -0.28/-0.10. Il meccanismo del
|
||||||
|
difetto e' che il modello piatto sottoprezza **l'ala che si COMPRA** (IV(lunga)-DVOL +7.5pp
|
||||||
|
contro +0.8pp della corta), e quel divario cresce con la distanza dallo strike e col tempo.
|
||||||
|
Quindi su tenori piu' lunghi f e' plausibilmente **peggiore**, e non misurato: ogni numero a
|
||||||
|
f=1.00 qui e' OTTIMISTA di una quantita' ignota. Il gate gira a f=1.00 per essere like-for-like
|
||||||
|
con le condizioni in cui VRP01 fu ammesso; la sensibilita' a f e' riportata a parte.
|
||||||
|
|
||||||
|
uv run python scripts/research/r0730_vrp_tenor_gate.py
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import importlib.util
|
||||||
|
import sys
|
||||||
|
from functools import lru_cache
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
PROJECT_ROOT = Path(__file__).resolve().parents[2]
|
||||||
|
sys.path.insert(0, str(PROJECT_ROOT))
|
||||||
|
sys.path.insert(0, str(PROJECT_ROOT / "scripts" / "research" / "alt"))
|
||||||
|
|
||||||
|
from src.portfolio.portfolio import metrics # noqa: E402
|
||||||
|
from src.portfolio.sleeves import VRP_CFG, _bs_put, _strike_from_delta # noqa: E402
|
||||||
|
|
||||||
|
import altlib # noqa: E402
|
||||||
|
|
||||||
|
_spec = importlib.util.spec_from_file_location(
|
||||||
|
"r0730_pt", PROJECT_ROOT / "scripts" / "research" / "r0730_vrp_profit_take.py")
|
||||||
|
PT = importlib.util.module_from_spec(_spec)
|
||||||
|
_spec.loader.exec_module(PT)
|
||||||
|
|
||||||
|
HOLDOUT = altlib.HOLDOUT # 2025-01-01, la stessa di tutto il progetto
|
||||||
|
ASSETS = ("BTC", "ETH")
|
||||||
|
|
||||||
|
TENORS = (5, 7, 10, 14, 18, 21, 28, 35)
|
||||||
|
SHORT_DELTAS = (-0.35, -0.28, -0.20)
|
||||||
|
LONG_DELTAS = (-0.15, -0.10, -0.05)
|
||||||
|
CANON = (VRP_CFG["tenor_d"], VRP_CFG["short_delta"], VRP_CFG["long_delta"])
|
||||||
|
|
||||||
|
|
||||||
|
@lru_cache(maxsize=8)
|
||||||
|
def _series(asset: str):
|
||||||
|
return PT.series(asset)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
def cell_asset(asset: str, tenor: int, sd: float, ld: float, f: float = 1.0) -> pd.Series:
|
||||||
|
"""VRP01 con struttura libera, tenuto a scadenza, fee REALI per gamba. Gate d'ingresso
|
||||||
|
(VRP>0 e IV-rank) identici al canonico: qui si muove la STRUTTURA, non il segnale."""
|
||||||
|
px, dvf, idx = _series(asset)
|
||||||
|
n = len(px)
|
||||||
|
cfg = VRP_CFG
|
||||||
|
tn = int(tenor)
|
||||||
|
T0 = tn / 365.25
|
||||||
|
rets: dict[pd.Timestamp, float] = {}
|
||||||
|
i = 60
|
||||||
|
while i + tn < n:
|
||||||
|
S0, sig = px[i], dvf[i]
|
||||||
|
skip = False
|
||||||
|
if cfg["gate_vrp"] and i >= 31:
|
||||||
|
rv = np.std(np.diff(np.log(px[i - 30:i + 1]))) * np.sqrt(365.25)
|
||||||
|
if (sig - rv) <= 0:
|
||||||
|
skip = True
|
||||||
|
if not skip and i >= 60:
|
||||||
|
ivr = float((dvf[:i] < dvf[i]).mean())
|
||||||
|
if ivr < cfg["gate_ivr"] or ivr > cfg["crash_skip"]:
|
||||||
|
skip = True
|
||||||
|
if skip:
|
||||||
|
rets[idx[i + tn]] = 0.0
|
||||||
|
i += tn
|
||||||
|
continue
|
||||||
|
Ks = _strike_from_delta(S0, T0, sig, sd)
|
||||||
|
Kl = _strike_from_delta(S0, T0, sig, ld)
|
||||||
|
ps, pl = _bs_put(S0, Ks, T0, sig), _bs_put(S0, Kl, T0, sig)
|
||||||
|
credit = (ps - pl) * f
|
||||||
|
fee = PT.leg_fee(S0, ps * f) + PT.leg_fee(S0, pl * f)
|
||||||
|
S1 = px[i + tn]
|
||||||
|
if Ks > S1:
|
||||||
|
fee += 0.5 * PT.FEE_UNDERLYING * S1
|
||||||
|
if Kl > S1:
|
||||||
|
fee += 0.5 * PT.FEE_UNDERLYING * S1
|
||||||
|
payoff = max(0.0, Ks - S1) - max(0.0, Kl - S1)
|
||||||
|
rets[idx[i + tn]] = (credit - payoff - fee) / Ks
|
||||||
|
i += tn
|
||||||
|
return pd.Series(rets).sort_index()
|
||||||
|
|
||||||
|
|
||||||
|
def cell_daily(tenor: int, sd: float, ld: float, f: float = 1.0) -> pd.Series:
|
||||||
|
"""Book 50/50 BTC+ETH su griglia giornaliera, convenzione di `_vrp_combo_returns`."""
|
||||||
|
a, b = cell_asset("BTC", tenor, sd, ld, f), cell_asset("ETH", tenor, sd, ld, f)
|
||||||
|
wk = pd.concat({"B": a, "E": b}, axis=1, join="inner").mean(axis=1).sort_index()
|
||||||
|
if wk.empty:
|
||||||
|
return wk
|
||||||
|
days = pd.date_range(wk.index.min().normalize(), wk.index.max().normalize(), freq="1D", tz="UTC")
|
||||||
|
daily = pd.Series(0.0, index=days)
|
||||||
|
daily.loc[wk.index.normalize()] = wk.values
|
||||||
|
return daily
|
||||||
|
|
||||||
|
|
||||||
|
def _sh(s: pd.Series) -> float:
|
||||||
|
return metrics(s)["sharpe"] if len(s) > 30 else float("nan")
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
def main() -> int:
|
||||||
|
print("=" * 94)
|
||||||
|
print(" R0730 VRP-TENOR — il tenore di VRP01 passa il gate onesto?")
|
||||||
|
print("=" * 94)
|
||||||
|
|
||||||
|
grid = [(tn, sd, ld) for tn in TENORS for sd in SHORT_DELTAS for ld in LONG_DELTAS]
|
||||||
|
print(f"\n FAMIGLIA: {len(TENORS)} tenori x {len(SHORT_DELTAS)} delta corti x "
|
||||||
|
f"{len(LONG_DELTAS)} delta lunghi = {len(grid)} celle")
|
||||||
|
print(f" Selezione IN-SAMPLE (pre-{HOLDOUT:%Y-%m-%d}), fee reali per gamba, f=1.00")
|
||||||
|
|
||||||
|
rows = []
|
||||||
|
for (tn, sd, ld) in grid:
|
||||||
|
d = cell_daily(tn, sd, ld)
|
||||||
|
if d.empty:
|
||||||
|
continue
|
||||||
|
ins = d[d.index < HOLDOUT]
|
||||||
|
m = metrics(d)
|
||||||
|
rows.append(dict(tenor=tn, sd=sd, ld=ld, daily=d,
|
||||||
|
is_sh=_sh(ins) if len(ins) > 60 else float("nan"),
|
||||||
|
full_sh=m["sharpe"], hold_sh=_sh(d[d.index >= HOLDOUT]),
|
||||||
|
dd=m["maxdd"], cagr=m["cagr"]))
|
||||||
|
valid = [r for r in rows if np.isfinite(r["is_sh"])]
|
||||||
|
chosen = max(valid, key=lambda r: r["is_sh"])
|
||||||
|
canon_row = next(r for r in rows if (r["tenor"], r["sd"], r["ld"]) == CANON)
|
||||||
|
|
||||||
|
# --- 1. la cella scelta al buio ------------------------------------------
|
||||||
|
by_is = sorted(valid, key=lambda r: r["is_sh"], reverse=True)
|
||||||
|
by_full = sorted(valid, key=lambda r: r["full_sh"], reverse=True)
|
||||||
|
rk_is = 1 + by_is.index(canon_row)
|
||||||
|
rk_full = 1 + by_full.index(canon_row)
|
||||||
|
|
||||||
|
print(f"\n (1) CELLA SCELTA AL BUIO (solo Sharpe in-sample)")
|
||||||
|
print(f" {'':6}{'tenore':>8}{'delta corto':>13}{'delta lungo':>13}"
|
||||||
|
f"{'Sh IS':>9}{'Sh FULL':>9}{'Sh HOLD':>9}{'maxDD':>8}{'CAGR':>8}")
|
||||||
|
print(f" {'scelta':6}{chosen['tenor']:>7}g{chosen['sd']:>13.2f}{chosen['ld']:>13.2f}"
|
||||||
|
f"{chosen['is_sh']:>9.2f}{chosen['full_sh']:>9.2f}{chosen['hold_sh']:>9.2f}"
|
||||||
|
f"{chosen['dd']*100:>7.1f}%{chosen['cagr']*100:>7.1f}%")
|
||||||
|
print(f" {'VRP01':6}{canon_row['tenor']:>7}g{canon_row['sd']:>13.2f}{canon_row['ld']:>13.2f}"
|
||||||
|
f"{canon_row['is_sh']:>9.2f}{canon_row['full_sh']:>9.2f}{canon_row['hold_sh']:>9.2f}"
|
||||||
|
f"{canon_row['dd']*100:>7.1f}%{canon_row['cagr']*100:>7.1f}%")
|
||||||
|
print(f"\n Rango del canonico: {rk_is}/{len(valid)} in-sample · "
|
||||||
|
f"{rk_full}/{len(valid)} sul campione pieno")
|
||||||
|
same = (chosen["tenor"], chosen["sd"], chosen["ld"]) == CANON
|
||||||
|
print(f" La selezione al buio {'RITROVA il canonico' if same else 'sceglie ALTRO'}.")
|
||||||
|
|
||||||
|
# --- 2. top-8 in-sample ---------------------------------------------------
|
||||||
|
print(f"\n (2) TOP 8 IN-SAMPLE — e come si comportano fuori campione")
|
||||||
|
print(f" {'tenore':>8}{'corto':>8}{'lungo':>8}{'Sh IS':>9}{'Sh FULL':>9}{'Sh HOLD':>9}")
|
||||||
|
for r in by_is[:8]:
|
||||||
|
mark = " <- VRP01" if (r["tenor"], r["sd"], r["ld"]) == CANON else ""
|
||||||
|
print(f" {r['tenor']:>7}g{r['sd']:>8.2f}{r['ld']:>8.2f}"
|
||||||
|
f"{r['is_sh']:>9.2f}{r['full_sh']:>9.2f}{r['hold_sh']:>9.2f}{mark}")
|
||||||
|
|
||||||
|
# --- 3. deflated Sharpe ---------------------------------------------------
|
||||||
|
all_full = [r["full_sh"] for r in rows]
|
||||||
|
dsr, sr0 = altlib.deflated_sharpe(chosen["full_sh"], all_full, chosen["daily"])
|
||||||
|
dsr_c, _ = altlib.deflated_sharpe(canon_row["full_sh"], all_full, canon_row["daily"])
|
||||||
|
print(f"\n (3) DEFLATED SHARPE su {len(all_full)} trial (Bailey & Lopez de Prado, PASS >= 0.95)")
|
||||||
|
print(f" massimo atteso sotto il nullo: {sr0:.3f}")
|
||||||
|
print(f" cella scelta : DSR {dsr:.3f} {'PASS' if dsr >= 0.95 else 'FAIL'}")
|
||||||
|
print(f" VRP01 canonico: DSR {dsr_c:.3f} {'PASS' if dsr_c >= 0.95 else 'FAIL'}")
|
||||||
|
|
||||||
|
# --- 4. marginale vs TP01 -------------------------------------------------
|
||||||
|
mg = altlib.marginal_vs_tp01(chosen["daily"])
|
||||||
|
mgc = altlib.marginal_vs_tp01(canon_row["daily"])
|
||||||
|
print(f"\n (4) MARGINALE vs TP01 (altlib.marginal_vs_tp01)")
|
||||||
|
for lab, m in (("cella scelta", mg), ("VRP01 canonico", mgc)):
|
||||||
|
print(f" {lab:<16} verdetto {str(m.get('marginal_verdict')):<10}"
|
||||||
|
f" corr {m.get('corr_full', float('nan')):+.3f}"
|
||||||
|
f" robust_oos {str(m.get('robust_oos'))!s:<5}"
|
||||||
|
f" insample_edge {str(m.get('has_insample_edge'))!s:<5}"
|
||||||
|
f" is_hedge {str(m.get('is_hedge'))!s}")
|
||||||
|
|
||||||
|
# --- 5. verdetto ----------------------------------------------------------
|
||||||
|
adds = mg.get("marginal_verdict") == "ADDS"
|
||||||
|
honest = bool(adds and mg.get("robust_oos") and mg.get("has_insample_edge")
|
||||||
|
and not mg.get("is_hedge") and dsr >= 0.95)
|
||||||
|
beats = chosen["full_sh"] > canon_row["full_sh"] and chosen["is_sh"] > canon_row["is_sh"]
|
||||||
|
print(f"\n (5) VERDETTO")
|
||||||
|
print(f" earns_slot_honest = {honest} (ADDS {adds} · DSR {dsr:.3f} >= 0.95 "
|
||||||
|
f"{dsr >= 0.95} · robust_oos {mg.get('robust_oos')} · non-hedge {not mg.get('is_hedge')})")
|
||||||
|
print(f" batte il canonico in-sample E full = {beats}")
|
||||||
|
if same:
|
||||||
|
print(" -> il buco e' CHIUSO: la selezione onesta ritrova la struttura gia' in uso.")
|
||||||
|
elif not honest:
|
||||||
|
print(" -> il buco e' CHIUSO: la cella scelta al buio NON passa il gate.")
|
||||||
|
else:
|
||||||
|
print(" -> ATTENZIONE: candidato che passa. Serve l'audit d'ancora e weights_tilt_null.")
|
||||||
|
|
||||||
|
# --- 6. sensibilita' a f ---------------------------------------------------
|
||||||
|
print(f"\n (6) SENSIBILITA' a f — f=0.73 e' misurato SOLO sul canonico (7g, -0.28/-0.10).")
|
||||||
|
print(" Su tenori piu' lunghi l'ala comprata e' piu' lontana e piu' lunga, quindi f e'")
|
||||||
|
print(" plausibilmente PEGGIORE e non misurato: questi numeri sono un TETTO.")
|
||||||
|
print(f" {'f':>6}{'Sh canonico':>14}{'Sh cella scelta':>18}")
|
||||||
|
for f in (1.00, 0.80, 0.73):
|
||||||
|
a = _sh(cell_daily(*CANON, f=f))
|
||||||
|
b = _sh(cell_daily(chosen["tenor"], chosen["sd"], chosen["ld"], f=f))
|
||||||
|
print(f" {f:>6.2f}{a:>14.2f}{b:>18.2f}")
|
||||||
|
|
||||||
|
# --- 7. la regione DAVVERO nuova (>10g) vince o perde? ---------------------
|
||||||
|
print("\n (7) LA REGIONE MAI ESPLORATA — la griglia 03/07 arrivava a 10g")
|
||||||
|
old = [r for r in valid if r["tenor"] <= 10]
|
||||||
|
new = [r for r in valid if r["tenor"] > 10]
|
||||||
|
b_old = max(old, key=lambda r: r["is_sh"])
|
||||||
|
b_new = max(new, key=lambda r: r["is_sh"])
|
||||||
|
rk_new = 1 + by_is.index(b_new)
|
||||||
|
print(f" miglior cella <=10g (gia' coperta dal 03/07): {b_old['tenor']:>3}g "
|
||||||
|
f"{b_old['sd']:+.2f}/{b_old['ld']:+.2f} Sh IS {b_old['is_sh']:.2f}")
|
||||||
|
print(f" miglior cella >10g (territorio NUOVO) : {b_new['tenor']:>3}g "
|
||||||
|
f"{b_new['sd']:+.2f}/{b_new['ld']:+.2f} Sh IS {b_new['is_sh']:.2f}"
|
||||||
|
f" -> rango {rk_new}/{len(valid)}")
|
||||||
|
print(f" celle >10g nella top-10 in-sample: "
|
||||||
|
f"{sum(1 for r in by_is[:10] if r['tenor'] > 10)}/10")
|
||||||
|
print(" -> il buco si chiude cosi': si e' aperta la regione mai guardata e la regione")
|
||||||
|
print(" mai guardata PERDE contro celle gia' esplorate nel 2026-07-03.")
|
||||||
|
|
||||||
|
# --- 8. il confound: la selezione va dove il modello sbaglia di piu' -------
|
||||||
|
print("\n (8) CONFOUND — dove va la selezione dentro la griglia")
|
||||||
|
print(f" {'delta lungo':>13}{'Sh IS mediano':>16}{'celle in top-10':>18}")
|
||||||
|
for ld in LONG_DELTAS:
|
||||||
|
sub = [r["is_sh"] for r in valid if r["ld"] == ld]
|
||||||
|
ntop = sum(1 for r in by_is[:10] if r["ld"] == ld)
|
||||||
|
print(f" {ld:>13.2f}{np.median(sub):>16.2f}{ntop:>15}/10")
|
||||||
|
print(" ⚠ Il 30/07 ha MISURATO che il modello piatto sottoprezza l'ala COMPRATA")
|
||||||
|
print(" (IV(lunga)-DVOL +7.5pp contro +0.8pp della corta, ~2.3x il modello) e che")
|
||||||
|
print(" l'errore cresce con la distanza dallo strike. Una selezione che premia il")
|
||||||
|
print(" delta lungo piu' piccolo sta massimizzando l'errore di modello, non l'edge.")
|
||||||
|
|
||||||
|
# --- 9. quanto e' robusto il MIO verdetto? --------------------------------
|
||||||
|
print("\n (9) ROBUSTEZZA DEL VERDETTO — il DSR dipende da quanti trial dichiaro")
|
||||||
|
for label, subset in (("solo gli 8 tenori", [r["full_sh"] for r in rows
|
||||||
|
if (r["sd"], r["ld"]) == CANON[1:]]),
|
||||||
|
("la griglia dichiarata (72)", all_full),
|
||||||
|
("72 + le 288 del 03/07", all_full + [np.nan] * 0 + list(
|
||||||
|
np.random.default_rng(0).normal(
|
||||||
|
float(np.mean(all_full)), float(np.std(all_full)), 288)))):
|
||||||
|
d, _ = altlib.deflated_sharpe(chosen["full_sh"], subset, chosen["daily"])
|
||||||
|
print(f" {label:<28} N={len(subset):>4} DSR {d:.3f} "
|
||||||
|
f"{'PASS' if d >= 0.95 else 'FAIL'}")
|
||||||
|
print(" ⚠ Con 8 trial passerebbe. La griglia dichiarata e' 72 perche' riaprire il")
|
||||||
|
print(" tenore riapre la struttura, e il conteggio si fa al RIALZO. Ma il verdetto")
|
||||||
|
print(" e' sul filo (0.948 vs 0.95): va citato come tale, non come refutazione netta.")
|
||||||
|
print(" ⚠ L'ultima riga usa trial SIMULATI dalla distribuzione osservata, non le")
|
||||||
|
print(" Sharpe vere del 03/07 (motore diverso): e' un'indicazione, non una misura.")
|
||||||
|
print()
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
@@ -0,0 +1,159 @@
|
|||||||
|
"""Test del gate onesto sul tenore di VRP01 (scripts/research/r0730_vrp_tenor_gate.py).
|
||||||
|
|
||||||
|
Cosa va congelato, in ordine di importanza:
|
||||||
|
|
||||||
|
* **La famiglia dichiarata.** Il deflated-Sharpe dipende da quanti trial si dichiarano, e il
|
||||||
|
verdetto di questo studio si RIBALTA fra 8 e 72 celle. La griglia e' stata dichiarata nel
|
||||||
|
docstring PRIMA di guardare i numeri; questi test la congelano, cosi' che un domani nessuno
|
||||||
|
possa farla passare restringendo il conteggio.
|
||||||
|
* **La selezione e' in-sample.** Se qualcuno la cambiasse in "cella a max hold-out" il gate
|
||||||
|
diventerebbe la trappola che esiste per prevenire.
|
||||||
|
* **La coerenza fra i due script del 30/07**: la cella canonica qui deve essere la stessa
|
||||||
|
serie del canonico misurato in `r0730_vrp_profit_take`.
|
||||||
|
* **Il controllo positivo**: un gate che dice sempre FAIL non e' un gate.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import importlib.util
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import pandas as pd
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
sys.path.insert(0, str(ROOT))
|
||||||
|
sys.path.insert(0, str(ROOT / "scripts" / "research" / "alt"))
|
||||||
|
|
||||||
|
_spec = importlib.util.spec_from_file_location(
|
||||||
|
"r0730_tenor", ROOT / "scripts" / "research" / "r0730_vrp_tenor_gate.py")
|
||||||
|
TG = importlib.util.module_from_spec(_spec)
|
||||||
|
_spec.loader.exec_module(TG)
|
||||||
|
|
||||||
|
import altlib # noqa: E402
|
||||||
|
from src.portfolio.sleeves import VRP_CFG # noqa: E402
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="module")
|
||||||
|
def grid():
|
||||||
|
rows = []
|
||||||
|
for tn in TG.TENORS:
|
||||||
|
for sd in TG.SHORT_DELTAS:
|
||||||
|
for ld in TG.LONG_DELTAS:
|
||||||
|
d = TG.cell_daily(tn, sd, ld)
|
||||||
|
ins = d[d.index < TG.HOLDOUT]
|
||||||
|
rows.append(dict(tenor=tn, sd=sd, ld=ld, daily=d,
|
||||||
|
is_sh=TG._sh(ins), full_sh=TG._sh(d),
|
||||||
|
hold_sh=TG._sh(d[d.index >= TG.HOLDOUT])))
|
||||||
|
return rows
|
||||||
|
|
||||||
|
|
||||||
|
# ===========================================================================
|
||||||
|
# la famiglia dichiarata — il numero da cui dipende il verdetto
|
||||||
|
# ===========================================================================
|
||||||
|
def test_la_griglia_dichiarata_e_di_72_celle():
|
||||||
|
assert len(TG.TENORS) * len(TG.SHORT_DELTAS) * len(TG.LONG_DELTAS) == 72
|
||||||
|
|
||||||
|
|
||||||
|
def test_la_griglia_copre_la_regione_mai_esplorata_dal_0307():
|
||||||
|
"""Il buco era 'oltre i 10 giorni': la griglia 03/07 era (3, 5, 7, 10)."""
|
||||||
|
assert max(TG.TENORS) > 10
|
||||||
|
assert sum(1 for t in TG.TENORS if t > 10) >= 4
|
||||||
|
|
||||||
|
|
||||||
|
def test_il_canonico_e_dentro_la_griglia():
|
||||||
|
"""Senza il canonico dentro non c'e' un termine di paragone."""
|
||||||
|
assert TG.CANON == (VRP_CFG["tenor_d"], VRP_CFG["short_delta"], VRP_CFG["long_delta"])
|
||||||
|
assert TG.CANON[0] in TG.TENORS
|
||||||
|
assert TG.CANON[1] in TG.SHORT_DELTAS
|
||||||
|
assert TG.CANON[2] in TG.LONG_DELTAS
|
||||||
|
|
||||||
|
|
||||||
|
# ===========================================================================
|
||||||
|
# coerenza fra i due script del 30/07
|
||||||
|
# ===========================================================================
|
||||||
|
def test_la_cella_canonica_e_lo_stesso_canonico_dell_altro_studio():
|
||||||
|
"""`r0730_vrp_profit_take.combo(pt=None)` e `cell_daily(*CANON)` devono essere la STESSA
|
||||||
|
serie: se divergono, i due diari raccontano due cose diverse chiamandole uguali."""
|
||||||
|
a = TG.cell_daily(*TG.CANON)
|
||||||
|
b = TG.PT.combo(pt=None, f=1.0, real_fee=True)[0]
|
||||||
|
j = pd.concat({"a": a, "b": b}, axis=1, join="inner")
|
||||||
|
assert len(j) == len(a) == len(b)
|
||||||
|
assert float((j["a"] - j["b"]).abs().max()) == pytest.approx(0.0, abs=1e-12)
|
||||||
|
|
||||||
|
|
||||||
|
# ===========================================================================
|
||||||
|
# la selezione non sbircia l'hold-out
|
||||||
|
# ===========================================================================
|
||||||
|
def test_la_cella_e_scelta_col_solo_sharpe_in_sample(grid):
|
||||||
|
chosen = max(grid, key=lambda r: r["is_sh"])
|
||||||
|
assert chosen["is_sh"] == max(r["is_sh"] for r in grid)
|
||||||
|
|
||||||
|
|
||||||
|
def test_scegliere_sull_hold_out_darebbe_una_cella_DIVERSA(grid):
|
||||||
|
"""E' il motivo per cui il gate esiste: la cella a max hold-out non e' quella a max
|
||||||
|
in-sample, quindi 'scegliere guardando l'hold-out' sarebbe una scelta, non una misura."""
|
||||||
|
a = max(grid, key=lambda r: r["is_sh"])
|
||||||
|
b = max(grid, key=lambda r: r["hold_sh"])
|
||||||
|
assert (a["tenor"], a["sd"], a["ld"]) != (b["tenor"], b["sd"], b["ld"])
|
||||||
|
|
||||||
|
|
||||||
|
# ===========================================================================
|
||||||
|
# il risultato: la regione nuova perde
|
||||||
|
# ===========================================================================
|
||||||
|
def test_la_regione_oltre_i_10_giorni_non_produce_il_vincitore(grid):
|
||||||
|
"""Il modo in cui il buco si chiude: si e' guardato dove non si era mai guardato, e li'
|
||||||
|
non c'e' il vincitore."""
|
||||||
|
chosen = max(grid, key=lambda r: r["is_sh"])
|
||||||
|
assert chosen["tenor"] <= 10
|
||||||
|
best_new = max((r for r in grid if r["tenor"] > 10), key=lambda r: r["is_sh"])
|
||||||
|
assert best_new["is_sh"] < chosen["is_sh"]
|
||||||
|
|
||||||
|
|
||||||
|
# ===========================================================================
|
||||||
|
# deflated Sharpe: quello di altlib, e il verdetto che ne dipende
|
||||||
|
# ===========================================================================
|
||||||
|
def test_usa_il_deflated_sharpe_di_altlib_non_una_copia():
|
||||||
|
assert TG.altlib.deflated_sharpe is altlib.deflated_sharpe
|
||||||
|
|
||||||
|
|
||||||
|
def test_il_verdetto_dipende_dal_numero_di_trial_dichiarati(grid):
|
||||||
|
"""Il fatto piu' importante da non dimenticare: a 8 trial passerebbe, a 72 no. La griglia
|
||||||
|
e' stata dichiarata prima di guardare, e il conteggio si fa al rialzo."""
|
||||||
|
chosen = max(grid, key=lambda r: r["is_sh"])
|
||||||
|
all72 = [r["full_sh"] for r in grid]
|
||||||
|
only8 = [r["full_sh"] for r in grid if (r["sd"], r["ld"]) == TG.CANON[1:]]
|
||||||
|
d72, _ = altlib.deflated_sharpe(chosen["full_sh"], all72, chosen["daily"])
|
||||||
|
d8, _ = altlib.deflated_sharpe(chosen["full_sh"], only8, chosen["daily"])
|
||||||
|
assert len(only8) == len(TG.TENORS) == 8
|
||||||
|
assert d8 > 0.95 > d72 # il verdetto si ribalta
|
||||||
|
assert d72 > 0.90 # ...e fallisce per poco: va citato cosi'
|
||||||
|
|
||||||
|
|
||||||
|
def test_il_gate_fallisce_sulla_griglia_dichiarata(grid):
|
||||||
|
chosen = max(grid, key=lambda r: r["is_sh"])
|
||||||
|
dsr, _ = altlib.deflated_sharpe(chosen["full_sh"], [r["full_sh"] for r in grid],
|
||||||
|
chosen["daily"])
|
||||||
|
assert dsr < 0.95
|
||||||
|
|
||||||
|
|
||||||
|
def test_controllo_positivo_il_deflated_sharpe_sa_promuovere(grid):
|
||||||
|
"""Obbligatorio: un gate che risponde sempre FAIL e' indistinguibile da uno rotto.
|
||||||
|
Una serie COSTRUITA molto forte, contro gli stessi 72 trial, deve passare."""
|
||||||
|
chosen = max(grid, key=lambda r: r["is_sh"])
|
||||||
|
strong = chosen["daily"] * 1.0
|
||||||
|
strong = strong + strong.std() * 0.5 # drift grande, stessa forma
|
||||||
|
dsr, _ = altlib.deflated_sharpe(TG._sh(strong), [r["full_sh"] for r in grid], strong)
|
||||||
|
assert dsr >= 0.95
|
||||||
|
|
||||||
|
|
||||||
|
# ===========================================================================
|
||||||
|
# il marginale resta ADDS: il verdetto NON e' "VRP e' rotto"
|
||||||
|
# ===========================================================================
|
||||||
|
def test_il_canonico_resta_ADDS_vs_tp01():
|
||||||
|
"""Il gate boccia un CAMBIO di struttura, non lo sleeve: se questo si rompe, la
|
||||||
|
conclusione del diario va riscritta."""
|
||||||
|
m = altlib.marginal_vs_tp01(TG.cell_daily(*TG.CANON))
|
||||||
|
assert m["marginal_verdict"] == "ADDS"
|
||||||
|
assert m["has_insample_edge"] and not m["is_hedge"]
|
||||||
Reference in New Issue
Block a user