analista di bordo: un modello scrive la prosa del giorno, in un campo suo

Aggiunto il quarto livello della pagina, tenuto separato dagli altri tre:
  numeri   -> misurati dal feed e dal DB
  Lettura  -> regole deterministiche, ognuna col suo id
  Analisi  -> questo: prosa di un modello, che puo' sbagliare
  Nota     -> l'operatore

NON scrive dentro `nota`, che era la richiesta letterale: quel campo e'
dell'operatore, ed e' cio' che a rileggere il giornale fra sei mesi permette
di sapere chi ha scritto cosa. L'agente ha `analisi`, marcato col modello,
con l'ora e con l'esito del controllo sui numeri.

Gira via `claude -p` (verificato con env -i che risponda nell'ambiente nudo
di cron), una chiamata al giorno sul giorno CHIUSO, tolte le tool.

Tre guardie, una per ogni modo in cui una prosa generata rovina un registro:
- NUMERO INVENTATO: numeri_non_supportati() estrae ogni cifra dall'analisi e
  verifica che compaia in cio' che il modello ha ricevuto. Oltre tre numeri
  liberi l'analisi e' RIFIUTATA e la pagina resta senza. E' un controllo
  debole per costruzione, e lo dichiara: prende l'invenzione, non il
  ragionamento sbagliato.
- COMMENTO DI SE': senza_analisi() toglie dalla pagina la sezione dell'agente
  prima di dargliela. Al primo giro reale il modello aveva letto la propria
  uscita precedente e prodotto un paragrafo sull'avviso che si era preso il
  giorno prima — un ciclo di retroazione che in poche settimane avrebbe
  riempito il giornale di meta-commento, e che nessun controllo automatico
  puo' distinguere da prosa valida.
- ANALISI DI IERI SPACCIATA PER OGGI: se il modello non risponde, la pagina
  resta VUOTA e il perche' viene registrato (stato + motivo). Il silenzio non
  diventa continuita'.

Corretto anche un falso positivo mio: il tripwire validava sulla sola pagina
mentre il prompt include anche il blocco storico, quindi bocciava un'equity
vera. Una guardia piu' stretta del contratto produce allarmi che si impara a
ignorare.

Ogni guardia ha un test in entrambe le direzioni. 695 test passano.
Strategia, pesi, config INVARIATI. Nessun ordine.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Adriano Dal Pastro
2026-08-23 17:46:29 +00:00
parent 1803e0ac9f
commit f5d9409213
7 changed files with 512 additions and 3 deletions
+15 -2
View File
@@ -152,6 +152,8 @@ def costruisci(con, giorno: date) -> dict:
def salva(con, voce: dict, nota: str | None = None) -> None:
"""Ricalcola le sezioni misurate. NON tocca ne' `nota` (operatore) ne' `analisi` (agente):
sono le uniche due cose in pagina che un ricalcolo non puo' rigenerare."""
esistente = con.execute("SELECT nota FROM journal WHERE giorno=?", (voce["giorno"],)).fetchone()
# la nota dell'operatore NON viene mai sovrascritta da un ricalcolo automatico
nota_finale = nota if nota is not None else (esistente["nota"] if esistente else "")
@@ -363,8 +365,9 @@ def _n(v, fmt="{:+.2f}", vuoto="n/d"):
def rendi_markdown(con, voce: dict) -> str:
g, m, lb, p, s = voce["giorno"], voce["mercato"], voce["libro"], voce["pnl"], voce["salute"]
nota = con.execute("SELECT nota FROM journal WHERE giorno=?", (g,)).fetchone()
nota = nota["nota"] if nota else ""
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.*", "",
"## Mercato", "",
@@ -425,6 +428,16 @@ def rendi_markdown(con, voce: dict) -> str:
"giudizio: quello sta nella Nota.*", ""]
icona = {"info": "", "nota": "📌 ", "attenzione": "⚠️ "}
L += [f"- `[{r['id']}]` {icona[r['livello']]}{r['testo']}" for r in righe] or ["- *(nessuna regola accesa)*"]
testo = (an.get("analisi") or "").strip()
if testo:
stato = an.get("analisi_stato") or "?"
cap = (f"*Scritta da `{an.get('analisi_modello')}` il {an.get('analisi_ts')}. "
f"Prosa di un modello: puo' sbagliare, e non e' una misura. "
f"Controllo sui numeri: **{stato}**.*")
L += ["", "## Analisi (agente)", "", cap, ""]
if an.get("analisi_motivi"):
L += [f"> ⚠️ {an['analisi_motivi']}", ""]
L += [testo]
L += ["", "## Nota", "", (nota if nota.strip() else "*(vuota — campo dell'operatore)*"), ""]
return "\n".join(L)