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
+4
View File
@@ -43,5 +43,9 @@ mkdir -p logs
# 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
# 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.
uv run python scripts/live/analista.py --giorno "$(date -u -d yesterday +%F)" --quiet || true
echo "===== done $(date -u '+%H:%M:%SZ') ====="
} >> logs/cron_daily.log 2>&1
+99
View File
@@ -0,0 +1,99 @@
"""L'analista di bordo: fa scrivere a un modello l'analisi del giorno nel campo `analisi`.
uv run python scripts/live/analista.py # oggi
uv run python scripts/live/analista.py --giorno 2026-08-21
uv run python scripts/live/analista.py --modello claude-opus-5
uv run python scripts/live/analista.py --secco # stampa e non salva
NON tocca `nota` (dell'operatore) e non tocca nessuna sezione misurata: scrive SOLO in `analisi`.
Se il modello non risponde, l'analisi di ieri NON diventa quella di oggi: si registra l'errore
e la sezione resta vuota. Sola lettura su tutto il resto; nessun ordine.
"""
from __future__ import annotations
import sys
from datetime import date, datetime, timedelta, timezone
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT))
from src.live import analista as A # noqa: E402
from src.live import journal as J # noqa: E402
from src.live import tradesdb as T # noqa: E402
GIORNI_STORICO = 10
def _arg(flag: str, default=None):
a = sys.argv[1:]
return a[a.index(flag) + 1] if flag in a and a.index(flag) + 1 < len(a) else default
def storico(con, g: date, n: int = GIORNI_STORICO) -> str:
"""Le righe essenziali dei giorni precedenti: serve per vedere COSA E' CAMBIATO."""
righe = ["giorno equity delta fill rt posizioni"]
for i in range(n, 0, -1):
d = (g - timedelta(days=i)).isoformat()
r = con.execute("SELECT pnl, libro FROM journal WHERE giorno=?", (d,)).fetchone()
if not r:
continue
import json as _j
p, lb = _j.loads(r["pnl"] or "{}"), _j.loads(r["libro"] or "{}")
pos = " ".join(f"{a} ${v['posizione']:+,.0f}" for a, v in sorted((lb.get("asset") or {}).items()))
dq = p.get("delta_equity")
righe.append(f"{d} {p.get('equity_fine') or float('nan'):8,.2f} "
f"{' n/d' if dq is None else f'{dq:+8.2f}'} "
f"{p.get('fill', 0):>4} {p.get('roundtrip_chiusi', 0):>2} {pos or 'flat'}")
return "\n".join(righe)
def scrivi(con, g: date, modello: str, secco: bool = False, verbose: bool = True) -> str:
voce = J.costruisci(con, g)
J.salva(con, voce) # le sezioni misurate prima, cosi' il modello le legge
pagina = J.rendi_markdown(con, voce)
st = storico(con, g)
prompt = A.costruisci_prompt(pagina, st)
# La guardia si applica a TUTTO cio' che il modello ha ricevuto, non alla sola pagina:
# i numeri del blocco storico gli sono stati dati, citarli non e' inventare. (Primo giro:
# validando sulla sola pagina il tripwire bocciava un'equity vera presa dallo storico —
# una guardia piu' stretta del contratto produce allarmi che si imparera' a ignorare.)
fonte = A.senza_analisi(pagina) + "\n" + st
testo, errore = A.interroga(prompt, modello=modello)
if errore:
stato, motivi = "errore", [errore]
testo = ""
else:
stato, motivi = A.valida(testo, fonte)
if stato == "rifiutata":
testo = ""
if verbose:
print(f" {g} modello {modello} -> {stato}"
+ (f" ({'; '.join(motivi)})" if motivi else ""))
if secco:
print("\n" + (testo or "(niente da salvare)"))
return stato
if testo:
con.execute("""UPDATE journal SET analisi=?, analisi_modello=?, analisi_ts=?,
analisi_stato=?, analisi_motivi=? WHERE giorno=?""",
(testo, modello, T.ora(), stato, "; ".join(motivi) or None, g.isoformat()))
else:
# niente analisi: si REGISTRA il perche' e NON si lascia in pagina quella di ieri
con.execute("""UPDATE journal SET analisi='', analisi_modello=?, analisi_ts=?,
analisi_stato=?, analisi_motivi=? WHERE giorno=?""",
(modello, T.ora(), stato, "; ".join(motivi) or None, g.isoformat()))
con.commit()
J.scrivi_file(con, voce)
return stato
if __name__ == "__main__":
con = T.connect()
g = date.fromisoformat(_arg("--giorno", datetime.now(timezone.utc).date().isoformat()))
st = scrivi(con, g, modello=_arg("--modello", A.MODELLO_DEFAULT),
secco="--secco" in sys.argv[1:], verbose="--quiet" not in sys.argv[1:])
con.close()
sys.exit(0 if st in ("ok", "sospetta") else 1)