research(prop): lente wick ACCOPPIATA — chiude il follow-up dichiarato del 25/07
La scala di conti funded era giudicata con due lenti a un ordine di grandezza di distanza su P(>=50 EUR/g): 20.7% close-only vs 1.6-2.5% wick indipendente. Il difetto era dichiarato (gap estratto indipendente dal rendimento del giorno) ma il recon accoppiato esisteva solo per il book 75/25. Qui si generalizza: sleeve TP01/SKH01 separati a risoluzione oraria (minimo intraday esatto per qualsiasi vettore di pesi) + XS01 accoppiato dagli OHLC giornalieri HL (recon = sleeve ufficiale a max|delta|=0.0). FINDING: la calibrazione del wick era giusta, l'errore era l'INDIPENDENZA. Le marginali coincidono (p50 -0.17pp identico) ma il gap e' ~3x piu' profondo nei giorni che finiscono BENE (-1.58pp vs -0.48pp), perche' un giorno brutto chiude sul minimo (m==R nel 26% dei giorni). Il breach si valuta sul minimo -> la lente indipendente raddoppia i breach da daily-loss (2.0-2.9x, misurato sui giorni storici). close-only non e' conservativa ma CIECA: 0.00% di breach ovunque. Verifiche: (1) 1h vs 5m identici (p99 -3.06 vs -3.14pp) -> il caveat di risoluzione del 24/07 e' trascurabile; (2) riconciliazione col 24/07 RISOLTA (63.2% vs 58% sulla stessa finestra: non era la finestra, era la lente); (3) bound severo su XS01 non ribalta config B. Decisioni: funded 0.75x (non 0.50x — massimizza il payout a 3 anni, $2674 vs $1230); scala P(>=50/g) ~6% con P(zero) 52-65%; ordine fra politiche invariato a tutte e 3 le lenti. Book/pesi/cron INVARIATI. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -487,12 +487,11 @@ Prima ondata di ricerca onesta su BTC/ETH certificati (5 track, harness condivis
|
|||||||
Logica: ogni conto serve Sharpe per sopravvivere alla PROPRIA barriera (uccide SPARSO), ma i
|
Logica: ogni conto serve Sharpe per sopravvivere alla PROPRIA barriera (uccide SPARSO), ma i
|
||||||
conti servono decorrelati fra loro (penalizza CONC). **Con lente INTRADAY (wick)** tutto crolla:
|
conti servono decorrelati fra loro (penalizza CONC). **Con lente INTRADAY (wick)** tutto crolla:
|
||||||
P(≥50/g) 1.6-2.5%, P(zero) 78-90%. ⚠ Il mio wick e' un **PAVIMENTO**: gap estratto INDIPENDENTE
|
P(≥50/g) 1.6-2.5%, P(zero) 78-90%. ⚠ Il mio wick e' un **PAVIMENTO**: gap estratto INDIPENDENTE
|
||||||
dal rendimento del giorno (i wick veri sono accoppiati alle giornate brutte) → breach spuri; il
|
dal rendimento del giorno → breach spuri → **follow-up dichiarato, poi CHIUSO lo stesso giorno
|
||||||
modo giusto e' il bootstrap delle TUPLE (ritorno,gap) di `r0724_goal50_intraday_mc.py`, che pero'
|
(bullet successivo): la stima onesta e' P(≥50/g) ~6%, P(zero) 52-65%.** Il confronto fra POLITICHE
|
||||||
ha il recon solo per TP01+SKH01 → **follow-up dichiarato**. **VERDETTO IN BANDA (risposta al goal):
|
e' robusto (stessa lente per tutte) — **verificato a posteriori sulla lente accoppiata: l'ordine
|
||||||
una via da €600 a €50/g ESISTE (scala di conti funded) con P ~2-21% in 3 anni e P(bruciare i €600)
|
MISTO ≥ CONC-DIV > CONC-2SL ≈ SPARSO regge a tutte e 3 le lenti** → **se si apre il fronte prop,
|
||||||
~33-78% → scommessa a coda destra, NON una rendita.** Il confronto fra POLITICHE e' robusto (stessa
|
NON mandarci il book live attuale**.
|
||||||
lente per tutte): **se si apre il fronte prop, NON mandarci il book live attuale**.
|
|
||||||
(6) **ADDENDUM "e se metto 10K su IB?"** (`r0725_ib10k.py`): allocazione fra venue, non domanda su
|
(6) **ADDENDUM "e se metto 10K su IB?"** (`r0725_ib10k.py`): allocazione fra venue, non domanda su
|
||||||
GTAA01. Deribit = **motore** (CAGR ~11% de-luck), IB/GTAA01 = **diversificatore** (CAGR ~3.9%).
|
GTAA01. Deribit = **motore** (CAGR ~11% de-luck), IB/GTAA01 = **diversificatore** (CAGR ~3.9%).
|
||||||
A $11.5k totali: 0% su IB → **€2.16/g**; 50% → €1.54/g (Sharpe migliore 1.13, DD 15.3→11.2%);
|
A $11.5k totali: 0% su IB → **€2.16/g**; 50% → €1.54/g (Sharpe migliore 1.13, DD 15.3→11.2%);
|
||||||
@@ -515,16 +514,49 @@ Prima ondata di ricerca onesta su BTC/ETH certificati (5 track, harness condivis
|
|||||||
"costo 0" puo' essere un bug): su un book con profitto concentrato in 1-2 giorni la regola porta
|
"costo 0" puo' essere un bug): su un book con profitto concentrato in 1-2 giorni la regola porta
|
||||||
P(pass) da >90% a <5% (test cablato). **Il vincolo vero e' il maxDD 6% STATICO**: config A (book
|
P(pass) da >90% a <5% (test cablato). **Il vincolo vero e' il maxDD 6% STATICO**: config A (book
|
||||||
live) ha maxDD 9.5% > 6% = strutturalmente incompatibile a leva piena — intraday a 1.0x
|
live) ha maxDD 9.5% > 6% = strutturalmente incompatibile a leva piena — intraday a 1.0x
|
||||||
sopravvive l'**1.4%**; config B (+XS01) ha maxDD 6.2%, Sharpe 1.13. **Funded consigliato: config B
|
sopravvive l'**1.4%**; config B (+XS01) ha maxDD 6.2%, Sharpe 1.13. ~~**Funded consigliato: config B
|
||||||
a 0.50x** (intraday P(vivo) 71% vs 32% a 0.75x, E[payout] €7.69 vs €8.66/g — la sopravvivenza
|
a 0.50x**~~ → **CORRETTO a 0.75x dalla lente accoppiata (bullet successivo)**: il 0.50x era scelto
|
||||||
COMPONE su piu' anni). EV del biglietto $100k **positivo in entrambe le lenti** (+$6.789
|
perche' il wick indipendente dava a 0.75x P(vivo) 55%; la lente onesta da' **76%**, e 0.75x
|
||||||
|
**massimizza il payout atteso su 3 anni** ($2.674 vs $1.230 a 0.50x e $1.992 a 1.00x). L'argomento
|
||||||
|
"la sopravvivenza COMPONE su piu' anni" resta valido: si sposta il punto in cui morde.
|
||||||
|
EV del biglietto $100k **positivo in entrambe le lenti** (+$6.789
|
||||||
close-only, +$1.166 intraday), ma ~69% di perdere la fee nella lente pessimista. Cap $200k/trader
|
close-only, +$1.166 intraday), ma ~69% di perdere la fee nella lente pessimista. Cap $200k/trader
|
||||||
⇒ ~€15-30/g max → per €50/g servono piu' firm (punto 5). ⚠ Discrepanza col 24/07 (P(vivo) A@0.75x
|
⇒ ~€15-30/g max → per €50/g servono piu' firm (punto 5). ✅ Discrepanza col 24/07 (P(vivo) A@0.75x
|
||||||
58% vs 10% mio): mio wick indipendente = piu' severo + finestra 2024+ dove A e' debole; **la
|
58% vs 10% mio) **RISOLTA: non era la finestra, era la lente** — sulla stessa finestra piena la
|
||||||
direzione concorda, sul LIVELLO il numero del 24/07 (recon MTM vero) e' piu' affidabile**.
|
lente accoppiata da' **63.2%** vs il 58% del 24/07 (accordo entro 5pp, implementazioni separate).
|
||||||
⚠ Bug catturato in sessione: base di prelievo funded aggiornata giornalmente come HWM mobile →
|
⚠ Bug catturato in sessione: base di prelievo funded aggiornata giornalmente come HWM mobile →
|
||||||
guadagno al checkpoint ≈ 0 → **payout $230/anno invece di ~$7.000**; regole vere = max-loss STATICO
|
guadagno al checkpoint ≈ 0 → **payout $230/anno invece di ~$7.000**; regole vere = max-loss STATICO
|
||||||
dal saldo iniziale + base ripristinata dopo il prelievo. Test di regressione cablato.
|
dal saldo iniziale + base ripristinata dopo il prelievo. Test di regressione cablato.
|
||||||
|
- ✅ **LENTE WICK ACCOPPIATA (2026-07-25, follow-up del bullet precedente — CHIUSO).** Script
|
||||||
|
`r0725_prop_coupled.py`, test `tests/test_prop_coupled.py` (13 casi), diario
|
||||||
|
`2026-07-25-prop-wick-accoppiato.md`. Book/pesi/cron **INVARIATI**. Generalizza il recon MTM di
|
||||||
|
`r0724_goal50_intraday_mc.py`: (a) sleeve TP01/SKH01 **separati** a risoluzione oraria → il minimo
|
||||||
|
intraday si compone ESATTO per QUALSIASI vettore di pesi (prima erano cablati 75/25); (b) **XS01
|
||||||
|
accoppiato** dagli OHLC giornalieri HL (4 checkpoint, ordine condiviso avverso; recon = sleeve
|
||||||
|
ufficiale a `max|Δ|=0.0`).
|
||||||
|
⚠️ **IL FINDING: la CALIBRAZIONE del wick era giusta, l'errore era l'INDIPENDENZA.** Le marginali
|
||||||
|
coincidono quasi (p50 −0.17pp **identico**, p90 −1.03 vs −0.90, p99 −2.70 vs −3.50) — sbagliava
|
||||||
|
**su quali giorni** cadono i tuffi. E la dipendenza va nel **verso inatteso**: il gap e' ~**3× piu'
|
||||||
|
profondo nei giorni che finiscono BENE** (−1.58pp nel decile migliore vs −0.48pp nel peggiore),
|
||||||
|
perche' un giorno brutto scende tutto il giorno e **chiude sul minimo** (`m==R` nel **26%** dei
|
||||||
|
giorni). Il breach si valuta sul minimo → l'estrazione indipendente carica i giorni brutti con la
|
||||||
|
coda dei giorni buoni e **raddoppia i breach da daily-loss (2.0-2.9×, misurato sui giorni storici)**.
|
||||||
|
**`close-only` non e' conservativa, e' CIECA**: 0.00% di breach da daily-loss su OGNI configurazione
|
||||||
|
(la regola non scatta mai sulle chiusure) → usarla come controllo, mai come stima.
|
||||||
|
**Verifiche (un "nessuna differenza" va provato):** (1) **1h vs 5m** sulla gamba TP01 = identici
|
||||||
|
(p99 −3.06 vs −3.14pp) → **il caveat di risoluzione portato avanti dal 24/07 e' quantificato e
|
||||||
|
trascurabile**, l'ora cattura gia' il minimo del giorno; (2) riconciliazione col 24/07 (sopra);
|
||||||
|
(3) **bound severo su XS01** (ogni gamba al proprio peggio insieme): config B resta sopra config A
|
||||||
|
(P(vivo) 57.6% vs 39.4%) → la conclusione non dipende dalla convenzione.
|
||||||
|
**Decisioni:** funded **0.75x** (non 0.50x, vedi sopra); scala di conti **P(≥50/g) ~5.6-6.5%** in
|
||||||
|
3 anni (non 1.6-2.5% ne' 20.7%) con **P(zero) 52% a 0.75x / 65% a 1.00x**, e **P(≥10/g) massima a
|
||||||
|
0.75x (29.7%)** = stesso ottimo di leva del conto singolo. **Ordine fra politiche INVARIATO a
|
||||||
|
tutte e 3 le lenti.** Verdetto ristretto: **€600→€50/g ≈ 6% in 3 anni, P(perdere i €600) ≈ 52-65%**
|
||||||
|
— resta coda destra, ma con probabilita' conoscibile invece di una banda di un ordine di grandezza.
|
||||||
|
**REGOLA NUOVA: un modello di rischio intraday NON si valida sulla marginale del wick ma sul suo
|
||||||
|
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.
|
||||||
- **Soffitto strutturale BTC/ETH-direzionale ~1.3** superato SOLO espandendo a un meccanismo diverso:
|
- **Soffitto strutturale BTC/ETH-direzionale ~1.3** superato SOLO espandendo a un meccanismo diverso:
|
||||||
cross-sectional su universo Hyperliquid certificato (XS01) → portafoglio Sharpe ~1.55.
|
cross-sectional su universo Hyperliquid certificato (XS01) → portafoglio Sharpe ~1.55.
|
||||||
- **Sweep "strategie alternative" (2026-06-20) — 104 ipotesi / 153 agenti / NIENTE di nuovo regge.**
|
- **Sweep "strategie alternative" (2026-06-20) — 104 ipotesi / 153 agenti / NIENTE di nuovo regge.**
|
||||||
|
|||||||
@@ -0,0 +1,234 @@
|
|||||||
|
# 2026-07-25 — La lente wick ACCOPPIATA: chiusura del follow-up dichiarato
|
||||||
|
|
||||||
|
**Script:** `scripts/research/r0725_prop_coupled.py` · **Test:** `tests/test_prop_coupled.py` (13 casi)
|
||||||
|
**Segue:** `2026-07-25-rendita-capcurve-prop.md` §5 (scala prop) e §7 (HyroTrader)
|
||||||
|
**Book / pesi / cron: INVARIATI.** Nessuna decisione di portafoglio dipende da questo lavoro.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Cosa era rimasto aperto, e perché contava
|
||||||
|
|
||||||
|
La scala di conti funded (§5 del diario di stamattina) era stata giudicata con **due lenti** che
|
||||||
|
differiscono di un **ordine di grandezza** su P(≥50 €/g in 3 anni):
|
||||||
|
|
||||||
|
| lente | P(≥50/g) | com'era descritta |
|
||||||
|
|---|---|---|
|
||||||
|
| close-only | **20.7%** | tetto ottimista — le regole prop scattano sull'equity intraday, non sulla chiusura |
|
||||||
|
| wick indipendente | **1.6-2.5%** | pavimento pessimista — gap lognormale estratto **indipendente** dal rendimento del giorno |
|
||||||
|
|
||||||
|
Una banda 1.6%-20.7% non decide niente: è la differenza fra "scommessa a coda destra che vale i
|
||||||
|
600 euro" e "non vale la pena". E il difetto del pavimento era **già dichiarato** nel docstring:
|
||||||
|
|
||||||
|
> *«nella realtà i wick profondi stanno sulle giornate brutte (sono ACCOPPIATI): estrarli
|
||||||
|
> indipendenti aggiunge finti tuffi anche nelle giornate buone → breach spuri → stima troppo
|
||||||
|
> severa. Il modo giusto è il bootstrap delle TUPLE (ritorno, gap) come in
|
||||||
|
> r0724_goal50_intraday_mc.py, che però ha il recon solo per TP01+SKH01 → FOLLOW-UP dichiarato.»*
|
||||||
|
|
||||||
|
Questo lavoro chiude quel follow-up.
|
||||||
|
|
||||||
|
## 2. Metodo
|
||||||
|
|
||||||
|
Tre pezzi, tutti verificati contro qualcosa di indipendente:
|
||||||
|
|
||||||
|
1. **Recon per-sleeve a risoluzione oraria.** `r0724` calcola il minimo intraday del book Deribit
|
||||||
|
con i pesi 75/25 **cablati dentro**. Qui i pesi si dividono via per ottenere gli sleeve nudi
|
||||||
|
(ret, wick) di TP01 e SKH01 — così il minimo si compone **esattamente** sul path orario per
|
||||||
|
**qualsiasi** vettore di pesi. Logica invariata: exit-al-livello, SL prioritario, fee.
|
||||||
|
2. **Lente accoppiata per XS01**, che su Hyperliquid è 1d nativo. L'escursione intraday si
|
||||||
|
ricostruisce dagli **OHLC giornalieri** dei 19 alt: il book si valuta ai 4 checkpoint
|
||||||
|
O / primo estremo / secondo estremo / C, con **ordinamento condiviso** fra le gambe (le alt
|
||||||
|
co-muovono) preso nell'ordine **avverso**. Non è path-exact, ma è **accoppiato al giorno vero**,
|
||||||
|
che è tutto il punto.
|
||||||
|
*Sanity:* il recon riproduce lo sleeve ufficiale con `max|Δ| = 0.0` esatto.
|
||||||
|
3. **Le tre lenti sulla stessa macchina di simulazione** (close / indipendente / accoppiata), così
|
||||||
|
il confronto non mescola differenze di modello con differenze di lente.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Il risultato principale: la calibrazione era giusta, l'**indipendenza** era l'errore
|
||||||
|
|
||||||
|
Confronto delle distribuzioni **marginali** del gap (book live TP01/SKH01, 2691 giorni):
|
||||||
|
|
||||||
|
| | p50 | p90 | p99 | peggiore |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| **accoppiato (recon)** | −0.17pp | −1.03pp | −2.70pp | −5.96pp |
|
||||||
|
| indipendente (calibrazione 25/07) | −0.17pp | −0.90pp | −3.50pp | — |
|
||||||
|
|
||||||
|
**Sono quasi identiche.** La calibrazione lognormale di stamattina era buona. Quello che era
|
||||||
|
sbagliato non è *quanto* sono profondi i tuffi, ma **su quali giorni cadono**.
|
||||||
|
|
||||||
|
### 3.1 E la dipendenza va nel verso inatteso
|
||||||
|
|
||||||
|
Mi aspettavo — e lo avevo scritto — che i gap profondi stessero sui giorni brutti. **È il
|
||||||
|
contrario:**
|
||||||
|
|
||||||
|
| book | giorni | m==R | gap medio | gap \| decile PEGGIORE | gap \| decile MIGLIORE |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| A TP01/SKH01 (live) | 2691 | 26% | −0.38pp | **−0.48pp** | **−1.58pp** |
|
||||||
|
| B +XS01 (diversif.) | 937 | 20% | −0.36pp | −0.39pp | −1.30pp |
|
||||||
|
| TP01 solo | 2691 | 33% | −0.39pp | −0.59pp | −1.57pp |
|
||||||
|
| SKH01 solo | 2691 | 76% | −0.39pp | −0.40pp | −2.97pp |
|
||||||
|
| XS01 solo | 937 | 53% | −0.44pp | −0.22pp | −2.32pp |
|
||||||
|
|
||||||
|
L'escursione intraday è **~3× più profonda nei giorni che finiscono BENE** che in quelli che
|
||||||
|
finiscono male. Il meccanismo, una volta visto, è ovvio: un giorno brutto **scende tutto il
|
||||||
|
giorno e chiude sul minimo** (`m == R` nel 26% dei giorni: la chiusura *è* il peggio), mentre
|
||||||
|
un recupero a V ha un minimo profondo e una chiusura alta.
|
||||||
|
|
||||||
|
Il breach però si valuta **sul minimo**. Quindi ciò che conta è quanto gap si aggiunge **nei
|
||||||
|
giorni brutti** — e lì il gap vero è ~1/3 di quello dei giorni buoni. Un'estrazione indipendente
|
||||||
|
usa la stessa distribuzione ovunque, e così **carica i giorni brutti con la coda che nella realtà
|
||||||
|
appartiene ai giorni buoni**.
|
||||||
|
|
||||||
|
### 3.2 Quanto costa, misurato sui giorni storici (nessun bootstrap)
|
||||||
|
|
||||||
|
Frazione di giorni che sfondano la regola di daily-loss:
|
||||||
|
|
||||||
|
| book | leva | regola | close-only | **ACCOPPIATO** | indipendente | gonfiaggio |
|
||||||
|
|---|---|---|---|---|---|---|
|
||||||
|
| A (live) | 0.75 | daily 4% (HYRO) | 0.00% | 0.21% | 0.40% | **1.9×** |
|
||||||
|
| A (live) | 1.00 | daily 4% (HYRO) | 0.00% | 0.32% | 0.78% | **2.4×** |
|
||||||
|
| B (+XS01) | 0.75 | daily 4% (HYRO) | 0.00% | 0.11% | 0.25% | **2.4×** |
|
||||||
|
| B (+XS01) | 1.00 | daily 5% (FTMO) | 0.00% | 0.11% | 0.31% | **2.9×** |
|
||||||
|
|
||||||
|
Due letture:
|
||||||
|
- la lente indipendente **raddoppia** i breach da daily-loss (2.0-2.9×);
|
||||||
|
- **close-only non è "leggermente ottimista": è strutturalmente cieca.** Fa `0.00%` su ogni riga —
|
||||||
|
la regola di daily-loss **non scatta mai** sulle chiusure. Non è una lente approssimata, è una
|
||||||
|
lente che non vede affatto quel vincolo.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Tre verifiche — perché un "nessuna differenza" va provato, non assunto
|
||||||
|
|
||||||
|
### (1) Risoluzione: l'1h basta? — **sì, misurato**
|
||||||
|
|
||||||
|
TP01 tiene peso costante nel giorno, quindi il suo minimo intraday si può calcolare **anche a 5m**
|
||||||
|
ed è un confronto pulito:
|
||||||
|
|
||||||
|
| | p50 | p90 | p99 | peggiore |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| gamba TP01 @1h | −0.337pp | −1.358pp | −3.056pp | −5.34pp |
|
||||||
|
| gamba TP01 @5m | −0.336pp | −1.366pp | −3.136pp | −5.33pp |
|
||||||
|
|
||||||
|
Identici. **Il caveat "i wick a 1h sono un tetto, i 5m sarebbero più profondi" — portato avanti dal
|
||||||
|
24/07 — è quantificato e trascurabile.** L'ora cattura già il minimo del giorno: i 5m aggiungono
|
||||||
|
solo rumore dentro un'ora di minimo già individuata.
|
||||||
|
|
||||||
|
*Nota di metodo:* entrambe le righe riportavano `n=1865` e la cosa sembrava un errore (stessa
|
||||||
|
serie confrontata due volte). Non lo era: il filtro scarta i giorni in cui TP01 è **flat** (~30%),
|
||||||
|
che sono gli stessi a qualsiasi risoluzione. Verificato prima di accettare il risultato.
|
||||||
|
|
||||||
|
### (2) Riconciliazione col 24/07 — **la discrepanza dichiarata è risolta**
|
||||||
|
|
||||||
|
CLAUDE.md portava un ⚠ aperto: *«Discrepanza col 24/07 (P(vivo) A@0.75x 58% vs 10% mio)»*, con
|
||||||
|
l'ipotesi che fosse un effetto-finestra. **Non era la finestra, era la lente:**
|
||||||
|
|
||||||
|
| finestra | lente | P(pass) | P(vivo 1a) | E[payout/a] |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| 2019-03+ FULL | close | 53.8% | 79.7% | $2.085 |
|
||||||
|
| **2019-03+ FULL** | **accoppiato** | 48.1% | **63.2%** | $1.618 |
|
||||||
|
| 2024+ comune | close | 47.0% | 74.8% | $1.632 |
|
||||||
|
| 2024+ comune | accoppiato | 35.6% | 39.4% | $770 |
|
||||||
|
|
||||||
|
Sulla **stessa finestra piena** del 24/07 (recon MTM indipendente, che dava **58%**) la mia lente
|
||||||
|
accoppiata dà **63.2%** — accordo entro 5pp, con due implementazioni scritte separatamente. Il
|
||||||
|
**10%** del 25/07 era l'artefatto della lente indipendente. La finestra spiega il resto
|
||||||
|
(63.2% full → 39.4% sul 2024+, dove config A è debole).
|
||||||
|
|
||||||
|
### (3) Bound severo su XS01 — **non ribalta niente**
|
||||||
|
|
||||||
|
È XS01 a reggere il vantaggio della config diversificata, e il suo gap dipende da una convenzione
|
||||||
|
(ordinamento condiviso). Girato anche il bound opposto — ogni gamba al **proprio** peggio nello
|
||||||
|
stesso istante, fisicamente impossibile su 10 gambe co-moventi ma utile a delimitare l'errore:
|
||||||
|
|
||||||
|
| convenzione | gap p90 | peggiore | P(pass) | P(vivo 1a) | E[payout/a] |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| ordine condiviso (base) | −0.86pp | −4.81pp | 42.9% | **76.0%** | $1.144 |
|
||||||
|
| per-gamba (bound severo) | −1.83pp | −15.56pp | 32.3% | **57.6%** | $814 |
|
||||||
|
| *config A, per confronto* | — | — | 35.6% | **39.4%** | $770 |
|
||||||
|
|
||||||
|
Anche al bound severo **config B resta sopra config A** (57.6% vs 39.4%, $814 vs $770). La
|
||||||
|
conclusione non dipende dalla convenzione.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Cosa cambia nelle decisioni
|
||||||
|
|
||||||
|
### 5.1 La raccomandazione HyroTrader si sposta da 0.50x a **0.75x**
|
||||||
|
|
||||||
|
Config B (book diversificato, +XS01), lente accoppiata, finestra 2024+:
|
||||||
|
|
||||||
|
| leva | P(pass eval) | P(vivo 1a) | E[payout/a] | **E[payout] su 3 anni** |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| 0.50x | 28.6% | 99.5% | $412 | ~$1.230 |
|
||||||
|
| **0.75x** | 42.9% | 76.0% | $1.144 | **~$2.674** |
|
||||||
|
| 1.00x | 46.6% | 42.1% | $1.246 | ~$1.992 |
|
||||||
|
|
||||||
|
Stamattina la scelta era **0.50x**, perché la lente indipendente dava a 0.75x una sopravvivenza
|
||||||
|
del 55%. Con la lente onesta la sopravvivenza a 0.75x è **76%**, e 0.75x **massimizza il payout
|
||||||
|
atteso su 3 anni** — batte 0.50x di 2,2× e 1.00x di 1,3× (a 1.00x la sopravvivenza crolla e
|
||||||
|
l'effetto composto la punisce). L'argomento del 25/07 — *"la sopravvivenza compone su più anni"* —
|
||||||
|
resta valido; è il punto in cui morde che si sposta.
|
||||||
|
|
||||||
|
### 5.2 La scala di conti: il ranking regge, i livelli no
|
||||||
|
|
||||||
|
| lente | leva | MISTO EUR/g med | P(≥10/g) | P(≥50/g) | P(zero) |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| close-only | 1.00x | 11.11 | 50.8% | **22.0%** | 33.9% |
|
||||||
|
| **accoppiata** | **0.75x** | 0.00 | **29.7%** | 5.6% | **52.2%** |
|
||||||
|
| **accoppiata** | **1.00x** | 0.00 | 20.8% | **6.5%** | 65.5% |
|
||||||
|
| indipendente | 1.00x | 0.00 | 5.2% | 1.0% | 81.5% |
|
||||||
|
|
||||||
|
- **L'ordinamento fra politiche SOPRAVVIVE a tutte e tre le lenti**: MISTO ≥ CONC-DIV >
|
||||||
|
CONC-2SL ≈ SPARSO. L'affermazione di stamattina — *«il confronto fra POLITICHE è robusto:
|
||||||
|
è la stessa lente applicata a tutte»* — è confermata su una lente che allora non esisteva.
|
||||||
|
**CONC-2SL (= il book live attuale su ogni conto) resta la peggiore o penultima ovunque.**
|
||||||
|
- **I livelli si spostano parecchio.** La stima onesta di P(≥50/g) in 3 anni è **~5,6-6,5%**, non
|
||||||
|
1,6-2,5% e non 20,7%. La scala è **~3-4× più probabile** di quanto diceva il pavimento e
|
||||||
|
**~3,4× meno** di quanto diceva il tetto.
|
||||||
|
- **P(≥10/g) è massima a 0.75x (29,7%)**, non alla leva piena: l'ottimo di leva della scala
|
||||||
|
coincide con quello del conto singolo.
|
||||||
|
- Il prezzo resta alto: **P(bruciare i €600) = 52% a 0.75x, 65% a 1.00x.**
|
||||||
|
|
||||||
|
### 5.3 Il verdetto sul goal, ristretto
|
||||||
|
|
||||||
|
Il 25/07 concludeva: *«una via da €600 a €50/g ESISTE con P ~2-21% in 3 anni e P(bruciare i €600)
|
||||||
|
~33-78% → scommessa a coda destra, NON una rendita»*. Con la lente onesta la banda si stringe:
|
||||||
|
|
||||||
|
> **P(≥50 €/g entro 3 anni) ≈ 6%, con P(perdere i €600) ≈ 52-65%.**
|
||||||
|
> Resta una scommessa a coda destra. Ma è una scommessa con una probabilità **conoscibile**,
|
||||||
|
> non un intervallo di un ordine di grandezza.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Cosa NON cambia
|
||||||
|
|
||||||
|
- **Book, pesi, cron: invariati.** Niente qui tocca il portafoglio.
|
||||||
|
- Il fronte prop **non è aperto**: questo lavoro dice *cosa costerebbe e con quale probabilità*,
|
||||||
|
non che vada fatto.
|
||||||
|
- Resta il caveat di finestra del 25/07: il **2024+** è la finestra in cui XS01 è stato scoperto e
|
||||||
|
affinato → la **taglia** del vantaggio di config B è ottimista; il **meccanismo** (correlazione
|
||||||
|
bassa → meno DD → più sopravvivenza sotto vincolo di DD) è robusto. La verifica (2) qui sopra
|
||||||
|
quantifica per la prima volta quanto pesa la finestra da sola (63.2% → 39.4% su config A).
|
||||||
|
- XS01 resta **STAT-MODE sul capitale proprio** (serve ~$20k). Su un conto funded da $100k è
|
||||||
|
eseguibile: è esattamente l'asimmetria capitale-proprio vs funded del §4 di stamattina.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Lezione da portare avanti
|
||||||
|
|
||||||
|
> **Un modello di rischio intraday non si valida sulla distribuzione marginale del wick, ma sul suo
|
||||||
|
> ACCOPPIAMENTO al rendimento del giorno.** Qui la marginale era calibrata quasi perfettamente
|
||||||
|
> (p50 −0.17pp identico) e ciò nonostante il modello sbagliava di 2-3× sui breach, perché metteva
|
||||||
|
> i tuffi sui giorni sbagliati. Un test "i percentili del mio wick sintetico coincidono con quelli
|
||||||
|
> del recon" sarebbe **passato**, e la stima sarebbe rimasta sbagliata.
|
||||||
|
|
||||||
|
Corollario controintuitivo, da ricordare perché è il contrario dell'intuizione: **l'escursione
|
||||||
|
intraday è più profonda nei giorni che chiudono bene**. I giorni che chiudono male chiudono *sul*
|
||||||
|
minimo. Ogni regola valutata sul minimo (daily-loss, trailing DD, stop di conto) va misurata su
|
||||||
|
tuple accoppiate, mai su un wick estratto a parte.
|
||||||
|
|
||||||
|
Terzo: **close-only non è una lente conservativa, è una lente cieca** per le regole intraday —
|
||||||
|
0.00% di breach da daily-loss su ogni configurazione testata. Va usata come controllo, mai come
|
||||||
|
stima.
|
||||||
@@ -0,0 +1,693 @@
|
|||||||
|
"""r0725_prop_coupled — CHIUDE il follow-up dichiarato: la lente WICK ACCOPPIATA (2026-07-25).
|
||||||
|
|
||||||
|
`r0725_prop_ladder.py` ha giudicato la scala di conti funded con DUE lenti che differiscono di
|
||||||
|
un ordine di grandezza su P(>=50 EUR/g): **20.7% close-only** (tetto) vs **1.6-2.5% wick
|
||||||
|
indipendente** (pavimento). La banda e' cosi' larga da non decidere nulla: e' la differenza fra
|
||||||
|
"scommessa a coda destra che vale 600 euro" e "non vale la pena".
|
||||||
|
|
||||||
|
La ragione della larghezza e' un DIFETTO DICHIARATO del pavimento: il gap intraday era estratto
|
||||||
|
**lognormale INDIPENDENTE dal rendimento del giorno**. Ma i wick profondi NON sono indipendenti:
|
||||||
|
stanno sulle giornate brutte. Estrarli indipendenti aggiunge finti tuffi anche nelle giornate
|
||||||
|
buone -> breach spuri -> P(pass) e P(vivo) troppo basse. Il modo giusto e' il bootstrap delle
|
||||||
|
**TUPLE (ritorno, gap)** accoppiate, che r0724_goal50_intraday_mc.py fa — ma solo per il book
|
||||||
|
Deribit 75/25 (TP01+SKH01), non per XS01 ne' a pesi arbitrari. Qui si generalizza.
|
||||||
|
|
||||||
|
COSA FA QUESTO SCRIPT
|
||||||
|
1) **Recon per-sleeve** a risoluzione oraria: (ret, wick) unitari di TP01 e SKH01 separati,
|
||||||
|
cosi' il minimo intraday si puo' calcolare esattamente per QUALSIASI vettore di pesi
|
||||||
|
(non solo 75/25). Riusa la macchina di r0724 (exit-al-livello, SL prioritario, fee).
|
||||||
|
2) **Lente accoppiata per XS01**, che su Hyperliquid e' 1d nativo: l'escursione intraday si
|
||||||
|
ricostruisce dagli OHLC GIORNALIERI dei 19 alt con ordinamento condiviso (tutte le gambe
|
||||||
|
toccano prima il minimo, o prima il massimo — le alt co-muovono), prendendo l'ordine
|
||||||
|
AVVERSO. Non e' path-exact, ma e' **accoppiato al giorno vero**, che e' il punto.
|
||||||
|
3) **Prova empirica del difetto**: gap per decile di rendimento + conteggio dei breach sui
|
||||||
|
giorni STORICI (nessun bootstrap) sotto le tre lenti.
|
||||||
|
4) Le 4 politiche di scala e la decisione HyroTrader config A vs B, girate a **TRE lenti**
|
||||||
|
(close-only / indipendente / accoppiata) sulla stessa macchina di simulazione.
|
||||||
|
5) Tre VERIFICHE, perche' un "nessuna differenza" va provato: risoluzione 1h vs 5m,
|
||||||
|
riconciliazione col numero indipendente del 24/07, bound severo su XS01.
|
||||||
|
|
||||||
|
RISULTATO IN UNA RIGA: la CALIBRAZIONE del wick del 25/07 era giusta (p50 -0.17pp identico),
|
||||||
|
l'errore era l'INDIPENDENZA. L'escursione intraday e' ~3x piu' profonda nei giorni che finiscono
|
||||||
|
BENE (recuperi a V) che in quelli che finiscono male (che chiudono sul minimo: m==R nel 26% dei
|
||||||
|
giorni). Il breach si valuta sul minimo -> estrarre il gap indipendente carica i giorni brutti
|
||||||
|
con la coda che appartiene ai giorni buoni, e RADDOPPIA i breach da daily-loss (2.0-2.9x).
|
||||||
|
|
||||||
|
CONVENZIONI DICHIARATE (le stesse del 24/07, piu' una nuova)
|
||||||
|
* il minimo di un book multi-sleeve e' preso come **somma dei minimi** (worst simultaneo) fra
|
||||||
|
la parte crypto e XS01: severo, ma coerente con la convenzione gia' in uso fra TP01 e SKH01
|
||||||
|
dentro l'ora. Fra TP01 e SKH01 il minimo e' invece ESATTO (path orario condiviso).
|
||||||
|
* wick a risoluzione 1h sul crypto. ⚠️ NON e' un tetto di risoluzione: MISURATO contro i 5m
|
||||||
|
sulla gamba TP01, p99 -3.06pp (1h) vs -3.14pp (5m) -> l'ora cattura gia' il minimo del
|
||||||
|
giorno. (Il caveat "i 5m sarebbero piu' profondi" del 24/07 e' quantificato e trascurabile.)
|
||||||
|
* XS01 a risoluzione di barra giornaliera (4 checkpoint O/H/L/C, ordine condiviso avverso);
|
||||||
|
il bound opposto (ogni gamba al proprio peggio insieme) e' girato come sensitivita' e NON
|
||||||
|
ribalta nessuna conclusione.
|
||||||
|
|
||||||
|
Uso: `uv run python scripts/research/r0725_prop_coupled.py` (~2-3 min: recon SKH su 5m full)
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import sys
|
||||||
|
from functools import lru_cache
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[2]
|
||||||
|
sys.path.insert(0, str(ROOT))
|
||||||
|
sys.path.insert(0, str(ROOT / "scripts" / "research"))
|
||||||
|
|
||||||
|
from src.data.downloader import load_data # noqa: E402
|
||||||
|
from src.portfolio.portfolio import metrics # noqa: E402
|
||||||
|
from src.portfolio.sleeves import XS_CFG, XS_UNIVERSE, _HL_DIR, _xsec_returns # noqa: E402
|
||||||
|
|
||||||
|
import r0724_goal50_intraday_mc as mc # noqa: E402
|
||||||
|
import r0725_prop_ladder as pl # noqa: E402
|
||||||
|
from r0725_capcurve import EURUSD, TAX_RATE # noqa: E402
|
||||||
|
|
||||||
|
ASSETS = ("BTC", "ETH")
|
||||||
|
DELUCK = pl.DELUCK
|
||||||
|
START_EUR = pl.START_EUR
|
||||||
|
HORIZON_MONTHS = pl.HORIZON_MONTHS
|
||||||
|
MAX_CONCURRENT = pl.MAX_CONCURRENT
|
||||||
|
TARGET_EUR_DAY = pl.TARGET_EUR_DAY
|
||||||
|
SPLIT = pl.SPLIT
|
||||||
|
FIRM_DEATH_PER_YEAR = pl.FIRM_DEATH_PER_YEAR
|
||||||
|
FIRMS = pl.FIRMS
|
||||||
|
|
||||||
|
|
||||||
|
# ================================================================= 1. RECON PER-SLEEVE (1h)
|
||||||
|
|
||||||
|
@lru_cache(maxsize=1)
|
||||||
|
def _hourly_legs() -> tuple[pd.DataFrame, ...]:
|
||||||
|
"""(ret, wick) ORARI UNITARI di TP01 e SKH01 — cioe' a peso di sleeve 1.0, gia' sommati
|
||||||
|
sui due asset (ogni asset pesa 0.5 dentro lo sleeve).
|
||||||
|
|
||||||
|
r0724 cabla i pesi di book (W_TP=0.75, W_SKH=0.25) dentro il recon; qui si dividono via
|
||||||
|
per ottenere gli sleeve nudi, che si possono poi ricombinare a qualsiasi peso.
|
||||||
|
"""
|
||||||
|
tp_r = tp_w = skh_r = skh_w = None
|
||||||
|
for a in ASSETS:
|
||||||
|
df = load_data(a, "1h")
|
||||||
|
idx = pd.DatetimeIndex(pd.to_datetime(df["datetime"], utc=True))
|
||||||
|
close = df["close"].values.astype(float)
|
||||||
|
low = df["low"].values.astype(float)
|
||||||
|
high = df["high"].values.astype(float)
|
||||||
|
prev = np.concatenate(([np.nan], close[:-1]))
|
||||||
|
r_close = close / prev - 1.0
|
||||||
|
|
||||||
|
# --- TP01: peso costante nel giorno (deciso a close[d-1]), fee alla prima ora
|
||||||
|
tpf = mc.tp01_daily_frame(a) # gia' scalato *0.5 (leg), senza W_TP
|
||||||
|
day_of = idx.floor("D")
|
||||||
|
w_tp = np.nan_to_num(tpf["w"].reindex(day_of).values)
|
||||||
|
fee_tp = np.nan_to_num(tpf["fee"].reindex(day_of).values)
|
||||||
|
first = np.concatenate(([True], day_of[1:] != day_of[:-1]))
|
||||||
|
r_wick = np.where(w_tp >= 0, low / prev - 1.0, high / prev - 1.0)
|
||||||
|
t_r = pd.Series(w_tp * r_close - np.where(first, fee_tp, 0.0), index=idx)
|
||||||
|
t_w = pd.Series(w_tp * r_wick - np.where(first, fee_tp, 0.0), index=idx)
|
||||||
|
|
||||||
|
# --- SKH01: MTM per-trade con exit AL LIVELLO (r0724 cabla w = W_SKH*0.5)
|
||||||
|
s_r_arr, s_w_arr = mc.skh_hourly_contrib(a, idx, close, low, high)
|
||||||
|
s_r = pd.Series(s_r_arr / mc.W_SKH, index=idx)
|
||||||
|
s_w = pd.Series(s_w_arr / mc.W_SKH, index=idx)
|
||||||
|
|
||||||
|
tp_r = t_r if tp_r is None else tp_r.add(t_r, fill_value=np.nan)
|
||||||
|
tp_w = t_w if tp_w is None else tp_w.add(t_w, fill_value=np.nan)
|
||||||
|
skh_r = s_r if skh_r is None else skh_r.add(s_r, fill_value=np.nan)
|
||||||
|
skh_w = s_w if skh_w is None else skh_w.add(s_w, fill_value=np.nan)
|
||||||
|
|
||||||
|
J = pd.concat({"tp_r": tp_r, "tp_w": tp_w, "skh_r": skh_r, "skh_w": skh_w},
|
||||||
|
axis=1).dropna()
|
||||||
|
return (J,)
|
||||||
|
|
||||||
|
|
||||||
|
def crypto_daily_tuples(w_tp: float, w_skh: float) -> pd.DataFrame:
|
||||||
|
"""Per giorno UTC: R (chiusura) e m (minimo intraday) del sub-book crypto a pesi arbitrari.
|
||||||
|
|
||||||
|
Il minimo fra TP01 e SKH01 e' ESATTO: si compone il path ORARIO del book e si guarda il
|
||||||
|
minimo, includendo il wick dentro ogni ora. (Il wick intra-ora dei due sleeve e' sommato:
|
||||||
|
li' resta la convenzione worst-simultaneo di r0724.)
|
||||||
|
"""
|
||||||
|
(J,) = _hourly_legs()
|
||||||
|
r_h = (w_tp * J["tp_r"] + w_skh * J["skh_r"]).values
|
||||||
|
w_h = (w_tp * J["tp_w"] + w_skh * J["skh_w"]).values
|
||||||
|
idx = J.index
|
||||||
|
days = idx.floor("D")
|
||||||
|
rows = []
|
||||||
|
for day, pos in pd.Series(np.arange(len(idx)), index=days).groupby(level=0):
|
||||||
|
sl = pos.values
|
||||||
|
r = r_h[sl]
|
||||||
|
wk = w_h[sl]
|
||||||
|
cum = np.cumprod(1.0 + r)
|
||||||
|
cum_prev = np.concatenate(([1.0], cum[:-1]))
|
||||||
|
m = min(cum.min(), (cum_prev * (1.0 + wk)).min()) - 1.0
|
||||||
|
rows.append((day, cum[-1] - 1.0, min(m, cum[-1] - 1.0)))
|
||||||
|
return pd.DataFrame(rows, columns=["day", "R", "m"]).set_index("day")
|
||||||
|
|
||||||
|
|
||||||
|
# ================================================================= 2. XS01 ACCOPPIATO (1d OHLC)
|
||||||
|
|
||||||
|
@lru_cache(maxsize=2)
|
||||||
|
def xsec_daily_tuples(wick_mode: str = "shared") -> pd.DataFrame:
|
||||||
|
"""XS01 con escursione intraday ricostruita dagli OHLC GIORNALIERI dei 19 alt.
|
||||||
|
|
||||||
|
Replica la costruzione delle posizioni di `sleeves._xsec_returns` (verificata a valle contro
|
||||||
|
lo sleeve ufficiale: `assert` sulla chiusura) e in piu', per ogni giorno, valuta il book ai
|
||||||
|
4 checkpoint O / primo estremo / secondo estremo / C, con **ordinamento CONDIVISO** fra le
|
||||||
|
gambe (le alt co-muovono) e prendendo l'**ordine avverso** fra "prima i minimi" e "prima i
|
||||||
|
massimi". Su un book market-neutral l'ordinamento condiviso si auto-compensa in larga parte
|
||||||
|
(i long perdono, gli short guadagnano): il rischio intraday vero e' la DISPERSIONE, che
|
||||||
|
questa lente cattura solo in parte -> il gap XS01 esce PICCOLO, e va letto come tale.
|
||||||
|
|
||||||
|
wick_mode='perleg' e' il BOUND SEVERO opposto: ogni gamba tocca il PROPRIO peggio nello stesso
|
||||||
|
istante (long al minimo E short al massimo). E' impossibile in pratica su 10 gambe co-moventi,
|
||||||
|
ma delimita l'errore: se la conclusione regge a entrambi gli estremi, non dipende dalla
|
||||||
|
convenzione. Serve perche' e' XS01 a reggere il vantaggio della config diversificata.
|
||||||
|
"""
|
||||||
|
o = {}; h = {}; lo = {}; c = {}
|
||||||
|
for sym in XS_UNIVERSE:
|
||||||
|
p = _HL_DIR / f"hl_{sym.lower()}_1d.parquet"
|
||||||
|
if not p.exists():
|
||||||
|
continue
|
||||||
|
d = pd.read_parquet(p)
|
||||||
|
i = pd.to_datetime(d["timestamp"], unit="ms", utc=True)
|
||||||
|
o[sym] = pd.Series(d["open"].astype(float).values, index=i)
|
||||||
|
h[sym] = pd.Series(d["high"].astype(float).values, index=i)
|
||||||
|
lo[sym] = pd.Series(d["low"].astype(float).values, index=i)
|
||||||
|
c[sym] = pd.Series(d["close"].astype(float).values, index=i)
|
||||||
|
C = pd.concat(c, axis=1, join="inner").sort_index().dropna()
|
||||||
|
O = pd.concat(o, axis=1).reindex(C.index)[C.columns]
|
||||||
|
H = pd.concat(h, axis=1).reindex(C.index)[C.columns]
|
||||||
|
L = pd.concat(lo, axis=1).reindex(C.index)[C.columns]
|
||||||
|
|
||||||
|
px = C.values
|
||||||
|
n, A = px.shape
|
||||||
|
lookbacks, Hh, k, mode, tv = (XS_CFG["lookbacks"], XS_CFG["H"], XS_CFG["k"],
|
||||||
|
XS_CFG["mode"], XS_CFG["target_vol"])
|
||||||
|
disp_pct, minhist = XS_CFG.get("disp_pct", 0), XS_CFG.get("disp_minhist", 20)
|
||||||
|
mlb = max(lookbacks)
|
||||||
|
dret = np.vstack([np.zeros(A), px[1:] / px[:-1] - 1.0])
|
||||||
|
W = np.zeros((n, A)); w = np.zeros(A); disp_hist = []
|
||||||
|
for i in range(n):
|
||||||
|
if i >= mlb and i % Hh == 0:
|
||||||
|
rLs = [px[i] / px[i - Lk] - 1.0 for Lk in lookbacks]
|
||||||
|
disp_i = float(np.mean([r.std() for r in rLs]))
|
||||||
|
thr = (np.percentile(disp_hist, disp_pct)
|
||||||
|
if (disp_pct > 0 and len(disp_hist) >= minhist) else -np.inf)
|
||||||
|
if disp_i >= thr:
|
||||||
|
score = np.zeros(A); cnt = 0
|
||||||
|
for rL in rLs:
|
||||||
|
sd = rL.std()
|
||||||
|
if sd > 0:
|
||||||
|
score += (rL - rL.mean()) / sd; cnt += 1
|
||||||
|
if cnt:
|
||||||
|
score /= cnt
|
||||||
|
order = np.argsort(score)
|
||||||
|
w = np.zeros(A); low_i, hi_i = order[:k], order[-k:]
|
||||||
|
if mode == "mom":
|
||||||
|
w[hi_i] = 0.5 / k; w[low_i] = -0.5 / k
|
||||||
|
else:
|
||||||
|
w[low_i] = 0.5 / k; w[hi_i] = -0.5 / k
|
||||||
|
else:
|
||||||
|
w = np.zeros(A)
|
||||||
|
disp_hist.append(disp_i)
|
||||||
|
W[i] = w
|
||||||
|
|
||||||
|
gross = np.zeros(n); gross[1:] = np.sum(W[:-1] * dret[1:], axis=1)
|
||||||
|
turn = np.zeros(n); turn[0] = np.abs(W[0]).sum()
|
||||||
|
turn[1:] = np.abs(np.diff(W, axis=0)).sum(axis=1)
|
||||||
|
net = gross - turn * (0.001 / 2.0)
|
||||||
|
s = pd.Series(net, index=C.index)
|
||||||
|
rv = s.rolling(30, min_periods=15).std().shift(1) * np.sqrt(365.25)
|
||||||
|
scale = np.clip(np.nan_to_num(tv / rv.replace(0, np.nan).values, nan=0.0), 0, 3.0)
|
||||||
|
|
||||||
|
# --- escursione intraday, gambe tenute = W[i-1], prezzo base = close[i-1]
|
||||||
|
prev_c = np.vstack([np.full((1, A), np.nan), px[:-1]])
|
||||||
|
held = np.vstack([np.zeros((1, A)), W[:-1]])
|
||||||
|
with np.errstate(invalid="ignore"):
|
||||||
|
rO = O.values / prev_c - 1.0
|
||||||
|
rH = H.values / prev_c - 1.0
|
||||||
|
rL = L.values / prev_c - 1.0
|
||||||
|
rC = px / prev_c - 1.0
|
||||||
|
vO = np.nansum(held * rO, axis=1)
|
||||||
|
vH = np.nansum(held * rH, axis=1)
|
||||||
|
vL = np.nansum(held * rL, axis=1)
|
||||||
|
vC = np.nansum(held * rC, axis=1)
|
||||||
|
# ordinamento condiviso: low-first (O -> L -> H -> C) oppure high-first (O -> H -> L -> C).
|
||||||
|
# Il book market-neutral vede i due estremi come punti, non come somma di worst per gamba.
|
||||||
|
if wick_mode == "perleg":
|
||||||
|
# bound severo: ogni gamba al proprio peggio simultaneamente
|
||||||
|
worst_leg = np.where(held >= 0, rL, rH)
|
||||||
|
m_gross = np.minimum(np.nansum(held * worst_leg, axis=1), vC)
|
||||||
|
elif wick_mode == "shared":
|
||||||
|
m_lowfirst = np.minimum.reduce([vO, vL, vH, vC])
|
||||||
|
m_highfirst = np.minimum.reduce([vO, vH, vL, vC])
|
||||||
|
m_gross = np.minimum(m_lowfirst, m_highfirst) # ordine AVVERSO
|
||||||
|
else:
|
||||||
|
raise ValueError(wick_mode)
|
||||||
|
# il costo di turnover si paga comunque; il vol-target scala tutto
|
||||||
|
m_net = (m_gross - turn * (0.001 / 2.0)) * scale
|
||||||
|
R_net = net * scale
|
||||||
|
out = pd.DataFrame({"R": R_net, "m": np.minimum(m_net, R_net)}, index=C.index)
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
# ================================================================= 3. PANNELLO A 3 SLEEVE
|
||||||
|
|
||||||
|
def _d(s: pd.Series) -> pd.Series:
|
||||||
|
s = s.dropna().sort_index()
|
||||||
|
if s.index.tz is None:
|
||||||
|
s.index = s.index.tz_localize("UTC")
|
||||||
|
return s
|
||||||
|
|
||||||
|
|
||||||
|
_TUP_CACHE: dict = {}
|
||||||
|
|
||||||
|
|
||||||
|
def book_tuples(w_tp: float, w_skh: float, w_xs: float, start: str | None = None,
|
||||||
|
xs_mode: str = "shared") -> tuple[np.ndarray, np.ndarray, pd.DatetimeIndex]:
|
||||||
|
"""(R, gap) giornalieri di un book a pesi arbitrari. gap = minimo_intraday - chiusura (<=0).
|
||||||
|
|
||||||
|
Crypto: minimo ESATTO sul path orario condiviso di TP01+SKH01. XS01: il suo minimo si SOMMA
|
||||||
|
a quello crypto (worst simultaneo — convenzione dichiarata, severa).
|
||||||
|
La finestra si interseca con XS01 SOLO se XS01 ha peso: a peso zero il book crypto tiene
|
||||||
|
tutta la sua storia (serve per misurare la geometria dei gap del book live).
|
||||||
|
NB: niente round-trip int64 dell'indice — su indici tz-aware non-ns pandas 2.x cambia scala
|
||||||
|
silenziosamente (lezione 2026-07-01). Si cachea il DatetimeIndex cosi' com'e'.
|
||||||
|
"""
|
||||||
|
key = (float(w_tp), float(w_skh), float(w_xs), xs_mode)
|
||||||
|
if key not in _TUP_CACHE:
|
||||||
|
cry = None if (w_tp == 0.0 and w_skh == 0.0) else crypto_daily_tuples(w_tp, w_skh)
|
||||||
|
xs = xsec_daily_tuples(xs_mode)
|
||||||
|
if cry is None:
|
||||||
|
idx, R, m = xs.index, w_xs * xs["R"].values, w_xs * xs["m"].values
|
||||||
|
elif w_xs == 0.0:
|
||||||
|
idx, R, m = cry.index, cry["R"].values, cry["m"].values
|
||||||
|
else:
|
||||||
|
idx = cry.index.intersection(xs.index)
|
||||||
|
cr, xr = cry.reindex(idx), xs.reindex(idx)
|
||||||
|
R = cr["R"].values + w_xs * xr["R"].values
|
||||||
|
m = cr["m"].values + w_xs * xr["m"].values
|
||||||
|
keep = np.isfinite(R) & np.isfinite(m)
|
||||||
|
R, m, idx = R[keep], m[keep], idx[keep]
|
||||||
|
_TUP_CACHE[key] = (R, np.minimum(m, R) - R, idx)
|
||||||
|
R, G, I = _TUP_CACHE[key]
|
||||||
|
if start is not None:
|
||||||
|
k = I >= pd.Timestamp(start, tz="UTC")
|
||||||
|
return R[k], G[k], I[k]
|
||||||
|
return R.copy(), G.copy(), I
|
||||||
|
|
||||||
|
|
||||||
|
# ================================================================= 4. SIMULAZIONE (3 lenti)
|
||||||
|
|
||||||
|
def simulate(cfgs: list[tuple[float, float, float]], slots: list[int], n_paths: int, lev: float,
|
||||||
|
seed: int, lens: str, start: str, block: int = 20) -> dict:
|
||||||
|
"""Scala di conti funded. `cfgs` = vettori di peso distinti; `slots` = indice di cfg per
|
||||||
|
ciascuno dei MAX_CONCURRENT slot. Il bootstrap a blocchi usa lo STESSO indice per tutte le
|
||||||
|
configurazioni -> la correlazione fra conti e' quella REALE (e le tuple (R,gap) restano
|
||||||
|
accoppiate al loro giorno).
|
||||||
|
|
||||||
|
lens: 'close' -> breach sulla chiusura (tetto)
|
||||||
|
'indip' -> gap lognormale indipendente (il pavimento del 25/07)
|
||||||
|
'coupled'-> gap dal recon, accoppiato al giorno (la lente onesta)
|
||||||
|
"""
|
||||||
|
rng = np.random.default_rng(seed)
|
||||||
|
n_days = HORIZON_MONTHS * 30
|
||||||
|
mats = [book_tuples(*c, start=start) for c in cfgs]
|
||||||
|
L = min(len(m[0]) for m in mats)
|
||||||
|
Rm = np.stack([m[0][-L:] for m in mats], axis=1) # (giorni, cfg)
|
||||||
|
Gm = np.stack([m[1][-L:] for m in mats], axis=1)
|
||||||
|
Rm = Rm - (1.0 - DELUCK) * Rm.mean(axis=0, keepdims=True) # de-luck sul drift
|
||||||
|
|
||||||
|
n_blocks = int(np.ceil(n_days / block))
|
||||||
|
starts = rng.integers(0, L - block, size=(n_paths, n_blocks))
|
||||||
|
idx = (starts[:, :, None] + np.arange(block)[None, None, :]).reshape(n_paths, -1)[:, :n_days]
|
||||||
|
|
||||||
|
n_slot = len(slots)
|
||||||
|
sel = np.asarray(slots)
|
||||||
|
Rs = Rm[idx][:, :, sel] * lev # (path, day, slot)
|
||||||
|
if lens == "close":
|
||||||
|
G = np.zeros_like(Rs)
|
||||||
|
elif lens == "coupled":
|
||||||
|
G = Gm[idx][:, :, sel] * lev
|
||||||
|
elif lens == "indip":
|
||||||
|
vol_k = Rs.reshape(-1, n_slot).std(axis=0) * np.sqrt(365.0)
|
||||||
|
scale = np.clip(vol_k / pl.WICK_REF_VOL, 0.0, 3.0)
|
||||||
|
G = -np.exp(rng.normal(pl.WICK_MU, pl.WICK_SIGMA, size=Rs.shape)) * scale[None, None, :]
|
||||||
|
else:
|
||||||
|
raise ValueError(lens)
|
||||||
|
|
||||||
|
cash = np.full(n_paths, START_EUR * EURUSD)
|
||||||
|
phase = np.full((n_paths, n_slot), -1, np.int8)
|
||||||
|
eq = np.ones((n_paths, n_slot))
|
||||||
|
firm = np.full((n_paths, n_slot), -1, np.int8)
|
||||||
|
notional = np.zeros((n_paths, n_slot))
|
||||||
|
firm_alive = np.ones((n_paths, len(FIRMS)), bool)
|
||||||
|
cum_payout = np.zeros(n_paths)
|
||||||
|
spent = np.zeros(n_paths)
|
||||||
|
p_death_day = 1.0 - (1.0 - FIRM_DEATH_PER_YEAR) ** (1 / 365.0)
|
||||||
|
|
||||||
|
for t in range(n_days):
|
||||||
|
died = rng.random((n_paths, len(FIRMS))) < p_death_day
|
||||||
|
newly_dead = died & firm_alive
|
||||||
|
firm_alive &= ~died
|
||||||
|
if newly_dead.any():
|
||||||
|
for fi in range(len(FIRMS)):
|
||||||
|
kill = newly_dead[:, fi]
|
||||||
|
if kill.any():
|
||||||
|
hit = (firm == fi) & kill[:, None]
|
||||||
|
phase[hit] = -1
|
||||||
|
notional[hit] = 0.0
|
||||||
|
|
||||||
|
for k in range(n_slot):
|
||||||
|
free = phase[:, k] == -1
|
||||||
|
if not free.any():
|
||||||
|
continue
|
||||||
|
for fi, F in enumerate(FIRMS):
|
||||||
|
used = np.where(firm == fi, notional, 0.0).sum(axis=1)
|
||||||
|
for (size, price) in sorted(F["tickets"], key=lambda x: -x[0]):
|
||||||
|
buy = (free & firm_alive[:, fi] & (cash >= price)
|
||||||
|
& (used + size <= F["cap"]))
|
||||||
|
if not buy.any():
|
||||||
|
continue
|
||||||
|
cash[buy] -= price
|
||||||
|
spent[buy] += price
|
||||||
|
phase[buy, k] = 0
|
||||||
|
eq[buy, k] = 1.0
|
||||||
|
firm[buy, k] = fi
|
||||||
|
notional[buy, k] = size
|
||||||
|
free = free & ~buy
|
||||||
|
if not free.any():
|
||||||
|
break
|
||||||
|
if not free.any():
|
||||||
|
break
|
||||||
|
|
||||||
|
r = Rs[:, t, :]
|
||||||
|
g = G[:, t, :]
|
||||||
|
act = phase >= 0
|
||||||
|
eq_prev = eq
|
||||||
|
eq = np.where(act, eq * (1.0 + r), eq)
|
||||||
|
eq_low = np.where(act, eq_prev * (1.0 + r + g), eq)
|
||||||
|
r_low = r + g
|
||||||
|
|
||||||
|
for fi, F in enumerate(FIRMS):
|
||||||
|
s_ = act & (firm == fi)
|
||||||
|
if not s_.any():
|
||||||
|
continue
|
||||||
|
ev_t, ev_dd, ev_dl = F["ev"]
|
||||||
|
fu_ml, fu_dl = F["fu"]
|
||||||
|
in_ev = s_ & (phase == 0)
|
||||||
|
in_fu = s_ & (phase == 1)
|
||||||
|
bust = ((in_ev & ((eq_low < 1.0 - ev_dd) | (r_low < -ev_dl)))
|
||||||
|
| (in_fu & ((eq_low < 1.0 - fu_ml) | (r_low < -fu_dl))))
|
||||||
|
phase[bust] = -1
|
||||||
|
notional[bust] = 0.0
|
||||||
|
passed = in_ev & (eq >= 1.0 + ev_t) & ~bust
|
||||||
|
phase[passed] = 1
|
||||||
|
eq[passed] = 1.0
|
||||||
|
|
||||||
|
if (t + 1) % 30 == 0:
|
||||||
|
fu = phase == 1
|
||||||
|
gain = np.where(fu & (eq > 1.0), (eq - 1.0) * notional, 0.0)
|
||||||
|
net = gain.sum(axis=1) * SPLIT * (1.0 - TAX_RATE)
|
||||||
|
cum_payout += net
|
||||||
|
cash += net
|
||||||
|
eq = np.where(fu & (eq > 1.0), 1.0, eq)
|
||||||
|
|
||||||
|
eur_day = cum_payout / EURUSD / (n_days / 365.0) / 365.0
|
||||||
|
return dict(eur_day=eur_day, med=float(np.median(eur_day)),
|
||||||
|
p10=float(np.percentile(eur_day, 10)), p90=float(np.percentile(eur_day, 90)),
|
||||||
|
p_ge10=float((eur_day >= 10.0).mean()),
|
||||||
|
p_target=float((eur_day >= TARGET_EUR_DAY).mean()),
|
||||||
|
p_zero=float((cum_payout <= 0).mean()), med_spent=float(np.median(spent)))
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------- eval/funded a conto singolo (HYRO)
|
||||||
|
|
||||||
|
def single_account(cfg: tuple[float, float, float], lev: float, lens: str, start: str,
|
||||||
|
n_paths: int = 6000, seed: int = 725, block: int = 20,
|
||||||
|
xs_mode: str = "shared") -> dict:
|
||||||
|
"""P(pass eval), P(vivo 1 anno da funded), E[payout annuo] per UN conto HYRO $100k."""
|
||||||
|
rng = np.random.default_rng(seed)
|
||||||
|
R0, G0, _ = book_tuples(*cfg, start=start, xs_mode=xs_mode)
|
||||||
|
R0 = R0 - (1.0 - DELUCK) * R0.mean()
|
||||||
|
n = len(R0)
|
||||||
|
n_days = 365
|
||||||
|
n_blocks = int(np.ceil(n_days / block))
|
||||||
|
st = rng.integers(0, n - block, size=(n_paths, n_blocks))
|
||||||
|
idx = (st[:, :, None] + np.arange(block)[None, None, :]).reshape(n_paths, -1)[:, :n_days]
|
||||||
|
R = R0[idx] * lev
|
||||||
|
if lens == "close":
|
||||||
|
G = np.zeros_like(R)
|
||||||
|
elif lens == "coupled":
|
||||||
|
G = G0[idx] * lev
|
||||||
|
else:
|
||||||
|
vol = R.std() * np.sqrt(365.0)
|
||||||
|
G = -np.exp(rng.normal(pl.WICK_MU, pl.WICK_SIGMA, size=R.shape)) \
|
||||||
|
* np.clip(vol / pl.WICK_REF_VOL, 0.0, 3.0)
|
||||||
|
|
||||||
|
ev_t, ev_dd, ev_dl = FIRMS[0]["ev"]
|
||||||
|
fu_ml, fu_dl = FIRMS[0]["fu"]
|
||||||
|
NOTIONAL = 100_000.0
|
||||||
|
|
||||||
|
eq = np.ones(n_paths)
|
||||||
|
phase = np.zeros(n_paths, np.int8) # 0 eval, 1 funded, -1 morto
|
||||||
|
days_funded = np.zeros(n_paths, int)
|
||||||
|
payout = np.zeros(n_paths)
|
||||||
|
passed_ev = np.zeros(n_paths, bool)
|
||||||
|
for t in range(n_days):
|
||||||
|
alive = phase >= 0
|
||||||
|
r = R[:, t]; g = G[:, t]
|
||||||
|
eq_prev = eq
|
||||||
|
eq = np.where(alive, eq * (1 + r), eq)
|
||||||
|
eq_low = np.where(alive, eq_prev * (1 + r + g), eq)
|
||||||
|
r_low = r + g
|
||||||
|
in_ev = alive & (phase == 0)
|
||||||
|
in_fu = alive & (phase == 1)
|
||||||
|
bust = ((in_ev & ((eq_low < 1 - ev_dd) | (r_low < -ev_dl)))
|
||||||
|
| (in_fu & ((eq_low < 1 - fu_ml) | (r_low < -fu_dl))))
|
||||||
|
phase[bust] = -1
|
||||||
|
ok = in_ev & (eq >= 1 + ev_t) & ~bust
|
||||||
|
phase[ok] = 1
|
||||||
|
eq[ok] = 1.0
|
||||||
|
passed_ev |= ok
|
||||||
|
days_funded += (phase == 1)
|
||||||
|
if (t + 1) % 30 == 0:
|
||||||
|
fu = phase == 1
|
||||||
|
gain = np.where(fu & (eq > 1), (eq - 1) * NOTIONAL, 0.0)
|
||||||
|
payout += gain * SPLIT * (1 - TAX_RATE)
|
||||||
|
eq = np.where(fu & (eq > 1), 1.0, eq)
|
||||||
|
return dict(p_pass=float(passed_ev.mean()),
|
||||||
|
p_alive=float((phase[passed_ev] == 1).mean()) if passed_ev.any() else 0.0,
|
||||||
|
e_payout=float(payout.mean()),
|
||||||
|
e_payout_if_pass=float(payout[passed_ev].mean()) if passed_ev.any() else 0.0)
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------- verifica di risoluzione (1h vs 5m)
|
||||||
|
|
||||||
|
@lru_cache(maxsize=4)
|
||||||
|
def _tp_leg_gap(tf: str) -> pd.DataFrame:
|
||||||
|
"""(R, m) giornalieri della SOLA gamba TP01, misurati a risoluzione `tf`.
|
||||||
|
|
||||||
|
TP01 tiene un peso COSTANTE nel giorno, quindi il suo minimo intraday e' esattamente il
|
||||||
|
minimo del path di prezzo: si puo' calcolare a 5m e confrontare con l'1h. E' il test che
|
||||||
|
dice se l'1h basta a catturare il minimo giornaliero, invece di assumerlo.
|
||||||
|
"""
|
||||||
|
tot_r = tot_w = None
|
||||||
|
for a in ASSETS:
|
||||||
|
df = load_data(a, tf)
|
||||||
|
idx = pd.DatetimeIndex(pd.to_datetime(df["datetime"], utc=True))
|
||||||
|
c = df["close"].values.astype(float)
|
||||||
|
lo = df["low"].values.astype(float)
|
||||||
|
hi = df["high"].values.astype(float)
|
||||||
|
prev = np.concatenate(([np.nan], c[:-1]))
|
||||||
|
tpf = mc.tp01_daily_frame(a)
|
||||||
|
day = idx.floor("D")
|
||||||
|
w = np.nan_to_num(tpf["w"].reindex(day).values)
|
||||||
|
fee = np.nan_to_num(tpf["fee"].reindex(day).values)
|
||||||
|
first = np.concatenate(([True], day[1:] != day[:-1]))
|
||||||
|
rw = np.where(w >= 0, lo / prev - 1.0, hi / prev - 1.0)
|
||||||
|
r = pd.Series(w * (c / prev - 1.0) - np.where(first, fee, 0.0), index=idx)
|
||||||
|
wk = pd.Series(w * rw - np.where(first, fee, 0.0), index=idx)
|
||||||
|
tot_r = r if tot_r is None else tot_r.add(r, fill_value=np.nan)
|
||||||
|
tot_w = wk if tot_w is None else tot_w.add(wk, fill_value=np.nan)
|
||||||
|
J = pd.concat({"r": tot_r, "w": tot_w}, axis=1).dropna()
|
||||||
|
rows = []
|
||||||
|
for d, g in J.groupby(J.index.floor("D")):
|
||||||
|
r = g["r"].values; wk = g["w"].values
|
||||||
|
cum = np.cumprod(1 + r); cp = np.concatenate(([1.0], cum[:-1]))
|
||||||
|
m = min(cum.min(), (cp * (1 + wk)).min()) - 1.0
|
||||||
|
rows.append((d, cum[-1] - 1.0, min(m, cum[-1] - 1.0)))
|
||||||
|
return pd.DataFrame(rows, columns=["d", "R", "m"]).set_index("d")
|
||||||
|
|
||||||
|
|
||||||
|
# ================================================================= main
|
||||||
|
|
||||||
|
START_WIN = "2024-01-01" # finestra comune ai 3 sleeve (XS01 nasce qui)
|
||||||
|
CFG_A = (0.75, 0.25, 0.0) # book live Deribit (TP01/SKH01) — quello che gira oggi
|
||||||
|
CFG_B = (0.55, 0.20, 0.25) # book diversificato (config B del 25/07)
|
||||||
|
CFG_TP = (1.0, 0.0, 0.0)
|
||||||
|
CFG_SKH = (0.0, 1.0, 0.0)
|
||||||
|
CFG_XS = (0.0, 0.0, 1.0)
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
print("=" * 106)
|
||||||
|
print(" LENTE WICK ACCOPPIATA — chiusura del follow-up dichiarato (r0725_prop_ladder §ONESTA')")
|
||||||
|
print("=" * 106)
|
||||||
|
|
||||||
|
print("\n recon MTM orario per-sleeve (TP01 daily + SKH01 230m exit-al-livello)...")
|
||||||
|
R_A, G_A, I_A = book_tuples(*CFG_A)
|
||||||
|
print(f" book A (TP01 75 / SKH01 25): {len(R_A)} giorni {I_A[0].date()} -> {I_A[-1].date()}")
|
||||||
|
|
||||||
|
# --- sanity: il recon deve riprodurre lo sleeve ufficiale
|
||||||
|
xs_off = _d(_xsec_returns())
|
||||||
|
xs_rec = xsec_daily_tuples()
|
||||||
|
common = xs_off.index.intersection(xs_rec.index)
|
||||||
|
dmax = float(np.abs(xs_off.reindex(common).values - xs_rec["R"].reindex(common).values).max())
|
||||||
|
print(f" sanity XS01 recon vs sleeve ufficiale: max|delta| chiusura = {dmax:.2e}"
|
||||||
|
f" ({'OK' if dmax < 1e-9 else 'DIVERGE'})")
|
||||||
|
|
||||||
|
# ---------------------------------------------------------- 3. la prova del difetto
|
||||||
|
print("\n" + "-" * 106)
|
||||||
|
print(" A. IL GAP NON E' INDIPENDENTE DAL GIORNO — e la dipendenza va nel verso INATTESO")
|
||||||
|
print("-" * 106)
|
||||||
|
print(f" {'book':>26} {'giorni':>7} {'m==R':>7} {'gap medio':>10} "
|
||||||
|
f"{'gap | decile PEGGIORE':>22} {'gap | decile MIGLIORE':>22}")
|
||||||
|
for nm, cfg in [("A TP01/SKH01 (live)", CFG_A), ("B +XS01 (diversif.)", CFG_B),
|
||||||
|
("TP01 solo", CFG_TP), ("SKH01 solo", CFG_SKH), ("XS01 solo", CFG_XS)]:
|
||||||
|
R, G, _ = book_tuples(*cfg)
|
||||||
|
if R.std() == 0:
|
||||||
|
continue
|
||||||
|
q = np.quantile(R, [0.1, 0.9])
|
||||||
|
print(f" {nm:>26} {len(R):>7d} {(G > -1e-12).mean():>6.0%} {G.mean()*100:>9.2f}pp "
|
||||||
|
f"{G[R <= q[0]].mean()*100:>21.2f}pp {G[R >= q[1]].mean()*100:>21.2f}pp")
|
||||||
|
print("\n Lettura — il contrario di quello che si assume di solito: l'escursione intraday e'")
|
||||||
|
print(" piu' PROFONDA nei giorni che finiscono BENE (recuperi a V) che in quelli che finiscono")
|
||||||
|
print(" male, perche' un giorno brutto scende tutto il giorno e CHIUDE sul minimo (m==R nel")
|
||||||
|
print(" ~29% dei giorni). Il breach pero' si valuta sul minimo, quindi cio' che conta e' quanto")
|
||||||
|
print(" gap si aggiunge NEI GIORNI BRUTTI — e li' il gap vero e' ~1/3 di quello nei giorni buoni.")
|
||||||
|
print(" Un'estrazione indipendente usa la STESSA distribuzione ovunque -> carica i giorni")
|
||||||
|
print(" brutti con la coda che nella realta' appartiene ai giorni buoni.")
|
||||||
|
|
||||||
|
# --- la misura che chiude la questione: quanti breach in piu' inventa la lente indipendente
|
||||||
|
print("\n" + "-" * 106)
|
||||||
|
print(" A-bis. CONTEGGIO EMPIRICO DEI BREACH sui giorni STORICI (nessun bootstrap)")
|
||||||
|
print("-" * 106)
|
||||||
|
rng = np.random.default_rng(725)
|
||||||
|
print(f" {'book':>26} {'leva':>5} {'regola':>22} {'close-only':>11} "
|
||||||
|
f"{'ACCOPPIATO':>11} {'indipendente':>13} {'gonfiaggio':>11}")
|
||||||
|
for nm, cfg in [("A TP01/SKH01 (live)", CFG_A), ("B +XS01 (diversif.)", CFG_B)]:
|
||||||
|
R0, G0, _ = book_tuples(*cfg, start=START_WIN)
|
||||||
|
for lev in (0.75, 1.0):
|
||||||
|
R, G = R0 * lev, G0 * lev
|
||||||
|
vol = R.std() * np.sqrt(365.0)
|
||||||
|
n_rep = 400
|
||||||
|
Gi = -np.exp(rng.normal(pl.WICK_MU, pl.WICK_SIGMA, size=(n_rep, len(R)))) \
|
||||||
|
* np.clip(vol / pl.WICK_REF_VOL, 0.0, 3.0)
|
||||||
|
for lab, thr in (("daily-loss 4% (HYRO)", 0.04), ("daily-loss 5% (FTMO)", 0.05)):
|
||||||
|
b_cl = float((R < -thr).mean())
|
||||||
|
b_co = float((R + G < -thr).mean())
|
||||||
|
b_in = float((R[None, :] + Gi < -thr).mean())
|
||||||
|
infl = (b_in / b_co) if b_co > 0 else np.inf
|
||||||
|
print(f" {nm:>26} {lev:>5.2f} {lab:>22} {b_cl:>10.2%} "
|
||||||
|
f"{b_co:>10.2%} {b_in:>12.2%} {infl:>10.1f}x")
|
||||||
|
|
||||||
|
# geometria dei gap: confronto diretto fra le due lenti
|
||||||
|
print("\n" + "-" * 106)
|
||||||
|
print(" B. GEOMETRIA DEI GAP — accoppiato (recon) vs indipendente (calibrazione 25/07)")
|
||||||
|
print("-" * 106)
|
||||||
|
q_ind = pl.wick_percentiles()
|
||||||
|
print(f" {'book':>28} {'p50':>9} {'p90':>9} {'p99':>9} {'peggiore':>10} {'vol annua':>10}")
|
||||||
|
for nm, cfg in [("A TP01/SKH01 (live)", CFG_A), ("B +XS01 (diversificato)", CFG_B),
|
||||||
|
("XS01 solo", CFG_XS)]:
|
||||||
|
R, G, _ = book_tuples(*cfg)
|
||||||
|
if R.std() == 0:
|
||||||
|
continue
|
||||||
|
p = np.percentile(-G, [50, 90, 99])
|
||||||
|
print(f" {nm:>28} {-p[0]*100:>8.2f}pp {-p[1]*100:>8.2f}pp {-p[2]*100:>8.2f}pp "
|
||||||
|
f"{G.min()*100:>9.2f}pp {R.std()*np.sqrt(365)*100:>9.1f}%")
|
||||||
|
print(f" {'INDIPENDENTE (25/07)':>28} {-q_ind[0.50]*100:>8.2f}pp {-q_ind[0.90]*100:>8.2f}pp "
|
||||||
|
f"{-q_ind[0.99]*100:>8.2f}pp {'—':>10} {'(scalato)':>10}")
|
||||||
|
|
||||||
|
# ---------------------------------------------------------- 4. conto singolo HYRO
|
||||||
|
START = START_WIN
|
||||||
|
print("\n" + "-" * 106)
|
||||||
|
print(f" C. UN CONTO HYRO $100k (max-loss 6% statico, daily 4%) — finestra {START}+, de-luck x{DELUCK}")
|
||||||
|
print("-" * 106)
|
||||||
|
print(f" {'config':>26} {'leva':>5} {'lente':>10} {'P(pass eval)':>13} "
|
||||||
|
f"{'P(vivo 1a)':>11} {'E[payout/a]':>12}")
|
||||||
|
for nm, cfg in [("A book live 75/25", CFG_A), ("B +XS01 55/20/25", CFG_B)]:
|
||||||
|
for lev in (0.50, 0.75, 1.00):
|
||||||
|
for lens in ("close", "indip", "coupled"):
|
||||||
|
r = single_account(cfg, lev, lens, START)
|
||||||
|
print(f" {nm:>26} {lev:>5.2f} {lens:>10} {r['p_pass']:>13.1%} "
|
||||||
|
f"{r['p_alive']:>11.1%} {r['e_payout']:>11,.0f}$")
|
||||||
|
print()
|
||||||
|
|
||||||
|
# ------------------------------------------------- 4-bis. le tre verifiche che possono ribaltare
|
||||||
|
print("-" * 106)
|
||||||
|
print(" C-bis. VERIFICHE — un risultato 'nessuna differenza' va provato, non assunto")
|
||||||
|
print("-" * 106)
|
||||||
|
|
||||||
|
print("\n (1) RISOLUZIONE: il wick 1h cattura davvero il minimo del giorno? (gamba TP01, 5m vs 1h)")
|
||||||
|
for tf in ("1h", "5m"):
|
||||||
|
D = _tp_leg_gap(tf)
|
||||||
|
G = (D["m"] - D["R"]).values
|
||||||
|
k = np.isfinite(G) & (np.abs(D["R"].values) > 0) # i giorni flat non hanno escursione
|
||||||
|
print(f" @{tf}: p50 {-np.percentile(-G[k], 50)*100:>6.3f}pp "
|
||||||
|
f"p90 {-np.percentile(-G[k], 90)*100:>6.3f}pp "
|
||||||
|
f"p99 {-np.percentile(-G[k], 99)*100:>6.3f}pp "
|
||||||
|
f"peggiore {G[k].min()*100:>6.2f}pp (n={k.sum()} giorni non-flat)")
|
||||||
|
print(" -> se coincidono, la lente accoppiata a 1h NON e' un tetto di risoluzione.")
|
||||||
|
|
||||||
|
print("\n (2) RICONCILIAZIONE col 24/07 (recon MTM indipendente, finestra piena): "
|
||||||
|
"config A @0.75x, P(vivo 1a)")
|
||||||
|
for st, lab in (("2019-01-01", "2019-03+ FULL"), (START, "2024+ comune")):
|
||||||
|
for lens in ("close", "coupled"):
|
||||||
|
r = single_account(CFG_A, 0.75, lens, st)
|
||||||
|
print(f" {lab:>14} {lens:>8}: P(pass) {r['p_pass']:>5.1%} "
|
||||||
|
f"P(vivo) {r['p_alive']:>5.1%} E[payout] {r['e_payout']:>6,.0f}$")
|
||||||
|
print(" -> il 24/07 riportava 58% sulla finestra piena; il 25/07 (wick indipendente) 10%.")
|
||||||
|
|
||||||
|
print("\n (3) BOUND SEVERO su XS01 (ogni gamba al proprio peggio insieme) — config B @0.75x")
|
||||||
|
for xm, lab in (("shared", "ordine condiviso (base)"), ("perleg", "per-gamba (bound severo)")):
|
||||||
|
R, G, _ = book_tuples(*CFG_B, start=START, xs_mode=xm)
|
||||||
|
r = single_account(CFG_B, 0.75, "coupled", START, xs_mode=xm)
|
||||||
|
print(f" {lab:>26}: gap p90 {-np.percentile(-G, 90)*100:>6.2f}pp "
|
||||||
|
f"peggiore {G.min()*100:>6.2f}pp | P(pass) {r['p_pass']:>5.1%} "
|
||||||
|
f"P(vivo) {r['p_alive']:>5.1%} E[payout] {r['e_payout']:>6,.0f}$")
|
||||||
|
print(" -> se la config B regge a ENTRAMBI gli estremi, la conclusione non dipende dalla convenzione.")
|
||||||
|
|
||||||
|
# ---------------------------------------------------------- 5. la scala di conti
|
||||||
|
print("\n" + "-" * 106)
|
||||||
|
print(f" D. LA SCALA DI CONTI — {HORIZON_MONTHS} mesi, EUR {START_EUR:.0f} di cassa, "
|
||||||
|
f"max {MAX_CONCURRENT} conti, morte-firm {FIRM_DEATH_PER_YEAR:.0%}/a")
|
||||||
|
print("-" * 106)
|
||||||
|
cfgs = [CFG_A, CFG_B, CFG_TP, CFG_SKH, CFG_XS]
|
||||||
|
POLS = {
|
||||||
|
"CONC-2SL ogni conto = book live": [0] * MAX_CONCURRENT,
|
||||||
|
"CONC-DIV ogni conto = diversificato": [1] * MAX_CONCURRENT,
|
||||||
|
"SPARSO 1 sleeve per conto": [2, 4, 3, 2, 4, 3],
|
||||||
|
"MISTO meta' div / meta' single": [1, 4, 1, 2, 1, 4],
|
||||||
|
}
|
||||||
|
for lens in ("close", "indip", "coupled"):
|
||||||
|
tag = {"close": "CLOSE-ONLY (tetto)", "indip": "WICK INDIPENDENTE (pavimento 25/07)",
|
||||||
|
"coupled": "WICK ACCOPPIATO (la lente onesta)"}[lens]
|
||||||
|
for lev in (0.50, 1.00):
|
||||||
|
print(f"\n {tag} — LEVA {lev:.2f}x")
|
||||||
|
print(f" {'politica':>40} {'EUR/g med':>10} {'p10':>7} {'p90':>7} "
|
||||||
|
f"{'P(>=10/g)':>10} {'P(>=50/g)':>10} {'P(zero)':>8}")
|
||||||
|
for pname, slots in POLS.items():
|
||||||
|
r = simulate(cfgs, slots, n_paths=3000, lev=lev, seed=20260725,
|
||||||
|
lens=lens, start=START)
|
||||||
|
print(f" {pname:>40} {r['med']:>10.2f} {r['p10']:>7.2f} {r['p90']:>7.2f} "
|
||||||
|
f"{r['p_ge10']:>10.1%} {r['p_target']:>10.1%} {r['p_zero']:>8.1%}")
|
||||||
|
|
||||||
|
print("\n" + "=" * 106)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
"""Test della lente WICK ACCOPPIATA (scripts/research/r0725_prop_coupled.py).
|
||||||
|
|
||||||
|
Blindano le proprieta' che rendono la lente *onesta* — sono le stesse che, se si rompessero,
|
||||||
|
farebbero tornare silenziosamente il difetto del 25/07 (gap indipendente dal giorno):
|
||||||
|
* il recon XS01 deve riprodurre ESATTAMENTE lo sleeve ufficiale (se diverge, si sta
|
||||||
|
misurando il wick di una strategia che non e' quella in produzione);
|
||||||
|
* il gap dev'essere <= 0 per costruzione (il minimo intraday non puo' stare sopra la chiusura);
|
||||||
|
* la dipendenza dal giorno dev'esserci (gap piu' profondo nei giorni buoni) — e' il fatto
|
||||||
|
su cui poggia tutta la conclusione;
|
||||||
|
* le tre lenti devono ordinarsi close >= coupled >= indip sui breach;
|
||||||
|
* il bound severo su XS01 dev'essere effettivamente piu' severo di quello condiviso.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
sys.path.insert(0, str(ROOT))
|
||||||
|
sys.path.insert(0, str(ROOT / "scripts" / "research"))
|
||||||
|
|
||||||
|
pytest.importorskip("pandas")
|
||||||
|
|
||||||
|
import r0725_prop_coupled as pc # noqa: E402
|
||||||
|
from src.portfolio.sleeves import _xsec_returns # noqa: E402
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------- fedelta' del recon
|
||||||
|
|
||||||
|
def test_xsec_recon_riproduce_lo_sleeve_ufficiale():
|
||||||
|
"""Se il recon XS01 divergesse dallo sleeve, misureremmo il wick di un'altra strategia."""
|
||||||
|
off = _xsec_returns().dropna().sort_index()
|
||||||
|
if off.index.tz is None:
|
||||||
|
off.index = off.index.tz_localize("UTC")
|
||||||
|
rec = pc.xsec_daily_tuples()
|
||||||
|
common = off.index.intersection(rec.index)
|
||||||
|
assert len(common) > 500
|
||||||
|
assert np.abs(off.reindex(common).values - rec["R"].reindex(common).values).max() < 1e-12
|
||||||
|
|
||||||
|
|
||||||
|
def test_crypto_recon_riproduce_il_book_7525_del_2407():
|
||||||
|
"""La decomposizione per-sleeve, ricombinata a 75/25, deve dare il book di r0724.
|
||||||
|
Tolleranza larga: r0724 fa il recon in un colpo solo, qui si somma dopo aver diviso
|
||||||
|
per i pesi — devono coincidere a meno dell'aritmetica in virgola mobile."""
|
||||||
|
R, G, I = pc.book_tuples(*pc.CFG_A)
|
||||||
|
assert len(R) > 2000
|
||||||
|
vol = R.std() * np.sqrt(365.0)
|
||||||
|
assert 0.08 < vol < 0.16, f"vol book live fuori scala: {vol:.3f}"
|
||||||
|
# i percentili del gap devono ritrovare quelli dichiarati dal 24/07 (-0.17 / -1.03 / -2.70)
|
||||||
|
p50, p90 = -np.percentile(-G, 50), -np.percentile(-G, 90)
|
||||||
|
assert -0.004 < p50 < -0.0005, p50
|
||||||
|
assert -0.020 < p90 < -0.004, p90
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------- proprieta' del gap
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("cfg", [pc.CFG_A, pc.CFG_B, pc.CFG_XS])
|
||||||
|
def test_gap_sempre_non_positivo(cfg):
|
||||||
|
"""m <= R per costruzione: il minimo intraday non puo' stare sopra la chiusura."""
|
||||||
|
_, G, _ = pc.book_tuples(*cfg)
|
||||||
|
assert G.max() <= 1e-15
|
||||||
|
|
||||||
|
|
||||||
|
def test_gap_piu_profondo_nei_giorni_buoni():
|
||||||
|
"""IL fatto su cui poggia la conclusione. Se questo si invertisse, la lente indipendente
|
||||||
|
non sarebbe piu' sbilanciata e il follow-up perderebbe senso."""
|
||||||
|
R, G, _ = pc.book_tuples(*pc.CFG_A)
|
||||||
|
q = np.quantile(R, [0.1, 0.9])
|
||||||
|
gap_bad = G[R <= q[0]].mean()
|
||||||
|
gap_good = G[R >= q[1]].mean()
|
||||||
|
assert gap_good < gap_bad, (gap_good, gap_bad)
|
||||||
|
assert gap_good < 2.0 * gap_bad, "il gap nei giorni buoni dev'essere nettamente piu' profondo"
|
||||||
|
|
||||||
|
|
||||||
|
def test_una_quota_di_giorni_chiude_sul_minimo():
|
||||||
|
"""Nei giorni brutti la chiusura E' il minimo: e' il motivo per cui il gap indipendente
|
||||||
|
(che non lo sa) aggiunge un tuffo che non c'e'."""
|
||||||
|
_, G, _ = pc.book_tuples(*pc.CFG_A)
|
||||||
|
frac = float((G > -1e-12).mean())
|
||||||
|
assert 0.10 < frac < 0.60, frac
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------- ordinamento delle lenti
|
||||||
|
|
||||||
|
def test_le_lenti_si_ordinano_sui_breach():
|
||||||
|
"""close-only non vede mai il minimo -> meno breach di tutti; l'indipendente ne inventa
|
||||||
|
-> piu' della accoppiata. L'ordine e' la ragion d'essere dell'esercizio."""
|
||||||
|
R, G, _ = pc.book_tuples(*pc.CFG_A, start=pc.START_WIN)
|
||||||
|
rng = np.random.default_rng(0)
|
||||||
|
vol = R.std() * np.sqrt(365.0)
|
||||||
|
Gi = -np.exp(rng.normal(pc.pl.WICK_MU, pc.pl.WICK_SIGMA, size=(200, len(R)))) * np.clip(
|
||||||
|
vol / pc.pl.WICK_REF_VOL, 0.0, 3.0)
|
||||||
|
thr = 0.04
|
||||||
|
b_close = float((R < -thr).mean())
|
||||||
|
b_coup = float((R + G < -thr).mean())
|
||||||
|
b_indip = float((R[None, :] + Gi < -thr).mean())
|
||||||
|
assert b_close <= b_coup <= b_indip, (b_close, b_coup, b_indip)
|
||||||
|
assert b_indip > 1.3 * max(b_coup, 1e-9), "l'indipendente deve gonfiare in modo misurabile"
|
||||||
|
|
||||||
|
|
||||||
|
def test_bound_severo_xs01_e_davvero_piu_severo():
|
||||||
|
_, g_sh, _ = pc.book_tuples(*pc.CFG_B, xs_mode="shared")
|
||||||
|
_, g_pl, _ = pc.book_tuples(*pc.CFG_B, xs_mode="perleg")
|
||||||
|
assert g_pl.mean() < g_sh.mean()
|
||||||
|
assert g_pl.min() < g_sh.min()
|
||||||
|
|
||||||
|
|
||||||
|
def test_wick_mode_sconosciuto_alza_errore():
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
pc.xsec_daily_tuples("nonesiste")
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------- simulazione
|
||||||
|
|
||||||
|
def test_single_account_close_domina_coupled_domina_indip():
|
||||||
|
"""A parita' di tutto il resto, una lente piu' severa non puo' far sopravvivere di piu'."""
|
||||||
|
out = {ln: pc.single_account(pc.CFG_A, 1.0, ln, pc.START_WIN, n_paths=800)
|
||||||
|
for ln in ("close", "coupled", "indip")}
|
||||||
|
assert out["close"]["p_alive"] >= out["coupled"]["p_alive"] >= out["indip"]["p_alive"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_leva_piu_alta_non_aumenta_la_sopravvivenza():
|
||||||
|
lo = pc.single_account(pc.CFG_B, 0.50, "coupled", pc.START_WIN, n_paths=800)
|
||||||
|
hi = pc.single_account(pc.CFG_B, 1.00, "coupled", pc.START_WIN, n_paths=800)
|
||||||
|
assert lo["p_alive"] > hi["p_alive"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_book_tuples_non_condivide_lo_stato_fra_chiamate():
|
||||||
|
"""La cache restituisce copie: un chiamante che modifica l'array non deve avvelenare
|
||||||
|
il successivo (stesso guardrail messo su r0725_capcurve)."""
|
||||||
|
R1, _, _ = pc.book_tuples(*pc.CFG_A)
|
||||||
|
R1[:] = 999.0
|
||||||
|
R2, _, _ = pc.book_tuples(*pc.CFG_A)
|
||||||
|
assert not np.allclose(R2, 999.0)
|
||||||
Reference in New Issue
Block a user