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>
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>
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>
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>
- 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>
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 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>
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>
Tutti i tier (S/A/B/C/D) ora passano per OpenRouter via OpenAI SDK.
Modelli Anthropic raggiungibili via prefisso `anthropic/...`.
- pyproject: rimosso `anthropic>=0.39` da deps + uv.lock
- config: rimosso `anthropic_api_key` field
- LLMClient: dispatch unico, single client OpenAI con base_url OpenRouter
- defaults S/A/B → `anthropic/claude-{opus-4-7,sonnet-4-6}`
- retry exceptions: solo openai.* (drop anthropic.*)
- test rinominati e adattati: tier S/A/B mockano OpenAI con prefisso `anthropic/`
- rimosso test `tier_S_without_anthropic_key_raises` (non più rilevante)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Estende ModelTier a 5 livelli (S/A/B/C/D) con routing automatico:
S/A/B via Anthropic SDK, C/D via OpenRouter (OpenAI SDK). Aggiunge
prezzi per tier S (Opus), A (Sonnet placeholder) e D (Llama). Refactor
LLMClient.complete con dispatch tramite tier_models map e helper
_call_anthropic / _call_openrouter. Settings esposte per tutti e 5
i modelli env-configurabili.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
LLM_MODEL_TIER_C, LLM_MODEL_TIER_B e OPENROUTER_BASE_URL ora override-abili
via env. Default invariati (back-compat). LLMClient accetta i tre valori
come kwargs opzionali; run_phase1 li propaga da Settings.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Avvolge LLMClient.complete con tenacity (3 attempts, backoff esponenziale
2-10s) che ritenta solo su errori transient di OpenAI/Anthropic SDK
(APIConnectionError, APITimeoutError, InternalServerError). RateLimit,
Authentication e 4xx non vengono ritentati. reraise=True preserva
l'eccezione originale dopo l'esaurimento dei tentativi.
Aggiunti 2 test (marker slow): esaurimento retry su APIConnectionError
e successo al secondo tentativo dopo APITimeoutError.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Aggiunge HypothesisAgent che invoca LLMClient con system/user template
parametrizzati sul genoma e sul MarketSummary, poi estrae la S-expression
(da fence markdown lisp/scheme/sexp o testo nudo), la parsa e la valida.
Restituisce HypothesisProposal con strategy=None + parse_error in caso di
output malformato, mantenendo sempre il CompletionResult per accounting.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
LLMClient instrada richieste in base a ModelTier del genome:
- Tier C -> Qwen 2.5 72B via OpenRouter (chat completions)
- Tier B -> Sonnet 4.6 via Anthropic (messages API)
CompletionResult dataclass frozen con text, tokens, tier, model.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Aggiunge `uniform_crossover` che eredita ogni campo da p1 o p2
con probabilita' 0.5, popola `parent_ids=[p1.id, p2.id]` e incrementa
la generazione. Deterministico dato lo stesso `random.Random` seed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Dataclass per genoma agente ipotesi con campi prompt/feature/temperature/
top_p/model_tier/lookback/style + parent_ids/generation. Id sha1[:16]
deterministico su contenuto canonico (feature_access ordinate, float
arrotondati). to_dict/from_dict per persistenza.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Implementa compile_strategy che produce funzione df -> Series[Side]
con valutazione regole in ordine, prima-che-matcha vince, FLAT default
e NaN per warmup degli indicatori.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Aggiunge validatore semantico per AST Strategy: arity check su
verbi logici/comparatori/data, whitelist indicatori
(sma, rsi, atr, macd, realized_vol) e feature OHLCV.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Aggiunge il modulo `multi_swarm.protocol` con la grammatica del DSL di
strategia (15 verbi: 4 azioni, 3 logici, 3 comparatori, 4 dati, `when`
e `strategy`) e un parser che produce un AST tipizzato (Strategy/Rule/
Node). Lessing delegato a sexpdata, validazione del set di verbi e
forma `(when <cond> <action>)` gestita dal parser. Sollevata ParseError
su top-level malformato, strategia vuota, verbi sconosciuti o azioni
non terminali.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Client minimale verso Cerbero MCP unified server con header
Authorization Bearer e X-Bot-Tag su ogni richiesta. Retry esponenziale
solo su ConnectionError (non su 4xx). 4xx/5xx -> RuntimeError.
Switch da httpx a requests per allineamento con libreria mock
`responses` (httpx richiederebbe respx). httpx resta in deps per usi
futuri. Aggiunto types-requests ai dev deps per mypy strict.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Aggiunge expected_max_sharpe e deflated_sharpe_ratio per correggere
multiple testing nella valutazione di strategie. Considera skewness,
kurtosis e numero di trial.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Metriche base per valutazione strategie: Sharpe annualizzato
(default 8760 periodi/anno per dati orari), max drawdown
percentuale dalla curva equity, total return cumulativo.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Engine sincrono bar-per-bar con delay 1: segnale a t-1 esegue a open di t
per evitare lookahead. Position sizing 1 unit, fees su entry+exit,
mark-to-market su close, chiusura forzata posizione open a fine serie.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Side StrEnum (long/short/flat), frozen dataclasses con calcolo
unrealized_pnl per Position e gross/fees/net_pnl per Trade
(fees in basis point, default 5bp). 4 test TDD passing,
ruff + mypy strict clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>