journal: la voce del giorno IN CORSO non si presenta piu' come una giornata

cron_daily (00:30 UTC) faceva DUE chiamate al giornale: chiudeva IERI (giusto)
e apriva OGGI. La pagina di oggi nasceva con ~30 minuti 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 chiusa -> passava ogni controllo di
completezza e freschezza, e la parzialita' stava solo in §Salute, quinta
sezione su otto. Stessa famiglia di "una barra presente non e' una giornata
presente", in veste nuova: la pagina e' presente, il giorno no.

Taglia misurata sul caso reale di oggi: la pagina congelata alle 00:37 diceva
"giorno: $+0.54 (1 letture)" e la regola [pnl] chiosava "senza operare". Alle
08:54 il libro aveva fatto 3 fill, 2 round-trip e +$25.86 ($642.56 -> $667.88).
Avrebbe raccontato una giornata ferma per tutta una giornata operativa.

Riparato in due pezzi indipendenti:
  1. la pagina dichiara la parzialita' nel TITOLO e nella prima riga —
     "PARZIALE (giorno in corso)", copertura esplicita (N giri su 24), il fatto
     che non si aggiorna da sola, e quali numeri sono di quella frazione;
  2. tolta la seconda chiamata dal cron. In docs/journal/ restano solo giorni
     chiusi; lo stato corrente si prende da journal.py a mano (pagina marcata)
     o da trades.db, che cron_book sincronizza ogni ora.

Scelta dichiarata: NON si rigenera ogni ora. Costerebbe una riga in cron_book
ma riscriverebbe un file tracciato da git 24 volte al giorno per un consumatore
che non esiste (l'analista legge il giorno chiuso). Se un domani servisse, la
strada e' RIGENERARLA, non congelarla: e' scritto nel commento del cron.

Prove (test_journal 28 -> 31): una accende la marcatura, una la tiene spenta su
un giorno chiuso (una marcatura sempre accesa non si legge), una e' guardia sul
SORGENTE di cron_daily.sh — ogni chiamata a journal.py deve avere --giorno
esplicito, perche' senza argomenti scrive OGGI.

Regolare docs/journal/2026-08-25.md rigenerato: ora si dichiara PARZIALE (9/24).

NON riparato, e registrato come debito aperto in CLAUDE.md §5:
test_wave_0726::test_t1_canonical_riproduce_il_backtest_ufficiale fallisce, e
falliva gia' prima di questa modifica (verificato con git stash sull'albero
pulito). 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, il cron lo ricostruisce ogni notte). La banda NON e' stata
allargata — lezione 07/08. Suite: 710 passati, 1 fallito.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Adriano Dal Pastro
2026-08-25 08:59:42 +00:00
parent 11a2370027
commit 2751a7efd0
6 changed files with 158 additions and 20 deletions
+7 -1
View File
@@ -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.
---
+14 -15
View File
@@ -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
+61
View File
@@ -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.**
+7 -2
View File
@@ -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.
+18 -2
View File
@@ -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 |",
"|---|---|---|---|---|---|---|---|"]
+51
View File
@@ -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}"