From 418f523be791a47850a13f9b0b893dc275fb73e9 Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Fri, 28 Aug 2026 12:54:08 +0000 Subject: [PATCH] =?UTF-8?q?gtaa:=20la=20decisione=20tenere/bloccare=20va?= =?UTF-8?q?=20a=20$15k=20=E2=80=94=20gate=20(A)=20ritirato,=20soglia=20sor?= =?UTF-8?q?vegliata?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Decisione dell'operatore: GTAA01 NON si blocca oggi, si decide quando il book arriva a $15k. Il motivo registrato non e' «non contribuisce» — la misura dice il contrario (+0,095/+0,124 di Sharpe a iso-rischio secondo la fase, positivo in tutte e cinque, hold-out +0,247, 6 anni su 8, correlazione col resto +0,087). Il motivo e' che sotto $15k di book lo sleeve NON E' ACCENDIBILE: GTAA_MIN_CAPITAL e' $3.000 allocati, che al peso 20% fa $15.000 contro i $2.068 attuali. Finora era manutenzione senza beneficio incassabile. Registrate anche le due ragioni contro il blocco, che restano valide: N7 (uno sleeve difensivo si giudica sul sinistro, e questa finestra non lo contiene) e N4 (togliendolo il portafoglio di ricerca diventa 100% cripto su un venue solo). E il costo che sarebbe andato pagato: GTAA01 e' uno dei quattro nomi di W_DEPLOY, da cui escono i $313k del muro. PERCHE' LA DECISIONE NON SI DIMENTICHI (N9). Una decisione parcheggiata su un numero che nessuno sorveglia e' parcheggiata per sempre. `journal.SOGLIE_CAPITALE` legge l'equity ogni giorno — il giornale lo fa comunque — e il giorno che supera la soglia la voce dice quale decisione si sblocca e perche'. Seconda riga: $20k, dove si riapre «100% Deribit fino a $20k». Una riga si TOGLIE quando la decisione e' presa. 5 test, incluso «equity non leggibile non e' una soglia superata» (P5). GATE (A) RITIRATO come pass/fail, congelando il MOTIVO e non l'esito — come il 07/08 col confronto fra ranghi, e per la stessa ragione: il criterio decide su un margine piu' piccolo del rumore che lo scuote. In piu' una guardia sulla CAUSA (`test_la_fase_di_ribilanciamento_e_ancora_ancorata_alla_POSIZIONE`) che si rompe il giorno che qualcuno ancorasse la fase al calendario: quel giorno il gate potrebbe tornare decidibile, ed e' un fatto da guardare, non da ignorare. Il test sul margine assoluto si e' auto-ritirato: diceva «se scendesse sotto un centesimo questo criterio smetterebbe di essere una misura», ed e' sceso a 0,0074. CLAUDE.md: §3 nuova riga · §5.2 RISOLTO (trasporto allarmi) · §5.4 riscritto (la domanda fiscale (a) ha una risposta alla fonte: derivati in c-quater al 26%, non c-sexies al 33% — Circolare AdE 30/E del 27/10/2023, citata verbatim) · §5.13 nuovo debito, la fase che ruota — quantificata e dichiarata INNOCUA oggi (D5): 0,029 di Sharpe a livello di portafoglio, dentro la banda [1,81-2,12]. Diario: docs/diary/2026-08-28-gtaa-fase-e-allarmi.md Suite: 789 passati, 0 falliti. Co-Authored-By: Claude Opus 5 (1M context) --- CLAUDE.md | 53 +++++-- docs/diary/2026-08-28-gtaa-fase-e-allarmi.md | 158 +++++++++++++++++++ src/live/journal.py | 35 ++++ tests/test_gtaa_band_gate.py | 65 +++++--- tests/test_journal.py | 34 ++++ 5 files changed, 316 insertions(+), 29 deletions(-) create mode 100644 docs/diary/2026-08-28-gtaa-fase-e-allarmi.md diff --git a/CLAUDE.md b/CLAUDE.md index cecd09d..4442d92 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -153,6 +153,7 @@ CLAUDE.md", la risposta e' la colonna *cosa la riapre*. | **Non de-esporre il weekend** (porta il 38% del gross di TP01 nel 31% del tempo) | 17/07 | — | ogni proposta "risk-off weekend" parte **REFUTED** salvo null de-levering superato | | **Peso 75/25 confermato 3 volte** (24/07, 26/07, 24/07-follow-up) | 26/07 | — | `weights_tilt_null` superato, che finora **fallisce** | | **Fee Deribit: ≤5 bps/lato → non si tocca nulla; >10 bps → rivedere il peso SKH01** (regola decisa *prima* di vedere il numero) | 26/07 | — | il sorvegliante `fee_watch` allerta da solo | +| **GTAA01: decisione tenere/bloccare RINVIATA a $15k di book** — sotto quella soglia lo sleeve **non e' deployabile** (`GTAA_MIN_CAPITAL` $3.000 allocati al peso 20%), quindi finora e' manutenzione senza beneficio incassabile. Contributo misurato a iso-rischio **+0,095 / +0,124** di Sharpe secondo la fase: **reale e sempre positivo**, e delle stesse dimensioni dello spread fra le baseline (0,12) | 28/08 | «GTAA01 non contribuisce» — la misura dice il contrario | **il book a $15k**, che il giornale sorveglia da solo (`journal.SOGLIE_CAPITALE`) | | **Gradino di leva NON autorizzato oggi.** 1,25x autorizzabile *a condizione*; **1,50x BOCCIATO** (peggior giorno strutturale 21,48% > 20%); **k max difendibile 1,40** | 23/08 | il 1,50x | costruire prima una **chiave di scala esplicita** in config **e** rifare `r0726_fee_sensitivity` | ⚠️ **La regola *"ogni cambio di scala passa dal cap di config, non da `target_vol`"* NON E' @@ -202,22 +203,42 @@ d'ancora in modo diverso (nella differenza si cancella in parte, nel livello per `paper_portfolio` NON rigenerato (GTAA su ADJUSTED_LAST: replay ≠ serie registrata, P12) — storia pre-fix dichiarata corrotta, coda non chiusa tolta. ⚠️ Nel fix, D6 pagata di nuovo: `asi8` in pandas 3 e' in `us`, non `ns` — blindato con test su tre risoluzioni. -2. 🚨 **Il trasporto degli allarmi e' un punto singolo di guasto** — 6,9% di invii falliti (2/29). - Fatte il 23/08: retry opzionale in `send()` e registrazione del motivo nel punto in cui veniva - ingoiato. **Resta:** `alerted=True` marcato **prima** dell'invio → un 🚨 perso e' perso per - l'episodio intero (gli episodi storici durano 200-2.324 ore). Cambiarlo **cambia il comportamento** - → decisione dell'operatore. +2. ✅ **RISOLTO (2026-08-28, decisione dell'operatore).** Il trasporto degli allarmi era un punto + singolo di guasto — 6,9% di invii falliti (2/29) e `alerted=True` marcato **prima** dell'invio, + quindi un 🚨 perso era perso per l'**episodio intero** (200-2.324 ore). Ora `venue_watch.run_once` + riceve il **sender iniettato** e il marcatore «gia' detto» si scrive **solo dopo un invio + riuscito**; su fallimento si disfano i soli marcatori — non le misure — e per il lock **le ore + continuano a correre**, cosi' una manutenzione che sfora la grazia sale ad ALERT anche col + trasporto giu'. `notify()` accetta `tentativi` (venue_watch: 3) e l'esito finisce nel log. + ⚠️ Resta scoperto **ogni altro chiamante di `notify()`**: la disciplina «commit dopo l'invio» + e' oggi solo in `venue_watch`. 3. ✅ **RISOLTO (2026-08-26, decisione dell'operatore: opzione "riscrivere adesso").** Gate XSR01 riscritto in modo dichiarato, prima dell'esito, in direzione che stringe: capitale $5k → **$20k** (riconcilia con la decisione 26/07, che e' posteriore e governa), gamba haircut letta da `r0826_xsr_haircut.py` a pavimento $10 con la frazione di ordini eseguiti accanto (misurata: haircut FULL −1,1% con **22% di eseguiti** — la guardia da sola era vacua), soglie numeriche INVARIATE. Diario `2026-08-26-xsr01-gate-riscritto.md`. -4. ❓ **Due domande al commercialista, aperte** (valgono ~$22k di muro e ~€30/mese di versamento): - (a) i derivati Deribit (inverse, margine e regolamento in cripto, sede extra-UE) stanno in - `c-sexies` (33%) o `c-quater` (26%)? E il collaterale sconta il 2‰ al 31/12? (b) i payout di una - prop firm: **ne' `c-sexies` ne' `c-quater`** — le fonti convergono su **lavoro autonomo**, nessuna - prassi ufficiale. *Non sono pareri fiscali: sono domande da porre.* +4. ◐ **(a) RISPOSTA TROVATA ALLA FONTE (2026-08-28), (b) resta aperta.** *Non sono pareri fiscali.* + **(a) I derivati stanno in `c-quater` → 26%, non in `c-sexies` → 33%.** Circolare AdE **30/E del + 27/10/2023**, §2.3.2 e §3, verbatim: «In generale, i redditi derivanti da **contratti derivati, + ancorche' aventi come sottostante cripto-attivita'**, costituiscono redditi diversi ai sensi + dell'articolo 67, comma 1, **lettera c-quater)**» e «qualora le cripto-attivita' costituiscano il + sottostante di un contratto derivato (ad es. contract for difference o **future**) i redditi + derivanti da tali strumenti finanziari costituiscono redditi diversi ai sensi dell'articolo + c-quater)». La circolare conferma esplicitamente che la riforma 2023 non sposta nulla: «tali + contratti **non rientrano** nell'ambito di applicazione della lettera c-sexies». ⚠️ Conseguenza: + **le minusvalenze su derivati NON si compensano con le plusvalenze cripto** (categorie diverse) e + non c'e' franchigia. **Il collaterale sconta il 2‰**: Deribit non e' intermediario residente, + quindi non applica il bollo e scatta l'*imposta sul valore delle cripto-attivita'* sul valore al + 31/12, in RW (a $2.068 fa **~$4/anno**: obbligo dichiarativo, non un costo). 📌 E la conversione + **USDC→USDE non e' fattispecie realizzativa** — §3.1: «dal 1° gennaio 2023 non costituisce + fattispecie realizzativa lo scambio di una cripto-valuta con un'altra», col valore di carico che + si trasferisce. **Resta da chiedere:** se il perpetual *inverse* regolato in cripto sia il caso + letterale della circolare (che nomina CFD e future) o serva un passaggio in piu'. + **(b) I payout di una prop firm: ancora aperta.** Le fonti convergono su **lavoro autonomo** + (partita IVA, ATECO, forfettario 5% per i primi 5 anni poi 15%, imponibile da coefficiente di + redditivita'), **non** redditi diversi al 26%. **Nessuna prassi ufficiale**, e il coefficiente + esatto dipende dal codice ATECO. 5. ✅ **FATTO (2026-08-26):** `test_leva_massima_da_config...` cancellato con nota in `tests/test_fee_sensitivity.py` — misurava `frac · n_asset`, con una chiave di scala avrebbe continuato a passare smettendo di controllare. La guardia giusta e' il tetto sul PRODOTTO di @@ -260,6 +281,18 @@ d'ancora in modo diverso (nella differenza si cancella in parte, nel livello per definizione, patchare la costante non bastava) e `docs/journal/`; per `book_executions.jsonl` (caricato ad-hoc via importlib, irraggiungibile da conftest) impronta a inizio suite e verifica a fine suite, col falso positivo possibile (fill reale del cron :47) dichiarato nel messaggio. +13. ⚠️ **GTAA01 gira su una finestra che ROTOLA, e la fase di ribilanciamento cambia da sola ogni + notte** (misurato 2026-08-28, `r0828_gtaa_band_phase.py`). IB serve una finestra di **30 anni** + e SPY e' al muro: la sua prima barra avanza di un giorno di borsa ogni notte (1996-07-02 il + 24/06 → 1996-09-04 il 28/08). `gtaa._gated_returns` ribilancia su `i % every == 0`, cioe' sulla + **posizione nell'array**: se la prima barra scivola si ri-fasa ogni decisione di trent'anni. + Effetto misurato: su una finestra **chiusa** (pre-2015) lo Sharpe si e' mosso fino a **0,076** + a codice fermo, e la banda scelta al buio cambia fra 25% / 40% / 60% da una notte all'altra. + **Ha gia' ucciso due criteri del gate (A)** (07/08 e 28/08). **Quantificato e innocuo OGGI** + (D5): a livello di portafoglio l'escursione sulle 5 fasi vale **0,029** di Sharpe, dentro la + banda dichiarata [1,81-2,12]. QQQ e IWM arriveranno allo stesso muro **fra 2,5 e 3,7 anni**. + Riparazione (ancorare la fase al calendario) rinviata con la decisione su GTAA01: cambierebbe + **tutti** i numeri registrati dello sleeve. --- diff --git a/docs/diary/2026-08-28-gtaa-fase-e-allarmi.md b/docs/diary/2026-08-28-gtaa-fase-e-allarmi.md new file mode 100644 index 0000000..ee8ea80 --- /dev/null +++ b/docs/diary/2026-08-28-gtaa-fase-e-allarmi.md @@ -0,0 +1,158 @@ +# 2026-08-28 — La fase che ruota, gli allarmi che si perdevano, e una risposta fiscale + +*Scritto il 2026-08-28. Numeri riprodotti dagli script citati; le letture in prosa sono firmate +come tali (P13).* + +## In una riga + +Un test rosso su GTAA01 non parlava di GTAA01: parlava del fatto che **la finestra "congelata" +pre-2015 rotola da sola ogni notte**. Nel frattempo si e' chiuso il debito del trasporto degli +allarmi, e la prima delle due domande al commercialista ha trovato una risposta alla fonte. + +--- + +## 1. Il gate (A) della banda GTAA01 e' una moneta — la finestra rotola + +`r0828_gtaa_band_phase.py`. Replica bit-exact della produzione verificata (max|Δ| = 0.00e+00 su +7.544 barre). Codice fermo dal 07/08: nessun commit su `src/portfolio/gtaa.py` ne' su +`r0727_gtaa_band_gate.py`. Si e' mosso il DATO. + +**(0) La finestra in-sample non e' ferma.** IB serve una finestra **rotolante di 30 anni**. SPY e' +l'unica gamba al muro (30,0 anni) e nel `logs/cron_daily.log` la sua data d'inizio avanza di un +giorno di borsa ogni notte: `1996-07-02` il 24/06 → `1996-09-04` oggi. QQQ e IWM arriveranno allo +stesso muro fra **2,5 e 3,7 anni**. + +**(1) L'amplificatore e' la fase.** `_gated_returns` ribilancia su `i % every == 0`, dove `i` e' la +**posizione nell'array**. Se la prima barra scivola, si ri-fasa ogni decisione di ribilanciamento +di trent'anni di storia. + +| misura | valore | +|---|---| +| spostamento max dello Sharpe in-sample dal 07/08 (finestra CHIUSA, codice fermo) | **0,076** | +| banda scelta al buio in 10 notti consecutive | 25% · 40% · 60% | +| notti col margine sotto il pavimento dichiarato (0,01) | **4/10** (minimo 0,0026) | +| a dato FISSO, muovendo solo la fase | 25% su 3 fasi, 60% su 2 | + +Il criterio decideva su ~0,007-0,015 di Sharpe mentre il rumore che lo scuote vale fino a 0,076. +E' lo stesso modo di fallire del criterio a ranghi ritirato il 07/08 (che decideva su 0,00116), in +un costume nuovo. + +## 2. Criterio (C): la mediana delle 5 fasi + +`r0828_gtaa_band_median_phase.py`. ⚠️ **Scelto DOPO aver visto la tabella delle fasi**: misura lo +strumento, non valida la banda. Pavimento tenuto a 0,01, invariato. + +- (i) scelta stabile su 10 notti: **SI** — `60%` in tutte e dieci +- (ii) margine sempre ≥ 0,01: **SI** — minimo 0,0197 +- (iii) potenza (al buio ≠ hold-out): **SI** — 60% contro 40% + +Escursione dello Sharpe in-sample su 10 notti: **−74/−99%** per ogni banda. Lo strumento regge. +**Ma la cella che sceglie non e' la proposta:** al buio esce il 60%, il 25% vince **0 notti su 10**. +Da tenere accanto: sull'hold-out il 60% e' **penultimo** (0,7430 contro 0,9315 del 40%), coerente +con lo Spearman IS/OOS ~0 misurato il 07/08 — la scelta in-sample **non predice**. + +**Contorno che vale piu' del verdetto:** le 5 serie di fase correlano **0,988** → **N_eff 1,01**. +La mediana di 5 fasi e' UNA osservazione: toglie l'artefatto, non compra precisione. + +### Le due domande che erano state confuse + +Il gate (A) puo' chiedere due cose diverse, e il 07/08 coincidevano **per fortuna di fase**: + +| forma | difetto | quanto e' grave | +|---|---|---| +| «la banda e' la scelta che si fa al buio?» | **non ha risoluzione** | cambia verdetto ogni notte | +| «la banda non e' stata selezionata sull'hold-out?» | **non ha potenza** | 1 cella su 30 e' l'argmax dell'hold-out → **29/30 lo passano per costruzione** | + +📌 *Lettura (agente, fallibile).* Il 25% non viene dalla griglia affatto: viene dall'argomento +**strutturale** del 27/07 — una banda in dollari assoluti degenera al variare del capitale, una +frazione no. Sotto la mediana delle fasi il 25% e' **2° in-sample e 3° sull'hold-out**: decente +ovunque, primo da nessuna parte, che e' la firma di un parametro NON selezionato sui dati. A un +parametro scelto per ragioni strutturali non si applica un gate di *selezione* ma uno di +*robustezza* — «fa danno da qualche parte?». Risposta: no. + +**Esito:** gate (A) **ritirato** come pass/fail, congelando il MOTIVO (come il 07/08), piu' una +guardia sulla CAUSA che si rompe il giorno che la fase venisse ancorata al calendario. + +## 3. Cosa cambia togliendo GTAA01 — e la decisione dell'operatore + +`r0828_senza_gtaa.py`. Portafoglio di **ricerca** a 5 sleeve; NON il book live. + +| | Sharpe | CAGR | maxDD | vol | +|---|---|---|---|---| +| CON GTAA01 | **2,28** | +19,2% | 6,1% | 7,8% | +| SENZA, iso-nozionale | 2,16 | **+23,8%** | 7,8% | 10,1% | +| SENZA, **iso-rischio** (×0,77) | 2,16 | +18,0% | 6,0% | 7,8% | + +A iso-nozionale togliendolo si guadagna **drift** e si compra **volatilita'**. A iso-rischio — la +sola lente onesta per un diversificatore a basso CAGR (M6) — **Δ Sharpe +0,121**, maxDD invariato. +Hold-out 2025+: **Δ +0,247**. Aiuta in **6 anni su 8**. Correlazione col resto **+0,087**: e' +davvero altro. Standalone Sharpe 1,06, CAGR +5,8%. + +⚠️ **Con la sua fascia di fase:** il Δ sulle 5 fasi va da **+0,095 a +0,124** e supera la soglia +dichiarata (0,12) in **2 fasi su 5**. Il **segno e' stabile**, il verdetto **binario** no. + +### La decisione: rinviata a $15k di book + +Discussa con l'operatore. L'argomento che ha deciso non e' «GTAA01 non contribuisce» — la misura +dice il contrario — ma che **a questo capitale lo sleeve non e' accendibile**: `GTAA_MIN_CAPITAL` +e' **$3.000 allocati**, che al peso 20% significa **$15.000 di book** contro i **$2.068** attuali. +Sotto quella soglia e' manutenzione senza beneficio incassabile. + +Due ragioni registrate **contro** il blocco immediato, entrambe regole del progetto: +- **N7** — uno sleeve difensivo si giudica **sul sinistro, non sul premio**, e la finestra + 2019-2026 non contiene il sinistro per cui esiste un GTAA a sei gambe; +- **N4** — il rischio di venue si compra con un **conto**, non con uno sleeve: togliere GTAA01 + renderebbe il portafoglio di ricerca **100% cripto su un venue solo**. + +E un costo che sarebbe andato pagato, non saltato: GTAA01 e' uno dei **quattro** nomi di +`r0726_wall_fixedpoint.W_DEPLOY`, da cui escono i **$313k** e tutta la pianificazione da €500/mese. + +**Perche' la decisione non si dimentichi** (N9), la soglia e' sorvegliata dal giornale: +`journal.SOGLIE_CAPITALE` legge l'equity ogni giorno e il giorno che supera $15k la voce lo dice, +con la domanda da rispondere accanto. Stessa tabella, seconda riga: **$20k**, dove si riapre +«100% Deribit fino a $20k». + +## 4. Allarmi: il marcatore «gia' detto» si scrive DOPO l'invio + +Debito §5.2 chiuso su decisione dell'operatore. `venue_watch.run_once` salvava lo stato coi +marcatori `alerted` gia' a True e l'invio lo faceva il chiamante **dopo**: col 6,9% di invii +falliti misurato (2/29), un 🚨 perso restava perso per l'**episodio intero** (200-2.324 ore). + +Ora il sender e' **iniettato** (la funzione resta testabile senza rete d'uscita, che era la ragione +del disegno precedente) e su fallimento si disfano i **soli marcatori**, non le misure: +asset in ALERT → ri-allerta l'ora dopo; lock MAINT/ALERT → flag azzerati **ma le ore continuano a +correre**, cosi' una manutenzione che sfora la grazia sale ad ALERT anche col trasporto giu'; +lock RIENTRATO → stato ripristinato, perche' il rientro si annuncia una volta sola. +`notify()` accetta `tentativi` (venue_watch: 3) e l'esito finisce nel log del cron. + +⚠️ Resta scoperto **ogni altro chiamante di `notify()`**: la disciplina e' oggi solo in venue_watch. + +## 5. L'analista del 27/08, e la riga di diagnosi che mancava + +La voce del 27/08 era senza analisi. Causa, dal transcript della sessione headless: +`Failed to authenticate: OAuth session expired and could not be refreshed`, exit 1 alle +`00:37:00.98Z`. La meccanica aveva retto (nessuna analisi vecchia spacciata per nuova, esito nel DB, +notifica partita) ma il motivo registrato era `uscita 1: ` — vuoto: `interroga()` leggeva il solo +**stderr**, e la CLI aveva scritto su **stdout**. Riparato (`motivo_uscita`), analisi recuperata. +Guasto **transitorio**: il refresh token era valido, unico caso su 24 sessioni. + +## 6. Fisco: la domanda (a) ha una risposta alla fonte + +Circolare AdE **30/E del 27/10/2023**, letta direttamente dal PDF ufficiale. **I derivati con +sottostante cripto stanno in `c-quater` → 26%**, non in `c-sexies` → 33%, e la circolare dice +esplicitamente che la riforma 2023 non sposta nulla. Il **collaterale** sconta il **2‰** al 31/12 +(intermediario non residente → imposta sul valore delle cripto-attivita', ~$4/anno a questo +capitale). E la conversione **USDC→USDE non e' fattispecie realizzativa**. Dettaglio e citazioni +verbatim in CLAUDE.md §5.4. La (b), i payout di una prop firm, **resta aperta**. + +## Cosa cambia + +- `CLAUDE.md`: §3 nuova riga (GTAA01 rinviato a $15k) · §5.2 risolto · §5.4 riscritto · §5.13 nuovo + debito (la fase che ruota, quantificata e innocua oggi). +- `src/live/journal.py`: `SOGLIE_CAPITALE` + regola `soglia_capitale`. +- `src/live/venue_watch.py`, `src/live/notifier.py`, `scripts/live/venue_watch.py`: commit dopo l'invio. +- `src/live/analista.py`: `motivo_uscita` da stdout E stderr. +- `tests/test_gtaa_band_gate.py`: gate (A) ritirato, motivo e causa congelati. +- Script nuovi: `r0828_gtaa_band_phase.py`, `r0828_gtaa_band_median_phase.py`, `r0828_senza_gtaa.py`. + +**Suite: 789 passati, 0 falliti.** diff --git a/src/live/journal.py b/src/live/journal.py index 3f969ec..e19f4af 100644 --- a/src/live/journal.py +++ b/src/live/journal.py @@ -264,6 +264,37 @@ def salva(con, voce: dict, nota: str | None = None) -> None: # (c) MUTA sul non misurato — se il numero e' None la regola tace, non arrotonda a zero. # Il campo `nota` resta l'unico posto dove puo' finire un giudizio umano. +# ============================================================================================= +# DECISIONI PARCHEGGIATE SU UNA SOGLIA DI CAPITALE +# ============================================================================================= +# N9: quando una decisione viene rinviata con un motivo, il motivo va registrato — altrimenti fra +# tre mesi la stessa analisi si ripresenta come se fosse nuova. E una decisione parcheggiata su un +# numero che NESSUNO sorveglia e' parcheggiata per sempre: qui la sorveglianza e' l'equity, che il +# giornale legge comunque ogni giorno. +# +# Una riga si TOGLIE quando la decisione e' presa. Finche' c'e', la voce del giorno la ripete: e' +# voluto — non e' un allarme che chiede un'azione impossibile (P14), e' una domanda che da quel +# giorno in poi si puo' finalmente rispondere. +SOGLIE_CAPITALE: tuple[tuple[float, str], ...] = ( + (15_000.0, "**GTAA01: tenere o bloccare.** Decisione rinviata a questa soglia il 2026-08-28 " + "(diario `2026-08-28-gtaa-fase-e-allarmi`). Sotto $15k di book lo sleeve NON e' " + "deployabile — `GTAA_MIN_CAPITAL` $3.000 allocati al peso 20% — quindi finora e' " + "stato manutenzione senza beneficio incassabile. Contributo misurato a iso-rischio " + "+0,095/+0,124 di Sharpe secondo la fase: reale, sempre positivo, e delle stesse " + "dimensioni dello spread fra le baseline."), + (20_000.0, "**«100% Deribit fino a $20k»** (decisione del 26/07): la soglia e' arrivata, e con " + "essa la domanda del secondo conto (N4: il rischio di venue si compra con un CONTO, " + "non con uno sleeve)."), +) + + +def soglie_superate(equity: float | None) -> list[tuple[float, str]]: + """PURA. Le decisioni parcheggiate che a questa equity si possono finalmente prendere.""" + if equity is None: + return [] + return [(s, t) for s, t in SOGLIE_CAPITALE if equity >= s] + + def _bucket_convinzione(tp: float) -> str: """TP01 media tre `np.sign()` e poi taglia a long-flat: la direzione vive in {0, 1/3, 1}. @@ -469,6 +500,10 @@ def lettura(con, voce: dict) -> list[dict]: dire("evidenza", f"Campione a oggi: **{gg} giorni, {n_rt} round-trip**. A questa taglia " "il P&L non distingue l'edge dalla fortuna — il criterio di morte del " "libro (`edge_watch`) lavora su una finestra di 36 mesi.", "nota") + + # --- decisioni che questa equity sblocca ------------------------------------------------- + for soglia, testo in soglie_superate(p.get("equity_fine")): + dire("soglia_capitale", f"📌 **Soglia ${soglia:,.0f} superata** — {testo}", "attenzione") return out def _n(v, fmt="{:+.2f}", vuoto="n/d"): diff --git a/tests/test_gtaa_band_gate.py b/tests/test_gtaa_band_gate.py index 1465f76..95ad0e7 100644 --- a/tests/test_gtaa_band_gate.py +++ b/tests/test_gtaa_band_gate.py @@ -101,19 +101,50 @@ def griglia_piena(): return G -def test_la_banda_proposta_e_quella_scelta_al_buio(griglia_piena): - """Il gate (A), nella forma decidibile (2026-08-07). +# ⚠️ 2026-08-28 — SECONDO CRITERIO RITIRATO, e per la stessa ragione del primo. +# `test_la_banda_proposta_e_quella_scelta_al_buio` asseriva che la banda scelta sui soli dati +# pre-2015 fosse la proposta. Ha smesso di passare senza che nessuno toccasse il codice: al buio +# esce il 60%, con margine 0.0074 — sotto il pavimento di 0.01 che il test stesso si era dato. +# Misurato in `r0828_gtaa_band_phase.py`: NON e' un fatto sulla banda, e' un fatto sul criterio. +# · IB serve una finestra ROTOLANTE di 30 anni e SPY e' al muro: la sua prima barra avanza di +# un giorno di borsa ogni notte. Il pre-2015 — che il progetto tratta come congelato — perde +# il giorno piu' vecchio ogni notte. +# · `gtaa._gated_returns` ribilancia su `i % every == 0`, cioe' sulla POSIZIONE nell'array: +# se la prima barra scivola, si ri-fasa ogni decisione di trent'anni. La fase canonica cambia +# da sola. +# · Esito: su 10 notti consecutive la scelta al buio e' 25% / 40% / 60%, e il margine sta sotto +# il pavimento in 4 notti su 10. A dato FISSO, muovendo la sola fase, il verdetto cambia lo +# stesso (25% su 3 fasi, 60% su 2). +# Come il 07/08 si congela il MOTIVO e non l'esito. La riparazione (ancorare la fase al calendario) +# e la decisione su GTAA01 sono rinviate alla soglia di $15k di book — sotto quella soglia lo +# sleeve non e' deployabile e la decisione non ha conseguenze (`journal.SOGLIE_CAPITALE`). - La proposta del 27/07 e' una BANDA: la cadenza settimanale e' gia' `REBAL_EVERY=5` in - produzione e non era in discussione. Quindi la domanda sulla provenienza della scelta e' - «a cadenza di produzione, quale banda si sceglie guardando SOLO il pre-2015?». - """ + +def test_il_gate_A_non_ha_RISOLUZIONE_ed_e_per_questo_che_e_stato_RITIRATO(griglia_piena): + """Congela il MOTIVO del secondo ritiro: il criterio decide su un margine piu' piccolo del + rumore che lo scuote. Non asserisce QUALE cella vinca — quella si muove da sola ogni notte, + ed e' esattamente il punto.""" G = griglia_piena riga = G[G["every"] == BG.PROPOSTA[0]] - blind = riga.sort_values("sh_in", ascending=False).iloc[0] - assert blind["frac"] == BG.PROPOSTA[1], ( - f"al buio si sceglierebbe la banda {blind['frac']:.0%}, non {BG.PROPOSTA[1]:.0%}: " - "la validazione del 27/07 andrebbe rifatta su quella cella") + ordinata = riga.sort_values("sh_in", ascending=False) + margine = float(ordinata["sh_in"].iloc[0] - ordinata["sh_in"].iloc[1]) + spread = float(riga["sh_in"].max() - riga["sh_in"].min()) + # il margine su cui il criterio decideva e' una FRAZIONE dello spread della griglia: + # a quella scala la scelta non e' una misura, e' l'ordine in cui il rumore si e' posato. + assert margine < 0.25 * spread, ( + f"il margine ({margine:.4f}) e' tornato grande rispetto allo spread di griglia " + f"({spread:.4f}): se il rumore si fosse ridotto, il gate (A) andrebbe RIABILITATO " + "invece di restare ritirato — ed e' un fatto da guardare, non da ignorare") + + +def test_la_fase_di_ribilanciamento_e_ancora_ancorata_alla_POSIZIONE(griglia_piena): + """La guardia sulla CAUSA. Se un giorno `_gated_returns` ancorasse la fase al calendario, + questo test si rompe e va riletto tutto: il gate (A) potrebbe tornare decidibile.""" + import inspect + src = inspect.getsource(BG.gtaa_returns.__globals__["_gated_returns"]) + assert "i % every" in src, ( + "la fase non e' piu' ancorata alla posizione nell'array: la ragione del ritiro del " + "gate (A) e' venuta meno, rileggere r0828_gtaa_band_phase.py") def test_chi_guardasse_l_hold_out_sceglierebbe_una_banda_DIVERSA(griglia_piena): @@ -128,15 +159,11 @@ def test_chi_guardasse_l_hold_out_sceglierebbe_una_banda_DIVERSA(griglia_piena): "potenza e non va citato come validazione") -def test_il_margine_del_blind_non_e_un_arrotondamento(griglia_piena): - """La ragione per cui questo criterio ha sostituito il confronto fra ranghi: decide su - ~0.015 di Sharpe invece che su ~0.001. Se il margine scendesse sotto un centesimo, anche - questo criterio smetterebbe di essere una misura e andrebbe ripensato a sua volta.""" - G = griglia_piena - riga = G[G["every"] == BG.PROPOSTA[0]] - blind = riga.sort_values("sh_in", ascending=False).iloc[0] - secondo = riga[riga["frac"] != blind["frac"]]["sh_in"].max() - assert blind["sh_in"] - secondo >= 0.01 +# `test_il_margine_del_blind_non_e_un_arrotondamento` (2026-08-07) chiedeva margine >= 0.01 e +# diceva, testualmente: «Se il margine scendesse sotto un centesimo, anche questo criterio +# smetterebbe di essere una misura e andrebbe ripensato a sua volta». Il 28/08 e' sceso a 0.0074. +# Il test aveva ragione e si e' auto-ritirato: la sua condizione e' ora dentro il test sul MOTIVO +# qui sopra, dove misura il margine CONTRO lo spread di griglia invece che contro un assoluto. def test_il_confronto_fra_ranghi_e_una_moneta_ed_e_per_questo_che_e_stato_RITIRATO(griglia_piena): diff --git a/tests/test_journal.py b/tests/test_journal.py index 5c0db41..8874ab6 100644 --- a/tests/test_journal.py +++ b/tests/test_journal.py @@ -467,3 +467,37 @@ def test_la_concentrazione_non_conta_i_versamenti_come_pnl(db, monkeypatch): righe = J.lettura(db, _voce(pnl=p, giorno="2026-08-25")) conc = [r for r in righe if r["id"] == "concentrazione"] assert conc == [] # il cumulato di TRADING e' 0: la regola deve tacere + + +# ============================================================================================= +# DECISIONI PARCHEGGIATE SU UNA SOGLIA DI CAPITALE +# ============================================================================================= +# Il 2026-08-28 la decisione «GTAA01: tenere o bloccare» e' stata rinviata a $15k di book, che e' +# la soglia sotto la quale lo sleeve non e' deployabile. Una decisione parcheggiata su un numero +# che nessuno sorveglia e' parcheggiata per sempre (N9): questi test blindano la sorveglianza. + +def test_sotto_soglia_non_dice_niente(): + assert J.soglie_superate(2_068.0) == [] + + +def test_la_soglia_gtaa_e_15k_e_scatta_quando_l_equity_arriva(): + soglie = [s for s, _ in J.SOGLIE_CAPITALE] + assert 15_000.0 in soglie, "la soglia di GTAA01 non e' piu' dichiarata" + assert J.soglie_superate(14_999.99) == [] + assert [s for s, _ in J.soglie_superate(15_000.0)] == [15_000.0] + + +def test_le_soglie_si_accumulano_e_restano_ordinate(): + """A $20k si devono presentare ENTRAMBE: la seconda non cancella la prima.""" + assert [s for s, _ in J.soglie_superate(20_000.0)] == [15_000.0, 20_000.0] + + +def test_equity_non_leggibile_non_e_una_soglia_superata(): + """P5: «non vedo» non e' «va tutto bene» — e non e' nemmeno «la soglia e' arrivata».""" + assert J.soglie_superate(None) == [] + + +def test_ogni_soglia_dice_COSA_decidere_non_solo_che_e_arrivata(): + """P4: un'allerta risponde a due domande. Una riga che dice solo «sei a $15k» non serve.""" + for _, testo in J.SOGLIE_CAPITALE: + assert len(testo) > 80 and "**" in testo, "la soglia non dice quale decisione sblocca"