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:
@@ -0,0 +1,202 @@
|
||||
"""L'analista di bordo: cosa puo' scrivere, e cosa non deve poter rompere.
|
||||
|
||||
Un modello che scrive in un registro di misure ha tre modi di rovinarlo, e qui c'e' un test per
|
||||
ognuno: inventare un numero, commentare se stesso, e lasciare in pagina l'analisi di ieri il
|
||||
giorno che non risponde.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import subprocess
|
||||
import sys
|
||||
from datetime import date
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT))
|
||||
sys.path.insert(0, str(ROOT / "scripts" / "live"))
|
||||
|
||||
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
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def db(tmp_path):
|
||||
con = T.connect(tmp_path / "t.db")
|
||||
yield con
|
||||
con.close()
|
||||
|
||||
|
||||
PAGINA = """# Giornale di bordo — 2026-08-21
|
||||
|
||||
## Mercato
|
||||
| **BTC** | $77,000.00 | +7.31% | RV30 43.9% |
|
||||
|
||||
## Libro
|
||||
Equity **$644.00** · leva lorda 0.30x
|
||||
|
||||
## Lettura
|
||||
- `[stato]` A mercato su BTC $+126.
|
||||
|
||||
## Analisi (agente)
|
||||
*Scritta da un modello.*
|
||||
Testo vecchio che il modello NON deve rileggere.
|
||||
|
||||
## Nota
|
||||
Contesto scritto dall'operatore.
|
||||
"""
|
||||
|
||||
|
||||
# ------------------------------------------------- il ciclo di retroazione
|
||||
|
||||
def test_toglie_la_propria_analisi_precedente():
|
||||
fuori = A.senza_analisi(PAGINA)
|
||||
assert "Testo vecchio" not in fuori
|
||||
assert "## Analisi" not in fuori
|
||||
|
||||
|
||||
def test_ma_lascia_tutto_il_resto_compresa_la_nota_dell_operatore():
|
||||
"""Controllo positivo: se togliesse anche il resto, il modello scriverebbe al buio."""
|
||||
fuori = A.senza_analisi(PAGINA)
|
||||
for pezzo in ("## Mercato", "## Libro", "## Lettura", "## Nota",
|
||||
"Contesto scritto dall'operatore", "$77,000.00", "`[stato]`"):
|
||||
assert pezzo in fuori
|
||||
|
||||
|
||||
def test_una_pagina_senza_analisi_non_viene_toccata():
|
||||
md = "# Titolo\n\n## Mercato\nx\n\n## Nota\ny\n"
|
||||
assert A.senza_analisi(md) == md.rstrip("\n")
|
||||
|
||||
|
||||
def test_il_prompt_non_contiene_l_analisi_vecchia():
|
||||
p = A.costruisci_prompt(PAGINA, "storico")
|
||||
assert "Testo vecchio" not in p and "storico" in p
|
||||
|
||||
|
||||
# ------------------------------------------------- i numeri inventati
|
||||
|
||||
def test_scopre_un_numero_che_non_c_e():
|
||||
assert A.numeri_non_supportati("L'equity e' salita a 812.40.", PAGINA) == ["812.4"]
|
||||
|
||||
|
||||
def test_non_segnala_i_numeri_presenti_comunque_formattati():
|
||||
testo = "Equity 644.00 (da $644.00), BTC a 77,000.00 e RV30 43.9%."
|
||||
assert A.numeri_non_supportati(testo, PAGINA) == []
|
||||
|
||||
|
||||
def test_tollera_i_conteggi_piccoli():
|
||||
"""«2 su 3 orizzonti», «24 giri»: non sono cifre che possano fabbricare una misura."""
|
||||
assert A.numeri_non_supportati("2 su 3 orizzonti, 24 giri, 10 round-trip.", PAGINA) == []
|
||||
|
||||
|
||||
def test_una_percentuale_inventata_viene_presa():
|
||||
assert "18.57" in A.numeri_non_supportati("Il mese fa +18.57%.", PAGINA)
|
||||
|
||||
|
||||
# ------------------------------------------------- la validazione
|
||||
|
||||
def test_analisi_pulita_e_ok():
|
||||
stato, motivi = A.valida("BTC ha fatto +7.31% e il libro era a $+126.", PAGINA)
|
||||
assert stato == "ok" and motivi == []
|
||||
|
||||
|
||||
def test_un_numero_libero_la_rende_sospetta_non_rifiutata():
|
||||
stato, motivi = A.valida("Equity a 812.40 dopo il rialzo.", PAGINA)
|
||||
assert stato == "sospetta" and "812.4" in motivi[0]
|
||||
|
||||
|
||||
def test_troppi_numeri_liberi_la_rifiutano():
|
||||
stato, _ = A.valida("Valori: 812.40, 913.55, 1044.10, 1155.20, 1266.30.", PAGINA)
|
||||
assert stato == "rifiutata"
|
||||
|
||||
|
||||
def test_risposta_vuota_rifiutata():
|
||||
assert A.valida(" ", PAGINA)[0] == "rifiutata"
|
||||
|
||||
|
||||
def test_pulisci_toglie_titoli_e_recinti():
|
||||
assert A.pulisci("```\n# Titolo\ntesto vero\n```") == "testo vero"
|
||||
|
||||
|
||||
# ------------------------------------------------- l'errore non si ingoia
|
||||
|
||||
def test_cli_assente_ritorna_errore_non_eccezione(monkeypatch):
|
||||
monkeypatch.setattr(subprocess, "run", lambda *a, **k: (_ for _ in ()).throw(FileNotFoundError()))
|
||||
testo, err = A.interroga("x")
|
||||
assert testo == "" and "non trovata" in err
|
||||
|
||||
|
||||
def test_timeout_ritorna_errore(monkeypatch):
|
||||
def boom(*a, **k):
|
||||
raise subprocess.TimeoutExpired(cmd="claude", timeout=1)
|
||||
monkeypatch.setattr(subprocess, "run", boom)
|
||||
assert "timeout" in A.interroga("x")[1]
|
||||
|
||||
|
||||
def test_uscita_non_zero_ritorna_errore(monkeypatch):
|
||||
class R:
|
||||
returncode, stdout, stderr = 2, "", "boom"
|
||||
monkeypatch.setattr(subprocess, "run", lambda *a, **k: R())
|
||||
assert "uscita 2" in A.interroga("x")[1]
|
||||
|
||||
|
||||
# ------------------------------------------------- il DB: cosa scrive e cosa non tocca
|
||||
|
||||
def _prepara(db, g="2026-08-21"):
|
||||
voce = dict(giorno=g, ts_scritto=T.ora(), mercato={}, libro={"errore": "x", "giri": 0},
|
||||
pnl=J.pnl_giorno(db, date.fromisoformat(g)),
|
||||
salute=dict(giri_book=0, feed_skh_min=None, atteso_giri=24,
|
||||
giorno_in_corso=False, giri_mancanti=24))
|
||||
J.salva(db, voce, nota="nota dell'operatore")
|
||||
return voce
|
||||
|
||||
|
||||
def test_l_agente_non_tocca_la_nota(db, monkeypatch, tmp_path):
|
||||
import analista as CLI
|
||||
_prepara(db)
|
||||
monkeypatch.setattr(A, "interroga", lambda *a, **k: ("Analisi valida senza numeri nuovi.", None))
|
||||
monkeypatch.setattr(J, "JOURNAL_DIR", tmp_path)
|
||||
CLI.scrivi(db, date(2026, 8, 21), modello="finto", verbose=False)
|
||||
r = db.execute("SELECT nota, analisi, analisi_stato FROM journal WHERE giorno='2026-08-21'").fetchone()
|
||||
assert r["nota"] == "nota dell'operatore"
|
||||
assert r["analisi"].startswith("Analisi valida") and r["analisi_stato"] == "ok"
|
||||
|
||||
|
||||
def test_se_il_modello_non_risponde_l_analisi_di_IERI_non_resta_in_pagina(db, monkeypatch, tmp_path):
|
||||
"""Il guasto peggiore: una pagina che mostra l'analisi del giorno prima come se fosse di oggi."""
|
||||
import analista as CLI
|
||||
_prepara(db)
|
||||
monkeypatch.setattr(J, "JOURNAL_DIR", tmp_path)
|
||||
monkeypatch.setattr(A, "interroga", lambda *a, **k: ("Analisi di ieri.", None))
|
||||
CLI.scrivi(db, date(2026, 8, 21), modello="finto", verbose=False)
|
||||
monkeypatch.setattr(A, "interroga", lambda *a, **k: ("", "rete KO"))
|
||||
CLI.scrivi(db, date(2026, 8, 21), modello="finto", verbose=False)
|
||||
r = db.execute("SELECT analisi, analisi_stato, analisi_motivi FROM journal "
|
||||
"WHERE giorno='2026-08-21'").fetchone()
|
||||
assert r["analisi"] == "" # niente in pagina
|
||||
assert r["analisi_stato"] == "errore" and "rete KO" in r["analisi_motivi"] # ma il perche' c'e'
|
||||
|
||||
|
||||
def test_un_ricalcolo_del_giornale_non_cancella_l_analisi(db, monkeypatch, tmp_path):
|
||||
import analista as CLI
|
||||
voce = _prepara(db)
|
||||
monkeypatch.setattr(J, "JOURNAL_DIR", tmp_path)
|
||||
monkeypatch.setattr(A, "interroga", lambda *a, **k: ("Analisi da conservare.", None))
|
||||
CLI.scrivi(db, date(2026, 8, 21), modello="finto", verbose=False)
|
||||
J.salva(db, voce) # il cron rigenera le sezioni misurate
|
||||
r = db.execute("SELECT analisi, nota FROM journal WHERE giorno='2026-08-21'").fetchone()
|
||||
assert r["analisi"] == "Analisi da conservare." and r["nota"] == "nota dell'operatore"
|
||||
|
||||
|
||||
def test_il_markdown_dichiara_modello_ora_e_esito_del_controllo(db, monkeypatch, tmp_path):
|
||||
import analista as CLI
|
||||
voce = _prepara(db)
|
||||
monkeypatch.setattr(J, "JOURNAL_DIR", tmp_path)
|
||||
monkeypatch.setattr(A, "interroga", lambda *a, **k: ("Testo dell'agente.", None))
|
||||
CLI.scrivi(db, date(2026, 8, 21), modello="modello-finto", verbose=False)
|
||||
md = J.rendi_markdown(db, voce)
|
||||
assert "## Analisi (agente)" in md and "modello-finto" in md
|
||||
assert "puo' sbagliare" in md and "Controllo sui numeri: **ok**" in md
|
||||
assert md.index("## Analisi") < md.index("## Nota") # l'agente prima, l'operatore in fondo
|
||||
Reference in New Issue
Block a user