20 Commits

Author SHA1 Message Date
Adriano Dal Pastro 23b7273e71 feat(paper): ETH a tick 5m + tooling per-year/per-tick analysis
scripts/run_paper_trading.py: AssetConfig ETH ora usa timeframe="5m" invece
del default 1h. Il winner c04dff7086 e' stato trovato dal GA su dati 5m
e a 1h la strategia perde:
- ETH @ 5m (native): +359.50% cum 7y, 77% winrate, max DD/yr 19%
- ETH @ 1h (precedente): -33.03% cum 7y, 67% winrate, max DD 74%
BTC resta a 1h (winner 238e4812 native a 1h, +104% 7y, Sharpe 2+ in 3 anni).

Nuovi script di analisi:
- scripts/yearly_strategies.py: breakdown per anno (2019-2025) di 4
  strategie su tick di discovery (trade/winrate/return/maxDD/Sharpe).
- scripts/multi_tick_strategies.py: confronto cross-tick (5m/15m/1h)
  per i 2 winner correnti. Documenta la divergenza tick-paper di ETH.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 22:10:38 +00:00
Adriano Dal Pastro 9c871d1d86 feat(validation): WFA tooling + multi-fold results phase1-100 runs
Aggiunge 2 script di analisi per validare i top-K genomi cross-fold:

- scripts/analyze_btc_winners.py: per-trade dump (wins/losses/winrate/
  avg_win/avg_loss/return/maxDD/Sharpe) per ogni top-K × 4 fold
  expanding-window WFA. Usato per identificare i winner robusti vs
  i lucky-shot overfit.

- scripts/compare_winners.py: cross-run comparison di 5 winner
  candidate (BTC 1h + ETH 1h + BTC 5m + ETH 5m) sui medesimi 4 fold,
  con totali cumulativi.

Risultati WFA freezati:
- validation-btc-100-001.json: BTC 1h baseline (undertrading=10)
- validation-btc-100-001-thr3.json: BTC 1h con threshold=3 (rilassato
  per strategie ultra-selettive)
- validation-btc-100-5m-thr3.json: BTC 5m con threshold=3

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 21:48:55 +00:00
Adriano Dal Pastro 8b767da5e7 feat(strategy): swap paper a 238e4812 (BTC) + c04dff7086 (ETH) — phase1-100 winners
Risultati phase1-{btc,eth}-100-001 (1h) + phase1-{btc,eth}-100-5m-001 (5m, 770K bars)
con 100-agent GA × 10 generazioni × fitness v2 hardened + WFA 0.7 split.

BTC (238e4812, meteorologist Gen 7 winner del run 1h):
- IS f=0.2604, Sharpe=0.437, 25 trades
- OOS: f=0.4184, Sharpe=1.296, +22.15% return fold 3, DD 7.4%
- WFA 4-fold: cum_ret +31.65%, 58.6% winrate, 2 fold Sharpe 2+

ETH (c04dff7086, mean-reversion oversold del run 5m):
- IS f=0.1881, OOS f=0.1789 (no overfit), Sharpe_OOS=0.171
- WFA 4-fold: cum_ret +23.83%, 77.8% winrate, 2 fold @ 100% winrate
- Primo ETH winner che sopravvive hard-kill v2 in tutta la storia progetto

Archive: btc_9cf506b8.json (precedente winner hardened-001), eth_facd6af85d5d.json.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 21:48:42 +00:00
Adriano Dal Pastro 1c0058ec3b feat(strategy): swap BTC paper strategy a 9cf506b8 (hardened-001 robust winner)
Sostituisce btc_fb63e851 (in produzione dal 13 maggio, net +$9.9k su 7y BTC,
Sharpe medio -0.20) con btc_9cf506b8, vincitore del run phase1-hardened-001
(K=50 G=15 7y, fitness-v2 con hard-kill su fees_eat_alpha + negative_net_pnl).

Performance comparate su 7y BTC continui:
- btc_fb63e851:   Net +$9,951  Sharpe medio -0.20  4/8 anni positivi
- btc_9cf506b8:   Net +$30,538 Sharpe medio +0.31  5/8 anni positivi
                  Best year 2021: +$18,938 (vs prod +$6,835)
                  Best Sharpe annuale: 2023 +1.27 (vs prod 2024 +1.16)
                  Zero adversarial findings su 7y continui.

Performance cross-asset (test su ETH 6.8y):
- btc_fb63e851 su ETH: -$120 (pseudo-flat, nessun segnale)
- btc_9cf506b8 su ETH: +$2,059 (4/7 anni positivi)
La strategia generalizza out-of-asset, indicatore di robustezza non-ETH-overfit.

DSL: physicist style, lookback 150 bar (~6 giorni). Triple-AND condition su
realized_vol + atr_pct + sma_pct per entry (long o short). Exit su sma_pct=0
o vol collasso (<0.1%). Selettivo: 502 trade in 7y = 1.4 trade/giorno medio.

btc_fb63e851 archiviato in strategies/archive/ per consultazione futura.
Il glob loader `btc_*.json` e' non-ricorsivo, quindi archive/ non viene
caricato automaticamente.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 13:21:37 +00:00
Adriano Dal Pastro 220e510d5e fix(orchestrator): escludi prompt_library dal config_dict JSON
PromptLibrary e' un frozen dataclass non JSON-serializable. Quando passata in
cfg.prompt_library e poi spread via **cfg.__dict__ in config_dict, faceva
fallire repo.create_run() con TypeError al primo run dopo refactor.

Fix: filtra cfg.__dict__ escludendo prompt_library, e salva separatamente la
lista degli stili (prompt_library_styles) per reproducibility.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 13:21:08 +00:00
Adriano Dal Pastro 9742df3a1f fix(fitness): hardening anti-overfit post-7y incident — 3 correzioni
Incident: extended run elite (Sharpe IS 1.93) net-negativo su 7y
continuous (fees=101% del gross). Multi-fold validation NON sufficiente:
ogni fold restarta equity, mascherando accumulo fees compound.

Correzioni:

1) Default --start esteso a 2018-09-01 (7.3 anni)
   - Copre bear 2018-19, halving 2020, COVID, ATH 2021, winter 2022,
     ETF rally 2024, regime corrente.
   - Una finestra corta (2y) lasciava il GA libero di overfit single-regime.

2) fees_eat_alpha promosso a hard-kill in fitness v2
   - Da soft-penalty 0.4x a hard-kill 0 fitness.
   - Una strategia con fees > 50% del gross non e' recuperabile via
     selection: il prodotto del GA non puo' deployare con quel cost burden.

3) Nuovo finding negative_net_pnl (HIGH, hard-kill)
   - Fires quando sum(trade.net_pnl) < 0 sul training window.
   - Cattura: gross negativo (no edge direzionale) E gross positivo ma
     fees > gross (edge insufficiente).
   - Sintesi del net-after-fees su finestra continua come "deal-breaker"
     non negoziabile via soft penalty.

CLI:
- --fitness-hard-kill <csv> per override esplicito.
- Default v2: no_trades,degenerate,undertrading,fees_eat_alpha,negative_net_pnl.

Test:
- 252 pass (251 + 1 nuovo: test_negative_net_pnl_fires_on_negative_gross).
- Test e2e WFA aggiornato: passa fitness_hard_kill_findings=('no_trades',)
  perche' il fixture sintetico non produce strategie profittevoli.
- test_no_findings_on_reasonable_strategy rinominato:
  test_rsi_mean_reversion_loses_money_on_synthetic_data (riflette
  semantica reale: RSI mean-rev su synthetic ohlcv ha net negativo).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 11:07:40 +00:00
Adriano Dal Pastro 21b5cf1eae feat(validation): add validation data for phase1-extended-001 with detailed results 2026-05-16 10:55:35 +00:00
Adriano Dal Pastro a29748e3d8 perf(backtest): vectorize engine + parallel LLM propose + multi-fold validation tool
- backtest/engine.py: state machine numpy invece di pd.iterrows()
  - 16.8x speedup su 2y (470ms -> 28ms), 11.3x su 7y (1744ms -> 154ms)
  - 7 parity test parametrici vs reference iterrows assicurano equivalenza
- orchestrator/run.py + run_phase1.py: --llm-concurrency N
  - ThreadPoolExecutor parallelizza hypothesis_agent.propose() per generazione
  - 5-8x speedup wall time GA loop (OpenRouter qwen-2.5 regge 6-10 concorrenti)
  - default 1 = backward-compat sequenziale
- scripts/validate_run.py: validation multi-fold standalone
  - prende run_id + top-K + N-folds expanding-window su dataset esteso (7y)
  - rivela overfitter mascherati da fitness IS alta (vedi
    phase1-extended-001: elite IS Sharpe 1.93 collassa OOS a -1.00)
  - ranking per robust_score = min(fitness_oos) su tutti i fold

Test 250/250 pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 10:53:48 +00:00
Adriano Dal Pastro fa11cca2bc docs(readme): allinea a stato finale post-refactor sessione 2026-05-15
Aggiornato per riflettere i 12 commit di refactor della sessione:

  - Layout: 3 servizi Docker (strategy-crypto-paper + strategy-crypto-gui
    + multi-swarm-core-gui), GUI split (core GA / strategy paper)
  - Test: 238 attesi (era 186)
  - Architettura prompt: SYSTEM compositor v3.2 documentato (9 sezioni
    iniettate da scaffold core + contenuto strategy via prompts.json)
  - Grammar: 8 indicatori (5 base + 3 _pct: atr_pct, sma_pct, macd_pct)
  - Input MarketSummary: 14 metriche (5 base + 4 regime + 5 geometria)
  - 7 stili cognitive in prompts.json (era 6, +military_strategist e
    psychologist), con focus_metrics + lookback consigliato + archetipo
  - Deploy: 2 URL distinti (/multi_swarm_core_gui + /strategy_crypto_gui),
    Traefik replacepathregex (NON stripprefix per evitare doppio root_path)
  - Note operative: chown 1000:1000 anche per strategies/ post git mv
  - 3 regression guard test ASCII/archetype/lookback documentati

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 22:05:06 +00:00
Adriano Dal Pastro 6526c6e6e3 refactor(prompts): strategy_crypto v3.2 — consolidamento + 3 invariant tests
Patch di consolidamento post-diagnosi v3.1, divisa in REGRESSIONI ripristinate
+ MIGLIORAMENTI opportunistici + INVARIANTI permanenti.

Regressioni v3.0 -> v3.1 ripristinate:
  1. ASCII-strict: caratteri circa-uguale (U+2248) sostituiti con 'vicino a' in 3 stili
     (historian, engineer, psychologist). Critico per encoding robustness.
  2. 'Archetipo dominante: <metafora>': chiusa identitaria reintrodotta in
     tutte le 7 directive. Ancora semantica resistente a mutate_prompt_llm.
  3. 'Lookback consigliato: X-Y barre': hint range differenziato per stile
     (physicist 150-300, biologist 80-200, historian 200-500, meteorologist
     50-150, engineer 60-120, military 100-200, psychologist 50-120) per
     orientare il parametro evoluto lookback_window del genoma.

Miglioramenti opportunistici:
  4. Voce attiva rinforzata: +verbi generativi ('costruisci', 'combina',
     'cattura', 'diagnostica', 'preferisci') in tutte le directive
  5. anti_patterns 5 -> 7 voci: aggiunti (6) chattering crossover same-type
     same-lookback, (7) soglie hard senza isteresi entry/exit
  6. output_priorities 4 -> 5 voci: aggiunta in cima (#1) 'coerenza con
     lente cognitiva' (fondamento del design swarm)
  7. domain_warnings: +frase 'seasonality > 0 non significa significativa, gate a 0.05'
  8. NEW _design_invariants metadata: documenta gli invarianti che future
     versioni DEVONO preservare (utile per chi edita + mutate_prompt_llm)

NEW invariant tests (regression guards permanenti):
  - test_strategy_crypto_directives_ascii_safe
  - test_strategy_crypto_directives_have_archetype_marker
  - test_strategy_crypto_directives_have_lookback_hint

Statistiche v3.2:
  - directive: 800-950 char (era 545-614 in v3.1, troppo snellite)
  - physicist 890, biologist 867, historian 887, meteorologist 884,
    engineer 904, military_strategist 898, psychologist 909

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 21:54:13 +00:00
Adriano Dal Pastro ccf9f7a33c refactor(prompts): strategy_crypto v3.1 — pulizia post-diagnosi
Pulizia contenuto prompts.json risolvendo le 9 debolezze identificate
in diagnosi v3.0 -> v3.1:

  - agent_role: + frase swarm awareness ("la diversita e' asset critico,
    esplora territori meno ovvi")
  - pattern_guidance: riscritto astratto (rimossi tutti i nomi di
    indicatori; SMA(short)>SMA(long), RSI>70, ecc -> "trend strutturale",
    "compressione vol", "mean reversion strutturale"). Il GA scopre
    l'incarnazione, le directive sono BIAS non template.
  - domain_warnings: riformulato come "NON assumere" (rimosso hint
    inferenziale su funding rate che avrebbe indotto hallucination)
  - directive: trimmate tutte sotto 900 char (era 922-975)
  - focus_metrics: standardizzati a 4 per stile (era 4-5, asimmetria
    estetica); rimosse ridondanze:
      * historian: rimosso autocorr_baseline (gia' visibile in
        Regime recente -> usare solo autocorr_recent come delta proxy)
      * psychologist: kurt/skew (gia' in base statistics) sostituiti
        con autocorr_recent + spectral_entropy (piu informativi)
  - NEW anti_patterns: lista esplicita (5 voci) per ridurre overfitting
    nel design della strategia (no > 4 AND, no singolo evento, no
    correlazione=causalita, no stazionarieta perfetta, no temporal
    features con seasonality < 0.05)
  - NEW output_priorities: trade-off espliciti (robustezza > ottimalita,
    semplicita > complessita, selettivita > attivita)
  - NEW _focus_metrics_design metadata: documenta l'intento delle scelte

Effetto atteso al prossimo Phase 1/2 run: ipotesi piu robuste
(meno overfitting cross-regime), strategie piu semplici (no 5+ AND),
maggior diversita di esplorazione (swarm awareness nel role), zero
inferenze su feature non accessibili.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 21:40:58 +00:00
Adriano Dal Pastro 19a3592a20 feat(prompt): PromptLibrary v3.1 — anti_patterns + output_priorities
Estende il compositor del SYSTEM con 2 sezioni opzionali iniettate
DOPO i VINCOLI core e PRIMA dell'EXAMPLE:

  ANTI-PATTERN DA EVITARE: lista esplicita di cose da evitare (overfitting,
    correlazione=causalita, > 4 AND, singolo evento estremo, ecc.)
  PRIORITA' DI OUTPUT: trade-off come "robustezza > ottimalita su singolo
    regime", "semplicita > complessita raffinata", "selettivita > attivita"

Razionale: ridurre la varianza non-utile nelle strategie generate
quando il LLM affronta trade-off, e prevenire overfitting nel design.
Entrambi i campi sono opzionali (skip se "") -> backward-compatible
con prompts.json v3.0.

PromptLibrary v3.1: +2 fields top-level (default "").
_build_system_prompt: 2 sezioni condizionali post-VINCOLI.

Test: +3 unit (compositor inject/skip + from_json parsing).
Tot: 235 test pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 21:38:35 +00:00
Adriano Dal Pastro 5202eb517b refactor(prompt): SYSTEM_TEMPLATE diventa compositor — strategy controlla agent_role/pattern_guidance/instruction
Refactor architetturale: il prompt non e' piu' un template hardcoded ma viene
COMPOSTO at-runtime da scaffold core + contenuto strategy.

CORE scaffold (universal, fisso):
  - Grammar JSON spec (legato al protocol/compiler)
  - UNITA' regole semantiche
  - VINCOLI del validator
  - Esempio output

STRATEGY content (tunable in prompts.json):
  - agent_role: "Sei un agente generatore di ipotesi ... [crypto/forex/equity]"
  - pattern_guidance: sezione di archetipi tecnici, ora crypto-specific
  - instruction: frase finale del USER ("Genera una strategia ... [crypto]")
  - domain_warnings: NEW opzionale, per disclaimer di dominio (es. crypto 24/7)

Implementazione:
  - PromptLibrary v3.0: 4 nuovi campi top-level (agent_role, pattern_guidance,
    instruction, domain_warnings), parsati da prompts.json, default fallback in default()
  - hypothesis.py: SYSTEM_TEMPLATE constant rimossa, sostituita da
    _build_system_prompt(lib, genome) che compone scaffold + content
  - USER_TEMPLATE: ultima riga ora ha placeholder {instruction}
  - prompts.json v3.0 in strategy_crypto: agent_role + pattern_guidance +
    instruction + domain_warnings popolati con flavor crypto-specific

Pattern: "core = framework, strategy = contenuto". Per future strategie
(forex, equity), basta creare un prompts.json con flavor diverso, zero
modifiche al core. Universal scaffold (grammar, vincoli, units) e'
condiviso e mantiene la garanzia di parse/validate.

Test: +5 unit (compositor + PromptLibrary fields).
Tot: 232 test pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 21:18:54 +00:00
Adriano Dal Pastro 898b24b6a3 fix(orchestrator): definisci prompt_library PRIMA di istanziare HypothesisAgent
Bug introdotto in b6f48e4: HypothesisAgent(prompt_library=prompt_library) era
chiamato a riga 109, ma prompt_library veniva definito a riga 123 -> NameError
a runtime quando run_phase1 viene eseguito.

Spostato il blocco di setup prompt_library + set_cognitive_styles PRIMA della
istanziazione di HypothesisAgent.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 21:05:39 +00:00
Adriano Dal Pastro b6f48e46fc feat(agents): 5 metriche geometrico-frattali + style-aware focus block
Aggiunge al MarketSummary 5 metriche regime-aware:
  - efficiency_ratio (Kaufman): discriminatore trending/ranging
  - tail_index_left/right (Hill): pesantezza code, robust vs kurtosis
  - structural_uptrend (HH/HL Dow-style): trend strutturale senza lag MA
  - compression_ratio: vol coil pre-breakout
  - spectral_entropy + dominant_cycle (gated): struttura ciclica nel FFT

Architettura "Style-aware focus, no filtering":
  - Tutte le 5 metriche restano visibili a tutti gli stili (landscape GA smooth)
  - prompts.json v2.2: ogni stile dichiara focus_metrics: list[str]
  - USER_TEMPLATE renderizza "Focus per la tua lente: ..." con i valori prioritari
  - Mutation cognitive_style preserva accesso a tutte le metriche (no discontinuita)

PromptLibrary esteso con focus field (parsato da JSON entry styles).
HypothesisAgent accetta prompt_library nel costruttore; orchestrator lo passa.

7 directive aggiornate per interpretare i 5 nuovi input attraverso la lente:
  - physicist: efficiency_ratio + dominant_cycle (modi armonici)
  - engineer: efficiency_ratio < 0.2 = no signal
  - psychologist: tail_left/right = paura/euforia ricorrente
  - ecc.

Test: +19 unit (metriche + focus rendering), +smoke MarketSummary.
Tot: 216 test pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 21:04:06 +00:00
Adriano Dal Pastro 0fd31d52ec feat(agents): estendi input LLM con 4 statistiche regime-aware + directive v2.1
Aggiunge al USER_TEMPLATE dell'HypothesisAgent 4 metriche calcolate su rolling
window 500 barre (no backward bias del full sample):
  - autocorr_lag1_recent vs autocorr_lag1_baseline (AR(1) delta vs storico)
  - hurst_recent (R/S analysis, persistenza di scala)
  - vol_percentile (0-100, posizione vol corrente nella distribuzione storica)
  - seasonality_hour, seasonality_dow (0-1, varianza spiegata da feature temporali)

Razionale: skew/kurt da soli sono ambigui — un AR(1) discrimina momentum vs
mean-reversion meglio di tutta la guidance sui momenti.

NEW funzioni in metrics/basic.py:
  - autocorr_lag1(returns)
  - hurst_exponent(returns) via R/S a scale multiple
  - vol_percentile_historical(returns, current_window=24, ref_window=2000)
  - seasonality_strength(returns, by={"hour"|"dow"})

MarketSummary esteso con 6 nuovi campi (con default); build_market_summary calcola
rolling-500 per "recent", full sample per "baseline".

prompts.json v2.1: tutte le 7 directive estese con frase di interpretazione
style-specific dei 4 nuovi input (no style collapse). Es:
  - physicist: "AR(1) sopra baseline = sistema con memoria fuori equilibrio"
  - engineer: "AR(1) > 0.05 con std contenuta = SNR favorevole"
  - psychologist: "AR(1) recente >> baseline = euforia coordinata"

Tests: +16 unit per le metriche, +1 smoke per MarketSummary populated.

Verifica: 207 test pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 20:43:58 +00:00
Adriano Dal Pastro a43157cd44 refactor(gui): split dashboard in core (GA) + strategy_crypto (paper)
- NEW src/multi_swarm_core/multi_swarm_core/dashboard/ con theme.py + data.py + nicegui_app.py
- theme.py condiviso (CSS + colors + _apply_theme + _json_to_html + _build_header parametrico)
- core GUI: pagine /, /convergence, /genomes — legge SOLO runs.db
- strategy GUI slim: solo /, legge SOLO strategy_crypto.db — importa theme dal core
- Aggiunto nicegui+plotly al core pyproject (uv.lock rigenerato)
- docker-compose: nuovo servizio multi-swarm-core-gui su /multi_swarm_core_gui
  (Traefik PathPrefix + replacepathregex, NO stripprefix per evitare doppio root_path)
- .env.example: DASHBOARD_ROOT_PATH ora per-servizio

Pattern: ogni modulo possiede la sua GUI, ogni GUI legge solo il proprio DB.
N strategie future = duplica lo scheletro strategy_crypto/frontend/.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 19:23:12 +00:00
Adriano Dal Pastro 436613bfde fix(traefik): sostituisci stripprefix con replacepathregex per evitare doppio root_path
Bug: la dashboard NiceGUI mostrava "Your browser does not support ES modules"
perche' le asset URL nell'HTML erano doppio-prefissate:
  /strategy_crypto_gui/strategy_crypto_gui/_nicegui/...

Root cause: il middleware traefik.stripprefix aggiunge automaticamente
X-Forwarded-Prefix header. uvicorn/Starlette legge il header e setta
root_path automaticamente, raddoppiando con quello passato esplicitamente
a ui.run(root_path="/strategy_crypto_gui").

Fix: traefik.replacepathregex strippa il prefix senza propagare il header.
uvicorn vede solo il root_path da ui.run -> asset prefissati una sola volta.

  - replacepathregex.regex=^/strategy_crypto_gui(/.*|$)
  - replacepathregex.replacement=$1

Verifica end-to-end:
- page: HTTP 200, asset prefix singolo
- /strategy_crypto_gui/_nicegui/3.12.0/static/socket.io.min.js: 200
- / (root): 404 (atteso)

NB: dopo cambio label, necessario `docker restart traefik-traefik-1` per
forzare refresh discovery (problema noto Traefik con label durante recreate
container nello stesso ciclo). Annotare per future modifiche middleware.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 19:10:18 +00:00
Adriano Dal Pastro f55e4f00c5 fix(deploy): rimuovi PYTHONPATH, member come runtime deps, StripPrefix per NiceGUI subpath
Tre fix di deploy emersi al primo build/up del nuovo compose:

1. Dockerfile: rimosso PYTHONPATH=/app/src
   Con layout uv workspace doppio-nest, PYTHONPATH=/app/src faceva ombra
   alla venv risolvendo le member-dir (multi_swarm_core/, strategy_crypto/)
   come namespace packages PEP 420 senza i sub-package del codice
   (cerbero, persistence, frontend, ecc.). I package sono installati come
   editable dal `uv sync --frozen` nella venv: niente PYTHONPATH necessario.

2. pyproject root: aggiunto [project] dependencies = [multi-swarm-core, strategy-crypto]
   I due member workspace erano in [dependency-groups.dev], escluse da
   `uv sync --frozen --no-dev` del builder Docker -> "ModuleNotFoundError:
   No module named multi_swarm_core". Spostati come dipendenze runtime del
   deployable app root; dev group ora contiene solo pytest/ruff/mypy.
   uv.lock rigenerato.

3. docker-compose.yml: aggiunto Traefik middleware StripPrefix
   NiceGUI con root_path="/strategy_crypto_gui" assume che il proxy
   strippi il prefix prima di girare al container (FastAPI route restano
   "/", "/paper", ecc.). Senza StripPrefix, NiceGUI riceveva
   "/strategy_crypto_gui/" e rispondeva 404. Aggiunte 2 label:
   - strategy-crypto-stripprefix middleware
   - router.middlewares = strategy-crypto-stripprefix

Verifica end-to-end:
- https://swarm.tielogic.xyz/strategy_crypto_gui/ -> HTTP 200 (31KB)
- https://swarm.tielogic.xyz/ -> HTTP 404 (root libera, atteso)
- paper run phase3-baseline-001 (fcf271d0...) tick=1 OK, $1000 equity
- state/strategy_crypto.db creato con 5 tabelle paper_trading_*

NB: permission fix per `src/strategy_crypto/strategy_crypto/strategies/`
fatto manualmente (chown 1000:1000) — i JSON migrati da git mv erano
root:root, container gira come uid 1000. Annotare per future strategies.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 18:59:14 +00:00
Adriano Dal Pastro 96e08ff78f feat(protocol): aggiungi sma_pct + macd_pct per completare la famiglia *_pct
Estende il fix atr_pct (commit f875df3 / 9c3b5ad) coprendo anche SMA e MACD:

protocol/compiler.py:
- _ind_sma_pct(length) = (close - sma) / sma
  Deviazione frazionale del close dalla SMA. Range tipico ±0.1.
  NB: NON e' sma/close (sempre ~1.0, inutile per literal). Uso ideale:
    sma_pct(50) > 0.05 -> "close 5% sopra media a 50 barre" (mean reversion)
- _ind_macd_pct(fast, slow, signal) = macd / close
  MACD come frazione del prezzo. Range tipico ±0.02. Uso ideale:
    macd_pct(12,26,9) > 0.005 -> "momentum > 0.5% del prezzo"

protocol/grammar.py: KNOWN_INDICATORS estesa con sma_pct + macd_pct
protocol/validator.py: arity (1,1) per sma_pct, (0,3) per macd_pct (come macd)

agents/hypothesis.py (system prompt LLM):
- Lista indicatori include sma_pct e macd_pct con annotazioni unita'
- Esempi corretti/errati estesi: sma_pct > 0.05, macd_pct > 0.005
- Pattern guidance: "Mean reversion: sma_pct(long) > 0.05" e
  "Momentum positivo conferma: macd_pct(12,26,9) > 0.005"

genome/mutation_prompt_llm.py: keyword whitelist estesa con sma_pct + macd_pct

Tests (+3):
- test_sma_pct_is_close_deviation_from_sma: identita' algebrica + sign
- test_macd_pct_is_macd_divided_by_close: identita' + scala (rapporto ~close)
- test_sma_pct_and_macd_pct_in_validator: regression validator

Verifica: 191 pass (era 188).

Closes [[protocol_unit_bug]] in full. Family *_pct ora completa per atr/sma/macd.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 18:28:21 +00:00
54 changed files with 7058 additions and 1117 deletions
+6 -3
View File
@@ -29,12 +29,15 @@ GA_DB_PATH=./state/runs.db
STRATEGY_CRYPTO_DB_PATH=./state/strategy_crypto.db STRATEGY_CRYPTO_DB_PATH=./state/strategy_crypto.db
# Docker / Traefik (usati SOLO da docker-compose.yml) # Docker / Traefik (usati SOLO da docker-compose.yml)
# Dominio base: traefik espone la dashboard su swarm.${DOMAIN_NAME}/strategy_crypto_gui # Dominio base: traefik espone le dashboard su swarm.${DOMAIN_NAME}/...
DOMAIN_NAME=tielogic.xyz DOMAIN_NAME=tielogic.xyz
# Porta interna della NiceGUI dashboard (Traefik fa il TLS davanti) # Porta interna della NiceGUI dashboard (Traefik fa il TLS davanti)
SWARM_DASHBOARD_PORT=8080 SWARM_DASHBOARD_PORT=8080
# Subpath URL del dashboard NiceGUI (usato come root_path in produzione) # Subpath URL del dashboard NiceGUI — ora PER-SERVIZIO nel docker-compose.yml:
DASHBOARD_ROOT_PATH=/strategy_crypto_gui # strategy-crypto-gui -> DASHBOARD_ROOT_PATH=/strategy_crypto_gui
# multi-swarm-core-gui -> DASHBOARD_ROOT_PATH=/multi_swarm_core_gui
# In sviluppo locale lascia vuoto (nessun subpath).
DASHBOARD_ROOT_PATH=
# Paper-trading runner — override del command nel compose (opzionali) # Paper-trading runner — override del command nel compose (opzionali)
PAPER_RUN_NAME=phase3-papertrade-prod PAPER_RUN_NAME=phase3-papertrade-prod
+6 -2
View File
@@ -37,8 +37,12 @@ COPY scripts ./scripts
ENV PATH="/app/.venv/bin:$PATH" \ ENV PATH="/app/.venv/bin:$PATH" \
PYTHONUNBUFFERED=1 \ PYTHONUNBUFFERED=1 \
PYTHONDONTWRITEBYTECODE=1 \ PYTHONDONTWRITEBYTECODE=1
PYTHONPATH=/app/src # NO PYTHONPATH: con uv workspace + layout doppio-nest, PYTHONPATH=/app/src
# farebbe ombra alla venv risolvendo le member-dir (multi_swarm_core/,
# strategy_crypto/) come namespace packages senza i sub-package del codice.
# I pacchetti sono installati come editable dal `uv sync --frozen` del builder
# e risolvibili direttamente via /app/.venv/.
RUN useradd -m -u 1000 app \ RUN useradd -m -u 1000 app \
&& mkdir -p /app/data /app/series /app/state /app/strategies \ && mkdir -p /app/data /app/series /app/state /app/strategies \
+115 -35
View File
@@ -12,23 +12,23 @@ git clone ssh://git@git.tielogic.xyz:222/Adriano/Multi_Swarm_Coevolutive.git
## Layout monorepo (uv workspace) ## Layout monorepo (uv workspace)
Il repo è un **workspace uv** con due member packages indipendenti: Il repo è un **workspace uv** con due member packages indipendenti, principio "**core = framework, strategy = contenuto**":
``` ```
multi_swarm_coevolutive/ repo root (workspace coordinator) multi_swarm_coevolutive/ repo root (workspace coordinator)
├── pyproject.toml workspace + dev deps + ruff/mypy/pytest ├── pyproject.toml workspace + dev deps + ruff/mypy/pytest
├── docker-compose.yml strategy-crypto-paper + strategy-crypto-gui ├── docker-compose.yml 3 servizi su immagine condivisa
├── Dockerfile immagine multi-swarm-coevolutive:dev ├── Dockerfile immagine multi-swarm-coevolutive:dev
├── uv.lock lock unico del workspace ├── uv.lock lock unico del workspace
├── data/, series/, state/ cache OHLCV + DB (runtime, gitignored) ├── data/, series/, state/ cache OHLCV + DB (runtime, gitignored)
├── scripts/ CLI entrypoints (run_phase1, run_paper_trading, ...) ├── scripts/ CLI entrypoints (run_phase1, run_paper_trading, ...)
└── src/ └── src/
├── multi_swarm_core/ WORKSPACE MEMBER (wheel: multi-swarm-core) ├── multi_swarm_core/ WORKSPACE MEMBER (wheel: multi-swarm-core)
│ ├── pyproject.toml deps: pandas, numpy, openai, pydantic, ... │ ├── pyproject.toml core deps (pandas, numpy, openai, pydantic, nicegui, ...)
│ ├── multi_swarm_core/ GA + genome + protocol + backtest + cerbero + │ ├── multi_swarm_core/ GA + genome + protocol + backtest + cerbero +
│ │ data + llm + agents + ga + orchestrator + │ │ data + llm + agents + ga + orchestrator +
│ │ metrics + persistence + config │ │ metrics + persistence + config + dashboard (GA-only)
│ ├── tests/ unit + integration (182 test) │ ├── tests/ unit + integration
│ └── docs/ design/ + decisions/ + reports/ │ └── docs/ design/ + decisions/ + reports/
└── strategy_crypto/ WORKSPACE MEMBER (wheel: strategy-crypto) └── strategy_crypto/ WORKSPACE MEMBER (wheel: strategy-crypto)
@@ -36,27 +36,73 @@ multi_swarm_coevolutive/ repo root (workspace coordinator)
├── README.md overview strategia + pattern per nuove strategie ├── README.md overview strategia + pattern per nuove strategie
├── strategy_crypto/ ├── strategy_crypto/
│ ├── backend/ paper-trading (executor, portfolio, persistence, schema) │ ├── backend/ paper-trading (executor, portfolio, persistence, schema)
│ ├── frontend/ NiceGUI dashboard dual-DB │ ├── frontend/ NiceGUI paper-only dashboard
── strategies/ JSON freezate (btc_*.json, eth_*.json) ── strategies/ JSON freezate (btc_*.json, eth_*.json)
└── tests/ smoke regression (import + json + schema) │ └── prompts.json v3.2: agent_role/pattern_guidance/instruction/
│ domain_warnings/anti_patterns/output_priorities +
│ 7 stili cognitive (directive + focus_metrics)
└── tests/ smoke regression
``` ```
**DB separati per dominio:** `state/runs.db` (GA core universale) + `state/strategy_crypto.db` (paper della strategia crypto). Pattern scala a N strategie senza naming collision. **DB separati per dominio:** `state/runs.db` (GA core universale) + `state/strategy_crypto.db` (paper della strategia crypto). Pattern scala a N strategie senza naming collision.
**Pattern N strategie future:** aggiungere `src/strategy_<asset>/` con lo stesso scheletro (`backend/`, `frontend/`, `strategies/`, `tests/`), DB dedicato `state/strategy_<asset>.db`, servizi Docker `strategy-<asset>-paper` + `strategy-<asset>-gui`, GUI su `/strategy_<asset>_gui`. **Pattern N strategie future:** aggiungere `src/strategy_<asset>/` con stesso scheletro (`backend/`, `frontend/`, `strategies/`, `tests/`, `prompts.json`), DB dedicato `state/strategy_<asset>.db`, servizi Docker `strategy-<asset>-paper` + `strategy-<asset>-gui`, GUI su `/strategy_<asset>_gui`. **Zero modifiche al core** richieste.
## Architettura prompt (v3.2)
**Compositor**: il SYSTEM prompt al LLM viene COMPOSTO at-runtime da scaffold core + contenuto strategy:
```
[1] agent_role ← strategy (prompts.json — chi è l'agente)
[2] cognitive_style + directive ← genome (evoluti dal GA)
[3] GRAMMAR_SPEC ← core scaffold (operatori, indicatori, units rules)
[4] pattern_guidance ← strategy (forme di curva astratte, no indicatori prescritti)
[5] domain_warnings ← strategy (es. "crypto trada 24/7, NON inferire funding rate")
[6] CONSTRAINTS ← core scaffold (validator semantics)
[7] anti_patterns ← strategy (7 voci: no >4 AND, no chattering, isteresi, ecc.)
[8] output_priorities ← strategy (5 voci, #1 coerenza con lente cognitiva)
[9] EXAMPLE ← core scaffold
```
**Input USER (calcolato da `build_market_summary`):**
- Base (5): mean, std, skew, kurt, vol_regime
- Regime recente rolling 500 (6): autocorr_lag1 (recent + baseline), hurst, vol_percentile, seasonality (hour + dow)
- Geometria & frattali (7): efficiency_ratio (Kaufman), tail_index (left + right Hill), structural_uptrend (HH/HL), compression, spectral_entropy, dominant_cycle (gated)
- Feature accessibili dal genome + lookback_window
- **Focus per la tua lente**: blocco style-aware (4 metriche prioritarie da `focus_metrics` di prompts.json)
- Instruction finale (da strategy)
**Grammar protocol JSON (8 indicatori):**
| Indicatore | Output | Range |
|------------|--------|-------|
| `sma(length)` | media mobile | unità prezzo |
| `sma_pct(length)` | (close-sma)/sma | ±0.1 frazione |
| `rsi(length)` | oscillator | 0-100 |
| `atr(length)` | true range | unità prezzo |
| `atr_pct(length)` | atr/close | 0-0.1 frazione |
| `realized_vol(window)` | std returns | 0-0.1 frazione |
| `macd(fast,slow,signal)` | momentum | unità prezzo |
| `macd_pct(...)` | macd/close | ±0.02 frazione |
**7 stili cognitive** (in `prompts.json`, editable): physicist, biologist, historian, meteorologist, engineer, military_strategist, psychologist. Ognuno con directive 800-950 char, ASCII-strict, archetipo dominante + lookback consigliato + 4 focus_metrics.
## Stato del progetto ## Stato del progetto
**Phase 3 (paper-trading forward-test) in corso** dal 13 maggio 2026 su VPS. Runner `scripts/run_paper_trading.py` long-running in Docker, dashboard NiceGUI su `https://swarm.tielogic.xyz/strategy_crypto_gui/`. Due strategie freezate: **Phase 3 (paper-trading forward-test) in corso** dal 13 maggio 2026 su VPS. Runner `scripts/run_paper_trading.py` long-running in Docker, dashboard NiceGUI su `https://swarm.tielogic.xyz/strategy_crypto_gui/`. Due strategie freezate in `src/strategy_crypto/strategy_crypto/strategies/`:
- `strategy_crypto/strategies/btc_fb63e851.json` — BTC-PERPETUAL, RSI estremi + ATR vs SMA + filtro orario 9-17 (Sharpe OOS +0,26 su 7,33 anni). - `btc_fb63e851.json` — BTC-PERPETUAL, RSI estremi + ATR vs SMA + filtro orario 9-17 (Sharpe OOS +0,26 su 7,33 anni).
- `strategy_crypto/strategies/eth_facd6af85d5d.json` — ETH-PERPETUAL, regime-based (Sharpe OOS +0,19 su 6,75 anni). - `eth_facd6af85d5d.json` — ETH-PERPETUAL, regime-based (con `atr_pct` post-fix bug unità).
Phase 1 → 2.7 chiuse (30 run GA, $3.74 cumulato LLM). Phase 1 → 2.7 chiuse (30 run GA, $3.74 cumulato LLM). Sessione refactor 15 maggio 2026:
- Split repo invertito, monorepo unificato come uv workspace
- Family `*_pct` completa (atr_pct, sma_pct, macd_pct) per fix bug unità
- Dashboard split: core (GA) vs strategy (paper)
- Prompt architecture compositor + prompts.json v3.2 (vedi decision log)
- [**Stato progetto e roadmap (14 maggio 2026)**](src/multi_swarm_core/docs/reports/2026-05-14-stato-progetto-e-roadmap.md) — riepilogo fasi, decisioni, caveat, roadmap. Documenti:
- Decision log: [`src/multi_swarm_core/docs/decisions/`](src/multi_swarm_core/docs/decisions/) (gate Phase 1, scelta nemotron). - [**Stato progetto e roadmap (14 maggio 2026)**](src/multi_swarm_core/docs/reports/2026-05-14-stato-progetto-e-roadmap.md)
- Design docs concettuali: [`src/multi_swarm_core/docs/design/`](src/multi_swarm_core/docs/design/). - Decision log: [`src/multi_swarm_core/docs/decisions/`](src/multi_swarm_core/docs/decisions/) (gate Phase 1, nemotron, atr_pct fix)
- Design docs concettuali: [`src/multi_swarm_core/docs/design/`](src/multi_swarm_core/docs/design/)
Stack: Python 3.13, uv workspace, hatchling, pytest+pytest-mock+responses, openai SDK (verso OpenRouter), requests+tenacity, pandas+numpy+scipy, sqlmodel+sqlite, nicegui+plotly, yfinance. Stack: Python 3.13, uv workspace, hatchling, pytest+pytest-mock+responses, openai SDK (verso OpenRouter), requests+tenacity, pandas+numpy+scipy, sqlmodel+sqlite, nicegui+plotly, yfinance.
@@ -65,7 +111,7 @@ Stack: Python 3.13, uv workspace, hatchling, pytest+pytest-mock+responses, opena
```bash ```bash
uv sync # installa entrambi i workspace member come editable uv sync # installa entrambi i workspace member come editable
cp .env.example .env # compila CERBERO_*_TOKEN e OPENROUTER_API_KEY cp .env.example .env # compila CERBERO_*_TOKEN e OPENROUTER_API_KEY
uv run pytest # 186 test attesi (182 core + 4 smoke strategy_crypto) uv run pytest # 252 test attesi (248 core + 4 smoke strategy_crypto)
``` ```
### Variabili .env richieste ### Variabili .env richieste
@@ -84,10 +130,9 @@ OPENROUTER_API_KEY=<sk-or-v1-...>
GA_DB_PATH=./state/runs.db GA_DB_PATH=./state/runs.db
STRATEGY_CRYPTO_DB_PATH=./state/strategy_crypto.db STRATEGY_CRYPTO_DB_PATH=./state/strategy_crypto.db
# Deploy Docker # Deploy Docker — DASHBOARD_ROOT_PATH ora per-servizio (vedi docker-compose.yml)
DOMAIN_NAME=tielogic.xyz DOMAIN_NAME=tielogic.xyz
SWARM_DASHBOARD_PORT=8080 SWARM_DASHBOARD_PORT=8080
DASHBOARD_ROOT_PATH=/strategy_crypto_gui # subpath traefik per la dashboard
``` ```
Backcompat: `DB_PATH` legacy continua a funzionare come alias di `GA_DB_PATH`. Backcompat: `DB_PATH` legacy continua a funzionare come alias di `GA_DB_PATH`.
@@ -105,14 +150,27 @@ uv run mypy src/ scripts/
# Smoke run (MockLLM + OHLCV sintetico, no API calls) # Smoke run (MockLLM + OHLCV sintetico, no API calls)
uv run python scripts/smoke_run.py uv run python scripts/smoke_run.py
# Run reale Phase 1/2 (Cerbero + OpenRouter, ~$0.07 per run K=20 10gen) # Run reale Phase 1/2 (Cerbero + OpenRouter, ~$0.15-0.25 per run K=20 10gen,
# ~$0.40-0.55 per run esteso K=40 20gen con WFA OOS).
# Default --start ora 2018-09-01 (7.3y, copre bear+halving+covid+ATH+winter+ETF).
uv run python scripts/run_phase1.py \ uv run python scripts/run_phase1.py \
--name run-XXX \ --name run-XXX \
--exchange deribit --symbol BTC-PERPETUAL --timeframe 1h \ --exchange deribit --symbol BTC-PERPETUAL --timeframe 1h \
--start 2024-01-01T00:00:00+00:00 \
--end 2026-01-01T00:00:00+00:00 \
--population-size 20 --n-generations 10 \ --population-size 20 --n-generations 10 \
--prompt-mutation-weight 0.30 --fitness-v2 --prompt-mutation-weight 0.30 --fitness-v2 \
--llm-concurrency 8 # 5-8x speedup wall time (default 1)
# fitness-v2 hardened: hard-kill su {no_trades, degenerate, undertrading,
# fees_eat_alpha, negative_net_pnl}. Override via --fitness-hard-kill CSV.
# Default --prompt-library: importlib.resources del package strategy_crypto/prompts.json
# Multi-fold validation di un run esistente (anti-overfit, 7y expanding-window)
uv run python scripts/validate_run.py \
--run-id <run_id> \
--top-k 10 --n-folds 4 --train-ratio 0.5 \
--start 2018-09-01T00:00:00+00:00 --end 2026-01-01T00:00:00+00:00 \
--fitness-v2 \
--output-json state/validation-XXX.json
# Ranking per "robust_score" = min(fitness_oos) su tutti i fold.
# Backtest standalone di una strategia JSON # Backtest standalone di una strategia JSON
uv run python scripts/backtest_strategy.py \ uv run python scripts/backtest_strategy.py \
@@ -123,32 +181,50 @@ uv run python scripts/backtest_strategy.py \
uv run python scripts/run_paper_trading.py \ uv run python scripts/run_paper_trading.py \
--name phase3-papertrade-XXX \ --name phase3-papertrade-XXX \
--initial-capital 1000 --poll-seconds 300 --initial-capital 1000 --poll-seconds 300
# Default --strategies-dir: importlib.resources del package strategy_crypto
# Dashboard NiceGUI locale # Dashboard NiceGUI locale (2 distinte)
uv run python -m strategy_crypto.frontend.nicegui_app uv run python -m multi_swarm_core.dashboard.nicegui_app # GA core (/, /convergence, /genomes)
# → http://localhost:8080 (env SWARM_DASHBOARD_PORT) uv run python -m strategy_crypto.frontend.nicegui_app # Strategy crypto (/ paper)
``` ```
## Dashboard ## Performance & Validation
NiceGUI dashboard (dark palette) — **dual-DB reader** (GA + paper): **Backtest engine vettorializzato** (`backtest/engine.py`): rimosso il loop `pd.iterrows()` a favore di state machine numpy. Speedup misurati:
| Dataset | Before (iterrows) | After (vectorized) | Speedup |
|---------|-------------------|--------------------|---------|
| 2 anni (17545 bar) | 470 ms | **28 ms** | **16.8×** |
| 7 anni (64297 bar) | 1744 ms | **154 ms** | **11.3×** |
Equivalenza numerica garantita: 5 parity test parametrici vs. reference implementation legacy (`test_backtest_engine_vectorized.py`).
**Parallel propose LLM** (`orchestrator/run.py`): `--llm-concurrency N` lancia N chiamate `hypothesis_agent.propose()` concorrenti per generazione tramite `ThreadPoolExecutor`. OpenRouter qwen-2.5 regge 6-10 concorrenti senza rate-limit. Default 1 = backward-compat.
**Multi-fold validation tool** (`scripts/validate_run.py`): qualunque run completato puo' essere rivalutato post-hoc su N fold expanding-window di un dataset esteso (tipicamente 7 anni). Vital per evitare il single-hold-out overfit: il GA puo' selezionare un genome con `fitness_is` alta che collassa OOS (osservato su `phase1-extended-001`: elite IS Sharpe 1.93, OOS Sharpe -1.00). Ranking finale per `robust_score = min(fitness_oos)`. Output JSON con per-fold breakdown + aggregati mean/min/std.
## Dashboard (split core + strategy)
Due NiceGUI dashboard distinte (dark palette, palette neon):
**Core GA**`multi_swarm_core.dashboard.nicegui_app``https://swarm.tielogic.xyz/multi_swarm_core_gui/`:
- **Overview** (`/`): lista runs GA, costo cumulato, metriche aggregate evaluations. - **Overview** (`/`): lista runs GA, costo cumulato, metriche aggregate evaluations.
- **GA Convergence** (`/convergence`): fitness median/max/p90 per generazione + entropy. - **GA Convergence** (`/convergence`): fitness median/max/p90 per generazione + entropy.
- **Genomes** (`/genomes`): top-K ordinati per fitness, ispezione system_prompt + JSON strategy. - **Genomes** (`/genomes`): top-K ordinati per fitness, ispezione system_prompt + JSON strategy.
- **Paper** (`/paper`): forward-test live con equity curve, posizioni aperte, trade list, tick log.
In produzione su `https://swarm.tielogic.xyz/strategy_crypto_gui/` (subpath gestito via `DASHBOARD_ROOT_PATH` + Traefik PathPrefix). La root del dominio resta libera per future GUI di altre strategie. **Strategy crypto**`strategy_crypto.frontend.nicegui_app``https://swarm.tielogic.xyz/strategy_crypto_gui/`:
- **Paper** (`/`): forward-test live con equity curve, posizioni aperte, trade list, tick log.
In produzione subpath gestiti via `DASHBOARD_ROOT_PATH` (per-servizio) + Traefik `replacepathregex` (NB: NON `stripprefix`, vedi sezione Deploy). La root del dominio resta libera per future GUI di altre strategie.
## Deploy ## Deploy
`docker-compose.yml` definisce due servizi su immagine `multi-swarm-coevolutive:dev`: `docker-compose.yml` definisce 3 servizi su immagine condivisa `multi-swarm-coevolutive:dev`:
- **`strategy-crypto-paper`** — runner `scripts/run_paper_trading.py` long-running. - **`strategy-crypto-paper`** — runner `scripts/run_paper_trading.py` long-running.
- **`strategy-crypto-gui`** — NiceGUI dashboard dietro Traefik su `https://swarm.${DOMAIN_NAME}/strategy_crypto_gui/`. - **`strategy-crypto-gui`** — NiceGUI paper dashboard su `https://swarm.${DOMAIN_NAME}/strategy_crypto_gui/`.
- **`multi-swarm-core-gui`** — NiceGUI GA dashboard su `https://swarm.${DOMAIN_NAME}/multi_swarm_core_gui/`.
Persistenza via bind mount: `./data/`, `./series/`, `./state/`. Le strategie JSON sono bind-mounted in read-only dal package: `./src/strategy_crypto/strategy_crypto/strategies/`. Persistenza via bind mount: `./data/`, `./series/`, `./state/`. Strategie JSON bind-mounted in read-only dal package: `./src/strategy_crypto/strategy_crypto/strategies/`.
```bash ```bash
docker compose up -d --build docker compose up -d --build
@@ -158,12 +234,16 @@ docker compose ps
Note operative: Note operative:
- Le bind-mount dir devono essere `chown 1000:1000` (uid utente `app` nel container). - Bind-mount dir devono essere `chown 1000:1000` (uid utente `app` nel container). **Anche `src/strategy_crypto/strategy_crypto/strategies/`** (creata da `git mv`, default `root:root`).
- Override del command paper via env (`PAPER_RUN_NAME`, `PAPER_INITIAL_CAPITAL`, ecc.). - Override del command paper via env (`PAPER_RUN_NAME`, `PAPER_INITIAL_CAPITAL`, ecc.).
- `SWARM_DASHBOARD_PORT` controlla la porta interna del container (Traefik fa il TLS). - `SWARM_DASHBOARD_PORT` controlla la porta interna del container (Traefik fa il TLS).
- **Traefik subpath**: usa `replacepathregex` middleware (NON `stripprefix`) per evitare doppio root_path (uvicorn legge `X-Forwarded-Prefix` da stripprefix + applica `root_path` di NiceGUI = doppio prefix). Vedi commit `436613b`.
- Dopo cambio label Traefik: `docker restart traefik-traefik-1` per forzare refresh discovery.
## Sviluppo ## Sviluppo
Conventional commits con prefix `feat:` `fix:` `chore:` `docs:` `refactor:` `test:`. Body italiano. Footer `Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>` su ogni commit collaborativo. Conventional commits con prefix `feat:` `fix:` `chore:` `docs:` `refactor:` `test:`. Body italiano. Footer `Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>` su ogni commit collaborativo.
Branch attuale: `main`. Workspace single-repo, monorepo unificato dal 15 maggio 2026 (split temporaneo monorepo→figlio invertito, vedi tag `v0.1.0-pre-split` come bookmark). Branch attuale: `main`. Workspace single-repo, monorepo unificato dal 15 maggio 2026 (split temporaneo monorepo→figlio invertito, vedi tag `v0.1.0-pre-split` come bookmark).
**Modificare il prompt LLM** senza toccare codice: edita `src/strategy_crypto/strategy_crypto/prompts.json`. Schema documentato in `_design_invariants` del JSON stesso. I 3 regression guard test (`test_strategy_crypto_directives_ascii_safe`, `..._have_archetype_marker`, `..._have_lookback_hint`) bloccano regressioni accidentali sulle invarianti di design.
+56 -10
View File
@@ -1,12 +1,14 @@
# docker-compose.yml — Multi-Swarm Coevolutive # docker-compose.yml — Multi-Swarm Coevolutive
# #
# Due servizi della strategia crypto, condividono la stessa immagine # Tre servizi della strategia crypto, condividono la stessa immagine
# `multi-swarm-coevolutive:dev` buildata dal Dockerfile root (uv workspace): # `multi-swarm-coevolutive:dev` buildata dal Dockerfile root (uv workspace):
# #
# * strategy-crypto-paper — paper-trading runner long-running # * strategy-crypto-paper — paper-trading runner long-running
# (scripts/run_paper_trading.py) # (scripts/run_paper_trading.py)
# * strategy-crypto-gui — NiceGUI dashboard esposta da Traefik su # * strategy-crypto-gui — NiceGUI dashboard esposta da Traefik su
# https://swarm.${DOMAIN_NAME:-tielogic.xyz}/strategy_crypto_gui # https://swarm.${DOMAIN_NAME:-tielogic.xyz}/strategy_crypto_gui
# * multi-swarm-core-gui — NiceGUI dashboard GA esposta su
# https://swarm.${DOMAIN_NAME:-tielogic.xyz}/multi_swarm_core_gui
# #
# Entrambi joinano la rete external `traefik` cosi' il client Cerbero # Entrambi joinano la rete external `traefik` cosi' il client Cerbero
# risolve direttamente l'host `cerbero-mcp` (porta 9000) senza passare # risolve direttamente l'host `cerbero-mcp` (porta 9000) senza passare
@@ -36,8 +38,11 @@ x-swarm-env: &swarm-env
# DB separati per dominio: # DB separati per dominio:
GA_DB_PATH: /app/state/runs.db GA_DB_PATH: /app/state/runs.db
STRATEGY_CRYPTO_DB_PATH: /app/state/strategy_crypto.db STRATEGY_CRYPTO_DB_PATH: /app/state/strategy_crypto.db
# Subpath sotto cui la dashboard NiceGUI e' esposta da Traefik # DASHBOARD_ROOT_PATH e' ora per-servizio (vedi environment blocks sotto).
DASHBOARD_ROOT_PATH: /strategy_crypto_gui # IMPORTANT: NON usare StripPrefix middleware con questo. NiceGUI/Starlette
# gestisce internamente il root_path su request path che ARRIVANO con prefix.
# StripPrefix causa doppio prefix negli asset URL (NiceGUI prefixa + uvicorn
# rilegge X-Forwarded-Prefix e prefixa di nuovo).
services: services:
strategy-crypto-paper: strategy-crypto-paper:
@@ -81,11 +86,10 @@ services:
env_file: .env env_file: .env
environment: environment:
<<: *swarm-env <<: *swarm-env
DASHBOARD_ROOT_PATH: /strategy_crypto_gui
volumes: volumes:
# Dashboard legge entrambi i DB: state/ in read-only (WAL: vedi nota) # Dashboard legge solo strategy_crypto.db: state/ in read-only (WAL: vedi nota)
- ./state:/app/state:ro - ./state:/app/state:ro
- ./data:/app/data:ro
- ./series:/app/series:ro
entrypoint: entrypoint:
- python - python
- -m - -m
@@ -109,4 +113,46 @@ services:
- traefik.http.routers.strategy-crypto-gui.entrypoints=websecure - traefik.http.routers.strategy-crypto-gui.entrypoints=websecure
- traefik.http.routers.strategy-crypto-gui.tls.certresolver=mytlschallenge - traefik.http.routers.strategy-crypto-gui.tls.certresolver=mytlschallenge
- "traefik.http.services.strategy-crypto-gui.loadbalancer.server.port=${SWARM_DASHBOARD_PORT:-8080}" - "traefik.http.services.strategy-crypto-gui.loadbalancer.server.port=${SWARM_DASHBOARD_PORT:-8080}"
# replacepathregex (NON stripprefix): strippa il prefix dalla request senza
# aggiungere X-Forwarded-Prefix header. uvicorn vede solo root_path da
# ui.run(root_path=...), quindi gli asset URL vengono prefissati una sola
# volta (no doppio prefix come succederebbe con stripprefix).
- "traefik.http.middlewares.strategy-crypto-replace.replacepathregex.regex=^/strategy_crypto_gui(/.*|$$)"
- "traefik.http.middlewares.strategy-crypto-replace.replacepathregex.replacement=$$1"
- "traefik.http.routers.strategy-crypto-gui.middlewares=strategy-crypto-replace"
- com.centurylinklabs.watchtower.enable=true
multi-swarm-core-gui:
image: multi-swarm-coevolutive:dev
build:
context: .
dockerfile: Dockerfile
container_name: multi-swarm-core-gui
restart: unless-stopped
networks: [traefik]
env_file: .env
environment:
<<: *swarm-env
DASHBOARD_ROOT_PATH: /multi_swarm_core_gui
volumes:
- ./state:/app/state:ro
entrypoint: [python, -m, multi_swarm_core.dashboard.nicegui_app]
command: []
healthcheck:
test: ["CMD", "python", "-c", "import os, urllib.request; urllib.request.urlopen(f'http://localhost:{os.environ.get(\"SWARM_DASHBOARD_PORT\",\"8080\")}/', timeout=3).close()"]
interval: 30s
timeout: 5s
retries: 3
start_period: 30s
labels:
- traefik.enable=true
- traefik.docker.network=traefik
- "traefik.http.routers.multi-swarm-core-gui.rule=Host(`swarm.${DOMAIN_NAME:-tielogic.xyz}`) && PathPrefix(`/multi_swarm_core_gui`)"
- traefik.http.routers.multi-swarm-core-gui.tls=true
- traefik.http.routers.multi-swarm-core-gui.entrypoints=websecure
- traefik.http.routers.multi-swarm-core-gui.tls.certresolver=mytlschallenge
- "traefik.http.services.multi-swarm-core-gui.loadbalancer.server.port=${SWARM_DASHBOARD_PORT:-8080}"
- "traefik.http.middlewares.multi-swarm-core-replace.replacepathregex.regex=^/multi_swarm_core_gui(/.*|$$)"
- "traefik.http.middlewares.multi-swarm-core-replace.replacepathregex.replacement=$$1"
- "traefik.http.routers.multi-swarm-core-gui.middlewares=multi-swarm-core-replace"
- com.centurylinklabs.watchtower.enable=true - com.centurylinklabs.watchtower.enable=true
+7 -2
View File
@@ -4,6 +4,13 @@ version = "0.1.0"
description = "Multi-Swarm Coevolutive: monorepo workspace (core + strategie)" description = "Multi-Swarm Coevolutive: monorepo workspace (core + strategie)"
authors = [{ name = "Adriano Dal Pastro", email = "adrianodalpastro@tielogic.com" }] authors = [{ name = "Adriano Dal Pastro", email = "adrianodalpastro@tielogic.com" }]
requires-python = ">=3.13" requires-python = ">=3.13"
# I due workspace member sono dipendenze runtime del deployable app root.
# Cosi' `uv sync --frozen --no-dev` (nel builder Docker) li installa entrambi
# come editable nella venv senza tirare pytest/ruff/mypy.
dependencies = [
"multi-swarm-core",
"strategy-crypto",
]
[tool.uv.workspace] [tool.uv.workspace]
members = ["src/multi_swarm_core", "src/strategy_crypto"] members = ["src/multi_swarm_core", "src/strategy_crypto"]
@@ -21,8 +28,6 @@ dev = [
"ruff>=0.7", "ruff>=0.7",
"mypy>=1.13", "mypy>=1.13",
"types-requests>=2.32", "types-requests>=2.32",
"multi-swarm-core",
"strategy-crypto",
] ]
[tool.ruff] [tool.ruff]
+125
View File
@@ -0,0 +1,125 @@
"""Analisi per-trade dei top-K candidate del run BTC.
Per ciascun genome top-K, ri-esegue il backtest su ogni fold WFA e raccoglie:
- n_trades, n_wins, n_losses, win_rate
- max_drawdown
- return, sharpe
- list trade pnl summary
Output stampato a stdout, non scrive su disco.
"""
from __future__ import annotations
import argparse
from datetime import datetime
import pandas as pd # type: ignore[import-untyped]
from multi_swarm_core.agents.hypothesis import _try_parse
from multi_swarm_core.backtest.engine import BacktestEngine
from multi_swarm_core.cerbero.client import CerberoClient
from multi_swarm_core.config import load_settings
from multi_swarm_core.data.cerbero_ohlcv import CerberoOHLCVLoader, OHLCVRequest
from multi_swarm_core.data.splits import expanding_walk_forward
from multi_swarm_core.metrics.basic import max_drawdown, sharpe_ratio, total_return
from multi_swarm_core.persistence.repository import Repository
from multi_swarm_core.protocol.compiler import compile_strategy
def main() -> None:
ap = argparse.ArgumentParser()
ap.add_argument("--run-id", required=True)
ap.add_argument("--top-k", type=int, default=10)
ap.add_argument("--n-folds", type=int, default=4)
ap.add_argument("--train-ratio", type=float, default=0.5)
ap.add_argument("--symbol", default="BTC-PERPETUAL")
ap.add_argument("--timeframe", default="1h")
ap.add_argument("--start", default="2018-09-01T00:00:00+00:00")
ap.add_argument("--end", default="2026-01-01T00:00:00+00:00")
ap.add_argument("--fees-bp", type=float, default=5.0)
args = ap.parse_args()
settings = load_settings()
repo = Repository(settings.ga_db_path)
repo.init_schema()
all_evals = repo.list_evaluations(args.run_id)
parseable = [
e for e in all_evals
if e.get("raw_text") and not e.get("parse_error") and e["fitness"] > 0
]
parseable.sort(key=lambda e: e["fitness"], reverse=True)
seen: set[str] = set()
top: list[dict] = []
for e in parseable:
if e["genome_id"] in seen:
continue
seen.add(e["genome_id"])
top.append(e)
if len(top) >= args.top_k:
break
token = (
settings.cerbero_mainnet_token.get_secret_value()
if settings.cerbero_mainnet_token
else settings.cerbero_testnet_token.get_secret_value()
)
cerbero = CerberoClient(
base_url=settings.cerbero_base_url,
token=token,
bot_tag=settings.cerbero_bot_tag,
)
loader = CerberoOHLCVLoader(client=cerbero, cache_dir=settings.series_dir)
ohlcv = loader.load(OHLCVRequest(
symbol=args.symbol,
timeframe=args.timeframe,
start=datetime.fromisoformat(args.start),
end=datetime.fromisoformat(args.end),
))
splits = expanding_walk_forward(ohlcv.index, train_ratio=args.train_ratio, n_folds=args.n_folds)
engine = BacktestEngine(fees_bp=args.fees_bp)
print(f"\n{'=' * 110}")
print(f"PER-TRADE ANALYSIS — top-{len(top)} genomes × {len(splits)} folds")
print(f"{'=' * 110}")
for ev in top:
strat, err = _try_parse(ev["raw_text"] or "")
if strat is None:
print(f"\n>>> {ev['genome_id'][:16]} — parse error: {err}")
continue
print(f"\n>>> {ev['genome_id']} (fit_IS={ev['fitness']:.4f}, sharpe_IS={ev['sharpe']:.3f})")
print(f"{'fold':<5} {'period':<26} {'trades':>7} {'wins':>5} {'losses':>7} {'win%':>6} {'avg_w':>9} {'avg_l':>9} {'ret':>7} {'maxDD':>7} {'sharpe':>7}")
for s in splits:
test_df = ohlcv.loc[s.test_idx]
try:
signal_fn = compile_strategy(strat)
signals = signal_fn(test_df)
bt = engine.run(test_df, signals)
except Exception as e:
print(f" fold {s.fold}: error {e}")
continue
trades = bt.trades
n_trades = len(trades)
wins = [t.net_pnl for t in trades if t.net_pnl > 0]
losses = [t.net_pnl for t in trades if t.net_pnl <= 0]
n_wins = len(wins)
n_losses = len(losses)
win_rate = (n_wins / n_trades * 100) if n_trades else 0.0
avg_w = (sum(wins) / n_wins) if n_wins else 0.0
avg_l = (sum(losses) / n_losses) if n_losses else 0.0
# Normalize equity for DD/return
if n_trades > 0:
notional = float(test_df["close"].iloc[0])
equity_pos = (bt.equity_curve / notional) + 1.0
ret_pct = total_return(equity_pos)
dd = max_drawdown(equity_pos)
sr = sharpe_ratio(bt.returns, periods_per_year=8760)
else:
ret_pct = dd = sr = 0.0
period = f"{str(s.test_idx[0])[:10]}..{str(s.test_idx[-1])[:10]}"
print(f"{s.fold:<5} {period:<26} {n_trades:>7} {n_wins:>5} {n_losses:>7} {win_rate:>5.1f}% {avg_w:>9.1f} {avg_l:>9.1f} {ret_pct:>6.2%} {dd:>6.2%} {sr:>7.3f}")
if __name__ == "__main__":
main()
+139
View File
@@ -0,0 +1,139 @@
"""Confronto per-trade dei 4 winner cross-run (BTC/ETH × 1h/5m).
Per ogni winner: ri-esegue il backtest su 4 fold WFA expanding-window e raccoglie
trade buoni/non buoni, win-rate, avg PnL, return, max DD, Sharpe.
"""
from __future__ import annotations
import argparse
from datetime import datetime
import pandas as pd # type: ignore[import-untyped]
from multi_swarm_core.agents.hypothesis import _try_parse
from multi_swarm_core.backtest.engine import BacktestEngine
from multi_swarm_core.cerbero.client import CerberoClient
from multi_swarm_core.config import load_settings
from multi_swarm_core.data.cerbero_ohlcv import CerberoOHLCVLoader, OHLCVRequest
from multi_swarm_core.data.splits import expanding_walk_forward
from multi_swarm_core.metrics.basic import max_drawdown, sharpe_ratio, total_return
from multi_swarm_core.persistence.repository import Repository
from multi_swarm_core.protocol.compiler import compile_strategy
# (run_name, genome_id, symbol, timeframe, label)
WINNERS = [
("phase1-btc-100-001", "238e481262c1594c", "BTC-PERPETUAL", "1h", "BTC 1h sharpshooter (Gen 7)"),
("phase1-btc-100-001", "23a24989e2ed0f84", "BTC-PERPETUAL", "1h", "BTC 1h robust (Gen 0 elite)"),
("phase1-eth-100-001", "4b45a72c13acf1d5", "ETH-PERPETUAL", "1h", "ETH 1h best-by-sharpe (killed)"),
("phase1-btc-100-5m-001", "f8ca6642adf7e0cd", "BTC-PERPETUAL", "5m", "BTC 5m robust winner"),
("phase1-eth-100-5m-001", "c04dff7086bb9588", "ETH-PERPETUAL", "5m", "ETH 5m OOS winner"),
]
def analyze_genome(run_id: str, genome_id: str, symbol: str, timeframe: str, label: str,
settings, cerbero, loader) -> None:
repo = Repository(settings.ga_db_path)
repo.init_schema()
evs = [e for e in repo.list_evaluations(run_id) if e["genome_id"] == genome_id]
if not evs:
print(f" no eval for {genome_id} in {run_id}")
return
ev = evs[0]
strat, err = _try_parse(ev.get("raw_text") or "")
if strat is None:
print(f" parse error: {err}")
return
req = OHLCVRequest(
symbol=symbol, timeframe=timeframe,
start=datetime.fromisoformat("2018-09-01T00:00:00+00:00"),
end=datetime.fromisoformat("2026-01-01T00:00:00+00:00"),
)
ohlcv = loader.load(req)
splits = expanding_walk_forward(ohlcv.index, train_ratio=0.5, n_folds=4)
engine = BacktestEngine(fees_bp=5.0)
print(f"\n>>> {label}")
print(f" {genome_id} | fit_IS={ev['fitness']:.4f} sharpe_IS={ev['sharpe']:.3f} trades_IS={ev['n_trades']}")
print(f" {'fold':<5} {'period':<26} {'trades':>7} {'wins':>5} {'losses':>7} {'win%':>6} {'avg_w':>10} {'avg_l':>10} {'ret':>8} {'maxDD':>7} {'sharpe':>7}")
sum_ret = 0.0
sum_trades = 0
sum_wins = 0
for s in splits:
test_df = ohlcv.loc[s.test_idx]
try:
signal_fn = compile_strategy(strat)
signals = signal_fn(test_df)
bt = engine.run(test_df, signals)
except Exception as e:
print(f" fold {s.fold}: error {e}")
continue
trades = bt.trades
n = len(trades)
wins = [t.net_pnl for t in trades if t.net_pnl > 0]
losses = [t.net_pnl for t in trades if t.net_pnl <= 0]
nw, nl = len(wins), len(losses)
wr = (nw / n * 100) if n else 0.0
aw = (sum(wins) / nw) if nw else 0.0
al = (sum(losses) / nl) if nl else 0.0
if n > 0:
notional = float(test_df["close"].iloc[0])
eq = (bt.equity_curve / notional) + 1.0
ret = total_return(eq)
dd = max_drawdown(eq)
sr = sharpe_ratio(bt.returns, periods_per_year=8760)
else:
ret = dd = sr = 0.0
period = f"{str(s.test_idx[0])[:10]}..{str(s.test_idx[-1])[:10]}"
print(f" {s.fold:<5} {period:<26} {n:>7} {nw:>5} {nl:>7} {wr:>5.1f}% {aw:>10.1f} {al:>10.1f} {ret:>7.2%} {dd:>6.2%} {sr:>7.3f}")
sum_ret += ret
sum_trades += n
sum_wins += nw
overall_wr = (sum_wins / sum_trades * 100) if sum_trades else 0.0
print(f" {'='*5} TOTALS: {sum_trades:>7} {sum_wins:>5} {sum_trades-sum_wins:>7} {overall_wr:>5.1f}% cum_ret={sum_ret:+.2%}")
def main() -> None:
settings = load_settings()
repo = Repository(settings.ga_db_path)
repo.init_schema()
name_to_id: dict[str, str] = {}
for w in WINNERS:
run_name = w[0]
if run_name in name_to_id:
continue
runs = repo.list_runs()
for r in runs:
if r["name"] == run_name:
name_to_id[run_name] = r["id"]
break
token = (
settings.cerbero_mainnet_token.get_secret_value()
if settings.cerbero_mainnet_token
else settings.cerbero_testnet_token.get_secret_value()
)
cerbero = CerberoClient(
base_url=settings.cerbero_base_url,
token=token,
bot_tag=settings.cerbero_bot_tag,
)
loader = CerberoOHLCVLoader(client=cerbero, cache_dir=settings.series_dir)
print(f"{'='*120}")
print(f"PER-TRADE COMPARISON — {len(WINNERS)} winner candidates × 4 folds WFA")
print(f"{'='*120}")
for run_name, genome_id, symbol, timeframe, label in WINNERS:
run_id = name_to_id.get(run_name)
if not run_id:
print(f"!!! run not found: {run_name}")
continue
analyze_genome(run_id, genome_id, symbol, timeframe, label, settings, cerbero, loader)
if __name__ == "__main__":
main()
+112
View File
@@ -0,0 +1,112 @@
"""2 winner cross-tick: BTC 238e4812 + ETH c04dff7086 su 5m / 15m / 1h.
Per ogni combinazione strategy × timeframe: backtest year-by-year (2019-2025)
con metriche per-anno e totale 7y.
"""
from __future__ import annotations
from datetime import datetime
from pathlib import Path
from multi_swarm_core.backtest.engine import BacktestEngine
from multi_swarm_core.cerbero.client import CerberoClient
from multi_swarm_core.config import load_settings
from multi_swarm_core.data.cerbero_ohlcv import CerberoOHLCVLoader, OHLCVRequest
from multi_swarm_core.metrics.basic import max_drawdown, sharpe_ratio, total_return
from multi_swarm_core.protocol.compiler import compile_strategy
from multi_swarm_core.protocol.parser import parse_strategy
WINNERS = [
# (label, path, symbol)
("BTC NEW (238e4812, native=1h)", "btc_238e4812.json", "BTC-PERPETUAL"),
("ETH NEW (c04dff7086, native=5m)", "eth_c04dff7086.json", "ETH-PERPETUAL"),
]
TIMEFRAMES = ["5m", "15m", "1h"]
YEARS = [
("2019", "2019-01-01T00:00:00+00:00", "2020-01-01T00:00:00+00:00"),
("2020", "2020-01-01T00:00:00+00:00", "2021-01-01T00:00:00+00:00"),
("2021", "2021-01-01T00:00:00+00:00", "2022-01-01T00:00:00+00:00"),
("2022", "2022-01-01T00:00:00+00:00", "2023-01-01T00:00:00+00:00"),
("2023", "2023-01-01T00:00:00+00:00", "2024-01-01T00:00:00+00:00"),
("2024", "2024-01-01T00:00:00+00:00", "2025-01-01T00:00:00+00:00"),
("2025", "2025-01-01T00:00:00+00:00", "2026-01-01T00:00:00+00:00"),
]
def evaluate(strat, ohlcv, engine, label, tf) -> None:
print(f"\n >>> tick={tf} | {len(ohlcv)} bars")
print(f" {'year':<6} {'trades':>7} {'wins':>5} {'losses':>7} {'win%':>6} {'ret':>8} {'maxDD':>7} {'sharpe':>7}")
sum_ret = 0.0
sum_trades = 0
sum_wins = 0
for year_label, start, end in YEARS:
mask = (ohlcv.index >= datetime.fromisoformat(start)) & (ohlcv.index < datetime.fromisoformat(end))
slice_df = ohlcv[mask]
if len(slice_df) == 0:
continue
try:
signal_fn = compile_strategy(strat)
signals = signal_fn(slice_df)
bt = engine.run(slice_df, signals)
except Exception as e:
print(f" {year_label:<6} ERROR: {e}")
continue
trades = bt.trades
n = len(trades)
wins = [t.net_pnl for t in trades if t.net_pnl > 0]
losses = [t.net_pnl for t in trades if t.net_pnl <= 0]
nw, nl = len(wins), len(losses)
wr = (nw / n * 100) if n else 0.0
if n > 0:
notional = float(slice_df["close"].iloc[0])
eq = (bt.equity_curve / notional) + 1.0
ret = total_return(eq)
dd = max_drawdown(eq)
sr = sharpe_ratio(bt.returns, periods_per_year=8760)
else:
ret = dd = sr = 0.0
print(f" {year_label:<6} {n:>7} {nw:>5} {nl:>7} {wr:>5.1f}% {ret:>7.2%} {dd:>6.2%} {sr:>7.3f}")
sum_ret += ret
sum_trades += n
sum_wins += nw
overall_wr = (sum_wins / sum_trades * 100) if sum_trades else 0.0
print(f" ===== 7y TOT: {sum_trades:>7} {sum_wins:>5} {sum_trades-sum_wins:>7} {overall_wr:>5.1f}% cum_ret={sum_ret:+.2%}")
def main() -> None:
settings = load_settings()
token = (
settings.cerbero_mainnet_token.get_secret_value()
if settings.cerbero_mainnet_token
else settings.cerbero_testnet_token.get_secret_value()
)
cerbero = CerberoClient(
base_url=settings.cerbero_base_url,
token=token,
bot_tag=settings.cerbero_bot_tag,
)
loader = CerberoOHLCVLoader(client=cerbero, cache_dir=settings.series_dir)
engine = BacktestEngine(fees_bp=5.0)
strategies_dir = Path("/app/strategies")
for label, fname, symbol in WINNERS:
path = strategies_dir / fname
strat = parse_strategy(path.read_text())
print(f"\n{'='*100}")
print(f">>> {label} — symbol={symbol}")
for tf in TIMEFRAMES:
try:
ohlcv = loader.load(OHLCVRequest(
symbol=symbol, timeframe=tf,
start=datetime.fromisoformat("2018-09-01T00:00:00+00:00"),
end=datetime.fromisoformat("2026-01-01T00:00:00+00:00"),
))
evaluate(strat, ohlcv, engine, label, tf)
except Exception as e:
print(f"\n >>> tick={tf} FAILED TO LOAD: {e}")
if __name__ == "__main__":
main()
+4 -2
View File
@@ -70,9 +70,11 @@ def load_assets(strategies_dir: Path) -> list[AssetConfig]:
raise FileNotFoundError( raise FileNotFoundError(
f"Expected btc_*.json and eth_*.json in {strategies_dir}" f"Expected btc_*.json and eth_*.json in {strategies_dir}"
) )
# ETH winner c04dff7086 e' tunato su 5m: a 1h la strategia perde (cum_ret -33% 7y).
# BTC winner 238e4812 e' tunato su 1h: tick native = paper tick.
return [ return [
AssetConfig(symbol="BTC-PERPETUAL", strategy_file=btc_files[0]), AssetConfig(symbol="BTC-PERPETUAL", strategy_file=btc_files[0], timeframe="1h"),
AssetConfig(symbol="ETH-PERPETUAL", strategy_file=eth_files[0]), AssetConfig(symbol="ETH-PERPETUAL", strategy_file=eth_files[0], timeframe="5m"),
] ]
+80 -6
View File
@@ -1,16 +1,48 @@
from __future__ import annotations from __future__ import annotations
import argparse import argparse
import importlib.resources
from datetime import datetime from datetime import datetime
from pathlib import Path
from multi_swarm_core.cerbero.client import CerberoClient from multi_swarm_core.cerbero.client import CerberoClient
from multi_swarm_core.config import load_settings from multi_swarm_core.config import load_settings
from multi_swarm_core.data.cerbero_ohlcv import CerberoOHLCVLoader, OHLCVRequest from multi_swarm_core.data.cerbero_ohlcv import CerberoOHLCVLoader, OHLCVRequest
from multi_swarm_core.genome.hypothesis import ModelTier from multi_swarm_core.genome.hypothesis import ModelTier
from multi_swarm_core.genome.prompt_library import PromptLibrary
from multi_swarm_core.llm.client import LLMClient from multi_swarm_core.llm.client import LLMClient
from multi_swarm_core.orchestrator.run import RunConfig, run_phase1 from multi_swarm_core.orchestrator.run import RunConfig, run_phase1
def _default_prompt_library_path() -> Path:
"""Default: prompts.json shippato col package strategy_crypto."""
return Path(str(importlib.resources.files("strategy_crypto") / "prompts.json"))
# Default v2 hard-kill list: oltre ai degenerate originali, fees_eat_alpha e
# negative_net_pnl sono deal-breaker non recuperabili via soft penalty (vedi
# 7y-overfit incident 2026-05-16: elite IS Sharpe 1.93 -> net -5% su 7y per fees).
_DEFAULT_V2_HARD_KILL: tuple[str, ...] = (
"no_trades",
"degenerate",
"undertrading",
"fees_eat_alpha",
"negative_net_pnl",
)
def _resolve_hard_kill(args) -> tuple[str, ...] | None:
"""Resolve la lista hard-kill da CLI args.
Priority: ``--fitness-hard-kill`` esplicito > default v2 > ``None`` (v1).
"""
if args.fitness_hard_kill:
return tuple(s.strip() for s in args.fitness_hard_kill.split(",") if s.strip())
if args.fitness_v2:
return _DEFAULT_V2_HARD_KILL
return None
def parse_args() -> argparse.Namespace: def parse_args() -> argparse.Namespace:
p = argparse.ArgumentParser(description="Multi-Swarm Phase 1 runner") p = argparse.ArgumentParser(description="Multi-Swarm Phase 1 runner")
p.add_argument("--name", default="phase1-spike-001") p.add_argument("--name", default="phase1-spike-001")
@@ -27,7 +59,10 @@ def parse_args() -> argparse.Namespace:
) )
p.add_argument("--symbol", default="BTC-PERPETUAL") p.add_argument("--symbol", default="BTC-PERPETUAL")
p.add_argument("--timeframe", default="1h") p.add_argument("--timeframe", default="1h")
p.add_argument("--start", default="2024-01-01T00:00:00+00:00") # Default esteso a 7.3 anni: copre bear 2018-19, halving 2020, COVID,
# ATH 2021, winter 2022, ETF rally 2024, regime corrente. Una finestra
# corta lascia il GA libero di overfit a un singolo regime.
p.add_argument("--start", default="2018-09-01T00:00:00+00:00")
p.add_argument("--end", default="2026-01-01T00:00:00+00:00") p.add_argument("--end", default="2026-01-01T00:00:00+00:00")
p.add_argument("--fees-bp", type=float, default=5.0) p.add_argument("--fees-bp", type=float, default=5.0)
p.add_argument("--n-trials-dsr", type=int, default=50) p.add_argument("--n-trials-dsr", type=int, default=50)
@@ -59,8 +94,10 @@ def parse_args() -> argparse.Namespace:
"--fitness-v2", "--fitness-v2",
action="store_true", action="store_true",
help=( help=(
"Attiva fitness v2: solo {no_trades, degenerate, undertrading} azzerano; " "Attiva fitness v2: hard-kill su {no_trades, degenerate, undertrading, "
"gli altri HIGH applicano soft penalty multiplicativa" "fees_eat_alpha, negative_net_pnl}; gli altri HIGH applicano soft penalty "
"multiplicativa. Versione hardened post 7y-overfit incident: fees + "
"net negativo non sono recuperabili."
), ),
) )
p.add_argument( p.add_argument(
@@ -69,6 +106,16 @@ def parse_args() -> argparse.Namespace:
default=0.4, default=0.4,
help="v2: fattore soft penalty per HIGH non-hard (default 0.4 → 1 HIGH → 0.71x)", help="v2: fattore soft penalty per HIGH non-hard (default 0.4 → 1 HIGH → 0.71x)",
) )
p.add_argument(
"--fitness-hard-kill",
type=str,
default=None,
help=(
"Override comma-separated della lista di finding name che azzerano la "
"fitness in modalita' v2. Es: 'no_trades,degenerate'. Default v2: "
"no_trades,degenerate,undertrading,fees_eat_alpha,negative_net_pnl."
),
)
p.add_argument( p.add_argument(
"--wfa-train-split", "--wfa-train-split",
type=float, type=float,
@@ -96,6 +143,26 @@ def parse_args() -> argparse.Namespace:
default=0.5, default=0.5,
help="Multi-objective: peso IS (1-alpha = OOS). 1.0=solo IS, 0.5=bilanciato, 0.0=solo OOS", help="Multi-objective: peso IS (1-alpha = OOS). 1.0=solo IS, 0.5=bilanciato, 0.0=solo OOS",
) )
p.add_argument(
"--prompt-library",
type=Path,
default=None,
help=(
"Path al file JSON con stili cognitivi + direttive system_prompt. "
"Default: strategy_crypto/prompts.json shippato col package. "
"Schema: {styles: {<name>: {directive: <testo>}}}"
),
)
p.add_argument(
"--llm-concurrency",
type=int,
default=1,
help=(
"Numero di propose() LLM concorrenti per generazione (default 1 = "
"serial). 6-10 tipicamente accettati da OpenRouter qwen-2.5 senza "
"rate-limit; riduce wall time GA loop di 5-8x."
),
)
return p.parse_args() return p.parse_args()
@@ -103,6 +170,13 @@ def main() -> None:
args = parse_args() args = parse_args()
settings = load_settings() settings = load_settings()
prompt_lib_path = args.prompt_library or _default_prompt_library_path()
prompt_library = PromptLibrary.from_json(prompt_lib_path)
print(
f"PromptLibrary loaded from {prompt_lib_path}: "
f"{len(prompt_library.styles)} stili ({', '.join(prompt_library.cognitive_styles)})"
)
token = ( token = (
settings.cerbero_mainnet_token.get_secret_value() settings.cerbero_mainnet_token.get_secret_value()
if settings.cerbero_mainnet_token if settings.cerbero_mainnet_token
@@ -153,14 +227,14 @@ def main() -> None:
fees_eat_alpha_threshold=args.fees_eat_alpha_threshold, fees_eat_alpha_threshold=args.fees_eat_alpha_threshold,
flat_too_long_threshold=args.flat_too_long_threshold, flat_too_long_threshold=args.flat_too_long_threshold,
undertrading_threshold=args.undertrading_threshold, undertrading_threshold=args.undertrading_threshold,
fitness_hard_kill_findings=( fitness_hard_kill_findings=_resolve_hard_kill(args),
("no_trades", "degenerate", "undertrading") if args.fitness_v2 else None
),
fitness_adversarial_soft_penalty=args.fitness_soft_penalty, fitness_adversarial_soft_penalty=args.fitness_soft_penalty,
wfa_train_split=args.wfa_train_split, wfa_train_split=args.wfa_train_split,
wfa_top_k=args.wfa_top_k, wfa_top_k=args.wfa_top_k,
eval_oos_during_loop=args.eval_oos_during_loop, eval_oos_during_loop=args.eval_oos_during_loop,
fitness_combined_alpha=args.fitness_combined_alpha, fitness_combined_alpha=args.fitness_combined_alpha,
prompt_library=prompt_library,
llm_concurrency=args.llm_concurrency,
) )
run_id = run_phase1(cfg, ohlcv=ohlcv, llm=llm) run_id = run_phase1(cfg, ohlcv=ohlcv, llm=llm)
+271
View File
@@ -0,0 +1,271 @@
"""Multi-fold validation di un run esistente.
Prende un ``run_id`` da ``state/runs.db``, seleziona i top-K genomi per fitness IS,
e li rivaluta su N fold expanding-window di un dataset OHLCV (tipicamente piu'
lungo del train del GA). Output: per-fold + aggregati (mean / min / std) della
fitness OOS.
Use case: il GA puo' selezionare un "lucky-shot" overfit a uno specifico regime.
Validare i top-K su finestre temporali diverse rivela quali strategie sono
robuste vs overfitter.
Esempio::
python scripts/validate_run.py \\
--run-id e263651598894da688d95fda90a34a96 \\
--top-k 10 --n-folds 4 \\
--symbol BTC-PERPETUAL --timeframe 1h \\
--start 2018-09-01 --end 2026-01-01
"""
from __future__ import annotations
import argparse
import json
import statistics
from datetime import datetime
from pathlib import Path
import pandas as pd # type: ignore[import-untyped]
from multi_swarm_core.agents.adversarial import AdversarialAgent
from multi_swarm_core.agents.falsification import FalsificationAgent
from multi_swarm_core.agents.hypothesis import _try_parse
from multi_swarm_core.cerbero.client import CerberoClient
from multi_swarm_core.config import load_settings
from multi_swarm_core.data.cerbero_ohlcv import CerberoOHLCVLoader, OHLCVRequest
from multi_swarm_core.data.splits import expanding_walk_forward
from multi_swarm_core.ga.fitness import compute_fitness
from multi_swarm_core.persistence.repository import Repository
def parse_args() -> argparse.Namespace:
p = argparse.ArgumentParser(description="Multi-fold validation di top-K genomi")
p.add_argument("--run-id", required=True, help="run_id da validare")
p.add_argument("--top-k", type=int, default=10, help="quanti genomi top valutare")
p.add_argument("--n-folds", type=int, default=4, help="numero fold expanding-window")
p.add_argument(
"--train-ratio",
type=float,
default=0.5,
help="frazione iniziale per il train iniziale (folds testano la coda)",
)
p.add_argument("--symbol", default="BTC-PERPETUAL")
p.add_argument("--timeframe", default="1h")
p.add_argument("--exchange", default="deribit", choices=["deribit", "bybit", "hyperliquid"])
p.add_argument("--start", default="2018-09-01T00:00:00+00:00")
p.add_argument("--end", default="2026-01-01T00:00:00+00:00")
p.add_argument("--fees-bp", type=float, default=5.0)
p.add_argument("--n-trials-dsr", type=int, default=50)
p.add_argument(
"--fees-eat-alpha-threshold", type=float, default=0.5,
)
p.add_argument(
"--flat-too-long-threshold", type=float, default=0.95,
)
p.add_argument(
"--undertrading-threshold", type=int, default=10,
)
p.add_argument(
"--fitness-v2", action="store_true",
help="Coerente con --fitness-v2 del run originale",
)
p.add_argument(
"--fitness-soft-penalty", type=float, default=0.4,
)
p.add_argument(
"--output-json",
type=Path,
default=None,
help="Path JSON dove salvare i risultati (default: stdout solo)",
)
return p.parse_args()
def main() -> None:
args = parse_args()
settings = load_settings()
# Repository: top-K genomi per fitness IS, con raw_text parsable.
repo = Repository(settings.ga_db_path)
repo.init_schema()
run = repo.get_run(args.run_id)
if run is None:
raise SystemExit(f"run_id non trovato: {args.run_id}")
print(f"Validating run: {run['name']} ({args.run_id})")
print(f" status: {run['status']}, cost: ${run['total_cost_usd']:.4f}")
all_evals = repo.list_evaluations(args.run_id)
parseable = [
e for e in all_evals
if e.get("raw_text") and not e.get("parse_error") and e["fitness"] > 0
]
parseable.sort(key=lambda e: e["fitness"], reverse=True)
# Dedup by genome_id (gli elite vengono salvati una sola volta ma possono apparire
# in evaluations multiple se rivalutati con eval_oos_during_loop).
seen_ids: set[str] = set()
top_genomes: list[dict] = []
for e in parseable:
if e["genome_id"] in seen_ids:
continue
seen_ids.add(e["genome_id"])
top_genomes.append(e)
if len(top_genomes) >= args.top_k:
break
print(f" selected top-{len(top_genomes)} genomes for validation")
# OHLCV: carica il dataset esteso.
token = (
settings.cerbero_mainnet_token.get_secret_value()
if settings.cerbero_mainnet_token
else settings.cerbero_testnet_token.get_secret_value()
)
cerbero = CerberoClient(
base_url=settings.cerbero_base_url,
token=token,
bot_tag=settings.cerbero_bot_tag,
)
loader = CerberoOHLCVLoader(client=cerbero, cache_dir=settings.series_dir)
req = OHLCVRequest(
symbol=args.symbol,
timeframe=args.timeframe,
start=datetime.fromisoformat(args.start),
end=datetime.fromisoformat(args.end),
exchange=args.exchange,
)
ohlcv = loader.load(req)
print(f" OHLCV: {len(ohlcv)} bars from {ohlcv.index[0]} to {ohlcv.index[-1]}")
splits = expanding_walk_forward(
ohlcv.index, train_ratio=args.train_ratio, n_folds=args.n_folds,
)
print(f" generated {len(splits)} folds")
for s in splits:
print(f" fold {s.fold}: test [{s.test_idx[0]} -> {s.test_idx[-1]}] ({len(s.test_idx)} bars)")
fals_agent = FalsificationAgent(fees_bp=args.fees_bp, n_trials_dsr=args.n_trials_dsr)
adv_agent = AdversarialAgent(
fees_bp=args.fees_bp,
fees_eat_alpha_threshold=args.fees_eat_alpha_threshold,
flat_too_long_threshold=args.flat_too_long_threshold,
undertrading_threshold=args.undertrading_threshold,
)
hard_kill = (
("no_trades", "degenerate", "undertrading") if args.fitness_v2 else None
)
# Itera per ogni genome + fold.
results: list[dict] = []
for gi, ev in enumerate(top_genomes):
strategy, parse_err = _try_parse(ev["raw_text"] or "")
if strategy is None:
print(f" [{gi}] {ev['genome_id'][:12]} skip (parse error: {parse_err})")
continue
per_fold: list[dict] = []
for s in splits:
test_df = ohlcv.loc[s.test_idx]
try:
fals = fals_agent.evaluate(strategy, test_df)
adv = adv_agent.review(strategy, test_df)
fit = compute_fitness(
fals, adv,
hard_kill_findings=hard_kill,
adversarial_soft_penalty=args.fitness_soft_penalty,
)
except Exception as e:
print(f" fold {s.fold} eval failed: {e}")
continue
per_fold.append({
"fold": s.fold,
"fitness": float(fit),
"sharpe": float(fals.sharpe),
"dsr": float(fals.dsr),
"dsr_pvalue": float(fals.dsr_pvalue),
"return": float(fals.total_return),
"max_dd": float(fals.max_drawdown),
"n_trades": int(fals.n_trades),
"test_start": str(s.test_idx[0]),
"test_end": str(s.test_idx[-1]),
})
if not per_fold:
continue
fits = [pf["fitness"] for pf in per_fold]
sharps = [pf["sharpe"] for pf in per_fold]
results.append({
"genome_id": ev["genome_id"],
"fitness_is": float(ev["fitness"]),
"sharpe_is": float(ev["sharpe"]),
"folds": per_fold,
"fitness_oos_mean": statistics.mean(fits),
"fitness_oos_min": min(fits),
"fitness_oos_max": max(fits),
"fitness_oos_std": statistics.pstdev(fits) if len(fits) > 1 else 0.0,
"sharpe_oos_mean": statistics.mean(sharps),
"sharpe_oos_min": min(sharps),
"robust_score": min(fits), # min across folds = pessimismo
})
# Ranking finale: per robust_score (min fitness) decrescente.
results.sort(key=lambda r: r["robust_score"], reverse=True)
print()
print(f"{'='*120}")
print(f"VALIDATION RESULTS ({len(results)} genomes, {len(splits)} folds)")
print(f"{'='*120}")
print(
f"{'rank':>4} {'genome':12} {'fit_is':>8} {'sh_is':>7} "
f"{'fit_mean':>9} {'fit_min':>8} {'fit_max':>8} {'fit_std':>8} "
f"{'sh_mean':>8} {'sh_min':>8} {'robust':>7}"
)
print("-" * 120)
for rank, r in enumerate(results, 1):
print(
f"{rank:>4} {r['genome_id'][:12]:12} "
f"{r['fitness_is']:>8.4f} {r['sharpe_is']:>7.3f} "
f"{r['fitness_oos_mean']:>9.4f} {r['fitness_oos_min']:>8.4f} "
f"{r['fitness_oos_max']:>8.4f} {r['fitness_oos_std']:>8.4f} "
f"{r['sharpe_oos_mean']:>8.3f} {r['sharpe_oos_min']:>8.3f} "
f"{r['robust_score']:>7.4f}"
)
if results:
winner = results[0]
print()
print(f"ROBUST WINNER: {winner['genome_id']}")
print(f" fitness_is={winner['fitness_is']:.4f}, "
f"fitness_oos_min={winner['fitness_oos_min']:.4f}, "
f"fitness_oos_mean={winner['fitness_oos_mean']:.4f}")
print(f" sharpe_is={winner['sharpe_is']:.3f}, "
f"sharpe_oos_min={winner['sharpe_oos_min']:.3f}")
print(f" per-fold breakdown:")
for pf in winner["folds"]:
print(
f" fold {pf['fold']} [{pf['test_start'][:10]} .. {pf['test_end'][:10]}]: "
f"fit={pf['fitness']:.4f} sharpe={pf['sharpe']:.3f} "
f"ret={pf['return']:.3f} n_trades={pf['n_trades']}"
)
if args.output_json:
payload = {
"run_id": args.run_id,
"run_name": run["name"],
"n_folds": len(splits),
"top_k_requested": args.top_k,
"top_k_evaluated": len(results),
"symbol": args.symbol,
"timeframe": args.timeframe,
"start": args.start,
"end": args.end,
"ohlcv_bars": len(ohlcv),
"results": results,
}
args.output_json.write_text(json.dumps(payload, indent=2, default=str))
print(f"\nResults saved to: {args.output_json}")
if __name__ == "__main__":
main()
+112
View File
@@ -0,0 +1,112 @@
"""Per-year breakdown delle 4 strategie: 2 NEW (BTC 238e4812 + ETH c04dff7086)
+ 2 OLD freezate (btc_9cf506b8 hardened-001 + eth_facd6af85d5d).
Backtest anno-per-anno (2019-2025) sul tick di discovery di ciascuna strategia.
Output: trade, wins/losses, win%, return%, max DD%, Sharpe per ogni anno.
"""
from __future__ import annotations
from datetime import datetime
from pathlib import Path
from multi_swarm_core.backtest.engine import BacktestEngine
from multi_swarm_core.cerbero.client import CerberoClient
from multi_swarm_core.config import load_settings
from multi_swarm_core.data.cerbero_ohlcv import CerberoOHLCVLoader, OHLCVRequest
from multi_swarm_core.metrics.basic import max_drawdown, sharpe_ratio, total_return
from multi_swarm_core.protocol.compiler import compile_strategy
from multi_swarm_core.protocol.parser import parse_strategy
STRATEGIES = [
# (label, path, symbol, timeframe)
("BTC NEW (238e4812, paper attuale)", "btc_238e4812.json", "BTC-PERPETUAL", "1h"),
("BTC OLD (9cf506b8, hardened-001 prev paper)", "archive/btc_9cf506b8.json", "BTC-PERPETUAL", "1h"),
("ETH NEW (c04dff7086, paper attuale)", "eth_c04dff7086.json", "ETH-PERPETUAL", "5m"),
("ETH OLD (facd6af85d5d, prev paper)", "archive/eth_facd6af85d5d.json", "ETH-PERPETUAL", "1h"),
]
YEARS = [
("2019", "2019-01-01T00:00:00+00:00", "2020-01-01T00:00:00+00:00"),
("2020", "2020-01-01T00:00:00+00:00", "2021-01-01T00:00:00+00:00"),
("2021", "2021-01-01T00:00:00+00:00", "2022-01-01T00:00:00+00:00"),
("2022", "2022-01-01T00:00:00+00:00", "2023-01-01T00:00:00+00:00"),
("2023", "2023-01-01T00:00:00+00:00", "2024-01-01T00:00:00+00:00"),
("2024", "2024-01-01T00:00:00+00:00", "2025-01-01T00:00:00+00:00"),
("2025", "2025-01-01T00:00:00+00:00", "2026-01-01T00:00:00+00:00"),
]
def main() -> None:
settings = load_settings()
token = (
settings.cerbero_mainnet_token.get_secret_value()
if settings.cerbero_mainnet_token
else settings.cerbero_testnet_token.get_secret_value()
)
cerbero = CerberoClient(
base_url=settings.cerbero_base_url,
token=token,
bot_tag=settings.cerbero_bot_tag,
)
loader = CerberoOHLCVLoader(client=cerbero, cache_dir=settings.series_dir)
engine = BacktestEngine(fees_bp=5.0)
strategies_dir = Path("/app/strategies")
for label, fname, symbol, timeframe in STRATEGIES:
path = strategies_dir / fname
strat = parse_strategy(path.read_text())
# Carica intero range una volta sola
ohlcv = loader.load(OHLCVRequest(
symbol=symbol, timeframe=timeframe,
start=datetime.fromisoformat("2018-09-01T00:00:00+00:00"),
end=datetime.fromisoformat("2026-01-01T00:00:00+00:00"),
))
print(f"\n{'=' * 110}")
print(f">>> {label}")
print(f" symbol={symbol} timeframe={timeframe} | {len(ohlcv)} bars total")
print(f" {'year':<6} {'bars':>6} {'trades':>7} {'wins':>5} {'losses':>7} {'win%':>6} {'avg_w':>10} {'avg_l':>10} {'ret':>8} {'maxDD':>7} {'sharpe':>7}")
sum_ret = 0.0
sum_trades = 0
sum_wins = 0
for year_label, start, end in YEARS:
mask = (ohlcv.index >= datetime.fromisoformat(start)) & (ohlcv.index < datetime.fromisoformat(end))
slice_df = ohlcv[mask]
if len(slice_df) == 0:
continue
try:
signal_fn = compile_strategy(strat)
signals = signal_fn(slice_df)
bt = engine.run(slice_df, signals)
except Exception as e:
print(f" {year_label:<6} ERROR: {e}")
continue
trades = bt.trades
n = len(trades)
wins = [t.net_pnl for t in trades if t.net_pnl > 0]
losses = [t.net_pnl for t in trades if t.net_pnl <= 0]
nw, nl = len(wins), len(losses)
wr = (nw / n * 100) if n else 0.0
aw = (sum(wins) / nw) if nw else 0.0
al = (sum(losses) / nl) if nl else 0.0
if n > 0:
notional = float(slice_df["close"].iloc[0])
eq = (bt.equity_curve / notional) + 1.0
ret = total_return(eq)
dd = max_drawdown(eq)
sr = sharpe_ratio(bt.returns, periods_per_year=8760)
else:
ret = dd = sr = 0.0
print(f" {year_label:<6} {len(slice_df):>6} {n:>7} {nw:>5} {nl:>7} {wr:>5.1f}% {aw:>10.1f} {al:>10.1f} {ret:>7.2%} {dd:>6.2%} {sr:>7.3f}")
sum_ret += ret
sum_trades += n
sum_wins += nw
overall_wr = (sum_wins / sum_trades * 100) if sum_trades else 0.0
print(f" {'='*5} TOTALS 7y: {sum_trades:>7} {sum_wins:>5} {sum_trades-sum_wins:>7} {overall_wr:>5.1f}% cum_ret={sum_ret:+.2%}")
if __name__ == "__main__":
main()
@@ -1,8 +1,8 @@
# Decisione: indicatore `atr_pct` per fix bug protocollo unità # Decisione: indicatori `*_pct` per fix bug protocollo unità
**Data:** 2026-05-15 **Data:** 2026-05-15
**Status:** Implementato **Status:** Implementato (atr_pct + sma_pct + macd_pct + prompt LLM aggiornato)
**Scope:** `multi_swarm_core.protocol.{compiler,grammar,validator}` + `strategy_crypto/strategies/eth_*.json` **Scope:** `multi_swarm_core.protocol.{compiler,grammar,validator}` + `agents/hypothesis.py` + `strategy_crypto/strategies/eth_*.json`
## Contesto ## Contesto
@@ -73,12 +73,25 @@ _ind_atr_pct(df, 14).mean() # ~0.011 (1.1% del prezzo)
`atr > 0.02` → 500/500 (broken). `atr_pct > 0.02` → 0/500 (real signal). `atr > 0.02` → 500/500 (broken). `atr_pct > 0.02` → 0/500 (real signal).
## Estensione: sma_pct + macd_pct (2026-05-15)
Aggiunti anche `sma_pct` e `macd_pct` per coerenza famiglia `*_pct`:
- **`sma_pct(length) = (close - sma) / sma`** — deviazione frazionale del
close dalla SMA. Range tipico ±0.1. NB: NON è `sma/close` (che sarebbe
sempre ~1.0, inutile per literal). Uso: `sma_pct(50) > 0.05` significa
"close 5% sopra la media a 50 barre" (mean reversion).
- **`macd_pct(fast, slow, signal) = macd / close`** — MACD come frazione
del prezzo. Range tipico ±0.02. Uso: `macd_pct > 0.005` significa
"momentum > 0.5% del prezzo".
Prompt LLM aggiornato di conseguenza (`agents/hypothesis.py`):
- Lista indicatori include `sma_pct` e `macd_pct` con annotazioni unità
- Pattern guidance: "Mean reversion strutturale: sma_pct(long) > 0.05",
"Momentum positivo conferma: macd_pct(12,26,9) > 0.005"
- 3 nuovi test (sma_pct identity, macd_pct identity, validator integration)
## Open items ## Open items
- ~~Aggiornare il system prompt LLM~~ ✅ **chiuso 2026-05-15**: - ~~Aggiornare il system prompt LLM~~ ✅ chiuso 2026-05-15
`agents/hypothesis.py` ora elenca `atr_pct` con annotazione unità e - ~~`sma_pct`, `macd_pct` se emergono usi analoghi~~ ✅ chiuso 2026-05-15
include una sezione "UNITÀ — REGOLA CRITICA" che spiega esplicitamente
al modello quando usare `atr_pct` (literal frazionali) vs `atr`
(confronti relativi). `mutation_prompt_llm._VALID_KEYWORDS` esteso.
- Considerare l'aggiunta di `sma_pct`, `macd_pct` se emergono usi
analoghi in future strategie (still open).
@@ -172,10 +172,11 @@ class AdversarialAgent:
) )
) )
# Fees-eat-alpha: gross_pnl > 0 ma fees > 50% del lordo. # Fees-eat-alpha: gross_pnl > 0 ma fees > soglia del lordo.
# La strategia ha edge teorico ma il margine viene mangiato dai # La strategia ha edge teorico ma il margine viene mangiato dai
# costi di transazione: non sostenibile in produzione. # costi di transazione: non sostenibile in produzione.
# Se gross_pnl <= 0 il check non si applica (gia' perdente). # Se gross_pnl <= 0 il check non si applica (la condizione e' coperta
# da ``negative_net_pnl`` sotto).
gross_pnl = sum(t.gross_pnl for t in result.trades) gross_pnl = sum(t.gross_pnl for t in result.trades)
total_fees = sum(t.fees for t in result.trades) total_fees = sum(t.fees for t in result.trades)
if gross_pnl > 0 and total_fees / gross_pnl > self._fees_eat_alpha_threshold: if gross_pnl > 0 and total_fees / gross_pnl > self._fees_eat_alpha_threshold:
@@ -190,4 +191,22 @@ class AdversarialAgent:
) )
) )
# Negative-net-pnl: somma di ``trade.net_pnl`` < 0 sul training.
# Cattura sia il caso "gross negativo" (no edge direzionale) sia il
# caso "gross positivo ma fees superiori a gross" (edge insufficiente).
# Sintesi del net-after-fees su finestra continua: deal-breaker, non
# negoziabile via soft penalty.
net_pnl = gross_pnl - total_fees
if net_pnl < 0:
report.findings.append(
Finding(
name="negative_net_pnl",
severity=Severity.HIGH,
detail=(
f"Net PnL ${net_pnl:.2f} < 0 after fees over {n_bars} bars; "
f"gross ${gross_pnl:.2f}, fees ${total_fees:.2f}"
),
)
)
return report return report
@@ -2,10 +2,12 @@ from __future__ import annotations
import re import re
from dataclasses import dataclass, field from dataclasses import dataclass, field
from typing import Any
import openai import openai
from ..genome.hypothesis import HypothesisAgentGenome from ..genome.hypothesis import HypothesisAgentGenome
from ..genome.prompt_library import PromptLibrary
from ..llm.client import CompletionResult, EmptyCompletionError, LLMClient from ..llm.client import CompletionResult, EmptyCompletionError, LLMClient
from ..protocol.parser import ParseError, Strategy, parse_strategy from ..protocol.parser import ParseError, Strategy, parse_strategy
from ..protocol.validator import ValidationError, validate_strategy from ..protocol.validator import ValidationError, validate_strategy
@@ -21,6 +23,19 @@ class MarketSummary:
skew: float skew: float
kurtosis: float kurtosis: float
volatility_regime: str volatility_regime: str
autocorr_lag1_recent: float = 0.0 # AR(1) ultimi 500 bar
autocorr_lag1_baseline: float = 0.0 # AR(1) full sample (riferimento)
hurst_recent: float = 0.5 # Hurst ultimi 500 bar
vol_percentile: float = 50.0 # 0-100 percentile della vol corrente
seasonality_hour: float = 0.0 # 0-1 varianza spiegata da hour
seasonality_dow: float = 0.0 # 0-1 varianza spiegata da dow
efficiency_ratio: float = 0.0 # Kaufman, 0-1
tail_index_left: float = 5.0 # Hill left tail
tail_index_right: float = 5.0 # Hill right tail
structural_uptrend: float = 0.5 # HH/HL score 0-1
compression: float = 1.0 # range recent / range ref
spectral_entropy: float = 1.0 # 0-1, Shannon FFT normalizzata
dominant_cycle: float | None = None # periodo barre, None se spectrum piatto
@dataclass(frozen=True) @dataclass(frozen=True)
@@ -41,12 +56,9 @@ class HypothesisProposal:
n_attempts: int = 1 n_attempts: int = 1
SYSTEM_TEMPLATE = """\ # === CORE SCAFFOLD constants (universal, legato al protocol/compiler) ===
Sei un agente generatore di ipotesi di trading quantitativo per un sistema swarm.
Il tuo stile cognitivo: {cognitive_style}
Direttiva personale: {system_prompt}
_SYSTEM_GRAMMAR_SPEC = """\
Devi proporre una strategia di trading espressa in JSON STRETTO. Devi proporre una strategia di trading espressa in JSON STRETTO.
La risposta deve essere un singolo oggetto JSON dentro fence ```json...``` La risposta deve essere un singolo oggetto JSON dentro fence ```json...```
con questa shape: con questa shape:
@@ -77,20 +89,29 @@ Crossover (eventi su 2 serie):
Leaf - indicatori (calcolati su close): Leaf - indicatori (calcolati su close):
{{"kind": "indicator", "name": "sma", "params": [<length>]}} // media mobile, UNITÀ PREZZO {{"kind": "indicator", "name": "sma", "params": [<length>]}} // media mobile, UNITÀ PREZZO
{{"kind": "indicator", "name": "sma_pct", "params": [<length>]}} // (close-sma)/sma, FRAZIONE ±0.1
{{"kind": "indicator", "name": "rsi", "params": [<length>]}} // 0-100, adimensionale {{"kind": "indicator", "name": "rsi", "params": [<length>]}} // 0-100, adimensionale
{{"kind": "indicator", "name": "atr", "params": [<length>]}} // true range, UNITÀ PREZZO {{"kind": "indicator", "name": "atr", "params": [<length>]}} // true range, UNITÀ PREZZO
{{"kind": "indicator", "name": "atr_pct", "params": [<length>]}} // atr/close, FRAZIONE 0.0-0.1 {{"kind": "indicator", "name": "atr_pct", "params": [<length>]}} // atr/close, FRAZIONE 0.0-0.1
{{"kind": "indicator", "name": "realized_vol", "params": [<window>]}} // std dei returns, FRAZIONE {{"kind": "indicator", "name": "realized_vol", "params": [<window>]}} // std dei returns, FRAZIONE
{{"kind": "indicator", "name": "macd", "params": [<fast>, <slow>, <signal>]}} {{"kind": "indicator", "name": "macd", "params": [<fast>, <slow>, <signal>]}} // UNITÀ PREZZO
// 0-3 numeri (tutti opzionali con default 12, 26, 9) {{"kind": "indicator", "name": "macd_pct", "params": [<fast>, <slow>, <signal>]}} // macd/close, FRAZIONE ±0.02
// params: 0-3 numeri (tutti opzionali, default 12, 26, 9)
UNITÀ — REGOLA CRITICA per i confronti con literal numerici: UNITÀ — REGOLA CRITICA per i confronti con literal numerici:
* Confronti con literal FRAZIONALI (0.01, 0.02, 0.05): usa `atr_pct`, `realized_vol` * Confronti con literal FRAZIONALI (0.01, 0.02, 0.05): usa le varianti _pct
Esempio CORRETTO: `atr_pct(14) > 0.02` significa "ATR > 2% del prezzo" Esempi CORRETTI:
Esempio ERRATO: `atr(14) > 0.02` è sempre TRUE su asset $>1 (atr in dollari) `atr_pct(14) > 0.02` "ATR > 2% del prezzo" (volatilità alta)
* Confronti RELATIVI fra indicatori in stessa unità: usa `atr`, `sma`, `macd` `sma_pct(50) > 0.05` "close 5% sopra SMA(50)" (deviazione media)
Esempio: `atr(14) > sma(14)` (entrambi in dollari, confronto valido) `macd_pct(12,26,9) > 0.005` "momentum > 0.5% del prezzo"
* RSI usa literal 0-100 (mai frazione): `rsi(14) > 70` Esempi ERRATI (sempre TRUE/FALSE su crypto, dead branch):
`atr(14) > 0.02` atr in dollari (~30 su ETH) >> 0.02
`sma(50) > 0.02` sma in dollari (~3000) >> 0.02
`macd > 0.02` macd in dollari, ordine ±10
* Confronti RELATIVI fra indicatori in stessa unità: usa nomi senza _pct
Esempi: `atr(14) > sma(14)` (entrambi $), `sma(50) > sma(200)` (golden cross)
`close > sma(50)` (entrambi $) — preferito su `sma_pct(50) > 0` (equivalente)
* RSI usa literal 0-100 (mai frazione): `rsi(14) > 70`, `rsi(14) < 30`
Leaf - feature OHLCV: Leaf - feature OHLCV:
{{"kind": "feature", "name": "open|high|low|close|volume"}} {{"kind": "feature", "name": "open|high|low|close|volume"}}
@@ -112,34 +133,18 @@ Esempi di gating temporale:
{{"op": "eq", "args": [{{"kind": "feature", "name": "is_weekend"}}, {{"kind": "literal", "value": 1}}]}} {{"op": "eq", "args": [{{"kind": "feature", "name": "is_weekend"}}, {{"kind": "literal", "value": 1}}]}}
Leaf - letterale numerico: Leaf - letterale numerico:
{{"kind": "literal", "value": 70.0}} {{"kind": "literal", "value": 70.0}}"""
PATTERN GUIDANCE (oltre agli indicatori, considera forma delle curve e ripetibilità):
Forme di curva:
- Trend ascendente: SMA(short) > SMA(long) E close > SMA(short)
- Trend discendente: SMA(short) < SMA(long) E close < SMA(short)
- Compressione di volatilità (pre-breakout): atr_pct(N) < 0.01 (sotto 1% del prezzo)
- Espansione di volatilità: atr_pct(N) > 0.03 (sopra 3%) OPPURE ATR(N) > ATR(N*2) confronto relativo
- Mean reversion strutturale: |close - SMA(long)| eccessivo → reversal atteso
Ripetibilità dell'andamento:
- Eventi crossover/crossunder ricorrenti (golden/death cross, RSI cross zone)
- Pattern intra-day: usa 'hour' per sfruttare orari di forte volatilità ricorrente
- Pattern settimanali: usa 'dow' o 'is_weekend' per cicli mercato
- Doppio top approx: RSI > 70 + crossunder RSI 70 (1° picco), poi entro N bar
nuovo crossover RSI 70 a livello close simile → entry short
- Range breakout: close > SMA(N) con SMA(short) > SMA(long) (compressione + spinta)
Cerca pattern che si REPLICANO nei dati storici, non singoli eventi rari.
_SYSTEM_CONSTRAINTS = """\
VINCOLI VINCOLI
- Gli indicator NON sono annidabili: 'params' accetta solo numeri, mai altri nodi. - Gli indicator NON sono annidabili: 'params' accetta solo numeri, mai altri nodi.
- Le regole sono valutate in ordine; la prima che matcha vince per ogni timestamp. - Le regole sono valutate in ordine; la prima che matcha vince per ogni timestamp.
- Default action se nessuna regola matcha = flat. - Default action se nessuna regola matcha = flat.
- 'op' e 'kind' sono mutuamente esclusivi sullo stesso nodo. - 'op' e 'kind' sono mutuamente esclusivi sullo stesso nodo.
Rispondi SOLO con il fence ```json...``` contenente l'oggetto strategy. Rispondi SOLO con il fence ```json...``` contenente l'oggetto strategy."""
_SYSTEM_EXAMPLE = """\
Esempio: Esempio:
```json ```json
@@ -161,8 +166,49 @@ Esempio:
}} }}
] ]
}} }}
``` ```"""
"""
def _build_system_prompt(lib: PromptLibrary, genome: HypothesisAgentGenome) -> str:
"""Compone il SYSTEM prompt da scaffold core + contenuto strategy-specific."""
parts: list[str] = []
# 1. Header strategy-specific (da prompts.json)
parts.append(lib.agent_role)
parts.append("")
# 2. Cognitive style + directive (sempre, dal genome)
parts.append(f"Il tuo stile cognitivo: {genome.cognitive_style}")
parts.append(f"Direttiva personale: {genome.system_prompt}")
parts.append("")
# 3. Grammar spec (core scaffold)
parts.append(_SYSTEM_GRAMMAR_SPEC)
# 4. Pattern guidance (da prompts.json, opzionale)
if lib.pattern_guidance:
parts.append(
"\nPATTERN GUIDANCE (oltre agli indicatori, considera forma delle curve e ripetibilità):\n"
)
parts.append(lib.pattern_guidance)
parts.append(
"\n Cerca pattern che si REPLICANO nei dati storici, non singoli eventi rari.\n"
)
# 5. Domain warnings (da prompts.json, opzionale)
if lib.domain_warnings:
parts.append("\nWARNING DI DOMINIO:\n")
parts.append(lib.domain_warnings)
parts.append("")
# 6. Vincoli (core scaffold)
parts.append(_SYSTEM_CONSTRAINTS)
# 7. NEW v3.1: anti-pattern e output priorities (da prompts.json, opzionali)
if lib.anti_patterns:
parts.append("\nANTI-PATTERN DA EVITARE:\n")
parts.append(lib.anti_patterns)
parts.append("")
if lib.output_priorities:
parts.append("\nPRIORITA' DI OUTPUT (trade-off):\n")
parts.append(lib.output_priorities)
parts.append("")
# 8. Esempio (core scaffold)
parts.append(_SYSTEM_EXAMPLE)
return "\n".join(parts)
USER_TEMPLATE = """\ USER_TEMPLATE = """\
@@ -171,13 +217,72 @@ Statistiche return: mean={return_mean:.5f}, std={return_std:.5f}, \
skew={skew:.3f}, kurt={kurtosis:.3f}. skew={skew:.3f}, kurt={kurtosis:.3f}.
Regime volatilità: {volatility_regime}. Regime volatilità: {volatility_regime}.
Regime recente (ultime 500 barre):
autocorr_lag1: {autocorr_lag1_recent:.3f} (baseline storica: {autocorr_lag1_baseline:.3f})
hurst: {hurst_recent:.3f} (0.5 = random walk, >0.55 trending, <0.45 mean rev)
vol_pct: {vol_percentile:.0f}° percentile storico
stagionalita: hour={seasonality_hour:.2f}, dow={seasonality_dow:.2f} (0-1, varianza spiegata)
Geometria & frattali:
efficiency_ratio: {efficiency_ratio:.3f} (Kaufman, 0=noise, 1=trend efficiente)
tail_index: left={tail_index_left:.2f}, right={tail_index_right:.2f} (Hill; <2 fat tail, >5 light)
structural_uptrend: {structural_uptrend:.2f} (HH/HL score, 0.5=range)
compression: {compression:.2f} (range recent / ref; <1 compressione, >1 espansione)
spectral_entropy: {spectral_entropy:.2f} (0=struttura, 1=rumore bianco)
dominant_cycle: {dominant_cycle_str} (None se spettro piatto)
Feature accessibili dal tuo genoma: {feature_access}. Feature accessibili dal tuo genoma: {feature_access}.
Lookback massimo che puoi usare nel ragionamento: {lookback_window} barre. Lookback massimo che puoi usare nel ragionamento: {lookback_window} barre.
Genera una strategia che cerchi anomalie sfruttabili in questo regime. {instruction}
""" """
def _render_focus_block(keys: list[str], market: MarketSummary) -> str:
"""Renderizza 'Focus per la tua lente' come riga del USER_TEMPLATE.
Mappa nomi simbolici a valori della MarketSummary. Skippa silenziosamente
chiavi sconosciute (fault-tolerant per evoluzione futura).
"""
field_map: dict[str, Any] = {
# Statistiche base
"mean": market.return_mean,
"std": market.return_std,
"skew": market.skew,
"kurt": market.kurtosis,
"vol_regime": market.volatility_regime,
# Regime recente
"autocorr_recent": market.autocorr_lag1_recent,
"autocorr_baseline": market.autocorr_lag1_baseline,
"hurst": market.hurst_recent,
"vol_pct": market.vol_percentile,
"seasonality_hour": market.seasonality_hour,
"seasonality_dow": market.seasonality_dow,
# Geometria/frattali
"efficiency_ratio": market.efficiency_ratio,
"tail_left": market.tail_index_left,
"tail_right": market.tail_index_right,
"structural_uptrend": market.structural_uptrend,
"compression": market.compression,
"spectral_entropy": market.spectral_entropy,
"dominant_cycle": market.dominant_cycle,
}
parts: list[str] = []
for k in keys:
if k not in field_map:
continue
v = field_map[k]
if v is None:
parts.append(f"{k}=N/A")
elif isinstance(v, float):
parts.append(f"{k}={v:.3f}")
else:
parts.append(f"{k}={v}")
if not parts:
return ""
return "\nFocus per la tua lente: " + ", ".join(parts) + "\n"
_RETRY_TEMPLATE = """\ _RETRY_TEMPLATE = """\
{original_user} {original_user}
@@ -260,20 +365,34 @@ def _try_parse(text: str) -> tuple[Strategy | None, str | None]:
class HypothesisAgent: class HypothesisAgent:
def __init__(self, llm: LLMClient, max_retries: int = 1): def __init__(
self,
llm: LLMClient,
max_retries: int = 1,
prompt_library: PromptLibrary | None = None,
):
if max_retries < 0: if max_retries < 0:
raise ValueError("max_retries must be >= 0") raise ValueError("max_retries must be >= 0")
self._llm = llm self._llm = llm
self._max_retries = max_retries self._max_retries = max_retries
self._prompt_library = prompt_library or PromptLibrary.default()
def propose( def propose(
self, self,
genome: HypothesisAgentGenome, genome: HypothesisAgentGenome,
market: MarketSummary, market: MarketSummary,
) -> HypothesisProposal: ) -> HypothesisProposal:
system = SYSTEM_TEMPLATE.format( system = _build_system_prompt(self._prompt_library, genome)
cognitive_style=genome.cognitive_style, dominant_cycle_str = (
system_prompt=genome.system_prompt, f"{market.dominant_cycle:.0f} barre"
if market.dominant_cycle is not None
else "N/A (spettro piatto)"
)
focus_keys = self._prompt_library.focus_metrics_for(genome.cognitive_style)
focus_block = _render_focus_block(focus_keys, market) if focus_keys else ""
instruction = (
self._prompt_library.instruction
or "Genera una strategia che cerchi anomalie sfruttabili in questo regime."
) )
original_user = USER_TEMPLATE.format( original_user = USER_TEMPLATE.format(
symbol=market.symbol, symbol=market.symbol,
@@ -284,9 +403,23 @@ class HypothesisAgent:
skew=market.skew, skew=market.skew,
kurtosis=market.kurtosis, kurtosis=market.kurtosis,
volatility_regime=market.volatility_regime, volatility_regime=market.volatility_regime,
autocorr_lag1_recent=market.autocorr_lag1_recent,
autocorr_lag1_baseline=market.autocorr_lag1_baseline,
hurst_recent=market.hurst_recent,
vol_percentile=market.vol_percentile,
seasonality_hour=market.seasonality_hour,
seasonality_dow=market.seasonality_dow,
efficiency_ratio=market.efficiency_ratio,
tail_index_left=market.tail_index_left,
tail_index_right=market.tail_index_right,
structural_uptrend=market.structural_uptrend,
compression=market.compression,
spectral_entropy=market.spectral_entropy,
dominant_cycle_str=dominant_cycle_str,
feature_access=", ".join(genome.feature_access), feature_access=", ".join(genome.feature_access),
lookback_window=genome.lookback_window, lookback_window=genome.lookback_window,
) instruction=instruction,
) + focus_block
completions: list[CompletionResult] = [] completions: list[CompletionResult] = []
errors: list[str] = [] errors: list[str] = []
@@ -3,6 +3,17 @@ from __future__ import annotations
import pandas as pd # type: ignore[import-untyped] import pandas as pd # type: ignore[import-untyped]
from scipy import stats # type: ignore[import-untyped] from scipy import stats # type: ignore[import-untyped]
from ..metrics.basic import (
autocorr_lag1,
compression_ratio,
efficiency_ratio_kaufman,
hurst_exponent,
seasonality_strength,
spectral_entropy_and_cycle,
structural_uptrend_score,
tail_index_hill,
vol_percentile_historical,
)
from .hypothesis import MarketSummary from .hypothesis import MarketSummary
@@ -24,6 +35,23 @@ def build_market_summary(
else: else:
regime = "high" regime = "high"
recent_window = min(500, len(returns))
recent_returns = returns.iloc[-recent_window:]
autocorr_recent = autocorr_lag1(recent_returns)
autocorr_baseline = autocorr_lag1(returns)
hurst_r = hurst_exponent(recent_returns)
vol_pct = vol_percentile_historical(returns, current_window=24, ref_window=2000)
season_h = seasonality_strength(returns, by="hour")
season_d = seasonality_strength(returns, by="dow")
eff_ratio = efficiency_ratio_kaufman(ohlcv["close"], length=100)
tail_l = tail_index_hill(returns, side="left")
tail_r = tail_index_hill(returns, side="right")
hh_hl = structural_uptrend_score(ohlcv["close"], window=5)
compress = compression_ratio(ohlcv["close"], recent_window=50, ref_window=200)
spec_entropy, dom_cycle = spectral_entropy_and_cycle(returns, length=256)
return MarketSummary( return MarketSummary(
symbol=symbol, symbol=symbol,
timeframe=timeframe, timeframe=timeframe,
@@ -33,4 +61,17 @@ def build_market_summary(
skew=skew, skew=skew,
kurtosis=kurt, kurtosis=kurt,
volatility_regime=regime, volatility_regime=regime,
autocorr_lag1_recent=autocorr_recent,
autocorr_lag1_baseline=autocorr_baseline,
hurst_recent=hurst_r,
vol_percentile=vol_pct,
seasonality_hour=season_h,
seasonality_dow=season_d,
efficiency_ratio=eff_ratio,
tail_index_left=tail_l,
tail_index_right=tail_r,
structural_uptrend=hh_hl,
compression=compress,
spectral_entropy=spec_entropy,
dominant_cycle=dom_cycle,
) )
@@ -2,6 +2,7 @@ from __future__ import annotations
from dataclasses import dataclass from dataclasses import dataclass
import numpy as np
import pandas as pd # type: ignore[import-untyped] import pandas as pd # type: ignore[import-untyped]
from .orders import Position, Side, Trade from .orders import Position, Side, Trade
@@ -28,74 +29,110 @@ class BacktestEngine:
self.fees_bp = fees_bp self.fees_bp = fees_bp
def run(self, ohlcv: pd.DataFrame, signals: pd.Series) -> BacktestResult: def run(self, ohlcv: pd.DataFrame, signals: pd.Series) -> BacktestResult:
n = len(ohlcv)
if n == 0:
empty = pd.Series([], dtype=float)
return BacktestResult(equity_curve=empty, returns=empty, trades=[])
signals = signals.reindex(ohlcv.index).ffill().fillna(Side.FLAT) signals = signals.reindex(ohlcv.index).ffill().fillna(Side.FLAT)
# Esecuzione con delay 1: segnale a t-1 esegue a open di t. # Esecuzione con delay 1: segnale a t-1 esegue a open di t.
shifted = [Side.FLAT, *list(signals.iloc[:-1])] executed = pd.Series(
executed_side = pd.Series(shifted, index=ohlcv.index, dtype=object) [Side.FLAT, *list(signals.iloc[:-1])],
index=ohlcv.index,
dtype=object,
)
# Codifica side in int per vectorizzazione: 0=FLAT, +1=LONG, -1=SHORT.
side_code = np.where(
executed.values == Side.LONG, 1,
np.where(executed.values == Side.SHORT, -1, 0),
).astype(np.int8)
opens = ohlcv["open"].to_numpy(dtype=np.float64)
closes = ohlcv["close"].to_numpy(dtype=np.float64)
ts_index = ohlcv.index
# Identifica transizioni: punto in cui side[i] != side[i-1] (con side[-1]=0).
prev = np.concatenate(([0], side_code[:-1]))
change = side_code != prev
# Indici di entry (cambio verso side != 0).
entry_idxs = np.flatnonzero(change & (side_code != 0))
# Indici di chiusura: per ogni entry, il prossimo indice dove side[i] != side_entry.
# Vectorized: per ogni entry_idx, cerca change & side != side_entry oltre l'entry.
position: Position | None = None
position_entry_ts: pd.Timestamp | None = None
trades: list[Trade] = [] trades: list[Trade] = []
equity = 0.0 # realized_pnl[t]: PnL netto cumulato dopo le chiusure avvenute a OPEN di t.
equity_history: list[float] = [] realized_pnl = np.zeros(n, dtype=np.float64)
returns_history: list[float] = [] fees_rate = self.fees_bp / 10000.0
prev_equity = 0.0 size = 1.0
for ts, row in ohlcv.iterrows(): # Posizione corrente all'inizio di ogni indice t (prima di applicare il transitorio):
target_side = executed_side.loc[ts] # used per MtM computation. open_side_at_t / open_entry_at_t.
current_side = position.side if position else Side.FLAT open_side = np.zeros(n, dtype=np.int8)
open_entry = np.zeros(n, dtype=np.float64)
if target_side != current_side: for entry_idx in entry_idxs:
if position is not None: entry_side = int(side_code[entry_idx])
assert position_entry_ts is not None entry_price = opens[entry_idx]
trade = Trade( # Cerca exit: primo indice > entry_idx dove side differisce.
entry_ts=position_entry_ts, after = side_code[entry_idx + 1:]
exit_ts=ts, rel = np.flatnonzero(after != entry_side)
side=position.side, if rel.size > 0:
size=position.size, exit_idx = entry_idx + 1 + int(rel[0])
entry_price=position.entry_price, exit_price = opens[exit_idx]
exit_price=float(row["open"]), exit_ts = ts_index[exit_idx]
fees_bp=self.fees_bp, gross = entry_side * (exit_price - entry_price) * size
) fees = fees_rate * size * (entry_price + exit_price)
trades.append(trade) net = gross - fees
equity += trade.net_pnl # La chiusura avviene a open[exit_idx]: dal bar exit_idx in poi il
position = None # PnL e' realizzato (non piu' MtM).
position_entry_ts = None realized_pnl[exit_idx:] += net
if target_side in (Side.LONG, Side.SHORT): # Posizione aperta in [entry_idx, exit_idx-1].
position = Position( open_side[entry_idx:exit_idx] = entry_side
side=target_side, entry_price=float(row["open"]), size=1.0 open_entry[entry_idx:exit_idx] = entry_price
) trades.append(Trade(
position_entry_ts = ts entry_ts=ts_index[entry_idx],
exit_ts=exit_ts,
side=Side.LONG if entry_side == 1 else Side.SHORT,
size=size,
entry_price=entry_price,
exit_price=exit_price,
fees_bp=self.fees_bp,
))
else:
# Ultima posizione ancora aperta: chiusura forced a close[-1].
# Parita' col loop legacy: MtM su [entry_idx, n-1), realized totale
# SOLO al bar n-1 (legacy fa equity_history[-1] = equity).
last_close = closes[-1]
gross = entry_side * (last_close - entry_price) * size
fees = fees_rate * size * (entry_price + last_close)
net = gross - fees
if entry_idx < n - 1:
open_side[entry_idx:n - 1] = entry_side
open_entry[entry_idx:n - 1] = entry_price
realized_pnl[-1] += net
trades.append(Trade(
entry_ts=ts_index[entry_idx],
exit_ts=ts_index[-1],
side=Side.LONG if entry_side == 1 else Side.SHORT,
size=size,
entry_price=entry_price,
exit_price=last_close,
fees_bp=self.fees_bp,
))
mark = float(row["close"]) # MtM unrealized per ogni bar in cui c'e' una posizione aperta.
mtm = position.unrealized_pnl(mark) if position else 0.0 mtm = open_side.astype(np.float64) * (closes - open_entry) * size
current_equity = equity + mtm equity_arr = realized_pnl + mtm
equity_history.append(current_equity) # Returns = first diff dell'equity (col loop legacy il primo bar e' equity[0]-0).
returns_history.append(current_equity - prev_equity) returns_arr = np.concatenate(([equity_arr[0]], np.diff(equity_arr)))
prev_equity = current_equity
if position is not None:
assert position_entry_ts is not None
last_ts = ohlcv.index[-1]
last_close = float(ohlcv["close"].iloc[-1])
trade = Trade(
entry_ts=position_entry_ts,
exit_ts=last_ts,
side=position.side,
size=position.size,
entry_price=position.entry_price,
exit_price=last_close,
fees_bp=self.fees_bp,
)
trades.append(trade)
equity += trade.net_pnl
equity_history[-1] = equity
if len(returns_history) >= 2:
returns_history[-1] = equity - equity_history[-2]
return BacktestResult( return BacktestResult(
equity_curve=pd.Series(equity_history, index=ohlcv.index, name="equity"), equity_curve=pd.Series(equity_arr, index=ts_index, name="equity"),
returns=pd.Series(returns_history, index=ohlcv.index, name="returns"), returns=pd.Series(returns_arr, index=ts_index, name="returns"),
trades=trades, trades=trades,
) )
# Lo facade Position re-export e' tenuto per backward-compat con import legacy.
__all__ = ["BacktestEngine", "BacktestResult", "Position", "Side", "Signal", "Trade"]
@@ -0,0 +1,68 @@
"""GA data access functions for the core dashboard.
Reads exclusively from runs.db (GA tables).
"""
from __future__ import annotations
import json
from pathlib import Path
from typing import Any
import pandas as pd # type: ignore[import-untyped]
from multi_swarm_core.persistence.repository import Repository
__all__ = [
"get_repo",
"list_runs_df",
"get_run_overview",
"generations_df",
"evaluations_df",
"genomes_df",
]
def get_repo(db_path: str | Path) -> Repository:
return Repository(db_path=db_path)
def list_runs_df(repo: Repository) -> pd.DataFrame:
return pd.DataFrame(repo.list_runs())
def get_run_overview(repo: Repository, run_id: str) -> dict[str, Any]:
run = repo.get_run(run_id)
return {
"name": run["name"],
"started_at": run["started_at"],
"completed_at": run["completed_at"],
"status": run["status"],
"total_cost_usd": run["total_cost_usd"],
"config": json.loads(run["config_json"]),
}
def generations_df(repo: Repository, run_id: str) -> pd.DataFrame:
return pd.DataFrame(repo.list_generations(run_id))
def evaluations_df(repo: Repository, run_id: str) -> pd.DataFrame:
return pd.DataFrame(repo.list_evaluations(run_id))
def genomes_df(
repo: Repository, run_id: str, generation_idx: int | None = None
) -> pd.DataFrame:
rows = repo.list_genomes(run_id, generation_idx)
flat: list[dict[str, Any]] = []
for r in rows:
payload = json.loads(r["payload_json"])
flat.append(
{
"id": r["id"],
"generation_idx": r["generation_idx"],
**payload,
}
)
return pd.DataFrame(flat)
@@ -0,0 +1,560 @@
"""Multi-Swarm Core Dashboard — GA pages: /, /convergence, /genomes.
Avvio: ``uv run python -m multi_swarm_core.dashboard.nicegui_app``
Legge SOLO runs.db (tabelle GA).
"""
from __future__ import annotations
import html
import os
from pathlib import Path
from typing import Any
import plotly.graph_objects as go # type: ignore[import-untyped]
from nicegui import app, ui
from multi_swarm_core.dashboard.data import (
evaluations_df,
generations_df,
genomes_df,
get_repo,
get_run_overview,
list_runs_df,
)
from multi_swarm_core.dashboard.theme import (
COLOR_ACCENT,
COLOR_PRIMARY,
COLOR_SECONDARY,
COLOR_SURFACE,
COLOR_TEXT,
COLOR_TEXT_MUTED,
_STATUS_BADGE,
_apply_theme,
_build_header,
_json_to_html,
)
GA_DB_PATH = os.environ.get("GA_DB_PATH", "./state/runs.db")
DASHBOARD_ROOT_PATH = os.environ.get("DASHBOARD_ROOT_PATH", "")
REFRESH_INTERVAL_S = 3.0
def _runs_options() -> dict[str, str]:
repo = get_repo(GA_DB_PATH)
runs = list_runs_df(repo)
if runs.empty:
return {}
return {
row["id"]: f"{row['name']}{row['status']} ({row['started_at'][:16]})"
for _, row in runs.iterrows()
}
def _snapshot(run_id: str) -> dict[str, Any]:
repo = get_repo(GA_DB_PATH)
ov = get_run_overview(repo, run_id)
evals = evaluations_df(repo, run_id)
gens = generations_df(repo, run_id)
cfg = ov["config"]
pop_size = int(cfg.get("population_size", 0))
n_gens = int(cfg.get("n_generations", 0))
evals_total = max(pop_size * n_gens, 1)
evals_done = len(evals)
gens_done = int(gens["completed_at"].notna().sum()) if not gens.empty else 0
live_cost = float(repo.total_cost(run_id)) if ov["status"] == "running" else float(
ov["total_cost_usd"]
)
top_fit = float(evals["fitness"].max()) if evals_done else float("nan")
median_fit = float(evals["fitness"].median()) if evals_done else float("nan")
parse_success = (
100.0 * float(evals["parse_error"].isna().sum()) / evals_done if evals_done else 0.0
)
return {
"status": ov["status"],
"name": cfg.get("run_name", ""),
"started_at": ov["started_at"],
"completed_at": ov["completed_at"] or "",
"cost_usd": live_cost,
"pop_size": pop_size,
"n_gens": n_gens,
"evals_done": evals_done,
"evals_total": evals_total,
"gens_done": gens_done,
"top_fit": top_fit,
"median_fit": median_fit,
"parse_success": parse_success,
"config": cfg,
"gens_df": gens,
}
def _convergence_figure(gens_df: Any) -> go.Figure:
fig = go.Figure()
if gens_df.empty:
fig.add_annotation(
text="Nessuna generazione registrata", x=0.5, y=0.5, showarrow=False,
font={"color": COLOR_TEXT_MUTED, "size": 14},
)
else:
fig.add_trace(
go.Scatter(
x=gens_df["generation_idx"], y=gens_df["fitness_max"],
name="max", mode="lines+markers",
line={"color": COLOR_PRIMARY, "width": 3, "shape": "spline", "smoothing": 0.6},
marker={"size": 9, "color": COLOR_PRIMARY,
"line": {"color": "#fff", "width": 1}},
fill="tozeroy",
fillcolor="rgba(255, 45, 135, 0.12)",
)
)
fig.add_trace(
go.Scatter(
x=gens_df["generation_idx"], y=gens_df["fitness_p90"],
name="p90", mode="lines+markers",
line={"color": COLOR_ACCENT, "width": 2, "dash": "dot", "shape": "spline"},
marker={"size": 7, "color": COLOR_ACCENT},
)
)
fig.add_trace(
go.Scatter(
x=gens_df["generation_idx"], y=gens_df["fitness_median"],
name="median", mode="lines+markers",
line={"color": COLOR_SECONDARY, "width": 2, "shape": "spline"},
marker={"size": 7, "color": COLOR_SECONDARY},
)
)
fig.update_layout(
template="plotly_dark",
paper_bgcolor=COLOR_SURFACE,
plot_bgcolor=COLOR_SURFACE,
font={"color": COLOR_TEXT},
xaxis={"title": "generation", "gridcolor": "rgba(148, 163, 184, 0.08)", "dtick": 1},
yaxis={"title": "fitness", "gridcolor": "rgba(148, 163, 184, 0.08)"},
title={"text": "Fitness convergence", "font": {"color": COLOR_TEXT, "size": 18}},
legend={"bgcolor": "rgba(19, 19, 26, 0.95)", "bordercolor": COLOR_PRIMARY, "borderwidth": 1},
margin={"l": 50, "r": 30, "t": 50, "b": 50},
)
return fig
def _entropy_figure(gens_df: Any) -> go.Figure:
fig = go.Figure()
if not gens_df.empty:
fig.add_trace(
go.Scatter(
x=gens_df["generation_idx"], y=gens_df["entropy"],
mode="lines+markers",
line={"color": COLOR_SECONDARY, "width": 3, "shape": "spline", "smoothing": 0.6},
marker={"size": 9, "color": COLOR_SECONDARY,
"line": {"color": "#fff", "width": 1}},
fill="tozeroy",
fillcolor="rgba(0, 217, 255, 0.12)",
name="entropy",
)
)
fig.add_hline(
y=0.5, line_dash="dash", line_color=COLOR_ACCENT,
annotation_text="gate threshold (0.5)",
annotation_font_color=COLOR_ACCENT,
)
fig.update_layout(
template="plotly_dark",
paper_bgcolor=COLOR_SURFACE,
plot_bgcolor=COLOR_SURFACE,
font={"color": COLOR_TEXT},
xaxis={"title": "generation", "gridcolor": "rgba(148, 163, 184, 0.08)", "dtick": 1},
yaxis={"title": "entropy", "gridcolor": "rgba(148, 163, 184, 0.08)"},
title={"text": "Diversity (fitness entropy)", "font": {"color": COLOR_TEXT, "size": 18}},
margin={"l": 50, "r": 30, "t": 50, "b": 50},
)
return fig
@ui.page("/")
def index() -> None:
_apply_theme()
_build_header(
active="/",
brand_subtitle="Coevolutivo / GA",
nav_items=[("/", "Overview"), ("/convergence", "Convergence"), ("/genomes", "Genomes")],
db_label=f"{Path(GA_DB_PATH).resolve().name}",
)
options = _runs_options()
if not options:
ui.label("Nessuna run nel database.").classes("text-h5")
return
state: dict[str, Any] = {"run_id": next(iter(options))}
with ui.row().classes("w-full items-center gap-4 q-mb-md"):
select = ui.select(options=options, value=state["run_id"], label="Run").classes(
"flex-grow"
)
status_badge = ui.badge("", color="primary").classes("text-body1 q-pa-sm")
ui.button("🔄 Refresh", on_click=lambda: refresh()).props("outline color=primary")
with ui.card().classes("w-full"):
ui.label("Progresso run").classes("text-subtitle1")
gen_label = ui.label("Generations: 0/0")
gen_bar = ui.linear_progress(0.0, show_value=False).props("size=20px color=primary")
eval_label = ui.label("Evaluations: 0/0 (0.0%)")
eval_bar = ui.linear_progress(0.0, show_value=False).props("size=20px color=accent")
with ui.row().classes("w-full gap-4"):
with ui.card().classes("flex-grow metric-card accent-cyan"):
ui.label("Top fitness").classes("text-caption")
top_lbl = ui.label("").classes("text-h4")
with ui.card().classes("flex-grow metric-card accent-purple"):
ui.label("Median fitness").classes("text-caption")
median_lbl = ui.label("").classes("text-h4")
with ui.card().classes("flex-grow metric-card accent-amber"):
ui.label("Parse success").classes("text-caption")
parse_lbl = ui.label("").classes("text-h4")
with ui.card().classes("flex-grow metric-card accent-green"):
ui.label("Cost (USD)").classes("text-caption")
cost_lbl = ui.label("").classes("text-h4")
with ui.row().classes("w-full gap-4 q-mt-md"):
started_lbl = ui.label("Started: —")
completed_lbl = ui.label("Completed: —")
ui.separator()
ui.label("Config").classes("text-subtitle1")
cfg_code = ui.html('<pre class="config-block"></pre>').classes("w-full")
def refresh() -> None:
run_id = select.value
if not run_id:
return
try:
s = _snapshot(run_id)
except Exception as e: # noqa: BLE001
ui.notify(f"Errore: {e}", type="negative")
return
text, color = _STATUS_BADGE.get(s["status"], (s["status"], "primary"))
status_badge.text = text
status_badge.props(f"color={color}")
gen_frac = min(s["gens_done"] / max(s["n_gens"], 1), 1.0)
eval_frac = min(s["evals_done"] / s["evals_total"], 1.0)
gen_bar.value = gen_frac
eval_bar.value = eval_frac
gen_label.text = f"Generations: {s['gens_done']}/{s['n_gens']}"
eval_label.text = (
f"Evaluations: {s['evals_done']}/{s['evals_total']} ({100 * eval_frac:.1f}%)"
)
top_lbl.text = f"{s['top_fit']:.4f}" if s["evals_done"] else ""
median_lbl.text = f"{s['median_fit']:.4f}" if s["evals_done"] else ""
parse_lbl.text = f"{s['parse_success']:.1f}%" if s["evals_done"] else ""
cost_lbl.text = f"${s['cost_usd']:.4f}"
started_lbl.text = f"Started: {s['started_at']}"
completed_lbl.text = f"Completed: {s['completed_at']}"
cfg_code.content = f'<pre class="config-block">{_json_to_html(s["config"])}</pre>'
def on_select_change() -> None:
state["run_id"] = select.value
refresh()
select.on_value_change(on_select_change)
ui.timer(REFRESH_INTERVAL_S, refresh)
refresh()
@ui.page("/convergence")
def convergence() -> None:
_apply_theme()
_build_header(
active="/convergence",
brand_subtitle="Coevolutivo / GA",
nav_items=[("/", "Overview"), ("/convergence", "Convergence"), ("/genomes", "Genomes")],
db_label=f"{Path(GA_DB_PATH).resolve().name}",
)
options = _runs_options()
if not options:
ui.label("Nessuna run nel database.").classes("text-h5")
return
state: dict[str, Any] = {"run_id": next(iter(options))}
with ui.row().classes("w-full items-center gap-4 q-mb-md"):
select = ui.select(options=options, value=state["run_id"], label="Run").classes(
"flex-grow"
)
gen_count_lbl = ui.label("Gens: 0/0").classes("text-body1").style(
f"color: {COLOR_PRIMARY}; font-weight: 600;"
)
ui.button("🔄 Refresh", on_click=lambda: refresh()).props("outline color=primary")
fitness_plot = ui.plotly(_convergence_figure(generations_df(get_repo(GA_DB_PATH), state["run_id"]))).classes("w-full")
entropy_plot = ui.plotly(_entropy_figure(generations_df(get_repo(GA_DB_PATH), state["run_id"]))).classes("w-full q-mt-md")
ui.separator()
ui.label("Tabella generazioni").classes("text-subtitle1 q-mt-md")
gens_table = ui.table(
columns=[
{"name": "generation_idx", "label": "gen", "field": "generation_idx", "sortable": True},
{"name": "n_genomes", "label": "n", "field": "n_genomes"},
{"name": "fitness_max", "label": "max", "field": "fitness_max"},
{"name": "fitness_p90", "label": "p90", "field": "fitness_p90"},
{"name": "fitness_median", "label": "median", "field": "fitness_median"},
{"name": "entropy", "label": "entropy", "field": "entropy"},
{"name": "completed_at", "label": "completed", "field": "completed_at"},
],
rows=[],
row_key="generation_idx",
).classes("w-full")
def refresh() -> None:
run_id = select.value
if not run_id:
return
try:
gens = generations_df(get_repo(GA_DB_PATH), run_id)
ov = get_run_overview(get_repo(GA_DB_PATH), run_id)
except Exception as e: # noqa: BLE001
ui.notify(f"Errore: {e}", type="negative")
return
n_gens = int(ov["config"].get("n_generations", 0))
gens_done = int(gens["completed_at"].notna().sum()) if not gens.empty else 0
gen_count_lbl.text = f"Gens: {gens_done}/{n_gens}"
fitness_plot.update_figure(_convergence_figure(gens))
entropy_plot.update_figure(_entropy_figure(gens))
if gens.empty:
gens_table.rows = []
else:
display_cols = [
"generation_idx", "n_genomes",
"fitness_max", "fitness_p90", "fitness_median",
"entropy", "completed_at",
]
gens_table.rows = [
{
col: (round(v, 6) if isinstance(v, float) else v)
for col, v in row.items()
if col in display_cols
}
for _, row in gens.iterrows()
]
gens_table.update()
def on_select_change() -> None:
state["run_id"] = select.value
refresh()
select.on_value_change(on_select_change)
ui.timer(REFRESH_INTERVAL_S, refresh)
refresh()
@ui.page("/genomes")
def genomes() -> None:
_apply_theme()
_build_header(
active="/genomes",
brand_subtitle="Coevolutivo / GA",
nav_items=[("/", "Overview"), ("/convergence", "Convergence"), ("/genomes", "Genomes")],
db_label=f"{Path(GA_DB_PATH).resolve().name}",
)
options = _runs_options()
if not options:
ui.label("Nessuna run nel database.").classes("text-h5")
return
state: dict[str, Any] = {
"run_id": next(iter(options)),
"selected_gid": None,
"merged": None,
}
with ui.row().classes("w-full items-center gap-4 q-mb-md"):
select = ui.select(options=options, value=state["run_id"], label="Run").classes(
"flex-grow"
)
top_k_select = ui.select(
options={10: "Top 10", 25: "Top 25", 50: "Top 50"},
value=10,
label="Top K",
)
ui.button("🔄 Refresh", on_click=lambda: refresh()).props("outline color=primary")
ui.label("Top genomi per fitness").classes("text-subtitle1 q-mt-sm")
top_table = ui.table(
columns=[
{"name": "genome_id", "label": "id", "field": "genome_id", "align": "left"},
{"name": "fitness", "label": "fitness", "field": "fitness", "sortable": True},
{"name": "dsr", "label": "DSR", "field": "dsr"},
{"name": "sharpe", "label": "Sharpe", "field": "sharpe"},
{"name": "max_dd", "label": "max DD", "field": "max_dd"},
{"name": "n_trades", "label": "trades", "field": "n_trades"},
{"name": "cognitive_style", "label": "style", "field": "cognitive_style"},
{"name": "temperature", "label": "T", "field": "temperature"},
{"name": "lookback_window", "label": "lookback", "field": "lookback_window"},
],
rows=[],
row_key="genome_id",
selection="single",
).classes("w-full")
ui.separator().classes("q-my-md")
with ui.card().classes("w-full"):
ui.label("Ispezione genoma").classes("text-subtitle1")
detail_hint = ui.label("Seleziona un genoma dalla tabella sopra.").classes(
"text-caption"
).style(f"color: {COLOR_TEXT_MUTED};")
with ui.row().classes("w-full gap-4 q-mt-sm"):
with ui.card().classes("flex-grow metric-card accent-cyan"):
ui.label("fitness").classes("text-caption")
fit_lbl = ui.label("").classes("text-h4")
with ui.card().classes("flex-grow metric-card accent-purple"):
ui.label("DSR").classes("text-caption")
dsr_lbl = ui.label("").classes("text-h4")
with ui.card().classes("flex-grow metric-card accent-amber"):
ui.label("Sharpe").classes("text-caption")
sharpe_lbl = ui.label("").classes("text-h4")
with ui.card().classes("flex-grow metric-card"):
ui.label("max DD").classes("text-caption")
dd_lbl = ui.label("").classes("text-h4")
with ui.card().classes("flex-grow metric-card accent-green"):
ui.label("trades").classes("text-caption")
trades_lbl = ui.label("").classes("text-h4")
with ui.card().classes("flex-grow metric-card"):
ui.label("style").classes("text-caption")
style_lbl = ui.label("").classes("text-h4")
ui.label("System prompt").classes("text-subtitle1 q-mt-md")
prompt_code = ui.html('<pre class="raw-block">—</pre>').classes("w-full")
ui.label("Raw LLM output").classes("text-subtitle1 q-mt-md")
raw_code = ui.html('<pre class="raw-block">—</pre>').classes("w-full")
parse_error_lbl = ui.label("").classes("q-mt-sm").style(
"color: #FF6B6B; font-weight: 600;"
)
def _render_detail(row: dict[str, Any]) -> None:
detail_hint.text = f"Genoma: {row.get('genome_id', '')}"
fit_lbl.text = f"{float(row.get('fitness', 0)):.4f}"
dsr_lbl.text = f"{float(row.get('dsr', 0)):.4f}"
sharpe_lbl.text = f"{float(row.get('sharpe', 0)):.3f}"
dd_lbl.text = f"{float(row.get('max_dd', 0)):.3f}"
trades_lbl.text = str(int(row.get("n_trades", 0)))
style_lbl.text = str(row.get("cognitive_style", ""))
prompt_code.content = (
f'<pre class="raw-block">{html.escape(str(row.get("system_prompt", "")))}</pre>'
)
raw_code.content = (
f'<pre class="raw-block">{html.escape(str(row.get("raw_text", "") or ""))}</pre>'
)
pe = row.get("parse_error")
parse_error_lbl.text = f"❌ Parse error: {pe}" if pe else ""
def refresh() -> None:
run_id = select.value
if not run_id:
return
try:
repo = get_repo(GA_DB_PATH)
evals = evaluations_df(repo, run_id)
gens = genomes_df(repo, run_id)
except Exception as e: # noqa: BLE001
ui.notify(f"Errore: {e}", type="negative")
return
if evals.empty:
top_table.rows = []
top_table.update()
return
merged = evals.merge(
gens, left_on="genome_id", right_on="id", how="left", suffixes=("", "_g")
)
state["merged"] = merged
k = int(top_k_select.value)
top = merged.sort_values("fitness", ascending=False).head(k)
rows = []
for _, r in top.iterrows():
rows.append(
{
"genome_id": str(r.get("genome_id", ""))[:12] + "",
"fitness": round(float(r.get("fitness", 0)), 4),
"dsr": round(float(r.get("dsr", 0)), 4),
"sharpe": round(float(r.get("sharpe", 0)), 3),
"max_dd": round(float(r.get("max_dd", 0)), 3),
"n_trades": int(r.get("n_trades", 0)),
"cognitive_style": str(r.get("cognitive_style", "")),
"temperature": round(float(r.get("temperature", 0)), 2),
"lookback_window": int(r.get("lookback_window", 0)),
"_full_id": str(r.get("genome_id", "")),
}
)
top_table.rows = rows
top_table.update()
sel = state.get("selected_gid")
if sel:
match = merged[merged["genome_id"] == sel]
if not match.empty:
_render_detail(match.iloc[0].to_dict())
def on_row_selected(e: Any) -> None:
rows = (e.args or {}).get("rows") or []
if not rows:
return
full_id = rows[0].get("_full_id")
if not full_id:
return
state["selected_gid"] = full_id
merged = state.get("merged")
if merged is None:
return
match = merged[merged["genome_id"] == full_id]
if not match.empty:
_render_detail(match.iloc[0].to_dict())
def on_select_change() -> None:
state["run_id"] = select.value
state["selected_gid"] = None
refresh()
select.on_value_change(on_select_change)
top_k_select.on_value_change(lambda _: refresh())
top_table.on("selection", on_row_selected)
ui.timer(REFRESH_INTERVAL_S, refresh)
refresh()
def main() -> None:
app.on_startup(
lambda: print(
f"GA DB: {Path(GA_DB_PATH).resolve()} | root_path: {DASHBOARD_ROOT_PATH or '/'}"
)
)
ui.run(
host="0.0.0.0",
port=int(os.environ.get("SWARM_DASHBOARD_PORT", "8080")),
title="Multi-Swarm Core Dashboard",
reload=False,
show=False,
dark=True,
root_path=DASHBOARD_ROOT_PATH,
)
if __name__ in {"__main__", "__mp_main__"}:
main()
@@ -0,0 +1,383 @@
"""Shared theme module for NiceGUI dashboards.
Exports palette constants, CSS, and helper functions used by both
multi_swarm_core.dashboard and strategy_crypto.frontend dashboards.
"""
from __future__ import annotations
import html
from typing import Any
from nicegui import ui
__all__ = [
"COLOR_BG",
"COLOR_SURFACE",
"COLOR_SURFACE_2",
"COLOR_BORDER",
"COLOR_BORDER_HOVER",
"COLOR_PRIMARY",
"COLOR_SECONDARY",
"COLOR_ACCENT",
"COLOR_SUCCESS",
"COLOR_DANGER",
"COLOR_TEXT",
"COLOR_TEXT_MUTED",
"_STATUS_BADGE",
"_CUSTOM_CSS",
"_json_to_html",
"_apply_theme",
"_build_header",
]
# --- Neon Trading Dashboard palette ---
COLOR_BG = "#0A0A0F"
COLOR_SURFACE = "#13131A"
COLOR_SURFACE_2 = "#1C1C26"
COLOR_BORDER = "rgba(255, 45, 135, 0.12)"
COLOR_BORDER_HOVER = "rgba(255, 45, 135, 0.45)"
COLOR_PRIMARY = "#FF2D87"
COLOR_SECONDARY = "#00D9FF"
COLOR_ACCENT = "#FFB800"
COLOR_SUCCESS = "#00E676"
COLOR_DANGER = "#FF3D60"
COLOR_TEXT = "#FFFFFF"
COLOR_TEXT_MUTED = "#7A7A8C"
_STATUS_BADGE: dict[str, tuple[str, str]] = {
"running": ("● running", "positive"),
"completed": ("✓ completed", "positive"),
"failed": ("✕ failed", "negative"),
}
_CUSTOM_CSS = f"""
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap');
html, body, .q-page, .q-card, .q-btn, .q-field, .q-table, .text-h4, .text-h6, .text-subtitle1, .text-caption, .text-body1, .nav-link, .brand, label, p, span, div {{
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
letter-spacing: -0.01em;
}}
.material-icons, .material-icons-outlined, .material-symbols-outlined, .q-icon, i.q-icon, i[class*="material"] {{
font-family: 'Material Icons' !important;
font-feature-settings: 'liga';
letter-spacing: normal !important;
}}
code, pre, .q-code, .nicegui-code {{ font-family: 'JetBrains Mono', 'Fira Code', monospace !important; font-size: 13.5px !important; }}
body, .q-page-container, .q-page {{
background: {COLOR_BG} !important;
color: {COLOR_TEXT};
background-image:
radial-gradient(ellipse 800px 400px at 20% 0%, rgba(255, 45, 135, 0.08) 0%, transparent 60%),
radial-gradient(ellipse 600px 400px at 80% 100%, rgba(0, 217, 255, 0.06) 0%, transparent 60%);
background-attachment: fixed;
}}
.q-card {{
background: {COLOR_SURFACE} !important;
color: {COLOR_TEXT} !important;
border: 1px solid {COLOR_BORDER};
border-radius: 14px !important;
box-shadow:
0 1px 2px rgba(0,0,0,0.5),
0 8px 24px rgba(0,0,0,0.25),
inset 0 1px 0 rgba(255,255,255,0.04);
transition: all 0.2s ease;
position: relative;
overflow: hidden;
}}
.q-card::before {{
content: '';
position: absolute;
top: 0; left: 0; right: 0;
height: 1px;
background: linear-gradient(90deg, transparent, rgba(255, 45, 135, 0.4), transparent);
opacity: 0.5;
}}
.q-card:hover {{
border-color: rgba(255, 45, 135, 0.5);
box-shadow:
0 1px 2px rgba(0,0,0,0.5),
0 8px 32px rgba(255, 45, 135, 0.15),
inset 0 1px 0 rgba(255,255,255,0.05);
}}
.metric-card {{
padding: 20px 16px;
text-align: left;
display: flex;
flex-direction: column;
gap: 6px;
min-width: 140px;
}}
.metric-card .text-caption {{
color: {COLOR_TEXT_MUTED} !important;
font-size: 11px !important;
font-weight: 500 !important;
text-transform: uppercase;
letter-spacing: 0.06em;
}}
.metric-card .text-h4 {{
color: {COLOR_TEXT} !important;
font-weight: 600 !important;
font-size: 26px !important;
line-height: 1.2 !important;
font-feature-settings: 'tnum';
}}
.metric-card.accent-cyan .text-h4 {{ color: {COLOR_PRIMARY} !important; }}
.metric-card.accent-purple .text-h4 {{ color: {COLOR_SECONDARY} !important; }}
.metric-card.accent-amber .text-h4 {{ color: {COLOR_ACCENT} !important; }}
.metric-card.accent-green .text-h4 {{ color: {COLOR_SUCCESS} !important; }}
.q-header {{
background: rgba(10, 10, 15, 0.75) !important;
backdrop-filter: blur(20px) saturate(180%);
-webkit-backdrop-filter: blur(20px) saturate(180%);
border-bottom: 1px solid {COLOR_BORDER} !important;
box-shadow: 0 1px 0 rgba(255, 45, 135, 0.15) !important;
}}
.nav-link {{
color: {COLOR_TEXT_MUTED} !important;
padding: 8px 14px;
border-radius: 8px;
text-decoration: none;
font-size: 13.5px;
font-weight: 500;
transition: all 0.15s ease;
position: relative;
}}
.nav-link:hover {{
color: {COLOR_TEXT} !important;
background: {COLOR_SURFACE_2};
}}
.nav-link.active {{
color: {COLOR_PRIMARY} !important;
background: rgba(255, 45, 135, 0.08);
}}
.nav-link.active::after {{
content: '';
position: absolute;
bottom: -16px;
left: 14px;
right: 14px;
height: 2px;
background: {COLOR_PRIMARY};
border-radius: 2px 2px 0 0;
}}
.brand {{
color: {COLOR_TEXT};
font-weight: 700;
font-size: 15px;
display: flex;
align-items: center;
gap: 8px;
}}
.brand-dot {{
width: 10px;
height: 10px;
border-radius: 50%;
background: {COLOR_PRIMARY};
box-shadow: 0 0 16px {COLOR_PRIMARY}, 0 0 4px {COLOR_PRIMARY};
animation: pulse-pink 2s ease-in-out infinite;
}}
@keyframes pulse-pink {{
0%, 100% {{ box-shadow: 0 0 16px {COLOR_PRIMARY}, 0 0 4px {COLOR_PRIMARY}; }}
50% {{ box-shadow: 0 0 24px {COLOR_PRIMARY}, 0 0 8px {COLOR_PRIMARY}; }}
}}
.q-linear-progress {{ height: 8px !important; border-radius: 6px !important; }}
.q-linear-progress__track {{ background: {COLOR_SURFACE_2} !important; }}
.q-linear-progress__model {{ border-radius: 6px !important; }}
.q-separator {{ background: {COLOR_BORDER} !important; }}
.q-field--outlined .q-field__control {{
background: {COLOR_SURFACE} !important;
border-radius: 8px !important;
}}
.q-field--outlined .q-field__control:before {{ border-color: {COLOR_BORDER} !important; }}
.q-field--outlined.q-field--focused .q-field__control:after {{ border-color: {COLOR_PRIMARY} !important; }}
.q-field__label {{ color: {COLOR_TEXT_MUTED} !important; }}
.q-field__native, .q-field__input {{ color: {COLOR_TEXT} !important; }}
.q-btn {{ border-radius: 8px !important; font-weight: 500 !important; text-transform: none !important; letter-spacing: 0 !important; }}
.q-table {{ background: transparent !important; color: {COLOR_TEXT} !important; }}
.q-table thead tr {{ background: {COLOR_SURFACE_2} !important; }}
.q-table th {{
color: {COLOR_TEXT_MUTED} !important;
font-size: 11px !important;
font-weight: 600 !important;
text-transform: uppercase;
letter-spacing: 0.06em;
}}
.q-table tbody tr {{ transition: background 0.15s; }}
.q-table tbody tr:hover {{ background: rgba(255, 45, 135, 0.05) !important; }}
.q-table tbody tr.selected {{ background: rgba(255, 45, 135, 0.12) !important; }}
.q-table td {{ border-bottom: 1px solid {COLOR_BORDER} !important; font-feature-settings: 'tnum'; }}
.text-h6 {{ font-weight: 600 !important; letter-spacing: -0.015em !important; }}
.text-subtitle1 {{
color: {COLOR_TEXT_MUTED} !important;
font-size: 11px !important;
font-weight: 600 !important;
text-transform: uppercase !important;
letter-spacing: 0.08em !important;
margin-bottom: 8px !important;
}}
code, pre, .nicegui-code {{
background: #1A1A24 !important;
color: {COLOR_TEXT} !important;
border: 1px solid {COLOR_BORDER};
border-radius: 10px !important;
padding: 16px !important;
font-size: 13.5px !important;
line-height: 1.6 !important;
}}
.hljs {{ background: transparent !important; color: {COLOR_TEXT} !important; }}
.hljs-attr, .hljs-attribute {{ color: {COLOR_SECONDARY} !important; font-weight: 500; }}
.hljs-string {{ color: {COLOR_SUCCESS} !important; }}
.hljs-number, .hljs-literal {{ color: {COLOR_PRIMARY} !important; font-weight: 500; }}
.hljs-keyword, .hljs-built_in {{ color: {COLOR_ACCENT} !important; }}
.hljs-punctuation, .hljs-meta {{ color: {COLOR_TEXT_MUTED} !important; }}
.hljs-comment {{ color: {COLOR_TEXT_MUTED} !important; font-style: italic; }}
.hljs-name, .hljs-title {{ color: {COLOR_PRIMARY} !important; }}
/* Prism.js tokens (NiceGUI usa Prism per ui.code) */
.token.property, .token.attr-name, .token.tag {{ color: {COLOR_SECONDARY} !important; font-weight: 500; }}
.token.string, .token.url {{ color: {COLOR_SUCCESS} !important; }}
.token.number, .token.boolean, .token.null, .token.symbol {{ color: {COLOR_PRIMARY} !important; font-weight: 500; }}
.token.keyword, .token.constant, .token.builtin, .token.atrule {{ color: {COLOR_ACCENT} !important; }}
.token.punctuation, .token.operator {{ color: {COLOR_TEXT_MUTED} !important; }}
.token.comment {{ color: {COLOR_TEXT_MUTED} !important; font-style: italic; }}
.token.function, .token.class-name {{ color: {COLOR_PRIMARY} !important; }}
pre[class*="language-"], code[class*="language-"] {{
color: {COLOR_TEXT} !important;
text-shadow: none !important;
}}
.q-badge {{ border-radius: 6px !important; font-weight: 500 !important; padding: 4px 10px !important; font-size: 12px !important; }}
.config-block {{
background: #1A1A24;
color: {COLOR_TEXT};
border: 1px solid {COLOR_BORDER};
border-radius: 10px;
padding: 18px 20px;
font-family: 'JetBrains Mono', 'Fira Code', monospace;
font-size: 13.5px;
line-height: 1.7;
overflow-x: auto;
white-space: pre;
margin: 0;
}}
.config-block .cb-key {{ color: {COLOR_SECONDARY}; font-weight: 500; }}
.config-block .cb-string {{ color: {COLOR_SUCCESS}; }}
.config-block .cb-number {{ color: {COLOR_PRIMARY}; font-weight: 500; }}
.config-block .cb-bool {{ color: {COLOR_ACCENT}; }}
.config-block .cb-null {{ color: {COLOR_ACCENT}; font-style: italic; }}
.config-block .cb-punct {{ color: {COLOR_TEXT_MUTED}; }}
.raw-block {{
background: #1A1A24;
color: {COLOR_TEXT};
border: 1px solid {COLOR_BORDER};
border-radius: 10px;
padding: 18px 20px;
font-family: 'JetBrains Mono', 'Fira Code', monospace;
font-size: 13px;
line-height: 1.6;
overflow-x: auto;
white-space: pre-wrap;
word-break: break-word;
max-height: 400px;
overflow-y: auto;
margin: 0;
}}
</style>
"""
def _json_to_html(obj: Any, indent: int = 0) -> str:
"""Render JSON con span colorati espliciti. Garantisce leggibilità ovunque."""
pad = " " * indent
inner_pad = " " * (indent + 1)
if isinstance(obj, dict):
if not obj:
return '<span class="cb-punct">{}</span>'
items = []
for k, v in obj.items():
key = f'<span class="cb-key">"{html.escape(str(k))}"</span>'
val = _json_to_html(v, indent + 1)
items.append(f"{inner_pad}{key}<span class=\"cb-punct\">:</span> {val}")
return ('<span class="cb-punct">{</span>\n'
+ '<span class="cb-punct">,</span>\n'.join(items)
+ f'\n{pad}<span class="cb-punct">}}</span>')
if isinstance(obj, list):
if not obj:
return '<span class="cb-punct">[]</span>'
items = [_json_to_html(x, indent + 1) for x in obj]
return ('<span class="cb-punct">[</span>\n'
+ '<span class="cb-punct">,</span>\n'.join(inner_pad + i for i in items)
+ f'\n{pad}<span class="cb-punct">]</span>')
if isinstance(obj, bool):
return f'<span class="cb-bool">{str(obj).lower()}</span>'
if obj is None:
return '<span class="cb-null">null</span>'
if isinstance(obj, (int, float)):
return f'<span class="cb-number">{obj}</span>'
return f'<span class="cb-string">"{html.escape(str(obj))}"</span>'
def _apply_theme() -> None:
ui.add_head_html(_CUSTOM_CSS)
ui.dark_mode().enable()
ui.colors(
primary=COLOR_PRIMARY,
secondary=COLOR_SECONDARY,
accent=COLOR_ACCENT,
dark=COLOR_BG,
dark_page=COLOR_BG,
positive=COLOR_SUCCESS,
negative=COLOR_DANGER,
info=COLOR_PRIMARY,
warning=COLOR_ACCENT,
)
def _build_header(
active: str,
brand_subtitle: str,
nav_items: list[tuple[str, str]],
db_label: str,
) -> None:
"""Render the top navigation header.
Args:
active: URL path of the currently active page (e.g. "/").
brand_subtitle: Text shown after the brand dot, e.g. "Coevolutivo / GA".
nav_items: List of (path, label) tuples for nav links.
db_label: Short DB identifier shown in the top-right corner.
"""
with ui.header().classes("items-center justify-between q-px-lg q-py-md"):
with ui.row().classes("items-center gap-8"):
with ui.row().classes("items-center gap-2").classes("brand"):
ui.html('<span class="brand-dot"></span>')
ui.html(
f'<span class="brand">Multi-Swarm <span style="color:{COLOR_TEXT_MUTED}'
f';font-weight:400;">/ {brand_subtitle}</span></span>'
)
with ui.row().classes("items-center gap-1"):
for path, label in nav_items:
cls = "nav-link active" if active == path else "nav-link"
ui.link(label, path).classes(cls)
with ui.row().classes("items-center gap-3"):
ui.html(
f'<span style="color:{COLOR_TEXT_MUTED};font-size:12px;'
f'font-family:JetBrains Mono,monospace;">{db_label}</span>'
)
@@ -3,34 +3,12 @@ from __future__ import annotations
import random import random
from ..genome.hypothesis import HypothesisAgentGenome, ModelTier from ..genome.hypothesis import HypothesisAgentGenome, ModelTier
from ..genome.mutation import COGNITIVE_STYLES from ..genome.prompt_library import PromptLibrary
STYLE_PROMPTS: dict[str, str] = { # Mantenuto come alias backcompat: equivalente a PromptLibrary.default().styles.
"physicist": ( # Nuovi caller dovrebbero usare PromptLibrary direttamente per supportare
"Cerca leggi conservative, simmetrie, regimi di scala. " # l'override via prompts.json di una strategia.
"Pensa in termini di flussi e potenziali." STYLE_PROMPTS: dict[str, str] = PromptLibrary.default().styles
),
"biologist": (
"Cerca pattern adattivi, nicchie ecologiche, "
"predator-prey dynamics tra partecipanti del mercato."
),
"historian": (
"Cerca pattern ricorrenti su scale temporali multiple, "
"analogie con regimi storici, mean reversion strutturali."
),
"meteorologist": (
"Cerca regimi di volatilità che si autoalimentano, "
"transizioni di stato come fronti, persistenza locale."
),
"ecologist": (
"Cerca interazioni multi-asset, correlazioni cluster, "
"segnali di stress sistemico nelle dinamiche di flusso."
),
"engineer": (
"Cerca segnali con rapporto S/N favorevole, filtri causali, "
"robustezza a perturbazioni di calibrazione."
),
}
def build_initial_population( def build_initial_population(
@@ -38,15 +16,22 @@ def build_initial_population(
model_tier: ModelTier, model_tier: ModelTier,
rng: random.Random, rng: random.Random,
feature_pool: tuple[str, ...] = ("close", "high", "low", "volume"), feature_pool: tuple[str, ...] = ("close", "high", "low", "volume"),
prompt_library: PromptLibrary | None = None,
) -> list[HypothesisAgentGenome]: ) -> list[HypothesisAgentGenome]:
"""Costruisce una popolazione iniziale K varia per stile cognitivo + parametri.""" """Costruisce una popolazione iniziale K varia per stile cognitivo + parametri.
``prompt_library`` controlla quali stili sono disponibili e quale system_prompt
iniziale viene assegnato. Default = builtin 6 stili (physicist, biologist, ...).
Override tipico: ``PromptLibrary.from_json(strategy_crypto/prompts.json)``.
"""
lib = prompt_library or PromptLibrary.default()
population: list[HypothesisAgentGenome] = [] population: list[HypothesisAgentGenome] = []
for i in range(k): for i in range(k):
style = COGNITIVE_STYLES[i % len(COGNITIVE_STYLES)] style = lib.style_at(i)
n_features = rng.randint(1, len(feature_pool)) n_features = rng.randint(1, len(feature_pool))
feats = sorted(rng.sample(feature_pool, k=n_features)) feats = sorted(rng.sample(feature_pool, k=n_features))
g = HypothesisAgentGenome( g = HypothesisAgentGenome(
system_prompt=STYLE_PROMPTS[style], system_prompt=lib.directive(style),
feature_access=feats, feature_access=feats,
temperature=round(rng.uniform(0.7, 1.2), 2), temperature=round(rng.uniform(0.7, 1.2), 2),
top_p=0.95, top_p=0.95,
@@ -7,6 +7,10 @@ from .hypothesis import HypothesisAgentGenome
FEATURE_POOL: tuple[str, ...] = ("open", "high", "low", "close", "volume") FEATURE_POOL: tuple[str, ...] = ("open", "high", "low", "close", "volume")
# Lista di default builtin (allineata con PromptLibrary.default()).
# Il dispatcher run_phase1 sovrascrive `COGNITIVE_STYLES` con la lista letta
# da prompts.json prima del loop GA, cosi' `mutate_cognitive_style` pesca
# dai candidati corretti per la strategia in corso.
COGNITIVE_STYLES: tuple[str, ...] = ( COGNITIVE_STYLES: tuple[str, ...] = (
"physicist", "physicist",
"biologist", "biologist",
@@ -17,6 +21,18 @@ COGNITIVE_STYLES: tuple[str, ...] = (
) )
def set_cognitive_styles(styles: tuple[str, ...]) -> None:
"""Sovrascrive la lista globale di stili candidati per la mutation.
Da chiamare PRIMA del GA loop (es. in run_phase1 dopo aver caricato la
PromptLibrary). Non thread-safe: pensata per uno script CLI.
"""
global COGNITIVE_STYLES
if not styles:
raise ValueError("set_cognitive_styles: lista vuota")
COGNITIVE_STYLES = tuple(styles)
def _clone_with(g: HypothesisAgentGenome, **overrides: Any) -> HypothesisAgentGenome: def _clone_with(g: HypothesisAgentGenome, **overrides: Any) -> HypothesisAgentGenome:
payload: dict[str, Any] = g.to_dict() payload: dict[str, Any] = g.to_dict()
payload.update(overrides) payload.update(overrides)
@@ -51,9 +51,9 @@ MUTATION_INSTRUCTIONS: dict[str, str] = {
# Keyword tecniche minime per validare che il prompt sia ancora "una strategia". # Keyword tecniche minime per validare che il prompt sia ancora "una strategia".
_VALID_KEYWORDS = ( _VALID_KEYWORDS = (
"rsi", "sma", "ema", "atr", "atr_pct", "realized_vol", "rsi", "sma", "sma_pct", "ema", "atr", "atr_pct", "realized_vol",
"momentum", "breakout", "mean", "reversion", "momentum", "breakout", "mean", "reversion",
"macd", "vwap", "bb", "bollinger", "stoch", "trend", "signal", "buy", "macd", "macd_pct", "vwap", "bb", "bollinger", "stoch", "trend", "signal", "buy",
"sell", "long", "short", "entry", "exit", "stop", "rule", "condition", "sell", "long", "short", "entry", "exit", "stop", "rule", "condition",
"if", "when", "and", "or", "gt", "lt", ">", "<", "ge", "le", "if", "when", "and", "or", "gt", "lt", ">", "<", "ge", "le",
"hour", "dow", "weekend", "indicator", "feature", "hour", "dow", "weekend", "indicator", "feature",
@@ -0,0 +1,234 @@
"""Libreria di stili cognitivi + direttive system_prompt per il GA.
Carica da un file JSON esterno (tipicamente shippato dal singolo strategy
member, es. ``strategy_crypto/prompts.json``) le coppie ``style -> directive``
usate da:
- :func:`multi_swarm_core.ga.initial.build_initial_population` per il
bootstrap della popolazione (style assegnato round-robin, directive
come system_prompt iniziale).
- :func:`multi_swarm_core.genome.mutation.mutate_cognitive_style` per
pescare i candidati di mutazione (range = key del JSON).
Schema JSON atteso::
{
"styles": {
"<name>": {"directive": "<testo system_prompt>"},
...
},
"agent_role": "<descrizione agente, opzionale>",
"pattern_guidance": "<sezione PATTERN GUIDANCE, opzionale>",
"instruction": "<frase finale USER, opzionale>",
"domain_warnings": "<warning di dominio, opzionale>"
}
I 6 stili default (physicist, biologist, historian, meteorologist,
ecologist, engineer) sono comunque disponibili via :meth:`PromptLibrary.default`
per backcompat con test/script senza file esterno.
"""
from __future__ import annotations
import json
from dataclasses import dataclass, field
from pathlib import Path
_DEFAULT_STYLES: dict[str, str] = {
"physicist": (
"Cerca leggi conservative, simmetrie, regimi di scala. "
"Pensa in termini di flussi e potenziali."
),
"biologist": (
"Cerca pattern adattivi, nicchie ecologiche, "
"predator-prey dynamics tra partecipanti del mercato."
),
"historian": (
"Cerca pattern ricorrenti su scale temporali multiple, "
"analogie con regimi storici, mean reversion strutturali."
),
"meteorologist": (
"Cerca regimi di volatilità che si autoalimentano, "
"transizioni di stato come fronti, persistenza locale."
),
"ecologist": (
"Cerca interazioni multi-asset, correlazioni cluster, "
"segnali di stress sistemico nelle dinamiche di flusso."
),
"engineer": (
"Cerca segnali con rapporto S/N favorevole, filtri causali, "
"robustezza a perturbazioni di calibrazione."
),
}
class PromptLibraryError(ValueError):
"""Sollevata su JSON malformato o stili invalid."""
_DEFAULT_PATTERN_GUIDANCE = (
"Forme di curva: trend (SMA cross), compressione volatilita (atr_pct basso), "
"espansione volatilita (atr_pct alto), mean reversion (sma_pct estremo), "
"momentum (macd_pct, rsi zone).\n\n"
" Ripetibilita dell'andamento: crossover/crossunder ricorrenti, pattern intra-day "
"(hour gate), pattern settimanali (dow/is_weekend), range breakout."
)
_DEFAULT_AGENT_ROLE = (
"Sei un agente generatore di ipotesi di trading quantitativo per un sistema swarm."
)
_DEFAULT_INSTRUCTION = (
"Genera una strategia che cerchi anomalie sfruttabili in questo regime."
)
@dataclass(frozen=True)
class PromptLibrary:
"""Set immutabile di stili cognitivi + direttive system_prompt.
v3.0: aggiunge 4 campi top-level strategy-specific iniettabili nel prompt
dal compositor ``_build_system_prompt``. Tutti opzionali con default sensati
per backcompat.
v3.1: aggiunge anti_patterns e output_priorities iniettati dopo i VINCOLI core.
"""
styles: dict[str, str]
focus: dict[str, list[str]] # style -> lista metriche prioritarie
# NEW v3.0: contenuto strategy-specific iniettabile nel prompt
agent_role: str = field(default="") # header SYSTEM, descrive chi e' l'agente
pattern_guidance: str = field(default="") # sezione PATTERN GUIDANCE del SYSTEM
instruction: str = field(default="") # frase finale USER ("Genera una strategia...")
domain_warnings: str = field(default="") # opzionale: warning di dominio (es. crypto 24/7)
anti_patterns: str = field(default="") # NEW v3.1: lista esplicita di pattern da evitare
output_priorities: str = field(default="") # NEW v3.1: trade-off espliciti di output
def __post_init__(self) -> None:
if not self.styles:
raise PromptLibraryError("PromptLibrary deve avere almeno uno stile")
for name, directive in self.styles.items():
if not isinstance(name, str) or not name.strip():
raise PromptLibraryError(f"nome stile invalido: {name!r}")
if not isinstance(directive, str) or not directive.strip():
raise PromptLibraryError(
f"directive vuota o invalida per stile {name!r}"
)
# Validate new optional string fields: if provided must be non-whitespace
for field_name, value in (
("agent_role", self.agent_role),
("pattern_guidance", self.pattern_guidance),
("instruction", self.instruction),
("domain_warnings", self.domain_warnings),
("anti_patterns", self.anti_patterns),
("output_priorities", self.output_priorities),
):
if not isinstance(value, str):
raise PromptLibraryError(
f"campo '{field_name}' deve essere stringa, non {type(value)}"
)
if value and not value.strip():
raise PromptLibraryError(
f"campo '{field_name}' fornito ma contiene solo whitespace"
)
@classmethod
def default(cls) -> PromptLibrary:
"""Libreria builtin con i 6 stili originali (fallback senza file)."""
return cls(
styles=dict(_DEFAULT_STYLES),
focus={},
agent_role=_DEFAULT_AGENT_ROLE,
pattern_guidance=_DEFAULT_PATTERN_GUIDANCE,
instruction=_DEFAULT_INSTRUCTION,
domain_warnings="",
anti_patterns="",
output_priorities="",
)
@classmethod
def from_json(cls, path: Path | str) -> PromptLibrary:
"""Carica da file JSON.
Schema::
{
"styles": {<name>: {"directive": "...", "focus_metrics": [...]}},
"agent_role": "...", // opzionale
"pattern_guidance": "...", // opzionale
"instruction": "...", // opzionale
"domain_warnings": "..." // opzionale
}
"""
p = Path(path)
try:
data = json.loads(p.read_text(encoding="utf-8"))
except FileNotFoundError as e:
raise PromptLibraryError(f"file non trovato: {p}") from e
except json.JSONDecodeError as e:
raise PromptLibraryError(f"JSON malformato in {p}: {e}") from e
if not isinstance(data, dict):
raise PromptLibraryError(f"root JSON deve essere un object, non {type(data)}")
styles_raw = data.get("styles")
if not isinstance(styles_raw, dict):
raise PromptLibraryError("manca chiave 'styles' (object) nel JSON")
styles: dict[str, str] = {}
focus: dict[str, list[str]] = {}
for name, entry in styles_raw.items():
if not isinstance(entry, dict):
raise PromptLibraryError(
f"entry per stile {name!r} deve essere object, non {type(entry)}"
)
directive = entry.get("directive")
if not isinstance(directive, str):
raise PromptLibraryError(
f"manca 'directive' (string) per stile {name!r}"
)
styles[name] = directive
fm = entry.get("focus_metrics", [])
if not isinstance(fm, list):
raise PromptLibraryError(
f"focus_metrics di {name!r} deve essere lista, non {type(fm)}"
)
focus[name] = [str(x) for x in fm]
# Parse new optional top-level fields (v3.0)
agent_role = data.get("agent_role", "")
pattern_guidance = data.get("pattern_guidance", "")
instruction = data.get("instruction", "")
domain_warnings = data.get("domain_warnings", "")
# Parse new optional top-level fields (v3.1)
anti_patterns_raw = data.get("anti_patterns", "")
output_priorities_raw = data.get("output_priorities", "")
if not isinstance(anti_patterns_raw, str):
raise PromptLibraryError(f"anti_patterns deve essere stringa, non {type(anti_patterns_raw)}")
if not isinstance(output_priorities_raw, str):
raise PromptLibraryError(f"output_priorities deve essere stringa, non {type(output_priorities_raw)}")
return cls(
styles=styles,
focus=focus,
agent_role=agent_role,
pattern_guidance=pattern_guidance,
instruction=instruction,
domain_warnings=domain_warnings,
anti_patterns=anti_patterns_raw,
output_priorities=output_priorities_raw,
)
@property
def cognitive_styles(self) -> tuple[str, ...]:
"""Tupla immutabile dei nomi degli stili, nell'ordine di insertion del JSON."""
return tuple(self.styles)
def directive(self, style: str) -> str:
"""Ritorna la directive di ``style`` o solleva KeyError."""
return self.styles[style]
def style_at(self, index: int) -> str:
"""Round-robin: ``cognitive_styles[index % len()]``."""
return self.cognitive_styles[index % len(self.cognitive_styles)]
def focus_metrics_for(self, style: str) -> list[str]:
"""Ritorna lista delle metriche prioritarie per ``style``. Empty list se non specificate."""
return self.focus.get(style, [])
@@ -4,6 +4,122 @@ import numpy as np
import pandas as pd # type: ignore[import-untyped] import pandas as pd # type: ignore[import-untyped]
def autocorr_lag1(returns: pd.Series) -> float:
"""Autocorrelazione dei ritorni con lag 1 (correlazione di Pearson)."""
if len(returns) < 2:
return 0.0
val = returns.autocorr(lag=1)
return float(val) if pd.notna(val) else 0.0
def hurst_exponent(returns: pd.Series) -> float:
"""Hurst exponent via R/S analysis (rescaled range).
Range 0-1: 0.5 random walk, >0.55 trending persistente, <0.45 mean reverting.
Implementazione classica con scale multiple (2^k bins).
"""
n = len(returns)
if n < 100:
return 0.5 # insufficiente dati, ritorna random-walk default
series = returns.dropna().values
n = len(series)
if n < 100:
return 0.5
# Scale: 2^k che dividono n in segmenti
scales = [2**k for k in range(4, int(np.log2(n // 2)) + 1)]
if not scales:
return 0.5
rs_values: list[float] = []
for scale in scales:
n_chunks = n // scale
if n_chunks < 1:
continue
rs_chunk: list[float] = []
for i in range(n_chunks):
chunk = series[i * scale : (i + 1) * scale]
mean = chunk.mean()
cumdev = (chunk - mean).cumsum()
r = cumdev.max() - cumdev.min()
s = chunk.std()
if s > 0:
rs_chunk.append(r / s)
if rs_chunk:
rs_values.append(float(np.mean(rs_chunk)))
if len(rs_values) < 2:
return 0.5
log_scales = np.log(scales[: len(rs_values)])
log_rs = np.log(rs_values)
# Hurst = slope della regressione log-log
h, _ = np.polyfit(log_scales, log_rs, 1)
return float(np.clip(h, 0.0, 1.0))
def vol_percentile_historical(
returns: pd.Series,
current_window: int = 24,
ref_window: int = 2000,
) -> float:
"""Percentile (0-100) della vol corrente nella distribuzione storica.
Vol = std rolling su current_window barre. Confronta l'ultimo valore contro
la distribuzione dei valori della stessa std rolling sugli ultimi ref_window.
Output: 0 (vol attuale tra le piu basse), 100 (tra le piu alte).
"""
if len(returns) < max(current_window, 100):
return 50.0
rolling_vol = returns.rolling(current_window, min_periods=current_window).std()
rolling_vol = rolling_vol.dropna()
if len(rolling_vol) < 10:
return 50.0
# Limita ref_window all'effettiva disponibilita
ref = rolling_vol.iloc[-ref_window:] if len(rolling_vol) > ref_window else rolling_vol
current = float(rolling_vol.iloc[-1])
pct = float((ref < current).sum()) / len(ref) * 100.0
return pct
def seasonality_strength(
returns: pd.Series,
by: str,
) -> float:
"""Frazione di varianza dei ritorni spiegata dalla feature temporale `by`.
`by` in {"hour", "dow"}. Output 0-1: 0 = no seasonality, 1 = tutta la varianza
e dovuta al ciclo. Calcolato come 1 - (var residua / var totale) usando i
gruppi indotti dalla feature.
"""
if not isinstance(returns.index, pd.DatetimeIndex):
return 0.0
if len(returns) < 50:
return 0.0
if by == "hour":
groups = returns.index.hour
elif by == "dow":
groups = returns.index.dayofweek
else:
raise ValueError(f"by deve essere 'hour' o 'dow', non {by!r}")
total_var = float(returns.var())
if total_var <= 0:
return 0.0
grouped = returns.groupby(groups)
group_means = grouped.transform("mean")
residuals = returns - group_means
residual_var = float(residuals.var())
explained = 1.0 - (residual_var / total_var)
return float(np.clip(explained, 0.0, 1.0))
def sharpe_ratio(returns: pd.Series, periods_per_year: int = 8760, rf: float = 0.0) -> float: def sharpe_ratio(returns: pd.Series, periods_per_year: int = 8760, rf: float = 0.0) -> float:
"""Sharpe annualizzato. periods_per_year=8760 per dati orari.""" """Sharpe annualizzato. periods_per_year=8760 per dati orari."""
excess = returns - rf / periods_per_year excess = returns - rf / periods_per_year
@@ -25,3 +141,135 @@ def total_return(equity: pd.Series) -> float:
if equity.iloc[0] == 0: if equity.iloc[0] == 0:
return float(equity.iloc[-1]) return float(equity.iloc[-1])
return float(equity.iloc[-1] / equity.iloc[0] - 1.0) return float(equity.iloc[-1] / equity.iloc[0] - 1.0)
def efficiency_ratio_kaufman(prices: pd.Series, length: int = 100) -> float:
"""Kaufman Efficiency Ratio: |net move| / sum(|step|) su rolling length.
Output 0-1: 0 = puro noise (movimento dissipativo), 1 = puro trend efficiente.
Discriminatore trending/ranging robusto.
"""
if len(prices) < length + 1:
return 0.0
recent = prices.iloc[-length:]
net_move = abs(recent.iloc[-1] - recent.iloc[0])
total_move = recent.diff().abs().sum()
if total_move == 0 or pd.isna(total_move):
return 0.0
return float(net_move / total_move)
def tail_index_hill(returns: pd.Series, side: str, top_frac: float = 0.05) -> float:
"""Hill estimator del tail index (pendenza coda) per side in {'left', 'right'}.
Output: indice di pesantezza coda. Valori piu bassi = coda piu pesante.
<2 = varianza infinita (Cauchy-like), 3-4 = tipico crypto, >5 = quasi-Gaussiana.
Robusto al singolo outlier (vs kurtosis).
"""
r = returns.dropna()
n = len(r)
if n < 50:
return 5.0 # default quasi-Gaussiano
k = max(int(n * top_frac), 5)
if side == "left":
tail = (-r).nlargest(k)
elif side == "right":
tail = r.nlargest(k)
else:
raise ValueError(f"side deve essere 'left' o 'right', non {side!r}")
# Filtra valori non-positivi (Hill richiede tail positiva)
tail = tail[tail > 0]
if len(tail) < 5:
return 5.0
log_tail = np.log(tail.values)
threshold = log_tail[-1]
excess = log_tail[:-1] - threshold
if excess.sum() <= 0:
return 5.0
hill = (len(excess)) / excess.sum()
return float(np.clip(hill, 1.0, 10.0))
def structural_uptrend_score(prices: pd.Series, window: int = 5) -> float:
"""Frazione di periodi in struttura HH/HL (Dow-style uptrend).
Identifica swing high/low usando rolling max/min con finestra ``window``.
Conta sequenze higher-high + higher-low (uptrend) vs lower-high + lower-low (downtrend).
Output: 0 (puro downtrend) - 0.5 (range/mixed) - 1 (puro uptrend).
"""
if len(prices) < 4 * window:
return 0.5
swing_high = prices.rolling(2 * window + 1, center=True).max() == prices
swing_low = prices.rolling(2 * window + 1, center=True).min() == prices
highs = prices[swing_high].dropna()
lows = prices[swing_low].dropna()
if len(highs) < 3 or len(lows) < 3:
return 0.5
hh = (highs.diff() > 0).sum()
lh = (highs.diff() < 0).sum()
hl = (lows.diff() > 0).sum()
ll = (lows.diff() < 0).sum()
up_signals = hh + hl
down_signals = lh + ll
total = up_signals + down_signals
if total == 0:
return 0.5
return float(up_signals / total)
def compression_ratio(prices: pd.Series, recent_window: int = 50, ref_window: int = 200) -> float:
"""Range(recent) / Range(ref). <1 = compressione vol, >1 = espansione.
Range = high - low della finestra. Cattura il "coil" pre-breakout.
"""
if len(prices) < ref_window:
return 1.0
recent = prices.iloc[-recent_window:]
ref = prices.iloc[-ref_window:]
recent_range = float(recent.max() - recent.min())
ref_range = float(ref.max() - ref.min())
if ref_range <= 0:
return 1.0
return recent_range / ref_range
def spectral_entropy_and_cycle(
returns: pd.Series, length: int = 256
) -> tuple[float, float | None]:
"""FFT su returns -> (entropy normalizzata, dominant_cycle gated).
entropy: 0-1, Shannon entropy normalizzata dello spettro di potenza.
0 = una sola frequenza domina, 1 = spettro piatto (rumore bianco).
dominant_cycle: periodo (barre) della frequenza dominante,
MA solo se entropy < 0.6 (struttura presente). Altrimenti None.
"""
r = returns.dropna()
if len(r) < length:
return 1.0, None
series = r.iloc[-length:].values
# Detrend
series = series - series.mean()
fft = np.fft.rfft(series)
power = np.abs(fft) ** 2
if power.sum() <= 0:
return 1.0, None
p = power / power.sum()
# Entropy normalizzata (Shannon / log(N))
nonzero = p[p > 0]
entropy = float(-(nonzero * np.log(nonzero)).sum() / np.log(len(nonzero)))
entropy = max(0.0, min(1.0, entropy))
if entropy >= 0.6:
return entropy, None
# Skip DC component (index 0)
if len(power) <= 1:
return entropy, None
peak_idx = int(np.argmax(power[1:])) + 1
if peak_idx == 0:
return entropy, None
cycle = float(length / peak_idx)
return entropy, cycle
@@ -13,6 +13,7 @@ possa leggere lo stato a run terminato (o in corso).
from __future__ import annotations from __future__ import annotations
import random import random
from concurrent.futures import ThreadPoolExecutor
from dataclasses import dataclass, field from dataclasses import dataclass, field
from pathlib import Path from pathlib import Path
@@ -20,13 +21,15 @@ import pandas as pd # type: ignore[import-untyped]
from ..agents.adversarial import AdversarialAgent from ..agents.adversarial import AdversarialAgent
from ..agents.falsification import FalsificationAgent from ..agents.falsification import FalsificationAgent
from ..agents.hypothesis import HypothesisAgent from ..agents.hypothesis import HypothesisAgent, HypothesisProposal, MarketSummary
from ..agents.market_summary import build_market_summary from ..agents.market_summary import build_market_summary
from ..ga.fitness import compute_fitness from ..ga.fitness import compute_fitness
from ..ga.initial import build_initial_population from ..ga.initial import build_initial_population
from ..ga.loop import GAConfig, next_generation from ..ga.loop import GAConfig, next_generation
from ..ga.summary import generation_summary from ..ga.summary import generation_summary
from ..genome.hypothesis import ModelTier from ..genome.hypothesis import HypothesisAgentGenome, ModelTier
from ..genome.mutation import set_cognitive_styles
from ..genome.prompt_library import PromptLibrary
from ..llm.client import LLMClient from ..llm.client import LLMClient
from ..llm.cost_tracker import CostTracker from ..llm.cost_tracker import CostTracker
from ..persistence.repository import Repository from ..persistence.repository import Repository
@@ -67,6 +70,33 @@ class RunConfig:
# 2x costo backtest engine. # 2x costo backtest engine.
eval_oos_during_loop: bool = False eval_oos_during_loop: bool = False
fitness_combined_alpha: float = 0.5 # peso IS (1-alpha = OOS) fitness_combined_alpha: float = 0.5 # peso IS (1-alpha = OOS)
# Libreria di stili cognitivi + system_prompt iniziali. Se None usa
# i 6 builtin (PromptLibrary.default()). Tipicamente caricata da
# strategy_crypto/prompts.json via PromptLibrary.from_json().
prompt_library: PromptLibrary | None = None
# Numero di propose() LLM concorrenti per generazione. 1 = sequenziale (default,
# backward compat). 6-10 tipicamente accettati da OpenRouter qwen-2.5 senza
# rate-limit. Riduce wall time GA loop di 5-8x su tier C.
llm_concurrency: int = 1
def _parallel_propose(
agent: HypothesisAgent,
genomes: list[HypothesisAgentGenome],
market: MarketSummary,
n_workers: int,
) -> list[HypothesisProposal]:
"""Esegue ``agent.propose()`` su una lista di genomi, opzionalmente in parallelo.
``n_workers <= 1`` mantiene il comportamento serial originale (ordine fisso,
determinismo data un seed). ``n_workers > 1`` usa un thread pool: l'order
dei risultati e' preservato (1:1 con ``genomes``). OpenAI/openrouter client
e' thread-safe; ``PromptLibrary`` e ``HypothesisAgent`` non hanno stato mutabile.
"""
if n_workers <= 1 or len(genomes) <= 1:
return [agent.propose(g, market) for g in genomes]
with ThreadPoolExecutor(max_workers=n_workers) as pool:
return list(pool.map(lambda g: agent.propose(g, market), genomes))
def run_phase1( def run_phase1(
@@ -82,10 +112,16 @@ def run_phase1(
repo = Repository(cfg.db_path) repo = Repository(cfg.db_path)
repo.init_schema() repo.init_schema()
# Escludi prompt_library (PromptLibrary dataclass non e' JSON-serializable);
# salva solo i nomi degli stili per reproducibility.
config_dict = { config_dict = {
**cfg.__dict__, **{k: v for k, v in cfg.__dict__.items() if k != "prompt_library"},
"db_path": str(cfg.db_path), "db_path": str(cfg.db_path),
"model_tier": cfg.model_tier.value, "model_tier": cfg.model_tier.value,
"prompt_library_styles": (
list(cfg.prompt_library.cognitive_styles)
if cfg.prompt_library is not None else None
),
} }
run_id = repo.create_run(name=cfg.run_name, config=config_dict) run_id = repo.create_run(name=cfg.run_name, config=config_dict)
@@ -100,7 +136,13 @@ def run_phase1(
market = build_market_summary(train_ohlcv, symbol=cfg.symbol, timeframe=cfg.timeframe) market = build_market_summary(train_ohlcv, symbol=cfg.symbol, timeframe=cfg.timeframe)
hypothesis_agent = HypothesisAgent(llm=llm) # Propaga la libreria di stili al modulo mutation (cosi' mutate_cognitive_style
# pesca dai candidati coerenti col JSON della strategia in corso). Va FATTO
# PRIMA di istanziare HypothesisAgent (che la riceve in costruttore).
prompt_library = cfg.prompt_library or PromptLibrary.default()
set_cognitive_styles(prompt_library.cognitive_styles)
hypothesis_agent = HypothesisAgent(llm=llm, prompt_library=prompt_library)
falsification_agent = FalsificationAgent( falsification_agent = FalsificationAgent(
fees_bp=cfg.fees_bp, n_trials_dsr=cfg.n_trials_dsr fees_bp=cfg.fees_bp, n_trials_dsr=cfg.n_trials_dsr
) )
@@ -113,7 +155,10 @@ def run_phase1(
cost_tracker = CostTracker() cost_tracker = CostTracker()
population = build_initial_population( population = build_initial_population(
k=cfg.population_size, model_tier=cfg.model_tier, rng=rng k=cfg.population_size,
model_tier=cfg.model_tier,
rng=rng,
prompt_library=prompt_library,
) )
fitnesses: dict[str, float] = {} fitnesses: dict[str, float] = {}
@@ -127,11 +172,20 @@ def run_phase1(
try: try:
for gen in range(cfg.n_generations): for gen in range(cfg.n_generations):
# Step 1: raccogli i genomi da valutare in questa generazione (esclude
# elite gia' presenti nella cache fitnesses) e lancia propose() in
# parallelo. La sezione DB-write resta serial sotto.
uncached = [g for g in population if g.id not in fitnesses]
proposals = _parallel_propose(
hypothesis_agent, uncached, market, cfg.llm_concurrency
)
proposal_by_id = {g.id: p for g, p in zip(uncached, proposals, strict=True)}
for genome in population: for genome in population:
if genome.id in fitnesses: if genome.id in fitnesses:
continue # elite gia' valutata in generazione precedente continue # elite gia' valutata in generazione precedente
repo.save_genome(run_id=run_id, generation_idx=gen, genome=genome) repo.save_genome(run_id=run_id, generation_idx=gen, genome=genome)
proposal = hypothesis_agent.propose(genome, market) proposal = proposal_by_id[genome.id]
# Registra costo per OGNI completion (incluse retry). # Registra costo per OGNI completion (incluse retry).
for completion in proposal.completions: for completion in proposal.completions:
cost_record = cost_tracker.record( cost_record = cost_tracker.record(
@@ -205,7 +259,7 @@ def run_phase1(
cfg.fitness_combined_alpha * fit cfg.fitness_combined_alpha * fit
+ (1.0 - cfg.fitness_combined_alpha) * fit_oos_inloop + (1.0 - cfg.fitness_combined_alpha) * fit_oos_inloop
) )
except Exception: # noqa: BLE001 except Exception:
pass # fallback: usa solo IS pass # fallback: usa solo IS
repo.save_evaluation( repo.save_evaluation(
run_id=run_id, run_id=run_id,
@@ -246,7 +300,7 @@ def run_phase1(
# WFA re-eval: i top_k genomi (by fitness in-sample > 0) vengono rivalutati # WFA re-eval: i top_k genomi (by fitness in-sample > 0) vengono rivalutati
# sul test_ohlcv. Le metriche OOS finiscono in evaluations.fitness_oos etc. # sul test_ohlcv. Le metriche OOS finiscono in evaluations.fitness_oos etc.
if test_ohlcv is not None and len(test_ohlcv) >= 100: if test_ohlcv is not None and len(test_ohlcv) >= 100:
from ..agents.hypothesis import _try_parse # noqa: PLC0415 from ..agents.hypothesis import _try_parse
all_evals = repo.list_evaluations(run_id) all_evals = repo.list_evaluations(run_id)
top_evals = sorted( top_evals = sorted(
@@ -261,7 +315,7 @@ def run_phase1(
try: try:
fals_oos = falsification_agent.evaluate(strategy, test_ohlcv) fals_oos = falsification_agent.evaluate(strategy, test_ohlcv)
adv_oos = adversarial_agent.review(strategy, test_ohlcv) adv_oos = adversarial_agent.review(strategy, test_ohlcv)
except Exception: # noqa: BLE001 except Exception:
continue continue
fit_oos = compute_fitness( fit_oos = compute_fitness(
fals_oos, adv_oos, fals_oos, adv_oos,
@@ -88,6 +88,29 @@ def _ind_atr_pct(df: pd.DataFrame, length: float) -> pd.Series:
return _atr(df, int(length)) / df["close"] return _atr(df, int(length)) / df["close"]
def _ind_sma_pct(df: pd.DataFrame, length: float) -> pd.Series:
# Deviazione frazionale del close dalla SMA: (close - sma) / sma.
# Range tipico +/- 0.1 (close +/- 10% dalla media). Uso ideale:
# sma_pct(50) > 0.05 -> "close 5% sopra la media a 50 barre"
# NB: non e' "sma/close" perche' quel valore (sempre ~1.0) e' inutile
# per confronti con literal frazionali.
sma = _sma(df["close"], int(length))
return (df["close"] - sma) / sma
def _ind_macd_pct(
df: pd.DataFrame,
fast: float = 12,
slow: float = 26,
signal: float = 9,
) -> pd.Series:
# MACD normalizzato come frazione del prezzo close: macd_value / close.
# Range tipico +/- 0.02. Uso: `macd_pct > 0` (momentum positivo) o
# `macd_pct > 0.005` (momentum positivo >= 0.5% del prezzo).
macd = _ind_macd(df, fast, slow, signal)
return macd / df["close"]
def _ind_realized_vol(df: pd.DataFrame, window: float) -> pd.Series: def _ind_realized_vol(df: pd.DataFrame, window: float) -> pd.Series:
return _realized_vol(df["close"], int(window)) return _realized_vol(df["close"], int(window))
@@ -109,11 +132,13 @@ def _ind_macd(
# against this map. # against this map.
INDICATOR_FNS: dict[str, Any] = { INDICATOR_FNS: dict[str, Any] = {
"sma": _ind_sma, "sma": _ind_sma,
"sma_pct": _ind_sma_pct,
"rsi": _ind_rsi, "rsi": _ind_rsi,
"atr": _ind_atr, "atr": _ind_atr,
"atr_pct": _ind_atr_pct, "atr_pct": _ind_atr_pct,
"realized_vol": _ind_realized_vol, "realized_vol": _ind_realized_vol,
"macd": _ind_macd, "macd": _ind_macd,
"macd_pct": _ind_macd_pct,
} }
_TIME_FEATURE_FNS: dict[str, Callable[[pd.DatetimeIndex], pd.Series]] = { _TIME_FEATURE_FNS: dict[str, Callable[[pd.DatetimeIndex], pd.Series]] = {
@@ -17,7 +17,7 @@ ACTION_VALUES: frozenset[str] = frozenset(
KIND_VALUES: frozenset[str] = frozenset({"indicator", "feature", "literal"}) KIND_VALUES: frozenset[str] = frozenset({"indicator", "feature", "literal"})
KNOWN_INDICATORS: frozenset[str] = frozenset( KNOWN_INDICATORS: frozenset[str] = frozenset(
{"sma", "rsi", "atr", "atr_pct", "macd", "realized_vol"} {"sma", "sma_pct", "rsi", "atr", "atr_pct", "macd", "macd_pct", "realized_vol"}
) )
KNOWN_FEATURES: frozenset[str] = frozenset( KNOWN_FEATURES: frozenset[str] = frozenset(
{"open", "high", "low", "close", "volume", {"open", "high", "low", "close", "volume",
@@ -31,12 +31,14 @@ from .parser import (
# Numero di parametri numerici accettati dopo il nome dell'indicatore. # Numero di parametri numerici accettati dopo il nome dell'indicatore.
# (min, max) sui soli numeri. Indicatori non sono annidabili in Phase 1. # (min, max) sui soli numeri. Indicatori non sono annidabili in Phase 1.
INDICATOR_ARITY: dict[str, tuple[int, int]] = { INDICATOR_ARITY: dict[str, tuple[int, int]] = {
"sma": (1, 1), # length "sma": (1, 1), # length (assoluto, unita' prezzo)
"sma_pct": (1, 1), # length: (close - sma)/sma, deviazione frazionale
"rsi": (1, 1), # length "rsi": (1, 1), # length
"atr": (1, 1), # length (assoluto, unita' prezzo) "atr": (1, 1), # length (assoluto, unita' prezzo)
"atr_pct": (1, 1), # length (frazione del close, per confronti con literal) "atr_pct": (1, 1), # length (frazione del close, per confronti con literal)
"realized_vol": (1, 1), # window "realized_vol": (1, 1), # window
"macd": (0, 3), # fast, slow, signal (tutti opzionali) "macd": (0, 3), # fast, slow, signal (tutti opzionali)
"macd_pct": (0, 3), # macd/close, frazionale (per confronti con literal)
} }
+2
View File
@@ -18,6 +18,8 @@ dependencies = [
"pyyaml>=6.0", "pyyaml>=6.0",
"pyarrow>=18.0", "pyarrow>=18.0",
"yfinance>=1.3.0", "yfinance>=1.3.0",
"nicegui>=3.11.1",
"plotly>=5.24",
] ]
[build-system] [build-system]
@@ -108,7 +108,13 @@ def test_e2e_wfa_populates_fitness_oos(
fake_llm, fake_llm,
mocker, mocker,
): ):
"""WFA: train_split=0.7 → top genomi devono avere fitness_oos popolato.""" """WFA: train_split=0.7 → top genomi devono avere fitness_oos popolato.
Usa fitness v2 con hard-kill minimale (solo no_trades): il fixture sintetico
non produce strategie profittevoli, quindi i check aggressivi
fees_eat_alpha/negative_net_pnl azzererebbero tutti i genomi rendendo
inverificabile il wiring WFA.
"""
cfg = RunConfig( cfg = RunConfig(
run_name="e2e-wfa-test", run_name="e2e-wfa-test",
population_size=5, population_size=5,
@@ -125,6 +131,7 @@ def test_e2e_wfa_populates_fitness_oos(
db_path=tmp_path / "runs.db", db_path=tmp_path / "runs.db",
wfa_train_split=0.7, wfa_train_split=0.7,
wfa_top_k=3, wfa_top_k=3,
fitness_hard_kill_findings=("no_trades",),
) )
run_id = run_phase1(cfg, ohlcv=synthetic_ohlcv, llm=fake_llm) run_id = run_phase1(cfg, ohlcv=synthetic_ohlcv, llm=fake_llm)
repo = Repository(db_path=tmp_path / "runs.db") repo = Repository(db_path=tmp_path / "runs.db")
@@ -3,7 +3,6 @@ import json
import numpy as np import numpy as np
import pandas as pd import pandas as pd
import pytest import pytest
from multi_swarm_core.agents.adversarial import ( from multi_swarm_core.agents.adversarial import (
AdversarialAgent, AdversarialAgent,
AdversarialReport, AdversarialReport,
@@ -54,7 +53,10 @@ def test_degenerate_always_long_flagged(ohlcv: pd.DataFrame) -> None:
assert any(f.name == "degenerate" and f.severity == Severity.HIGH for f in report.findings) assert any(f.name == "degenerate" and f.severity == Severity.HIGH for f in report.findings)
def test_no_findings_on_reasonable_strategy(ohlcv: pd.DataFrame) -> None: def test_rsi_mean_reversion_loses_money_on_synthetic_data(ohlcv: pd.DataFrame) -> None:
"""RSI mean-reversion sul fixture sintetico ha net negativo: deve firare
negative_net_pnl (deal-breaker). Conferma che il check cattura strategie
che perdono sul training, indipendentemente dal motivo (no edge / fees)."""
src = json.dumps( src = json.dumps(
{ {
"rules": [ "rules": [
@@ -84,8 +86,59 @@ def test_no_findings_on_reasonable_strategy(ohlcv: pd.DataFrame) -> None:
ast = parse_strategy(src) ast = parse_strategy(src)
agent = AdversarialAgent() agent = AdversarialAgent()
report = agent.review(ast, ohlcv) report = agent.review(ast, ohlcv)
assert any(
f.name == "negative_net_pnl" and f.severity == Severity.HIGH
for f in report.findings
)
def test_profitable_strategy_no_high_findings(
monkeypatch: pytest.MonkeyPatch, ohlcv: pd.DataFrame
) -> None:
"""Sanity test: una strategia con gross > 0 e fees << gross + n_trades ragionevole
+ signal misto non deve produrre nessun finding HIGH."""
n = 15
# entry=100 exit=110 gross=10 per trade, fees a 5bp -> 0.105 per trade
# totali: gross=150, fees=1.575 -> net=+148.4
fake_trades = [
_make_trade(
ohlcv.index[i * 30],
ohlcv.index[i * 30 + 1],
entry_price=100.0,
exit_price=110.0,
)
for i in range(n)
]
# 50/50 LONG/FLAT per evitare degenerate/flat_too_long/time_in_market.
fake_signals = pd.Series(
[Side.LONG if i % 2 == 0 else Side.FLAT for i in range(len(ohlcv))],
index=ohlcv.index,
dtype=object,
)
def fake_run(self, ohlcv: pd.DataFrame, signals: pd.Series) -> BacktestResult: # type: ignore[no-untyped-def]
return BacktestResult(
equity_curve=pd.Series([0.0] * len(ohlcv), index=ohlcv.index, name="equity"),
returns=pd.Series([0.0] * len(ohlcv), index=ohlcv.index, name="returns"),
trades=fake_trades,
)
def fake_compile(strategy): # type: ignore[no-untyped-def]
return lambda df: fake_signals
monkeypatch.setattr(
"multi_swarm_core.agents.adversarial.BacktestEngine.run", fake_run
)
monkeypatch.setattr(
"multi_swarm_core.agents.adversarial.compile_strategy", fake_compile
)
ast = parse_strategy(_MINIMAL_STRATEGY_SRC)
report = AdversarialAgent().review(ast, ohlcv)
high_findings = [f for f in report.findings if f.severity == Severity.HIGH] high_findings = [f for f in report.findings if f.severity == Severity.HIGH]
assert len(high_findings) == 0 assert high_findings == [], (
f"expected no HIGH findings, got: {[f.name for f in high_findings]}"
)
def test_zero_trade_strategy_flagged(ohlcv: pd.DataFrame) -> None: def test_zero_trade_strategy_flagged(ohlcv: pd.DataFrame) -> None:
@@ -383,6 +436,55 @@ def test_fees_eat_alpha_flagged(monkeypatch: pytest.MonkeyPatch,
) )
def test_negative_net_pnl_fires_on_negative_gross(
monkeypatch: pytest.MonkeyPatch, ohlcv: pd.DataFrame
) -> None:
"""gross_pnl < 0 (perdente direzionale) -> HIGH negative_net_pnl.
fees_eat_alpha NON deve firare perche' la sua condizione richiede gross > 0.
"""
n = 15
# entry=100 exit=95 gross=-5 per trade (LONG perdente)
fake_trades = [
_make_trade(
ohlcv.index[i * 30],
ohlcv.index[i * 30 + 1],
entry_price=100.0,
exit_price=95.0,
)
for i in range(n)
]
fake_signals = pd.Series(
[Side.LONG if i % 2 == 0 else Side.FLAT for i in range(len(ohlcv))],
index=ohlcv.index,
dtype=object,
)
def fake_run(self, ohlcv, signals): # type: ignore[no-untyped-def]
return BacktestResult(
equity_curve=pd.Series([0.0] * len(ohlcv), index=ohlcv.index, name="equity"),
returns=pd.Series([0.0] * len(ohlcv), index=ohlcv.index, name="returns"),
trades=fake_trades,
)
def fake_compile(strategy): # type: ignore[no-untyped-def]
return lambda df: fake_signals
monkeypatch.setattr(
"multi_swarm_core.agents.adversarial.BacktestEngine.run", fake_run
)
monkeypatch.setattr(
"multi_swarm_core.agents.adversarial.compile_strategy", fake_compile
)
ast = parse_strategy(_MINIMAL_STRATEGY_SRC)
report = AdversarialAgent().review(ast, ohlcv)
assert any(
f.name == "negative_net_pnl" and f.severity == Severity.HIGH
for f in report.findings
)
assert not any(f.name == "fees_eat_alpha" for f in report.findings)
def test_time_in_market_too_high_flagged(monkeypatch: pytest.MonkeyPatch, def test_time_in_market_too_high_flagged(monkeypatch: pytest.MonkeyPatch,
ohlcv: pd.DataFrame) -> None: ohlcv: pd.DataFrame) -> None:
"""Signal LONG per >80% delle bar -> HIGH time_in_market_too_high.""" """Signal LONG per >80% delle bar -> HIGH time_in_market_too_high."""
@@ -0,0 +1,160 @@
"""Parity check: engine vettorializzato vs reference iterrows implementation.
Mantiene una copia inline del loop ``iterrows`` come reference per garantire
che la vettorizzazione produca esattamente gli stessi trades, equity_curve e
returns su input pseudocasuali, indipendentemente dal regime di prezzo.
"""
from __future__ import annotations
import numpy as np
import pandas as pd
import pytest
from multi_swarm_core.backtest.engine import BacktestEngine, BacktestResult
from multi_swarm_core.backtest.orders import Position, Side, Trade
def _legacy_run(
ohlcv: pd.DataFrame, signals: pd.Series, fees_bp: float = 5.0
) -> BacktestResult:
"""Reference implementation: il loop iterrows originale (pre-vectorize).
Mantenuto qui esclusivamente come oracolo per i test di parità.
"""
signals = signals.reindex(ohlcv.index).ffill().fillna(Side.FLAT)
shifted = [Side.FLAT, *list(signals.iloc[:-1])]
executed_side = pd.Series(shifted, index=ohlcv.index, dtype=object)
position: Position | None = None
position_entry_ts: pd.Timestamp | None = None
trades: list[Trade] = []
equity = 0.0
equity_history: list[float] = []
returns_history: list[float] = []
prev_equity = 0.0
for ts, row in ohlcv.iterrows():
target_side = executed_side.loc[ts]
current_side = position.side if position else Side.FLAT
if target_side != current_side:
if position is not None:
assert position_entry_ts is not None
trade = Trade(
entry_ts=position_entry_ts,
exit_ts=ts,
side=position.side,
size=position.size,
entry_price=position.entry_price,
exit_price=float(row["open"]),
fees_bp=fees_bp,
)
trades.append(trade)
equity += trade.net_pnl
position = None
position_entry_ts = None
if target_side in (Side.LONG, Side.SHORT):
position = Position(
side=target_side, entry_price=float(row["open"]), size=1.0
)
position_entry_ts = ts
mark = float(row["close"])
mtm = position.unrealized_pnl(mark) if position else 0.0
current_equity = equity + mtm
equity_history.append(current_equity)
returns_history.append(current_equity - prev_equity)
prev_equity = current_equity
if position is not None:
assert position_entry_ts is not None
last_ts = ohlcv.index[-1]
last_close = float(ohlcv["close"].iloc[-1])
trade = Trade(
entry_ts=position_entry_ts,
exit_ts=last_ts,
side=position.side,
size=position.size,
entry_price=position.entry_price,
exit_price=last_close,
fees_bp=fees_bp,
)
trades.append(trade)
equity += trade.net_pnl
equity_history[-1] = equity
if len(returns_history) >= 2:
returns_history[-1] = equity - equity_history[-2]
return BacktestResult(
equity_curve=pd.Series(equity_history, index=ohlcv.index, name="equity"),
returns=pd.Series(returns_history, index=ohlcv.index, name="returns"),
trades=trades,
)
def _random_ohlcv(n: int, seed: int) -> pd.DataFrame:
rng = np.random.default_rng(seed)
rets = rng.normal(0.0, 0.01, size=n)
close = 100.0 * np.exp(np.cumsum(rets))
idx = pd.date_range("2024-01-01", periods=n, freq="1h", tz="UTC")
return pd.DataFrame(
{
"open": close * (1 + rng.normal(0, 0.001, n)),
"high": close * 1.005,
"low": close * 0.995,
"close": close,
"volume": rng.uniform(1.0, 100.0, n),
},
index=idx,
)
def _random_signals(n: int, seed: int, p_change: float = 0.1) -> pd.Series:
"""Segnali con persistenza: ad ogni bar con prob p_change cambia stato."""
rng = np.random.default_rng(seed + 999)
states = [Side.LONG, Side.SHORT, Side.FLAT]
out: list[Side] = [rng.choice(states)]
for _ in range(1, n):
out.append(rng.choice(states) if rng.random() < p_change else out[-1])
idx = pd.date_range("2024-01-01", periods=n, freq="1h", tz="UTC")
return pd.Series(out, index=idx, dtype=object)
@pytest.mark.parametrize("seed", [0, 1, 42, 123, 999])
def test_vectorized_equals_legacy(seed: int) -> None:
df = _random_ohlcv(500, seed)
signals = _random_signals(500, seed)
engine = BacktestEngine(fees_bp=5.0)
new = engine.run(df, signals)
ref = _legacy_run(df, signals, fees_bp=5.0)
pd.testing.assert_series_equal(
new.equity_curve, ref.equity_curve, rtol=1e-9, atol=1e-9
)
pd.testing.assert_series_equal(
new.returns, ref.returns, rtol=1e-9, atol=1e-9
)
assert len(new.trades) == len(ref.trades)
for nt, rt in zip(new.trades, ref.trades, strict=True):
assert nt.entry_ts == rt.entry_ts
assert nt.exit_ts == rt.exit_ts
assert nt.side == rt.side
assert nt.entry_price == pytest.approx(rt.entry_price, abs=1e-12)
assert nt.exit_price == pytest.approx(rt.exit_price, abs=1e-12)
assert nt.net_pnl == pytest.approx(rt.net_pnl, abs=1e-12)
def test_vectorized_handles_position_still_open_at_end() -> None:
"""Edge case: signal LONG fino all'ultimo bar (exit a close[-1] forced)."""
df = _random_ohlcv(100, seed=7)
signals = pd.Series([Side.LONG] * 100, index=df.index)
new = BacktestEngine(fees_bp=10.0).run(df, signals)
ref = _legacy_run(df, signals, fees_bp=10.0)
pd.testing.assert_series_equal(new.equity_curve, ref.equity_curve, atol=1e-9)
assert len(new.trades) == 1
assert new.trades[0].side == Side.LONG
def test_vectorized_zero_signals_no_trades() -> None:
df = _random_ohlcv(50, seed=3)
signals = pd.Series([Side.FLAT] * 50, index=df.index)
new = BacktestEngine().run(df, signals)
assert len(new.trades) == 0
assert (new.equity_curve == 0).all()
@@ -2,6 +2,7 @@ import json
from multi_swarm_core.agents.hypothesis import HypothesisAgent, MarketSummary from multi_swarm_core.agents.hypothesis import HypothesisAgent, MarketSummary
from multi_swarm_core.genome.hypothesis import HypothesisAgentGenome, ModelTier from multi_swarm_core.genome.hypothesis import HypothesisAgentGenome, ModelTier
from multi_swarm_core.genome.prompt_library import PromptLibrary
from multi_swarm_core.llm.client import CompletionResult, EmptyCompletionError from multi_swarm_core.llm.client import CompletionResult, EmptyCompletionError
@@ -248,3 +249,174 @@ def test_hypothesis_agent_returns_failed_proposal_on_only_empty_completions(mock
assert "empty_completion" in proposal.parse_error assert "empty_completion" in proposal.parse_error
# 3 tentativi tutti falliti. # 3 tentativi tutti falliti.
assert fake_llm.complete.call_count == 3 assert fake_llm.complete.call_count == 3
def test_propose_renders_focus_block_for_style(mocker): # type: ignore[no-untyped-def]
"""Con PromptLibrary che ha focus_metrics, il LLM mock riceve 'Focus per la tua lente' nel user message."""
fake_llm = mocker.MagicMock()
fake_llm.complete.return_value = CompletionResult(
text=VALID_STRATEGY_JSON,
input_tokens=200,
output_tokens=80,
tier=ModelTier.C,
model="qwen",
)
# PromptLibrary con focus_metrics per physicist
lib = PromptLibrary(
styles={"physicist": "Pensa come un fisico."},
focus={"physicist": ["efficiency_ratio", "spectral_entropy", "hurst"]},
)
agent = HypothesisAgent(llm=fake_llm, prompt_library=lib)
proposal = agent.propose(make_genome(), make_summary())
assert proposal.strategy is not None
# Verifica che il user message contenga il focus block
call_kwargs = fake_llm.complete.call_args.kwargs
user_msg = call_kwargs["user"]
assert "Focus per la tua lente:" in user_msg
assert "efficiency_ratio=" in user_msg
assert "spectral_entropy=" in user_msg
assert "hurst=" in user_msg
def test_propose_no_focus_block_when_style_not_in_library(mocker): # type: ignore[no-untyped-def]
"""Stile senza focus_metrics → nessun focus block nel user message."""
fake_llm = mocker.MagicMock()
fake_llm.complete.return_value = CompletionResult(
text=VALID_STRATEGY_JSON,
input_tokens=200,
output_tokens=80,
tier=ModelTier.C,
model="qwen",
)
# PromptLibrary senza focus_metrics per physicist
lib = PromptLibrary(
styles={"physicist": "Pensa come un fisico."},
focus={},
)
agent = HypothesisAgent(llm=fake_llm, prompt_library=lib)
proposal = agent.propose(make_genome(), make_summary())
assert proposal.strategy is not None
call_kwargs = fake_llm.complete.call_args.kwargs
user_msg = call_kwargs["user"]
assert "Focus per la tua lente:" not in user_msg
def test_build_system_prompt_includes_role_and_guidance_from_library(mocker): # type: ignore[no-untyped-def]
"""agent_role e pattern_guidance da PromptLibrary appaiono nel SYSTEM prompt."""
fake_llm = mocker.MagicMock()
fake_llm.complete.return_value = CompletionResult(
text=VALID_STRATEGY_JSON,
input_tokens=200,
output_tokens=80,
tier=ModelTier.C,
model="qwen",
)
lib = PromptLibrary(
styles={"physicist": "Pensa come un fisico."},
focus={},
agent_role="ROLE_X",
pattern_guidance="GUIDE_X",
)
agent = HypothesisAgent(llm=fake_llm, prompt_library=lib)
agent.propose(make_genome(), make_summary())
call_kwargs = fake_llm.complete.call_args.kwargs
system_msg = call_kwargs["system"]
assert "ROLE_X" in system_msg
assert "GUIDE_X" in system_msg
def test_build_system_prompt_skips_empty_optional_sections(mocker): # type: ignore[no-untyped-def]
"""domain_warnings='' e pattern_guidance='' → sezioni opzionali assenti nel SYSTEM."""
fake_llm = mocker.MagicMock()
fake_llm.complete.return_value = CompletionResult(
text=VALID_STRATEGY_JSON,
input_tokens=200,
output_tokens=80,
tier=ModelTier.C,
model="qwen",
)
lib = PromptLibrary(
styles={"physicist": "Pensa come un fisico."},
focus={},
domain_warnings="",
pattern_guidance="",
)
agent = HypothesisAgent(llm=fake_llm, prompt_library=lib)
agent.propose(make_genome(), make_summary())
call_kwargs = fake_llm.complete.call_args.kwargs
system_msg = call_kwargs["system"]
assert "WARNING DI DOMINIO" not in system_msg
assert "PATTERN GUIDANCE" not in system_msg
def test_user_template_uses_instruction_from_library(mocker): # type: ignore[no-untyped-def]
"""instruction da PromptLibrary appare nel USER message; default non viene usato."""
fake_llm = mocker.MagicMock()
fake_llm.complete.return_value = CompletionResult(
text=VALID_STRATEGY_JSON,
input_tokens=200,
output_tokens=80,
tier=ModelTier.C,
model="qwen",
)
lib = PromptLibrary(
styles={"physicist": "Pensa come un fisico."},
focus={},
instruction="INSTR_X",
)
agent = HypothesisAgent(llm=fake_llm, prompt_library=lib)
agent.propose(make_genome(), make_summary())
call_kwargs = fake_llm.complete.call_args.kwargs
user_msg = call_kwargs["user"]
assert "INSTR_X" in user_msg
assert "Genera una strategia che cerchi anomalie sfruttabili in questo regime." not in user_msg
def test_build_system_prompt_includes_anti_patterns_and_priorities(mocker): # type: ignore[no-untyped-def]
"""anti_patterns e output_priorities da PromptLibrary appaiono nel SYSTEM prompt."""
fake_llm = mocker.MagicMock()
fake_llm.complete.return_value = CompletionResult(
text=VALID_STRATEGY_JSON,
input_tokens=200,
output_tokens=80,
tier=ModelTier.C,
model="qwen",
)
lib = PromptLibrary(
styles={"physicist": "Pensa come un fisico."},
focus={},
anti_patterns="EVITA_X",
output_priorities="PRIORI_X",
)
agent = HypothesisAgent(llm=fake_llm, prompt_library=lib)
agent.propose(make_genome(), make_summary())
call_kwargs = fake_llm.complete.call_args.kwargs
system_msg = call_kwargs["system"]
assert "ANTI-PATTERN DA EVITARE" in system_msg
assert "EVITA_X" in system_msg
assert "PRIORITA' DI OUTPUT" in system_msg
assert "PRIORI_X" in system_msg
def test_build_system_prompt_skips_anti_patterns_and_priorities_when_empty(mocker): # type: ignore[no-untyped-def]
"""anti_patterns='' e output_priorities='' -> sezioni opzionali assenti nel SYSTEM."""
fake_llm = mocker.MagicMock()
fake_llm.complete.return_value = CompletionResult(
text=VALID_STRATEGY_JSON,
input_tokens=200,
output_tokens=80,
tier=ModelTier.C,
model="qwen",
)
lib = PromptLibrary(
styles={"physicist": "Pensa come un fisico."},
focus={},
anti_patterns="",
output_priorities="",
)
agent = HypothesisAgent(llm=fake_llm, prompt_library=lib)
agent.propose(make_genome(), make_summary())
call_kwargs = fake_llm.complete.call_args.kwargs
system_msg = call_kwargs["system"]
assert "ANTI-PATTERN" not in system_msg
assert "PRIORITA' DI OUTPUT" not in system_msg
@@ -31,3 +31,83 @@ def test_volatility_regime_high_for_volatile() -> None:
) )
s = build_market_summary(df, symbol="BTC/USDT", timeframe="1h") s = build_market_summary(df, symbol="BTC/USDT", timeframe="1h")
assert s.volatility_regime in {"medium", "high"} assert s.volatility_regime in {"medium", "high"}
def test_build_summary_new_fields_populated() -> None:
"""I 6 nuovi campi devono essere float nei range attesi."""
idx = pd.date_range("2024-01-01", periods=600, freq="1h", tz="UTC")
np.random.seed(42)
close = 100 + np.cumsum(np.random.normal(0, 1, 600))
df = pd.DataFrame(
{"open": close, "high": close + 0.5, "low": close - 0.5, "close": close, "volume": 1.0},
index=idx,
)
s = build_market_summary(df, symbol="ETH/USDT", timeframe="1h")
# autocorr fields: float in [-1, 1]
assert isinstance(s.autocorr_lag1_recent, float)
assert isinstance(s.autocorr_lag1_baseline, float)
assert -1.0 <= s.autocorr_lag1_recent <= 1.0
assert -1.0 <= s.autocorr_lag1_baseline <= 1.0
# hurst: float in [0, 1]
assert isinstance(s.hurst_recent, float)
assert 0.0 <= s.hurst_recent <= 1.0
# vol_percentile: float in [0, 100]
assert isinstance(s.vol_percentile, float)
assert 0.0 <= s.vol_percentile <= 100.0
# seasonality fields: float in [0, 1]
assert isinstance(s.seasonality_hour, float)
assert isinstance(s.seasonality_dow, float)
assert 0.0 <= s.seasonality_hour <= 1.0
assert 0.0 <= s.seasonality_dow <= 1.0
def test_build_summary_geometric_fields_populated() -> None:
"""I 7 nuovi campi geometrico-frattali devono essere non-default e nei range attesi."""
idx = pd.date_range("2024-01-01", periods=600, freq="1h", tz="UTC")
np.random.seed(5)
close = 100 + np.cumsum(np.random.normal(0, 1, 600))
df = pd.DataFrame(
{"open": close, "high": close + 0.5, "low": close - 0.5, "close": close, "volume": 1.0},
index=idx,
)
s = build_market_summary(df, symbol="BTC/USDT", timeframe="1h")
# efficiency_ratio: float in [0, 1]
assert isinstance(s.efficiency_ratio, float)
assert 0.0 <= s.efficiency_ratio <= 1.0
# tail_index_left/right: float in [1, 10]
assert isinstance(s.tail_index_left, float)
assert isinstance(s.tail_index_right, float)
assert 1.0 <= s.tail_index_left <= 10.0
assert 1.0 <= s.tail_index_right <= 10.0
# structural_uptrend: float in [0, 1]
assert isinstance(s.structural_uptrend, float)
assert 0.0 <= s.structural_uptrend <= 1.0
# compression: float > 0
assert isinstance(s.compression, float)
assert s.compression > 0.0
# spectral_entropy: float in [0, 1]
assert isinstance(s.spectral_entropy, float)
assert 0.0 <= s.spectral_entropy <= 1.0
# dominant_cycle: None or positive float
assert s.dominant_cycle is None or (isinstance(s.dominant_cycle, float) and s.dominant_cycle > 0)
# Verifica che i campi siano stati popolati (non tutti ai valori default)
defaults_only = (
s.efficiency_ratio == 0.0
and s.tail_index_left == 5.0
and s.tail_index_right == 5.0
and s.structural_uptrend == 0.5
and s.compression == 1.0
and s.spectral_entropy == 1.0
)
assert not defaults_only, "Tutti i campi geometrici sono rimasti ai valori default: calcolo non avvenuto"
@@ -2,7 +2,20 @@ import numpy as np
import pandas as pd import pandas as pd
import pytest import pytest
from multi_swarm_core.metrics.basic import max_drawdown, sharpe_ratio, total_return from multi_swarm_core.metrics.basic import (
autocorr_lag1,
compression_ratio,
efficiency_ratio_kaufman,
hurst_exponent,
max_drawdown,
seasonality_strength,
sharpe_ratio,
spectral_entropy_and_cycle,
structural_uptrend_score,
tail_index_hill,
total_return,
vol_percentile_historical,
)
def test_sharpe_zero_returns(): def test_sharpe_zero_returns():
@@ -38,3 +51,240 @@ def test_max_drawdown_known_curve():
def test_total_return(): def test_total_return():
eq = pd.Series([100.0, 110.0, 105.0, 120.0]) eq = pd.Series([100.0, 110.0, 105.0, 120.0])
assert total_return(eq) == pytest.approx(0.20) assert total_return(eq) == pytest.approx(0.20)
# --- New metrics tests ---
def test_autocorr_lag1_short_series():
"""Serie corta ritorna 0.0 senza errori."""
r = pd.Series([0.01])
assert autocorr_lag1(r) == 0.0
def test_autocorr_lag1_all_nan():
"""Serie di NaN ritorna 0.0."""
r = pd.Series([float("nan")] * 10)
assert autocorr_lag1(r) == 0.0
def test_autocorr_lag1_range():
"""Autocorrelazione sempre in [-1, 1]."""
np.random.seed(7)
r = pd.Series(np.random.normal(0, 0.01, 500))
val = autocorr_lag1(r)
assert -1.0 <= val <= 1.0
def test_autocorr_lag1_trending_positive():
"""Serie con autocorrelazione positiva artificiale deve dare val > 0."""
# costruiamo una serie con forte AR(1): x_t = 0.8*x_{t-1} + noise
np.random.seed(1)
n = 300
x = np.zeros(n)
for i in range(1, n):
x[i] = 0.8 * x[i - 1] + np.random.normal(0, 0.1)
r = pd.Series(x)
assert autocorr_lag1(r) > 0.5
def test_hurst_short_series():
"""Serie < 100 elementi ritorna 0.5 (random-walk default)."""
r = pd.Series(np.random.normal(0, 0.01, 50))
assert hurst_exponent(r) == pytest.approx(0.5)
def test_hurst_range():
"""Hurst sempre in [0, 1]."""
np.random.seed(3)
r = pd.Series(np.random.normal(0, 0.01, 500))
h = hurst_exponent(r)
assert 0.0 <= h <= 1.0
def test_hurst_random_walk_approx_half():
"""Random walk iid deve avere Hurst vicino a 0.5."""
np.random.seed(42)
r = pd.Series(np.random.normal(0, 0.01, 2000))
h = hurst_exponent(r)
assert 0.3 <= h <= 0.7
def test_vol_percentile_short_series():
"""Serie troppo corta ritorna 50.0."""
r = pd.Series(np.random.normal(0, 0.01, 10))
assert vol_percentile_historical(r) == pytest.approx(50.0)
def test_vol_percentile_range():
"""Percentile sempre in [0, 100]."""
np.random.seed(5)
r = pd.Series(np.random.normal(0, 0.01, 500))
p = vol_percentile_historical(r, current_window=24, ref_window=200)
assert 0.0 <= p <= 100.0
def test_vol_percentile_high_vol_at_end():
"""Vol molto alta alla fine deve dare percentile elevato."""
np.random.seed(9)
low_vol = np.random.normal(0, 0.001, 400)
high_vol = np.random.normal(0, 0.05, 100)
r = pd.Series(np.concatenate([low_vol, high_vol]))
p = vol_percentile_historical(r, current_window=24, ref_window=400)
assert p > 70.0
def test_seasonality_strength_no_datetimeindex():
"""Senza DatetimeIndex ritorna 0.0."""
r = pd.Series(np.random.normal(0, 0.01, 200))
assert seasonality_strength(r, by="hour") == 0.0
def test_seasonality_strength_short_series():
"""Serie < 50 elementi ritorna 0.0."""
idx = pd.date_range("2024-01-01", periods=30, freq="1h", tz="UTC")
r = pd.Series(np.random.normal(0, 0.01, 30), index=idx)
assert seasonality_strength(r, by="hour") == 0.0
def test_seasonality_strength_range():
"""Risultato sempre in [0, 1]."""
np.random.seed(11)
idx = pd.date_range("2024-01-01", periods=500, freq="1h", tz="UTC")
r = pd.Series(np.random.normal(0, 0.01, 500), index=idx)
val = seasonality_strength(r, by="hour")
assert 0.0 <= val <= 1.0
def test_seasonality_strength_dow_range():
"""Stagionalita dow sempre in [0, 1]."""
np.random.seed(13)
idx = pd.date_range("2024-01-01", periods=500, freq="1h", tz="UTC")
r = pd.Series(np.random.normal(0, 0.01, 500), index=idx)
val = seasonality_strength(r, by="dow")
assert 0.0 <= val <= 1.0
def test_seasonality_strength_invalid_by():
"""by non valido solleva ValueError."""
idx = pd.date_range("2024-01-01", periods=100, freq="1h", tz="UTC")
r = pd.Series(np.random.normal(0, 0.01, 100), index=idx)
with pytest.raises(ValueError, match="'minute'"):
seasonality_strength(r, by="minute")
# --- Geometric/fractal metrics tests ---
def test_efficiency_ratio_pure_trend():
"""Slope perfetto (prezzi linearmente crescenti) -> efficiency_ratio vicino a 1.0."""
prices = pd.Series(np.linspace(100.0, 200.0, 200))
er = efficiency_ratio_kaufman(prices, length=100)
assert er == pytest.approx(1.0, abs=1e-6)
def test_efficiency_ratio_random_walk():
"""Random walk iid -> efficiency_ratio basso (attorno a 0.1-0.4)."""
np.random.seed(42)
prices = pd.Series(100.0 + np.cumsum(np.random.normal(0, 1, 300)))
er = efficiency_ratio_kaufman(prices, length=100)
assert 0.0 <= er <= 0.6
def test_efficiency_ratio_short_series():
"""Serie troppo corta ritorna 0.0."""
prices = pd.Series([100.0, 101.0, 102.0])
assert efficiency_ratio_kaufman(prices, length=100) == 0.0
def test_tail_index_hill_left_right_separate():
"""tail_index_hill left e right ritornano float in [1, 10]."""
np.random.seed(7)
r = pd.Series(np.random.normal(0, 0.01, 500))
left = tail_index_hill(r, side="left")
right = tail_index_hill(r, side="right")
assert 1.0 <= left <= 10.0
assert 1.0 <= right <= 10.0
def test_tail_index_hill_invalid_side():
"""side non valido solleva ValueError."""
r = pd.Series(np.random.normal(0, 0.01, 200))
with pytest.raises(ValueError, match="'both'"):
tail_index_hill(r, side="both")
def test_tail_index_hill_short_series():
"""Serie < 50 ritorna default 5.0."""
r = pd.Series([0.01, -0.01, 0.02])
assert tail_index_hill(r, side="left") == pytest.approx(5.0)
assert tail_index_hill(r, side="right") == pytest.approx(5.0)
def test_structural_uptrend_pure_uptrend():
"""Prezzi costantemente crescenti -> structural_uptrend vicino a 1.0."""
prices = pd.Series(np.linspace(100.0, 200.0, 100))
score = structural_uptrend_score(prices, window=5)
assert score >= 0.5
def test_structural_uptrend_pure_downtrend():
"""Prezzi costantemente decrescenti -> structural_uptrend vicino a 0.0."""
prices = pd.Series(np.linspace(200.0, 100.0, 100))
score = structural_uptrend_score(prices, window=5)
assert score <= 0.5
def test_structural_uptrend_short_series():
"""Serie troppo corta ritorna 0.5 (neutro)."""
prices = pd.Series([100.0, 101.0, 99.0])
assert structural_uptrend_score(prices, window=5) == pytest.approx(0.5)
def test_compression_ratio_compress_then_expand():
"""Finestra recente compressa rispetto a ref -> ratio < 1."""
np.random.seed(1)
# Ref window: alta volatilita
ref_part = np.random.normal(0, 5.0, 200)
# Recent window: bassa volatilita
recent_part = np.random.normal(0, 0.2, 50)
prices = pd.Series(100.0 + np.cumsum(np.concatenate([ref_part, recent_part])))
ratio = compression_ratio(prices, recent_window=50, ref_window=200)
assert ratio < 1.0
def test_compression_ratio_short_series():
"""Serie troppo corta ritorna 1.0 (neutro)."""
prices = pd.Series([100.0, 101.0, 99.0])
assert compression_ratio(prices, recent_window=50, ref_window=200) == pytest.approx(1.0)
def test_spectral_entropy_pure_sine():
"""Seno puro -> entropy bassa e dominant_cycle ben definito."""
n = 256
t = np.arange(n)
cycle_period = 32 # barre
series = np.sin(2 * np.pi * t / cycle_period)
r = pd.Series(series)
entropy, cycle = spectral_entropy_and_cycle(r, length=256)
assert entropy < 0.6
assert cycle is not None
# Il ciclo dominante deve essere vicino a cycle_period
assert abs(cycle - cycle_period) <= 5.0
def test_spectral_entropy_white_noise():
"""Rumore bianco -> entropy alta e dominant_cycle = None."""
np.random.seed(99)
r = pd.Series(np.random.normal(0, 1, 512))
entropy, cycle = spectral_entropy_and_cycle(r, length=256)
assert entropy >= 0.6
assert cycle is None
def test_spectral_entropy_short_series():
"""Serie troppo corta ritorna (1.0, None)."""
r = pd.Series([0.01, -0.01, 0.02])
entropy, cycle = spectral_entropy_and_cycle(r, length=256)
assert entropy == pytest.approx(1.0)
assert cycle is None
@@ -0,0 +1,78 @@
"""Test che `_parallel_propose` preservi l'ordine dei risultati e funzioni
sia in modalita' sequenziale (workers=1) che concorrente (workers>1).
Non vogliamo testare il vero `HypothesisAgent.propose()` (che fa chiamate
LLM); usiamo un dummy con una latenza simulata per validare ordine e parallelismo.
"""
from __future__ import annotations
import time
from dataclasses import dataclass
from typing import Any
from multi_swarm_core.orchestrator.run import _parallel_propose
@dataclass
class _FakeGenome:
id: str
@dataclass
class _FakeProposal:
genome_id: str
class _FakeAgent:
"""Agent dummy: propose() dorme 50ms e ritorna un proposal con l'id del genome."""
def __init__(self, delay_s: float = 0.05) -> None:
self._delay = delay_s
self.call_count = 0
def propose(self, genome: _FakeGenome, market: Any) -> _FakeProposal:
time.sleep(self._delay)
self.call_count += 1
return _FakeProposal(genome_id=genome.id)
def test_parallel_propose_preserves_order_serial() -> None:
agent = _FakeAgent(delay_s=0.01)
genomes = [_FakeGenome(id=f"g{i}") for i in range(5)]
results = _parallel_propose(agent, genomes, market=None, n_workers=1)
assert [r.genome_id for r in results] == ["g0", "g1", "g2", "g3", "g4"]
assert agent.call_count == 5
def test_parallel_propose_preserves_order_concurrent() -> None:
agent = _FakeAgent(delay_s=0.05)
genomes = [_FakeGenome(id=f"g{i}") for i in range(8)]
results = _parallel_propose(agent, genomes, market=None, n_workers=4)
assert [r.genome_id for r in results] == [f"g{i}" for i in range(8)]
assert agent.call_count == 8
def test_parallel_propose_actually_parallelizes() -> None:
"""Wall time con 4 worker su 4 task da 100ms deve essere ~100ms, non ~400ms."""
agent = _FakeAgent(delay_s=0.1)
genomes = [_FakeGenome(id=f"g{i}") for i in range(4)]
t0 = time.time()
_parallel_propose(agent, genomes, market=None, n_workers=4)
elapsed = time.time() - t0
# serial sarebbe 0.4s; con 4 worker scendiamo a ~0.1s (max 0.2 per overhead).
assert elapsed < 0.2, f"expected <200ms with 4 workers, got {elapsed * 1000:.0f}ms"
def test_parallel_propose_handles_single_genome() -> None:
agent = _FakeAgent()
results = _parallel_propose(agent, [_FakeGenome(id="solo")], market=None, n_workers=8)
assert len(results) == 1
assert results[0].genome_id == "solo"
def test_parallel_propose_empty_input() -> None:
agent = _FakeAgent()
results = _parallel_propose(agent, [], market=None, n_workers=4)
assert results == []
assert agent.call_count == 0
@@ -0,0 +1,146 @@
"""Unit tests per PromptLibrary v3.0 — nuovi campi top-level."""
from __future__ import annotations
import json
import tempfile
from pathlib import Path
import pytest
from multi_swarm_core.genome.prompt_library import PromptLibrary, PromptLibraryError
def _write_json(data: dict, tmp_path: Path) -> Path:
p = tmp_path / "prompts.json"
p.write_text(json.dumps(data), encoding="utf-8")
return p
def test_from_json_loads_new_top_level_fields(tmp_path: Path) -> None:
"""from_json() legge agent_role, pattern_guidance, instruction, domain_warnings."""
data = {
"styles": {"physicist": {"directive": "Cerca leggi conservative."}},
"agent_role": "Sei un agente test.",
"pattern_guidance": "Forme di curva: trend, compressione.",
"instruction": "Genera qualcosa di utile.",
"domain_warnings": "Attenzione: mercato 24/7.",
}
lib = PromptLibrary.from_json(_write_json(data, tmp_path))
assert lib.agent_role == "Sei un agente test."
assert lib.pattern_guidance == "Forme di curva: trend, compressione."
assert lib.instruction == "Genera qualcosa di utile."
assert lib.domain_warnings == "Attenzione: mercato 24/7."
def test_from_json_defaults_new_fields_when_absent(tmp_path: Path) -> None:
"""from_json() usa stringa vuota come default per i 4 campi opzionali."""
data = {
"styles": {"physicist": {"directive": "Cerca leggi conservative."}},
}
lib = PromptLibrary.from_json(_write_json(data, tmp_path))
assert lib.agent_role == ""
assert lib.pattern_guidance == ""
assert lib.instruction == ""
assert lib.domain_warnings == ""
def test_default_provides_universal_fallbacks() -> None:
"""PromptLibrary.default() ha agent_role, pattern_guidance, instruction non vuoti; domain_warnings vuoto."""
lib = PromptLibrary.default()
assert lib.agent_role != ""
assert lib.pattern_guidance != ""
assert lib.instruction != ""
assert lib.domain_warnings == ""
def test_prompt_library_rejects_whitespace_only_fields() -> None:
"""Campi opzionali forniti ma solo whitespace sollevano PromptLibraryError."""
with pytest.raises(PromptLibraryError, match="agent_role"):
PromptLibrary(
styles={"physicist": "Cerca leggi."},
focus={},
agent_role=" ",
)
def test_prompt_library_accepts_empty_string_for_optional_fields() -> None:
"""Stringa vuota '' e' accettata per tutti i campi opzionali."""
lib = PromptLibrary(
styles={"physicist": "Cerca leggi."},
focus={},
agent_role="",
pattern_guidance="",
instruction="",
domain_warnings="",
)
assert lib.agent_role == ""
assert lib.domain_warnings == ""
def test_from_json_loads_anti_patterns_and_output_priorities(tmp_path: Path) -> None:
"""from_json() legge anti_patterns e output_priorities (v3.1)."""
data = {
"styles": {"physicist": {"directive": "Cerca leggi conservative."}},
"anti_patterns": "Evita overfitting.",
"output_priorities": "Robustezza > ottimalita.",
}
lib = PromptLibrary.from_json(_write_json(data, tmp_path))
assert lib.anti_patterns == "Evita overfitting."
assert lib.output_priorities == "Robustezza > ottimalita."
def test_strategy_crypto_directives_ascii_safe() -> None:
"""REGRESSION GUARD: nessuna directive contiene caratteri > U+007F.
v3.1 aveva regredito introducendo il carattere circa-uguale (U+2248) in 3 stili.
v3.2 ripristina ASCII-strict come invariante permanente.
"""
import importlib.resources
path = importlib.resources.files("strategy_crypto") / "prompts.json"
lib = PromptLibrary.from_json(str(path))
for style, directive in lib.styles.items():
non_ascii = [c for c in directive if ord(c) > 127]
assert not non_ascii, (
f"directive di {style!r} contiene caratteri non-ASCII: "
f"{non_ascii} (codepoints: {[hex(ord(c)) for c in non_ascii]})"
)
def test_strategy_crypto_directives_have_archetype_marker() -> None:
"""REGRESSION GUARD: ogni directive chiude con 'Archetipo dominante: ...'.
L'archetipo e' l'ancora semantica identitaria della lente; deve essere
presente per resistere alle riscritture di mutate_prompt_llm.
"""
import importlib.resources
path = importlib.resources.files("strategy_crypto") / "prompts.json"
lib = PromptLibrary.from_json(str(path))
for style, directive in lib.styles.items():
assert "Archetipo dominante:" in directive, (
f"directive di {style!r} manca del marker 'Archetipo dominante:'"
)
def test_strategy_crypto_directives_have_lookback_hint() -> None:
"""REGRESSION GUARD: ogni directive contiene un hint 'Lookback consigliato: X-Y barre'.
Il range numerico orienta il parametro evoluto lookback_window del genoma;
differenziato per stile per favorire diversita di scala temporale nella
popolazione iniziale.
"""
import re
import importlib.resources
path = importlib.resources.files("strategy_crypto") / "prompts.json"
lib = PromptLibrary.from_json(str(path))
pattern = re.compile(r"[Ll]ookback consigliato:\s*\d+\s*-\s*\d+", re.IGNORECASE)
for style, directive in lib.styles.items():
assert pattern.search(directive), (
f"directive di {style!r} manca dell'hint 'Lookback consigliato: X-Y'"
)
@@ -309,3 +309,84 @@ def test_atr_pct_in_strategy_eval(ohlcv: pd.DataFrame) -> None:
# senza dead-branch (qualche match e' possibile in warmup). # senza dead-branch (qualche match e' possibile in warmup).
non_warmup = signal.iloc[30:] non_warmup = signal.iloc[30:]
assert (non_warmup == Side.FLAT).all() or (non_warmup == Side.LONG).any() assert (non_warmup == Side.FLAT).all() or (non_warmup == Side.LONG).any()
def test_sma_pct_is_close_deviation_from_sma() -> None:
"""sma_pct = (close - sma) / sma: deviazione frazionale del close dalla SMA."""
from multi_swarm_core.protocol.compiler import _ind_sma, _ind_sma_pct
idx = pd.date_range("2024-01-01", periods=100, freq="1h", tz="UTC")
# Prezzo che cresce poi torna: sma_pct passa da +qualcosa a -qualcosa
close = np.concatenate([np.linspace(100, 120, 50), np.linspace(120, 95, 50)])
df = pd.DataFrame(
{"open": close, "high": close + 0.5, "low": close - 0.5, "close": close, "volume": 1.0},
index=idx,
)
sma = _ind_sma(df, 20)
sma_pct = _ind_sma_pct(df, 20)
expected = (df["close"] - sma) / sma
assert np.allclose(sma_pct.dropna(), expected.dropna())
# In salita: close > sma -> sma_pct positivo
assert (sma_pct.iloc[30:50] > 0).any()
# In discesa estesa: close < sma -> sma_pct negativo
assert (sma_pct.iloc[80:] < 0).any()
def test_macd_pct_is_macd_divided_by_close() -> None:
"""macd_pct = macd / close: momentum normalizzato al prezzo."""
from multi_swarm_core.protocol.compiler import _ind_macd, _ind_macd_pct
idx = pd.date_range("2024-01-01", periods=300, freq="1h", tz="UTC")
# Random walk realistico su crypto (~3000 USDT, vol ~30/bar): MACD ha
# ampiezza non trascurabile vs trend lineare puro.
rng = np.random.default_rng(42)
close = 3000.0 + np.cumsum(rng.standard_normal(300) * 30)
df = pd.DataFrame(
{"open": close, "high": close + 5, "low": close - 5, "close": close, "volume": 1.0},
index=idx,
)
macd = _ind_macd(df, 12, 26, 9)
macd_pct = _ind_macd_pct(df, 12, 26, 9)
# Identita' algebrica: macd_pct == macd / close
assert np.allclose(macd_pct.dropna(), (macd / df["close"]).dropna())
# macd_pct ha scala << 1 (frazione del prezzo, ordine 1e-3)
assert macd_pct.abs().mean() < 0.05
# macd assoluto e' >> macd_pct (rapporto = close ~3000)
ratio = macd.abs().mean() / max(macd_pct.abs().mean(), 1e-12)
assert 1000 < ratio < 5000
def test_sma_pct_and_macd_pct_in_validator() -> None:
"""Regression: i nuovi indicatori sono accettati dal validator."""
from multi_swarm_core.protocol.validator import validate_strategy
spec = {
"rules": [
{
"condition": {
"op": "and",
"args": [
{
"op": "gt",
"args": [
{"kind": "indicator", "name": "sma_pct", "params": [50]},
{"kind": "literal", "value": 0.05},
],
},
{
"op": "gt",
"args": [
{"kind": "indicator", "name": "macd_pct", "params": [12, 26, 9]},
{"kind": "literal", "value": 0.005},
],
},
],
},
"action": "entry-long",
}
]
}
strat = parse_strategy(json.dumps(spec))
validate_strategy(strat) # no exception
+1
View File
@@ -18,3 +18,4 @@ build-backend = "hatchling.build"
[tool.hatch.build.targets.wheel.force-include] [tool.hatch.build.targets.wheel.force-include]
"strategy_crypto/strategies" = "strategy_crypto/strategies" "strategy_crypto/strategies" = "strategy_crypto/strategies"
"strategy_crypto/prompts.json" = "strategy_crypto/prompts.json"
@@ -1,3 +1,8 @@
"""Paper-trading data access functions for the strategy_crypto dashboard.
Reads exclusively from strategy_crypto.db (paper_trading_* tables).
"""
from __future__ import annotations from __future__ import annotations
import json import json
@@ -7,53 +12,6 @@ from typing import Any
import pandas as pd # type: ignore[import-untyped] import pandas as pd # type: ignore[import-untyped]
from multi_swarm_core.persistence.repository import Repository
def get_repo(db_path: str | Path) -> Repository:
return Repository(db_path=db_path)
def list_runs_df(repo: Repository) -> pd.DataFrame:
return pd.DataFrame(repo.list_runs())
def get_run_overview(repo: Repository, run_id: str) -> dict[str, Any]:
run = repo.get_run(run_id)
return {
"name": run["name"],
"started_at": run["started_at"],
"completed_at": run["completed_at"],
"status": run["status"],
"total_cost_usd": run["total_cost_usd"],
"config": json.loads(run["config_json"]),
}
def generations_df(repo: Repository, run_id: str) -> pd.DataFrame:
return pd.DataFrame(repo.list_generations(run_id))
def evaluations_df(repo: Repository, run_id: str) -> pd.DataFrame:
return pd.DataFrame(repo.list_evaluations(run_id))
def genomes_df(
repo: Repository, run_id: str, generation_idx: int | None = None
) -> pd.DataFrame:
rows = repo.list_genomes(run_id, generation_idx)
flat: list[dict[str, Any]] = []
for r in rows:
payload = json.loads(r["payload_json"])
flat.append(
{
"id": r["id"],
"generation_idx": r["generation_idx"],
**payload,
}
)
return pd.DataFrame(flat)
def _paper_conn(db_path: str | Path) -> sqlite3.Connection: def _paper_conn(db_path: str | Path) -> sqlite3.Connection:
conn = sqlite3.connect(str(db_path)) conn = sqlite3.connect(str(db_path))
@@ -1,9 +1,7 @@
"""NiceGUI dashboard — port progressivo da Streamlit. """Strategy Crypto Dashboard — paper-trading page: /.
Avvio: ``uv run python -m strategy_crypto.frontend.nicegui_app`` Avvio: ``uv run python -m strategy_crypto.frontend.nicegui_app``
Default port 8080. Streamlit resta su 8501 durante la migrazione. Default port 8080. Legge SOLO strategy_crypto.db (paper_trading_* tables).
Riusa ``dashboard.data`` (Repository helpers) senza modifiche al backend.
Palette "Neon Trading Dashboard" (ispirata screenshot 2026-05-11): Palette "Neon Trading Dashboard" (ispirata screenshot 2026-05-11):
- BG: #0A0A0F (near-black con tinge blu) - BG: #0A0A0F (near-black con tinge blu)
@@ -18,8 +16,6 @@ Palette "Neon Trading Dashboard" (ispirata screenshot 2026-05-11):
from __future__ import annotations from __future__ import annotations
import html
import json
import os import os
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any
@@ -29,12 +25,6 @@ import plotly.graph_objects as go # type: ignore[import-untyped]
from nicegui import app, ui from nicegui import app, ui
from strategy_crypto.frontend.data import ( from strategy_crypto.frontend.data import (
evaluations_df,
generations_df,
genomes_df,
get_repo,
get_run_overview,
list_runs_df,
paper_equity_df, paper_equity_df,
paper_positions_df, paper_positions_df,
paper_run_summary, paper_run_summary,
@@ -42,840 +32,21 @@ from strategy_crypto.frontend.data import (
paper_ticks_df, paper_ticks_df,
paper_trades_df, paper_trades_df,
) )
from multi_swarm_core.dashboard.theme import (
COLOR_PRIMARY,
COLOR_SURFACE,
COLOR_SURFACE_2,
COLOR_TEXT,
COLOR_TEXT_MUTED,
_STATUS_BADGE,
_apply_theme,
_build_header,
)
# Dual-DB: GA core e paper strategy_crypto vivono in DB separati.
GA_DB_PATH = os.environ.get("GA_DB_PATH", "./state/runs.db")
PAPER_DB_PATH = os.environ.get("STRATEGY_CRYPTO_DB_PATH", "./state/strategy_crypto.db") PAPER_DB_PATH = os.environ.get("STRATEGY_CRYPTO_DB_PATH", "./state/strategy_crypto.db")
# Subpath per Traefik: "" in dev, "/strategy_crypto_gui" in prod.
DASHBOARD_ROOT_PATH = os.environ.get("DASHBOARD_ROOT_PATH", "") DASHBOARD_ROOT_PATH = os.environ.get("DASHBOARD_ROOT_PATH", "")
REFRESH_INTERVAL_S = 3.0 REFRESH_INTERVAL_S = 3.0
# --- Neon Trading Dashboard palette ---
COLOR_BG = "#0A0A0F"
COLOR_SURFACE = "#13131A"
COLOR_SURFACE_2 = "#1C1C26"
COLOR_BORDER = "rgba(255, 45, 135, 0.12)"
COLOR_BORDER_HOVER = "rgba(255, 45, 135, 0.45)"
COLOR_PRIMARY = "#FF2D87"
COLOR_SECONDARY = "#00D9FF"
COLOR_ACCENT = "#FFB800"
COLOR_SUCCESS = "#00E676"
COLOR_DANGER = "#FF3D60"
COLOR_TEXT = "#FFFFFF"
COLOR_TEXT_MUTED = "#7A7A8C"
_STATUS_BADGE: dict[str, tuple[str, str]] = {
"running": ("● running", "positive"),
"completed": ("✓ completed", "positive"),
"failed": ("✕ failed", "negative"),
}
_CUSTOM_CSS = f"""
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap');
html, body, .q-page, .q-card, .q-btn, .q-field, .q-table, .text-h4, .text-h6, .text-subtitle1, .text-caption, .text-body1, .nav-link, .brand, label, p, span, div {{
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
letter-spacing: -0.01em;
}}
.material-icons, .material-icons-outlined, .material-symbols-outlined, .q-icon, i.q-icon, i[class*="material"] {{
font-family: 'Material Icons' !important;
font-feature-settings: 'liga';
letter-spacing: normal !important;
}}
code, pre, .q-code, .nicegui-code {{ font-family: 'JetBrains Mono', 'Fira Code', monospace !important; font-size: 13.5px !important; }}
body, .q-page-container, .q-page {{
background: {COLOR_BG} !important;
color: {COLOR_TEXT};
background-image:
radial-gradient(ellipse 800px 400px at 20% 0%, rgba(255, 45, 135, 0.08) 0%, transparent 60%),
radial-gradient(ellipse 600px 400px at 80% 100%, rgba(0, 217, 255, 0.06) 0%, transparent 60%);
background-attachment: fixed;
}}
.q-card {{
background: {COLOR_SURFACE} !important;
color: {COLOR_TEXT} !important;
border: 1px solid {COLOR_BORDER};
border-radius: 14px !important;
box-shadow:
0 1px 2px rgba(0,0,0,0.5),
0 8px 24px rgba(0,0,0,0.25),
inset 0 1px 0 rgba(255,255,255,0.04);
transition: all 0.2s ease;
position: relative;
overflow: hidden;
}}
.q-card::before {{
content: '';
position: absolute;
top: 0; left: 0; right: 0;
height: 1px;
background: linear-gradient(90deg, transparent, rgba(255, 45, 135, 0.4), transparent);
opacity: 0.5;
}}
.q-card:hover {{
border-color: rgba(255, 45, 135, 0.5);
box-shadow:
0 1px 2px rgba(0,0,0,0.5),
0 8px 32px rgba(255, 45, 135, 0.15),
inset 0 1px 0 rgba(255,255,255,0.05);
}}
.metric-card {{
padding: 20px 16px;
text-align: left;
display: flex;
flex-direction: column;
gap: 6px;
min-width: 140px;
}}
.metric-card .text-caption {{
color: {COLOR_TEXT_MUTED} !important;
font-size: 11px !important;
font-weight: 500 !important;
text-transform: uppercase;
letter-spacing: 0.06em;
}}
.metric-card .text-h4 {{
color: {COLOR_TEXT} !important;
font-weight: 600 !important;
font-size: 26px !important;
line-height: 1.2 !important;
font-feature-settings: 'tnum';
}}
.metric-card.accent-cyan .text-h4 {{ color: {COLOR_PRIMARY} !important; }}
.metric-card.accent-purple .text-h4 {{ color: {COLOR_SECONDARY} !important; }}
.metric-card.accent-amber .text-h4 {{ color: {COLOR_ACCENT} !important; }}
.metric-card.accent-green .text-h4 {{ color: {COLOR_SUCCESS} !important; }}
.q-header {{
background: rgba(10, 10, 15, 0.75) !important;
backdrop-filter: blur(20px) saturate(180%);
-webkit-backdrop-filter: blur(20px) saturate(180%);
border-bottom: 1px solid {COLOR_BORDER} !important;
box-shadow: 0 1px 0 rgba(255, 45, 135, 0.15) !important;
}}
.nav-link {{
color: {COLOR_TEXT_MUTED} !important;
padding: 8px 14px;
border-radius: 8px;
text-decoration: none;
font-size: 13.5px;
font-weight: 500;
transition: all 0.15s ease;
position: relative;
}}
.nav-link:hover {{
color: {COLOR_TEXT} !important;
background: {COLOR_SURFACE_2};
}}
.nav-link.active {{
color: {COLOR_PRIMARY} !important;
background: rgba(255, 45, 135, 0.08);
}}
.nav-link.active::after {{
content: '';
position: absolute;
bottom: -16px;
left: 14px;
right: 14px;
height: 2px;
background: {COLOR_PRIMARY};
border-radius: 2px 2px 0 0;
}}
.brand {{
color: {COLOR_TEXT};
font-weight: 700;
font-size: 15px;
display: flex;
align-items: center;
gap: 8px;
}}
.brand-dot {{
width: 10px;
height: 10px;
border-radius: 50%;
background: {COLOR_PRIMARY};
box-shadow: 0 0 16px {COLOR_PRIMARY}, 0 0 4px {COLOR_PRIMARY};
animation: pulse-pink 2s ease-in-out infinite;
}}
@keyframes pulse-pink {{
0%, 100% {{ box-shadow: 0 0 16px {COLOR_PRIMARY}, 0 0 4px {COLOR_PRIMARY}; }}
50% {{ box-shadow: 0 0 24px {COLOR_PRIMARY}, 0 0 8px {COLOR_PRIMARY}; }}
}}
.q-linear-progress {{ height: 8px !important; border-radius: 6px !important; }}
.q-linear-progress__track {{ background: {COLOR_SURFACE_2} !important; }}
.q-linear-progress__model {{ border-radius: 6px !important; }}
.q-separator {{ background: {COLOR_BORDER} !important; }}
.q-field--outlined .q-field__control {{
background: {COLOR_SURFACE} !important;
border-radius: 8px !important;
}}
.q-field--outlined .q-field__control:before {{ border-color: {COLOR_BORDER} !important; }}
.q-field--outlined.q-field--focused .q-field__control:after {{ border-color: {COLOR_PRIMARY} !important; }}
.q-field__label {{ color: {COLOR_TEXT_MUTED} !important; }}
.q-field__native, .q-field__input {{ color: {COLOR_TEXT} !important; }}
.q-btn {{ border-radius: 8px !important; font-weight: 500 !important; text-transform: none !important; letter-spacing: 0 !important; }}
.q-table {{ background: transparent !important; color: {COLOR_TEXT} !important; }}
.q-table thead tr {{ background: {COLOR_SURFACE_2} !important; }}
.q-table th {{
color: {COLOR_TEXT_MUTED} !important;
font-size: 11px !important;
font-weight: 600 !important;
text-transform: uppercase;
letter-spacing: 0.06em;
}}
.q-table tbody tr {{ transition: background 0.15s; }}
.q-table tbody tr:hover {{ background: rgba(255, 45, 135, 0.05) !important; }}
.q-table tbody tr.selected {{ background: rgba(255, 45, 135, 0.12) !important; }}
.q-table td {{ border-bottom: 1px solid {COLOR_BORDER} !important; font-feature-settings: 'tnum'; }}
.text-h6 {{ font-weight: 600 !important; letter-spacing: -0.015em !important; }}
.text-subtitle1 {{
color: {COLOR_TEXT_MUTED} !important;
font-size: 11px !important;
font-weight: 600 !important;
text-transform: uppercase !important;
letter-spacing: 0.08em !important;
margin-bottom: 8px !important;
}}
code, pre, .nicegui-code {{
background: #1A1A24 !important;
color: {COLOR_TEXT} !important;
border: 1px solid {COLOR_BORDER};
border-radius: 10px !important;
padding: 16px !important;
font-size: 13.5px !important;
line-height: 1.6 !important;
}}
.hljs {{ background: transparent !important; color: {COLOR_TEXT} !important; }}
.hljs-attr, .hljs-attribute {{ color: {COLOR_SECONDARY} !important; font-weight: 500; }}
.hljs-string {{ color: {COLOR_SUCCESS} !important; }}
.hljs-number, .hljs-literal {{ color: {COLOR_PRIMARY} !important; font-weight: 500; }}
.hljs-keyword, .hljs-built_in {{ color: {COLOR_ACCENT} !important; }}
.hljs-punctuation, .hljs-meta {{ color: {COLOR_TEXT_MUTED} !important; }}
.hljs-comment {{ color: {COLOR_TEXT_MUTED} !important; font-style: italic; }}
.hljs-name, .hljs-title {{ color: {COLOR_PRIMARY} !important; }}
/* Prism.js tokens (NiceGUI usa Prism per ui.code) */
.token.property, .token.attr-name, .token.tag {{ color: {COLOR_SECONDARY} !important; font-weight: 500; }}
.token.string, .token.url {{ color: {COLOR_SUCCESS} !important; }}
.token.number, .token.boolean, .token.null, .token.symbol {{ color: {COLOR_PRIMARY} !important; font-weight: 500; }}
.token.keyword, .token.constant, .token.builtin, .token.atrule {{ color: {COLOR_ACCENT} !important; }}
.token.punctuation, .token.operator {{ color: {COLOR_TEXT_MUTED} !important; }}
.token.comment {{ color: {COLOR_TEXT_MUTED} !important; font-style: italic; }}
.token.function, .token.class-name {{ color: {COLOR_PRIMARY} !important; }}
pre[class*="language-"], code[class*="language-"] {{
color: {COLOR_TEXT} !important;
text-shadow: none !important;
}}
.q-badge {{ border-radius: 6px !important; font-weight: 500 !important; padding: 4px 10px !important; font-size: 12px !important; }}
.config-block {{
background: #1A1A24;
color: {COLOR_TEXT};
border: 1px solid {COLOR_BORDER};
border-radius: 10px;
padding: 18px 20px;
font-family: 'JetBrains Mono', 'Fira Code', monospace;
font-size: 13.5px;
line-height: 1.7;
overflow-x: auto;
white-space: pre;
margin: 0;
}}
.config-block .cb-key {{ color: {COLOR_SECONDARY}; font-weight: 500; }}
.config-block .cb-string {{ color: {COLOR_SUCCESS}; }}
.config-block .cb-number {{ color: {COLOR_PRIMARY}; font-weight: 500; }}
.config-block .cb-bool {{ color: {COLOR_ACCENT}; }}
.config-block .cb-null {{ color: {COLOR_ACCENT}; font-style: italic; }}
.config-block .cb-punct {{ color: {COLOR_TEXT_MUTED}; }}
.raw-block {{
background: #1A1A24;
color: {COLOR_TEXT};
border: 1px solid {COLOR_BORDER};
border-radius: 10px;
padding: 18px 20px;
font-family: 'JetBrains Mono', 'Fira Code', monospace;
font-size: 13px;
line-height: 1.6;
overflow-x: auto;
white-space: pre-wrap;
word-break: break-word;
max-height: 400px;
overflow-y: auto;
margin: 0;
}}
</style>
"""
def _json_to_html(obj: Any, indent: int = 0) -> str:
"""Render JSON con span colorati espliciti. Garantisce leggibilità ovunque."""
pad = " " * indent
inner_pad = " " * (indent + 1)
if isinstance(obj, dict):
if not obj:
return '<span class="cb-punct">{}</span>'
items = []
for k, v in obj.items():
key = f'<span class="cb-key">"{html.escape(str(k))}"</span>'
val = _json_to_html(v, indent + 1)
items.append(f"{inner_pad}{key}<span class=\"cb-punct\">:</span> {val}")
return ('<span class="cb-punct">{</span>\n'
+ '<span class="cb-punct">,</span>\n'.join(items)
+ f'\n{pad}<span class="cb-punct">}}</span>')
if isinstance(obj, list):
if not obj:
return '<span class="cb-punct">[]</span>'
items = [_json_to_html(x, indent + 1) for x in obj]
return ('<span class="cb-punct">[</span>\n'
+ '<span class="cb-punct">,</span>\n'.join(inner_pad + i for i in items)
+ f'\n{pad}<span class="cb-punct">]</span>')
if isinstance(obj, bool):
return f'<span class="cb-bool">{str(obj).lower()}</span>'
if obj is None:
return '<span class="cb-null">null</span>'
if isinstance(obj, (int, float)):
return f'<span class="cb-number">{obj}</span>'
return f'<span class="cb-string">"{html.escape(str(obj))}"</span>'
def _apply_theme() -> None:
ui.add_head_html(_CUSTOM_CSS)
ui.dark_mode().enable()
ui.colors(
primary=COLOR_PRIMARY,
secondary=COLOR_SECONDARY,
accent=COLOR_ACCENT,
dark=COLOR_BG,
dark_page=COLOR_BG,
positive=COLOR_SUCCESS,
negative=COLOR_DANGER,
info=COLOR_PRIMARY,
warning=COLOR_ACCENT,
)
def _build_header(active: str) -> None:
with ui.header().classes("items-center justify-between q-px-lg q-py-md"):
with ui.row().classes("items-center gap-8"):
with ui.row().classes("items-center gap-2").classes("brand"):
ui.html('<span class="brand-dot"></span>')
ui.html('<span class="brand">Multi-Swarm <span style="color:'
+ COLOR_TEXT_MUTED + ';font-weight:400;">/ Coevolutivo</span></span>')
with ui.row().classes("items-center gap-1"):
for path, label in (
("/", "Overview"),
("/convergence", "Convergence"),
("/genomes", "Genomes"),
("/paper", "Paper"),
):
cls = "nav-link active" if active == path else "nav-link"
ui.link(label, path).classes(cls)
with ui.row().classes("items-center gap-3"):
ui.html(f'<span style="color:{COLOR_TEXT_MUTED};font-size:12px;'
f'font-family:JetBrains Mono,monospace;">'
f'{Path(GA_DB_PATH).resolve().name} + {Path(PAPER_DB_PATH).resolve().name}</span>')
def _runs_options() -> dict[str, str]:
repo = get_repo(GA_DB_PATH)
runs = list_runs_df(repo)
if runs.empty:
return {}
return {
row["id"]: f"{row['name']}{row['status']} ({row['started_at'][:16]})"
for _, row in runs.iterrows()
}
def _snapshot(run_id: str) -> dict[str, Any]:
repo = get_repo(GA_DB_PATH)
ov = get_run_overview(repo, run_id)
evals = evaluations_df(repo, run_id)
gens = generations_df(repo, run_id)
cfg = ov["config"]
pop_size = int(cfg.get("population_size", 0))
n_gens = int(cfg.get("n_generations", 0))
evals_total = max(pop_size * n_gens, 1)
evals_done = len(evals)
gens_done = int(gens["completed_at"].notna().sum()) if not gens.empty else 0
# runs.total_cost_usd è 0 finché complete_run non viene chiamato.
# Per le run in corso leggiamo la somma live da cost_records.
live_cost = float(repo.total_cost(run_id)) if ov["status"] == "running" else float(
ov["total_cost_usd"]
)
top_fit = float(evals["fitness"].max()) if evals_done else float("nan")
median_fit = float(evals["fitness"].median()) if evals_done else float("nan")
parse_success = (
100.0 * float(evals["parse_error"].isna().sum()) / evals_done if evals_done else 0.0
)
return {
"status": ov["status"],
"name": cfg.get("run_name", ""),
"started_at": ov["started_at"],
"completed_at": ov["completed_at"] or "",
"cost_usd": live_cost,
"pop_size": pop_size,
"n_gens": n_gens,
"evals_done": evals_done,
"evals_total": evals_total,
"gens_done": gens_done,
"top_fit": top_fit,
"median_fit": median_fit,
"parse_success": parse_success,
"config": cfg,
"gens_df": gens,
}
def _convergence_figure(gens_df: Any) -> go.Figure:
fig = go.Figure()
if gens_df.empty:
fig.add_annotation(
text="Nessuna generazione registrata", x=0.5, y=0.5, showarrow=False,
font={"color": COLOR_TEXT_MUTED, "size": 14},
)
else:
fig.add_trace(
go.Scatter(
x=gens_df["generation_idx"], y=gens_df["fitness_max"],
name="max", mode="lines+markers",
line={"color": COLOR_PRIMARY, "width": 3, "shape": "spline", "smoothing": 0.6},
marker={"size": 9, "color": COLOR_PRIMARY,
"line": {"color": "#fff", "width": 1}},
fill="tozeroy",
fillcolor="rgba(255, 45, 135, 0.12)",
)
)
fig.add_trace(
go.Scatter(
x=gens_df["generation_idx"], y=gens_df["fitness_p90"],
name="p90", mode="lines+markers",
line={"color": COLOR_ACCENT, "width": 2, "dash": "dot", "shape": "spline"},
marker={"size": 7, "color": COLOR_ACCENT},
)
)
fig.add_trace(
go.Scatter(
x=gens_df["generation_idx"], y=gens_df["fitness_median"],
name="median", mode="lines+markers",
line={"color": COLOR_SECONDARY, "width": 2, "shape": "spline"},
marker={"size": 7, "color": COLOR_SECONDARY},
)
)
fig.update_layout(
template="plotly_dark",
paper_bgcolor=COLOR_SURFACE,
plot_bgcolor=COLOR_SURFACE,
font={"color": COLOR_TEXT},
xaxis={"title": "generation", "gridcolor": "rgba(148, 163, 184, 0.08)", "dtick": 1},
yaxis={"title": "fitness", "gridcolor": "rgba(148, 163, 184, 0.08)"},
title={"text": "Fitness convergence", "font": {"color": COLOR_TEXT, "size": 18}},
legend={"bgcolor": "rgba(19, 19, 26, 0.95)", "bordercolor": COLOR_PRIMARY, "borderwidth": 1},
margin={"l": 50, "r": 30, "t": 50, "b": 50},
)
return fig
def _entropy_figure(gens_df: Any) -> go.Figure:
fig = go.Figure()
if not gens_df.empty:
fig.add_trace(
go.Scatter(
x=gens_df["generation_idx"], y=gens_df["entropy"],
mode="lines+markers",
line={"color": COLOR_SECONDARY, "width": 3, "shape": "spline", "smoothing": 0.6},
marker={"size": 9, "color": COLOR_SECONDARY,
"line": {"color": "#fff", "width": 1}},
fill="tozeroy",
fillcolor="rgba(0, 217, 255, 0.12)",
name="entropy",
)
)
fig.add_hline(
y=0.5, line_dash="dash", line_color=COLOR_ACCENT,
annotation_text="gate threshold (0.5)",
annotation_font_color=COLOR_ACCENT,
)
fig.update_layout(
template="plotly_dark",
paper_bgcolor=COLOR_SURFACE,
plot_bgcolor=COLOR_SURFACE,
font={"color": COLOR_TEXT},
xaxis={"title": "generation", "gridcolor": "rgba(148, 163, 184, 0.08)", "dtick": 1},
yaxis={"title": "entropy", "gridcolor": "rgba(148, 163, 184, 0.08)"},
title={"text": "Diversity (fitness entropy)", "font": {"color": COLOR_TEXT, "size": 18}},
margin={"l": 50, "r": 30, "t": 50, "b": 50},
)
return fig
@ui.page("/")
def index() -> None:
_apply_theme()
_build_header(active="/")
options = _runs_options()
if not options:
ui.label("Nessuna run nel database.").classes("text-h5")
return
state: dict[str, Any] = {"run_id": next(iter(options))}
with ui.row().classes("w-full items-center gap-4 q-mb-md"):
select = ui.select(options=options, value=state["run_id"], label="Run").classes(
"flex-grow"
)
status_badge = ui.badge("", color="primary").classes("text-body1 q-pa-sm")
ui.button("🔄 Refresh", on_click=lambda: refresh()).props("outline color=primary")
with ui.card().classes("w-full"):
ui.label("Progresso run").classes("text-subtitle1")
gen_label = ui.label("Generations: 0/0")
gen_bar = ui.linear_progress(0.0, show_value=False).props("size=20px color=primary")
eval_label = ui.label("Evaluations: 0/0 (0.0%)")
eval_bar = ui.linear_progress(0.0, show_value=False).props("size=20px color=accent")
with ui.row().classes("w-full gap-4"):
with ui.card().classes("flex-grow metric-card accent-cyan"):
ui.label("Top fitness").classes("text-caption")
top_lbl = ui.label("").classes("text-h4")
with ui.card().classes("flex-grow metric-card accent-purple"):
ui.label("Median fitness").classes("text-caption")
median_lbl = ui.label("").classes("text-h4")
with ui.card().classes("flex-grow metric-card accent-amber"):
ui.label("Parse success").classes("text-caption")
parse_lbl = ui.label("").classes("text-h4")
with ui.card().classes("flex-grow metric-card accent-green"):
ui.label("Cost (USD)").classes("text-caption")
cost_lbl = ui.label("").classes("text-h4")
with ui.row().classes("w-full gap-4 q-mt-md"):
started_lbl = ui.label("Started: —")
completed_lbl = ui.label("Completed: —")
ui.separator()
ui.label("Config").classes("text-subtitle1")
cfg_code = ui.html('<pre class="config-block"></pre>').classes("w-full")
def refresh() -> None:
run_id = select.value
if not run_id:
return
try:
s = _snapshot(run_id)
except Exception as e: # noqa: BLE001
ui.notify(f"Errore: {e}", type="negative")
return
text, color = _STATUS_BADGE.get(s["status"], (s["status"], "primary"))
status_badge.text = text
status_badge.props(f"color={color}")
gen_frac = min(s["gens_done"] / max(s["n_gens"], 1), 1.0)
eval_frac = min(s["evals_done"] / s["evals_total"], 1.0)
gen_bar.value = gen_frac
eval_bar.value = eval_frac
gen_label.text = f"Generations: {s['gens_done']}/{s['n_gens']}"
eval_label.text = (
f"Evaluations: {s['evals_done']}/{s['evals_total']} ({100 * eval_frac:.1f}%)"
)
top_lbl.text = f"{s['top_fit']:.4f}" if s["evals_done"] else ""
median_lbl.text = f"{s['median_fit']:.4f}" if s["evals_done"] else ""
parse_lbl.text = f"{s['parse_success']:.1f}%" if s["evals_done"] else ""
cost_lbl.text = f"${s['cost_usd']:.4f}"
started_lbl.text = f"Started: {s['started_at']}"
completed_lbl.text = f"Completed: {s['completed_at']}"
cfg_code.content = f'<pre class="config-block">{_json_to_html(s["config"])}</pre>'
def on_select_change() -> None:
state["run_id"] = select.value
refresh()
select.on_value_change(on_select_change)
ui.timer(REFRESH_INTERVAL_S, refresh)
refresh()
@ui.page("/convergence")
def convergence() -> None:
_apply_theme()
_build_header(active="/convergence")
options = _runs_options()
if not options:
ui.label("Nessuna run nel database.").classes("text-h5")
return
state: dict[str, Any] = {"run_id": next(iter(options))}
with ui.row().classes("w-full items-center gap-4 q-mb-md"):
select = ui.select(options=options, value=state["run_id"], label="Run").classes(
"flex-grow"
)
gen_count_lbl = ui.label("Gens: 0/0").classes("text-body1").style(
f"color: {COLOR_PRIMARY}; font-weight: 600;"
)
ui.button("🔄 Refresh", on_click=lambda: refresh()).props("outline color=primary")
fitness_plot = ui.plotly(_convergence_figure(generations_df(get_repo(GA_DB_PATH), state["run_id"]))).classes("w-full")
entropy_plot = ui.plotly(_entropy_figure(generations_df(get_repo(GA_DB_PATH), state["run_id"]))).classes("w-full q-mt-md")
ui.separator()
ui.label("Tabella generazioni").classes("text-subtitle1 q-mt-md")
gens_table = ui.table(
columns=[
{"name": "generation_idx", "label": "gen", "field": "generation_idx", "sortable": True},
{"name": "n_genomes", "label": "n", "field": "n_genomes"},
{"name": "fitness_max", "label": "max", "field": "fitness_max"},
{"name": "fitness_p90", "label": "p90", "field": "fitness_p90"},
{"name": "fitness_median", "label": "median", "field": "fitness_median"},
{"name": "entropy", "label": "entropy", "field": "entropy"},
{"name": "completed_at", "label": "completed", "field": "completed_at"},
],
rows=[],
row_key="generation_idx",
).classes("w-full")
def refresh() -> None:
run_id = select.value
if not run_id:
return
try:
gens = generations_df(get_repo(GA_DB_PATH), run_id)
ov = get_run_overview(get_repo(GA_DB_PATH), run_id)
except Exception as e: # noqa: BLE001
ui.notify(f"Errore: {e}", type="negative")
return
n_gens = int(ov["config"].get("n_generations", 0))
gens_done = int(gens["completed_at"].notna().sum()) if not gens.empty else 0
gen_count_lbl.text = f"Gens: {gens_done}/{n_gens}"
fitness_plot.update_figure(_convergence_figure(gens))
entropy_plot.update_figure(_entropy_figure(gens))
if gens.empty:
gens_table.rows = []
else:
display_cols = [
"generation_idx", "n_genomes",
"fitness_max", "fitness_p90", "fitness_median",
"entropy", "completed_at",
]
gens_table.rows = [
{
col: (round(v, 6) if isinstance(v, float) else v)
for col, v in row.items()
if col in display_cols
}
for _, row in gens.iterrows()
]
gens_table.update()
def on_select_change() -> None:
state["run_id"] = select.value
refresh()
select.on_value_change(on_select_change)
ui.timer(REFRESH_INTERVAL_S, refresh)
refresh()
@ui.page("/genomes")
def genomes() -> None:
_apply_theme()
_build_header(active="/genomes")
options = _runs_options()
if not options:
ui.label("Nessuna run nel database.").classes("text-h5")
return
state: dict[str, Any] = {
"run_id": next(iter(options)),
"selected_gid": None,
"merged": None,
}
with ui.row().classes("w-full items-center gap-4 q-mb-md"):
select = ui.select(options=options, value=state["run_id"], label="Run").classes(
"flex-grow"
)
top_k_select = ui.select(
options={10: "Top 10", 25: "Top 25", 50: "Top 50"},
value=10,
label="Top K",
)
ui.button("🔄 Refresh", on_click=lambda: refresh()).props("outline color=primary")
ui.label("Top genomi per fitness").classes("text-subtitle1 q-mt-sm")
top_table = ui.table(
columns=[
{"name": "genome_id", "label": "id", "field": "genome_id", "align": "left"},
{"name": "fitness", "label": "fitness", "field": "fitness", "sortable": True},
{"name": "dsr", "label": "DSR", "field": "dsr"},
{"name": "sharpe", "label": "Sharpe", "field": "sharpe"},
{"name": "max_dd", "label": "max DD", "field": "max_dd"},
{"name": "n_trades", "label": "trades", "field": "n_trades"},
{"name": "cognitive_style", "label": "style", "field": "cognitive_style"},
{"name": "temperature", "label": "T", "field": "temperature"},
{"name": "lookback_window", "label": "lookback", "field": "lookback_window"},
],
rows=[],
row_key="genome_id",
selection="single",
).classes("w-full")
ui.separator().classes("q-my-md")
with ui.card().classes("w-full"):
ui.label("Ispezione genoma").classes("text-subtitle1")
detail_hint = ui.label("Seleziona un genoma dalla tabella sopra.").classes(
"text-caption"
).style(f"color: {COLOR_TEXT_MUTED};")
with ui.row().classes("w-full gap-4 q-mt-sm"):
with ui.card().classes("flex-grow metric-card accent-cyan"):
ui.label("fitness").classes("text-caption")
fit_lbl = ui.label("").classes("text-h4")
with ui.card().classes("flex-grow metric-card accent-purple"):
ui.label("DSR").classes("text-caption")
dsr_lbl = ui.label("").classes("text-h4")
with ui.card().classes("flex-grow metric-card accent-amber"):
ui.label("Sharpe").classes("text-caption")
sharpe_lbl = ui.label("").classes("text-h4")
with ui.card().classes("flex-grow metric-card"):
ui.label("max DD").classes("text-caption")
dd_lbl = ui.label("").classes("text-h4")
with ui.card().classes("flex-grow metric-card accent-green"):
ui.label("trades").classes("text-caption")
trades_lbl = ui.label("").classes("text-h4")
with ui.card().classes("flex-grow metric-card"):
ui.label("style").classes("text-caption")
style_lbl = ui.label("").classes("text-h4")
ui.label("System prompt").classes("text-subtitle1 q-mt-md")
prompt_code = ui.html('<pre class="raw-block">—</pre>').classes("w-full")
ui.label("Raw LLM output").classes("text-subtitle1 q-mt-md")
raw_code = ui.html('<pre class="raw-block">—</pre>').classes("w-full")
parse_error_lbl = ui.label("").classes("q-mt-sm").style(
"color: #FF6B6B; font-weight: 600;"
)
def _render_detail(row: dict[str, Any]) -> None:
detail_hint.text = f"Genoma: {row.get('genome_id', '')}"
fit_lbl.text = f"{float(row.get('fitness', 0)):.4f}"
dsr_lbl.text = f"{float(row.get('dsr', 0)):.4f}"
sharpe_lbl.text = f"{float(row.get('sharpe', 0)):.3f}"
dd_lbl.text = f"{float(row.get('max_dd', 0)):.3f}"
trades_lbl.text = str(int(row.get("n_trades", 0)))
style_lbl.text = str(row.get("cognitive_style", ""))
prompt_code.content = (
f'<pre class="raw-block">{html.escape(str(row.get("system_prompt", "")))}</pre>'
)
raw_code.content = (
f'<pre class="raw-block">{html.escape(str(row.get("raw_text", "") or ""))}</pre>'
)
pe = row.get("parse_error")
parse_error_lbl.text = f"❌ Parse error: {pe}" if pe else ""
def refresh() -> None:
run_id = select.value
if not run_id:
return
try:
repo = get_repo(GA_DB_PATH)
evals = evaluations_df(repo, run_id)
gens = genomes_df(repo, run_id)
except Exception as e: # noqa: BLE001
ui.notify(f"Errore: {e}", type="negative")
return
if evals.empty:
top_table.rows = []
top_table.update()
return
merged = evals.merge(
gens, left_on="genome_id", right_on="id", how="left", suffixes=("", "_g")
)
state["merged"] = merged
k = int(top_k_select.value)
top = merged.sort_values("fitness", ascending=False).head(k)
rows = []
for _, r in top.iterrows():
rows.append(
{
"genome_id": str(r.get("genome_id", ""))[:12] + "",
"fitness": round(float(r.get("fitness", 0)), 4),
"dsr": round(float(r.get("dsr", 0)), 4),
"sharpe": round(float(r.get("sharpe", 0)), 3),
"max_dd": round(float(r.get("max_dd", 0)), 3),
"n_trades": int(r.get("n_trades", 0)),
"cognitive_style": str(r.get("cognitive_style", "")),
"temperature": round(float(r.get("temperature", 0)), 2),
"lookback_window": int(r.get("lookback_window", 0)),
"_full_id": str(r.get("genome_id", "")),
}
)
top_table.rows = rows
top_table.update()
sel = state.get("selected_gid")
if sel:
match = merged[merged["genome_id"] == sel]
if not match.empty:
_render_detail(match.iloc[0].to_dict())
def on_row_selected(e: Any) -> None:
rows = (e.args or {}).get("rows") or []
if not rows:
return
full_id = rows[0].get("_full_id")
if not full_id:
return
state["selected_gid"] = full_id
merged = state.get("merged")
if merged is None:
return
match = merged[merged["genome_id"] == full_id]
if not match.empty:
_render_detail(match.iloc[0].to_dict())
def on_select_change() -> None:
state["run_id"] = select.value
state["selected_gid"] = None
refresh()
select.on_value_change(on_select_change)
top_k_select.on_value_change(lambda _: refresh())
top_table.on("selection", on_row_selected)
ui.timer(REFRESH_INTERVAL_S, refresh)
refresh()
def _paper_runs_options(only_running: bool = False) -> dict[str, str]: def _paper_runs_options(only_running: bool = False) -> dict[str, str]:
runs = paper_runs_df(PAPER_DB_PATH) runs = paper_runs_df(PAPER_DB_PATH)
@@ -925,10 +96,15 @@ def _paper_equity_figure(eq_df: Any, initial_capital: float) -> go.Figure:
return fig return fig
@ui.page("/paper") @ui.page("/")
def paper() -> None: def paper() -> None:
_apply_theme() _apply_theme()
_build_header(active="/paper") _build_header(
active="/",
brand_subtitle="Strategy Crypto",
nav_items=[("/", "Paper")],
db_label=f"{Path(PAPER_DB_PATH).resolve().name}",
)
options = _paper_runs_options() options = _paper_runs_options()
if not options: if not options:
@@ -1087,7 +263,6 @@ def paper() -> None:
def main() -> None: def main() -> None:
app.on_startup( app.on_startup(
lambda: print( lambda: print(
f"GA DB: {Path(GA_DB_PATH).resolve()} | "
f"Paper DB: {Path(PAPER_DB_PATH).resolve()} | " f"Paper DB: {Path(PAPER_DB_PATH).resolve()} | "
f"root_path: {DASHBOARD_ROOT_PATH or '/'}" f"root_path: {DASHBOARD_ROOT_PATH or '/'}"
) )
@@ -0,0 +1,50 @@
{
"_comment": "Stili cognitivi e direttive del system_prompt per il GA di strategy_crypto. Modifica liberamente: cambia 'directive' di uno stile esistente o aggiungi nuove voci a 'styles'. Il nome dello stile (key) viene usato come 'cognitive_style' del genoma; la 'directive' diventa il system_prompt iniziale.",
"_schema": "3.2",
"_changelog": "v3.2 - Patch consolidamento: ripristinati 3 invarianti regrediti in v3.1 (ASCII-safe, archetipo dominante, hint lookback); voce attiva rinforzata; anti_patterns +2 (chattering, isteresi); output_priorities +1 (#1 coerenza con lente cognitiva); domain_warnings +1 frase (soglia seasonality 0.05); NEW _design_invariants metadata. Lunghezza directive 800-950 char (era 545-614 in v3.1, troppo snellite). v3.1 - Refactor contenuto post-diagnosi. v3.0 - Refactor compositore. v2.2 - Metriche geometrico-frattali. v2.1 - directive estese. v2.0 - Riprogettato per blind-generator GA.",
"_focus_metrics_design": "Le focus_metrics sono ENFASI per la lente, non filtri. Standardizzate a 4 per stile (cognitive budget). Evitano ridondanze con la sezione 'Regime recente' del USER_TEMPLATE.",
"_design_invariants": "Caratteristiche che future versioni DEVONO preservare: (1) ASCII-safe: nessun carattere > U+007F nelle directive (es. il carattere circa-uguale U+2248 era una regressione di v3.1, ripristinato in v3.2); (2) Archetipo dominante: ogni directive chiude con 'Archetipo dominante: <metafora>.' come ancora identitaria resistente a mutate_prompt_llm; (3) Lookback consigliato: ogni directive include un range numerico diversificato per stile per orientare il parametro evoluto lookback_window del genoma; (4) Metafora ancorante: la lente cognitiva e' descritta in forma 'Il mercato e ...' come prima frase; (5) Lunghezza directive: tra 800 e 950 char (sweet spot per robustezza a mutate_prompt_llm).",
"agent_role": "Sei un agente generatore di ipotesi di trading quantitativo per un sistema swarm coevolutivo specializzato in mercati crypto. Sei parte di una popolazione che esplora collettivamente lo spazio delle strategie: la diversita delle ipotesi e un asset critico per il sistema. Preferisci esplorare territori meno ovvi rispetto a quelli che la tua lente cognitiva renderebbe predicibili.",
"pattern_guidance": "Forme di curva da cercare:\n - Trend strutturale (direzione persistente con basso ritracciamento)\n - Compressione di volatilita (pre-breakout, energia accumulata)\n - Espansione di volatilita (regime di shock: momentum o cattura difensiva)\n - Mean reversion strutturale (deviazione eccessiva dalla media verso ritorno atteso)\n - Esaurimento direzionale (estremi di oscillatori, divergenza prezzo-momento)\n\nRipetibilita da sfruttare:\n - Eventi crossover ricorrenti su serie correlate\n - Cicli intra-day se la seasonality oraria e significativa\n - Cicli settimanali se la seasonality settimanale e significativa\n - Pattern doppio (top/bottom) con conferma su livello simile\n - Range breakout dopo periodo di compressione\n\nCerca pattern che si REPLICANO nei dati storici, non singoli eventi rari.",
"instruction": "Genera una strategia che cerchi anomalie sfruttabili in questo regime crypto.",
"domain_warnings": "Crypto trada 24/7 senza CME gap: non assumere chiusure settimanali. Tail pesanti e vol clustering esteso caratterizzano BTC/ETH: evita ipotesi gaussiane. NON tentare di inferire funding rate, news o eventi macro: non sono accessibili. Le statistiche fornite sono l'unica informazione su cui basarsi. Una seasonality_hour o seasonality_dow > 0 NON significa che la stagionalita sia significativa: usa la soglia 0.05 come gate minimo (sotto e' rumore statistico).",
"anti_patterns": "Evita: (1) basare strategia su singolo evento estremo (kurt outlier senza ricorrenza nelle 500 barre recenti); (2) usare piu di 4 condizioni in AND (sovra-fitting combinatorio, brittle a piccoli shift di regime); (3) confondere correlazione storica con causalita (autocorr o pattern temporali sono BIAS, non leggi); (4) assumere stazionarieta perfetta del regime (i delta 'recente vs baseline' indicano lo scostamento); (5) usare feature temporali (hour, dow, is_weekend) quando la seasonality corrispondente e' < 0.05 (rumore, no signal); (6) crossover tra indicatori dello stesso tipo con lookback vicini (chattering: oscilla tra entry/exit senza segnale netto); (7) soglie hard senza isteresi tra entry ed exit (genera chattering al confine; usa soglie diverse per entry vs exit, almeno 10-20% di gap).",
"output_priorities": "Quando emerge trade-off: (1) coerenza con la lente cognitiva: la strategia deve essere riconoscibile come emanata dal tuo stile (es. engineer = pochi gate robusti, psychologist = contrarian su estremi). E' il fondamento del design swarm: ipotesi omogeneizzate riducono la diversita della popolazione; (2) robustezza cross-regime > ottimalita su singolo regime; (3) semplicita interpretabile (2-3 condizioni con razionale chiaro) > complessita raffinata (5+ condizioni accordate); (4) selettivita (poche entry forti, alto SNR) > attivita (molte entry deboli); (5) condizioni con razionale meccanico > pattern statistici fortuiti.",
"styles": {
"physicist": {
"directive": "Il mercato e un sistema fisico con energia (std), simmetrie (skew) e memoria (autocorr). Leggi kurt come densita di eventi estremi (fat tails = fuori equilibrio), skew come forzante asimmetrica. AR(1) positivo molto sopra baseline = memoria coerente, costruisci ipotesi di momentum legittimo; Hurst > 0.55 conferma persistenza di scala su orizzonti multipli; vol_pct alto con kurt bassa = energia immagazzinata non ancora rilasciata, cattura il rilascio; efficiency_ratio elevato = movimento efficiente, sfrutta direzionalita; spectral_entropy bassa con dominant_cycle definito = modi armonici sfruttabili, combina con conferma sulla fase. Preferisci pattern coerenti su piu lookback rispetto a singoli eventi rumorosi. Diagnostica regimi simmetrici e rotture di simmetria. Lookback consigliato: 150-300 barre. Archetipo dominante: sistema fisico in equilibrio (o pre-rottura di simmetria).",
"focus_metrics": ["hurst", "dominant_cycle", "efficiency_ratio", "spectral_entropy"]
},
"biologist": {
"directive": "Il mercato e un ecosistema dove strategie competono per alpha finito. Skew negativo segnala predazione asimmetrica (vol-selling crowded subisce shock), positivo predatori che cacciano breakout. Kurt alta = eventi di estinzione o fioritura. AR(1) positivo persistente = una specie sta colonizzando la nicchia (overcrowding imminente, preferisci fade); Hurst > 0.55 con vol_pct basso = nicchia stabile (occupa con strategie direzionali); tail asimmetrico (left molto piu pesante di right) = predazione asimmetrica strutturale, costruisci contrarian sulla coda; structural_uptrend persistente = specie dominante stabile. Combina seasonality con uno o due gate di regime per evitare di sovrapporti a fasi gia mature. Cattura la coda opposta al consensus. Lookback consigliato: 80-200 barre. Archetipo dominante: ecosistema con dinamiche predator-prey e nicchie evolutive.",
"focus_metrics": ["tail_left", "tail_right", "structural_uptrend", "seasonality_hour"]
},
"historian": {
"directive": "Il mercato attraversa fasi cicliche che si ripetono in forma simile. Mean = drift strutturale, std = ampiezza ciclo, kurt alta + vol regime medium/high = fase tardiva (pre-transizione); kurt bassa + skew vicino a zero = fase di accumulazione o stabilita. AR(1) recente molto sopra baseline storica = regime accelera rispetto al normale, diagnostica se markup o distribuzione; Hurst > 0.55 con vol_pct alto = fase markup matura, costruisci ipotesi di mean reversion strutturale attesa; structural_uptrend sostenuto = fase di accumulo o markup attiva, sfrutta la direzionalita; compression < 1 = consolidamento pre-fase nuova, preferisci breakout direzionali a conferma di rottura. Identifica analogie tra il regime corrente e fasi tipiche (accumulazione, markup, distribuzione, markdown). Lookback consigliato: 200-500 barre. Archetipo dominante: ciclo storico ricorrente in fasi tipiche.",
"focus_metrics": ["autocorr_recent", "structural_uptrend", "compression", "hurst"]
},
"meteorologist": {
"directive": "La volatilita ha climi persistenti e fronti di transizione. Vol_regime + std + kurt definiscono il microclima: std bassa + kurt bassa = calma stabile (preferisci vendere vol con gate sicuri); std alta + kurt alta = tempesta (compra convexity o resta flat); std bassa + kurt alta = calma ingannevole pre-fronte (riduci esposizione). AR(1) recente sopra baseline = fronte persistente in arrivo, cattura la direzione del fronte; Hurst > 0.55 = sistema su scala lunga (ciclone), Hurst < 0.45 = turbolenza locale (no trend persistente, preferisci range-trading); vol_pct estremo = posizione nel ciclo seasonal, modula la size; compression < 1 = compressione vol pre-rilascio, posizionati per il breakout. Costruisci strategie con gate espliciti su vol che attivano logiche diverse. Lookback consigliato: 50-150 barre. Archetipo dominante: clima atmosferico con fronti e regimi persistenti.",
"focus_metrics": ["vol_pct", "compression", "tail_right", "dominant_cycle"]
},
"engineer": {
"directive": "Tratta ogni segnale come un sistema di controllo: serve SNR favorevole, causalita, robustezza. Std e il rumore di fondo. Kurt alta riduce affidabilita dei segnali medi (gli estremi dominano le statistiche). AR(1) > 0.05 con std contenuta = SNR favorevole, costruisci ipotesi di momentum filtrato; AR(1) vicino a zero = random walk, evita di costruire signal su questo; Hurst < 0.45 = filtro mean-reversion causale efficace, sfrutta con isteresi; efficiency_ratio < 0.2 = no signal, non costruire; spectral_entropy > 0.8 = white noise, regime non modellabile; tail_index < 2.5 = saturazione dei sensori, riduci leverage; seasonality < 0.05 = feature temporali sono rumore, NON usarle. Preferisci pattern semplici e tarabili: poche condizioni in AND, soglie con margine, isteresi entry/exit. Lookback consigliato: 60-120 barre. Archetipo dominante: sistema di controllo ingegneristico con SNR e robustezza.",
"focus_metrics": ["efficiency_ratio", "spectral_entropy", "tail_left", "autocorr_recent"]
},
"military_strategist": {
"directive": "Distingui campagna offensiva da campagna difensiva e adatta dottrina. Vol regime medium/low + skew positivo + kurt moderata = terreno favorevole all'attacco (costruisci entry direzionali su breakout o momentum). Vol regime high + kurt elevata = terreno ostile (difesa: posizioni limitate, exit rapide, gate restrittivi). AR(1) > 0 = vento alle spalle, carica con momentum; AR(1) negativo = imboscata possibile, preferisci contrarian; Hurst > 0.55 = posizione difendibile, hold trade; structural_uptrend > 0.7 = terreno occupato dall'avversario (decidi se attaccare o ritirarti); compression < 0.5 = preparazione attacco silenziosa, posizionati per breakout; vol_pct alta = artiglieria nemica attiva, ritirata. Concentrazione: poche condizioni forti. Sorpresa: contrarian su consensus estremo. Lookback consigliato: 100-200 barre. Archetipo dominante: stratega militare che bilancia offesa e difesa.",
"focus_metrics": ["structural_uptrend", "compression", "vol_pct", "tail_left"]
},
"psychologist": {
"directive": "Il mercato e folla con emozioni misurabili. Skew e kurt sono il termometro emotivo: skew neg + kurt alta = paura ricorrente (capitulation spikes, cattura il rimbalzo); skew pos + kurt alta = euforia (FOMO spikes, preferisci fade gli estremi al rialzo); skew vicino a zero + kurt bassa = apatia o range (gioca i bordi del range). AR(1) recente molto sopra baseline = euforia coordinata in corso, posizionati contro l'ultimo arrivato; Hurst > 0.55 = trance collettiva (trend trance, dura piu del razionale); tail_left pesante (Hill < 2.5) = paura sistemica strutturale, contrarian sulla capitulation; spectral_entropy alta = caos comportamentale, riduci dimensionalita del signal; vol_pct estremo = momentum emozionale puro, fade gli estremi. Sfrutta crossover di oscillatori in regimi razionali (kurt vicina a 3). Lookback consigliato: 50-120 barre. Archetipo dominante: psicologo del comportamento collettivo.",
"focus_metrics": ["tail_left", "tail_right", "autocorr_recent", "spectral_entropy"]
}
}
}
@@ -0,0 +1,140 @@
{
"rules": [
{
"condition": {
"op": "and",
"args": [
{
"op": "gt",
"args": [
{
"kind": "indicator",
"name": "realized_vol",
"params": [150]
},
{
"kind": "literal",
"value": 0.01
}
]
},
{
"op": "lt",
"args": [
{
"kind": "indicator",
"name": "atr_pct",
"params": [150]
},
{
"kind": "literal",
"value": 0.02
}
]
},
{
"op": "gt",
"args": [
{
"kind": "indicator",
"name": "sma_pct",
"params": [150]
},
{
"kind": "literal",
"value": 0.05
}
]
}
]
},
"action": "entry-long"
},
{
"condition": {
"op": "and",
"args": [
{
"op": "lt",
"args": [
{
"kind": "indicator",
"name": "realized_vol",
"params": [150]
},
{
"kind": "literal",
"value": 0.005
}
]
},
{
"op": "gt",
"args": [
{
"kind": "indicator",
"name": "atr_pct",
"params": [150]
},
{
"kind": "literal",
"value": 0.03
}
]
},
{
"op": "lt",
"args": [
{
"kind": "indicator",
"name": "sma_pct",
"params": [150]
},
{
"kind": "literal",
"value": -0.05
}
]
}
]
},
"action": "entry-short"
},
{
"condition": {
"op": "or",
"args": [
{
"op": "eq",
"args": [
{
"kind": "indicator",
"name": "sma_pct",
"params": [150]
},
{
"kind": "literal",
"value": 0.0
}
]
},
{
"op": "lt",
"args": [
{
"kind": "indicator",
"name": "realized_vol",
"params": [150]
},
{
"kind": "literal",
"value": 0.001
}
]
}
]
},
"action": "exit"
}
]
}
@@ -0,0 +1,84 @@
{
"rules": [
{
"condition": {
"op": "and",
"args": [
{
"op": "gt",
"args": [
{
"kind": "indicator",
"name": "realized_vol",
"params": [
150
]
},
{
"kind": "literal",
"value": 0.007
}
]
},
{
"op": "lt",
"args": [
{
"kind": "indicator",
"name": "atr_pct",
"params": [
150
]
},
{
"kind": "literal",
"value": 0.0042
}
]
}
]
},
"action": "entry-long"
},
{
"condition": {
"op": "or",
"args": [
{
"op": "crossunder",
"args": [
{
"kind": "indicator",
"name": "rsi",
"params": [
14
]
},
{
"kind": "literal",
"value": 70.0
}
]
},
{
"op": "gt",
"args": [
{
"kind": "indicator",
"name": "atr_pct",
"params": [
150
]
},
{
"kind": "literal",
"value": 0.007
}
]
}
]
},
"action": "exit"
}
]
}
@@ -0,0 +1,120 @@
{
"rules": [
{
"condition": {
"op": "and",
"args": [
{
"op": "lt",
"args": [
{
"kind": "indicator",
"name": "rsi",
"params": [
14
]
},
{
"kind": "literal",
"value": 30.0
}
]
},
{
"op": "gt",
"args": [
{
"kind": "indicator",
"name": "atr_pct",
"params": [
14
]
},
{
"kind": "literal",
"value": 0.01
}
]
},
{
"op": "lt",
"args": [
{
"kind": "indicator",
"name": "macd_pct",
"params": [
12,
26,
9
]
},
{
"kind": "literal",
"value": -0.005
}
]
}
]
},
"action": "entry-long"
},
{
"condition": {
"op": "or",
"args": [
{
"op": "gt",
"args": [
{
"kind": "indicator",
"name": "rsi",
"params": [
14
]
},
{
"kind": "literal",
"value": 70.0
}
]
},
{
"op": "lt",
"args": [
{
"kind": "indicator",
"name": "atr_pct",
"params": [
14
]
},
{
"kind": "literal",
"value": 0.005
}
]
},
{
"op": "gt",
"args": [
{
"kind": "indicator",
"name": "macd_pct",
"params": [
12,
26,
9
]
},
{
"kind": "literal",
"value": 0.005
}
]
}
]
},
"action": "exit"
}
]
}
+634
View File
@@ -0,0 +1,634 @@
{
"run_id": "0392aa1c2d644459afa5a23f43c38ac6",
"run_name": "phase1-btc-100-001",
"n_folds": 4,
"top_k_requested": 10,
"top_k_evaluated": 10,
"symbol": "BTC-PERPETUAL",
"timeframe": "1h",
"start": "2018-09-01T00:00:00+00:00",
"end": "2026-01-01T00:00:00+00:00",
"ohlcv_bars": 64297,
"results": [
{
"genome_id": "23a24989e2ed0f84",
"fitness_is": 0.25047738452013774,
"sharpe_is": 0.5152551943136504,
"folds": [
{
"fold": 0,
"fitness": 0.4454407113532186,
"sharpe": 0.940612398713799,
"dsr": 0.09856838950479485,
"dsr_pvalue": 0.9014316104952051,
"return": 0.12691347502077277,
"max_dd": 0.08467873586477132,
"n_trades": 50,
"test_start": "2022-05-02 12:00:00+00:00",
"test_end": "2023-04-02 08:00:00+00:00"
},
{
"fold": 1,
"fitness": 0.33651846595831003,
"sharpe": 0.6297236131089199,
"dsr": 0.05704792862404472,
"dsr_pvalue": 0.9429520713759553,
"return": 0.16916039262594973,
"max_dd": 0.2420995418754207,
"n_trades": 61,
"test_start": "2023-04-02 09:00:00+00:00",
"test_end": "2024-03-02 05:00:00+00:00"
},
{
"fold": 2,
"fitness": 0.08496628060413243,
"sharpe": -0.291593157960215,
"dsr": 0.006828013272159182,
"dsr_pvalue": 0.9931719867278408,
"return": -0.06496567446731383,
"max_dd": 0.1933746053658072,
"n_trades": 72,
"test_start": "2024-03-02 06:00:00+00:00",
"test_end": "2025-01-31 02:00:00+00:00"
},
{
"fold": 3,
"fitness": 0.10029133262703777,
"sharpe": -0.08634860278039096,
"dsr": 0.01165220864726802,
"dsr_pvalue": 0.988347791352732,
"return": -0.007636913661893563,
"max_dd": 0.061872083556258554,
"n_trades": 29,
"test_start": "2025-01-31 03:00:00+00:00",
"test_end": "2025-12-31 23:00:00+00:00"
}
],
"fitness_oos_mean": 0.2418041976356747,
"fitness_oos_min": 0.08496628060413243,
"fitness_oos_max": 0.4454407113532186,
"fitness_oos_std": 0.15416115393045135,
"sharpe_oos_mean": 0.2980985627705282,
"sharpe_oos_min": -0.291593157960215,
"robust_score": 0.08496628060413243
},
{
"genome_id": "ddda3a5d7fcf95d8",
"fitness_is": 0.24345612215631274,
"sharpe_is": 0.4859910845049414,
"folds": [
{
"fold": 0,
"fitness": 0.38630034957174436,
"sharpe": 0.6292230751631145,
"dsr": 0.05660411470808308,
"dsr_pvalue": 0.9433958852919169,
"return": 0.0808908197444953,
"max_dd": 0.08123461559976199,
"n_trades": 44,
"test_start": "2022-05-02 12:00:00+00:00",
"test_end": "2023-04-02 08:00:00+00:00"
},
{
"fold": 1,
"fitness": 0.3444344428619903,
"sharpe": 0.670768621640302,
"dsr": 0.06172291934756436,
"dsr_pvalue": 0.9382770806524356,
"return": 0.1769344040247678,
"max_dd": 0.24038922925189188,
"n_trades": 46,
"test_start": "2023-04-02 09:00:00+00:00",
"test_end": "2024-03-02 05:00:00+00:00"
},
{
"fold": 2,
"fitness": 0.08496628060413243,
"sharpe": -0.291593157960215,
"dsr": 0.006828013272159182,
"dsr_pvalue": 0.9931719867278408,
"return": -0.06496567446731383,
"max_dd": 0.1933746053658072,
"n_trades": 72,
"test_start": "2024-03-02 06:00:00+00:00",
"test_end": "2025-01-31 02:00:00+00:00"
},
{
"fold": 3,
"fitness": 0.10134367585820397,
"sharpe": -0.25028965416710486,
"dsr": 0.0070613574740692985,
"dsr_pvalue": 0.9929386425259307,
"return": -0.01793962898000656,
"max_dd": 0.05380115145734951,
"n_trades": 18,
"test_start": "2025-01-31 03:00:00+00:00",
"test_end": "2025-12-31 23:00:00+00:00"
}
],
"fitness_oos_mean": 0.22926118722401778,
"fitness_oos_min": 0.08496628060413243,
"fitness_oos_max": 0.38630034957174436,
"fitness_oos_std": 0.13703109785336132,
"sharpe_oos_mean": 0.18952722116902415,
"sharpe_oos_min": -0.291593157960215,
"robust_score": 0.08496628060413243
},
{
"genome_id": "75fffb926a15ff30",
"fitness_is": 0.2317839302261713,
"sharpe_is": 0.5074946608465971,
"folds": [
{
"fold": 0,
"fitness": 0.06821040478798467,
"sharpe": -0.42408026342979865,
"dsr": 0.004964388516380099,
"dsr_pvalue": 0.9950356114836199,
"return": -0.004117833402575766,
"max_dd": 0.01551842276077859,
"n_trades": 12,
"test_start": "2022-05-02 12:00:00+00:00",
"test_end": "2023-04-02 08:00:00+00:00"
},
{
"fold": 1,
"fitness": 0.30367484453258525,
"sharpe": 0.7648008345130556,
"dsr": 0.0596988345287043,
"dsr_pvalue": 0.9403011654712957,
"return": 0.040989700605122525,
"max_dd": 0.036878373324561994,
"n_trades": 31,
"test_start": "2023-04-02 09:00:00+00:00",
"test_end": "2024-03-02 05:00:00+00:00"
},
{
"fold": 2,
"fitness": 0.014172300674502565,
"sharpe": -1.443859476065376,
"dsr": 9.400808942425867e-05,
"dsr_pvalue": 0.9999059919105757,
"return": -0.02894431062955649,
"max_dd": 0.036019686142963456,
"n_trades": 7,
"test_start": "2024-03-02 06:00:00+00:00",
"test_end": "2025-01-31 02:00:00+00:00"
},
{
"fold": 3,
"fitness": 0.15455497647301103,
"sharpe": 0.11384980407793575,
"dsr": 0.018924861238402986,
"dsr_pvalue": 0.981075138761597,
"return": 0.004017688385377749,
"max_dd": 0.034520559216801125,
"n_trades": 21,
"test_start": "2025-01-31 03:00:00+00:00",
"test_end": "2025-12-31 23:00:00+00:00"
}
],
"fitness_oos_mean": 0.1351531316170209,
"fitness_oos_min": 0.014172300674502565,
"fitness_oos_max": 0.30367484453258525,
"fitness_oos_std": 0.1094231344810833,
"sharpe_oos_mean": -0.24732227522604583,
"sharpe_oos_min": -1.443859476065376,
"robust_score": 0.014172300674502565
},
{
"genome_id": "1cba64abfb67fd63",
"fitness_is": 0.24779915639787098,
"sharpe_is": 0.686744434641618,
"folds": [
{
"fold": 0,
"fitness": 0.23623418911693914,
"sharpe": 0.3904002210885913,
"dsr": 0.03526702699157904,
"dsr_pvalue": 0.964732973008421,
"return": 0.04525398577066886,
"max_dd": 0.09020089585606307,
"n_trades": 71,
"test_start": "2022-05-02 12:00:00+00:00",
"test_end": "2023-04-02 08:00:00+00:00"
},
{
"fold": 1,
"fitness": 0.018226313030461766,
"sharpe": -1.287441608051841,
"dsr": 0.0005995136178100196,
"dsr_pvalue": 0.99940048638219,
"return": -0.08712458661694344,
"max_dd": 0.08774369104277313,
"n_trades": 23,
"test_start": "2023-04-02 09:00:00+00:00",
"test_end": "2024-03-02 05:00:00+00:00"
},
{
"fold": 2,
"fitness": 0.005524367416662587,
"sharpe": -1.78701596924334,
"dsr": 2.01452172810692e-05,
"dsr_pvalue": 0.9999798547827189,
"return": -0.338652173037419,
"max_dd": 0.3725940753872713,
"n_trades": 60,
"test_start": "2024-03-02 06:00:00+00:00",
"test_end": "2025-01-31 02:00:00+00:00"
},
{
"fold": 3,
"fitness": 0.0633884962634609,
"sharpe": -0.551177699295004,
"dsr": 0.003506443748025072,
"dsr_pvalue": 0.9964935562519749,
"return": -0.05484487559184925,
"max_dd": 0.10783012035662387,
"n_trades": 53,
"test_start": "2025-01-31 03:00:00+00:00",
"test_end": "2025-12-31 23:00:00+00:00"
}
],
"fitness_oos_mean": 0.0808433414568811,
"fitness_oos_min": 0.005524367416662587,
"fitness_oos_max": 0.23623418911693914,
"fitness_oos_std": 0.09225620203631806,
"sharpe_oos_mean": -0.8088087638753985,
"sharpe_oos_min": -1.78701596924334,
"robust_score": 0.005524367416662587
},
{
"genome_id": "238e481262c1594c",
"fitness_is": 0.2604059091373716,
"sharpe_is": 0.4370965264203703,
"folds": [
{
"fold": 0,
"fitness": 0.7584616560730608,
"sharpe": 2.414722662010953,
"dsr": 0.5606058252346415,
"dsr_pvalue": 0.43939417476535847,
"return": 0.0933177386269215,
"max_dd": 0.023570677223421994,
"n_trades": 3,
"test_start": "2022-05-02 12:00:00+00:00",
"test_end": "2023-04-02 08:00:00+00:00"
},
{
"fold": 1,
"fitness": 0.16218807661575446,
"sharpe": 0.012873968372421442,
"dsr": 0.015091968754361085,
"dsr_pvalue": 0.9849080312456389,
"return": 0.0016924869828314204,
"max_dd": 0.14842112455963283,
"n_trades": 15,
"test_start": "2023-04-02 09:00:00+00:00",
"test_end": "2024-03-02 05:00:00+00:00"
},
{
"fold": 2,
"fitness": 0.0,
"sharpe": 0.0,
"dsr": 0.0,
"dsr_pvalue": 1.0,
"return": 0.0,
"max_dd": 0.0,
"n_trades": 0,
"test_start": "2024-03-02 06:00:00+00:00",
"test_end": "2025-01-31 02:00:00+00:00"
},
{
"fold": 3,
"fitness": 0.6516189601780499,
"sharpe": 2.0590304959441346,
"dsr": 0.41524419503560983,
"dsr_pvalue": 0.5847558049643902,
"return": 0.22153641670021273,
"max_dd": 0.0736556977759442,
"n_trades": 11,
"test_start": "2025-01-31 03:00:00+00:00",
"test_end": "2025-12-31 23:00:00+00:00"
}
],
"fitness_oos_mean": 0.3930671732167163,
"fitness_oos_min": 0.0,
"fitness_oos_max": 0.7584616560730608,
"fitness_oos_std": 0.3194405713707209,
"sharpe_oos_mean": 1.1216567815818772,
"sharpe_oos_min": 0.0,
"robust_score": 0.0
},
{
"genome_id": "9d484d1d7154e3d8",
"fitness_is": 0.24281082171633084,
"sharpe_is": 0.5383459590297983,
"folds": [
{
"fold": 0,
"fitness": 0.0,
"sharpe": -0.17966776746614493,
"dsr": 0.010596058628339637,
"dsr_pvalue": 0.9894039413716603,
"return": -0.00014822652679680193,
"max_dd": 0.00043372790281119014,
"n_trades": 1,
"test_start": "2022-05-02 12:00:00+00:00",
"test_end": "2023-04-02 08:00:00+00:00"
},
{
"fold": 1,
"fitness": 0.03510752668391289,
"sharpe": -0.9516408190993537,
"dsr": 2.2816330745239768e-07,
"dsr_pvalue": 0.9999997718366925,
"return": -0.022264081410075964,
"max_dd": 0.026510937718929387,
"n_trades": 3,
"test_start": "2023-04-02 09:00:00+00:00",
"test_end": "2024-03-02 05:00:00+00:00"
},
{
"fold": 2,
"fitness": 0.0,
"sharpe": -1.2370529718735044,
"dsr": 3.550540879152349e-10,
"dsr_pvalue": 0.9999999996449459,
"return": -0.005011777500302128,
"max_dd": 0.005011777500302128,
"n_trades": 1,
"test_start": "2024-03-02 06:00:00+00:00",
"test_end": "2025-01-31 02:00:00+00:00"
},
{
"fold": 3,
"fitness": 0.009397695173924475,
"sharpe": -1.662121842926161,
"dsr": 6.564863915519434e-10,
"dsr_pvalue": 0.9999999993435136,
"return": -0.023018388061418094,
"max_dd": 0.02710730869264358,
"n_trades": 3,
"test_start": "2025-01-31 03:00:00+00:00",
"test_end": "2025-12-31 23:00:00+00:00"
}
],
"fitness_oos_mean": 0.011126305464459342,
"fitness_oos_min": 0.0,
"fitness_oos_max": 0.03510752668391289,
"fitness_oos_std": 0.014367292814669608,
"sharpe_oos_mean": -1.0076208503412911,
"sharpe_oos_min": -1.662121842926161,
"robust_score": 0.0
},
{
"genome_id": "e1f8b425fec3b95e",
"fitness_is": 0.23502719247621787,
"sharpe_is": 0.49764640014032885,
"folds": [
{
"fold": 0,
"fitness": 0.0,
"sharpe": 0.0,
"dsr": 0.0,
"dsr_pvalue": 1.0,
"return": 0.0,
"max_dd": 0.0,
"n_trades": 0,
"test_start": "2022-05-02 12:00:00+00:00",
"test_end": "2023-04-02 08:00:00+00:00"
},
{
"fold": 1,
"fitness": 0.0,
"sharpe": -1.3160615555061568,
"dsr": 2.0480110912755528e-14,
"dsr_pvalue": 0.9999999999999796,
"return": -0.004197667464114874,
"max_dd": 0.004197667464114874,
"n_trades": 1,
"test_start": "2023-04-02 09:00:00+00:00",
"test_end": "2024-03-02 05:00:00+00:00"
},
{
"fold": 2,
"fitness": 0.0,
"sharpe": 0.0,
"dsr": 0.0,
"dsr_pvalue": 1.0,
"return": 0.0,
"max_dd": 0.0,
"n_trades": 0,
"test_start": "2024-03-02 06:00:00+00:00",
"test_end": "2025-01-31 02:00:00+00:00"
},
{
"fold": 3,
"fitness": 0.0,
"sharpe": 0.0,
"dsr": 0.0,
"dsr_pvalue": 1.0,
"return": 0.0,
"max_dd": 0.0,
"n_trades": 0,
"test_start": "2025-01-31 03:00:00+00:00",
"test_end": "2025-12-31 23:00:00+00:00"
}
],
"fitness_oos_mean": 0.0,
"fitness_oos_min": 0.0,
"fitness_oos_max": 0.0,
"fitness_oos_std": 0.0,
"sharpe_oos_mean": -0.3290153888765392,
"sharpe_oos_min": -1.3160615555061568,
"robust_score": 0.0
},
{
"genome_id": "de4c3cce96bc6233",
"fitness_is": 0.22983732104518312,
"sharpe_is": 0.3042292766075092,
"folds": [
{
"fold": 0,
"fitness": 0.7099634427811606,
"sharpe": 2.175438924887508,
"dsr": 0.4516683289747405,
"dsr_pvalue": 0.5483316710252595,
"return": 0.05162527264229322,
"max_dd": 0.013388106636764294,
"n_trades": 5,
"test_start": "2022-05-02 12:00:00+00:00",
"test_end": "2023-04-02 08:00:00+00:00"
},
{
"fold": 1,
"fitness": 0.11214839774268798,
"sharpe": -0.09717412830030508,
"dsr": 0.011407831629063609,
"dsr_pvalue": 0.9885921683709364,
"return": -0.010810784900084469,
"max_dd": 0.146727446687109,
"n_trades": 17,
"test_start": "2023-04-02 09:00:00+00:00",
"test_end": "2024-03-02 05:00:00+00:00"
},
{
"fold": 2,
"fitness": 0.0,
"sharpe": 0.0,
"dsr": 0.0,
"dsr_pvalue": 1.0,
"return": 0.0,
"max_dd": 0.0,
"n_trades": 0,
"test_start": "2024-03-02 06:00:00+00:00",
"test_end": "2025-01-31 02:00:00+00:00"
},
{
"fold": 3,
"fitness": 0.48948263578942797,
"sharpe": 1.2111587578615337,
"dsr": 0.15099566068576153,
"dsr_pvalue": 0.8490043393142385,
"return": 0.09392150449517889,
"max_dd": 0.09248924622174504,
"n_trades": 17,
"test_start": "2025-01-31 03:00:00+00:00",
"test_end": "2025-12-31 23:00:00+00:00"
}
],
"fitness_oos_mean": 0.32789861907831913,
"fitness_oos_min": 0.0,
"fitness_oos_max": 0.7099634427811606,
"fitness_oos_std": 0.28554710047880966,
"sharpe_oos_mean": 0.8223558886121842,
"sharpe_oos_min": -0.09717412830030508,
"robust_score": 0.0
},
{
"genome_id": "14711c6816c009bb",
"fitness_is": 0.2231252597460312,
"sharpe_is": 0.5100989455793494,
"folds": [
{
"fold": 0,
"fitness": 0.0,
"sharpe": 0.0,
"dsr": 0.0,
"dsr_pvalue": 1.0,
"return": 0.0,
"max_dd": 0.0,
"n_trades": 0,
"test_start": "2022-05-02 12:00:00+00:00",
"test_end": "2023-04-02 08:00:00+00:00"
},
{
"fold": 1,
"fitness": 0.041279942400821015,
"sharpe": -0.7516739863723977,
"dsr": 0.0003109810948998818,
"dsr_pvalue": 0.9996890189051001,
"return": -0.002913092808893847,
"max_dd": 0.003336150788066372,
"n_trades": 3,
"test_start": "2023-04-02 09:00:00+00:00",
"test_end": "2024-03-02 05:00:00+00:00"
},
{
"fold": 2,
"fitness": 0.0,
"sharpe": 0.0,
"dsr": 0.0,
"dsr_pvalue": 1.0,
"return": 0.0,
"max_dd": 0.0,
"n_trades": 0,
"test_start": "2024-03-02 06:00:00+00:00",
"test_end": "2025-01-31 02:00:00+00:00"
},
{
"fold": 3,
"fitness": 0.0,
"sharpe": 0.0,
"dsr": 0.0,
"dsr_pvalue": 1.0,
"return": 0.0,
"max_dd": 0.0,
"n_trades": 0,
"test_start": "2025-01-31 03:00:00+00:00",
"test_end": "2025-12-31 23:00:00+00:00"
}
],
"fitness_oos_mean": 0.010319985600205254,
"fitness_oos_min": 0.0,
"fitness_oos_max": 0.041279942400821015,
"fitness_oos_std": 0.017874739392934696,
"sharpe_oos_mean": -0.18791849659309942,
"sharpe_oos_min": -0.7516739863723977,
"robust_score": 0.0
},
{
"genome_id": "c7f6f7c415a8c1df",
"fitness_is": 0.2231252597460312,
"sharpe_is": 0.5100989455793494,
"folds": [
{
"fold": 0,
"fitness": 0.0,
"sharpe": 0.0,
"dsr": 0.0,
"dsr_pvalue": 1.0,
"return": 0.0,
"max_dd": 0.0,
"n_trades": 0,
"test_start": "2022-05-02 12:00:00+00:00",
"test_end": "2023-04-02 08:00:00+00:00"
},
{
"fold": 1,
"fitness": 0.041279942400821015,
"sharpe": -0.7516739863723977,
"dsr": 0.0003109810948998818,
"dsr_pvalue": 0.9996890189051001,
"return": -0.002913092808893847,
"max_dd": 0.003336150788066372,
"n_trades": 3,
"test_start": "2023-04-02 09:00:00+00:00",
"test_end": "2024-03-02 05:00:00+00:00"
},
{
"fold": 2,
"fitness": 0.0,
"sharpe": 0.0,
"dsr": 0.0,
"dsr_pvalue": 1.0,
"return": 0.0,
"max_dd": 0.0,
"n_trades": 0,
"test_start": "2024-03-02 06:00:00+00:00",
"test_end": "2025-01-31 02:00:00+00:00"
},
{
"fold": 3,
"fitness": 0.0,
"sharpe": 0.0,
"dsr": 0.0,
"dsr_pvalue": 1.0,
"return": 0.0,
"max_dd": 0.0,
"n_trades": 0,
"test_start": "2025-01-31 03:00:00+00:00",
"test_end": "2025-12-31 23:00:00+00:00"
}
],
"fitness_oos_mean": 0.010319985600205254,
"fitness_oos_min": 0.0,
"fitness_oos_max": 0.041279942400821015,
"fitness_oos_std": 0.017874739392934696,
"sharpe_oos_mean": -0.18791849659309942,
"sharpe_oos_min": -0.7516739863723977,
"robust_score": 0.0
}
]
}
+634
View File
@@ -0,0 +1,634 @@
{
"run_id": "0392aa1c2d644459afa5a23f43c38ac6",
"run_name": "phase1-btc-100-001",
"n_folds": 4,
"top_k_requested": 10,
"top_k_evaluated": 10,
"symbol": "BTC-PERPETUAL",
"timeframe": "1h",
"start": "2018-09-01T00:00:00+00:00",
"end": "2026-01-01T00:00:00+00:00",
"ohlcv_bars": 64297,
"results": [
{
"genome_id": "23a24989e2ed0f84",
"fitness_is": 0.25047738452013774,
"sharpe_is": 0.5152551943136504,
"folds": [
{
"fold": 0,
"fitness": 0.4454407113532186,
"sharpe": 0.940612398713799,
"dsr": 0.09856838950479485,
"dsr_pvalue": 0.9014316104952051,
"return": 0.12691347502077277,
"max_dd": 0.08467873586477132,
"n_trades": 50,
"test_start": "2022-05-02 12:00:00+00:00",
"test_end": "2023-04-02 08:00:00+00:00"
},
{
"fold": 1,
"fitness": 0.33651846595831003,
"sharpe": 0.6297236131089199,
"dsr": 0.05704792862404472,
"dsr_pvalue": 0.9429520713759553,
"return": 0.16916039262594973,
"max_dd": 0.2420995418754207,
"n_trades": 61,
"test_start": "2023-04-02 09:00:00+00:00",
"test_end": "2024-03-02 05:00:00+00:00"
},
{
"fold": 2,
"fitness": 0.08496628060413243,
"sharpe": -0.291593157960215,
"dsr": 0.006828013272159182,
"dsr_pvalue": 0.9931719867278408,
"return": -0.06496567446731383,
"max_dd": 0.1933746053658072,
"n_trades": 72,
"test_start": "2024-03-02 06:00:00+00:00",
"test_end": "2025-01-31 02:00:00+00:00"
},
{
"fold": 3,
"fitness": 0.10029133262703777,
"sharpe": -0.08634860278039096,
"dsr": 0.01165220864726802,
"dsr_pvalue": 0.988347791352732,
"return": -0.007636913661893563,
"max_dd": 0.061872083556258554,
"n_trades": 29,
"test_start": "2025-01-31 03:00:00+00:00",
"test_end": "2025-12-31 23:00:00+00:00"
}
],
"fitness_oos_mean": 0.2418041976356747,
"fitness_oos_min": 0.08496628060413243,
"fitness_oos_max": 0.4454407113532186,
"fitness_oos_std": 0.15416115393045135,
"sharpe_oos_mean": 0.2980985627705282,
"sharpe_oos_min": -0.291593157960215,
"robust_score": 0.08496628060413243
},
{
"genome_id": "ddda3a5d7fcf95d8",
"fitness_is": 0.24345612215631274,
"sharpe_is": 0.4859910845049414,
"folds": [
{
"fold": 0,
"fitness": 0.38630034957174436,
"sharpe": 0.6292230751631145,
"dsr": 0.05660411470808308,
"dsr_pvalue": 0.9433958852919169,
"return": 0.0808908197444953,
"max_dd": 0.08123461559976199,
"n_trades": 44,
"test_start": "2022-05-02 12:00:00+00:00",
"test_end": "2023-04-02 08:00:00+00:00"
},
{
"fold": 1,
"fitness": 0.3444344428619903,
"sharpe": 0.670768621640302,
"dsr": 0.06172291934756436,
"dsr_pvalue": 0.9382770806524356,
"return": 0.1769344040247678,
"max_dd": 0.24038922925189188,
"n_trades": 46,
"test_start": "2023-04-02 09:00:00+00:00",
"test_end": "2024-03-02 05:00:00+00:00"
},
{
"fold": 2,
"fitness": 0.08496628060413243,
"sharpe": -0.291593157960215,
"dsr": 0.006828013272159182,
"dsr_pvalue": 0.9931719867278408,
"return": -0.06496567446731383,
"max_dd": 0.1933746053658072,
"n_trades": 72,
"test_start": "2024-03-02 06:00:00+00:00",
"test_end": "2025-01-31 02:00:00+00:00"
},
{
"fold": 3,
"fitness": 0.10134367585820397,
"sharpe": -0.25028965416710486,
"dsr": 0.0070613574740692985,
"dsr_pvalue": 0.9929386425259307,
"return": -0.01793962898000656,
"max_dd": 0.05380115145734951,
"n_trades": 18,
"test_start": "2025-01-31 03:00:00+00:00",
"test_end": "2025-12-31 23:00:00+00:00"
}
],
"fitness_oos_mean": 0.22926118722401778,
"fitness_oos_min": 0.08496628060413243,
"fitness_oos_max": 0.38630034957174436,
"fitness_oos_std": 0.13703109785336132,
"sharpe_oos_mean": 0.18952722116902415,
"sharpe_oos_min": -0.291593157960215,
"robust_score": 0.08496628060413243
},
{
"genome_id": "1cba64abfb67fd63",
"fitness_is": 0.24779915639787098,
"sharpe_is": 0.686744434641618,
"folds": [
{
"fold": 0,
"fitness": 0.23623418911693914,
"sharpe": 0.3904002210885913,
"dsr": 0.03526702699157904,
"dsr_pvalue": 0.964732973008421,
"return": 0.04525398577066886,
"max_dd": 0.09020089585606307,
"n_trades": 71,
"test_start": "2022-05-02 12:00:00+00:00",
"test_end": "2023-04-02 08:00:00+00:00"
},
{
"fold": 1,
"fitness": 0.018226313030461766,
"sharpe": -1.287441608051841,
"dsr": 0.0005995136178100196,
"dsr_pvalue": 0.99940048638219,
"return": -0.08712458661694344,
"max_dd": 0.08774369104277313,
"n_trades": 23,
"test_start": "2023-04-02 09:00:00+00:00",
"test_end": "2024-03-02 05:00:00+00:00"
},
{
"fold": 2,
"fitness": 0.005524367416662587,
"sharpe": -1.78701596924334,
"dsr": 2.01452172810692e-05,
"dsr_pvalue": 0.9999798547827189,
"return": -0.338652173037419,
"max_dd": 0.3725940753872713,
"n_trades": 60,
"test_start": "2024-03-02 06:00:00+00:00",
"test_end": "2025-01-31 02:00:00+00:00"
},
{
"fold": 3,
"fitness": 0.0633884962634609,
"sharpe": -0.551177699295004,
"dsr": 0.003506443748025072,
"dsr_pvalue": 0.9964935562519749,
"return": -0.05484487559184925,
"max_dd": 0.10783012035662387,
"n_trades": 53,
"test_start": "2025-01-31 03:00:00+00:00",
"test_end": "2025-12-31 23:00:00+00:00"
}
],
"fitness_oos_mean": 0.0808433414568811,
"fitness_oos_min": 0.005524367416662587,
"fitness_oos_max": 0.23623418911693914,
"fitness_oos_std": 0.09225620203631806,
"sharpe_oos_mean": -0.8088087638753985,
"sharpe_oos_min": -1.78701596924334,
"robust_score": 0.005524367416662587
},
{
"genome_id": "238e481262c1594c",
"fitness_is": 0.2604059091373716,
"sharpe_is": 0.4370965264203703,
"folds": [
{
"fold": 0,
"fitness": 0.0,
"sharpe": 2.414722662010953,
"dsr": 0.5606058252346415,
"dsr_pvalue": 0.43939417476535847,
"return": 0.0933177386269215,
"max_dd": 0.023570677223421994,
"n_trades": 3,
"test_start": "2022-05-02 12:00:00+00:00",
"test_end": "2023-04-02 08:00:00+00:00"
},
{
"fold": 1,
"fitness": 0.16218807661575446,
"sharpe": 0.012873968372421442,
"dsr": 0.015091968754361085,
"dsr_pvalue": 0.9849080312456389,
"return": 0.0016924869828314204,
"max_dd": 0.14842112455963283,
"n_trades": 15,
"test_start": "2023-04-02 09:00:00+00:00",
"test_end": "2024-03-02 05:00:00+00:00"
},
{
"fold": 2,
"fitness": 0.0,
"sharpe": 0.0,
"dsr": 0.0,
"dsr_pvalue": 1.0,
"return": 0.0,
"max_dd": 0.0,
"n_trades": 0,
"test_start": "2024-03-02 06:00:00+00:00",
"test_end": "2025-01-31 02:00:00+00:00"
},
{
"fold": 3,
"fitness": 0.6516189601780499,
"sharpe": 2.0590304959441346,
"dsr": 0.41524419503560983,
"dsr_pvalue": 0.5847558049643902,
"return": 0.22153641670021273,
"max_dd": 0.0736556977759442,
"n_trades": 11,
"test_start": "2025-01-31 03:00:00+00:00",
"test_end": "2025-12-31 23:00:00+00:00"
}
],
"fitness_oos_mean": 0.2034517591984511,
"fitness_oos_min": 0.0,
"fitness_oos_max": 0.6516189601780499,
"fitness_oos_std": 0.2670869559600687,
"sharpe_oos_mean": 1.1216567815818772,
"sharpe_oos_min": 0.0,
"robust_score": 0.0
},
{
"genome_id": "9d484d1d7154e3d8",
"fitness_is": 0.24281082171633084,
"sharpe_is": 0.5383459590297983,
"folds": [
{
"fold": 0,
"fitness": 0.0,
"sharpe": -0.17966776746614493,
"dsr": 0.010596058628339637,
"dsr_pvalue": 0.9894039413716603,
"return": -0.00014822652679680193,
"max_dd": 0.00043372790281119014,
"n_trades": 1,
"test_start": "2022-05-02 12:00:00+00:00",
"test_end": "2023-04-02 08:00:00+00:00"
},
{
"fold": 1,
"fitness": 0.0,
"sharpe": -0.9516408190993537,
"dsr": 2.2816330745239768e-07,
"dsr_pvalue": 0.9999997718366925,
"return": -0.022264081410075964,
"max_dd": 0.026510937718929387,
"n_trades": 3,
"test_start": "2023-04-02 09:00:00+00:00",
"test_end": "2024-03-02 05:00:00+00:00"
},
{
"fold": 2,
"fitness": 0.0,
"sharpe": -1.2370529718735044,
"dsr": 3.550540879152349e-10,
"dsr_pvalue": 0.9999999996449459,
"return": -0.005011777500302128,
"max_dd": 0.005011777500302128,
"n_trades": 1,
"test_start": "2024-03-02 06:00:00+00:00",
"test_end": "2025-01-31 02:00:00+00:00"
},
{
"fold": 3,
"fitness": 0.0,
"sharpe": -1.662121842926161,
"dsr": 6.564863915519434e-10,
"dsr_pvalue": 0.9999999993435136,
"return": -0.023018388061418094,
"max_dd": 0.02710730869264358,
"n_trades": 3,
"test_start": "2025-01-31 03:00:00+00:00",
"test_end": "2025-12-31 23:00:00+00:00"
}
],
"fitness_oos_mean": 0.0,
"fitness_oos_min": 0.0,
"fitness_oos_max": 0.0,
"fitness_oos_std": 0.0,
"sharpe_oos_mean": -1.0076208503412911,
"sharpe_oos_min": -1.662121842926161,
"robust_score": 0.0
},
{
"genome_id": "e1f8b425fec3b95e",
"fitness_is": 0.23502719247621787,
"sharpe_is": 0.49764640014032885,
"folds": [
{
"fold": 0,
"fitness": 0.0,
"sharpe": 0.0,
"dsr": 0.0,
"dsr_pvalue": 1.0,
"return": 0.0,
"max_dd": 0.0,
"n_trades": 0,
"test_start": "2022-05-02 12:00:00+00:00",
"test_end": "2023-04-02 08:00:00+00:00"
},
{
"fold": 1,
"fitness": 0.0,
"sharpe": -1.3160615555061568,
"dsr": 2.0480110912755528e-14,
"dsr_pvalue": 0.9999999999999796,
"return": -0.004197667464114874,
"max_dd": 0.004197667464114874,
"n_trades": 1,
"test_start": "2023-04-02 09:00:00+00:00",
"test_end": "2024-03-02 05:00:00+00:00"
},
{
"fold": 2,
"fitness": 0.0,
"sharpe": 0.0,
"dsr": 0.0,
"dsr_pvalue": 1.0,
"return": 0.0,
"max_dd": 0.0,
"n_trades": 0,
"test_start": "2024-03-02 06:00:00+00:00",
"test_end": "2025-01-31 02:00:00+00:00"
},
{
"fold": 3,
"fitness": 0.0,
"sharpe": 0.0,
"dsr": 0.0,
"dsr_pvalue": 1.0,
"return": 0.0,
"max_dd": 0.0,
"n_trades": 0,
"test_start": "2025-01-31 03:00:00+00:00",
"test_end": "2025-12-31 23:00:00+00:00"
}
],
"fitness_oos_mean": 0.0,
"fitness_oos_min": 0.0,
"fitness_oos_max": 0.0,
"fitness_oos_std": 0.0,
"sharpe_oos_mean": -0.3290153888765392,
"sharpe_oos_min": -1.3160615555061568,
"robust_score": 0.0
},
{
"genome_id": "75fffb926a15ff30",
"fitness_is": 0.2317839302261713,
"sharpe_is": 0.5074946608465971,
"folds": [
{
"fold": 0,
"fitness": 0.06821040478798467,
"sharpe": -0.42408026342979865,
"dsr": 0.004964388516380099,
"dsr_pvalue": 0.9950356114836199,
"return": -0.004117833402575766,
"max_dd": 0.01551842276077859,
"n_trades": 12,
"test_start": "2022-05-02 12:00:00+00:00",
"test_end": "2023-04-02 08:00:00+00:00"
},
{
"fold": 1,
"fitness": 0.30367484453258525,
"sharpe": 0.7648008345130556,
"dsr": 0.0596988345287043,
"dsr_pvalue": 0.9403011654712957,
"return": 0.040989700605122525,
"max_dd": 0.036878373324561994,
"n_trades": 31,
"test_start": "2023-04-02 09:00:00+00:00",
"test_end": "2024-03-02 05:00:00+00:00"
},
{
"fold": 2,
"fitness": 0.0,
"sharpe": -1.443859476065376,
"dsr": 9.400808942425867e-05,
"dsr_pvalue": 0.9999059919105757,
"return": -0.02894431062955649,
"max_dd": 0.036019686142963456,
"n_trades": 7,
"test_start": "2024-03-02 06:00:00+00:00",
"test_end": "2025-01-31 02:00:00+00:00"
},
{
"fold": 3,
"fitness": 0.15455497647301103,
"sharpe": 0.11384980407793575,
"dsr": 0.018924861238402986,
"dsr_pvalue": 0.981075138761597,
"return": 0.004017688385377749,
"max_dd": 0.034520559216801125,
"n_trades": 21,
"test_start": "2025-01-31 03:00:00+00:00",
"test_end": "2025-12-31 23:00:00+00:00"
}
],
"fitness_oos_mean": 0.13161005644839524,
"fitness_oos_min": 0.0,
"fitness_oos_max": 0.30367484453258525,
"fitness_oos_std": 0.11343884193961566,
"sharpe_oos_mean": -0.24732227522604583,
"sharpe_oos_min": -1.443859476065376,
"robust_score": 0.0
},
{
"genome_id": "de4c3cce96bc6233",
"fitness_is": 0.22983732104518312,
"sharpe_is": 0.3042292766075092,
"folds": [
{
"fold": 0,
"fitness": 0.0,
"sharpe": 2.175438924887508,
"dsr": 0.4516683289747405,
"dsr_pvalue": 0.5483316710252595,
"return": 0.05162527264229322,
"max_dd": 0.013388106636764294,
"n_trades": 5,
"test_start": "2022-05-02 12:00:00+00:00",
"test_end": "2023-04-02 08:00:00+00:00"
},
{
"fold": 1,
"fitness": 0.11214839774268798,
"sharpe": -0.09717412830030508,
"dsr": 0.011407831629063609,
"dsr_pvalue": 0.9885921683709364,
"return": -0.010810784900084469,
"max_dd": 0.146727446687109,
"n_trades": 17,
"test_start": "2023-04-02 09:00:00+00:00",
"test_end": "2024-03-02 05:00:00+00:00"
},
{
"fold": 2,
"fitness": 0.0,
"sharpe": 0.0,
"dsr": 0.0,
"dsr_pvalue": 1.0,
"return": 0.0,
"max_dd": 0.0,
"n_trades": 0,
"test_start": "2024-03-02 06:00:00+00:00",
"test_end": "2025-01-31 02:00:00+00:00"
},
{
"fold": 3,
"fitness": 0.48948263578942797,
"sharpe": 1.2111587578615337,
"dsr": 0.15099566068576153,
"dsr_pvalue": 0.8490043393142385,
"return": 0.09392150449517889,
"max_dd": 0.09248924622174504,
"n_trades": 17,
"test_start": "2025-01-31 03:00:00+00:00",
"test_end": "2025-12-31 23:00:00+00:00"
}
],
"fitness_oos_mean": 0.150407758383029,
"fitness_oos_min": 0.0,
"fitness_oos_max": 0.48948263578942797,
"fitness_oos_std": 0.20104759307710415,
"sharpe_oos_mean": 0.8223558886121842,
"sharpe_oos_min": -0.09717412830030508,
"robust_score": 0.0
},
{
"genome_id": "14711c6816c009bb",
"fitness_is": 0.2231252597460312,
"sharpe_is": 0.5100989455793494,
"folds": [
{
"fold": 0,
"fitness": 0.0,
"sharpe": 0.0,
"dsr": 0.0,
"dsr_pvalue": 1.0,
"return": 0.0,
"max_dd": 0.0,
"n_trades": 0,
"test_start": "2022-05-02 12:00:00+00:00",
"test_end": "2023-04-02 08:00:00+00:00"
},
{
"fold": 1,
"fitness": 0.0,
"sharpe": -0.7516739863723977,
"dsr": 0.0003109810948998818,
"dsr_pvalue": 0.9996890189051001,
"return": -0.002913092808893847,
"max_dd": 0.003336150788066372,
"n_trades": 3,
"test_start": "2023-04-02 09:00:00+00:00",
"test_end": "2024-03-02 05:00:00+00:00"
},
{
"fold": 2,
"fitness": 0.0,
"sharpe": 0.0,
"dsr": 0.0,
"dsr_pvalue": 1.0,
"return": 0.0,
"max_dd": 0.0,
"n_trades": 0,
"test_start": "2024-03-02 06:00:00+00:00",
"test_end": "2025-01-31 02:00:00+00:00"
},
{
"fold": 3,
"fitness": 0.0,
"sharpe": 0.0,
"dsr": 0.0,
"dsr_pvalue": 1.0,
"return": 0.0,
"max_dd": 0.0,
"n_trades": 0,
"test_start": "2025-01-31 03:00:00+00:00",
"test_end": "2025-12-31 23:00:00+00:00"
}
],
"fitness_oos_mean": 0.0,
"fitness_oos_min": 0.0,
"fitness_oos_max": 0.0,
"fitness_oos_std": 0.0,
"sharpe_oos_mean": -0.18791849659309942,
"sharpe_oos_min": -0.7516739863723977,
"robust_score": 0.0
},
{
"genome_id": "c7f6f7c415a8c1df",
"fitness_is": 0.2231252597460312,
"sharpe_is": 0.5100989455793494,
"folds": [
{
"fold": 0,
"fitness": 0.0,
"sharpe": 0.0,
"dsr": 0.0,
"dsr_pvalue": 1.0,
"return": 0.0,
"max_dd": 0.0,
"n_trades": 0,
"test_start": "2022-05-02 12:00:00+00:00",
"test_end": "2023-04-02 08:00:00+00:00"
},
{
"fold": 1,
"fitness": 0.0,
"sharpe": -0.7516739863723977,
"dsr": 0.0003109810948998818,
"dsr_pvalue": 0.9996890189051001,
"return": -0.002913092808893847,
"max_dd": 0.003336150788066372,
"n_trades": 3,
"test_start": "2023-04-02 09:00:00+00:00",
"test_end": "2024-03-02 05:00:00+00:00"
},
{
"fold": 2,
"fitness": 0.0,
"sharpe": 0.0,
"dsr": 0.0,
"dsr_pvalue": 1.0,
"return": 0.0,
"max_dd": 0.0,
"n_trades": 0,
"test_start": "2024-03-02 06:00:00+00:00",
"test_end": "2025-01-31 02:00:00+00:00"
},
{
"fold": 3,
"fitness": 0.0,
"sharpe": 0.0,
"dsr": 0.0,
"dsr_pvalue": 1.0,
"return": 0.0,
"max_dd": 0.0,
"n_trades": 0,
"test_start": "2025-01-31 03:00:00+00:00",
"test_end": "2025-12-31 23:00:00+00:00"
}
],
"fitness_oos_mean": 0.0,
"fitness_oos_min": 0.0,
"fitness_oos_max": 0.0,
"fitness_oos_std": 0.0,
"sharpe_oos_mean": -0.18791849659309942,
"sharpe_oos_min": -0.7516739863723977,
"robust_score": 0.0
}
]
}
+634
View File
@@ -0,0 +1,634 @@
{
"run_id": "1b3ac5a72ade4eeca96f63af1c692529",
"run_name": "phase1-btc-100-5m-001",
"n_folds": 4,
"top_k_requested": 10,
"top_k_evaluated": 10,
"symbol": "BTC-PERPETUAL",
"timeframe": "5m",
"start": "2018-09-01T00:00:00+00:00",
"end": "2026-01-01T00:00:00+00:00",
"ohlcv_bars": 771553,
"results": [
{
"genome_id": "f8ca6642adf7e0cd",
"fitness_is": 0.18938156615467444,
"sharpe_is": 0.30869023048836236,
"folds": [
{
"fold": 0,
"fitness": 0.2352965252827188,
"sharpe": 0.3693814228315177,
"dsr": 1.683506715156222e-10,
"dsr_pvalue": 0.9999999998316493,
"return": 0.04254736022830463,
"max_dd": 0.027161767758352023,
"n_trades": 22,
"test_start": "2022-05-02 12:00:00+00:00",
"test_end": "2023-04-02 08:55:00+00:00"
},
{
"fold": 1,
"fitness": 0.225344374241631,
"sharpe": 0.2966596569312926,
"dsr": 1.0070583060973712e-10,
"dsr_pvalue": 0.9999999998992942,
"return": 0.0323635859324094,
"max_dd": 0.020860357367648123,
"n_trades": 4,
"test_start": "2023-04-02 09:00:00+00:00",
"test_end": "2024-03-02 05:55:00+00:00"
},
{
"fold": 2,
"fitness": 0.22012851949138668,
"sharpe": 0.2582559733706436,
"dsr": 6.7095779615621785e-12,
"dsr_pvalue": 0.9999999999932904,
"return": 0.024874816913196707,
"max_dd": 0.01617874217143948,
"n_trades": 6,
"test_start": "2024-03-02 06:00:00+00:00",
"test_end": "2025-01-31 02:55:00+00:00"
},
{
"fold": 3,
"fitness": 0.10824559047403624,
"sharpe": -0.19939596313642038,
"dsr": 4.446294307701592e-18,
"dsr_pvalue": 1.0,
"return": -0.012815761172245588,
"max_dd": 0.030585031985383666,
"n_trades": 4,
"test_start": "2025-01-31 03:00:00+00:00",
"test_end": "2025-12-31 23:55:00+00:00"
}
],
"fitness_oos_mean": 0.1972537523724432,
"fitness_oos_min": 0.10824559047403624,
"fitness_oos_max": 0.2352965252827188,
"fitness_oos_std": 0.05167698584229592,
"sharpe_oos_mean": 0.18122527249925838,
"sharpe_oos_min": -0.19939596313642038,
"robust_score": 0.10824559047403624
},
{
"genome_id": "44e4031ccb1dc02a",
"fitness_is": 0.22571210498548258,
"sharpe_is": 0.3821051381739021,
"folds": [
{
"fold": 0,
"fitness": 0.2750172552747618,
"sharpe": 0.6074693455147703,
"dsr": 1.1545160457503457e-26,
"dsr_pvalue": 1.0,
"return": 0.043108172266182354,
"max_dd": 0.001458755248422002,
"n_trades": 10,
"test_start": "2022-05-02 12:00:00+00:00",
"test_end": "2023-04-02 08:55:00+00:00"
},
{
"fold": 1,
"fitness": 0.0,
"sharpe": 0.06258366543576925,
"dsr": 2.937538417346168e-13,
"dsr_pvalue": 0.9999999999997062,
"return": 0.005830222073507807,
"max_dd": 0.020860357367648123,
"n_trades": 2,
"test_start": "2023-04-02 09:00:00+00:00",
"test_end": "2024-03-02 05:55:00+00:00"
},
{
"fold": 2,
"fitness": 0.18648515967821389,
"sharpe": 0.06128925460246632,
"dsr": 1.8274259811076416e-13,
"dsr_pvalue": 0.9999999999998173,
"return": 0.004414468283732154,
"max_dd": 0.01617874217143948,
"n_trades": 3,
"test_start": "2024-03-02 06:00:00+00:00",
"test_end": "2025-01-31 02:55:00+00:00"
},
{
"fold": 3,
"fitness": 0.0,
"sharpe": 0.0,
"dsr": 0.0,
"dsr_pvalue": 1.0,
"return": 0.0,
"max_dd": 0.0,
"n_trades": 0,
"test_start": "2025-01-31 03:00:00+00:00",
"test_end": "2025-12-31 23:55:00+00:00"
}
],
"fitness_oos_mean": 0.11537560373824392,
"fitness_oos_min": 0.0,
"fitness_oos_max": 0.2750172552747618,
"fitness_oos_std": 0.11954610588306011,
"sharpe_oos_mean": 0.18283556638825146,
"sharpe_oos_min": 0.0,
"robust_score": 0.0
},
{
"genome_id": "662816b63c4feadf",
"fitness_is": 0.20828744896100454,
"sharpe_is": 0.20788153582834326,
"folds": [
{
"fold": 0,
"fitness": 0.0,
"sharpe": 0.09737359146830797,
"dsr": 2.4288846142332667e-14,
"dsr_pvalue": 0.9999999999999757,
"return": 0.0030306913996627216,
"max_dd": 0.007271637996670698,
"n_trades": 2,
"test_start": "2022-05-02 12:00:00+00:00",
"test_end": "2023-04-02 08:55:00+00:00"
},
{
"fold": 1,
"fitness": 0.0,
"sharpe": 0.0,
"dsr": 0.0,
"dsr_pvalue": 1.0,
"return": 0.0,
"max_dd": 0.0,
"n_trades": 0,
"test_start": "2023-04-02 09:00:00+00:00",
"test_end": "2024-03-02 05:55:00+00:00"
},
{
"fold": 2,
"fitness": 0.0,
"sharpe": 0.0,
"dsr": 0.0,
"dsr_pvalue": 1.0,
"return": 0.0,
"max_dd": 0.0,
"n_trades": 0,
"test_start": "2024-03-02 06:00:00+00:00",
"test_end": "2025-01-31 02:55:00+00:00"
},
{
"fold": 3,
"fitness": 0.0,
"sharpe": 0.0,
"dsr": 0.0,
"dsr_pvalue": 1.0,
"return": 0.0,
"max_dd": 0.0,
"n_trades": 0,
"test_start": "2025-01-31 03:00:00+00:00",
"test_end": "2025-12-31 23:55:00+00:00"
}
],
"fitness_oos_mean": 0.0,
"fitness_oos_min": 0.0,
"fitness_oos_max": 0.0,
"fitness_oos_std": 0.0,
"sharpe_oos_mean": 0.024343397867076993,
"sharpe_oos_min": 0.0,
"robust_score": 0.0
},
{
"genome_id": "cf698463450b5daf",
"fitness_is": 0.20181196371847498,
"sharpe_is": 0.3623641433398033,
"folds": [
{
"fold": 0,
"fitness": 0.2266655258766632,
"sharpe": 0.2999909794832819,
"dsr": 6.8141661859725965e-12,
"dsr_pvalue": 0.9999999999931858,
"return": 0.018823783888960888,
"max_dd": 0.01731422516010631,
"n_trades": 4,
"test_start": "2022-05-02 12:00:00+00:00",
"test_end": "2023-04-02 08:55:00+00:00"
},
{
"fold": 1,
"fitness": 0.0,
"sharpe": 0.44955543499020745,
"dsr": 9.481303288744627e-40,
"dsr_pvalue": 1.0,
"return": 0.049849479597766866,
"max_dd": 0.004685656497556025,
"n_trades": 1,
"test_start": "2023-04-02 09:00:00+00:00",
"test_end": "2024-03-02 05:55:00+00:00"
},
{
"fold": 2,
"fitness": 0.0,
"sharpe": 0.04410712874184496,
"dsr": 4.2725586842847596e-14,
"dsr_pvalue": 0.9999999999999573,
"return": 0.004410420254631564,
"max_dd": 0.01996463311196068,
"n_trades": 2,
"test_start": "2024-03-02 06:00:00+00:00",
"test_end": "2025-01-31 02:55:00+00:00"
},
{
"fold": 3,
"fitness": 0.0,
"sharpe": 0.4450404801682897,
"dsr": 1.4117362531214693e-27,
"dsr_pvalue": 1.0,
"return": 0.0526088849839641,
"max_dd": 0.005792329453230141,
"n_trades": 1,
"test_start": "2025-01-31 03:00:00+00:00",
"test_end": "2025-12-31 23:55:00+00:00"
}
],
"fitness_oos_mean": 0.0566663814691658,
"fitness_oos_min": 0.0,
"fitness_oos_max": 0.2266655258766632,
"fitness_oos_std": 0.09814905178567468,
"sharpe_oos_mean": 0.30967350584590597,
"sharpe_oos_min": 0.04410712874184496,
"robust_score": 0.0
},
{
"genome_id": "ddbfbef72f07d76a",
"fitness_is": 0.2007403940095307,
"sharpe_is": 0.3527941850815434,
"folds": [
{
"fold": 0,
"fitness": 0.25816377963939535,
"sharpe": 0.5144509577982044,
"dsr": 8.645767195175167e-13,
"dsr_pvalue": 0.9999999999991355,
"return": 0.07304433130107668,
"max_dd": 0.01915225360494227,
"n_trades": 5,
"test_start": "2022-05-02 12:00:00+00:00",
"test_end": "2023-04-02 08:55:00+00:00"
},
{
"fold": 1,
"fitness": 0.0,
"sharpe": 0.44955543499020745,
"dsr": 9.481303288744627e-40,
"dsr_pvalue": 1.0,
"return": 0.049849479597766866,
"max_dd": 0.004685656497556025,
"n_trades": 1,
"test_start": "2023-04-02 09:00:00+00:00",
"test_end": "2024-03-02 05:55:00+00:00"
},
{
"fold": 2,
"fitness": 0.23195306903928886,
"sharpe": 0.32337815238365364,
"dsr": 8.912615856501225e-14,
"dsr_pvalue": 0.9999999999999108,
"return": 0.033935014244555584,
"max_dd": 0.010486324211451901,
"n_trades": 3,
"test_start": "2024-03-02 06:00:00+00:00",
"test_end": "2025-01-31 02:55:00+00:00"
},
{
"fold": 3,
"fitness": 0.0,
"sharpe": 0.35283933132790596,
"dsr": 6.658494490709348e-14,
"dsr_pvalue": 0.9999999999999334,
"return": 0.05344386510725729,
"max_dd": 0.022762761145810856,
"n_trades": 1,
"test_start": "2025-01-31 03:00:00+00:00",
"test_end": "2025-12-31 23:55:00+00:00"
}
],
"fitness_oos_mean": 0.12252921216967105,
"fitness_oos_min": 0.0,
"fitness_oos_max": 0.25816377963939535,
"fitness_oos_std": 0.12287913982320425,
"sharpe_oos_mean": 0.41005596912499287,
"sharpe_oos_min": 0.32337815238365364,
"robust_score": 0.0
},
{
"genome_id": "2e49f8e8e6cec686",
"fitness_is": 0.19435604112910157,
"sharpe_is": 0.3713282033185845,
"folds": [
{
"fold": 0,
"fitness": 0.26072350293525676,
"sharpe": 0.5284128848027025,
"dsr": 2.486680252274251e-13,
"dsr_pvalue": 0.9999999999997513,
"return": 0.05881454144506426,
"max_dd": 0.016516772761264658,
"n_trades": 5,
"test_start": "2022-05-02 12:00:00+00:00",
"test_end": "2023-04-02 08:55:00+00:00"
},
{
"fold": 1,
"fitness": 0.0,
"sharpe": 0.44955543499020745,
"dsr": 9.481303288744627e-40,
"dsr_pvalue": 1.0,
"return": 0.049849479597766866,
"max_dd": 0.004685656497556025,
"n_trades": 1,
"test_start": "2023-04-02 09:00:00+00:00",
"test_end": "2024-03-02 05:55:00+00:00"
},
{
"fold": 2,
"fitness": 0.21422619151698888,
"sharpe": 0.2269482258874523,
"dsr": 3.6571308232390715e-13,
"dsr_pvalue": 0.9999999999996343,
"return": 0.02568538444204793,
"max_dd": 0.019558622265454236,
"n_trades": 3,
"test_start": "2024-03-02 06:00:00+00:00",
"test_end": "2025-01-31 02:55:00+00:00"
},
{
"fold": 3,
"fitness": 0.0,
"sharpe": 0.4450404801682897,
"dsr": 1.4117362531214693e-27,
"dsr_pvalue": 1.0,
"return": 0.0526088849839641,
"max_dd": 0.005792329453230141,
"n_trades": 1,
"test_start": "2025-01-31 03:00:00+00:00",
"test_end": "2025-12-31 23:55:00+00:00"
}
],
"fitness_oos_mean": 0.11873742361306142,
"fitness_oos_min": 0.0,
"fitness_oos_max": 0.26072350293525676,
"fitness_oos_std": 0.11987003696674377,
"sharpe_oos_mean": 0.412489256462163,
"sharpe_oos_min": 0.2269482258874523,
"robust_score": 0.0
},
{
"genome_id": "9852e8e4e480ef21",
"fitness_is": 0.1922948882127238,
"sharpe_is": 0.15925760158030436,
"folds": [
{
"fold": 0,
"fitness": 0.1494822926244717,
"sharpe": 0.0804180937424291,
"dsr": 2.443102889370683e-16,
"dsr_pvalue": 0.9999999999999998,
"return": 0.0014913672331040573,
"max_dd": 0.003691144778103467,
"n_trades": 3,
"test_start": "2022-05-02 12:00:00+00:00",
"test_end": "2023-04-02 08:55:00+00:00"
},
{
"fold": 1,
"fitness": 0.021783719225659744,
"sharpe": -1.222169028737873,
"dsr": 2.0819357051846864e-80,
"dsr_pvalue": 1.0,
"return": -0.018262798724970497,
"max_dd": 0.018262798724970497,
"n_trades": 16,
"test_start": "2023-04-02 09:00:00+00:00",
"test_end": "2024-03-02 05:55:00+00:00"
},
{
"fold": 2,
"fitness": 0.0,
"sharpe": 0.0,
"dsr": 0.0,
"dsr_pvalue": 1.0,
"return": 0.0,
"max_dd": 0.0,
"n_trades": 0,
"test_start": "2024-03-02 06:00:00+00:00",
"test_end": "2025-01-31 02:55:00+00:00"
},
{
"fold": 3,
"fitness": 0.0,
"sharpe": 0.0,
"dsr": 0.0,
"dsr_pvalue": 1.0,
"return": 0.0,
"max_dd": 0.0,
"n_trades": 0,
"test_start": "2025-01-31 03:00:00+00:00",
"test_end": "2025-12-31 23:55:00+00:00"
}
],
"fitness_oos_mean": 0.042816502962532865,
"fitness_oos_min": 0.0,
"fitness_oos_max": 0.1494822926244717,
"fitness_oos_std": 0.06222233226062855,
"sharpe_oos_mean": -0.285437733748861,
"sharpe_oos_min": -1.222169028737873,
"robust_score": 0.0
},
{
"genome_id": "16a1761c8da604e7",
"fitness_is": 0.19121238216295305,
"sharpe_is": 0.15255013590499958,
"folds": [
{
"fold": 0,
"fitness": 0.0,
"sharpe": 0.0,
"dsr": 0.0,
"dsr_pvalue": 1.0,
"return": 0.0,
"max_dd": 0.0,
"n_trades": 0,
"test_start": "2022-05-02 12:00:00+00:00",
"test_end": "2023-04-02 08:55:00+00:00"
},
{
"fold": 1,
"fitness": 0.0,
"sharpe": 0.0,
"dsr": 0.0,
"dsr_pvalue": 1.0,
"return": 0.0,
"max_dd": 0.0,
"n_trades": 0,
"test_start": "2023-04-02 09:00:00+00:00",
"test_end": "2024-03-02 05:55:00+00:00"
},
{
"fold": 2,
"fitness": 0.0,
"sharpe": 0.0,
"dsr": 0.0,
"dsr_pvalue": 1.0,
"return": 0.0,
"max_dd": 0.0,
"n_trades": 0,
"test_start": "2024-03-02 06:00:00+00:00",
"test_end": "2025-01-31 02:55:00+00:00"
},
{
"fold": 3,
"fitness": 0.0,
"sharpe": 0.0,
"dsr": 0.0,
"dsr_pvalue": 1.0,
"return": 0.0,
"max_dd": 0.0,
"n_trades": 0,
"test_start": "2025-01-31 03:00:00+00:00",
"test_end": "2025-12-31 23:55:00+00:00"
}
],
"fitness_oos_mean": 0.0,
"fitness_oos_min": 0.0,
"fitness_oos_max": 0.0,
"fitness_oos_std": 0.0,
"sharpe_oos_mean": 0.0,
"sharpe_oos_min": 0.0,
"robust_score": 0.0
},
{
"genome_id": "add18cb7382296fd",
"fitness_is": 0.1879996666147147,
"sharpe_is": 0.28550736974923013,
"folds": [
{
"fold": 0,
"fitness": 0.0,
"sharpe": 0.0,
"dsr": 0.0,
"dsr_pvalue": 1.0,
"return": 0.0,
"max_dd": 0.0,
"n_trades": 0,
"test_start": "2022-05-02 12:00:00+00:00",
"test_end": "2023-04-02 08:55:00+00:00"
},
{
"fold": 1,
"fitness": 0.0,
"sharpe": 0.44955543499020745,
"dsr": 9.481303288744627e-40,
"dsr_pvalue": 1.0,
"return": 0.049849479597766866,
"max_dd": 0.004685656497556025,
"n_trades": 1,
"test_start": "2023-04-02 09:00:00+00:00",
"test_end": "2024-03-02 05:55:00+00:00"
},
{
"fold": 2,
"fitness": 0.0,
"sharpe": 0.150691158813143,
"dsr": 4.263193477728488e-17,
"dsr_pvalue": 1.0,
"return": 0.009379645576139906,
"max_dd": 0.008611115582095263,
"n_trades": 1,
"test_start": "2024-03-02 06:00:00+00:00",
"test_end": "2025-01-31 02:55:00+00:00"
},
{
"fold": 3,
"fitness": 0.0,
"sharpe": 0.46456569507517853,
"dsr": 9.733950727895419e-26,
"dsr_pvalue": 1.0,
"return": 0.056366295538783584,
"max_dd": 0.005792329453230141,
"n_trades": 1,
"test_start": "2025-01-31 03:00:00+00:00",
"test_end": "2025-12-31 23:55:00+00:00"
}
],
"fitness_oos_mean": 0.0,
"fitness_oos_min": 0.0,
"fitness_oos_max": 0.0,
"fitness_oos_std": 0.0,
"sharpe_oos_mean": 0.26620307221963224,
"sharpe_oos_min": 0.0,
"robust_score": 0.0
},
{
"genome_id": "4ee566eb5e132d8c",
"fitness_is": 0.18745393246665415,
"sharpe_is": 0.31006907324235156,
"folds": [
{
"fold": 0,
"fitness": 0.2166522335961314,
"sharpe": 0.23875257586420637,
"dsr": 4.3199206915491695e-12,
"dsr_pvalue": 0.9999999999956801,
"return": 0.01599067972499668,
"max_dd": 0.01736213317014228,
"n_trades": 3,
"test_start": "2022-05-02 12:00:00+00:00",
"test_end": "2023-04-02 08:55:00+00:00"
},
{
"fold": 1,
"fitness": 0.0,
"sharpe": 0.44955543499020745,
"dsr": 9.481303288744627e-40,
"dsr_pvalue": 1.0,
"return": 0.049849479597766866,
"max_dd": 0.004685656497556025,
"n_trades": 1,
"test_start": "2023-04-02 09:00:00+00:00",
"test_end": "2024-03-02 05:55:00+00:00"
},
{
"fold": 2,
"fitness": 0.0,
"sharpe": 0.04410712874184496,
"dsr": 4.2725586842847596e-14,
"dsr_pvalue": 0.9999999999999573,
"return": 0.004410420254631564,
"max_dd": 0.01996463311196068,
"n_trades": 2,
"test_start": "2024-03-02 06:00:00+00:00",
"test_end": "2025-01-31 02:55:00+00:00"
},
{
"fold": 3,
"fitness": 0.0,
"sharpe": 0.46456569507517853,
"dsr": 9.733950727895419e-26,
"dsr_pvalue": 1.0,
"return": 0.056366295538783584,
"max_dd": 0.005792329453230141,
"n_trades": 1,
"test_start": "2025-01-31 03:00:00+00:00",
"test_end": "2025-12-31 23:55:00+00:00"
}
],
"fitness_oos_mean": 0.05416305839903285,
"fitness_oos_min": 0.0,
"fitness_oos_max": 0.2166522335961314,
"fitness_oos_std": 0.09381316904044511,
"sharpe_oos_mean": 0.2992452086678593,
"sharpe_oos_min": 0.04410712874184496,
"robust_score": 0.0
}
]
}
+634
View File
@@ -0,0 +1,634 @@
{
"run_id": "e263651598894da688d95fda90a34a96",
"run_name": "phase1-extended-001",
"n_folds": 4,
"top_k_requested": 10,
"top_k_evaluated": 10,
"symbol": "BTC-PERPETUAL",
"timeframe": "1h",
"start": "2018-09-01T00:00:00+00:00",
"end": "2026-01-01T00:00:00+00:00",
"ohlcv_bars": 64297,
"results": [
{
"genome_id": "fe6e01eb690d3960",
"fitness_is": 0.3513762485888574,
"sharpe_is": 0.9011072752402621,
"folds": [
{
"fold": 0,
"fitness": 0.18429629882863333,
"sharpe": 0.18578971215949266,
"dsr": 0.022696217108110216,
"dsr_pvalue": 0.9773037828918898,
"return": 0.03138679502492736,
"max_dd": 0.19089436189057732,
"n_trades": 90,
"test_start": "2022-05-02 12:00:00+00:00",
"test_end": "2023-04-02 08:00:00+00:00"
},
{
"fold": 1,
"fitness": 0.5261711065682141,
"sharpe": 1.875154815895858,
"dsr": 0.34945842578669783,
"dsr_pvalue": 0.6505415742133022,
"return": 0.684094224950746,
"max_dd": 0.26051011671170043,
"n_trades": 108,
"test_start": "2023-04-02 09:00:00+00:00",
"test_end": "2024-03-02 05:00:00+00:00"
},
{
"fold": 2,
"fitness": 0.29904777517998976,
"sharpe": 0.45027968136531676,
"dsr": 0.040236883094469954,
"dsr_pvalue": 0.9597631169055301,
"return": 0.16192920610625539,
"max_dd": 0.25615601205401484,
"n_trades": 87,
"test_start": "2024-03-02 06:00:00+00:00",
"test_end": "2025-01-31 02:00:00+00:00"
},
{
"fold": 3,
"fitness": 0.06652893772008044,
"sharpe": -0.7859068293026578,
"dsr": 0.0016949251764253048,
"dsr_pvalue": 0.9983050748235747,
"return": -0.1801701961968295,
"max_dd": 0.3050931306970407,
"n_trades": 89,
"test_start": "2025-01-31 03:00:00+00:00",
"test_end": "2025-12-31 23:00:00+00:00"
}
],
"fitness_oos_mean": 0.2690110295742294,
"fitness_oos_min": 0.06652893772008044,
"fitness_oos_max": 0.5261711065682141,
"fitness_oos_std": 0.16971232602043682,
"sharpe_oos_mean": 0.43132934502950243,
"sharpe_oos_min": -0.7859068293026578,
"robust_score": 0.06652893772008044
},
{
"genome_id": "d98739b2ba8d65e8",
"fitness_is": 0.3581811122056351,
"sharpe_is": 1.5316294902683918,
"folds": [
{
"fold": 0,
"fitness": 0.03897301517910094,
"sharpe": -1.1213338499931884,
"dsr": 0.0007106609032094727,
"dsr_pvalue": 0.9992893390967905,
"return": -0.18358503193809717,
"max_dd": 0.24053109269341416,
"n_trades": 65,
"test_start": "2022-05-02 12:00:00+00:00",
"test_end": "2023-04-02 08:00:00+00:00"
},
{
"fold": 1,
"fitness": 0.13868820192362147,
"sharpe": 0.1139203144076397,
"dsr": 0.018885390702584475,
"dsr_pvalue": 0.9811146092974156,
"return": 0.019694298831973045,
"max_dd": 0.1528666578131679,
"n_trades": 21,
"test_start": "2023-04-02 09:00:00+00:00",
"test_end": "2024-03-02 05:00:00+00:00"
},
{
"fold": 2,
"fitness": 0.43784574830368517,
"sharpe": 1.922993791724599,
"dsr": 0.36463909799020594,
"dsr_pvalue": 0.6353609020097941,
"return": 0.31046814355338936,
"max_dd": 0.09604869735695161,
"n_trades": 48,
"test_start": "2024-03-02 06:00:00+00:00",
"test_end": "2025-01-31 02:00:00+00:00"
},
{
"fold": 3,
"fitness": 0.377910610011883,
"sharpe": 1.3313322991701542,
"dsr": 0.17322722607861918,
"dsr_pvalue": 0.8267727739213808,
"return": 0.12021252899342505,
"max_dd": 0.04712452925993322,
"n_trades": 32,
"test_start": "2025-01-31 03:00:00+00:00",
"test_end": "2025-12-31 23:00:00+00:00"
}
],
"fitness_oos_mean": 0.24835439385457264,
"fitness_oos_min": 0.03897301517910094,
"fitness_oos_max": 0.43784574830368517,
"fitness_oos_std": 0.1647414807695958,
"sharpe_oos_mean": 0.5617281388273011,
"sharpe_oos_min": -1.1213338499931884,
"robust_score": 0.03897301517910094
},
{
"genome_id": "0dd6619fdcbe37f4",
"fitness_is": 0.3765498201912705,
"sharpe_is": 0.9388498977535525,
"folds": [
{
"fold": 0,
"fitness": 0.03550929012857722,
"sharpe": -1.1093436310362703,
"dsr": 0.0005959199839818188,
"dsr_pvalue": 0.9994040800160182,
"return": -0.24600377155172415,
"max_dd": 0.38950670401585935,
"n_trades": 122,
"test_start": "2022-05-02 12:00:00+00:00",
"test_end": "2023-04-02 08:00:00+00:00"
},
{
"fold": 1,
"fitness": 0.049414033104104804,
"sharpe": -0.9995738302518242,
"dsr": 0.0007695523975576842,
"dsr_pvalue": 0.9992304476024423,
"return": -0.15162579158457645,
"max_dd": 0.21485725923368693,
"n_trades": 79,
"test_start": "2023-04-02 09:00:00+00:00",
"test_end": "2024-03-02 05:00:00+00:00"
},
{
"fold": 2,
"fitness": 0.4634730710112187,
"sharpe": 1.2076100738049764,
"dsr": 0.15231462029944348,
"dsr_pvalue": 0.8476853797005566,
"return": 0.3072592298707053,
"max_dd": 0.15464658494822137,
"n_trades": 153,
"test_start": "2024-03-02 06:00:00+00:00",
"test_end": "2025-01-31 02:00:00+00:00"
},
{
"fold": 3,
"fitness": 0.1215451856323226,
"sharpe": -0.22288474503734051,
"dsr": 0.00830898358713792,
"dsr_pvalue": 0.9916910164128621,
"return": -0.035113534418310444,
"max_dd": 0.17145164314561556,
"n_trades": 74,
"test_start": "2025-01-31 03:00:00+00:00",
"test_end": "2025-12-31 23:00:00+00:00"
}
],
"fitness_oos_mean": 0.16748539496905585,
"fitness_oos_min": 0.03550929012857722,
"fitness_oos_max": 0.4634730710112187,
"fitness_oos_std": 0.17398113830545858,
"sharpe_oos_mean": -0.28104803313011467,
"sharpe_oos_min": -1.1093436310362703,
"robust_score": 0.03550929012857722
},
{
"genome_id": "00545b157923dc6b",
"fitness_is": 0.34960092770407836,
"sharpe_is": 0.9785144736247469,
"folds": [
{
"fold": 0,
"fitness": 0.027180640826810924,
"sharpe": -1.26345444662059,
"dsr": 0.00030602669379661417,
"dsr_pvalue": 0.9996939733062034,
"return": -0.2411006699210636,
"max_dd": 0.36676673786660746,
"n_trades": 79,
"test_start": "2022-05-02 12:00:00+00:00",
"test_end": "2023-04-02 08:00:00+00:00"
},
{
"fold": 1,
"fitness": 0.5208756815430423,
"sharpe": 1.770946043023605,
"dsr": 0.3128216779179025,
"dsr_pvalue": 0.6871783220820975,
"return": 0.60240576625387,
"max_dd": 0.2331907909757441,
"n_trades": 45,
"test_start": "2023-04-02 09:00:00+00:00",
"test_end": "2024-03-02 05:00:00+00:00"
},
{
"fold": 2,
"fitness": 0.2959791903863346,
"sharpe": 0.3683437692731339,
"dsr": 0.033871432018897314,
"dsr_pvalue": 0.9661285679811027,
"return": 0.134452978611995,
"max_dd": 0.19964940601028408,
"n_trades": 58,
"test_start": "2024-03-02 06:00:00+00:00",
"test_end": "2025-01-31 02:00:00+00:00"
},
{
"fold": 3,
"fitness": 0.15883382666451054,
"sharpe": 0.031160944245222175,
"dsr": 0.015769276637227145,
"dsr_pvalue": 0.9842307233627728,
"return": 0.006899603676653765,
"max_dd": 0.1947452948903118,
"n_trades": 36,
"test_start": "2025-01-31 03:00:00+00:00",
"test_end": "2025-12-31 23:00:00+00:00"
}
],
"fitness_oos_mean": 0.2507173348551746,
"fitness_oos_min": 0.027180640826810924,
"fitness_oos_max": 0.5208756815430423,
"fitness_oos_std": 0.1826508968673572,
"sharpe_oos_mean": 0.2267490774803428,
"sharpe_oos_min": -1.26345444662059,
"robust_score": 0.027180640826810924
},
{
"genome_id": "eea882db55f8dd5e",
"fitness_is": 0.3181543783162782,
"sharpe_is": 1.3214317725559044,
"folds": [
{
"fold": 0,
"fitness": 0.02689139923003529,
"sharpe": -1.3020990219857191,
"dsr": 0.00036878360260577075,
"dsr_pvalue": 0.9996312163973943,
"return": -0.22280992288117984,
"max_dd": 0.28735423155244094,
"n_trades": 69,
"test_start": "2022-05-02 12:00:00+00:00",
"test_end": "2023-04-02 08:00:00+00:00"
},
{
"fold": 1,
"fitness": 0.1064185213723357,
"sharpe": -0.3110958507357913,
"dsr": 0.006879713400940769,
"dsr_pvalue": 0.9931202865990593,
"return": -0.057958925555868235,
"max_dd": 0.19529025657454543,
"n_trades": 33,
"test_start": "2023-04-02 09:00:00+00:00",
"test_end": "2024-03-02 05:00:00+00:00"
},
{
"fold": 2,
"fitness": 0.37938211463073945,
"sharpe": 1.490827385000592,
"dsr": 0.2224878866287407,
"dsr_pvalue": 0.7775121133712593,
"return": 0.2563445845249124,
"max_dd": 0.1053935530975972,
"n_trades": 59,
"test_start": "2024-03-02 06:00:00+00:00",
"test_end": "2025-01-31 02:00:00+00:00"
},
{
"fold": 3,
"fitness": 0.4175480977898098,
"sharpe": 1.6336803280087653,
"dsr": 0.260079721684657,
"dsr_pvalue": 0.739920278315343,
"return": 0.157201259896103,
"max_dd": 0.04639072483391873,
"n_trades": 43,
"test_start": "2025-01-31 03:00:00+00:00",
"test_end": "2025-12-31 23:00:00+00:00"
}
],
"fitness_oos_mean": 0.23256003325573005,
"fitness_oos_min": 0.02689139923003529,
"fitness_oos_max": 0.4175480977898098,
"fitness_oos_std": 0.16881097094204098,
"sharpe_oos_mean": 0.3778282100719617,
"sharpe_oos_min": -1.3020990219857191,
"robust_score": 0.02689139923003529
},
{
"genome_id": "24a3e592397d4bda",
"fitness_is": 0.3181543783162782,
"sharpe_is": 1.3214317725559044,
"folds": [
{
"fold": 0,
"fitness": 0.02689139923003529,
"sharpe": -1.3020990219857191,
"dsr": 0.00036878360260577075,
"dsr_pvalue": 0.9996312163973943,
"return": -0.22280992288117984,
"max_dd": 0.28735423155244094,
"n_trades": 69,
"test_start": "2022-05-02 12:00:00+00:00",
"test_end": "2023-04-02 08:00:00+00:00"
},
{
"fold": 1,
"fitness": 0.1064185213723357,
"sharpe": -0.3110958507357913,
"dsr": 0.006879713400940769,
"dsr_pvalue": 0.9931202865990593,
"return": -0.057958925555868235,
"max_dd": 0.19529025657454543,
"n_trades": 33,
"test_start": "2023-04-02 09:00:00+00:00",
"test_end": "2024-03-02 05:00:00+00:00"
},
{
"fold": 2,
"fitness": 0.37938211463073945,
"sharpe": 1.490827385000592,
"dsr": 0.2224878866287407,
"dsr_pvalue": 0.7775121133712593,
"return": 0.2563445845249124,
"max_dd": 0.1053935530975972,
"n_trades": 59,
"test_start": "2024-03-02 06:00:00+00:00",
"test_end": "2025-01-31 02:00:00+00:00"
},
{
"fold": 3,
"fitness": 0.4175480977898098,
"sharpe": 1.6336803280087653,
"dsr": 0.260079721684657,
"dsr_pvalue": 0.739920278315343,
"return": 0.157201259896103,
"max_dd": 0.04639072483391873,
"n_trades": 43,
"test_start": "2025-01-31 03:00:00+00:00",
"test_end": "2025-12-31 23:00:00+00:00"
}
],
"fitness_oos_mean": 0.23256003325573005,
"fitness_oos_min": 0.02689139923003529,
"fitness_oos_max": 0.4175480977898098,
"fitness_oos_std": 0.16881097094204098,
"sharpe_oos_mean": 0.3778282100719617,
"sharpe_oos_min": -1.3020990219857191,
"robust_score": 0.02689139923003529
},
{
"genome_id": "7d6636945c3fdee5",
"fitness_is": 0.4143633094822953,
"sharpe_is": 1.0518304581395579,
"folds": [
{
"fold": 0,
"fitness": 0.024065408548587387,
"sharpe": -1.362275778196952,
"dsr": 0.0002459819911643212,
"dsr_pvalue": 0.9997540180088357,
"return": -0.27317643202118824,
"max_dd": 0.283710961847723,
"n_trades": 76,
"test_start": "2022-05-02 12:00:00+00:00",
"test_end": "2023-04-02 08:00:00+00:00"
},
{
"fold": 1,
"fitness": 0.10523269843210308,
"sharpe": -0.32050989428963983,
"dsr": 0.006397259401103736,
"dsr_pvalue": 0.9936027405988963,
"return": -0.058925951660568465,
"max_dd": 0.19264026914858506,
"n_trades": 115,
"test_start": "2023-04-02 09:00:00+00:00",
"test_end": "2024-03-02 05:00:00+00:00"
},
{
"fold": 2,
"fitness": 0.37331175980311637,
"sharpe": 0.6268880704230287,
"dsr": 0.05821057212982157,
"dsr_pvalue": 0.9417894278701784,
"return": 0.13285431586579133,
"max_dd": 0.11992643530986899,
"n_trades": 66,
"test_start": "2024-03-02 06:00:00+00:00",
"test_end": "2025-01-31 02:00:00+00:00"
},
{
"fold": 3,
"fitness": 0.058003508089270274,
"sharpe": -0.9065054278150867,
"dsr": 0.0011919907667399207,
"dsr_pvalue": 0.9988080092332601,
"return": -0.11553575296643481,
"max_dd": 0.21946607715577302,
"n_trades": 108,
"test_start": "2025-01-31 03:00:00+00:00",
"test_end": "2025-12-31 23:00:00+00:00"
}
],
"fitness_oos_mean": 0.14015334371826926,
"fitness_oos_min": 0.024065408548587387,
"fitness_oos_max": 0.37331175980311637,
"fitness_oos_std": 0.13766562991922562,
"sharpe_oos_mean": -0.4906007574696624,
"sharpe_oos_min": -1.362275778196952,
"robust_score": 0.024065408548587387
},
{
"genome_id": "94413564f2f6c03e",
"fitness_is": 0.4504680245893446,
"sharpe_is": 1.931509193896414,
"folds": [
{
"fold": 0,
"fitness": 0.1454684156938894,
"sharpe": -0.1847503153026734,
"dsr": 0.010432618759993285,
"dsr_pvalue": 0.9895673812400068,
"return": -0.006041376713751601,
"max_dd": 0.028928093840880797,
"n_trades": 10,
"test_start": "2022-05-02 12:00:00+00:00",
"test_end": "2023-04-02 08:00:00+00:00"
},
{
"fold": 1,
"fitness": 0.0,
"sharpe": 0.7414755036663777,
"dsr": 0.032908166066855475,
"dsr_pvalue": 0.9670918339331446,
"return": 0.024390796509991652,
"max_dd": 0.01848789755136504,
"n_trades": 3,
"test_start": "2023-04-02 09:00:00+00:00",
"test_end": "2024-03-02 05:00:00+00:00"
},
{
"fold": 2,
"fitness": 0.6298210284045589,
"sharpe": 2.8227093941852077,
"dsr": 0.783195485997482,
"dsr_pvalue": 0.216804514002518,
"return": 0.1153800539734966,
"max_dd": 0.009172322853563938,
"n_trades": 13,
"test_start": "2024-03-02 06:00:00+00:00",
"test_end": "2025-01-31 02:00:00+00:00"
},
{
"fold": 3,
"fitness": 0.0,
"sharpe": -0.3705731110916588,
"dsr": 0.005298299731142135,
"dsr_pvalue": 0.9947017002688578,
"return": -0.01618834464316521,
"max_dd": 0.050704690156295765,
"n_trades": 8,
"test_start": "2025-01-31 03:00:00+00:00",
"test_end": "2025-12-31 23:00:00+00:00"
}
],
"fitness_oos_mean": 0.1938223610246121,
"fitness_oos_min": 0.0,
"fitness_oos_max": 0.6298210284045589,
"fitness_oos_std": 0.2586344704657744,
"sharpe_oos_mean": 0.7522153678643133,
"sharpe_oos_min": -0.3705731110916588,
"robust_score": 0.0
},
{
"genome_id": "9deb48ad9a1d6a01",
"fitness_is": 0.38499883511982724,
"sharpe_is": 1.7309660717184492,
"folds": [
{
"fold": 0,
"fitness": 0.11865902601061347,
"sharpe": -0.33840465028379796,
"dsr": 0.006577366557928182,
"dsr_pvalue": 0.9934226334420718,
"return": -0.012941050841296264,
"max_dd": 0.03402877313520886,
"n_trades": 17,
"test_start": "2022-05-02 12:00:00+00:00",
"test_end": "2023-04-02 08:00:00+00:00"
},
{
"fold": 1,
"fitness": 0.0,
"sharpe": 0.5508017623316874,
"dsr": 0.028490827818300125,
"dsr_pvalue": 0.9715091721816999,
"return": 0.01831303651843519,
"max_dd": 0.017308885097101046,
"n_trades": 7,
"test_start": "2023-04-02 09:00:00+00:00",
"test_end": "2024-03-02 05:00:00+00:00"
},
{
"fold": 2,
"fitness": 0.0,
"sharpe": 2.1433702644761987,
"dsr": 0.4071948898247041,
"dsr_pvalue": 0.5928051101752959,
"return": 0.10827348451282881,
"max_dd": 0.008755897100329497,
"n_trades": 8,
"test_start": "2024-03-02 06:00:00+00:00",
"test_end": "2025-01-31 02:00:00+00:00"
},
{
"fold": 3,
"fitness": 0.0,
"sharpe": 1.0955591683486918,
"dsr": 0.07909942404164044,
"dsr_pvalue": 0.9209005759583595,
"return": 0.01372945300669004,
"max_dd": 0.006329669977511941,
"n_trades": 5,
"test_start": "2025-01-31 03:00:00+00:00",
"test_end": "2025-12-31 23:00:00+00:00"
}
],
"fitness_oos_mean": 0.029664756502653367,
"fitness_oos_min": 0.0,
"fitness_oos_max": 0.11865902601061347,
"fitness_oos_std": 0.05138086545675487,
"sharpe_oos_mean": 0.862831636218195,
"sharpe_oos_min": -0.33840465028379796,
"robust_score": 0.0
},
{
"genome_id": "1bb0f36ae3f178ae",
"fitness_is": 0.3289012666358953,
"sharpe_is": 1.2144322416340778,
"folds": [
{
"fold": 0,
"fitness": 0.03325222099260249,
"sharpe": -1.0706470661093082,
"dsr": 0.0007248785745670455,
"dsr_pvalue": 0.999275121425433,
"return": -0.09901039935604483,
"max_dd": 0.13711458096989376,
"n_trades": 27,
"test_start": "2022-05-02 12:00:00+00:00",
"test_end": "2023-04-02 08:00:00+00:00"
},
{
"fold": 1,
"fitness": 0.0,
"sharpe": 0.12138335573748249,
"dsr": 0.018713173174332784,
"dsr_pvalue": 0.9812868268256673,
"return": 0.00555346714044469,
"max_dd": 0.02724874190208475,
"n_trades": 7,
"test_start": "2023-04-02 09:00:00+00:00",
"test_end": "2024-03-02 05:00:00+00:00"
},
{
"fold": 2,
"fitness": 0.32995249837249846,
"sharpe": 0.9733164430116448,
"dsr": 0.07297295015940324,
"dsr_pvalue": 0.9270270498405968,
"return": 0.05047188141942249,
"max_dd": 0.02617799182077058,
"n_trades": 13,
"test_start": "2024-03-02 06:00:00+00:00",
"test_end": "2025-01-31 02:00:00+00:00"
},
{
"fold": 3,
"fitness": 0.0,
"sharpe": 1.3421850442772436,
"dsr": 0.05521595540810741,
"dsr_pvalue": 0.9447840445918926,
"return": 0.06121506651714692,
"max_dd": 0.00995276327591354,
"n_trades": 3,
"test_start": "2025-01-31 03:00:00+00:00",
"test_end": "2025-12-31 23:00:00+00:00"
}
],
"fitness_oos_mean": 0.09080117984127524,
"fitness_oos_min": 0.0,
"fitness_oos_max": 0.32995249837249846,
"fitness_oos_std": 0.13873981434768828,
"sharpe_oos_mean": 0.34155944422926565,
"sharpe_oos_min": -1.0706470661093082,
"robust_score": 0.0
}
]
}
Generated
+12 -4
View File
@@ -897,32 +897,36 @@ wheels = [
name = "multi-swarm-coevolutive" name = "multi-swarm-coevolutive"
version = "0.1.0" version = "0.1.0"
source = { virtual = "." } source = { virtual = "." }
dependencies = [
{ name = "multi-swarm-core" },
{ name = "strategy-crypto" },
]
[package.dev-dependencies] [package.dev-dependencies]
dev = [ dev = [
{ name = "multi-swarm-core" },
{ name = "mypy" }, { name = "mypy" },
{ name = "pytest" }, { name = "pytest" },
{ name = "pytest-asyncio" }, { name = "pytest-asyncio" },
{ name = "pytest-mock" }, { name = "pytest-mock" },
{ name = "responses" }, { name = "responses" },
{ name = "ruff" }, { name = "ruff" },
{ name = "strategy-crypto" },
{ name = "types-requests" }, { name = "types-requests" },
] ]
[package.metadata] [package.metadata]
requires-dist = [
{ name = "multi-swarm-core", editable = "src/multi_swarm_core" },
{ name = "strategy-crypto", editable = "src/strategy_crypto" },
]
[package.metadata.requires-dev] [package.metadata.requires-dev]
dev = [ dev = [
{ name = "multi-swarm-core", editable = "src/multi_swarm_core" },
{ name = "mypy", specifier = ">=1.13" }, { name = "mypy", specifier = ">=1.13" },
{ name = "pytest", specifier = ">=8.3" }, { name = "pytest", specifier = ">=8.3" },
{ name = "pytest-asyncio", specifier = ">=0.24" }, { name = "pytest-asyncio", specifier = ">=0.24" },
{ name = "pytest-mock", specifier = ">=3.14" }, { name = "pytest-mock", specifier = ">=3.14" },
{ name = "responses", specifier = ">=0.25" }, { name = "responses", specifier = ">=0.25" },
{ name = "ruff", specifier = ">=0.7" }, { name = "ruff", specifier = ">=0.7" },
{ name = "strategy-crypto", editable = "src/strategy_crypto" },
{ name = "types-requests", specifier = ">=2.32" }, { name = "types-requests", specifier = ">=2.32" },
] ]
@@ -932,9 +936,11 @@ version = "0.1.0"
source = { editable = "src/multi_swarm_core" } source = { editable = "src/multi_swarm_core" }
dependencies = [ dependencies = [
{ name = "httpx" }, { name = "httpx" },
{ name = "nicegui" },
{ name = "numpy" }, { name = "numpy" },
{ name = "openai" }, { name = "openai" },
{ name = "pandas" }, { name = "pandas" },
{ name = "plotly" },
{ name = "pyarrow" }, { name = "pyarrow" },
{ name = "pydantic" }, { name = "pydantic" },
{ name = "pydantic-settings" }, { name = "pydantic-settings" },
@@ -949,9 +955,11 @@ dependencies = [
[package.metadata] [package.metadata]
requires-dist = [ requires-dist = [
{ name = "httpx", specifier = ">=0.28" }, { name = "httpx", specifier = ">=0.28" },
{ name = "nicegui", specifier = ">=3.11.1" },
{ name = "numpy", specifier = ">=2.1" }, { name = "numpy", specifier = ">=2.1" },
{ name = "openai", specifier = ">=1.55" }, { name = "openai", specifier = ">=1.55" },
{ name = "pandas", specifier = ">=2.2" }, { name = "pandas", specifier = ">=2.2" },
{ name = "plotly", specifier = ">=5.24" },
{ name = "pyarrow", specifier = ">=18.0" }, { name = "pyarrow", specifier = ">=18.0" },
{ name = "pydantic", specifier = ">=2.9" }, { name = "pydantic", specifier = ">=2.9" },
{ name = "pydantic-settings", specifier = ">=2.6" }, { name = "pydantic-settings", specifier = ">=2.6" },