diff --git a/CLAUDE.md b/CLAUDE.md index ca4153f..23e7234 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -182,7 +182,13 @@ d'ancora in modo diverso (nella differenza si cancella in parte, nel livello per sposterebbe il libro sulla riga 4h, dove BTC **raddoppia** gli scatti del disaster-SL. 8. ⚠️ **Il Rulebook Deribit** (ADL, perdita socializzata, *emergency powers*, conti dormienti) **non lo sorveglia nessuno**; `MAINT_GRACE_HOURS`=2 **presume** gli annunci invece di leggerli. -9. ⚠️ **Nessuna tabella pubblicata prima del 22/08 contiene fisco E funding insieme.** L'unica +9. ⚠️ **`tests/test_wave_0726.py::test_t1_canonical_riproduce_il_backtest_ufficiale` FALLISCE** + (dal ~24-25/08): Sharpe hold-out SKH01 `canonical` **1,9223** contro la banda cablata + `1.3 < h < 1.9`. **Il codice non e' cambiato, sono cambiati i DATI** — `data/raw/` e' gitignored + e il cron lo ricostruisce ogni notte, quindi la finestra si allunga e il numero deriva (verso + l'**alto**). **La banda non e' stata allargata**: si guarda sotto prima di toccarla (lezione + 07/08). Suite: **710 passati, 1 fallito**. +10. ⚠️ **Nessuna tabella pubblicata prima del 22/08 contiene fisco E funding insieme.** L'unica congiunta e' quella in `30-piano-capitale-fisco.md` — le altre vanno lette con lo sconto. --- diff --git a/docs/journal/2026-08-25.md b/docs/journal/2026-08-25.md index fd1c66a..8b69fec 100644 --- a/docs/journal/2026-08-25.md +++ b/docs/journal/2026-08-25.md @@ -1,6 +1,6 @@ -# Giornale di bordo — 2026-08-25 +# Giornale di bordo — 2026-08-25 · ⚠️ PARZIALE (giorno in corso) -*Scritto 2026-08-25T00:37:36+00:00. Numeri misurati; nessuna interpretazione automatica.* +*Scritto 2026-08-25T08:54:46+00:00 — la giornata NON e' chiusa: la pagina copre 9 giri su 24 e **non si aggiorna da sola**. I numeri di GIORNO (P&L del giorno, letture, giri) sono di questa frazione, non del giorno; i CUMULATI sono corretti. La versione completa la scrive il cron dopo le 00:30 UTC del giorno seguente.* ## Mercato @@ -11,36 +11,35 @@ ## Libro -Equity **$642.56** · nozionale lordo $256 · leva lorda 0.40x · barra dati 2026-08-24 · 1 giri +Equity **$667.88** · nozionale lordo $337 · leva lorda 0.50x · barra dati 2026-08-25 · 9 giri | | TP01 | SKH01 | target | posizione | azione | |---|---|---|---|---|---| -| **BTC** | +0.449 | flat | $+108 | $+111 | HOLD (a target) | -| **ETH** | +0.271 | LONG @ 2,436.4 | $+146 | $+145 | HOLD (a target) | +| **BTC** | +0.450 | flat | $+113 | $+192 | SELL $-79 | +| **ETH** | +0.274 | LONG @ 2,436.4 | $+152 | $+145 | BUY $+7 | ## P&L -- giorno: **$+0.54** di equity (1 letture) -- realizzato: +0.00 netto su 0 round-trip chiusi · 0 fill · fee 0.0000 -- cumulato dall'arming: **$+44.50** +- giorno: **$+25.86** di equity (9 letture) +- realizzato: +1.90 netto su 2 round-trip chiusi · 3 fill · fee 0.0585 +- cumulato dall'arming: **$+69.82** ## Salute -- giri di `book_execute`: 1/1 *(giorno in corso)* +- giri di `book_execute`: 9/9 *(giorno in corso)* - eta' feed SKH all'ultimo giro: 0 min ## Lettura *Generata da regole dichiarate in `src/live/journal.py` — combina i numeri qui sopra e nient'altro. L'id fra parentesi quadre dice quale regola ha parlato. Non e' un giudizio: quello sta nella Nota.* -- `[stato]` A mercato su BTC $+111 (TP01 +0.449, SKH01 flat) e ETH $+145 (TP01 +0.271, SKH01 long). -- `[leva]` Leva lorda **0.40x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 0.60x. -- `[pnl]` Equity $+0.54 **senza operare**: e' mark-to-market sulle posizioni gia' aperte. -- `[concentrazione]` 📌 **Il 102% di tutto il P&L cumulato ($+44.50) viene dagli ultimi 7 giorni.** Oltre il 100% perche' **il periodo precedente era in perdita**: senza questi 7 giorni il libro sarebbe sotto. Un risultato concentrato in una finestra non e' un tasso di rendimento: e' un evento. -- `[drawdown]` 📌 Equity **-0.68%** sotto il picco ($646.95). ⚠️ e' un DD su letture ORARIE, non sul minimo intra-giorno. +- `[stato]` A mercato su BTC $+192 (TP01 +0.450, SKH01 flat) e ETH $+145 (TP01 +0.274, SKH01 long). +- `[leva]` Leva lorda **0.50x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 0.50x. +- `[pnl]` Equity $+25.86 con 3 fill e 2 round-trip chiusi (realizzato $+1.90 netto). +- `[concentrazione]` 📌 **Il 101% di tutto il P&L cumulato ($+69.82) viene dagli ultimi 7 giorni.** Oltre il 100% perche' **il periodo precedente era in perdita**: senza questi 7 giorni il libro sarebbe sotto. Un risultato concentrato in una finestra non e' un tasso di rendimento: e' un evento. - `[vol]` BTC: implicita 43.5 contro realizzata 30g 43.4 (sopra); DVOL al 60° pctl di un anno; IV-rank espandente 0.20 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). - `[vol]` ETH: implicita 58.5 contro realizzata 30g 68.7 (sotto); DVOL al 49° pctl di un anno; IV-rank espandente 0.21 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo). -- `[evidenza]` 📌 Campione a oggi: **63 giorni, 14 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. +- `[evidenza]` 📌 Campione a oggi: **63 giorni, 16 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 diff --git a/docs/memory/40-produzione-e-deploy.md b/docs/memory/40-produzione-e-deploy.md index 4f43a98..ee9c781 100644 --- a/docs/memory/40-produzione-e-deploy.md +++ b/docs/memory/40-produzione-e-deploy.md @@ -739,3 +739,64 @@ e i vincoli di deploy (PRIIPs/UCITS/broker). include anche lo storico, e bocciava un'equity vera); (c) **una regola che si accende su $2 di cumulato insegna a saltare la sezione**: serve una soglia di rilevanza; (d) chi scrive in un registro va **firmato**, o il registro perde il suo valore probatorio. + +--- + +## Addendum 2026-08-25 — la voce del GIORNO IN CORSO si presentava come una giornata + +*(scritto 2026-08-25T08:59:12Z, ora letta da `date -u`)* + +`cron_daily` gira alle **00:30 UTC** e faceva **due** chiamate al giornale: chiudeva IERI +(giorno completo, corretto) **e apriva OGGI**. La pagina di oggi nasceva quindi con **~30 minuti +di giornata dentro** e **nessuno la aggiornava fino alla notte dopo**. + +🚨 **Il difetto non era il dato mancante, era che la pagina non lo diceva dove si legge.** Aveva +**tutte le sezioni** di una pagina chiusa — Mercato, Libro, P&L, Salute, Lettura, Nota — quindi +passava ogni controllo di completezza e di freschezza; la parzialita' era dichiarata **solo in +§Salute**, quinta sezione su otto (`giri di book_execute: 1/1 *(giorno in corso)*`), mentre +titolo, P&L e le regole della Lettura parlavano al passato di una frazione di giornata. +E' la stessa famiglia gia' codificata due volte — *"una riga presente non e' un dato presente"*, +*"una barra presente non e' una giornata presente"* — in una veste nuova: **la pagina e' +presente, il giorno no**. + +📌 **La taglia, misurata sul caso reale del giorno stesso** (ed e' il motivo per cui non era un +difetto cosmetico): la pagina congelata alle 00:37 diceva **"giorno: $+0.54 di equity (1 letture)"** +e la regola `[pnl]` chiosava **"senza operare: e' mark-to-market sulle posizioni gia' aperte"**. +Alle 08:54 dello stesso giorno il libro aveva fatto **3 fill, 2 round-trip chiusi e +$25.86** di +equity ($642.56 → $667.88). **La pagina avrebbe raccontato una giornata ferma per tutta una +giornata operativa**, e chi l'avesse letta — o un modello che se la fosse ritrovata nel prompt — +non aveva modo di accorgersene senza contare i giri. + +✅ **Riparato in due pezzi indipendenti:** +1. **La pagina dichiara la propria parzialita' nel TITOLO e nella prima riga** + (`src/live/journal.py::rendi_markdown`): `⚠️ PARZIALE (giorno in corso)` + copertura + esplicita (*"copre N giri su 24"*), il fatto che **non si aggiorna da sola**, e quali numeri + sono di quella frazione (giorno) e quali restano corretti (cumulati). +2. **Il cron non la congela piu'**: tolta la seconda chiamata da `scripts/cron_daily.sh`. In + `docs/journal/` restano **solo giorni chiusi**; chi vuole lo stato corrente lancia + `journal.py` a mano (e riceve una pagina marcata PARZIALE) o legge `trades.db`, che + `cron_book` sincronizza **ogni ora**. + +⚖️ **Scelta dichiarata: NON si rigenera la voce di oggi ogni ora.** Sarebbe l'altra soluzione +difendibile e costa una riga in `cron_book`, ma riscriverebbe un file **tracciato da git 24 +volte al giorno** per un consumatore che oggi non esiste (l'analista legge il giorno **chiuso**). +Se un domani servisse la voce sempre fresca, **la strada e' rigenerarla, non congelarla**: e' +scritto nel commento del cron perche' chi ci torna non debba ri-derivarlo. + +**Prove (3, in `tests/test_journal.py`, 28 → 31):** una **accende** la marcatura su un giorno in +corso e verifica titolo + copertura + *"non si aggiorna da sola"*; una la **tiene spenta** su un +giorno chiuso (*una marcatura sempre accesa non si legge*); la terza e' una **guardia sul +SORGENTE** di `cron_daily.sh` — ogni chiamata a `journal.py` deve avere `--giorno` esplicito, +perche' `journal.py` **senza argomenti scrive OGGI**. La guardia **non vieta** di rigenerare +spesso: vieta di scrivere la pagina **una volta e lasciarla li'**. + +⚠️ **Trovato girando la suite completa, NON riparato — e' un'altra cosa:** +`tests/test_wave_0726.py::test_t1_canonical_riproduce_il_backtest_ufficiale` **fallisce**, e +falliva gia' prima di questa modifica (verificato con `git stash`: stesso fallimento sull'albero +pulito). Lo Sharpe hold-out di SKH01 `canonical` vale **1,9223** contro la banda cablata +`1.3 < h < 1.9`. **Il codice non e' cambiato: sono cambiati i DATI** — `data/raw/` e' gitignored +e il cron lo ricostruisce ogni notte, quindi la finestra hold-out si allunga e il numero deriva +(verso l'**alto**, cioe' non e' un peggioramento). **La banda NON e' stata allargata**: e' la +lezione del 2026-08-07 (*"un test che fallisce senza che il codice sia cambiato sta segnalando +che i dati non sono versionati — si guarda sotto prima di toccarlo"*), e allargare una tolleranza +per far passare un test e' esattamente la manovra che quella lezione vieta. **Resta aperto.** diff --git a/scripts/cron_daily.sh b/scripts/cron_daily.sh index c2c9af6..375f619 100755 --- a/scripts/cron_daily.sh +++ b/scripts/cron_daily.sh @@ -39,10 +39,15 @@ mkdir -p logs # decidono su queste serie: un monitor fermo produce silenzio, e il silenzio sembra uno zero. # Va DOPO tutti i monitor, altrimenti misura lo stato di ieri. uv run python scripts/live/monitor_health.py --quiet || true - # Giornale di bordo. Gira alle 00:30 UTC -> chiude IERI (giorno completo) e apre OGGI. + # Giornale di bordo. Gira alle 00:30 UTC -> chiude IERI, che a quest'ora e' un giorno COMPLETO. # Sola lettura: feed certificato, cron_book.log, trades.db. Il campo `nota` non lo tocca. uv run python scripts/live/journal.py --giorno "$(date -u -d yesterday +%F)" --quiet || true - uv run python scripts/live/journal.py --quiet || true + # NON si apre piu' la voce di OGGI (tolto 2026-08-25). A quest'ora coprirebbe ~30 minuti e + # nessuno la aggiorna fino alla notte dopo: una pagina con TUTTE le sezioni, che si legge come + # una giornata e ne contiene mezz'ora. Chi vuole lo stato corrente lancia `journal.py` a mano + # (la pagina si dichiara PARZIALE nel titolo) o legge trades.db, che cron_book sincronizza ogni + # ora. Se un giorno servisse la voce sempre fresca, la strada e' RIGENERARLA ogni ora in + # cron_book, non congelarla qui. # Analista di bordo: un modello scrive la prosa del giorno CHIUSO nel campo `analisi`. # Scrive SOLO li': non tocca `nota` (operatore) ne' le sezioni misurate, e se non risponde # la pagina resta senza analisi invece di mostrare quella di ieri. Costo: 1 chiamata/giorno. diff --git a/src/live/journal.py b/src/live/journal.py index 48dd17e..ea44777 100644 --- a/src/live/journal.py +++ b/src/live/journal.py @@ -368,8 +368,24 @@ def rendi_markdown(con, voce: dict) -> str: riga = con.execute("SELECT * FROM journal WHERE giorno=?", (g,)).fetchone() nota = (riga["nota"] if riga else "") or "" an = dict(riga) if riga else {} - L = [f"# Giornale di bordo — {g}", "", - f"*Scritto {voce['ts_scritto']}. Numeri misurati; nessuna interpretazione automatica.*", "", + # Un giorno IN CORSO ha tutte le sezioni di uno chiuso -> passa ogni controllo di + # completezza e si legge come una giornata intera. La parzialita' va dichiarata nel TITOLO + # e nella prima riga, non nella quinta sezione su otto: chi apre la pagina — o un modello + # che se la ritrova nel prompt — legge quelle due. Stessa famiglia di "una barra presente + # non e' una giornata presente": qui la pagina e' presente, il giorno no. + if s.get("giorno_in_corso"): + testata = [ + f"# Giornale di bordo — {g} · ⚠️ PARZIALE (giorno in corso)", "", + f"*Scritto {voce['ts_scritto']} — la giornata NON e' chiusa: la pagina copre " + f"{s.get('giri_book', 0)} giri su 24 e **non si aggiorna da sola**. I numeri di " + f"GIORNO (P&L del giorno, letture, giri) sono di questa frazione, non del giorno; " + f"i CUMULATI sono corretti. La versione completa la scrive il cron dopo le 00:30 " + f"UTC del giorno seguente.*"] + else: + testata = [ + f"# Giornale di bordo — {g}", "", + f"*Scritto {voce['ts_scritto']}. Numeri misurati; nessuna interpretazione automatica.*"] + L = testata + ["", "## Mercato", "", "| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL |", "|---|---|---|---|---|---|---|---|"] diff --git a/tests/test_journal.py b/tests/test_journal.py index fdf2c99..4f06261 100644 --- a/tests/test_journal.py +++ b/tests/test_journal.py @@ -307,3 +307,54 @@ def test_concentrazione_si_accende_quando_la_taglia_conta(db): db.commit() v = _voce(libro=_libro(BTC=dict(posizione=100.0)), pnl=_pnl(), giorno="2026-06-28") assert "concentrazione" in _ids(J.lettura(db, v)) + + +# ------------------------------------------- il giorno IN CORSO non si presenta come chiuso +# +# Una pagina di giorno in corso ha TUTTE le sezioni di una chiusa: passa ogni controllo di +# completezza e freschezza, e si legge come una giornata. Fino al 2026-08-25 lo diceva solo +# la §Salute — quinta sezione su otto — mentre titolo, P&L e Lettura parlavano al passato di +# una frazione di giornata. Le due prove sono accoppiate: una accende la marcatura, l'altra +# verifica che su un giorno chiuso resti SPENTA (una marcatura sempre accesa non si legge). + +def _voce_salute(con, in_corso: bool, giri: int): + return dict(giorno="2026-08-25", ts_scritto=T.ora(), mercato={}, + libro=dict(giri=giri, ultimo_giro="2026-08-25T07:07:01+00:00", equity=642.0, + barra="2026-08-25", feed_skh_min=0, nozionale_lordo=200.0, + leva_lorda=0.31, asset={}), + pnl=J.pnl_giorno(con, date(2026, 8, 25)), + salute=dict(giri_book=giri, feed_skh_min=0, atteso_giri=giri if in_corso else 24, + giorno_in_corso=in_corso, giri_mancanti=0)) + + +def test_una_pagina_di_giorno_in_corso_si_dichiara_parziale_nel_titolo(db): + v = _voce_salute(db, in_corso=True, giri=8) + J.salva(db, v) + md = J.rendi_markdown(db, v) + titolo = md.splitlines()[0] + assert "PARZIALE" in titolo and "giorno in corso" in titolo # nel posto che si legge + testata = "\n".join(md.splitlines()[:6]) + assert "NON e' chiusa" in testata # e non solo nel titolo + assert "8 giri su 24" in testata # con la copertura dichiarata + assert "non si aggiorna da sola" in testata # e col fatto che e' congelata + + +def test_una_pagina_di_giorno_chiuso_non_si_dichiara_parziale(db): + v = _voce_salute(db, in_corso=False, giri=24) + J.salva(db, v) + md = J.rendi_markdown(db, v) + assert "PARZIALE" not in md and "non si aggiorna da sola" not in md + assert md.splitlines()[0] == "# Giornale di bordo — 2026-08-25" + + +def test_il_cron_non_apre_piu_la_voce_del_giorno_in_corso(): + """Guardia sul SORGENTE: `journal.py` senza `--giorno` scrive OGGI, e alle 00:30 UTC oggi + e' mezz'ora. Chi la rimettesse congelerebbe di nuovo una pagina che sembra una giornata. + Il test non vieta di rigenerarla spesso: vieta di scriverla UNA volta e lasciarla li'. + """ + cron = (ROOT / "scripts" / "cron_daily.sh").read_text() + chiamate = [r.strip() for r in cron.splitlines() + if "scripts/live/journal.py" in r and not r.strip().startswith("#")] + assert chiamate, "il giornale non gira piu' nel cron: e' un'altra rottura, non questa" + for r in chiamate: + assert "--giorno" in r, f"apre un giorno implicito (= oggi, parziale): {r}"