diff --git a/docs/decisions/2026-05-11-phase1-5-nemotron-run.md b/docs/decisions/2026-05-11-phase1-5-nemotron-run.md new file mode 100644 index 0000000..a69614b --- /dev/null +++ b/docs/decisions/2026-05-11-phase1-5-nemotron-run.md @@ -0,0 +1,117 @@ +# Phase 1.5 — Run nemotron tier C — Decision Memo + +**Data**: 11 maggio 2026 +**Run di riferimento**: `phase1.5-nemotron-001` (id `434c417e2b6f42bb8cf32514e5d0db1d`) +**Tier LLM**: C → `nvidia/nemotron-3-super-120b-a12b:free` +**Durata wallclock**: 2 h 26 min (08:15 → 10:11 UTC, gen 0 → gen 9) +**Spesa totale**: $0.1244 (price-table tier C; il modello effettivo è `:free` su OpenRouter, ma il cost tracker applica la pricing nominale del tier) +**Status**: ✅ Completato, ma esito strategico **NO-GO** sulla configurazione corrente + +--- + +## 1. Premessa + +Il run `phase1.5-nemotron-001` è la prima esecuzione end-to-end del loop GA con: + +- l'Adversarial layer aggiornato in Phase 1.5 (commits `56a631f` + `d3662f6`), con tre nuovi check HIGH (`flat_too_long`, `fees_eat_alpha`, `time_in_market_too_high`) più i due esistenti rinforzati; +- il tier C ribindato a `nvidia/nemotron-3-super-120b-a12b:free`, modello scelto in benchmark contro sette alternative per stabilità JSON e costo nullo; +- il fix `EmptyCompletionError` su `llm/client.py` (commit `9d0deb3`) introdotto durante la stessa sessione per gestire le risposte vuote che alcuni provider `:free` ritornano sporadicamente. + +L'obiettivo dichiarato del run era verificare se il nuovo budget di vincoli adversarial — più stretto del v5 — fosse compatibile con la capacità generativa di nemotron, e se la popolazione riuscisse a esplorare una zona di fitness positiva non degenere. + +--- + +## 2. Hard gate Phase 1 — ripercorrenza + +I 5 hard gate originali (definiti nello spec strategico di Phase 1) sono stati rivalutati su questo run come sanity check, non come passaggio formale di gate. + +| # | Gate | Soglia | Misura | Esito | +|---|------|--------|--------|-------| +| 1 | Loop converge | mediana cresce ≥3 gen consecutive | Gen 0→8: median oscilla tra 0.0 e 0.0073 senza crescita strutturale | ❌ FAIL | +| 2 | Parse success | ≥80% proposte LLM parse-OK | 81/89 = **91.0%** | ✅ PASS | +| 3 | Top-5 ratio | top-5 fitness ≥10× mediana | top-5 = 0.0162–0.0215; mediana ≈ 0 → ratio indefinito | ⚠️ N/A | +| 4 | Entropy | ≥0.5 a fine run | 0.845 alla gen 9 | ✅ PASS | +| 5 | Budget | costo ≤ cap | $0.1244 vs cap $700 (0.02%) | ✅ PASS | + +Il gate critico è il numero 1. La popolazione non converge: il `max_fitness` resta inchiodato a `0.0215` dalla generazione 0 fino alla 9, segnale che l'elite preservation cattura un singolo genoma poco peggiore degli altri ma altrettanto inadatto, mentre il resto della popolazione non riesce a superarlo. La mediana è zero in 9 generazioni su 10 (singolo picco a 0.0073 in gen 8). + +--- + +## 3. Lettura dei top genomi + +I cinque genomi a fitness più alta hanno tutti caratteristiche economicamente disastrose: + +| Genome ID | Fitness | DSR | Sharpe | Total return | n_trades | +|-----------|---------|-----|--------|--------------|----------| +| `0e1f9d7af25cfd6a` | 0.0215 | 0.000 | −1.083 | −115.9% | 385 | +| `85a8116ab2cd2735` | 0.0215 | 0.000 | −1.083 | −115.9% | 385 | +| `92aae563277b6f21` | 0.0193 | 0.000 | −1.129 | −131.0% | 597 | +| `01d0ca99bbdd7320` | 0.0180 | 0.000 | −1.112 | −131.7% | 602 | +| `194b096f7edab53c` | 0.0162 | 0.000 | −1.154 | −150.7% | 369 | + +Il fatto che **DSR sia zero per tutti i top-5** indica che nessuna strategia passa il deflation test di Bailey & López 2014: il loop non sta generando proposte con edge statistico anche solo apparente. Il valore di fitness positivo che li seleziona deriva interamente dal termine `tanh(sharpe) × penalty(dd)` della fitness v1, che resta debolmente non nullo anche per Sharpe negativi grazie alla penalty di drawdown e a saturazioni numeriche. I primi due genomi hanno fitness identico a 0.0215 e total return identico — verosimilmente lo stesso elite riproposto a generazioni adiacenti. + +--- + +## 4. Adversarial findings — il sistema fa il suo lavoro + +Il layer Adversarial Phase 1.5 ha emesso 98 finding sul run: + +| Severità | Check | Conteggio | +|----------|-------|-----------| +| HIGH | `fees_eat_alpha` (nuovo P1.5) | 35 | +| MEDIUM | `overtrading` | 19 | +| HIGH | `no_trades` | 16 | +| HIGH | `flat_too_long` (nuovo P1.5) | 15 | +| HIGH | `time_in_market_too_high` (nuovo P1.5) | 8 | +| HIGH | `undertrading` | 4 | +| HIGH | `degenerate` | 1 | + +Il dato saliente è che i tre check introdotti in Phase 1.5 — `fees_eat_alpha`, `flat_too_long`, `time_in_market_too_high` — sono effettivamente attivi e killano strategie. In particolare `fees_eat_alpha` è la categoria più popolata: 35 occorrenze HIGH. Esempi tipici dai detail dei finding: + +- `Fees $17073.82 = 2032.6% of gross $840.00`; +- `Fees $70646.03 = 12671.9% of gross $557.50`; +- `Signal flat for 98.8% of bars (>95% threshold)`. + +Il messaggio è netto: il pool di strategie generato da nemotron, ai prompt e ai gradi di libertà attuali, oscilla tra due estremi degeneri — strategie inattive (flat 98%+) e strategie iperattive (overtrading + fee che divorano l'alpha lordo). Phase 1.5 cattura entrambi gli estremi, ma il loop GA non ha materiale di partenza sano da cui evolvere. + +--- + +## 5. Decisione + +**Esito**: NO-GO sulla combinazione `tier C = nemotron` + `Phase 1.5 adversarial` come configurazione di Phase 2. + +Le ragioni a supporto della decisione sono tre. + +Primo, la convergenza è assente per nove generazioni consecutive, non un plateau di selezione raggiunto dopo una fase di salita. Non si tratta cioè di un loop che ha già trovato il suo ottimo e lo conserva, ma di un loop che non ne ha trovato uno. + +Secondo, la distanza dal baseline Phase 1 v5 è di un ordine di grandezza: max fitness `0.0215` qui contro `0.3347` nel run di gate Phase 1, mediana che oscilla sullo zero contro una mediana attorno a `0.005`–`0.09`. Nemotron, in questa configurazione, sta producendo proposte qualitativamente più povere di qwen-2.5-72b nello stesso schema operativo. + +Terzo, i finding adversarial non puntano a un bug del sistema ma a una mancanza di edge nelle proposte. Il loop sta sanzionando correttamente — il problema è a monte, nella generazione. + +--- + +## 6. Tre direzioni per Phase 2 + +Tre opzioni si configurano per il passo successivo. Vanno valutate prima di una nuova esecuzione, non in parallelo a essa. + +**Direzione A — Riportare tier C a `qwen/qwen-2.5-72b-instruct`** (configurazione di gate Phase 1). Il run di riferimento `phase1-real-005` è già un baseline noto: max fitness `0.3347`, top genome problematico (flat 99.8%) ma generato sotto Phase 1 adversarial. Rilanciare lo stesso pool con Phase 1.5 adversarial isolerebbe l'effetto del solo hardening sul medesimo motore generativo, senza confondere variabili. Questo è il percorso più informativo nel breve. + +**Direzione B — Mantenere nemotron ma rilassare i prompt di Hypothesis**. L'ipotesi alternativa è che il prompting attuale, calibrato su qwen, sia troppo terso o troppo vincolato per la modalità di ragionamento di nemotron. Iterare due o tre versioni del prompt — più esempi few-shot, vincoli espliciti su `n_trades` minimo e `time_in_market` target — può cambiare radicalmente la qualità dell'output senza cambiare il modello. + +**Direzione C — Sostituire il tier C con un modello a pagamento di fascia comparabile**. Tra i benchmark precedenti, `deepseek/deepseek-v4-flash` è già usato come tier A/B nel file `.env`; promuoverlo a tier C significa accettare una spesa marginale (stima $1–3 per run di 10 gen × 20 pop) in cambio di una qualità generativa nota. + +La preferenza dell'operatore per modelli cost-conscious orienta verso A o B. La direzione C resta utile come benchmark di controllo se A e B fallissero a loro volta. + +--- + +## 7. Operazioni di pulizia eseguite contestualmente + +- Il run zombie `phase1-real-008` (id `6ebcff9f7f6544c18ced50313cf72ca9`, marcato `running` da 07:11 UTC senza processo associato) è stato chiuso a `status='failed'` direttamente in `runs.db`, per evitare contaminazione delle query di dashboard. +- Il commit `9d0deb3` (`fix(llm): handle empty completions + missing usage`) è già su `main`. Il `client.py` ora tratta `resp.choices == []` e `resp.usage is None` come errori retryable invece che assertion failure: precondizione necessaria per qualsiasi run successivo su provider `:free`. + +--- + +## 8. Note per chi legge + +Questo memo è un documento di decisione, non un rapporto tecnico completo. Il rapporto tecnico esteso del run può essere ricostruito da `runs.db` interrogando le tabelle `runs`, `generations`, `evaluations`, `adversarial_findings`, `cost_records` con `run_id='434c417e2b6f42bb8cf32514e5d0db1d'`. Il design Phase 1.5 e le motivazioni delle soglie adversarial restano definiti nel commit `56a631f` e nei suoi file di test.