15 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
36 changed files with 5446 additions and 193 deletions
+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.
+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()
@@ -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
@@ -27,6 +29,13 @@ class MarketSummary:
vol_percentile: float = 50.0 # 0-100 percentile della vol corrente vol_percentile: float = 50.0 # 0-100 percentile della vol corrente
seasonality_hour: float = 0.0 # 0-1 varianza spiegata da hour seasonality_hour: float = 0.0 # 0-1 varianza spiegata da hour
seasonality_dow: float = 0.0 # 0-1 varianza spiegata da dow 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)
@@ -47,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:
@@ -127,35 +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: sma_pct(long) > 0.05 (close 5% sopra media) OR sma_pct(long) < -0.05
- Momentum positivo conferma: macd_pct(12,26,9) > 0.005 (> 0.5% del prezzo)
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
@@ -177,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 = """\
@@ -193,13 +223,66 @@ Regime recente (ultime 500 barre):
vol_pct: {vol_percentile:.0f}° percentile storico vol_pct: {vol_percentile:.0f}° percentile storico
stagionalita: hour={seasonality_hour:.2f}, dow={seasonality_dow:.2f} (0-1, varianza spiegata) 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}
@@ -282,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,
@@ -312,9 +409,17 @@ class HypothesisAgent:
vol_percentile=market.vol_percentile, vol_percentile=market.vol_percentile,
seasonality_hour=market.seasonality_hour, seasonality_hour=market.seasonality_hour,
seasonality_dow=market.seasonality_dow, 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] = []
@@ -5,8 +5,13 @@ from scipy import stats # type: ignore[import-untyped]
from ..metrics.basic import ( from ..metrics.basic import (
autocorr_lag1, autocorr_lag1,
compression_ratio,
efficiency_ratio_kaufman,
hurst_exponent, hurst_exponent,
seasonality_strength, seasonality_strength,
spectral_entropy_and_cycle,
structural_uptrend_score,
tail_index_hill,
vol_percentile_historical, vol_percentile_historical,
) )
from .hypothesis import MarketSummary from .hypothesis import MarketSummary
@@ -40,6 +45,13 @@ def build_market_summary(
season_h = seasonality_strength(returns, by="hour") season_h = seasonality_strength(returns, by="hour")
season_d = seasonality_strength(returns, by="dow") 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,
@@ -55,4 +67,11 @@ def build_market_summary(
vol_percentile=vol_pct, vol_percentile=vol_pct,
seasonality_hour=season_h, seasonality_hour=season_h,
seasonality_dow=season_d, 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]
gross = entry_side * (exit_price - entry_price) * size
fees = fees_rate * size * (entry_price + exit_price)
net = gross - fees
# La chiusura avviene a open[exit_idx]: dal bar exit_idx in poi il
# PnL e' realizzato (non piu' MtM).
realized_pnl[exit_idx:] += net
# Posizione aperta in [entry_idx, exit_idx-1].
open_side[entry_idx:exit_idx] = entry_side
open_entry[entry_idx:exit_idx] = entry_price
trades.append(Trade(
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, fees_bp=self.fees_bp,
) ))
trades.append(trade) else:
equity += trade.net_pnl # Ultima posizione ancora aperta: chiusura forced a close[-1].
position = None # Parita' col loop legacy: MtM su [entry_idx, n-1), realized totale
position_entry_ts = None # SOLO al bar n-1 (legacy fa equity_history[-1] = equity).
if target_side in (Side.LONG, Side.SHORT): last_close = closes[-1]
position = Position( gross = entry_side * (last_close - entry_price) * size
side=target_side, entry_price=float(row["open"]), size=1.0 fees = fees_rate * size * (entry_price + last_close)
) net = gross - fees
position_entry_ts = ts if entry_idx < n - 1:
open_side[entry_idx:n - 1] = entry_side
mark = float(row["close"]) open_entry[entry_idx:n - 1] = entry_price
mtm = position.unrealized_pnl(mark) if position else 0.0 realized_pnl[-1] += net
current_equity = equity + mtm trades.append(Trade(
equity_history.append(current_equity) entry_ts=ts_index[entry_idx],
returns_history.append(current_equity - prev_equity) exit_ts=ts_index[-1],
prev_equity = current_equity side=Side.LONG if entry_side == 1 else Side.SHORT,
size=size,
if position is not None: entry_price=entry_price,
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, exit_price=last_close,
fees_bp=self.fees_bp, fees_bp=self.fees_bp,
) ))
trades.append(trade)
equity += trade.net_pnl # MtM unrealized per ogni bar in cui c'e' una posizione aperta.
equity_history[-1] = equity mtm = open_side.astype(np.float64) * (closes - open_entry) * size
if len(returns_history) >= 2: equity_arr = realized_pnl + mtm
returns_history[-1] = equity - equity_history[-2] # Returns = first diff dell'equity (col loop legacy il primo bar e' equity[0]-0).
returns_arr = np.concatenate(([equity_arr[0]], np.diff(equity_arr)))
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"]
@@ -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)
@@ -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, [])
@@ -141,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,
@@ -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
@@ -63,3 +63,51 @@ def test_build_summary_new_fields_populated() -> None:
assert isinstance(s.seasonality_dow, float) assert isinstance(s.seasonality_dow, float)
assert 0.0 <= s.seasonality_hour <= 1.0 assert 0.0 <= s.seasonality_hour <= 1.0
assert 0.0 <= s.seasonality_dow <= 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"
@@ -4,10 +4,15 @@ import pytest
from multi_swarm_core.metrics.basic import ( from multi_swarm_core.metrics.basic import (
autocorr_lag1, autocorr_lag1,
compression_ratio,
efficiency_ratio_kaufman,
hurst_exponent, hurst_exponent,
max_drawdown, max_drawdown,
seasonality_strength, seasonality_strength,
sharpe_ratio, sharpe_ratio,
spectral_entropy_and_cycle,
structural_uptrend_score,
tail_index_hill,
total_return, total_return,
vol_percentile_historical, vol_percentile_historical,
) )
@@ -166,3 +171,120 @@ def test_seasonality_strength_invalid_by():
r = pd.Series(np.random.normal(0, 0.01, 100), index=idx) r = pd.Series(np.random.normal(0, 0.01, 100), index=idx)
with pytest.raises(ValueError, match="'minute'"): with pytest.raises(ValueError, match="'minute'"):
seasonality_strength(r, by="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'"
)
+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,29 +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.", "_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": "2.1", "_schema": "3.2",
"_changelog": "v2.1 - directive estese con interpretazione style-specific dei 4 nuovi input statistici (autocorr_lag1, hurst, vol_pct, seasonality). v2.0 - Riprogettato per blind-generator GA. Directive medio-compatte (~700 char) che orientano l'esplorazione cognitiva senza prescrivere indicatori specifici (lascia evolvere il GA). Mappate sulle 4 statistiche disponibili: mean, std, skew, kurtosis + volatility_regime. Rimosse ecologist (richiede multi-asset), game_theorist/epidemiologist (richiedono info esterne non visibili all'agente). Tenute 7 lenti che mappano bene sulle statistiche aggregate.", "_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.",
"_design_notes": "Le directive sono BIAS DI ESPLORAZIONE, non template. Suggeriscono cosa cercare nei 4 momenti e quali archetipi di strategia preferire, lasciando al GA la scoperta della combinazione esatta di indicatori e soglie. Sono pensate per essere riscritte dall'operatore mutate_prompt_llm mantenendo coerenza con la lente.", "_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": { "styles": {
"physicist": { "physicist": {
"directive": "Pensa come un fisico: il mercato e un sistema con leggi di conservazione e regimi di scala. Leggi std come dispersione energetica e kurtosis come densita di eventi estremi (kurt alta = fat tails, sistema fuori equilibrio). Cerca simmetrie nei ritorni (skew ≈0 = sistema simmetrico) e rotture (skew marcato = forzante asimmetrica). AR(1) positivo significativamente sopra baseline = sistema con memoria fuori equilibrio, momentum legittimo; Hurst > 0.55 conferma persistenza di scala; vol percentile alto + kurt bassa = energia immagazzinata non ancora rilasciata. Preferisci ritorno all'equilibrio in regime simmetrico/basso vol, propagazione (momentum/breakout) in regime asimmetrico/alto vol. Pattern coerenti su piu lookback sono robusti, pattern singoli sono rumore." "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": { "biologist": {
"directive": "Pensa come un biologo evoluzionista: il mercato e un ecosistema di strategie in competizione. Skew negativo = predazione asimmetrica (vol-selling crowded che subisce shock). Skew positivo = predatori che cacciano breakout. Kurt alta = eventi di estinzione/fioritura. AR(1) positivo persistente = una specie sta colonizzando la nicchia (overcrowding imminente, fade); Hurst > 0.55 con vol percentile basso = nicchia stabile (occupa); seasonality forte = ritmo biologico ricorrente, sfruttabile. Preferisci contrarian in regime di skew estremo (fade la specie dominante) e coordinamento in regime simmetrico. Pattern asimmetrici: cattura la coda opposta al consensus." "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": { "historian": {
"directive": "Pensa come uno storico: i regimi si ripetono ma non identici. Usa mean come drift strutturale e std come ampiezza ciclo. Kurt alta + vol regime medium/high = fase tardiva (eventi estremi addensati, pre-transizione); kurt bassa + skew ≈0 = accumulazione/stabilita. AR(1) recente >> baseline storica = regime sta accelerando rispetto al normale; Hurst > 0.55 + vol percentile alto = fase markup matura, mean reversion attesa; seasonality forte = abitudini collettive consolidate, replicabili. Preferisci mean reversion strutturali: deviazioni significative tendono a ritornare su orizzonti multipli. Identifica analogie tra regime corrente e fasi tipiche (compressione vol, espansione, esaurimento trend)." "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": { "meteorologist": {
"directive": "Pensa come un meteorologo: la volatilita ha regimi persistenti con transizioni brusche. Vol regime e input primario. Std + kurt = microclima: std bassa + kurt bassa = calma stabile (vendi vol), std alta + kurt alta = tempesta (compra convexity), std bassa + kurt alta = calma ingannevole (riduci esposizione). AR(1) recente > baseline = fronte persistente in arrivo; Hurst > 0.55 = sistema su scala lunga (ciclone), Hurst < 0.45 = turbolenza locale (no trend persistente); vol percentile estremo = posizione nel ciclo seasonal; seasonality alta = pattern cyclonico ricorrente. Strategie regime-aware: gate espliciti su vol che attivano logiche diverse. Pattern multi-regime sono robusti." "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": { "engineer": {
"directive": "Pensa come un ingegnere di controllo: ogni segnale deve avere SNR favorevole e robustezza. Std e il rumore di fondo: il segnale deve essere significativamente piu grande della varianza intraday. Kurt alta riduce affidabilita dei segnali medi. AR(1) > 0.05 con std contenuta = SNR favorevole, segnale azionabile; AR(1) ≈0 = random walk, non costruirci sopra; Hurst < 0.45 = filtro mean-reversion causale efficace; vol percentile > 80 = saturazione (sensori instabili, riduci leverage); seasonality < 0.05 = feature temporali sono rumore, NON usarle. Pattern semplici e tarabili: poche condizioni in AND, soglie con margine, isteresi entry/exit. Robustezza > ottimalita su singolo regime." "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": { "military_strategist": {
"directive": "Pensa come uno stratega: distingui regime offensivo da difensivo. Vol regime medium/low + skew positivo + kurt moderata = terreno favorevole all'attacco (entry direzionali su breakout/momentum). Vol regime high + kurt elevata = terreno ostile (difesa: posizioni limitate, exit rapide). AR(1) > 0 = vento alle spalle, carica con momentum; AR(1) < 0 = imboscata possibile, contrarian; Hurst > 0.55 = posizione difendibile (hold trade); vol percentile alta = artiglieria nemica attiva (ritirata); seasonality forte = via predicibile da percorrere, sfruttala. Concentrazione: poche condizioni forti. Economia: flat quando il segnale non e dominante. Sorpresa: contrarian su skew estremo (consensus). Sicurezza: ogni entry con exit chiara." "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": { "psychologist": {
"directive": "Pensa come uno psicologo del comportamento collettivo: skew e kurt catturano emozioni. Skew neg + kurt alta = paura ricorrente (capitulation spikes, fade gli estremi al ribasso). Skew pos + kurt alta = euforia (FOMO spikes, fade gli estremi al rialzo). Skew ≈0 + kurt bassa = apatia/range (gioca i bordi). AR(1) recente >> baseline = euforia coordinata in corso, posizionati contro l'ultimo arrivato; Hurst > 0.55 = trance collettiva (trend trance, dura piu del razionale); vol percentile estremo + kurt alta = momentum emozionale puro; seasonality intra-day forte = bias circadiani sfruttabili (FOMO apertura US, panico chiusura asiatica). Estremi di oscillatori in regimi emotivi (kurt alta), crossover in regimi razionali (kurt ≈3). Contrarian sugli estremi, continuazione sulle medie." "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
}
]
}