Aggiunti due meccanismi per selection multi-objective:
1) Helper compute_combined_fitness(fit_train, fit_oos, alpha):
formula = alpha*IS + (1-alpha)*OOS, fallback a IS se OOS è None/NaN.
2) RunConfig.eval_oos_during_loop (default False) + fitness_combined_alpha
(default 0.5). Quando True E wfa_train_split attivo, ogni genome con
fitness IS > 0 viene rivalutato su test_ohlcv DURANTE il loop GA e la
fitness usata per tournament_select/elite_select è quella combinata.
2x costo backtest engine (richiede 2 evaluation per genome).
3) CLI flags --eval-oos-during-loop e --fitness-combined-alpha.
Motivazione: il run phase2-7-max7y-v2-wfa-001 ha mostrato che il top by
fitness_IS (4e1be9fa, ratio OOS 0.31) NON è il top per performance OOS reale
(634111992702, ratio 1.42, ret_OOS +105% / 2.2y). Selezionare durante GA
con combined fitness orienta l'evoluzione verso strategie OOS-robust by
design invece di filtrarle a posteriori.
Backward compat: default eval_oos_during_loop=False → comportamento
invariato per run senza il flag.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Run phase2-6-flat-wfa-001 è andato in hang per 4h+ a gen 3/10 su una
chiamata OpenRouter (process stuck in do_poll syscall, 0% CPU). Il SDK
OpenAI ha default timeout 600s ma evidentemente il TLS/socket handshake
può bloccarsi senza interrompere; senza timeout esplicito i retry tenacity
non scattano mai e il run resta pending.
Fix: OpenAI(timeout=60.0). Se una chiamata supera 60s, raise
APITimeoutError (già in _RETRYABLE_EXCEPTIONS) e tenacity ritenta fino a 5
volte con backoff esponenziale fino a 30s. Worst case totale: ~5×60s =
5min per call (vs 4h+ hang precedente).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Due fondamenta scientifiche per filtrare overfit e lucky-shot:
1) undertrading_threshold parametrico (era hardcoded 10):
- AdversarialAgent.__init__(undertrading_threshold=10)
- CLI flag --undertrading-threshold
- Aggiunto a hard_kill_findings v2 default
{"no_trades", "degenerate", "undertrading"}: ora un genome con 1 trade
fortunato (es. genome 80be6bcc-1trade-fit-0.21 di fitness-v2-combo) viene
killato anche sotto fitness v2 soft-kill.
- Test parametric: undertrading_threshold=25 → 15 trade triggerano HIGH.
2) Walk-Forward Validation (WFA):
- RunConfig.wfa_train_split (None=off, 0<x<1=on) + wfa_top_k=5
- run_phase1: split ohlcv in train/test; GA usa solo train; a fine GA
i top_k genomi (by fitness in-sample, fitness>0) vengono rivalutati
sul test_ohlcv via falsification+adversarial+compute_fitness.
- Schema migration: evaluations + fitness_oos, sharpe_oos, return_oos,
max_dd_oos, n_trades_oos (ALTER TABLE con try/except per DB pre-2.6).
- Repository.update_evaluation_oos helper per popolare colonne OOS.
- CLI flags --wfa-train-split, --wfa-top-k.
- Test integration: train_split=0.7 → fitness_oos popolato per top_k.
Motivazione: la fase 2.5 ha generato 17 run con fitness fino a 0.36 + DSR
positivo, ma OOS test su 7 anni mostra che flat-ablation top crolla -37%
mentre fitness-v2 top regge (+143%). WFA in-run permette ora di vedere
direttamente il degradation train→test senza eseguire backtest separati,
rendendo possibile filtrare overfit early durante l'ottimizzazione.
Tests (+2 → 193 totale):
- test_undertrading_threshold_parametric
- test_e2e_wfa_populates_fitness_oos
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Script utility per validare OOS strategie discovered durante run Phase 2.5.
Carica un JSON strategia (formato Hypothesis output), fetcha OHLCV via
Cerbero, esegue BacktestEngine + FalsificationReport + AdversarialReport,
stampa metriche annualizzate (CAGR, Sharpe, max DD, Calmar).
Esempio:
uv run python scripts/backtest_strategy.py /tmp/strategy.json \
--start 2018-09-01 --end 2026-01-01 --label my-strategy
Validato sui top 2 genomi Phase 2.5 (flat-ablation e fitness-v2-combo):
flat-ablation top overfit su 7y (-37%), fitness-v2 top regge (+143% in 7y,
CAGR +12.8%). Conferma che strategie con time gate temporal feature
generalizzano meglio di strategie con SMA crossover hard-tied al regime
del training period.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Aggiunto parametro hard_kill_findings opzionale a compute_fitness.
None (default) = v1 backward-compat: ogni HIGH azzera la fitness.
tuple non vuota = v2: solo finding con name nel set azzerano; gli altri
HIGH applicano penalità moltiplicativa
adv_penalty = 1 / (1 + soft_penalty * n_soft_high)
(default soft_penalty=0.4 → 1 HIGH soft = 0.71x, 2 = 0.56x, 3 = 0.45x).
Motivazione: tutti i run Phase 2/2.5 mostrano 55-87 finding HIGH dominanti
da fees_eat_alpha + flat_too_long. La fitness v1 azzera ogni genome con
anche solo 1 HIGH → median sempre 0 perché quasi tutti i genomi sopravvivono
in modo binario (top integro vs zerati). v2 fornisce gradient continuo:
strategie 'quasi-buone' restano valutabili e il GA può evolverle.
Hard kill v2 default: {"no_trades", "degenerate"} (la strategia letteralmente
non funziona — niente da salvare). Tutti gli altri HIGH (fees_eat_alpha,
flat_too_long, time_in_market_too_high, undertrading, overtrading) → soft.
RunConfig: fitness_hard_kill_findings (None = v1, tuple = v2) +
fitness_adversarial_soft_penalty (default 0.4). CLI flag --fitness-v2 imposta
hard_kill_findings = ("no_trades", "degenerate") e
--fitness-soft-penalty per il fattore custom.
+5 test (12 totali in test_fitness, 191 totale suite):
- v2 soft HIGH non azzera
- v2 hard kill ancora azzera
- v2 cumulativo: più soft HIGH = penalty più severa
- v2 senza findings = identico a v1
- backward compat hard_kill_findings=None = v1
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Estende AdversarialAgent con flat_too_long_threshold (default 0.95)
configurabile, simmetrico a fees_eat_alpha_threshold. Propagato a
RunConfig.flat_too_long_threshold e flag CLI --flat-too-long-threshold.
Motivazione: pop30-combo ha registrato 75 finding flat_too_long HIGH
(secondo killer dopo fees_eat_alpha 87). Rilassare la soglia 0.95→0.98
ammette strategie più passive ma marginalmente attive — analogo
all'ablation fees già verificata (+23% stabile).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mark task 6 (cost attribution) come done dopo commit ba4eb09. Aggiornato
header status con sweet spot empirico weight=0.30 (curva U validata su
run 004 vs control vs weight-0.50).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Task 6 del piano Phase 2.5 (deferito → ora completato):
- CostRecord: nuovo campo call_kind (default "hypothesis")
- CostTracker.record: accetta call_kind opzionale, summary include
by_call_kind breakdown (hypothesis vs mutation)
- Schema cost_records: aggiunta colonna call_kind TEXT NOT NULL DEFAULT
'hypothesis' + migration soft via ALTER TABLE in init_schema (silently
catched per DB pre-Task 6)
- Repository.save_cost_record: nuova arg call_kind opzionale
- mutate_prompt_llm: accetta cost_tracker/repo/run_id opzionali e logga
la call mutator con call_kind="mutation" quando sink presente
- weighted_random_mutate, next_generation: propagano cost sink
- orchestrator.run_phase1: passa cost_tracker+repo+run_id a
next_generation solo se prompt_mutation_weight > 0
Esposto fees_eat_alpha_threshold come parametro AdversarialAgent
(default 0.5 = comportamento Phase 1.5 invariato), propagato via
RunConfig.fees_eat_alpha_threshold e flag CLI
--fees-eat-alpha-threshold. Abilita ablation con soglia 0.7-0.8 senza
modificare codice — adversarial finding dominante in tutti i run
Phase 2/2.5 (50+ HIGH per run).
Tests (+4 → 186 totale):
- test_cost_tracker: default call_kind="hypothesis"; breakdown
by_call_kind con hypothesis+mutation
- test_mutation_prompt_llm: logging mutation cost con sink completo;
backward compat senza sink (no errore)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Run phase2-5-qwen25-prompt-mut-003 fallito a gen 5 (76 evals done, $0.061
spesi) per HTTP 429 RateLimit upstream da DeepInfra su qwen-2.5-72b.
RateLimitError NON era in _RETRYABLE_EXCEPTIONS quindi tenacity falliva
subito, propagando il 429 a propose() e all'orchestrator (run failed).
Tre fix:
1) Aggiunto openai.RateLimitError a _RETRYABLE_EXCEPTIONS.
2) Bumpato stop_after_attempt(3) → 5 e wait_exponential max 10s → 30s.
Più tempo per il rate limit upstream di sbloccare prima di rinunciare.
3) hypothesis.py: try/except RateLimitError in propose() come per
EmptyCompletionError — anche se tenacity esaurisce i 5 retry, il genome
viene marcato fitness=0 e il loop esterno continua senza crash totale.
Test: aggiornato test_completion_retries_on_connection_error per
assert call_count == 5 (era 3). Tutti 182 verdi.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
OpenRouter ha cominciato a routare qwen/qwen-2.5-72b-instruct attraverso
Novita, che rifiuta /chat/completions con HTTP 400:
"model: qwen/qwen-2.5-72b-instruct does not support endpoint: completions".
Aggiunto LLMClient.DEFAULT_PROVIDER_IGNORE = ("Novita",) e flag opzionale
provider_ignore al constructor. Le request includono ora
extra_body={"provider": {"ignore": ["Novita"]}} per default.
Verificato manualmente: con ignore Novita, qwen-2.5-72b risponde
normalmente via altri provider (DeepInfra, Together, ecc).
Mantenuta backward compat: passando provider_ignore=() si disabilita
l'esclusione (utile per test o se in futuro Novita si sistema).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Run phase2-5-qwen25-prompt-mut-001 fallito perché qwen-2.5-72b ha emesso
empty completion ripetutamente; le 3 retry tenacity in LLMClient.complete
si sono esaurite e l'exception è bollata fino a run_phase1, marcando l'intero
run come failed (perso ~$0.017 di tier C).
Fix: HypothesisAgent.propose() ora cattura EmptyCompletionError nel loop
max_attempts trattandola come parse-fail "empty_completion" e ritentando
con max_retries+1 tentativi (cumulativo: max_retries tenacity × max_attempts
loop esterno, default 3×3 = 9 retry effettive). Se TUTTI i tentativi
falliscono con empty, ritorna proposal con strategy=None e
parse_error="empty_completion: ...", lasciando l'orchestrator continuare
con quel genome marcato come "fitness=0" senza crash totale del run.
+2 test: success dopo 1 empty + retry; failure totale dopo 3 empty consecutive.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Task 5 del piano Phase 2.5: nuovo modulo src/multi_swarm/metrics/diversity.py
con population_prompt_diversity(prompts) che ritorna la diversità media
1 - SequenceMatcher.ratio() su tutte le coppie distinte. 0.0 identici,
fino a ~0.9 totalmente diversi (SequenceMatcher considera spazi/lunghezza).
5 test: edge case empty/single, identici, diversi, intermediate, simmetria.
Piano aggiornato a stato "IMPLEMENTATO 2026-05-11": checkbox task 1-5
spuntate, task 6 (cost attribution per call_kind) deferito con motivazione.
Header preambolo aggiornato con trigger verificati e decisione collaterale
rollback tier C.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Run controllo phase2-qwen25-control-001 (seed 42, stessa pipeline Phase 2,
solo tier C switched) ha dimostrato che qwen-2.5-72b è qualitativamente
SUPERIORE a qwen3-235b sul nostro workload:
| metrica | qwen3-235b | qwen-2.5-72b | delta |
| ----------------- | ---------- | ------------ | ----- |
| max fitness | 0.0238 | 0.0311 | +30% |
| median > 0 in gen | mai | 4 gen su 10 | -- |
| entropy media | 0.199 | 0.85 | 4.3x |
| genomi fit > 0 | 5 | 10 | 2x |
| parse success | 97.7% | 100% | + |
| durata | 50 min | 28 min | 0.56x |
| LLM calls | 148 | 90 | 0.61x |
| cost USD | 0.0223 | 0.0122 | 0.55x |
Controintuitivo: 235B con context 262k era atteso superiore al 72B legacy.
In pratica qwen3-235b in tier C produce strategie meno diverse,
meno parsabili e meno ottimizzabili dal GA.
Ripristinati prezzi cost_tracker tier C a 0.40/0.40 (qwen-2.5-72b).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bug: la dashboard mostrava \$0.0000 per Cost (USD) durante i run in corso
perché leggeva runs.total_cost_usd, che viene aggiornato solo dentro
Repository.complete_run a fine run. I record per-call esistevano già in
cost_records (124 record / \$0.019 sul run phase2-qwen3-001 attivo).
Fix: in _snapshot() se il run è status=running uso Repository.total_cost(run_id)
che fa SUM(cost_usd) live su cost_records. Per i run completed/failed continuo
a leggere total_cost_usd dal record runs (storico autoritativo).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Soluzione robusta al bug "chiavi JSON invisibili": invece di tentare
override CSS su Prism.js/highlight.js (che nelle build NiceGUI possono
non essere attivi o variare), il JSON viene serializzato server-side con
span colorati espliciti e renderizzato via ui.html.
Implementato helper _json_to_html(obj) che produce HTML con classi:
- .cb-key (chiavi) → cyan #00D9FF
- .cb-string → green #00E676
- .cb-number → pink #FF2D87
- .cb-bool / .cb-null → amber #FFB800
- .cb-punct (graffe, virgole, due punti) → muted
Stessa logica per system_prompt e raw LLM output in /genomes: sostituito
ui.code(language=text) con ui.html('<pre class="raw-block">') e
html.escape() per safety XSS. Background dedicato #1A1A24 e font
JetBrains Mono 13.5px, line-height 1.7, ottimo contrasto su qualsiasi
browser senza dipendere da syntax highlighter esterno.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Due bug visivi distinti:
1) Icone Material rotte ("arrow_drop_down" come testo nel selector):
Il selettore CSS '*' applicava font Inter a tutti gli elementi inclusi
gli .material-icons che richiedono il loro proprio font ligature.
Sostituito '*' con whitelist esplicita di elementi UI; aggiunto override
esplicito per .material-icons / .q-icon che forza font 'Material Icons'.
2) JSON keys invisibili nel code block config:
NiceGUI ui.code usa Prism.js (non highlight.js) per il syntax highlighting.
Aggiunti selettori .token.* coordinati con la palette neon:
- .token.property / .token.attr-name → cyan (chiavi JSON)
- .token.string → green (valori string)
- .token.number / .token.boolean / .token.null → pink
- .token.punctuation / .token.operator → muted
- .token.keyword / .token.builtin → amber
Più reset text-shadow per pre[class*=language-].
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Aggiunta route /genomes:
- Run selector + Top K (10/25/50)
- Tabella sortable con fitness, DSR, Sharpe, max DD, trades, style, T, lookback
- Selezione riga → pannello ispezione con 6 metric card e blocchi code per
system_prompt + raw_text + parse_error eventuale
- Auto-refresh ogni 3s (mantiene selezione corrente se ancora valida)
- Header nav aggiornato con link Genomes
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Prima fase migrazione da Streamlit a NiceGUI. Pagina indice riproduce
l'Overview con:
- run selector reattivo
- 2 progress bar live (generations, evaluations) con WebSocket push
- 4 metric card (top fitness, median, parse success %, cost)
- timer auto-refresh ogni 3s (no click manuale)
- status badge colorato (running/completed/failed)
- config JSON code block
Avvio: uv run python -m multi_swarm.dashboard.nicegui_app (porta 8080)
Streamlit resta attivo su 8501 durante la migrazione. Backend invariato:
riusa dashboard/data.py senza modifiche.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Aggiunto blocco "Progresso run" sopra le metriche statiche con:
- progress bar generazioni (gens_done / n_generations)
- progress bar evaluations (evals_done / pop × gen) con percentuale
- metric top fitness / median fitness / cost so far
- pulsante Refresh manuale + timestamp ultimo update
- emoji status (🟢 running / ✅ completed / ❌ failed)
Niente nuove dipendenze: solo st.progress + st.rerun standard Streamlit.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Piano in tasca: NON attivare finché Phase 2 (qwen3 + temporal) non raggiunge
una delle condizioni di trigger documentate:
- plateau max fitness < 0.01 per >= 4 gen consecutive
- diversità prompt Levenshtein <= 0.15
- top genome quasi-fit (>= 0.10) con adversarial HIGH >= 2
Sei tipi di mutazione (tighten_threshold, swap_comparator, add/remove_condition,
change_timeframe, add_temporal_gate) via mutator tier B (deepseek-v4-flash),
weighted_random_mutate dispatcher con default 0.0 (opt-in), diversity guard
Levenshtein, fallback random_mutate su validation fail.
Costo stimato +$0.006/run. A/B con baseline B = phase2-qwen3-001 vs
trattamento T = phase2-qwen3-prompt-mut-001 per attribuzione effetto.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Tier S → google/gemini-3-flash-preview ($0.50/$3.00)
- Tier A/B → deepseek/deepseek-v4-flash ($0.14/$0.28)
- Tier C → qwen/qwen3-235b-a22b-2507 ($0.071/$0.10) — Phase 2 target
- Tier D → openai/gpt-oss-20b ($0.03/$0.14)
Aggiornato cost_tracker con prezzi reali per tier. Defaults config.py
ora rispecchiano .env corrente per evitare divergenze dead-code.
Tier S/A/B/D restano cablati ma non ancora invocati nel loop Phase 2
(solo Hypothesis tier C attivo).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Aggiunge hour/dow/is_weekend/minute_of_hour come FeatureNode nella
grammatica esistente. Universal access (non passa da feature_access),
riuso di FeatureNode (no nuovo tipo AST), few-shot examples nel prompt
Hypothesis. Cinque file toccati, ~120 LOC, backward-compatible.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Run phase1.5-nemotron-001 completato in 2h26min, costo $0.1244.
Max fitness 0.0215 stagnante (15x peggio del baseline qwen 0.3347),
DSR=0 universale, Sharpe -1.08/-1.15. Loop non converge.
Adversarial Phase 1.5 attivo: 98 finding totali, 35 fees_eat_alpha
HIGH + 15 flat_too_long + 8 time_in_market — i 3 nuovi check killano
correttamente, ma popolazione non ha materiale sano da cui evolvere.
Tre direzioni candidate per Phase 2: A) rollback qwen-2.5-72b,
B) prompt re-tuning nemotron, C) promuovere deepseek-v4-flash.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
EmptyCompletionError ora retryable. resp.usage può essere None su
provider :free (es. nemotron-3-super-120b-a12b:free) → no assert.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Simmetrico opposto di flat_too_long: penalizza strategie LONG/SHORT su
piu' dell'80% delle bar. Una sempre-in-market e' leveraged B&H camuffato,
esposto a funding cumulato (perp ogni 8h), tail risk eventi notturni e
nessuna opportunity-cost flexibility. Sweet spot fitness positiva: 5-80%
time in market.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Stringe le soglie esistenti e aggiunge due check HIGH per killare le
strategie degeneri scoperte nel run v5 (top-1 +2.66% vs BTC B&H +106%,
flat 99.8% del tempo, fees 69% del lordo).
- overtrading: soglia da n_bars/5 a n_bars/20 (MEDIUM)
- undertrading: HIGH se n_trades < 10 (era MEDIUM <5) — sample troppo
piccolo per distinguere edge da rumore (lucky shot)
- flat_too_long (NEW, HIGH): signal attivo per <5% delle bar — la
strategia ha mancato il regime, e' una non-strategia
- fees_eat_alpha (NEW, HIGH): gross_pnl > 0 ma fees > 50% del lordo —
margine sottile non sostenibile in produzione
Test count: 141 -> 145 (+4 nuovi test deterministici via monkeypatch).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 1 chiusa con tutti i 5 hard gate passati (run phase1-real-005):
- Loop converge: 3 gen consecutive crescita median 0.0001 -> 0.0188.
- Parse success: 100% (98/98) grazie a JSON grammar.
- Top-5 vs median: 1116x ratio (top-1 fit 0.3347 vs median 0.0003).
- Entropy fitness: 0.914 a gen 9 (sopra soglia 0.5).
- Cost: $0.069 reale vs $700 cap.
Decision: GO Phase 2 con 3 aggiustamenti (Adversarial soglie piu' strette,
speciation di base, walk-forward 70/30).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 1 v0 usava `max(0, dsr - 0.5*max_dd)` che azzerava brutalmente la fitness
quando max_dd > 2*dsr. Real run v4 aveva 55/55 strategie a fitness=0 (DSR ~0.001,
max_dd > 0.5), zero pressione selettiva sul GA.
v1: base = 0.5*dsr + 0.5*0.5*(tanh(sharpe)+1) in [0,1], modulata da penalty
moltiplicativa 1/(1+k*max_dd) in (0,1]. Hard kill (no-trade, HIGH adversarial)
preservati. Fitness sempre >0 per strategie con almeno 1 trade -> il GA
puo' preferire "meno cattivo" a "catastrofico" anche su sharpe negativo.
Tests: +3 nuovi (continuous mediocre, bounded, monotonic drawdown), 4 esistenti
restano verdi. Suite 138 -> 141 passed. ruff + mypy strict puliti.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
HypothesisAgent.propose ora riprova una volta in caso di parse o
validation error: il prompt user del retry include l'output precedente
(troncato a 800 char) e il messaggio di errore, così l'LLM può
auto-correggersi. Configurabile via max_retries (default 1).
Cambia il modello dati di HypothesisProposal: completion (singolare)
diventa completions: list[CompletionResult] con n_attempts. L'orchestrator
itera su completions per registrare il costo di ogni chiamata LLM,
incluse le retry.
Phase 1 v4 mostrava 64% di parse failure recuperabili: il retry punta
a tagliare quel tasso senza inflazionare i token oltre 2x worst-case.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sostituisce la grammatica S-expression con uno schema JSON stretto. La
grammatica S-expression falliva il parsing nel 64% delle generazioni del
modello Qwen3-235B sul run reale; JSON e' nativo per gli LLM moderni e
si parsa con json.loads.
Cambiamenti principali:
- grammar.py: costanti rinominate LOGICAL_OPS / COMPARATOR_OPS /
CROSSOVER_OPS / ACTION_VALUES / KIND_VALUES.
- parser.py: nuovo AST a dataclass tipizzato (OpNode, IndicatorNode,
FeatureNode, LiteralNode, Rule, Strategy); parse_strategy ora consuma
JSON tramite json.loads.
- validator.py: walk dispatchato per tipo (isinstance) invece di
pattern-matching su 'kind'; arity check su operatori e indicator.
- compiler.py: traversal del nuovo AST tipizzato, dispatch per
isinstance; logica indicator/feature/literal invariata.
- hypothesis.py: prompt SYSTEM riscritto con esempi JSON e vincoli
espliciti su no-nesting; estrazione via fence ```json``` + fallback
brace-balanced.
- __init__.py: re-export pubblico delle entita' del protocollo.
- Tutti i test (parser, validator, compiler, hypothesis_agent,
falsification, adversarial, e2e, smoke_run) migrati a JSON.
- Rimossa dipendenza sexpdata da pyproject.toml + uv.lock.
Test: 135 passed (era 122; aggiunti casi parser/validator).
ruff + mypy strict clean. Smoke run end-to-end OK.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Run reale phase1-real-003 ha rivelato: l'LLM genera occasionalmente
"(indicator sma 20 50)" o "(indicator sma (feature close) 20)". Il primo
crashava _ind_sma con TypeError. Il secondo passava attraverso il
validator ma non era supportato dal compiler.
Validator ora:
- Aggiunge INDICATOR_ARITY: sma/rsi/atr/realized_vol = 1 arg, macd = 0-3.
- Rifiuta esplicitamente Node fra gli args di indicator (no-nesting Phase 1).
- Rifiuta arity fuori range con messaggio chiaro.
Strategie con questi pattern vengono ora rigettate dal validator come
parse_error invece di crashare il run. Test suite resta 122 PASSED.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Run reale phase1-real-002 ha rivelato:
1. Cerbero/Deribit cap ~5000 candele per call. Una richiesta di 2 anni
1h (17500 candele) ritorna troncata. CerberoOHLCVLoader._fetch ora
pagina in chunk da 4500 barre, concatena e dedupe.
2. _ind_macd accettava solo (df, fast, slow). Il prompt suggerisce
"(indicator macd 12 26 9)" con 3 numeri (fast/slow/signal). Aggiunto
signal=9 default e calcolo histogram (macd_line - signal_line).
Test suite 122 PASSED, ruff e mypy clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Run reale phase1-real-001 ha rivelato due problemi:
1. 67% parse_error perche' qwen3 nestava indicatori non supportati
(es. "(sma (indicator realized_vol 30) 150)"). Il prompt SYSTEM
ora esplicita le regole strette: indicator non e' annidabile,
sma/rsi/etc. esistono solo come 1o argomento di indicator,
crossover/crossunder accetta espressioni-serie come (feature close)
o (indicator sma N).
2. max_drawdown calcolato su equity assoluta (P&L in unita' BTC) +1.0
produceva drawdown nominali enormi (>89000) per strategie con
posizioni perdenti su BTC a $96k. Normalizziamo dividendo per il
notional iniziale (close[0]), cosi' max_dd diventa drawdown
relativo al wealth iniziale.
Test suite resta 122 PASSED, ruff e mypy clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Aggiunge i template per gate decision memo (sez. 4.4 spec) e technical
report (sez. 4.5 spec). Da popolare con numeri reali a chiusura del run
phase1-real-001 (in corso).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Cerbero MCP diventa unica fonte di verità per dati di mercato Phase 1.
Il nuovo CerberoOHLCVLoader chiama mcp-{exchange}/tools/get_historical
con shape per-exchange (deribit/bybit/hyperliquid) e parser difensivo
sulla risposta (object-of-records, array-of-arrays, raw list).
- src/multi_swarm/data/cerbero_ohlcv.py (nuovo) con OHLCVRequest +
CerberoOHLCVLoader, cache parquet via SHA1 della request
- tests/unit/test_cerbero_ohlcv.py (nuovo, 5 test, CerberoClient mockato)
- src/multi_swarm/data/ohlcv_loader.py + test ccxt rimossi
- scripts/run_phase1.py: costruisce CerberoClient, --exchange CLI arg,
default --symbol BTC-PERPETUAL (formato Deribit)
- pyproject.toml: rimosso ccxt>=4.4 (uv sync ha rimosso 16 transitivi)
- .env.example: CERBERO_BASE_URL=https://cerbero-mcp.tielogic.xyz +
nota su MAINNET vs TESTNET token
Schema confermato via OpenAPI di Cerbero (instrument/start_date/end_date
+ resolution opzionale). Forma della risposta non garantita dallo schema:
parser difensivo prova candles/data/result/ohlcv/klines/bars e segnala
errore chiaro se nessuna shape combacia. Live verification skippata
(nessun token in .env).
Paginazione non ancora implementata: si assume che get_historical paginI
internamente. Da rivedere se una live call mostra cap (~1000 candele).
Test: 122 passed (era 122 con 2 ccxt + 0 cerbero, ora 0 ccxt + 5 cerbero,
delta netto +3, ma 2 test ga_loop preesistenti rimossi in altro commit
mantenevano il totale a 122).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>