Compare commits
20 Commits
9c3b5ad586
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 23b7273e71 | |||
| 9c871d1d86 | |||
| 8b767da5e7 | |||
| 1c0058ec3b | |||
| 220e510d5e | |||
| 9742df3a1f | |||
| 21b5cf1eae | |||
| a29748e3d8 | |||
| fa11cca2bc | |||
| 6526c6e6e3 | |||
| ccf9f7a33c | |||
| 19a3592a20 | |||
| 5202eb517b | |||
| 898b24b6a3 | |||
| b6f48e46fc | |||
| 0fd31d52ec | |||
| a43157cd44 | |||
| 436613bfde | |||
| f55e4f00c5 | |||
| 96e08ff78f |
+6
-3
@@ -29,12 +29,15 @@ GA_DB_PATH=./state/runs.db
|
|||||||
STRATEGY_CRYPTO_DB_PATH=./state/strategy_crypto.db
|
STRATEGY_CRYPTO_DB_PATH=./state/strategy_crypto.db
|
||||||
|
|
||||||
# Docker / Traefik (usati SOLO da docker-compose.yml)
|
# Docker / Traefik (usati SOLO da docker-compose.yml)
|
||||||
# Dominio base: traefik espone la dashboard su swarm.${DOMAIN_NAME}/strategy_crypto_gui
|
# Dominio base: traefik espone le dashboard su swarm.${DOMAIN_NAME}/...
|
||||||
DOMAIN_NAME=tielogic.xyz
|
DOMAIN_NAME=tielogic.xyz
|
||||||
# Porta interna della NiceGUI dashboard (Traefik fa il TLS davanti)
|
# Porta interna della NiceGUI dashboard (Traefik fa il TLS davanti)
|
||||||
SWARM_DASHBOARD_PORT=8080
|
SWARM_DASHBOARD_PORT=8080
|
||||||
# Subpath URL del dashboard NiceGUI (usato come root_path in produzione)
|
# Subpath URL del dashboard NiceGUI — ora PER-SERVIZIO nel docker-compose.yml:
|
||||||
DASHBOARD_ROOT_PATH=/strategy_crypto_gui
|
# strategy-crypto-gui -> DASHBOARD_ROOT_PATH=/strategy_crypto_gui
|
||||||
|
# multi-swarm-core-gui -> DASHBOARD_ROOT_PATH=/multi_swarm_core_gui
|
||||||
|
# In sviluppo locale lascia vuoto (nessun subpath).
|
||||||
|
DASHBOARD_ROOT_PATH=
|
||||||
|
|
||||||
# Paper-trading runner — override del command nel compose (opzionali)
|
# Paper-trading runner — override del command nel compose (opzionali)
|
||||||
PAPER_RUN_NAME=phase3-papertrade-prod
|
PAPER_RUN_NAME=phase3-papertrade-prod
|
||||||
|
|||||||
+6
-2
@@ -37,8 +37,12 @@ COPY scripts ./scripts
|
|||||||
|
|
||||||
ENV PATH="/app/.venv/bin:$PATH" \
|
ENV PATH="/app/.venv/bin:$PATH" \
|
||||||
PYTHONUNBUFFERED=1 \
|
PYTHONUNBUFFERED=1 \
|
||||||
PYTHONDONTWRITEBYTECODE=1 \
|
PYTHONDONTWRITEBYTECODE=1
|
||||||
PYTHONPATH=/app/src
|
# NO PYTHONPATH: con uv workspace + layout doppio-nest, PYTHONPATH=/app/src
|
||||||
|
# farebbe ombra alla venv risolvendo le member-dir (multi_swarm_core/,
|
||||||
|
# strategy_crypto/) come namespace packages senza i sub-package del codice.
|
||||||
|
# I pacchetti sono installati come editable dal `uv sync --frozen` del builder
|
||||||
|
# e risolvibili direttamente via /app/.venv/.
|
||||||
|
|
||||||
RUN useradd -m -u 1000 app \
|
RUN useradd -m -u 1000 app \
|
||||||
&& mkdir -p /app/data /app/series /app/state /app/strategies \
|
&& mkdir -p /app/data /app/series /app/state /app/strategies \
|
||||||
|
|||||||
@@ -12,23 +12,23 @@ git clone ssh://git@git.tielogic.xyz:222/Adriano/Multi_Swarm_Coevolutive.git
|
|||||||
|
|
||||||
## Layout monorepo (uv workspace)
|
## Layout monorepo (uv workspace)
|
||||||
|
|
||||||
Il repo è un **workspace uv** con due member packages indipendenti:
|
Il repo è un **workspace uv** con due member packages indipendenti, principio "**core = framework, strategy = contenuto**":
|
||||||
|
|
||||||
```
|
```
|
||||||
multi_swarm_coevolutive/ repo root (workspace coordinator)
|
multi_swarm_coevolutive/ repo root (workspace coordinator)
|
||||||
├── pyproject.toml workspace + dev deps + ruff/mypy/pytest
|
├── pyproject.toml workspace + dev deps + ruff/mypy/pytest
|
||||||
├── docker-compose.yml strategy-crypto-paper + strategy-crypto-gui
|
├── docker-compose.yml 3 servizi su immagine condivisa
|
||||||
├── Dockerfile immagine multi-swarm-coevolutive:dev
|
├── Dockerfile immagine multi-swarm-coevolutive:dev
|
||||||
├── uv.lock lock unico del workspace
|
├── uv.lock lock unico del workspace
|
||||||
├── data/, series/, state/ cache OHLCV + DB (runtime, gitignored)
|
├── data/, series/, state/ cache OHLCV + DB (runtime, gitignored)
|
||||||
├── scripts/ CLI entrypoints (run_phase1, run_paper_trading, ...)
|
├── scripts/ CLI entrypoints (run_phase1, run_paper_trading, ...)
|
||||||
└── src/
|
└── src/
|
||||||
├── multi_swarm_core/ WORKSPACE MEMBER (wheel: multi-swarm-core)
|
├── multi_swarm_core/ WORKSPACE MEMBER (wheel: multi-swarm-core)
|
||||||
│ ├── pyproject.toml deps: pandas, numpy, openai, pydantic, ...
|
│ ├── pyproject.toml core deps (pandas, numpy, openai, pydantic, nicegui, ...)
|
||||||
│ ├── multi_swarm_core/ GA + genome + protocol + backtest + cerbero +
|
│ ├── multi_swarm_core/ GA + genome + protocol + backtest + cerbero +
|
||||||
│ │ data + llm + agents + ga + orchestrator +
|
│ │ data + llm + agents + ga + orchestrator +
|
||||||
│ │ metrics + persistence + config
|
│ │ metrics + persistence + config + dashboard (GA-only)
|
||||||
│ ├── tests/ unit + integration (182 test)
|
│ ├── tests/ unit + integration
|
||||||
│ └── docs/ design/ + decisions/ + reports/
|
│ └── docs/ design/ + decisions/ + reports/
|
||||||
│
|
│
|
||||||
└── strategy_crypto/ WORKSPACE MEMBER (wheel: strategy-crypto)
|
└── strategy_crypto/ WORKSPACE MEMBER (wheel: strategy-crypto)
|
||||||
@@ -36,27 +36,73 @@ multi_swarm_coevolutive/ repo root (workspace coordinator)
|
|||||||
├── README.md overview strategia + pattern per nuove strategie
|
├── README.md overview strategia + pattern per nuove strategie
|
||||||
├── strategy_crypto/
|
├── strategy_crypto/
|
||||||
│ ├── backend/ paper-trading (executor, portfolio, persistence, schema)
|
│ ├── backend/ paper-trading (executor, portfolio, persistence, schema)
|
||||||
│ ├── frontend/ NiceGUI dashboard dual-DB
|
│ ├── frontend/ NiceGUI paper-only dashboard
|
||||||
│ └── strategies/ JSON freezate (btc_*.json, eth_*.json)
|
│ ├── strategies/ JSON freezate (btc_*.json, eth_*.json)
|
||||||
└── tests/ smoke regression (import + json + schema)
|
│ └── prompts.json v3.2: agent_role/pattern_guidance/instruction/
|
||||||
|
│ domain_warnings/anti_patterns/output_priorities +
|
||||||
|
│ 7 stili cognitive (directive + focus_metrics)
|
||||||
|
└── tests/ smoke regression
|
||||||
```
|
```
|
||||||
|
|
||||||
**DB separati per dominio:** `state/runs.db` (GA core universale) + `state/strategy_crypto.db` (paper della strategia crypto). Pattern scala a N strategie senza naming collision.
|
**DB separati per dominio:** `state/runs.db` (GA core universale) + `state/strategy_crypto.db` (paper della strategia crypto). Pattern scala a N strategie senza naming collision.
|
||||||
|
|
||||||
**Pattern N strategie future:** aggiungere `src/strategy_<asset>/` con lo stesso scheletro (`backend/`, `frontend/`, `strategies/`, `tests/`), DB dedicato `state/strategy_<asset>.db`, servizi Docker `strategy-<asset>-paper` + `strategy-<asset>-gui`, GUI su `/strategy_<asset>_gui`.
|
**Pattern N strategie future:** aggiungere `src/strategy_<asset>/` con stesso scheletro (`backend/`, `frontend/`, `strategies/`, `tests/`, `prompts.json`), DB dedicato `state/strategy_<asset>.db`, servizi Docker `strategy-<asset>-paper` + `strategy-<asset>-gui`, GUI su `/strategy_<asset>_gui`. **Zero modifiche al core** richieste.
|
||||||
|
|
||||||
|
## Architettura prompt (v3.2)
|
||||||
|
|
||||||
|
**Compositor**: il SYSTEM prompt al LLM viene COMPOSTO at-runtime da scaffold core + contenuto strategy:
|
||||||
|
|
||||||
|
```
|
||||||
|
[1] agent_role ← strategy (prompts.json — chi è l'agente)
|
||||||
|
[2] cognitive_style + directive ← genome (evoluti dal GA)
|
||||||
|
[3] GRAMMAR_SPEC ← core scaffold (operatori, indicatori, units rules)
|
||||||
|
[4] pattern_guidance ← strategy (forme di curva astratte, no indicatori prescritti)
|
||||||
|
[5] domain_warnings ← strategy (es. "crypto trada 24/7, NON inferire funding rate")
|
||||||
|
[6] CONSTRAINTS ← core scaffold (validator semantics)
|
||||||
|
[7] anti_patterns ← strategy (7 voci: no >4 AND, no chattering, isteresi, ecc.)
|
||||||
|
[8] output_priorities ← strategy (5 voci, #1 coerenza con lente cognitiva)
|
||||||
|
[9] EXAMPLE ← core scaffold
|
||||||
|
```
|
||||||
|
|
||||||
|
**Input USER (calcolato da `build_market_summary`):**
|
||||||
|
- Base (5): mean, std, skew, kurt, vol_regime
|
||||||
|
- Regime recente rolling 500 (6): autocorr_lag1 (recent + baseline), hurst, vol_percentile, seasonality (hour + dow)
|
||||||
|
- Geometria & frattali (7): efficiency_ratio (Kaufman), tail_index (left + right Hill), structural_uptrend (HH/HL), compression, spectral_entropy, dominant_cycle (gated)
|
||||||
|
- Feature accessibili dal genome + lookback_window
|
||||||
|
- **Focus per la tua lente**: blocco style-aware (4 metriche prioritarie da `focus_metrics` di prompts.json)
|
||||||
|
- Instruction finale (da strategy)
|
||||||
|
|
||||||
|
**Grammar protocol JSON (8 indicatori):**
|
||||||
|
| Indicatore | Output | Range |
|
||||||
|
|------------|--------|-------|
|
||||||
|
| `sma(length)` | media mobile | unità prezzo |
|
||||||
|
| `sma_pct(length)` | (close-sma)/sma | ±0.1 frazione |
|
||||||
|
| `rsi(length)` | oscillator | 0-100 |
|
||||||
|
| `atr(length)` | true range | unità prezzo |
|
||||||
|
| `atr_pct(length)` | atr/close | 0-0.1 frazione |
|
||||||
|
| `realized_vol(window)` | std returns | 0-0.1 frazione |
|
||||||
|
| `macd(fast,slow,signal)` | momentum | unità prezzo |
|
||||||
|
| `macd_pct(...)` | macd/close | ±0.02 frazione |
|
||||||
|
|
||||||
|
**7 stili cognitive** (in `prompts.json`, editable): physicist, biologist, historian, meteorologist, engineer, military_strategist, psychologist. Ognuno con directive 800-950 char, ASCII-strict, archetipo dominante + lookback consigliato + 4 focus_metrics.
|
||||||
|
|
||||||
## Stato del progetto
|
## Stato del progetto
|
||||||
|
|
||||||
**Phase 3 (paper-trading forward-test) in corso** dal 13 maggio 2026 su VPS. Runner `scripts/run_paper_trading.py` long-running in Docker, dashboard NiceGUI su `https://swarm.tielogic.xyz/strategy_crypto_gui/`. Due strategie freezate:
|
**Phase 3 (paper-trading forward-test) in corso** dal 13 maggio 2026 su VPS. Runner `scripts/run_paper_trading.py` long-running in Docker, dashboard NiceGUI su `https://swarm.tielogic.xyz/strategy_crypto_gui/`. Due strategie freezate in `src/strategy_crypto/strategy_crypto/strategies/`:
|
||||||
|
|
||||||
- `strategy_crypto/strategies/btc_fb63e851.json` — BTC-PERPETUAL, RSI estremi + ATR vs SMA + filtro orario 9-17 (Sharpe OOS +0,26 su 7,33 anni).
|
- `btc_fb63e851.json` — BTC-PERPETUAL, RSI estremi + ATR vs SMA + filtro orario 9-17 (Sharpe OOS +0,26 su 7,33 anni).
|
||||||
- `strategy_crypto/strategies/eth_facd6af85d5d.json` — ETH-PERPETUAL, regime-based (Sharpe OOS +0,19 su 6,75 anni).
|
- `eth_facd6af85d5d.json` — ETH-PERPETUAL, regime-based (con `atr_pct` post-fix bug unità).
|
||||||
|
|
||||||
Phase 1 → 2.7 chiuse (30 run GA, $3.74 cumulato LLM).
|
Phase 1 → 2.7 chiuse (30 run GA, $3.74 cumulato LLM). Sessione refactor 15 maggio 2026:
|
||||||
|
- Split repo invertito, monorepo unificato come uv workspace
|
||||||
|
- Family `*_pct` completa (atr_pct, sma_pct, macd_pct) per fix bug unità
|
||||||
|
- Dashboard split: core (GA) vs strategy (paper)
|
||||||
|
- Prompt architecture compositor + prompts.json v3.2 (vedi decision log)
|
||||||
|
|
||||||
- [**Stato progetto e roadmap (14 maggio 2026)**](src/multi_swarm_core/docs/reports/2026-05-14-stato-progetto-e-roadmap.md) — riepilogo fasi, decisioni, caveat, roadmap.
|
Documenti:
|
||||||
- Decision log: [`src/multi_swarm_core/docs/decisions/`](src/multi_swarm_core/docs/decisions/) (gate Phase 1, scelta nemotron).
|
- [**Stato progetto e roadmap (14 maggio 2026)**](src/multi_swarm_core/docs/reports/2026-05-14-stato-progetto-e-roadmap.md)
|
||||||
- Design docs concettuali: [`src/multi_swarm_core/docs/design/`](src/multi_swarm_core/docs/design/).
|
- Decision log: [`src/multi_swarm_core/docs/decisions/`](src/multi_swarm_core/docs/decisions/) (gate Phase 1, nemotron, atr_pct fix)
|
||||||
|
- Design docs concettuali: [`src/multi_swarm_core/docs/design/`](src/multi_swarm_core/docs/design/)
|
||||||
|
|
||||||
Stack: Python 3.13, uv workspace, hatchling, pytest+pytest-mock+responses, openai SDK (verso OpenRouter), requests+tenacity, pandas+numpy+scipy, sqlmodel+sqlite, nicegui+plotly, yfinance.
|
Stack: Python 3.13, uv workspace, hatchling, pytest+pytest-mock+responses, openai SDK (verso OpenRouter), requests+tenacity, pandas+numpy+scipy, sqlmodel+sqlite, nicegui+plotly, yfinance.
|
||||||
|
|
||||||
@@ -65,7 +111,7 @@ Stack: Python 3.13, uv workspace, hatchling, pytest+pytest-mock+responses, opena
|
|||||||
```bash
|
```bash
|
||||||
uv sync # installa entrambi i workspace member come editable
|
uv sync # installa entrambi i workspace member come editable
|
||||||
cp .env.example .env # compila CERBERO_*_TOKEN e OPENROUTER_API_KEY
|
cp .env.example .env # compila CERBERO_*_TOKEN e OPENROUTER_API_KEY
|
||||||
uv run pytest # 186 test attesi (182 core + 4 smoke strategy_crypto)
|
uv run pytest # 252 test attesi (248 core + 4 smoke strategy_crypto)
|
||||||
```
|
```
|
||||||
|
|
||||||
### Variabili .env richieste
|
### Variabili .env richieste
|
||||||
@@ -84,10 +130,9 @@ OPENROUTER_API_KEY=<sk-or-v1-...>
|
|||||||
GA_DB_PATH=./state/runs.db
|
GA_DB_PATH=./state/runs.db
|
||||||
STRATEGY_CRYPTO_DB_PATH=./state/strategy_crypto.db
|
STRATEGY_CRYPTO_DB_PATH=./state/strategy_crypto.db
|
||||||
|
|
||||||
# Deploy Docker
|
# Deploy Docker — DASHBOARD_ROOT_PATH ora per-servizio (vedi docker-compose.yml)
|
||||||
DOMAIN_NAME=tielogic.xyz
|
DOMAIN_NAME=tielogic.xyz
|
||||||
SWARM_DASHBOARD_PORT=8080
|
SWARM_DASHBOARD_PORT=8080
|
||||||
DASHBOARD_ROOT_PATH=/strategy_crypto_gui # subpath traefik per la dashboard
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Backcompat: `DB_PATH` legacy continua a funzionare come alias di `GA_DB_PATH`.
|
Backcompat: `DB_PATH` legacy continua a funzionare come alias di `GA_DB_PATH`.
|
||||||
@@ -105,14 +150,27 @@ uv run mypy src/ scripts/
|
|||||||
# Smoke run (MockLLM + OHLCV sintetico, no API calls)
|
# Smoke run (MockLLM + OHLCV sintetico, no API calls)
|
||||||
uv run python scripts/smoke_run.py
|
uv run python scripts/smoke_run.py
|
||||||
|
|
||||||
# Run reale Phase 1/2 (Cerbero + OpenRouter, ~$0.07 per run K=20 10gen)
|
# Run reale Phase 1/2 (Cerbero + OpenRouter, ~$0.15-0.25 per run K=20 10gen,
|
||||||
|
# ~$0.40-0.55 per run esteso K=40 20gen con WFA OOS).
|
||||||
|
# Default --start ora 2018-09-01 (7.3y, copre bear+halving+covid+ATH+winter+ETF).
|
||||||
uv run python scripts/run_phase1.py \
|
uv run python scripts/run_phase1.py \
|
||||||
--name run-XXX \
|
--name run-XXX \
|
||||||
--exchange deribit --symbol BTC-PERPETUAL --timeframe 1h \
|
--exchange deribit --symbol BTC-PERPETUAL --timeframe 1h \
|
||||||
--start 2024-01-01T00:00:00+00:00 \
|
|
||||||
--end 2026-01-01T00:00:00+00:00 \
|
|
||||||
--population-size 20 --n-generations 10 \
|
--population-size 20 --n-generations 10 \
|
||||||
--prompt-mutation-weight 0.30 --fitness-v2
|
--prompt-mutation-weight 0.30 --fitness-v2 \
|
||||||
|
--llm-concurrency 8 # 5-8x speedup wall time (default 1)
|
||||||
|
# fitness-v2 hardened: hard-kill su {no_trades, degenerate, undertrading,
|
||||||
|
# fees_eat_alpha, negative_net_pnl}. Override via --fitness-hard-kill CSV.
|
||||||
|
# Default --prompt-library: importlib.resources del package strategy_crypto/prompts.json
|
||||||
|
|
||||||
|
# Multi-fold validation di un run esistente (anti-overfit, 7y expanding-window)
|
||||||
|
uv run python scripts/validate_run.py \
|
||||||
|
--run-id <run_id> \
|
||||||
|
--top-k 10 --n-folds 4 --train-ratio 0.5 \
|
||||||
|
--start 2018-09-01T00:00:00+00:00 --end 2026-01-01T00:00:00+00:00 \
|
||||||
|
--fitness-v2 \
|
||||||
|
--output-json state/validation-XXX.json
|
||||||
|
# Ranking per "robust_score" = min(fitness_oos) su tutti i fold.
|
||||||
|
|
||||||
# Backtest standalone di una strategia JSON
|
# Backtest standalone di una strategia JSON
|
||||||
uv run python scripts/backtest_strategy.py \
|
uv run python scripts/backtest_strategy.py \
|
||||||
@@ -123,32 +181,50 @@ uv run python scripts/backtest_strategy.py \
|
|||||||
uv run python scripts/run_paper_trading.py \
|
uv run python scripts/run_paper_trading.py \
|
||||||
--name phase3-papertrade-XXX \
|
--name phase3-papertrade-XXX \
|
||||||
--initial-capital 1000 --poll-seconds 300
|
--initial-capital 1000 --poll-seconds 300
|
||||||
# Default --strategies-dir: importlib.resources del package strategy_crypto
|
|
||||||
|
|
||||||
# Dashboard NiceGUI locale
|
# Dashboard NiceGUI locale (2 distinte)
|
||||||
uv run python -m strategy_crypto.frontend.nicegui_app
|
uv run python -m multi_swarm_core.dashboard.nicegui_app # GA core (/, /convergence, /genomes)
|
||||||
# → http://localhost:8080 (env SWARM_DASHBOARD_PORT)
|
uv run python -m strategy_crypto.frontend.nicegui_app # Strategy crypto (/ paper)
|
||||||
```
|
```
|
||||||
|
|
||||||
## Dashboard
|
## Performance & Validation
|
||||||
|
|
||||||
NiceGUI dashboard (dark palette) — **dual-DB reader** (GA + paper):
|
**Backtest engine vettorializzato** (`backtest/engine.py`): rimosso il loop `pd.iterrows()` a favore di state machine numpy. Speedup misurati:
|
||||||
|
|
||||||
|
| Dataset | Before (iterrows) | After (vectorized) | Speedup |
|
||||||
|
|---------|-------------------|--------------------|---------|
|
||||||
|
| 2 anni (17545 bar) | 470 ms | **28 ms** | **16.8×** |
|
||||||
|
| 7 anni (64297 bar) | 1744 ms | **154 ms** | **11.3×** |
|
||||||
|
|
||||||
|
Equivalenza numerica garantita: 5 parity test parametrici vs. reference implementation legacy (`test_backtest_engine_vectorized.py`).
|
||||||
|
|
||||||
|
**Parallel propose LLM** (`orchestrator/run.py`): `--llm-concurrency N` lancia N chiamate `hypothesis_agent.propose()` concorrenti per generazione tramite `ThreadPoolExecutor`. OpenRouter qwen-2.5 regge 6-10 concorrenti senza rate-limit. Default 1 = backward-compat.
|
||||||
|
|
||||||
|
**Multi-fold validation tool** (`scripts/validate_run.py`): qualunque run completato puo' essere rivalutato post-hoc su N fold expanding-window di un dataset esteso (tipicamente 7 anni). Vital per evitare il single-hold-out overfit: il GA puo' selezionare un genome con `fitness_is` alta che collassa OOS (osservato su `phase1-extended-001`: elite IS Sharpe 1.93, OOS Sharpe -1.00). Ranking finale per `robust_score = min(fitness_oos)`. Output JSON con per-fold breakdown + aggregati mean/min/std.
|
||||||
|
|
||||||
|
## Dashboard (split core + strategy)
|
||||||
|
|
||||||
|
Due NiceGUI dashboard distinte (dark palette, palette neon):
|
||||||
|
|
||||||
|
**Core GA** — `multi_swarm_core.dashboard.nicegui_app` → `https://swarm.tielogic.xyz/multi_swarm_core_gui/`:
|
||||||
- **Overview** (`/`): lista runs GA, costo cumulato, metriche aggregate evaluations.
|
- **Overview** (`/`): lista runs GA, costo cumulato, metriche aggregate evaluations.
|
||||||
- **GA Convergence** (`/convergence`): fitness median/max/p90 per generazione + entropy.
|
- **GA Convergence** (`/convergence`): fitness median/max/p90 per generazione + entropy.
|
||||||
- **Genomes** (`/genomes`): top-K ordinati per fitness, ispezione system_prompt + JSON strategy.
|
- **Genomes** (`/genomes`): top-K ordinati per fitness, ispezione system_prompt + JSON strategy.
|
||||||
- **Paper** (`/paper`): forward-test live con equity curve, posizioni aperte, trade list, tick log.
|
|
||||||
|
|
||||||
In produzione su `https://swarm.tielogic.xyz/strategy_crypto_gui/` (subpath gestito via `DASHBOARD_ROOT_PATH` + Traefik PathPrefix). La root del dominio resta libera per future GUI di altre strategie.
|
**Strategy crypto** — `strategy_crypto.frontend.nicegui_app` → `https://swarm.tielogic.xyz/strategy_crypto_gui/`:
|
||||||
|
- **Paper** (`/`): forward-test live con equity curve, posizioni aperte, trade list, tick log.
|
||||||
|
|
||||||
|
In produzione subpath gestiti via `DASHBOARD_ROOT_PATH` (per-servizio) + Traefik `replacepathregex` (NB: NON `stripprefix`, vedi sezione Deploy). La root del dominio resta libera per future GUI di altre strategie.
|
||||||
|
|
||||||
## Deploy
|
## Deploy
|
||||||
|
|
||||||
`docker-compose.yml` definisce due servizi su immagine `multi-swarm-coevolutive:dev`:
|
`docker-compose.yml` definisce 3 servizi su immagine condivisa `multi-swarm-coevolutive:dev`:
|
||||||
|
|
||||||
- **`strategy-crypto-paper`** — runner `scripts/run_paper_trading.py` long-running.
|
- **`strategy-crypto-paper`** — runner `scripts/run_paper_trading.py` long-running.
|
||||||
- **`strategy-crypto-gui`** — NiceGUI dashboard dietro Traefik su `https://swarm.${DOMAIN_NAME}/strategy_crypto_gui/`.
|
- **`strategy-crypto-gui`** — NiceGUI paper dashboard su `https://swarm.${DOMAIN_NAME}/strategy_crypto_gui/`.
|
||||||
|
- **`multi-swarm-core-gui`** — NiceGUI GA dashboard su `https://swarm.${DOMAIN_NAME}/multi_swarm_core_gui/`.
|
||||||
|
|
||||||
Persistenza via bind mount: `./data/`, `./series/`, `./state/`. Le strategie JSON sono bind-mounted in read-only dal package: `./src/strategy_crypto/strategy_crypto/strategies/`.
|
Persistenza via bind mount: `./data/`, `./series/`, `./state/`. Strategie JSON bind-mounted in read-only dal package: `./src/strategy_crypto/strategy_crypto/strategies/`.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker compose up -d --build
|
docker compose up -d --build
|
||||||
@@ -158,12 +234,16 @@ docker compose ps
|
|||||||
|
|
||||||
Note operative:
|
Note operative:
|
||||||
|
|
||||||
- Le bind-mount dir devono essere `chown 1000:1000` (uid utente `app` nel container).
|
- Bind-mount dir devono essere `chown 1000:1000` (uid utente `app` nel container). **Anche `src/strategy_crypto/strategy_crypto/strategies/`** (creata da `git mv`, default `root:root`).
|
||||||
- Override del command paper via env (`PAPER_RUN_NAME`, `PAPER_INITIAL_CAPITAL`, ecc.).
|
- Override del command paper via env (`PAPER_RUN_NAME`, `PAPER_INITIAL_CAPITAL`, ecc.).
|
||||||
- `SWARM_DASHBOARD_PORT` controlla la porta interna del container (Traefik fa il TLS).
|
- `SWARM_DASHBOARD_PORT` controlla la porta interna del container (Traefik fa il TLS).
|
||||||
|
- **Traefik subpath**: usa `replacepathregex` middleware (NON `stripprefix`) per evitare doppio root_path (uvicorn legge `X-Forwarded-Prefix` da stripprefix + applica `root_path` di NiceGUI = doppio prefix). Vedi commit `436613b`.
|
||||||
|
- Dopo cambio label Traefik: `docker restart traefik-traefik-1` per forzare refresh discovery.
|
||||||
|
|
||||||
## Sviluppo
|
## Sviluppo
|
||||||
|
|
||||||
Conventional commits con prefix `feat:` `fix:` `chore:` `docs:` `refactor:` `test:`. Body italiano. Footer `Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>` su ogni commit collaborativo.
|
Conventional commits con prefix `feat:` `fix:` `chore:` `docs:` `refactor:` `test:`. Body italiano. Footer `Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>` su ogni commit collaborativo.
|
||||||
|
|
||||||
Branch attuale: `main`. Workspace single-repo, monorepo unificato dal 15 maggio 2026 (split temporaneo monorepo→figlio invertito, vedi tag `v0.1.0-pre-split` come bookmark).
|
Branch attuale: `main`. Workspace single-repo, monorepo unificato dal 15 maggio 2026 (split temporaneo monorepo→figlio invertito, vedi tag `v0.1.0-pre-split` come bookmark).
|
||||||
|
|
||||||
|
**Modificare il prompt LLM** senza toccare codice: edita `src/strategy_crypto/strategy_crypto/prompts.json`. Schema documentato in `_design_invariants` del JSON stesso. I 3 regression guard test (`test_strategy_crypto_directives_ascii_safe`, `..._have_archetype_marker`, `..._have_lookback_hint`) bloccano regressioni accidentali sulle invarianti di design.
|
||||||
|
|||||||
+56
-10
@@ -1,12 +1,14 @@
|
|||||||
# docker-compose.yml — Multi-Swarm Coevolutive
|
# docker-compose.yml — Multi-Swarm Coevolutive
|
||||||
#
|
#
|
||||||
# Due servizi della strategia crypto, condividono la stessa immagine
|
# Tre servizi della strategia crypto, condividono la stessa immagine
|
||||||
# `multi-swarm-coevolutive:dev` buildata dal Dockerfile root (uv workspace):
|
# `multi-swarm-coevolutive:dev` buildata dal Dockerfile root (uv workspace):
|
||||||
#
|
#
|
||||||
# * strategy-crypto-paper — paper-trading runner long-running
|
# * strategy-crypto-paper — paper-trading runner long-running
|
||||||
# (scripts/run_paper_trading.py)
|
# (scripts/run_paper_trading.py)
|
||||||
# * strategy-crypto-gui — NiceGUI dashboard esposta da Traefik su
|
# * strategy-crypto-gui — NiceGUI dashboard esposta da Traefik su
|
||||||
# https://swarm.${DOMAIN_NAME:-tielogic.xyz}/strategy_crypto_gui
|
# https://swarm.${DOMAIN_NAME:-tielogic.xyz}/strategy_crypto_gui
|
||||||
|
# * multi-swarm-core-gui — NiceGUI dashboard GA esposta su
|
||||||
|
# https://swarm.${DOMAIN_NAME:-tielogic.xyz}/multi_swarm_core_gui
|
||||||
#
|
#
|
||||||
# Entrambi joinano la rete external `traefik` cosi' il client Cerbero
|
# Entrambi joinano la rete external `traefik` cosi' il client Cerbero
|
||||||
# risolve direttamente l'host `cerbero-mcp` (porta 9000) senza passare
|
# risolve direttamente l'host `cerbero-mcp` (porta 9000) senza passare
|
||||||
@@ -36,8 +38,11 @@ x-swarm-env: &swarm-env
|
|||||||
# DB separati per dominio:
|
# DB separati per dominio:
|
||||||
GA_DB_PATH: /app/state/runs.db
|
GA_DB_PATH: /app/state/runs.db
|
||||||
STRATEGY_CRYPTO_DB_PATH: /app/state/strategy_crypto.db
|
STRATEGY_CRYPTO_DB_PATH: /app/state/strategy_crypto.db
|
||||||
# Subpath sotto cui la dashboard NiceGUI e' esposta da Traefik
|
# DASHBOARD_ROOT_PATH e' ora per-servizio (vedi environment blocks sotto).
|
||||||
DASHBOARD_ROOT_PATH: /strategy_crypto_gui
|
# IMPORTANT: NON usare StripPrefix middleware con questo. NiceGUI/Starlette
|
||||||
|
# gestisce internamente il root_path su request path che ARRIVANO con prefix.
|
||||||
|
# StripPrefix causa doppio prefix negli asset URL (NiceGUI prefixa + uvicorn
|
||||||
|
# rilegge X-Forwarded-Prefix e prefixa di nuovo).
|
||||||
|
|
||||||
services:
|
services:
|
||||||
strategy-crypto-paper:
|
strategy-crypto-paper:
|
||||||
@@ -81,11 +86,10 @@ services:
|
|||||||
env_file: .env
|
env_file: .env
|
||||||
environment:
|
environment:
|
||||||
<<: *swarm-env
|
<<: *swarm-env
|
||||||
|
DASHBOARD_ROOT_PATH: /strategy_crypto_gui
|
||||||
volumes:
|
volumes:
|
||||||
# Dashboard legge entrambi i DB: state/ in read-only (WAL: vedi nota)
|
# Dashboard legge solo strategy_crypto.db: state/ in read-only (WAL: vedi nota)
|
||||||
- ./state:/app/state:ro
|
- ./state:/app/state:ro
|
||||||
- ./data:/app/data:ro
|
|
||||||
- ./series:/app/series:ro
|
|
||||||
entrypoint:
|
entrypoint:
|
||||||
- python
|
- python
|
||||||
- -m
|
- -m
|
||||||
@@ -109,4 +113,46 @@ services:
|
|||||||
- traefik.http.routers.strategy-crypto-gui.entrypoints=websecure
|
- traefik.http.routers.strategy-crypto-gui.entrypoints=websecure
|
||||||
- traefik.http.routers.strategy-crypto-gui.tls.certresolver=mytlschallenge
|
- traefik.http.routers.strategy-crypto-gui.tls.certresolver=mytlschallenge
|
||||||
- "traefik.http.services.strategy-crypto-gui.loadbalancer.server.port=${SWARM_DASHBOARD_PORT:-8080}"
|
- "traefik.http.services.strategy-crypto-gui.loadbalancer.server.port=${SWARM_DASHBOARD_PORT:-8080}"
|
||||||
|
# replacepathregex (NON stripprefix): strippa il prefix dalla request senza
|
||||||
|
# aggiungere X-Forwarded-Prefix header. uvicorn vede solo root_path da
|
||||||
|
# ui.run(root_path=...), quindi gli asset URL vengono prefissati una sola
|
||||||
|
# volta (no doppio prefix come succederebbe con stripprefix).
|
||||||
|
- "traefik.http.middlewares.strategy-crypto-replace.replacepathregex.regex=^/strategy_crypto_gui(/.*|$$)"
|
||||||
|
- "traefik.http.middlewares.strategy-crypto-replace.replacepathregex.replacement=$$1"
|
||||||
|
- "traefik.http.routers.strategy-crypto-gui.middlewares=strategy-crypto-replace"
|
||||||
|
- com.centurylinklabs.watchtower.enable=true
|
||||||
|
|
||||||
|
multi-swarm-core-gui:
|
||||||
|
image: multi-swarm-coevolutive:dev
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
container_name: multi-swarm-core-gui
|
||||||
|
restart: unless-stopped
|
||||||
|
networks: [traefik]
|
||||||
|
env_file: .env
|
||||||
|
environment:
|
||||||
|
<<: *swarm-env
|
||||||
|
DASHBOARD_ROOT_PATH: /multi_swarm_core_gui
|
||||||
|
volumes:
|
||||||
|
- ./state:/app/state:ro
|
||||||
|
entrypoint: [python, -m, multi_swarm_core.dashboard.nicegui_app]
|
||||||
|
command: []
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "python", "-c", "import os, urllib.request; urllib.request.urlopen(f'http://localhost:{os.environ.get(\"SWARM_DASHBOARD_PORT\",\"8080\")}/', timeout=3).close()"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 3
|
||||||
|
start_period: 30s
|
||||||
|
labels:
|
||||||
|
- traefik.enable=true
|
||||||
|
- traefik.docker.network=traefik
|
||||||
|
- "traefik.http.routers.multi-swarm-core-gui.rule=Host(`swarm.${DOMAIN_NAME:-tielogic.xyz}`) && PathPrefix(`/multi_swarm_core_gui`)"
|
||||||
|
- traefik.http.routers.multi-swarm-core-gui.tls=true
|
||||||
|
- traefik.http.routers.multi-swarm-core-gui.entrypoints=websecure
|
||||||
|
- traefik.http.routers.multi-swarm-core-gui.tls.certresolver=mytlschallenge
|
||||||
|
- "traefik.http.services.multi-swarm-core-gui.loadbalancer.server.port=${SWARM_DASHBOARD_PORT:-8080}"
|
||||||
|
- "traefik.http.middlewares.multi-swarm-core-replace.replacepathregex.regex=^/multi_swarm_core_gui(/.*|$$)"
|
||||||
|
- "traefik.http.middlewares.multi-swarm-core-replace.replacepathregex.replacement=$$1"
|
||||||
|
- "traefik.http.routers.multi-swarm-core-gui.middlewares=multi-swarm-core-replace"
|
||||||
- com.centurylinklabs.watchtower.enable=true
|
- com.centurylinklabs.watchtower.enable=true
|
||||||
|
|||||||
+7
-2
@@ -4,6 +4,13 @@ version = "0.1.0"
|
|||||||
description = "Multi-Swarm Coevolutive: monorepo workspace (core + strategie)"
|
description = "Multi-Swarm Coevolutive: monorepo workspace (core + strategie)"
|
||||||
authors = [{ name = "Adriano Dal Pastro", email = "adrianodalpastro@tielogic.com" }]
|
authors = [{ name = "Adriano Dal Pastro", email = "adrianodalpastro@tielogic.com" }]
|
||||||
requires-python = ">=3.13"
|
requires-python = ">=3.13"
|
||||||
|
# I due workspace member sono dipendenze runtime del deployable app root.
|
||||||
|
# Cosi' `uv sync --frozen --no-dev` (nel builder Docker) li installa entrambi
|
||||||
|
# come editable nella venv senza tirare pytest/ruff/mypy.
|
||||||
|
dependencies = [
|
||||||
|
"multi-swarm-core",
|
||||||
|
"strategy-crypto",
|
||||||
|
]
|
||||||
|
|
||||||
[tool.uv.workspace]
|
[tool.uv.workspace]
|
||||||
members = ["src/multi_swarm_core", "src/strategy_crypto"]
|
members = ["src/multi_swarm_core", "src/strategy_crypto"]
|
||||||
@@ -21,8 +28,6 @@ dev = [
|
|||||||
"ruff>=0.7",
|
"ruff>=0.7",
|
||||||
"mypy>=1.13",
|
"mypy>=1.13",
|
||||||
"types-requests>=2.32",
|
"types-requests>=2.32",
|
||||||
"multi-swarm-core",
|
|
||||||
"strategy-crypto",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[tool.ruff]
|
[tool.ruff]
|
||||||
|
|||||||
@@ -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()
|
||||||
@@ -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()
|
||||||
@@ -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()
|
||||||
@@ -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
@@ -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)
|
||||||
|
|||||||
@@ -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()
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
"""Per-year breakdown delle 4 strategie: 2 NEW (BTC 238e4812 + ETH c04dff7086)
|
||||||
|
+ 2 OLD freezate (btc_9cf506b8 hardened-001 + eth_facd6af85d5d).
|
||||||
|
|
||||||
|
Backtest anno-per-anno (2019-2025) sul tick di discovery di ciascuna strategia.
|
||||||
|
Output: trade, wins/losses, win%, return%, max DD%, Sharpe per ogni anno.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from multi_swarm_core.backtest.engine import BacktestEngine
|
||||||
|
from multi_swarm_core.cerbero.client import CerberoClient
|
||||||
|
from multi_swarm_core.config import load_settings
|
||||||
|
from multi_swarm_core.data.cerbero_ohlcv import CerberoOHLCVLoader, OHLCVRequest
|
||||||
|
from multi_swarm_core.metrics.basic import max_drawdown, sharpe_ratio, total_return
|
||||||
|
from multi_swarm_core.protocol.compiler import compile_strategy
|
||||||
|
from multi_swarm_core.protocol.parser import parse_strategy
|
||||||
|
|
||||||
|
|
||||||
|
STRATEGIES = [
|
||||||
|
# (label, path, symbol, timeframe)
|
||||||
|
("BTC NEW (238e4812, paper attuale)", "btc_238e4812.json", "BTC-PERPETUAL", "1h"),
|
||||||
|
("BTC OLD (9cf506b8, hardened-001 prev paper)", "archive/btc_9cf506b8.json", "BTC-PERPETUAL", "1h"),
|
||||||
|
("ETH NEW (c04dff7086, paper attuale)", "eth_c04dff7086.json", "ETH-PERPETUAL", "5m"),
|
||||||
|
("ETH OLD (facd6af85d5d, prev paper)", "archive/eth_facd6af85d5d.json", "ETH-PERPETUAL", "1h"),
|
||||||
|
]
|
||||||
|
|
||||||
|
YEARS = [
|
||||||
|
("2019", "2019-01-01T00:00:00+00:00", "2020-01-01T00:00:00+00:00"),
|
||||||
|
("2020", "2020-01-01T00:00:00+00:00", "2021-01-01T00:00:00+00:00"),
|
||||||
|
("2021", "2021-01-01T00:00:00+00:00", "2022-01-01T00:00:00+00:00"),
|
||||||
|
("2022", "2022-01-01T00:00:00+00:00", "2023-01-01T00:00:00+00:00"),
|
||||||
|
("2023", "2023-01-01T00:00:00+00:00", "2024-01-01T00:00:00+00:00"),
|
||||||
|
("2024", "2024-01-01T00:00:00+00:00", "2025-01-01T00:00:00+00:00"),
|
||||||
|
("2025", "2025-01-01T00:00:00+00:00", "2026-01-01T00:00:00+00:00"),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
settings = load_settings()
|
||||||
|
token = (
|
||||||
|
settings.cerbero_mainnet_token.get_secret_value()
|
||||||
|
if settings.cerbero_mainnet_token
|
||||||
|
else settings.cerbero_testnet_token.get_secret_value()
|
||||||
|
)
|
||||||
|
cerbero = CerberoClient(
|
||||||
|
base_url=settings.cerbero_base_url,
|
||||||
|
token=token,
|
||||||
|
bot_tag=settings.cerbero_bot_tag,
|
||||||
|
)
|
||||||
|
loader = CerberoOHLCVLoader(client=cerbero, cache_dir=settings.series_dir)
|
||||||
|
engine = BacktestEngine(fees_bp=5.0)
|
||||||
|
strategies_dir = Path("/app/strategies")
|
||||||
|
|
||||||
|
for label, fname, symbol, timeframe in STRATEGIES:
|
||||||
|
path = strategies_dir / fname
|
||||||
|
strat = parse_strategy(path.read_text())
|
||||||
|
|
||||||
|
# Carica intero range una volta sola
|
||||||
|
ohlcv = loader.load(OHLCVRequest(
|
||||||
|
symbol=symbol, timeframe=timeframe,
|
||||||
|
start=datetime.fromisoformat("2018-09-01T00:00:00+00:00"),
|
||||||
|
end=datetime.fromisoformat("2026-01-01T00:00:00+00:00"),
|
||||||
|
))
|
||||||
|
|
||||||
|
print(f"\n{'=' * 110}")
|
||||||
|
print(f">>> {label}")
|
||||||
|
print(f" symbol={symbol} timeframe={timeframe} | {len(ohlcv)} bars total")
|
||||||
|
print(f" {'year':<6} {'bars':>6} {'trades':>7} {'wins':>5} {'losses':>7} {'win%':>6} {'avg_w':>10} {'avg_l':>10} {'ret':>8} {'maxDD':>7} {'sharpe':>7}")
|
||||||
|
|
||||||
|
sum_ret = 0.0
|
||||||
|
sum_trades = 0
|
||||||
|
sum_wins = 0
|
||||||
|
for year_label, start, end in YEARS:
|
||||||
|
mask = (ohlcv.index >= datetime.fromisoformat(start)) & (ohlcv.index < datetime.fromisoformat(end))
|
||||||
|
slice_df = ohlcv[mask]
|
||||||
|
if len(slice_df) == 0:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
signal_fn = compile_strategy(strat)
|
||||||
|
signals = signal_fn(slice_df)
|
||||||
|
bt = engine.run(slice_df, signals)
|
||||||
|
except Exception as e:
|
||||||
|
print(f" {year_label:<6} ERROR: {e}")
|
||||||
|
continue
|
||||||
|
trades = bt.trades
|
||||||
|
n = len(trades)
|
||||||
|
wins = [t.net_pnl for t in trades if t.net_pnl > 0]
|
||||||
|
losses = [t.net_pnl for t in trades if t.net_pnl <= 0]
|
||||||
|
nw, nl = len(wins), len(losses)
|
||||||
|
wr = (nw / n * 100) if n else 0.0
|
||||||
|
aw = (sum(wins) / nw) if nw else 0.0
|
||||||
|
al = (sum(losses) / nl) if nl else 0.0
|
||||||
|
if n > 0:
|
||||||
|
notional = float(slice_df["close"].iloc[0])
|
||||||
|
eq = (bt.equity_curve / notional) + 1.0
|
||||||
|
ret = total_return(eq)
|
||||||
|
dd = max_drawdown(eq)
|
||||||
|
sr = sharpe_ratio(bt.returns, periods_per_year=8760)
|
||||||
|
else:
|
||||||
|
ret = dd = sr = 0.0
|
||||||
|
print(f" {year_label:<6} {len(slice_df):>6} {n:>7} {nw:>5} {nl:>7} {wr:>5.1f}% {aw:>10.1f} {al:>10.1f} {ret:>7.2%} {dd:>6.2%} {sr:>7.3f}")
|
||||||
|
sum_ret += ret
|
||||||
|
sum_trades += n
|
||||||
|
sum_wins += nw
|
||||||
|
overall_wr = (sum_wins / sum_trades * 100) if sum_trades else 0.0
|
||||||
|
print(f" {'='*5} TOTALS 7y: {sum_trades:>7} {sum_wins:>5} {sum_trades-sum_wins:>7} {overall_wr:>5.1f}% cum_ret={sum_ret:+.2%}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
# Decisione: indicatore `atr_pct` per fix bug protocollo unità
|
# Decisione: indicatori `*_pct` per fix bug protocollo unità
|
||||||
|
|
||||||
**Data:** 2026-05-15
|
**Data:** 2026-05-15
|
||||||
**Status:** Implementato
|
**Status:** Implementato (atr_pct + sma_pct + macd_pct + prompt LLM aggiornato)
|
||||||
**Scope:** `multi_swarm_core.protocol.{compiler,grammar,validator}` + `strategy_crypto/strategies/eth_*.json`
|
**Scope:** `multi_swarm_core.protocol.{compiler,grammar,validator}` + `agents/hypothesis.py` + `strategy_crypto/strategies/eth_*.json`
|
||||||
|
|
||||||
## Contesto
|
## Contesto
|
||||||
|
|
||||||
@@ -73,12 +73,25 @@ _ind_atr_pct(df, 14).mean() # ~0.011 (1.1% del prezzo)
|
|||||||
|
|
||||||
`atr > 0.02` → 500/500 (broken). `atr_pct > 0.02` → 0/500 (real signal).
|
`atr > 0.02` → 500/500 (broken). `atr_pct > 0.02` → 0/500 (real signal).
|
||||||
|
|
||||||
|
## Estensione: sma_pct + macd_pct (2026-05-15)
|
||||||
|
|
||||||
|
Aggiunti anche `sma_pct` e `macd_pct` per coerenza famiglia `*_pct`:
|
||||||
|
|
||||||
|
- **`sma_pct(length) = (close - sma) / sma`** — deviazione frazionale del
|
||||||
|
close dalla SMA. Range tipico ±0.1. NB: NON è `sma/close` (che sarebbe
|
||||||
|
sempre ~1.0, inutile per literal). Uso: `sma_pct(50) > 0.05` significa
|
||||||
|
"close 5% sopra la media a 50 barre" (mean reversion).
|
||||||
|
- **`macd_pct(fast, slow, signal) = macd / close`** — MACD come frazione
|
||||||
|
del prezzo. Range tipico ±0.02. Uso: `macd_pct > 0.005` significa
|
||||||
|
"momentum > 0.5% del prezzo".
|
||||||
|
|
||||||
|
Prompt LLM aggiornato di conseguenza (`agents/hypothesis.py`):
|
||||||
|
- Lista indicatori include `sma_pct` e `macd_pct` con annotazioni unità
|
||||||
|
- Pattern guidance: "Mean reversion strutturale: sma_pct(long) > 0.05",
|
||||||
|
"Momentum positivo conferma: macd_pct(12,26,9) > 0.005"
|
||||||
|
- 3 nuovi test (sma_pct identity, macd_pct identity, validator integration)
|
||||||
|
|
||||||
## Open items
|
## Open items
|
||||||
|
|
||||||
- ~~Aggiornare il system prompt LLM~~ ✅ **chiuso 2026-05-15**:
|
- ~~Aggiornare il system prompt LLM~~ ✅ chiuso 2026-05-15
|
||||||
`agents/hypothesis.py` ora elenca `atr_pct` con annotazione unità e
|
- ~~`sma_pct`, `macd_pct` se emergono usi analoghi~~ ✅ chiuso 2026-05-15
|
||||||
include una sezione "UNITÀ — REGOLA CRITICA" che spiega esplicitamente
|
|
||||||
al modello quando usare `atr_pct` (literal frazionali) vs `atr`
|
|
||||||
(confronti relativi). `mutation_prompt_llm._VALID_KEYWORDS` esteso.
|
|
||||||
- Considerare l'aggiunta di `sma_pct`, `macd_pct` se emergono usi
|
|
||||||
analoghi in future strategie (still open).
|
|
||||||
|
|||||||
@@ -172,10 +172,11 @@ class AdversarialAgent:
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
# Fees-eat-alpha: gross_pnl > 0 ma fees > 50% del lordo.
|
# Fees-eat-alpha: gross_pnl > 0 ma fees > soglia del lordo.
|
||||||
# La strategia ha edge teorico ma il margine viene mangiato dai
|
# La strategia ha edge teorico ma il margine viene mangiato dai
|
||||||
# costi di transazione: non sostenibile in produzione.
|
# costi di transazione: non sostenibile in produzione.
|
||||||
# Se gross_pnl <= 0 il check non si applica (gia' perdente).
|
# Se gross_pnl <= 0 il check non si applica (la condizione e' coperta
|
||||||
|
# da ``negative_net_pnl`` sotto).
|
||||||
gross_pnl = sum(t.gross_pnl for t in result.trades)
|
gross_pnl = sum(t.gross_pnl for t in result.trades)
|
||||||
total_fees = sum(t.fees for t in result.trades)
|
total_fees = sum(t.fees for t in result.trades)
|
||||||
if gross_pnl > 0 and total_fees / gross_pnl > self._fees_eat_alpha_threshold:
|
if gross_pnl > 0 and total_fees / gross_pnl > self._fees_eat_alpha_threshold:
|
||||||
@@ -190,4 +191,22 @@ class AdversarialAgent:
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Negative-net-pnl: somma di ``trade.net_pnl`` < 0 sul training.
|
||||||
|
# Cattura sia il caso "gross negativo" (no edge direzionale) sia il
|
||||||
|
# caso "gross positivo ma fees superiori a gross" (edge insufficiente).
|
||||||
|
# Sintesi del net-after-fees su finestra continua: deal-breaker, non
|
||||||
|
# negoziabile via soft penalty.
|
||||||
|
net_pnl = gross_pnl - total_fees
|
||||||
|
if net_pnl < 0:
|
||||||
|
report.findings.append(
|
||||||
|
Finding(
|
||||||
|
name="negative_net_pnl",
|
||||||
|
severity=Severity.HIGH,
|
||||||
|
detail=(
|
||||||
|
f"Net PnL ${net_pnl:.2f} < 0 after fees over {n_bars} bars; "
|
||||||
|
f"gross ${gross_pnl:.2f}, fees ${total_fees:.2f}"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
return report
|
return report
|
||||||
|
|||||||
@@ -2,10 +2,12 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import re
|
import re
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
import openai
|
import openai
|
||||||
|
|
||||||
from ..genome.hypothesis import HypothesisAgentGenome
|
from ..genome.hypothesis import HypothesisAgentGenome
|
||||||
|
from ..genome.prompt_library import PromptLibrary
|
||||||
from ..llm.client import CompletionResult, EmptyCompletionError, LLMClient
|
from ..llm.client import CompletionResult, EmptyCompletionError, LLMClient
|
||||||
from ..protocol.parser import ParseError, Strategy, parse_strategy
|
from ..protocol.parser import ParseError, Strategy, parse_strategy
|
||||||
from ..protocol.validator import ValidationError, validate_strategy
|
from ..protocol.validator import ValidationError, validate_strategy
|
||||||
@@ -21,6 +23,19 @@ class MarketSummary:
|
|||||||
skew: float
|
skew: float
|
||||||
kurtosis: float
|
kurtosis: float
|
||||||
volatility_regime: str
|
volatility_regime: str
|
||||||
|
autocorr_lag1_recent: float = 0.0 # AR(1) ultimi 500 bar
|
||||||
|
autocorr_lag1_baseline: float = 0.0 # AR(1) full sample (riferimento)
|
||||||
|
hurst_recent: float = 0.5 # Hurst ultimi 500 bar
|
||||||
|
vol_percentile: float = 50.0 # 0-100 percentile della vol corrente
|
||||||
|
seasonality_hour: float = 0.0 # 0-1 varianza spiegata da hour
|
||||||
|
seasonality_dow: float = 0.0 # 0-1 varianza spiegata da dow
|
||||||
|
efficiency_ratio: float = 0.0 # Kaufman, 0-1
|
||||||
|
tail_index_left: float = 5.0 # Hill left tail
|
||||||
|
tail_index_right: float = 5.0 # Hill right tail
|
||||||
|
structural_uptrend: float = 0.5 # HH/HL score 0-1
|
||||||
|
compression: float = 1.0 # range recent / range ref
|
||||||
|
spectral_entropy: float = 1.0 # 0-1, Shannon FFT normalizzata
|
||||||
|
dominant_cycle: float | None = None # periodo barre, None se spectrum piatto
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
@@ -41,12 +56,9 @@ class HypothesisProposal:
|
|||||||
n_attempts: int = 1
|
n_attempts: int = 1
|
||||||
|
|
||||||
|
|
||||||
SYSTEM_TEMPLATE = """\
|
# === CORE SCAFFOLD constants (universal, legato al protocol/compiler) ===
|
||||||
Sei un agente generatore di ipotesi di trading quantitativo per un sistema swarm.
|
|
||||||
|
|
||||||
Il tuo stile cognitivo: {cognitive_style}
|
|
||||||
Direttiva personale: {system_prompt}
|
|
||||||
|
|
||||||
|
_SYSTEM_GRAMMAR_SPEC = """\
|
||||||
Devi proporre una strategia di trading espressa in JSON STRETTO.
|
Devi proporre una strategia di trading espressa in JSON STRETTO.
|
||||||
La risposta deve essere un singolo oggetto JSON dentro fence ```json...```
|
La risposta deve essere un singolo oggetto JSON dentro fence ```json...```
|
||||||
con questa shape:
|
con questa shape:
|
||||||
@@ -77,20 +89,29 @@ Crossover (eventi su 2 serie):
|
|||||||
|
|
||||||
Leaf - indicatori (calcolati su close):
|
Leaf - indicatori (calcolati su close):
|
||||||
{{"kind": "indicator", "name": "sma", "params": [<length>]}} // media mobile, UNITÀ PREZZO
|
{{"kind": "indicator", "name": "sma", "params": [<length>]}} // media mobile, UNITÀ PREZZO
|
||||||
|
{{"kind": "indicator", "name": "sma_pct", "params": [<length>]}} // (close-sma)/sma, FRAZIONE ±0.1
|
||||||
{{"kind": "indicator", "name": "rsi", "params": [<length>]}} // 0-100, adimensionale
|
{{"kind": "indicator", "name": "rsi", "params": [<length>]}} // 0-100, adimensionale
|
||||||
{{"kind": "indicator", "name": "atr", "params": [<length>]}} // true range, UNITÀ PREZZO
|
{{"kind": "indicator", "name": "atr", "params": [<length>]}} // true range, UNITÀ PREZZO
|
||||||
{{"kind": "indicator", "name": "atr_pct", "params": [<length>]}} // atr/close, FRAZIONE 0.0-0.1
|
{{"kind": "indicator", "name": "atr_pct", "params": [<length>]}} // atr/close, FRAZIONE 0.0-0.1
|
||||||
{{"kind": "indicator", "name": "realized_vol", "params": [<window>]}} // std dei returns, FRAZIONE
|
{{"kind": "indicator", "name": "realized_vol", "params": [<window>]}} // std dei returns, FRAZIONE
|
||||||
{{"kind": "indicator", "name": "macd", "params": [<fast>, <slow>, <signal>]}}
|
{{"kind": "indicator", "name": "macd", "params": [<fast>, <slow>, <signal>]}} // UNITÀ PREZZO
|
||||||
// 0-3 numeri (tutti opzionali con default 12, 26, 9)
|
{{"kind": "indicator", "name": "macd_pct", "params": [<fast>, <slow>, <signal>]}} // macd/close, FRAZIONE ±0.02
|
||||||
|
// params: 0-3 numeri (tutti opzionali, default 12, 26, 9)
|
||||||
|
|
||||||
UNITÀ — REGOLA CRITICA per i confronti con literal numerici:
|
UNITÀ — REGOLA CRITICA per i confronti con literal numerici:
|
||||||
* Confronti con literal FRAZIONALI (0.01, 0.02, 0.05): usa `atr_pct`, `realized_vol`
|
* Confronti con literal FRAZIONALI (0.01, 0.02, 0.05): usa le varianti _pct
|
||||||
Esempio CORRETTO: `atr_pct(14) > 0.02` significa "ATR > 2% del prezzo"
|
Esempi CORRETTI:
|
||||||
Esempio ERRATO: `atr(14) > 0.02` è sempre TRUE su asset $>1 (atr in dollari)
|
`atr_pct(14) > 0.02` "ATR > 2% del prezzo" (volatilità alta)
|
||||||
* Confronti RELATIVI fra indicatori in stessa unità: usa `atr`, `sma`, `macd`
|
`sma_pct(50) > 0.05` "close 5% sopra SMA(50)" (deviazione media)
|
||||||
Esempio: `atr(14) > sma(14)` (entrambi in dollari, confronto valido)
|
`macd_pct(12,26,9) > 0.005` "momentum > 0.5% del prezzo"
|
||||||
* RSI usa literal 0-100 (mai frazione): `rsi(14) > 70`
|
Esempi ERRATI (sempre TRUE/FALSE su crypto, dead branch):
|
||||||
|
`atr(14) > 0.02` atr in dollari (~30 su ETH) >> 0.02
|
||||||
|
`sma(50) > 0.02` sma in dollari (~3000) >> 0.02
|
||||||
|
`macd > 0.02` macd in dollari, ordine ±10
|
||||||
|
* Confronti RELATIVI fra indicatori in stessa unità: usa nomi senza _pct
|
||||||
|
Esempi: `atr(14) > sma(14)` (entrambi $), `sma(50) > sma(200)` (golden cross)
|
||||||
|
`close > sma(50)` (entrambi $) — preferito su `sma_pct(50) > 0` (equivalente)
|
||||||
|
* RSI usa literal 0-100 (mai frazione): `rsi(14) > 70`, `rsi(14) < 30`
|
||||||
|
|
||||||
Leaf - feature OHLCV:
|
Leaf - feature OHLCV:
|
||||||
{{"kind": "feature", "name": "open|high|low|close|volume"}}
|
{{"kind": "feature", "name": "open|high|low|close|volume"}}
|
||||||
@@ -112,34 +133,18 @@ Esempi di gating temporale:
|
|||||||
{{"op": "eq", "args": [{{"kind": "feature", "name": "is_weekend"}}, {{"kind": "literal", "value": 1}}]}}
|
{{"op": "eq", "args": [{{"kind": "feature", "name": "is_weekend"}}, {{"kind": "literal", "value": 1}}]}}
|
||||||
|
|
||||||
Leaf - letterale numerico:
|
Leaf - letterale numerico:
|
||||||
{{"kind": "literal", "value": 70.0}}
|
{{"kind": "literal", "value": 70.0}}"""
|
||||||
|
|
||||||
PATTERN GUIDANCE (oltre agli indicatori, considera forma delle curve e ripetibilità):
|
|
||||||
|
|
||||||
Forme di curva:
|
|
||||||
- Trend ascendente: SMA(short) > SMA(long) E close > SMA(short)
|
|
||||||
- Trend discendente: SMA(short) < SMA(long) E close < SMA(short)
|
|
||||||
- Compressione di volatilità (pre-breakout): atr_pct(N) < 0.01 (sotto 1% del prezzo)
|
|
||||||
- Espansione di volatilità: atr_pct(N) > 0.03 (sopra 3%) OPPURE ATR(N) > ATR(N*2) confronto relativo
|
|
||||||
- Mean reversion strutturale: |close - SMA(long)| eccessivo → reversal atteso
|
|
||||||
|
|
||||||
Ripetibilità dell'andamento:
|
|
||||||
- Eventi crossover/crossunder ricorrenti (golden/death cross, RSI cross zone)
|
|
||||||
- Pattern intra-day: usa 'hour' per sfruttare orari di forte volatilità ricorrente
|
|
||||||
- Pattern settimanali: usa 'dow' o 'is_weekend' per cicli mercato
|
|
||||||
- Doppio top approx: RSI > 70 + crossunder RSI 70 (1° picco), poi entro N bar
|
|
||||||
nuovo crossover RSI 70 a livello close simile → entry short
|
|
||||||
- Range breakout: close > SMA(N) con SMA(short) > SMA(long) (compressione + spinta)
|
|
||||||
|
|
||||||
Cerca pattern che si REPLICANO nei dati storici, non singoli eventi rari.
|
|
||||||
|
|
||||||
|
_SYSTEM_CONSTRAINTS = """\
|
||||||
VINCOLI
|
VINCOLI
|
||||||
- Gli indicator NON sono annidabili: 'params' accetta solo numeri, mai altri nodi.
|
- Gli indicator NON sono annidabili: 'params' accetta solo numeri, mai altri nodi.
|
||||||
- Le regole sono valutate in ordine; la prima che matcha vince per ogni timestamp.
|
- Le regole sono valutate in ordine; la prima che matcha vince per ogni timestamp.
|
||||||
- Default action se nessuna regola matcha = flat.
|
- Default action se nessuna regola matcha = flat.
|
||||||
- 'op' e 'kind' sono mutuamente esclusivi sullo stesso nodo.
|
- 'op' e 'kind' sono mutuamente esclusivi sullo stesso nodo.
|
||||||
|
|
||||||
Rispondi SOLO con il fence ```json...``` contenente l'oggetto strategy.
|
Rispondi SOLO con il fence ```json...``` contenente l'oggetto strategy."""
|
||||||
|
|
||||||
|
_SYSTEM_EXAMPLE = """\
|
||||||
Esempio:
|
Esempio:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
@@ -161,8 +166,49 @@ Esempio:
|
|||||||
}}
|
}}
|
||||||
]
|
]
|
||||||
}}
|
}}
|
||||||
```
|
```"""
|
||||||
"""
|
|
||||||
|
|
||||||
|
def _build_system_prompt(lib: PromptLibrary, genome: HypothesisAgentGenome) -> str:
|
||||||
|
"""Compone il SYSTEM prompt da scaffold core + contenuto strategy-specific."""
|
||||||
|
parts: list[str] = []
|
||||||
|
# 1. Header strategy-specific (da prompts.json)
|
||||||
|
parts.append(lib.agent_role)
|
||||||
|
parts.append("")
|
||||||
|
# 2. Cognitive style + directive (sempre, dal genome)
|
||||||
|
parts.append(f"Il tuo stile cognitivo: {genome.cognitive_style}")
|
||||||
|
parts.append(f"Direttiva personale: {genome.system_prompt}")
|
||||||
|
parts.append("")
|
||||||
|
# 3. Grammar spec (core scaffold)
|
||||||
|
parts.append(_SYSTEM_GRAMMAR_SPEC)
|
||||||
|
# 4. Pattern guidance (da prompts.json, opzionale)
|
||||||
|
if lib.pattern_guidance:
|
||||||
|
parts.append(
|
||||||
|
"\nPATTERN GUIDANCE (oltre agli indicatori, considera forma delle curve e ripetibilità):\n"
|
||||||
|
)
|
||||||
|
parts.append(lib.pattern_guidance)
|
||||||
|
parts.append(
|
||||||
|
"\n Cerca pattern che si REPLICANO nei dati storici, non singoli eventi rari.\n"
|
||||||
|
)
|
||||||
|
# 5. Domain warnings (da prompts.json, opzionale)
|
||||||
|
if lib.domain_warnings:
|
||||||
|
parts.append("\nWARNING DI DOMINIO:\n")
|
||||||
|
parts.append(lib.domain_warnings)
|
||||||
|
parts.append("")
|
||||||
|
# 6. Vincoli (core scaffold)
|
||||||
|
parts.append(_SYSTEM_CONSTRAINTS)
|
||||||
|
# 7. NEW v3.1: anti-pattern e output priorities (da prompts.json, opzionali)
|
||||||
|
if lib.anti_patterns:
|
||||||
|
parts.append("\nANTI-PATTERN DA EVITARE:\n")
|
||||||
|
parts.append(lib.anti_patterns)
|
||||||
|
parts.append("")
|
||||||
|
if lib.output_priorities:
|
||||||
|
parts.append("\nPRIORITA' DI OUTPUT (trade-off):\n")
|
||||||
|
parts.append(lib.output_priorities)
|
||||||
|
parts.append("")
|
||||||
|
# 8. Esempio (core scaffold)
|
||||||
|
parts.append(_SYSTEM_EXAMPLE)
|
||||||
|
return "\n".join(parts)
|
||||||
|
|
||||||
|
|
||||||
USER_TEMPLATE = """\
|
USER_TEMPLATE = """\
|
||||||
@@ -171,13 +217,72 @@ Statistiche return: mean={return_mean:.5f}, std={return_std:.5f}, \
|
|||||||
skew={skew:.3f}, kurt={kurtosis:.3f}.
|
skew={skew:.3f}, kurt={kurtosis:.3f}.
|
||||||
Regime volatilità: {volatility_regime}.
|
Regime volatilità: {volatility_regime}.
|
||||||
|
|
||||||
|
Regime recente (ultime 500 barre):
|
||||||
|
autocorr_lag1: {autocorr_lag1_recent:.3f} (baseline storica: {autocorr_lag1_baseline:.3f})
|
||||||
|
hurst: {hurst_recent:.3f} (0.5 = random walk, >0.55 trending, <0.45 mean rev)
|
||||||
|
vol_pct: {vol_percentile:.0f}° percentile storico
|
||||||
|
stagionalita: hour={seasonality_hour:.2f}, dow={seasonality_dow:.2f} (0-1, varianza spiegata)
|
||||||
|
|
||||||
|
Geometria & frattali:
|
||||||
|
efficiency_ratio: {efficiency_ratio:.3f} (Kaufman, 0=noise, 1=trend efficiente)
|
||||||
|
tail_index: left={tail_index_left:.2f}, right={tail_index_right:.2f} (Hill; <2 fat tail, >5 light)
|
||||||
|
structural_uptrend: {structural_uptrend:.2f} (HH/HL score, 0.5=range)
|
||||||
|
compression: {compression:.2f} (range recent / ref; <1 compressione, >1 espansione)
|
||||||
|
spectral_entropy: {spectral_entropy:.2f} (0=struttura, 1=rumore bianco)
|
||||||
|
dominant_cycle: {dominant_cycle_str} (None se spettro piatto)
|
||||||
|
|
||||||
Feature accessibili dal tuo genoma: {feature_access}.
|
Feature accessibili dal tuo genoma: {feature_access}.
|
||||||
Lookback massimo che puoi usare nel ragionamento: {lookback_window} barre.
|
Lookback massimo che puoi usare nel ragionamento: {lookback_window} barre.
|
||||||
|
|
||||||
Genera una strategia che cerchi anomalie sfruttabili in questo regime.
|
{instruction}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def _render_focus_block(keys: list[str], market: MarketSummary) -> str:
|
||||||
|
"""Renderizza 'Focus per la tua lente' come riga del USER_TEMPLATE.
|
||||||
|
|
||||||
|
Mappa nomi simbolici a valori della MarketSummary. Skippa silenziosamente
|
||||||
|
chiavi sconosciute (fault-tolerant per evoluzione futura).
|
||||||
|
"""
|
||||||
|
field_map: dict[str, Any] = {
|
||||||
|
# Statistiche base
|
||||||
|
"mean": market.return_mean,
|
||||||
|
"std": market.return_std,
|
||||||
|
"skew": market.skew,
|
||||||
|
"kurt": market.kurtosis,
|
||||||
|
"vol_regime": market.volatility_regime,
|
||||||
|
# Regime recente
|
||||||
|
"autocorr_recent": market.autocorr_lag1_recent,
|
||||||
|
"autocorr_baseline": market.autocorr_lag1_baseline,
|
||||||
|
"hurst": market.hurst_recent,
|
||||||
|
"vol_pct": market.vol_percentile,
|
||||||
|
"seasonality_hour": market.seasonality_hour,
|
||||||
|
"seasonality_dow": market.seasonality_dow,
|
||||||
|
# Geometria/frattali
|
||||||
|
"efficiency_ratio": market.efficiency_ratio,
|
||||||
|
"tail_left": market.tail_index_left,
|
||||||
|
"tail_right": market.tail_index_right,
|
||||||
|
"structural_uptrend": market.structural_uptrend,
|
||||||
|
"compression": market.compression,
|
||||||
|
"spectral_entropy": market.spectral_entropy,
|
||||||
|
"dominant_cycle": market.dominant_cycle,
|
||||||
|
}
|
||||||
|
parts: list[str] = []
|
||||||
|
for k in keys:
|
||||||
|
if k not in field_map:
|
||||||
|
continue
|
||||||
|
v = field_map[k]
|
||||||
|
if v is None:
|
||||||
|
parts.append(f"{k}=N/A")
|
||||||
|
elif isinstance(v, float):
|
||||||
|
parts.append(f"{k}={v:.3f}")
|
||||||
|
else:
|
||||||
|
parts.append(f"{k}={v}")
|
||||||
|
if not parts:
|
||||||
|
return ""
|
||||||
|
return "\nFocus per la tua lente: " + ", ".join(parts) + "\n"
|
||||||
|
|
||||||
|
|
||||||
_RETRY_TEMPLATE = """\
|
_RETRY_TEMPLATE = """\
|
||||||
{original_user}
|
{original_user}
|
||||||
|
|
||||||
@@ -260,20 +365,34 @@ def _try_parse(text: str) -> tuple[Strategy | None, str | None]:
|
|||||||
|
|
||||||
|
|
||||||
class HypothesisAgent:
|
class HypothesisAgent:
|
||||||
def __init__(self, llm: LLMClient, max_retries: int = 1):
|
def __init__(
|
||||||
|
self,
|
||||||
|
llm: LLMClient,
|
||||||
|
max_retries: int = 1,
|
||||||
|
prompt_library: PromptLibrary | None = None,
|
||||||
|
):
|
||||||
if max_retries < 0:
|
if max_retries < 0:
|
||||||
raise ValueError("max_retries must be >= 0")
|
raise ValueError("max_retries must be >= 0")
|
||||||
self._llm = llm
|
self._llm = llm
|
||||||
self._max_retries = max_retries
|
self._max_retries = max_retries
|
||||||
|
self._prompt_library = prompt_library or PromptLibrary.default()
|
||||||
|
|
||||||
def propose(
|
def propose(
|
||||||
self,
|
self,
|
||||||
genome: HypothesisAgentGenome,
|
genome: HypothesisAgentGenome,
|
||||||
market: MarketSummary,
|
market: MarketSummary,
|
||||||
) -> HypothesisProposal:
|
) -> HypothesisProposal:
|
||||||
system = SYSTEM_TEMPLATE.format(
|
system = _build_system_prompt(self._prompt_library, genome)
|
||||||
cognitive_style=genome.cognitive_style,
|
dominant_cycle_str = (
|
||||||
system_prompt=genome.system_prompt,
|
f"{market.dominant_cycle:.0f} barre"
|
||||||
|
if market.dominant_cycle is not None
|
||||||
|
else "N/A (spettro piatto)"
|
||||||
|
)
|
||||||
|
focus_keys = self._prompt_library.focus_metrics_for(genome.cognitive_style)
|
||||||
|
focus_block = _render_focus_block(focus_keys, market) if focus_keys else ""
|
||||||
|
instruction = (
|
||||||
|
self._prompt_library.instruction
|
||||||
|
or "Genera una strategia che cerchi anomalie sfruttabili in questo regime."
|
||||||
)
|
)
|
||||||
original_user = USER_TEMPLATE.format(
|
original_user = USER_TEMPLATE.format(
|
||||||
symbol=market.symbol,
|
symbol=market.symbol,
|
||||||
@@ -284,9 +403,23 @@ class HypothesisAgent:
|
|||||||
skew=market.skew,
|
skew=market.skew,
|
||||||
kurtosis=market.kurtosis,
|
kurtosis=market.kurtosis,
|
||||||
volatility_regime=market.volatility_regime,
|
volatility_regime=market.volatility_regime,
|
||||||
|
autocorr_lag1_recent=market.autocorr_lag1_recent,
|
||||||
|
autocorr_lag1_baseline=market.autocorr_lag1_baseline,
|
||||||
|
hurst_recent=market.hurst_recent,
|
||||||
|
vol_percentile=market.vol_percentile,
|
||||||
|
seasonality_hour=market.seasonality_hour,
|
||||||
|
seasonality_dow=market.seasonality_dow,
|
||||||
|
efficiency_ratio=market.efficiency_ratio,
|
||||||
|
tail_index_left=market.tail_index_left,
|
||||||
|
tail_index_right=market.tail_index_right,
|
||||||
|
structural_uptrend=market.structural_uptrend,
|
||||||
|
compression=market.compression,
|
||||||
|
spectral_entropy=market.spectral_entropy,
|
||||||
|
dominant_cycle_str=dominant_cycle_str,
|
||||||
feature_access=", ".join(genome.feature_access),
|
feature_access=", ".join(genome.feature_access),
|
||||||
lookback_window=genome.lookback_window,
|
lookback_window=genome.lookback_window,
|
||||||
)
|
instruction=instruction,
|
||||||
|
) + focus_block
|
||||||
|
|
||||||
completions: list[CompletionResult] = []
|
completions: list[CompletionResult] = []
|
||||||
errors: list[str] = []
|
errors: list[str] = []
|
||||||
|
|||||||
@@ -3,6 +3,17 @@ from __future__ import annotations
|
|||||||
import pandas as pd # type: ignore[import-untyped]
|
import pandas as pd # type: ignore[import-untyped]
|
||||||
from scipy import stats # type: ignore[import-untyped]
|
from scipy import stats # type: ignore[import-untyped]
|
||||||
|
|
||||||
|
from ..metrics.basic import (
|
||||||
|
autocorr_lag1,
|
||||||
|
compression_ratio,
|
||||||
|
efficiency_ratio_kaufman,
|
||||||
|
hurst_exponent,
|
||||||
|
seasonality_strength,
|
||||||
|
spectral_entropy_and_cycle,
|
||||||
|
structural_uptrend_score,
|
||||||
|
tail_index_hill,
|
||||||
|
vol_percentile_historical,
|
||||||
|
)
|
||||||
from .hypothesis import MarketSummary
|
from .hypothesis import MarketSummary
|
||||||
|
|
||||||
|
|
||||||
@@ -24,6 +35,23 @@ def build_market_summary(
|
|||||||
else:
|
else:
|
||||||
regime = "high"
|
regime = "high"
|
||||||
|
|
||||||
|
recent_window = min(500, len(returns))
|
||||||
|
recent_returns = returns.iloc[-recent_window:]
|
||||||
|
|
||||||
|
autocorr_recent = autocorr_lag1(recent_returns)
|
||||||
|
autocorr_baseline = autocorr_lag1(returns)
|
||||||
|
hurst_r = hurst_exponent(recent_returns)
|
||||||
|
vol_pct = vol_percentile_historical(returns, current_window=24, ref_window=2000)
|
||||||
|
season_h = seasonality_strength(returns, by="hour")
|
||||||
|
season_d = seasonality_strength(returns, by="dow")
|
||||||
|
|
||||||
|
eff_ratio = efficiency_ratio_kaufman(ohlcv["close"], length=100)
|
||||||
|
tail_l = tail_index_hill(returns, side="left")
|
||||||
|
tail_r = tail_index_hill(returns, side="right")
|
||||||
|
hh_hl = structural_uptrend_score(ohlcv["close"], window=5)
|
||||||
|
compress = compression_ratio(ohlcv["close"], recent_window=50, ref_window=200)
|
||||||
|
spec_entropy, dom_cycle = spectral_entropy_and_cycle(returns, length=256)
|
||||||
|
|
||||||
return MarketSummary(
|
return MarketSummary(
|
||||||
symbol=symbol,
|
symbol=symbol,
|
||||||
timeframe=timeframe,
|
timeframe=timeframe,
|
||||||
@@ -33,4 +61,17 @@ def build_market_summary(
|
|||||||
skew=skew,
|
skew=skew,
|
||||||
kurtosis=kurt,
|
kurtosis=kurt,
|
||||||
volatility_regime=regime,
|
volatility_regime=regime,
|
||||||
|
autocorr_lag1_recent=autocorr_recent,
|
||||||
|
autocorr_lag1_baseline=autocorr_baseline,
|
||||||
|
hurst_recent=hurst_r,
|
||||||
|
vol_percentile=vol_pct,
|
||||||
|
seasonality_hour=season_h,
|
||||||
|
seasonality_dow=season_d,
|
||||||
|
efficiency_ratio=eff_ratio,
|
||||||
|
tail_index_left=tail_l,
|
||||||
|
tail_index_right=tail_r,
|
||||||
|
structural_uptrend=hh_hl,
|
||||||
|
compression=compress,
|
||||||
|
spectral_entropy=spec_entropy,
|
||||||
|
dominant_cycle=dom_cycle,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
import pandas as pd # type: ignore[import-untyped]
|
import pandas as pd # type: ignore[import-untyped]
|
||||||
|
|
||||||
from .orders import Position, Side, Trade
|
from .orders import Position, Side, Trade
|
||||||
@@ -28,74 +29,110 @@ class BacktestEngine:
|
|||||||
self.fees_bp = fees_bp
|
self.fees_bp = fees_bp
|
||||||
|
|
||||||
def run(self, ohlcv: pd.DataFrame, signals: pd.Series) -> BacktestResult:
|
def run(self, ohlcv: pd.DataFrame, signals: pd.Series) -> BacktestResult:
|
||||||
|
n = len(ohlcv)
|
||||||
|
if n == 0:
|
||||||
|
empty = pd.Series([], dtype=float)
|
||||||
|
return BacktestResult(equity_curve=empty, returns=empty, trades=[])
|
||||||
|
|
||||||
signals = signals.reindex(ohlcv.index).ffill().fillna(Side.FLAT)
|
signals = signals.reindex(ohlcv.index).ffill().fillna(Side.FLAT)
|
||||||
|
|
||||||
# Esecuzione con delay 1: segnale a t-1 esegue a open di t.
|
# Esecuzione con delay 1: segnale a t-1 esegue a open di t.
|
||||||
shifted = [Side.FLAT, *list(signals.iloc[:-1])]
|
executed = pd.Series(
|
||||||
executed_side = pd.Series(shifted, index=ohlcv.index, dtype=object)
|
[Side.FLAT, *list(signals.iloc[:-1])],
|
||||||
|
index=ohlcv.index,
|
||||||
|
dtype=object,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Codifica side in int per vectorizzazione: 0=FLAT, +1=LONG, -1=SHORT.
|
||||||
|
side_code = np.where(
|
||||||
|
executed.values == Side.LONG, 1,
|
||||||
|
np.where(executed.values == Side.SHORT, -1, 0),
|
||||||
|
).astype(np.int8)
|
||||||
|
opens = ohlcv["open"].to_numpy(dtype=np.float64)
|
||||||
|
closes = ohlcv["close"].to_numpy(dtype=np.float64)
|
||||||
|
ts_index = ohlcv.index
|
||||||
|
|
||||||
|
# Identifica transizioni: punto in cui side[i] != side[i-1] (con side[-1]=0).
|
||||||
|
prev = np.concatenate(([0], side_code[:-1]))
|
||||||
|
change = side_code != prev
|
||||||
|
|
||||||
|
# Indici di entry (cambio verso side != 0).
|
||||||
|
entry_idxs = np.flatnonzero(change & (side_code != 0))
|
||||||
|
# Indici di chiusura: per ogni entry, il prossimo indice dove side[i] != side_entry.
|
||||||
|
# Vectorized: per ogni entry_idx, cerca change & side != side_entry oltre l'entry.
|
||||||
|
|
||||||
position: Position | None = None
|
|
||||||
position_entry_ts: pd.Timestamp | None = None
|
|
||||||
trades: list[Trade] = []
|
trades: list[Trade] = []
|
||||||
equity = 0.0
|
# realized_pnl[t]: PnL netto cumulato dopo le chiusure avvenute a OPEN di t.
|
||||||
equity_history: list[float] = []
|
realized_pnl = np.zeros(n, dtype=np.float64)
|
||||||
returns_history: list[float] = []
|
fees_rate = self.fees_bp / 10000.0
|
||||||
prev_equity = 0.0
|
size = 1.0
|
||||||
|
|
||||||
for ts, row in ohlcv.iterrows():
|
# Posizione corrente all'inizio di ogni indice t (prima di applicare il transitorio):
|
||||||
target_side = executed_side.loc[ts]
|
# used per MtM computation. open_side_at_t / open_entry_at_t.
|
||||||
current_side = position.side if position else Side.FLAT
|
open_side = np.zeros(n, dtype=np.int8)
|
||||||
|
open_entry = np.zeros(n, dtype=np.float64)
|
||||||
|
|
||||||
if target_side != current_side:
|
for entry_idx in entry_idxs:
|
||||||
if position is not None:
|
entry_side = int(side_code[entry_idx])
|
||||||
assert position_entry_ts is not None
|
entry_price = opens[entry_idx]
|
||||||
trade = Trade(
|
# Cerca exit: primo indice > entry_idx dove side differisce.
|
||||||
entry_ts=position_entry_ts,
|
after = side_code[entry_idx + 1:]
|
||||||
exit_ts=ts,
|
rel = np.flatnonzero(after != entry_side)
|
||||||
side=position.side,
|
if rel.size > 0:
|
||||||
size=position.size,
|
exit_idx = entry_idx + 1 + int(rel[0])
|
||||||
entry_price=position.entry_price,
|
exit_price = opens[exit_idx]
|
||||||
exit_price=float(row["open"]),
|
exit_ts = ts_index[exit_idx]
|
||||||
fees_bp=self.fees_bp,
|
gross = entry_side * (exit_price - entry_price) * size
|
||||||
)
|
fees = fees_rate * size * (entry_price + exit_price)
|
||||||
trades.append(trade)
|
net = gross - fees
|
||||||
equity += trade.net_pnl
|
# La chiusura avviene a open[exit_idx]: dal bar exit_idx in poi il
|
||||||
position = None
|
# PnL e' realizzato (non piu' MtM).
|
||||||
position_entry_ts = None
|
realized_pnl[exit_idx:] += net
|
||||||
if target_side in (Side.LONG, Side.SHORT):
|
# Posizione aperta in [entry_idx, exit_idx-1].
|
||||||
position = Position(
|
open_side[entry_idx:exit_idx] = entry_side
|
||||||
side=target_side, entry_price=float(row["open"]), size=1.0
|
open_entry[entry_idx:exit_idx] = entry_price
|
||||||
)
|
trades.append(Trade(
|
||||||
position_entry_ts = ts
|
entry_ts=ts_index[entry_idx],
|
||||||
|
exit_ts=exit_ts,
|
||||||
|
side=Side.LONG if entry_side == 1 else Side.SHORT,
|
||||||
|
size=size,
|
||||||
|
entry_price=entry_price,
|
||||||
|
exit_price=exit_price,
|
||||||
|
fees_bp=self.fees_bp,
|
||||||
|
))
|
||||||
|
else:
|
||||||
|
# Ultima posizione ancora aperta: chiusura forced a close[-1].
|
||||||
|
# Parita' col loop legacy: MtM su [entry_idx, n-1), realized totale
|
||||||
|
# SOLO al bar n-1 (legacy fa equity_history[-1] = equity).
|
||||||
|
last_close = closes[-1]
|
||||||
|
gross = entry_side * (last_close - entry_price) * size
|
||||||
|
fees = fees_rate * size * (entry_price + last_close)
|
||||||
|
net = gross - fees
|
||||||
|
if entry_idx < n - 1:
|
||||||
|
open_side[entry_idx:n - 1] = entry_side
|
||||||
|
open_entry[entry_idx:n - 1] = entry_price
|
||||||
|
realized_pnl[-1] += net
|
||||||
|
trades.append(Trade(
|
||||||
|
entry_ts=ts_index[entry_idx],
|
||||||
|
exit_ts=ts_index[-1],
|
||||||
|
side=Side.LONG if entry_side == 1 else Side.SHORT,
|
||||||
|
size=size,
|
||||||
|
entry_price=entry_price,
|
||||||
|
exit_price=last_close,
|
||||||
|
fees_bp=self.fees_bp,
|
||||||
|
))
|
||||||
|
|
||||||
mark = float(row["close"])
|
# MtM unrealized per ogni bar in cui c'e' una posizione aperta.
|
||||||
mtm = position.unrealized_pnl(mark) if position else 0.0
|
mtm = open_side.astype(np.float64) * (closes - open_entry) * size
|
||||||
current_equity = equity + mtm
|
equity_arr = realized_pnl + mtm
|
||||||
equity_history.append(current_equity)
|
# Returns = first diff dell'equity (col loop legacy il primo bar e' equity[0]-0).
|
||||||
returns_history.append(current_equity - prev_equity)
|
returns_arr = np.concatenate(([equity_arr[0]], np.diff(equity_arr)))
|
||||||
prev_equity = current_equity
|
|
||||||
|
|
||||||
if position is not None:
|
|
||||||
assert position_entry_ts is not None
|
|
||||||
last_ts = ohlcv.index[-1]
|
|
||||||
last_close = float(ohlcv["close"].iloc[-1])
|
|
||||||
trade = Trade(
|
|
||||||
entry_ts=position_entry_ts,
|
|
||||||
exit_ts=last_ts,
|
|
||||||
side=position.side,
|
|
||||||
size=position.size,
|
|
||||||
entry_price=position.entry_price,
|
|
||||||
exit_price=last_close,
|
|
||||||
fees_bp=self.fees_bp,
|
|
||||||
)
|
|
||||||
trades.append(trade)
|
|
||||||
equity += trade.net_pnl
|
|
||||||
equity_history[-1] = equity
|
|
||||||
if len(returns_history) >= 2:
|
|
||||||
returns_history[-1] = equity - equity_history[-2]
|
|
||||||
|
|
||||||
return BacktestResult(
|
return BacktestResult(
|
||||||
equity_curve=pd.Series(equity_history, index=ohlcv.index, name="equity"),
|
equity_curve=pd.Series(equity_arr, index=ts_index, name="equity"),
|
||||||
returns=pd.Series(returns_history, index=ohlcv.index, name="returns"),
|
returns=pd.Series(returns_arr, index=ts_index, name="returns"),
|
||||||
trades=trades,
|
trades=trades,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# Lo facade Position re-export e' tenuto per backward-compat con import legacy.
|
||||||
|
__all__ = ["BacktestEngine", "BacktestResult", "Position", "Side", "Signal", "Trade"]
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
"""GA data access functions for the core dashboard.
|
||||||
|
|
||||||
|
Reads exclusively from runs.db (GA tables).
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import pandas as pd # type: ignore[import-untyped]
|
||||||
|
|
||||||
|
from multi_swarm_core.persistence.repository import Repository
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"get_repo",
|
||||||
|
"list_runs_df",
|
||||||
|
"get_run_overview",
|
||||||
|
"generations_df",
|
||||||
|
"evaluations_df",
|
||||||
|
"genomes_df",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def get_repo(db_path: str | Path) -> Repository:
|
||||||
|
return Repository(db_path=db_path)
|
||||||
|
|
||||||
|
|
||||||
|
def list_runs_df(repo: Repository) -> pd.DataFrame:
|
||||||
|
return pd.DataFrame(repo.list_runs())
|
||||||
|
|
||||||
|
|
||||||
|
def get_run_overview(repo: Repository, run_id: str) -> dict[str, Any]:
|
||||||
|
run = repo.get_run(run_id)
|
||||||
|
return {
|
||||||
|
"name": run["name"],
|
||||||
|
"started_at": run["started_at"],
|
||||||
|
"completed_at": run["completed_at"],
|
||||||
|
"status": run["status"],
|
||||||
|
"total_cost_usd": run["total_cost_usd"],
|
||||||
|
"config": json.loads(run["config_json"]),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def generations_df(repo: Repository, run_id: str) -> pd.DataFrame:
|
||||||
|
return pd.DataFrame(repo.list_generations(run_id))
|
||||||
|
|
||||||
|
|
||||||
|
def evaluations_df(repo: Repository, run_id: str) -> pd.DataFrame:
|
||||||
|
return pd.DataFrame(repo.list_evaluations(run_id))
|
||||||
|
|
||||||
|
|
||||||
|
def genomes_df(
|
||||||
|
repo: Repository, run_id: str, generation_idx: int | None = None
|
||||||
|
) -> pd.DataFrame:
|
||||||
|
rows = repo.list_genomes(run_id, generation_idx)
|
||||||
|
flat: list[dict[str, Any]] = []
|
||||||
|
for r in rows:
|
||||||
|
payload = json.loads(r["payload_json"])
|
||||||
|
flat.append(
|
||||||
|
{
|
||||||
|
"id": r["id"],
|
||||||
|
"generation_idx": r["generation_idx"],
|
||||||
|
**payload,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return pd.DataFrame(flat)
|
||||||
@@ -0,0 +1,560 @@
|
|||||||
|
"""Multi-Swarm Core Dashboard — GA pages: /, /convergence, /genomes.
|
||||||
|
|
||||||
|
Avvio: ``uv run python -m multi_swarm_core.dashboard.nicegui_app``
|
||||||
|
Legge SOLO runs.db (tabelle GA).
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import html
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import plotly.graph_objects as go # type: ignore[import-untyped]
|
||||||
|
from nicegui import app, ui
|
||||||
|
|
||||||
|
from multi_swarm_core.dashboard.data import (
|
||||||
|
evaluations_df,
|
||||||
|
generations_df,
|
||||||
|
genomes_df,
|
||||||
|
get_repo,
|
||||||
|
get_run_overview,
|
||||||
|
list_runs_df,
|
||||||
|
)
|
||||||
|
from multi_swarm_core.dashboard.theme import (
|
||||||
|
COLOR_ACCENT,
|
||||||
|
COLOR_PRIMARY,
|
||||||
|
COLOR_SECONDARY,
|
||||||
|
COLOR_SURFACE,
|
||||||
|
COLOR_TEXT,
|
||||||
|
COLOR_TEXT_MUTED,
|
||||||
|
_STATUS_BADGE,
|
||||||
|
_apply_theme,
|
||||||
|
_build_header,
|
||||||
|
_json_to_html,
|
||||||
|
)
|
||||||
|
|
||||||
|
GA_DB_PATH = os.environ.get("GA_DB_PATH", "./state/runs.db")
|
||||||
|
DASHBOARD_ROOT_PATH = os.environ.get("DASHBOARD_ROOT_PATH", "")
|
||||||
|
REFRESH_INTERVAL_S = 3.0
|
||||||
|
|
||||||
|
|
||||||
|
def _runs_options() -> dict[str, str]:
|
||||||
|
repo = get_repo(GA_DB_PATH)
|
||||||
|
runs = list_runs_df(repo)
|
||||||
|
if runs.empty:
|
||||||
|
return {}
|
||||||
|
return {
|
||||||
|
row["id"]: f"{row['name']} — {row['status']} ({row['started_at'][:16]})"
|
||||||
|
for _, row in runs.iterrows()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _snapshot(run_id: str) -> dict[str, Any]:
|
||||||
|
repo = get_repo(GA_DB_PATH)
|
||||||
|
ov = get_run_overview(repo, run_id)
|
||||||
|
evals = evaluations_df(repo, run_id)
|
||||||
|
gens = generations_df(repo, run_id)
|
||||||
|
|
||||||
|
cfg = ov["config"]
|
||||||
|
pop_size = int(cfg.get("population_size", 0))
|
||||||
|
n_gens = int(cfg.get("n_generations", 0))
|
||||||
|
evals_total = max(pop_size * n_gens, 1)
|
||||||
|
evals_done = len(evals)
|
||||||
|
gens_done = int(gens["completed_at"].notna().sum()) if not gens.empty else 0
|
||||||
|
live_cost = float(repo.total_cost(run_id)) if ov["status"] == "running" else float(
|
||||||
|
ov["total_cost_usd"]
|
||||||
|
)
|
||||||
|
|
||||||
|
top_fit = float(evals["fitness"].max()) if evals_done else float("nan")
|
||||||
|
median_fit = float(evals["fitness"].median()) if evals_done else float("nan")
|
||||||
|
parse_success = (
|
||||||
|
100.0 * float(evals["parse_error"].isna().sum()) / evals_done if evals_done else 0.0
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"status": ov["status"],
|
||||||
|
"name": cfg.get("run_name", "—"),
|
||||||
|
"started_at": ov["started_at"],
|
||||||
|
"completed_at": ov["completed_at"] or "—",
|
||||||
|
"cost_usd": live_cost,
|
||||||
|
"pop_size": pop_size,
|
||||||
|
"n_gens": n_gens,
|
||||||
|
"evals_done": evals_done,
|
||||||
|
"evals_total": evals_total,
|
||||||
|
"gens_done": gens_done,
|
||||||
|
"top_fit": top_fit,
|
||||||
|
"median_fit": median_fit,
|
||||||
|
"parse_success": parse_success,
|
||||||
|
"config": cfg,
|
||||||
|
"gens_df": gens,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _convergence_figure(gens_df: Any) -> go.Figure:
|
||||||
|
fig = go.Figure()
|
||||||
|
if gens_df.empty:
|
||||||
|
fig.add_annotation(
|
||||||
|
text="Nessuna generazione registrata", x=0.5, y=0.5, showarrow=False,
|
||||||
|
font={"color": COLOR_TEXT_MUTED, "size": 14},
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
fig.add_trace(
|
||||||
|
go.Scatter(
|
||||||
|
x=gens_df["generation_idx"], y=gens_df["fitness_max"],
|
||||||
|
name="max", mode="lines+markers",
|
||||||
|
line={"color": COLOR_PRIMARY, "width": 3, "shape": "spline", "smoothing": 0.6},
|
||||||
|
marker={"size": 9, "color": COLOR_PRIMARY,
|
||||||
|
"line": {"color": "#fff", "width": 1}},
|
||||||
|
fill="tozeroy",
|
||||||
|
fillcolor="rgba(255, 45, 135, 0.12)",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
fig.add_trace(
|
||||||
|
go.Scatter(
|
||||||
|
x=gens_df["generation_idx"], y=gens_df["fitness_p90"],
|
||||||
|
name="p90", mode="lines+markers",
|
||||||
|
line={"color": COLOR_ACCENT, "width": 2, "dash": "dot", "shape": "spline"},
|
||||||
|
marker={"size": 7, "color": COLOR_ACCENT},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
fig.add_trace(
|
||||||
|
go.Scatter(
|
||||||
|
x=gens_df["generation_idx"], y=gens_df["fitness_median"],
|
||||||
|
name="median", mode="lines+markers",
|
||||||
|
line={"color": COLOR_SECONDARY, "width": 2, "shape": "spline"},
|
||||||
|
marker={"size": 7, "color": COLOR_SECONDARY},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
fig.update_layout(
|
||||||
|
template="plotly_dark",
|
||||||
|
paper_bgcolor=COLOR_SURFACE,
|
||||||
|
plot_bgcolor=COLOR_SURFACE,
|
||||||
|
font={"color": COLOR_TEXT},
|
||||||
|
xaxis={"title": "generation", "gridcolor": "rgba(148, 163, 184, 0.08)", "dtick": 1},
|
||||||
|
yaxis={"title": "fitness", "gridcolor": "rgba(148, 163, 184, 0.08)"},
|
||||||
|
title={"text": "Fitness convergence", "font": {"color": COLOR_TEXT, "size": 18}},
|
||||||
|
legend={"bgcolor": "rgba(19, 19, 26, 0.95)", "bordercolor": COLOR_PRIMARY, "borderwidth": 1},
|
||||||
|
margin={"l": 50, "r": 30, "t": 50, "b": 50},
|
||||||
|
)
|
||||||
|
return fig
|
||||||
|
|
||||||
|
|
||||||
|
def _entropy_figure(gens_df: Any) -> go.Figure:
|
||||||
|
fig = go.Figure()
|
||||||
|
if not gens_df.empty:
|
||||||
|
fig.add_trace(
|
||||||
|
go.Scatter(
|
||||||
|
x=gens_df["generation_idx"], y=gens_df["entropy"],
|
||||||
|
mode="lines+markers",
|
||||||
|
line={"color": COLOR_SECONDARY, "width": 3, "shape": "spline", "smoothing": 0.6},
|
||||||
|
marker={"size": 9, "color": COLOR_SECONDARY,
|
||||||
|
"line": {"color": "#fff", "width": 1}},
|
||||||
|
fill="tozeroy",
|
||||||
|
fillcolor="rgba(0, 217, 255, 0.12)",
|
||||||
|
name="entropy",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
fig.add_hline(
|
||||||
|
y=0.5, line_dash="dash", line_color=COLOR_ACCENT,
|
||||||
|
annotation_text="gate threshold (0.5)",
|
||||||
|
annotation_font_color=COLOR_ACCENT,
|
||||||
|
)
|
||||||
|
fig.update_layout(
|
||||||
|
template="plotly_dark",
|
||||||
|
paper_bgcolor=COLOR_SURFACE,
|
||||||
|
plot_bgcolor=COLOR_SURFACE,
|
||||||
|
font={"color": COLOR_TEXT},
|
||||||
|
xaxis={"title": "generation", "gridcolor": "rgba(148, 163, 184, 0.08)", "dtick": 1},
|
||||||
|
yaxis={"title": "entropy", "gridcolor": "rgba(148, 163, 184, 0.08)"},
|
||||||
|
title={"text": "Diversity (fitness entropy)", "font": {"color": COLOR_TEXT, "size": 18}},
|
||||||
|
margin={"l": 50, "r": 30, "t": 50, "b": 50},
|
||||||
|
)
|
||||||
|
return fig
|
||||||
|
|
||||||
|
|
||||||
|
@ui.page("/")
|
||||||
|
def index() -> None:
|
||||||
|
_apply_theme()
|
||||||
|
_build_header(
|
||||||
|
active="/",
|
||||||
|
brand_subtitle="Coevolutivo / GA",
|
||||||
|
nav_items=[("/", "Overview"), ("/convergence", "Convergence"), ("/genomes", "Genomes")],
|
||||||
|
db_label=f"⛁ {Path(GA_DB_PATH).resolve().name}",
|
||||||
|
)
|
||||||
|
|
||||||
|
options = _runs_options()
|
||||||
|
if not options:
|
||||||
|
ui.label("Nessuna run nel database.").classes("text-h5")
|
||||||
|
return
|
||||||
|
|
||||||
|
state: dict[str, Any] = {"run_id": next(iter(options))}
|
||||||
|
|
||||||
|
with ui.row().classes("w-full items-center gap-4 q-mb-md"):
|
||||||
|
select = ui.select(options=options, value=state["run_id"], label="Run").classes(
|
||||||
|
"flex-grow"
|
||||||
|
)
|
||||||
|
status_badge = ui.badge("…", color="primary").classes("text-body1 q-pa-sm")
|
||||||
|
ui.button("🔄 Refresh", on_click=lambda: refresh()).props("outline color=primary")
|
||||||
|
|
||||||
|
with ui.card().classes("w-full"):
|
||||||
|
ui.label("Progresso run").classes("text-subtitle1")
|
||||||
|
gen_label = ui.label("Generations: 0/0")
|
||||||
|
gen_bar = ui.linear_progress(0.0, show_value=False).props("size=20px color=primary")
|
||||||
|
eval_label = ui.label("Evaluations: 0/0 (0.0%)")
|
||||||
|
eval_bar = ui.linear_progress(0.0, show_value=False).props("size=20px color=accent")
|
||||||
|
|
||||||
|
with ui.row().classes("w-full gap-4"):
|
||||||
|
with ui.card().classes("flex-grow metric-card accent-cyan"):
|
||||||
|
ui.label("Top fitness").classes("text-caption")
|
||||||
|
top_lbl = ui.label("—").classes("text-h4")
|
||||||
|
with ui.card().classes("flex-grow metric-card accent-purple"):
|
||||||
|
ui.label("Median fitness").classes("text-caption")
|
||||||
|
median_lbl = ui.label("—").classes("text-h4")
|
||||||
|
with ui.card().classes("flex-grow metric-card accent-amber"):
|
||||||
|
ui.label("Parse success").classes("text-caption")
|
||||||
|
parse_lbl = ui.label("—").classes("text-h4")
|
||||||
|
with ui.card().classes("flex-grow metric-card accent-green"):
|
||||||
|
ui.label("Cost (USD)").classes("text-caption")
|
||||||
|
cost_lbl = ui.label("—").classes("text-h4")
|
||||||
|
|
||||||
|
with ui.row().classes("w-full gap-4 q-mt-md"):
|
||||||
|
started_lbl = ui.label("Started: —")
|
||||||
|
completed_lbl = ui.label("Completed: —")
|
||||||
|
ui.separator()
|
||||||
|
ui.label("Config").classes("text-subtitle1")
|
||||||
|
cfg_code = ui.html('<pre class="config-block"></pre>').classes("w-full")
|
||||||
|
|
||||||
|
def refresh() -> None:
|
||||||
|
run_id = select.value
|
||||||
|
if not run_id:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
s = _snapshot(run_id)
|
||||||
|
except Exception as e: # noqa: BLE001
|
||||||
|
ui.notify(f"Errore: {e}", type="negative")
|
||||||
|
return
|
||||||
|
|
||||||
|
text, color = _STATUS_BADGE.get(s["status"], (s["status"], "primary"))
|
||||||
|
status_badge.text = text
|
||||||
|
status_badge.props(f"color={color}")
|
||||||
|
|
||||||
|
gen_frac = min(s["gens_done"] / max(s["n_gens"], 1), 1.0)
|
||||||
|
eval_frac = min(s["evals_done"] / s["evals_total"], 1.0)
|
||||||
|
gen_bar.value = gen_frac
|
||||||
|
eval_bar.value = eval_frac
|
||||||
|
gen_label.text = f"Generations: {s['gens_done']}/{s['n_gens']}"
|
||||||
|
eval_label.text = (
|
||||||
|
f"Evaluations: {s['evals_done']}/{s['evals_total']} ({100 * eval_frac:.1f}%)"
|
||||||
|
)
|
||||||
|
|
||||||
|
top_lbl.text = f"{s['top_fit']:.4f}" if s["evals_done"] else "—"
|
||||||
|
median_lbl.text = f"{s['median_fit']:.4f}" if s["evals_done"] else "—"
|
||||||
|
parse_lbl.text = f"{s['parse_success']:.1f}%" if s["evals_done"] else "—"
|
||||||
|
cost_lbl.text = f"${s['cost_usd']:.4f}"
|
||||||
|
|
||||||
|
started_lbl.text = f"Started: {s['started_at']}"
|
||||||
|
completed_lbl.text = f"Completed: {s['completed_at']}"
|
||||||
|
cfg_code.content = f'<pre class="config-block">{_json_to_html(s["config"])}</pre>'
|
||||||
|
|
||||||
|
def on_select_change() -> None:
|
||||||
|
state["run_id"] = select.value
|
||||||
|
refresh()
|
||||||
|
|
||||||
|
select.on_value_change(on_select_change)
|
||||||
|
ui.timer(REFRESH_INTERVAL_S, refresh)
|
||||||
|
refresh()
|
||||||
|
|
||||||
|
|
||||||
|
@ui.page("/convergence")
|
||||||
|
def convergence() -> None:
|
||||||
|
_apply_theme()
|
||||||
|
_build_header(
|
||||||
|
active="/convergence",
|
||||||
|
brand_subtitle="Coevolutivo / GA",
|
||||||
|
nav_items=[("/", "Overview"), ("/convergence", "Convergence"), ("/genomes", "Genomes")],
|
||||||
|
db_label=f"⛁ {Path(GA_DB_PATH).resolve().name}",
|
||||||
|
)
|
||||||
|
|
||||||
|
options = _runs_options()
|
||||||
|
if not options:
|
||||||
|
ui.label("Nessuna run nel database.").classes("text-h5")
|
||||||
|
return
|
||||||
|
|
||||||
|
state: dict[str, Any] = {"run_id": next(iter(options))}
|
||||||
|
|
||||||
|
with ui.row().classes("w-full items-center gap-4 q-mb-md"):
|
||||||
|
select = ui.select(options=options, value=state["run_id"], label="Run").classes(
|
||||||
|
"flex-grow"
|
||||||
|
)
|
||||||
|
gen_count_lbl = ui.label("Gens: 0/0").classes("text-body1").style(
|
||||||
|
f"color: {COLOR_PRIMARY}; font-weight: 600;"
|
||||||
|
)
|
||||||
|
ui.button("🔄 Refresh", on_click=lambda: refresh()).props("outline color=primary")
|
||||||
|
|
||||||
|
fitness_plot = ui.plotly(_convergence_figure(generations_df(get_repo(GA_DB_PATH), state["run_id"]))).classes("w-full")
|
||||||
|
entropy_plot = ui.plotly(_entropy_figure(generations_df(get_repo(GA_DB_PATH), state["run_id"]))).classes("w-full q-mt-md")
|
||||||
|
|
||||||
|
ui.separator()
|
||||||
|
ui.label("Tabella generazioni").classes("text-subtitle1 q-mt-md")
|
||||||
|
gens_table = ui.table(
|
||||||
|
columns=[
|
||||||
|
{"name": "generation_idx", "label": "gen", "field": "generation_idx", "sortable": True},
|
||||||
|
{"name": "n_genomes", "label": "n", "field": "n_genomes"},
|
||||||
|
{"name": "fitness_max", "label": "max", "field": "fitness_max"},
|
||||||
|
{"name": "fitness_p90", "label": "p90", "field": "fitness_p90"},
|
||||||
|
{"name": "fitness_median", "label": "median", "field": "fitness_median"},
|
||||||
|
{"name": "entropy", "label": "entropy", "field": "entropy"},
|
||||||
|
{"name": "completed_at", "label": "completed", "field": "completed_at"},
|
||||||
|
],
|
||||||
|
rows=[],
|
||||||
|
row_key="generation_idx",
|
||||||
|
).classes("w-full")
|
||||||
|
|
||||||
|
def refresh() -> None:
|
||||||
|
run_id = select.value
|
||||||
|
if not run_id:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
gens = generations_df(get_repo(GA_DB_PATH), run_id)
|
||||||
|
ov = get_run_overview(get_repo(GA_DB_PATH), run_id)
|
||||||
|
except Exception as e: # noqa: BLE001
|
||||||
|
ui.notify(f"Errore: {e}", type="negative")
|
||||||
|
return
|
||||||
|
|
||||||
|
n_gens = int(ov["config"].get("n_generations", 0))
|
||||||
|
gens_done = int(gens["completed_at"].notna().sum()) if not gens.empty else 0
|
||||||
|
gen_count_lbl.text = f"Gens: {gens_done}/{n_gens}"
|
||||||
|
|
||||||
|
fitness_plot.update_figure(_convergence_figure(gens))
|
||||||
|
entropy_plot.update_figure(_entropy_figure(gens))
|
||||||
|
|
||||||
|
if gens.empty:
|
||||||
|
gens_table.rows = []
|
||||||
|
else:
|
||||||
|
display_cols = [
|
||||||
|
"generation_idx", "n_genomes",
|
||||||
|
"fitness_max", "fitness_p90", "fitness_median",
|
||||||
|
"entropy", "completed_at",
|
||||||
|
]
|
||||||
|
gens_table.rows = [
|
||||||
|
{
|
||||||
|
col: (round(v, 6) if isinstance(v, float) else v)
|
||||||
|
for col, v in row.items()
|
||||||
|
if col in display_cols
|
||||||
|
}
|
||||||
|
for _, row in gens.iterrows()
|
||||||
|
]
|
||||||
|
gens_table.update()
|
||||||
|
|
||||||
|
def on_select_change() -> None:
|
||||||
|
state["run_id"] = select.value
|
||||||
|
refresh()
|
||||||
|
|
||||||
|
select.on_value_change(on_select_change)
|
||||||
|
ui.timer(REFRESH_INTERVAL_S, refresh)
|
||||||
|
refresh()
|
||||||
|
|
||||||
|
|
||||||
|
@ui.page("/genomes")
|
||||||
|
def genomes() -> None:
|
||||||
|
_apply_theme()
|
||||||
|
_build_header(
|
||||||
|
active="/genomes",
|
||||||
|
brand_subtitle="Coevolutivo / GA",
|
||||||
|
nav_items=[("/", "Overview"), ("/convergence", "Convergence"), ("/genomes", "Genomes")],
|
||||||
|
db_label=f"⛁ {Path(GA_DB_PATH).resolve().name}",
|
||||||
|
)
|
||||||
|
|
||||||
|
options = _runs_options()
|
||||||
|
if not options:
|
||||||
|
ui.label("Nessuna run nel database.").classes("text-h5")
|
||||||
|
return
|
||||||
|
|
||||||
|
state: dict[str, Any] = {
|
||||||
|
"run_id": next(iter(options)),
|
||||||
|
"selected_gid": None,
|
||||||
|
"merged": None,
|
||||||
|
}
|
||||||
|
|
||||||
|
with ui.row().classes("w-full items-center gap-4 q-mb-md"):
|
||||||
|
select = ui.select(options=options, value=state["run_id"], label="Run").classes(
|
||||||
|
"flex-grow"
|
||||||
|
)
|
||||||
|
top_k_select = ui.select(
|
||||||
|
options={10: "Top 10", 25: "Top 25", 50: "Top 50"},
|
||||||
|
value=10,
|
||||||
|
label="Top K",
|
||||||
|
)
|
||||||
|
ui.button("🔄 Refresh", on_click=lambda: refresh()).props("outline color=primary")
|
||||||
|
|
||||||
|
ui.label("Top genomi per fitness").classes("text-subtitle1 q-mt-sm")
|
||||||
|
top_table = ui.table(
|
||||||
|
columns=[
|
||||||
|
{"name": "genome_id", "label": "id", "field": "genome_id", "align": "left"},
|
||||||
|
{"name": "fitness", "label": "fitness", "field": "fitness", "sortable": True},
|
||||||
|
{"name": "dsr", "label": "DSR", "field": "dsr"},
|
||||||
|
{"name": "sharpe", "label": "Sharpe", "field": "sharpe"},
|
||||||
|
{"name": "max_dd", "label": "max DD", "field": "max_dd"},
|
||||||
|
{"name": "n_trades", "label": "trades", "field": "n_trades"},
|
||||||
|
{"name": "cognitive_style", "label": "style", "field": "cognitive_style"},
|
||||||
|
{"name": "temperature", "label": "T", "field": "temperature"},
|
||||||
|
{"name": "lookback_window", "label": "lookback", "field": "lookback_window"},
|
||||||
|
],
|
||||||
|
rows=[],
|
||||||
|
row_key="genome_id",
|
||||||
|
selection="single",
|
||||||
|
).classes("w-full")
|
||||||
|
|
||||||
|
ui.separator().classes("q-my-md")
|
||||||
|
|
||||||
|
with ui.card().classes("w-full"):
|
||||||
|
ui.label("Ispezione genoma").classes("text-subtitle1")
|
||||||
|
detail_hint = ui.label("Seleziona un genoma dalla tabella sopra.").classes(
|
||||||
|
"text-caption"
|
||||||
|
).style(f"color: {COLOR_TEXT_MUTED};")
|
||||||
|
|
||||||
|
with ui.row().classes("w-full gap-4 q-mt-sm"):
|
||||||
|
with ui.card().classes("flex-grow metric-card accent-cyan"):
|
||||||
|
ui.label("fitness").classes("text-caption")
|
||||||
|
fit_lbl = ui.label("—").classes("text-h4")
|
||||||
|
with ui.card().classes("flex-grow metric-card accent-purple"):
|
||||||
|
ui.label("DSR").classes("text-caption")
|
||||||
|
dsr_lbl = ui.label("—").classes("text-h4")
|
||||||
|
with ui.card().classes("flex-grow metric-card accent-amber"):
|
||||||
|
ui.label("Sharpe").classes("text-caption")
|
||||||
|
sharpe_lbl = ui.label("—").classes("text-h4")
|
||||||
|
with ui.card().classes("flex-grow metric-card"):
|
||||||
|
ui.label("max DD").classes("text-caption")
|
||||||
|
dd_lbl = ui.label("—").classes("text-h4")
|
||||||
|
with ui.card().classes("flex-grow metric-card accent-green"):
|
||||||
|
ui.label("trades").classes("text-caption")
|
||||||
|
trades_lbl = ui.label("—").classes("text-h4")
|
||||||
|
with ui.card().classes("flex-grow metric-card"):
|
||||||
|
ui.label("style").classes("text-caption")
|
||||||
|
style_lbl = ui.label("—").classes("text-h4")
|
||||||
|
|
||||||
|
ui.label("System prompt").classes("text-subtitle1 q-mt-md")
|
||||||
|
prompt_code = ui.html('<pre class="raw-block">—</pre>').classes("w-full")
|
||||||
|
|
||||||
|
ui.label("Raw LLM output").classes("text-subtitle1 q-mt-md")
|
||||||
|
raw_code = ui.html('<pre class="raw-block">—</pre>').classes("w-full")
|
||||||
|
|
||||||
|
parse_error_lbl = ui.label("").classes("q-mt-sm").style(
|
||||||
|
"color: #FF6B6B; font-weight: 600;"
|
||||||
|
)
|
||||||
|
|
||||||
|
def _render_detail(row: dict[str, Any]) -> None:
|
||||||
|
detail_hint.text = f"Genoma: {row.get('genome_id', '—')}"
|
||||||
|
fit_lbl.text = f"{float(row.get('fitness', 0)):.4f}"
|
||||||
|
dsr_lbl.text = f"{float(row.get('dsr', 0)):.4f}"
|
||||||
|
sharpe_lbl.text = f"{float(row.get('sharpe', 0)):.3f}"
|
||||||
|
dd_lbl.text = f"{float(row.get('max_dd', 0)):.3f}"
|
||||||
|
trades_lbl.text = str(int(row.get("n_trades", 0)))
|
||||||
|
style_lbl.text = str(row.get("cognitive_style", "—"))
|
||||||
|
prompt_code.content = (
|
||||||
|
f'<pre class="raw-block">{html.escape(str(row.get("system_prompt", "—")))}</pre>'
|
||||||
|
)
|
||||||
|
raw_code.content = (
|
||||||
|
f'<pre class="raw-block">{html.escape(str(row.get("raw_text", "—") or "—"))}</pre>'
|
||||||
|
)
|
||||||
|
pe = row.get("parse_error")
|
||||||
|
parse_error_lbl.text = f"❌ Parse error: {pe}" if pe else ""
|
||||||
|
|
||||||
|
def refresh() -> None:
|
||||||
|
run_id = select.value
|
||||||
|
if not run_id:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
repo = get_repo(GA_DB_PATH)
|
||||||
|
evals = evaluations_df(repo, run_id)
|
||||||
|
gens = genomes_df(repo, run_id)
|
||||||
|
except Exception as e: # noqa: BLE001
|
||||||
|
ui.notify(f"Errore: {e}", type="negative")
|
||||||
|
return
|
||||||
|
|
||||||
|
if evals.empty:
|
||||||
|
top_table.rows = []
|
||||||
|
top_table.update()
|
||||||
|
return
|
||||||
|
|
||||||
|
merged = evals.merge(
|
||||||
|
gens, left_on="genome_id", right_on="id", how="left", suffixes=("", "_g")
|
||||||
|
)
|
||||||
|
state["merged"] = merged
|
||||||
|
|
||||||
|
k = int(top_k_select.value)
|
||||||
|
top = merged.sort_values("fitness", ascending=False).head(k)
|
||||||
|
|
||||||
|
rows = []
|
||||||
|
for _, r in top.iterrows():
|
||||||
|
rows.append(
|
||||||
|
{
|
||||||
|
"genome_id": str(r.get("genome_id", "—"))[:12] + "…",
|
||||||
|
"fitness": round(float(r.get("fitness", 0)), 4),
|
||||||
|
"dsr": round(float(r.get("dsr", 0)), 4),
|
||||||
|
"sharpe": round(float(r.get("sharpe", 0)), 3),
|
||||||
|
"max_dd": round(float(r.get("max_dd", 0)), 3),
|
||||||
|
"n_trades": int(r.get("n_trades", 0)),
|
||||||
|
"cognitive_style": str(r.get("cognitive_style", "—")),
|
||||||
|
"temperature": round(float(r.get("temperature", 0)), 2),
|
||||||
|
"lookback_window": int(r.get("lookback_window", 0)),
|
||||||
|
"_full_id": str(r.get("genome_id", "")),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
top_table.rows = rows
|
||||||
|
top_table.update()
|
||||||
|
|
||||||
|
sel = state.get("selected_gid")
|
||||||
|
if sel:
|
||||||
|
match = merged[merged["genome_id"] == sel]
|
||||||
|
if not match.empty:
|
||||||
|
_render_detail(match.iloc[0].to_dict())
|
||||||
|
|
||||||
|
def on_row_selected(e: Any) -> None:
|
||||||
|
rows = (e.args or {}).get("rows") or []
|
||||||
|
if not rows:
|
||||||
|
return
|
||||||
|
full_id = rows[0].get("_full_id")
|
||||||
|
if not full_id:
|
||||||
|
return
|
||||||
|
state["selected_gid"] = full_id
|
||||||
|
merged = state.get("merged")
|
||||||
|
if merged is None:
|
||||||
|
return
|
||||||
|
match = merged[merged["genome_id"] == full_id]
|
||||||
|
if not match.empty:
|
||||||
|
_render_detail(match.iloc[0].to_dict())
|
||||||
|
|
||||||
|
def on_select_change() -> None:
|
||||||
|
state["run_id"] = select.value
|
||||||
|
state["selected_gid"] = None
|
||||||
|
refresh()
|
||||||
|
|
||||||
|
select.on_value_change(on_select_change)
|
||||||
|
top_k_select.on_value_change(lambda _: refresh())
|
||||||
|
top_table.on("selection", on_row_selected)
|
||||||
|
ui.timer(REFRESH_INTERVAL_S, refresh)
|
||||||
|
refresh()
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
app.on_startup(
|
||||||
|
lambda: print(
|
||||||
|
f"GA DB: {Path(GA_DB_PATH).resolve()} | root_path: {DASHBOARD_ROOT_PATH or '/'}"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
ui.run(
|
||||||
|
host="0.0.0.0",
|
||||||
|
port=int(os.environ.get("SWARM_DASHBOARD_PORT", "8080")),
|
||||||
|
title="Multi-Swarm Core Dashboard",
|
||||||
|
reload=False,
|
||||||
|
show=False,
|
||||||
|
dark=True,
|
||||||
|
root_path=DASHBOARD_ROOT_PATH,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ in {"__main__", "__mp_main__"}:
|
||||||
|
main()
|
||||||
@@ -0,0 +1,383 @@
|
|||||||
|
"""Shared theme module for NiceGUI dashboards.
|
||||||
|
|
||||||
|
Exports palette constants, CSS, and helper functions used by both
|
||||||
|
multi_swarm_core.dashboard and strategy_crypto.frontend dashboards.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import html
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from nicegui import ui
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"COLOR_BG",
|
||||||
|
"COLOR_SURFACE",
|
||||||
|
"COLOR_SURFACE_2",
|
||||||
|
"COLOR_BORDER",
|
||||||
|
"COLOR_BORDER_HOVER",
|
||||||
|
"COLOR_PRIMARY",
|
||||||
|
"COLOR_SECONDARY",
|
||||||
|
"COLOR_ACCENT",
|
||||||
|
"COLOR_SUCCESS",
|
||||||
|
"COLOR_DANGER",
|
||||||
|
"COLOR_TEXT",
|
||||||
|
"COLOR_TEXT_MUTED",
|
||||||
|
"_STATUS_BADGE",
|
||||||
|
"_CUSTOM_CSS",
|
||||||
|
"_json_to_html",
|
||||||
|
"_apply_theme",
|
||||||
|
"_build_header",
|
||||||
|
]
|
||||||
|
|
||||||
|
# --- Neon Trading Dashboard palette ---
|
||||||
|
COLOR_BG = "#0A0A0F"
|
||||||
|
COLOR_SURFACE = "#13131A"
|
||||||
|
COLOR_SURFACE_2 = "#1C1C26"
|
||||||
|
COLOR_BORDER = "rgba(255, 45, 135, 0.12)"
|
||||||
|
COLOR_BORDER_HOVER = "rgba(255, 45, 135, 0.45)"
|
||||||
|
COLOR_PRIMARY = "#FF2D87"
|
||||||
|
COLOR_SECONDARY = "#00D9FF"
|
||||||
|
COLOR_ACCENT = "#FFB800"
|
||||||
|
COLOR_SUCCESS = "#00E676"
|
||||||
|
COLOR_DANGER = "#FF3D60"
|
||||||
|
COLOR_TEXT = "#FFFFFF"
|
||||||
|
COLOR_TEXT_MUTED = "#7A7A8C"
|
||||||
|
|
||||||
|
_STATUS_BADGE: dict[str, tuple[str, str]] = {
|
||||||
|
"running": ("● running", "positive"),
|
||||||
|
"completed": ("✓ completed", "positive"),
|
||||||
|
"failed": ("✕ failed", "negative"),
|
||||||
|
}
|
||||||
|
|
||||||
|
_CUSTOM_CSS = f"""
|
||||||
|
<style>
|
||||||
|
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap');
|
||||||
|
|
||||||
|
html, body, .q-page, .q-card, .q-btn, .q-field, .q-table, .text-h4, .text-h6, .text-subtitle1, .text-caption, .text-body1, .nav-link, .brand, label, p, span, div {{
|
||||||
|
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
|
||||||
|
letter-spacing: -0.01em;
|
||||||
|
}}
|
||||||
|
.material-icons, .material-icons-outlined, .material-symbols-outlined, .q-icon, i.q-icon, i[class*="material"] {{
|
||||||
|
font-family: 'Material Icons' !important;
|
||||||
|
font-feature-settings: 'liga';
|
||||||
|
letter-spacing: normal !important;
|
||||||
|
}}
|
||||||
|
code, pre, .q-code, .nicegui-code {{ font-family: 'JetBrains Mono', 'Fira Code', monospace !important; font-size: 13.5px !important; }}
|
||||||
|
|
||||||
|
body, .q-page-container, .q-page {{
|
||||||
|
background: {COLOR_BG} !important;
|
||||||
|
color: {COLOR_TEXT};
|
||||||
|
background-image:
|
||||||
|
radial-gradient(ellipse 800px 400px at 20% 0%, rgba(255, 45, 135, 0.08) 0%, transparent 60%),
|
||||||
|
radial-gradient(ellipse 600px 400px at 80% 100%, rgba(0, 217, 255, 0.06) 0%, transparent 60%);
|
||||||
|
background-attachment: fixed;
|
||||||
|
}}
|
||||||
|
|
||||||
|
.q-card {{
|
||||||
|
background: {COLOR_SURFACE} !important;
|
||||||
|
color: {COLOR_TEXT} !important;
|
||||||
|
border: 1px solid {COLOR_BORDER};
|
||||||
|
border-radius: 14px !important;
|
||||||
|
box-shadow:
|
||||||
|
0 1px 2px rgba(0,0,0,0.5),
|
||||||
|
0 8px 24px rgba(0,0,0,0.25),
|
||||||
|
inset 0 1px 0 rgba(255,255,255,0.04);
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}}
|
||||||
|
.q-card::before {{
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0; left: 0; right: 0;
|
||||||
|
height: 1px;
|
||||||
|
background: linear-gradient(90deg, transparent, rgba(255, 45, 135, 0.4), transparent);
|
||||||
|
opacity: 0.5;
|
||||||
|
}}
|
||||||
|
.q-card:hover {{
|
||||||
|
border-color: rgba(255, 45, 135, 0.5);
|
||||||
|
box-shadow:
|
||||||
|
0 1px 2px rgba(0,0,0,0.5),
|
||||||
|
0 8px 32px rgba(255, 45, 135, 0.15),
|
||||||
|
inset 0 1px 0 rgba(255,255,255,0.05);
|
||||||
|
}}
|
||||||
|
|
||||||
|
.metric-card {{
|
||||||
|
padding: 20px 16px;
|
||||||
|
text-align: left;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
min-width: 140px;
|
||||||
|
}}
|
||||||
|
.metric-card .text-caption {{
|
||||||
|
color: {COLOR_TEXT_MUTED} !important;
|
||||||
|
font-size: 11px !important;
|
||||||
|
font-weight: 500 !important;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
}}
|
||||||
|
.metric-card .text-h4 {{
|
||||||
|
color: {COLOR_TEXT} !important;
|
||||||
|
font-weight: 600 !important;
|
||||||
|
font-size: 26px !important;
|
||||||
|
line-height: 1.2 !important;
|
||||||
|
font-feature-settings: 'tnum';
|
||||||
|
}}
|
||||||
|
.metric-card.accent-cyan .text-h4 {{ color: {COLOR_PRIMARY} !important; }}
|
||||||
|
.metric-card.accent-purple .text-h4 {{ color: {COLOR_SECONDARY} !important; }}
|
||||||
|
.metric-card.accent-amber .text-h4 {{ color: {COLOR_ACCENT} !important; }}
|
||||||
|
.metric-card.accent-green .text-h4 {{ color: {COLOR_SUCCESS} !important; }}
|
||||||
|
|
||||||
|
.q-header {{
|
||||||
|
background: rgba(10, 10, 15, 0.75) !important;
|
||||||
|
backdrop-filter: blur(20px) saturate(180%);
|
||||||
|
-webkit-backdrop-filter: blur(20px) saturate(180%);
|
||||||
|
border-bottom: 1px solid {COLOR_BORDER} !important;
|
||||||
|
box-shadow: 0 1px 0 rgba(255, 45, 135, 0.15) !important;
|
||||||
|
}}
|
||||||
|
|
||||||
|
.nav-link {{
|
||||||
|
color: {COLOR_TEXT_MUTED} !important;
|
||||||
|
padding: 8px 14px;
|
||||||
|
border-radius: 8px;
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 13.5px;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
position: relative;
|
||||||
|
}}
|
||||||
|
.nav-link:hover {{
|
||||||
|
color: {COLOR_TEXT} !important;
|
||||||
|
background: {COLOR_SURFACE_2};
|
||||||
|
}}
|
||||||
|
.nav-link.active {{
|
||||||
|
color: {COLOR_PRIMARY} !important;
|
||||||
|
background: rgba(255, 45, 135, 0.08);
|
||||||
|
}}
|
||||||
|
.nav-link.active::after {{
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
bottom: -16px;
|
||||||
|
left: 14px;
|
||||||
|
right: 14px;
|
||||||
|
height: 2px;
|
||||||
|
background: {COLOR_PRIMARY};
|
||||||
|
border-radius: 2px 2px 0 0;
|
||||||
|
}}
|
||||||
|
|
||||||
|
.brand {{
|
||||||
|
color: {COLOR_TEXT};
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 15px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}}
|
||||||
|
.brand-dot {{
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: {COLOR_PRIMARY};
|
||||||
|
box-shadow: 0 0 16px {COLOR_PRIMARY}, 0 0 4px {COLOR_PRIMARY};
|
||||||
|
animation: pulse-pink 2s ease-in-out infinite;
|
||||||
|
}}
|
||||||
|
@keyframes pulse-pink {{
|
||||||
|
0%, 100% {{ box-shadow: 0 0 16px {COLOR_PRIMARY}, 0 0 4px {COLOR_PRIMARY}; }}
|
||||||
|
50% {{ box-shadow: 0 0 24px {COLOR_PRIMARY}, 0 0 8px {COLOR_PRIMARY}; }}
|
||||||
|
}}
|
||||||
|
|
||||||
|
.q-linear-progress {{ height: 8px !important; border-radius: 6px !important; }}
|
||||||
|
.q-linear-progress__track {{ background: {COLOR_SURFACE_2} !important; }}
|
||||||
|
.q-linear-progress__model {{ border-radius: 6px !important; }}
|
||||||
|
|
||||||
|
.q-separator {{ background: {COLOR_BORDER} !important; }}
|
||||||
|
|
||||||
|
.q-field--outlined .q-field__control {{
|
||||||
|
background: {COLOR_SURFACE} !important;
|
||||||
|
border-radius: 8px !important;
|
||||||
|
}}
|
||||||
|
.q-field--outlined .q-field__control:before {{ border-color: {COLOR_BORDER} !important; }}
|
||||||
|
.q-field--outlined.q-field--focused .q-field__control:after {{ border-color: {COLOR_PRIMARY} !important; }}
|
||||||
|
.q-field__label {{ color: {COLOR_TEXT_MUTED} !important; }}
|
||||||
|
.q-field__native, .q-field__input {{ color: {COLOR_TEXT} !important; }}
|
||||||
|
|
||||||
|
.q-btn {{ border-radius: 8px !important; font-weight: 500 !important; text-transform: none !important; letter-spacing: 0 !important; }}
|
||||||
|
|
||||||
|
.q-table {{ background: transparent !important; color: {COLOR_TEXT} !important; }}
|
||||||
|
.q-table thead tr {{ background: {COLOR_SURFACE_2} !important; }}
|
||||||
|
.q-table th {{
|
||||||
|
color: {COLOR_TEXT_MUTED} !important;
|
||||||
|
font-size: 11px !important;
|
||||||
|
font-weight: 600 !important;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
}}
|
||||||
|
.q-table tbody tr {{ transition: background 0.15s; }}
|
||||||
|
.q-table tbody tr:hover {{ background: rgba(255, 45, 135, 0.05) !important; }}
|
||||||
|
.q-table tbody tr.selected {{ background: rgba(255, 45, 135, 0.12) !important; }}
|
||||||
|
.q-table td {{ border-bottom: 1px solid {COLOR_BORDER} !important; font-feature-settings: 'tnum'; }}
|
||||||
|
|
||||||
|
.text-h6 {{ font-weight: 600 !important; letter-spacing: -0.015em !important; }}
|
||||||
|
.text-subtitle1 {{
|
||||||
|
color: {COLOR_TEXT_MUTED} !important;
|
||||||
|
font-size: 11px !important;
|
||||||
|
font-weight: 600 !important;
|
||||||
|
text-transform: uppercase !important;
|
||||||
|
letter-spacing: 0.08em !important;
|
||||||
|
margin-bottom: 8px !important;
|
||||||
|
}}
|
||||||
|
|
||||||
|
code, pre, .nicegui-code {{
|
||||||
|
background: #1A1A24 !important;
|
||||||
|
color: {COLOR_TEXT} !important;
|
||||||
|
border: 1px solid {COLOR_BORDER};
|
||||||
|
border-radius: 10px !important;
|
||||||
|
padding: 16px !important;
|
||||||
|
font-size: 13.5px !important;
|
||||||
|
line-height: 1.6 !important;
|
||||||
|
}}
|
||||||
|
.hljs {{ background: transparent !important; color: {COLOR_TEXT} !important; }}
|
||||||
|
.hljs-attr, .hljs-attribute {{ color: {COLOR_SECONDARY} !important; font-weight: 500; }}
|
||||||
|
.hljs-string {{ color: {COLOR_SUCCESS} !important; }}
|
||||||
|
.hljs-number, .hljs-literal {{ color: {COLOR_PRIMARY} !important; font-weight: 500; }}
|
||||||
|
.hljs-keyword, .hljs-built_in {{ color: {COLOR_ACCENT} !important; }}
|
||||||
|
.hljs-punctuation, .hljs-meta {{ color: {COLOR_TEXT_MUTED} !important; }}
|
||||||
|
.hljs-comment {{ color: {COLOR_TEXT_MUTED} !important; font-style: italic; }}
|
||||||
|
.hljs-name, .hljs-title {{ color: {COLOR_PRIMARY} !important; }}
|
||||||
|
|
||||||
|
/* Prism.js tokens (NiceGUI usa Prism per ui.code) */
|
||||||
|
.token.property, .token.attr-name, .token.tag {{ color: {COLOR_SECONDARY} !important; font-weight: 500; }}
|
||||||
|
.token.string, .token.url {{ color: {COLOR_SUCCESS} !important; }}
|
||||||
|
.token.number, .token.boolean, .token.null, .token.symbol {{ color: {COLOR_PRIMARY} !important; font-weight: 500; }}
|
||||||
|
.token.keyword, .token.constant, .token.builtin, .token.atrule {{ color: {COLOR_ACCENT} !important; }}
|
||||||
|
.token.punctuation, .token.operator {{ color: {COLOR_TEXT_MUTED} !important; }}
|
||||||
|
.token.comment {{ color: {COLOR_TEXT_MUTED} !important; font-style: italic; }}
|
||||||
|
.token.function, .token.class-name {{ color: {COLOR_PRIMARY} !important; }}
|
||||||
|
pre[class*="language-"], code[class*="language-"] {{
|
||||||
|
color: {COLOR_TEXT} !important;
|
||||||
|
text-shadow: none !important;
|
||||||
|
}}
|
||||||
|
|
||||||
|
.q-badge {{ border-radius: 6px !important; font-weight: 500 !important; padding: 4px 10px !important; font-size: 12px !important; }}
|
||||||
|
|
||||||
|
.config-block {{
|
||||||
|
background: #1A1A24;
|
||||||
|
color: {COLOR_TEXT};
|
||||||
|
border: 1px solid {COLOR_BORDER};
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 18px 20px;
|
||||||
|
font-family: 'JetBrains Mono', 'Fira Code', monospace;
|
||||||
|
font-size: 13.5px;
|
||||||
|
line-height: 1.7;
|
||||||
|
overflow-x: auto;
|
||||||
|
white-space: pre;
|
||||||
|
margin: 0;
|
||||||
|
}}
|
||||||
|
.config-block .cb-key {{ color: {COLOR_SECONDARY}; font-weight: 500; }}
|
||||||
|
.config-block .cb-string {{ color: {COLOR_SUCCESS}; }}
|
||||||
|
.config-block .cb-number {{ color: {COLOR_PRIMARY}; font-weight: 500; }}
|
||||||
|
.config-block .cb-bool {{ color: {COLOR_ACCENT}; }}
|
||||||
|
.config-block .cb-null {{ color: {COLOR_ACCENT}; font-style: italic; }}
|
||||||
|
.config-block .cb-punct {{ color: {COLOR_TEXT_MUTED}; }}
|
||||||
|
|
||||||
|
.raw-block {{
|
||||||
|
background: #1A1A24;
|
||||||
|
color: {COLOR_TEXT};
|
||||||
|
border: 1px solid {COLOR_BORDER};
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 18px 20px;
|
||||||
|
font-family: 'JetBrains Mono', 'Fira Code', monospace;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.6;
|
||||||
|
overflow-x: auto;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-word;
|
||||||
|
max-height: 400px;
|
||||||
|
overflow-y: auto;
|
||||||
|
margin: 0;
|
||||||
|
}}
|
||||||
|
</style>
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def _json_to_html(obj: Any, indent: int = 0) -> str:
|
||||||
|
"""Render JSON con span colorati espliciti. Garantisce leggibilità ovunque."""
|
||||||
|
pad = " " * indent
|
||||||
|
inner_pad = " " * (indent + 1)
|
||||||
|
if isinstance(obj, dict):
|
||||||
|
if not obj:
|
||||||
|
return '<span class="cb-punct">{}</span>'
|
||||||
|
items = []
|
||||||
|
for k, v in obj.items():
|
||||||
|
key = f'<span class="cb-key">"{html.escape(str(k))}"</span>'
|
||||||
|
val = _json_to_html(v, indent + 1)
|
||||||
|
items.append(f"{inner_pad}{key}<span class=\"cb-punct\">:</span> {val}")
|
||||||
|
return ('<span class="cb-punct">{</span>\n'
|
||||||
|
+ '<span class="cb-punct">,</span>\n'.join(items)
|
||||||
|
+ f'\n{pad}<span class="cb-punct">}}</span>')
|
||||||
|
if isinstance(obj, list):
|
||||||
|
if not obj:
|
||||||
|
return '<span class="cb-punct">[]</span>'
|
||||||
|
items = [_json_to_html(x, indent + 1) for x in obj]
|
||||||
|
return ('<span class="cb-punct">[</span>\n'
|
||||||
|
+ '<span class="cb-punct">,</span>\n'.join(inner_pad + i for i in items)
|
||||||
|
+ f'\n{pad}<span class="cb-punct">]</span>')
|
||||||
|
if isinstance(obj, bool):
|
||||||
|
return f'<span class="cb-bool">{str(obj).lower()}</span>'
|
||||||
|
if obj is None:
|
||||||
|
return '<span class="cb-null">null</span>'
|
||||||
|
if isinstance(obj, (int, float)):
|
||||||
|
return f'<span class="cb-number">{obj}</span>'
|
||||||
|
return f'<span class="cb-string">"{html.escape(str(obj))}"</span>'
|
||||||
|
|
||||||
|
|
||||||
|
def _apply_theme() -> None:
|
||||||
|
ui.add_head_html(_CUSTOM_CSS)
|
||||||
|
ui.dark_mode().enable()
|
||||||
|
ui.colors(
|
||||||
|
primary=COLOR_PRIMARY,
|
||||||
|
secondary=COLOR_SECONDARY,
|
||||||
|
accent=COLOR_ACCENT,
|
||||||
|
dark=COLOR_BG,
|
||||||
|
dark_page=COLOR_BG,
|
||||||
|
positive=COLOR_SUCCESS,
|
||||||
|
negative=COLOR_DANGER,
|
||||||
|
info=COLOR_PRIMARY,
|
||||||
|
warning=COLOR_ACCENT,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _build_header(
|
||||||
|
active: str,
|
||||||
|
brand_subtitle: str,
|
||||||
|
nav_items: list[tuple[str, str]],
|
||||||
|
db_label: str,
|
||||||
|
) -> None:
|
||||||
|
"""Render the top navigation header.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
active: URL path of the currently active page (e.g. "/").
|
||||||
|
brand_subtitle: Text shown after the brand dot, e.g. "Coevolutivo / GA".
|
||||||
|
nav_items: List of (path, label) tuples for nav links.
|
||||||
|
db_label: Short DB identifier shown in the top-right corner.
|
||||||
|
"""
|
||||||
|
with ui.header().classes("items-center justify-between q-px-lg q-py-md"):
|
||||||
|
with ui.row().classes("items-center gap-8"):
|
||||||
|
with ui.row().classes("items-center gap-2").classes("brand"):
|
||||||
|
ui.html('<span class="brand-dot"></span>')
|
||||||
|
ui.html(
|
||||||
|
f'<span class="brand">Multi-Swarm <span style="color:{COLOR_TEXT_MUTED}'
|
||||||
|
f';font-weight:400;">/ {brand_subtitle}</span></span>'
|
||||||
|
)
|
||||||
|
with ui.row().classes("items-center gap-1"):
|
||||||
|
for path, label in nav_items:
|
||||||
|
cls = "nav-link active" if active == path else "nav-link"
|
||||||
|
ui.link(label, path).classes(cls)
|
||||||
|
with ui.row().classes("items-center gap-3"):
|
||||||
|
ui.html(
|
||||||
|
f'<span style="color:{COLOR_TEXT_MUTED};font-size:12px;'
|
||||||
|
f'font-family:JetBrains Mono,monospace;">{db_label}</span>'
|
||||||
|
)
|
||||||
@@ -3,34 +3,12 @@ from __future__ import annotations
|
|||||||
import random
|
import random
|
||||||
|
|
||||||
from ..genome.hypothesis import HypothesisAgentGenome, ModelTier
|
from ..genome.hypothesis import HypothesisAgentGenome, ModelTier
|
||||||
from ..genome.mutation import COGNITIVE_STYLES
|
from ..genome.prompt_library import PromptLibrary
|
||||||
|
|
||||||
STYLE_PROMPTS: dict[str, str] = {
|
# Mantenuto come alias backcompat: equivalente a PromptLibrary.default().styles.
|
||||||
"physicist": (
|
# Nuovi caller dovrebbero usare PromptLibrary direttamente per supportare
|
||||||
"Cerca leggi conservative, simmetrie, regimi di scala. "
|
# l'override via prompts.json di una strategia.
|
||||||
"Pensa in termini di flussi e potenziali."
|
STYLE_PROMPTS: dict[str, str] = PromptLibrary.default().styles
|
||||||
),
|
|
||||||
"biologist": (
|
|
||||||
"Cerca pattern adattivi, nicchie ecologiche, "
|
|
||||||
"predator-prey dynamics tra partecipanti del mercato."
|
|
||||||
),
|
|
||||||
"historian": (
|
|
||||||
"Cerca pattern ricorrenti su scale temporali multiple, "
|
|
||||||
"analogie con regimi storici, mean reversion strutturali."
|
|
||||||
),
|
|
||||||
"meteorologist": (
|
|
||||||
"Cerca regimi di volatilità che si autoalimentano, "
|
|
||||||
"transizioni di stato come fronti, persistenza locale."
|
|
||||||
),
|
|
||||||
"ecologist": (
|
|
||||||
"Cerca interazioni multi-asset, correlazioni cluster, "
|
|
||||||
"segnali di stress sistemico nelle dinamiche di flusso."
|
|
||||||
),
|
|
||||||
"engineer": (
|
|
||||||
"Cerca segnali con rapporto S/N favorevole, filtri causali, "
|
|
||||||
"robustezza a perturbazioni di calibrazione."
|
|
||||||
),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def build_initial_population(
|
def build_initial_population(
|
||||||
@@ -38,15 +16,22 @@ def build_initial_population(
|
|||||||
model_tier: ModelTier,
|
model_tier: ModelTier,
|
||||||
rng: random.Random,
|
rng: random.Random,
|
||||||
feature_pool: tuple[str, ...] = ("close", "high", "low", "volume"),
|
feature_pool: tuple[str, ...] = ("close", "high", "low", "volume"),
|
||||||
|
prompt_library: PromptLibrary | None = None,
|
||||||
) -> list[HypothesisAgentGenome]:
|
) -> list[HypothesisAgentGenome]:
|
||||||
"""Costruisce una popolazione iniziale K varia per stile cognitivo + parametri."""
|
"""Costruisce una popolazione iniziale K varia per stile cognitivo + parametri.
|
||||||
|
|
||||||
|
``prompt_library`` controlla quali stili sono disponibili e quale system_prompt
|
||||||
|
iniziale viene assegnato. Default = builtin 6 stili (physicist, biologist, ...).
|
||||||
|
Override tipico: ``PromptLibrary.from_json(strategy_crypto/prompts.json)``.
|
||||||
|
"""
|
||||||
|
lib = prompt_library or PromptLibrary.default()
|
||||||
population: list[HypothesisAgentGenome] = []
|
population: list[HypothesisAgentGenome] = []
|
||||||
for i in range(k):
|
for i in range(k):
|
||||||
style = COGNITIVE_STYLES[i % len(COGNITIVE_STYLES)]
|
style = lib.style_at(i)
|
||||||
n_features = rng.randint(1, len(feature_pool))
|
n_features = rng.randint(1, len(feature_pool))
|
||||||
feats = sorted(rng.sample(feature_pool, k=n_features))
|
feats = sorted(rng.sample(feature_pool, k=n_features))
|
||||||
g = HypothesisAgentGenome(
|
g = HypothesisAgentGenome(
|
||||||
system_prompt=STYLE_PROMPTS[style],
|
system_prompt=lib.directive(style),
|
||||||
feature_access=feats,
|
feature_access=feats,
|
||||||
temperature=round(rng.uniform(0.7, 1.2), 2),
|
temperature=round(rng.uniform(0.7, 1.2), 2),
|
||||||
top_p=0.95,
|
top_p=0.95,
|
||||||
|
|||||||
@@ -7,6 +7,10 @@ from .hypothesis import HypothesisAgentGenome
|
|||||||
|
|
||||||
FEATURE_POOL: tuple[str, ...] = ("open", "high", "low", "close", "volume")
|
FEATURE_POOL: tuple[str, ...] = ("open", "high", "low", "close", "volume")
|
||||||
|
|
||||||
|
# Lista di default builtin (allineata con PromptLibrary.default()).
|
||||||
|
# Il dispatcher run_phase1 sovrascrive `COGNITIVE_STYLES` con la lista letta
|
||||||
|
# da prompts.json prima del loop GA, cosi' `mutate_cognitive_style` pesca
|
||||||
|
# dai candidati corretti per la strategia in corso.
|
||||||
COGNITIVE_STYLES: tuple[str, ...] = (
|
COGNITIVE_STYLES: tuple[str, ...] = (
|
||||||
"physicist",
|
"physicist",
|
||||||
"biologist",
|
"biologist",
|
||||||
@@ -17,6 +21,18 @@ COGNITIVE_STYLES: tuple[str, ...] = (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def set_cognitive_styles(styles: tuple[str, ...]) -> None:
|
||||||
|
"""Sovrascrive la lista globale di stili candidati per la mutation.
|
||||||
|
|
||||||
|
Da chiamare PRIMA del GA loop (es. in run_phase1 dopo aver caricato la
|
||||||
|
PromptLibrary). Non thread-safe: pensata per uno script CLI.
|
||||||
|
"""
|
||||||
|
global COGNITIVE_STYLES
|
||||||
|
if not styles:
|
||||||
|
raise ValueError("set_cognitive_styles: lista vuota")
|
||||||
|
COGNITIVE_STYLES = tuple(styles)
|
||||||
|
|
||||||
|
|
||||||
def _clone_with(g: HypothesisAgentGenome, **overrides: Any) -> HypothesisAgentGenome:
|
def _clone_with(g: HypothesisAgentGenome, **overrides: Any) -> HypothesisAgentGenome:
|
||||||
payload: dict[str, Any] = g.to_dict()
|
payload: dict[str, Any] = g.to_dict()
|
||||||
payload.update(overrides)
|
payload.update(overrides)
|
||||||
|
|||||||
@@ -51,9 +51,9 @@ MUTATION_INSTRUCTIONS: dict[str, str] = {
|
|||||||
|
|
||||||
# Keyword tecniche minime per validare che il prompt sia ancora "una strategia".
|
# Keyword tecniche minime per validare che il prompt sia ancora "una strategia".
|
||||||
_VALID_KEYWORDS = (
|
_VALID_KEYWORDS = (
|
||||||
"rsi", "sma", "ema", "atr", "atr_pct", "realized_vol",
|
"rsi", "sma", "sma_pct", "ema", "atr", "atr_pct", "realized_vol",
|
||||||
"momentum", "breakout", "mean", "reversion",
|
"momentum", "breakout", "mean", "reversion",
|
||||||
"macd", "vwap", "bb", "bollinger", "stoch", "trend", "signal", "buy",
|
"macd", "macd_pct", "vwap", "bb", "bollinger", "stoch", "trend", "signal", "buy",
|
||||||
"sell", "long", "short", "entry", "exit", "stop", "rule", "condition",
|
"sell", "long", "short", "entry", "exit", "stop", "rule", "condition",
|
||||||
"if", "when", "and", "or", "gt", "lt", ">", "<", "ge", "le",
|
"if", "when", "and", "or", "gt", "lt", ">", "<", "ge", "le",
|
||||||
"hour", "dow", "weekend", "indicator", "feature",
|
"hour", "dow", "weekend", "indicator", "feature",
|
||||||
|
|||||||
@@ -0,0 +1,234 @@
|
|||||||
|
"""Libreria di stili cognitivi + direttive system_prompt per il GA.
|
||||||
|
|
||||||
|
Carica da un file JSON esterno (tipicamente shippato dal singolo strategy
|
||||||
|
member, es. ``strategy_crypto/prompts.json``) le coppie ``style -> directive``
|
||||||
|
usate da:
|
||||||
|
- :func:`multi_swarm_core.ga.initial.build_initial_population` per il
|
||||||
|
bootstrap della popolazione (style assegnato round-robin, directive
|
||||||
|
come system_prompt iniziale).
|
||||||
|
- :func:`multi_swarm_core.genome.mutation.mutate_cognitive_style` per
|
||||||
|
pescare i candidati di mutazione (range = key del JSON).
|
||||||
|
|
||||||
|
Schema JSON atteso::
|
||||||
|
|
||||||
|
{
|
||||||
|
"styles": {
|
||||||
|
"<name>": {"directive": "<testo system_prompt>"},
|
||||||
|
...
|
||||||
|
},
|
||||||
|
"agent_role": "<descrizione agente, opzionale>",
|
||||||
|
"pattern_guidance": "<sezione PATTERN GUIDANCE, opzionale>",
|
||||||
|
"instruction": "<frase finale USER, opzionale>",
|
||||||
|
"domain_warnings": "<warning di dominio, opzionale>"
|
||||||
|
}
|
||||||
|
|
||||||
|
I 6 stili default (physicist, biologist, historian, meteorologist,
|
||||||
|
ecologist, engineer) sono comunque disponibili via :meth:`PromptLibrary.default`
|
||||||
|
per backcompat con test/script senza file esterno.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
_DEFAULT_STYLES: dict[str, str] = {
|
||||||
|
"physicist": (
|
||||||
|
"Cerca leggi conservative, simmetrie, regimi di scala. "
|
||||||
|
"Pensa in termini di flussi e potenziali."
|
||||||
|
),
|
||||||
|
"biologist": (
|
||||||
|
"Cerca pattern adattivi, nicchie ecologiche, "
|
||||||
|
"predator-prey dynamics tra partecipanti del mercato."
|
||||||
|
),
|
||||||
|
"historian": (
|
||||||
|
"Cerca pattern ricorrenti su scale temporali multiple, "
|
||||||
|
"analogie con regimi storici, mean reversion strutturali."
|
||||||
|
),
|
||||||
|
"meteorologist": (
|
||||||
|
"Cerca regimi di volatilità che si autoalimentano, "
|
||||||
|
"transizioni di stato come fronti, persistenza locale."
|
||||||
|
),
|
||||||
|
"ecologist": (
|
||||||
|
"Cerca interazioni multi-asset, correlazioni cluster, "
|
||||||
|
"segnali di stress sistemico nelle dinamiche di flusso."
|
||||||
|
),
|
||||||
|
"engineer": (
|
||||||
|
"Cerca segnali con rapporto S/N favorevole, filtri causali, "
|
||||||
|
"robustezza a perturbazioni di calibrazione."
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class PromptLibraryError(ValueError):
|
||||||
|
"""Sollevata su JSON malformato o stili invalid."""
|
||||||
|
|
||||||
|
|
||||||
|
_DEFAULT_PATTERN_GUIDANCE = (
|
||||||
|
"Forme di curva: trend (SMA cross), compressione volatilita (atr_pct basso), "
|
||||||
|
"espansione volatilita (atr_pct alto), mean reversion (sma_pct estremo), "
|
||||||
|
"momentum (macd_pct, rsi zone).\n\n"
|
||||||
|
" Ripetibilita dell'andamento: crossover/crossunder ricorrenti, pattern intra-day "
|
||||||
|
"(hour gate), pattern settimanali (dow/is_weekend), range breakout."
|
||||||
|
)
|
||||||
|
|
||||||
|
_DEFAULT_AGENT_ROLE = (
|
||||||
|
"Sei un agente generatore di ipotesi di trading quantitativo per un sistema swarm."
|
||||||
|
)
|
||||||
|
|
||||||
|
_DEFAULT_INSTRUCTION = (
|
||||||
|
"Genera una strategia che cerchi anomalie sfruttabili in questo regime."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class PromptLibrary:
|
||||||
|
"""Set immutabile di stili cognitivi + direttive system_prompt.
|
||||||
|
|
||||||
|
v3.0: aggiunge 4 campi top-level strategy-specific iniettabili nel prompt
|
||||||
|
dal compositor ``_build_system_prompt``. Tutti opzionali con default sensati
|
||||||
|
per backcompat.
|
||||||
|
v3.1: aggiunge anti_patterns e output_priorities iniettati dopo i VINCOLI core.
|
||||||
|
"""
|
||||||
|
|
||||||
|
styles: dict[str, str]
|
||||||
|
focus: dict[str, list[str]] # style -> lista metriche prioritarie
|
||||||
|
# NEW v3.0: contenuto strategy-specific iniettabile nel prompt
|
||||||
|
agent_role: str = field(default="") # header SYSTEM, descrive chi e' l'agente
|
||||||
|
pattern_guidance: str = field(default="") # sezione PATTERN GUIDANCE del SYSTEM
|
||||||
|
instruction: str = field(default="") # frase finale USER ("Genera una strategia...")
|
||||||
|
domain_warnings: str = field(default="") # opzionale: warning di dominio (es. crypto 24/7)
|
||||||
|
anti_patterns: str = field(default="") # NEW v3.1: lista esplicita di pattern da evitare
|
||||||
|
output_priorities: str = field(default="") # NEW v3.1: trade-off espliciti di output
|
||||||
|
|
||||||
|
def __post_init__(self) -> None:
|
||||||
|
if not self.styles:
|
||||||
|
raise PromptLibraryError("PromptLibrary deve avere almeno uno stile")
|
||||||
|
for name, directive in self.styles.items():
|
||||||
|
if not isinstance(name, str) or not name.strip():
|
||||||
|
raise PromptLibraryError(f"nome stile invalido: {name!r}")
|
||||||
|
if not isinstance(directive, str) or not directive.strip():
|
||||||
|
raise PromptLibraryError(
|
||||||
|
f"directive vuota o invalida per stile {name!r}"
|
||||||
|
)
|
||||||
|
# Validate new optional string fields: if provided must be non-whitespace
|
||||||
|
for field_name, value in (
|
||||||
|
("agent_role", self.agent_role),
|
||||||
|
("pattern_guidance", self.pattern_guidance),
|
||||||
|
("instruction", self.instruction),
|
||||||
|
("domain_warnings", self.domain_warnings),
|
||||||
|
("anti_patterns", self.anti_patterns),
|
||||||
|
("output_priorities", self.output_priorities),
|
||||||
|
):
|
||||||
|
if not isinstance(value, str):
|
||||||
|
raise PromptLibraryError(
|
||||||
|
f"campo '{field_name}' deve essere stringa, non {type(value)}"
|
||||||
|
)
|
||||||
|
if value and not value.strip():
|
||||||
|
raise PromptLibraryError(
|
||||||
|
f"campo '{field_name}' fornito ma contiene solo whitespace"
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def default(cls) -> PromptLibrary:
|
||||||
|
"""Libreria builtin con i 6 stili originali (fallback senza file)."""
|
||||||
|
return cls(
|
||||||
|
styles=dict(_DEFAULT_STYLES),
|
||||||
|
focus={},
|
||||||
|
agent_role=_DEFAULT_AGENT_ROLE,
|
||||||
|
pattern_guidance=_DEFAULT_PATTERN_GUIDANCE,
|
||||||
|
instruction=_DEFAULT_INSTRUCTION,
|
||||||
|
domain_warnings="",
|
||||||
|
anti_patterns="",
|
||||||
|
output_priorities="",
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_json(cls, path: Path | str) -> PromptLibrary:
|
||||||
|
"""Carica da file JSON.
|
||||||
|
|
||||||
|
Schema::
|
||||||
|
|
||||||
|
{
|
||||||
|
"styles": {<name>: {"directive": "...", "focus_metrics": [...]}},
|
||||||
|
"agent_role": "...", // opzionale
|
||||||
|
"pattern_guidance": "...", // opzionale
|
||||||
|
"instruction": "...", // opzionale
|
||||||
|
"domain_warnings": "..." // opzionale
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
p = Path(path)
|
||||||
|
try:
|
||||||
|
data = json.loads(p.read_text(encoding="utf-8"))
|
||||||
|
except FileNotFoundError as e:
|
||||||
|
raise PromptLibraryError(f"file non trovato: {p}") from e
|
||||||
|
except json.JSONDecodeError as e:
|
||||||
|
raise PromptLibraryError(f"JSON malformato in {p}: {e}") from e
|
||||||
|
|
||||||
|
if not isinstance(data, dict):
|
||||||
|
raise PromptLibraryError(f"root JSON deve essere un object, non {type(data)}")
|
||||||
|
styles_raw = data.get("styles")
|
||||||
|
if not isinstance(styles_raw, dict):
|
||||||
|
raise PromptLibraryError("manca chiave 'styles' (object) nel JSON")
|
||||||
|
|
||||||
|
styles: dict[str, str] = {}
|
||||||
|
focus: dict[str, list[str]] = {}
|
||||||
|
for name, entry in styles_raw.items():
|
||||||
|
if not isinstance(entry, dict):
|
||||||
|
raise PromptLibraryError(
|
||||||
|
f"entry per stile {name!r} deve essere object, non {type(entry)}"
|
||||||
|
)
|
||||||
|
directive = entry.get("directive")
|
||||||
|
if not isinstance(directive, str):
|
||||||
|
raise PromptLibraryError(
|
||||||
|
f"manca 'directive' (string) per stile {name!r}"
|
||||||
|
)
|
||||||
|
styles[name] = directive
|
||||||
|
fm = entry.get("focus_metrics", [])
|
||||||
|
if not isinstance(fm, list):
|
||||||
|
raise PromptLibraryError(
|
||||||
|
f"focus_metrics di {name!r} deve essere lista, non {type(fm)}"
|
||||||
|
)
|
||||||
|
focus[name] = [str(x) for x in fm]
|
||||||
|
|
||||||
|
# Parse new optional top-level fields (v3.0)
|
||||||
|
agent_role = data.get("agent_role", "")
|
||||||
|
pattern_guidance = data.get("pattern_guidance", "")
|
||||||
|
instruction = data.get("instruction", "")
|
||||||
|
domain_warnings = data.get("domain_warnings", "")
|
||||||
|
# Parse new optional top-level fields (v3.1)
|
||||||
|
anti_patterns_raw = data.get("anti_patterns", "")
|
||||||
|
output_priorities_raw = data.get("output_priorities", "")
|
||||||
|
if not isinstance(anti_patterns_raw, str):
|
||||||
|
raise PromptLibraryError(f"anti_patterns deve essere stringa, non {type(anti_patterns_raw)}")
|
||||||
|
if not isinstance(output_priorities_raw, str):
|
||||||
|
raise PromptLibraryError(f"output_priorities deve essere stringa, non {type(output_priorities_raw)}")
|
||||||
|
|
||||||
|
return cls(
|
||||||
|
styles=styles,
|
||||||
|
focus=focus,
|
||||||
|
agent_role=agent_role,
|
||||||
|
pattern_guidance=pattern_guidance,
|
||||||
|
instruction=instruction,
|
||||||
|
domain_warnings=domain_warnings,
|
||||||
|
anti_patterns=anti_patterns_raw,
|
||||||
|
output_priorities=output_priorities_raw,
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def cognitive_styles(self) -> tuple[str, ...]:
|
||||||
|
"""Tupla immutabile dei nomi degli stili, nell'ordine di insertion del JSON."""
|
||||||
|
return tuple(self.styles)
|
||||||
|
|
||||||
|
def directive(self, style: str) -> str:
|
||||||
|
"""Ritorna la directive di ``style`` o solleva KeyError."""
|
||||||
|
return self.styles[style]
|
||||||
|
|
||||||
|
def style_at(self, index: int) -> str:
|
||||||
|
"""Round-robin: ``cognitive_styles[index % len()]``."""
|
||||||
|
return self.cognitive_styles[index % len(self.cognitive_styles)]
|
||||||
|
|
||||||
|
def focus_metrics_for(self, style: str) -> list[str]:
|
||||||
|
"""Ritorna lista delle metriche prioritarie per ``style``. Empty list se non specificate."""
|
||||||
|
return self.focus.get(style, [])
|
||||||
@@ -4,6 +4,122 @@ import numpy as np
|
|||||||
import pandas as pd # type: ignore[import-untyped]
|
import pandas as pd # type: ignore[import-untyped]
|
||||||
|
|
||||||
|
|
||||||
|
def autocorr_lag1(returns: pd.Series) -> float:
|
||||||
|
"""Autocorrelazione dei ritorni con lag 1 (correlazione di Pearson)."""
|
||||||
|
if len(returns) < 2:
|
||||||
|
return 0.0
|
||||||
|
val = returns.autocorr(lag=1)
|
||||||
|
return float(val) if pd.notna(val) else 0.0
|
||||||
|
|
||||||
|
|
||||||
|
def hurst_exponent(returns: pd.Series) -> float:
|
||||||
|
"""Hurst exponent via R/S analysis (rescaled range).
|
||||||
|
|
||||||
|
Range 0-1: 0.5 random walk, >0.55 trending persistente, <0.45 mean reverting.
|
||||||
|
Implementazione classica con scale multiple (2^k bins).
|
||||||
|
"""
|
||||||
|
n = len(returns)
|
||||||
|
if n < 100:
|
||||||
|
return 0.5 # insufficiente dati, ritorna random-walk default
|
||||||
|
|
||||||
|
series = returns.dropna().values
|
||||||
|
n = len(series)
|
||||||
|
if n < 100:
|
||||||
|
return 0.5
|
||||||
|
|
||||||
|
# Scale: 2^k che dividono n in segmenti
|
||||||
|
scales = [2**k for k in range(4, int(np.log2(n // 2)) + 1)]
|
||||||
|
if not scales:
|
||||||
|
return 0.5
|
||||||
|
|
||||||
|
rs_values: list[float] = []
|
||||||
|
for scale in scales:
|
||||||
|
n_chunks = n // scale
|
||||||
|
if n_chunks < 1:
|
||||||
|
continue
|
||||||
|
rs_chunk: list[float] = []
|
||||||
|
for i in range(n_chunks):
|
||||||
|
chunk = series[i * scale : (i + 1) * scale]
|
||||||
|
mean = chunk.mean()
|
||||||
|
cumdev = (chunk - mean).cumsum()
|
||||||
|
r = cumdev.max() - cumdev.min()
|
||||||
|
s = chunk.std()
|
||||||
|
if s > 0:
|
||||||
|
rs_chunk.append(r / s)
|
||||||
|
if rs_chunk:
|
||||||
|
rs_values.append(float(np.mean(rs_chunk)))
|
||||||
|
|
||||||
|
if len(rs_values) < 2:
|
||||||
|
return 0.5
|
||||||
|
|
||||||
|
log_scales = np.log(scales[: len(rs_values)])
|
||||||
|
log_rs = np.log(rs_values)
|
||||||
|
# Hurst = slope della regressione log-log
|
||||||
|
h, _ = np.polyfit(log_scales, log_rs, 1)
|
||||||
|
return float(np.clip(h, 0.0, 1.0))
|
||||||
|
|
||||||
|
|
||||||
|
def vol_percentile_historical(
|
||||||
|
returns: pd.Series,
|
||||||
|
current_window: int = 24,
|
||||||
|
ref_window: int = 2000,
|
||||||
|
) -> float:
|
||||||
|
"""Percentile (0-100) della vol corrente nella distribuzione storica.
|
||||||
|
|
||||||
|
Vol = std rolling su current_window barre. Confronta l'ultimo valore contro
|
||||||
|
la distribuzione dei valori della stessa std rolling sugli ultimi ref_window.
|
||||||
|
Output: 0 (vol attuale tra le piu basse), 100 (tra le piu alte).
|
||||||
|
"""
|
||||||
|
if len(returns) < max(current_window, 100):
|
||||||
|
return 50.0
|
||||||
|
|
||||||
|
rolling_vol = returns.rolling(current_window, min_periods=current_window).std()
|
||||||
|
rolling_vol = rolling_vol.dropna()
|
||||||
|
if len(rolling_vol) < 10:
|
||||||
|
return 50.0
|
||||||
|
|
||||||
|
# Limita ref_window all'effettiva disponibilita
|
||||||
|
ref = rolling_vol.iloc[-ref_window:] if len(rolling_vol) > ref_window else rolling_vol
|
||||||
|
current = float(rolling_vol.iloc[-1])
|
||||||
|
pct = float((ref < current).sum()) / len(ref) * 100.0
|
||||||
|
return pct
|
||||||
|
|
||||||
|
|
||||||
|
def seasonality_strength(
|
||||||
|
returns: pd.Series,
|
||||||
|
by: str,
|
||||||
|
) -> float:
|
||||||
|
"""Frazione di varianza dei ritorni spiegata dalla feature temporale `by`.
|
||||||
|
|
||||||
|
`by` in {"hour", "dow"}. Output 0-1: 0 = no seasonality, 1 = tutta la varianza
|
||||||
|
e dovuta al ciclo. Calcolato come 1 - (var residua / var totale) usando i
|
||||||
|
gruppi indotti dalla feature.
|
||||||
|
"""
|
||||||
|
if not isinstance(returns.index, pd.DatetimeIndex):
|
||||||
|
return 0.0
|
||||||
|
if len(returns) < 50:
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
if by == "hour":
|
||||||
|
groups = returns.index.hour
|
||||||
|
elif by == "dow":
|
||||||
|
groups = returns.index.dayofweek
|
||||||
|
else:
|
||||||
|
raise ValueError(f"by deve essere 'hour' o 'dow', non {by!r}")
|
||||||
|
|
||||||
|
total_var = float(returns.var())
|
||||||
|
if total_var <= 0:
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
grouped = returns.groupby(groups)
|
||||||
|
group_means = grouped.transform("mean")
|
||||||
|
residuals = returns - group_means
|
||||||
|
residual_var = float(residuals.var())
|
||||||
|
|
||||||
|
explained = 1.0 - (residual_var / total_var)
|
||||||
|
return float(np.clip(explained, 0.0, 1.0))
|
||||||
|
|
||||||
|
|
||||||
def sharpe_ratio(returns: pd.Series, periods_per_year: int = 8760, rf: float = 0.0) -> float:
|
def sharpe_ratio(returns: pd.Series, periods_per_year: int = 8760, rf: float = 0.0) -> float:
|
||||||
"""Sharpe annualizzato. periods_per_year=8760 per dati orari."""
|
"""Sharpe annualizzato. periods_per_year=8760 per dati orari."""
|
||||||
excess = returns - rf / periods_per_year
|
excess = returns - rf / periods_per_year
|
||||||
@@ -25,3 +141,135 @@ def total_return(equity: pd.Series) -> float:
|
|||||||
if equity.iloc[0] == 0:
|
if equity.iloc[0] == 0:
|
||||||
return float(equity.iloc[-1])
|
return float(equity.iloc[-1])
|
||||||
return float(equity.iloc[-1] / equity.iloc[0] - 1.0)
|
return float(equity.iloc[-1] / equity.iloc[0] - 1.0)
|
||||||
|
|
||||||
|
|
||||||
|
def efficiency_ratio_kaufman(prices: pd.Series, length: int = 100) -> float:
|
||||||
|
"""Kaufman Efficiency Ratio: |net move| / sum(|step|) su rolling length.
|
||||||
|
|
||||||
|
Output 0-1: 0 = puro noise (movimento dissipativo), 1 = puro trend efficiente.
|
||||||
|
Discriminatore trending/ranging robusto.
|
||||||
|
"""
|
||||||
|
if len(prices) < length + 1:
|
||||||
|
return 0.0
|
||||||
|
recent = prices.iloc[-length:]
|
||||||
|
net_move = abs(recent.iloc[-1] - recent.iloc[0])
|
||||||
|
total_move = recent.diff().abs().sum()
|
||||||
|
if total_move == 0 or pd.isna(total_move):
|
||||||
|
return 0.0
|
||||||
|
return float(net_move / total_move)
|
||||||
|
|
||||||
|
|
||||||
|
def tail_index_hill(returns: pd.Series, side: str, top_frac: float = 0.05) -> float:
|
||||||
|
"""Hill estimator del tail index (pendenza coda) per side in {'left', 'right'}.
|
||||||
|
|
||||||
|
Output: indice di pesantezza coda. Valori piu bassi = coda piu pesante.
|
||||||
|
<2 = varianza infinita (Cauchy-like), 3-4 = tipico crypto, >5 = quasi-Gaussiana.
|
||||||
|
Robusto al singolo outlier (vs kurtosis).
|
||||||
|
"""
|
||||||
|
r = returns.dropna()
|
||||||
|
n = len(r)
|
||||||
|
if n < 50:
|
||||||
|
return 5.0 # default quasi-Gaussiano
|
||||||
|
k = max(int(n * top_frac), 5)
|
||||||
|
if side == "left":
|
||||||
|
tail = (-r).nlargest(k)
|
||||||
|
elif side == "right":
|
||||||
|
tail = r.nlargest(k)
|
||||||
|
else:
|
||||||
|
raise ValueError(f"side deve essere 'left' o 'right', non {side!r}")
|
||||||
|
# Filtra valori non-positivi (Hill richiede tail positiva)
|
||||||
|
tail = tail[tail > 0]
|
||||||
|
if len(tail) < 5:
|
||||||
|
return 5.0
|
||||||
|
log_tail = np.log(tail.values)
|
||||||
|
threshold = log_tail[-1]
|
||||||
|
excess = log_tail[:-1] - threshold
|
||||||
|
if excess.sum() <= 0:
|
||||||
|
return 5.0
|
||||||
|
hill = (len(excess)) / excess.sum()
|
||||||
|
return float(np.clip(hill, 1.0, 10.0))
|
||||||
|
|
||||||
|
|
||||||
|
def structural_uptrend_score(prices: pd.Series, window: int = 5) -> float:
|
||||||
|
"""Frazione di periodi in struttura HH/HL (Dow-style uptrend).
|
||||||
|
|
||||||
|
Identifica swing high/low usando rolling max/min con finestra ``window``.
|
||||||
|
Conta sequenze higher-high + higher-low (uptrend) vs lower-high + lower-low (downtrend).
|
||||||
|
Output: 0 (puro downtrend) - 0.5 (range/mixed) - 1 (puro uptrend).
|
||||||
|
"""
|
||||||
|
if len(prices) < 4 * window:
|
||||||
|
return 0.5
|
||||||
|
swing_high = prices.rolling(2 * window + 1, center=True).max() == prices
|
||||||
|
swing_low = prices.rolling(2 * window + 1, center=True).min() == prices
|
||||||
|
|
||||||
|
highs = prices[swing_high].dropna()
|
||||||
|
lows = prices[swing_low].dropna()
|
||||||
|
|
||||||
|
if len(highs) < 3 or len(lows) < 3:
|
||||||
|
return 0.5
|
||||||
|
|
||||||
|
hh = (highs.diff() > 0).sum()
|
||||||
|
lh = (highs.diff() < 0).sum()
|
||||||
|
hl = (lows.diff() > 0).sum()
|
||||||
|
ll = (lows.diff() < 0).sum()
|
||||||
|
|
||||||
|
up_signals = hh + hl
|
||||||
|
down_signals = lh + ll
|
||||||
|
total = up_signals + down_signals
|
||||||
|
if total == 0:
|
||||||
|
return 0.5
|
||||||
|
return float(up_signals / total)
|
||||||
|
|
||||||
|
|
||||||
|
def compression_ratio(prices: pd.Series, recent_window: int = 50, ref_window: int = 200) -> float:
|
||||||
|
"""Range(recent) / Range(ref). <1 = compressione vol, >1 = espansione.
|
||||||
|
|
||||||
|
Range = high - low della finestra. Cattura il "coil" pre-breakout.
|
||||||
|
"""
|
||||||
|
if len(prices) < ref_window:
|
||||||
|
return 1.0
|
||||||
|
recent = prices.iloc[-recent_window:]
|
||||||
|
ref = prices.iloc[-ref_window:]
|
||||||
|
recent_range = float(recent.max() - recent.min())
|
||||||
|
ref_range = float(ref.max() - ref.min())
|
||||||
|
if ref_range <= 0:
|
||||||
|
return 1.0
|
||||||
|
return recent_range / ref_range
|
||||||
|
|
||||||
|
|
||||||
|
def spectral_entropy_and_cycle(
|
||||||
|
returns: pd.Series, length: int = 256
|
||||||
|
) -> tuple[float, float | None]:
|
||||||
|
"""FFT su returns -> (entropy normalizzata, dominant_cycle gated).
|
||||||
|
|
||||||
|
entropy: 0-1, Shannon entropy normalizzata dello spettro di potenza.
|
||||||
|
0 = una sola frequenza domina, 1 = spettro piatto (rumore bianco).
|
||||||
|
dominant_cycle: periodo (barre) della frequenza dominante,
|
||||||
|
MA solo se entropy < 0.6 (struttura presente). Altrimenti None.
|
||||||
|
"""
|
||||||
|
r = returns.dropna()
|
||||||
|
if len(r) < length:
|
||||||
|
return 1.0, None
|
||||||
|
series = r.iloc[-length:].values
|
||||||
|
# Detrend
|
||||||
|
series = series - series.mean()
|
||||||
|
fft = np.fft.rfft(series)
|
||||||
|
power = np.abs(fft) ** 2
|
||||||
|
if power.sum() <= 0:
|
||||||
|
return 1.0, None
|
||||||
|
p = power / power.sum()
|
||||||
|
# Entropy normalizzata (Shannon / log(N))
|
||||||
|
nonzero = p[p > 0]
|
||||||
|
entropy = float(-(nonzero * np.log(nonzero)).sum() / np.log(len(nonzero)))
|
||||||
|
entropy = max(0.0, min(1.0, entropy))
|
||||||
|
|
||||||
|
if entropy >= 0.6:
|
||||||
|
return entropy, None
|
||||||
|
# Skip DC component (index 0)
|
||||||
|
if len(power) <= 1:
|
||||||
|
return entropy, None
|
||||||
|
peak_idx = int(np.argmax(power[1:])) + 1
|
||||||
|
if peak_idx == 0:
|
||||||
|
return entropy, None
|
||||||
|
cycle = float(length / peak_idx)
|
||||||
|
return entropy, cycle
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ possa leggere lo stato a run terminato (o in corso).
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import random
|
import random
|
||||||
|
from concurrent.futures import ThreadPoolExecutor
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
@@ -20,13 +21,15 @@ import pandas as pd # type: ignore[import-untyped]
|
|||||||
|
|
||||||
from ..agents.adversarial import AdversarialAgent
|
from ..agents.adversarial import AdversarialAgent
|
||||||
from ..agents.falsification import FalsificationAgent
|
from ..agents.falsification import FalsificationAgent
|
||||||
from ..agents.hypothesis import HypothesisAgent
|
from ..agents.hypothesis import HypothesisAgent, HypothesisProposal, MarketSummary
|
||||||
from ..agents.market_summary import build_market_summary
|
from ..agents.market_summary import build_market_summary
|
||||||
from ..ga.fitness import compute_fitness
|
from ..ga.fitness import compute_fitness
|
||||||
from ..ga.initial import build_initial_population
|
from ..ga.initial import build_initial_population
|
||||||
from ..ga.loop import GAConfig, next_generation
|
from ..ga.loop import GAConfig, next_generation
|
||||||
from ..ga.summary import generation_summary
|
from ..ga.summary import generation_summary
|
||||||
from ..genome.hypothesis import ModelTier
|
from ..genome.hypothesis import HypothesisAgentGenome, ModelTier
|
||||||
|
from ..genome.mutation import set_cognitive_styles
|
||||||
|
from ..genome.prompt_library import PromptLibrary
|
||||||
from ..llm.client import LLMClient
|
from ..llm.client import LLMClient
|
||||||
from ..llm.cost_tracker import CostTracker
|
from ..llm.cost_tracker import CostTracker
|
||||||
from ..persistence.repository import Repository
|
from ..persistence.repository import Repository
|
||||||
@@ -67,6 +70,33 @@ class RunConfig:
|
|||||||
# 2x costo backtest engine.
|
# 2x costo backtest engine.
|
||||||
eval_oos_during_loop: bool = False
|
eval_oos_during_loop: bool = False
|
||||||
fitness_combined_alpha: float = 0.5 # peso IS (1-alpha = OOS)
|
fitness_combined_alpha: float = 0.5 # peso IS (1-alpha = OOS)
|
||||||
|
# Libreria di stili cognitivi + system_prompt iniziali. Se None usa
|
||||||
|
# i 6 builtin (PromptLibrary.default()). Tipicamente caricata da
|
||||||
|
# strategy_crypto/prompts.json via PromptLibrary.from_json().
|
||||||
|
prompt_library: PromptLibrary | None = None
|
||||||
|
# Numero di propose() LLM concorrenti per generazione. 1 = sequenziale (default,
|
||||||
|
# backward compat). 6-10 tipicamente accettati da OpenRouter qwen-2.5 senza
|
||||||
|
# rate-limit. Riduce wall time GA loop di 5-8x su tier C.
|
||||||
|
llm_concurrency: int = 1
|
||||||
|
|
||||||
|
|
||||||
|
def _parallel_propose(
|
||||||
|
agent: HypothesisAgent,
|
||||||
|
genomes: list[HypothesisAgentGenome],
|
||||||
|
market: MarketSummary,
|
||||||
|
n_workers: int,
|
||||||
|
) -> list[HypothesisProposal]:
|
||||||
|
"""Esegue ``agent.propose()`` su una lista di genomi, opzionalmente in parallelo.
|
||||||
|
|
||||||
|
``n_workers <= 1`` mantiene il comportamento serial originale (ordine fisso,
|
||||||
|
determinismo data un seed). ``n_workers > 1`` usa un thread pool: l'order
|
||||||
|
dei risultati e' preservato (1:1 con ``genomes``). OpenAI/openrouter client
|
||||||
|
e' thread-safe; ``PromptLibrary`` e ``HypothesisAgent`` non hanno stato mutabile.
|
||||||
|
"""
|
||||||
|
if n_workers <= 1 or len(genomes) <= 1:
|
||||||
|
return [agent.propose(g, market) for g in genomes]
|
||||||
|
with ThreadPoolExecutor(max_workers=n_workers) as pool:
|
||||||
|
return list(pool.map(lambda g: agent.propose(g, market), genomes))
|
||||||
|
|
||||||
|
|
||||||
def run_phase1(
|
def run_phase1(
|
||||||
@@ -82,10 +112,16 @@ def run_phase1(
|
|||||||
|
|
||||||
repo = Repository(cfg.db_path)
|
repo = Repository(cfg.db_path)
|
||||||
repo.init_schema()
|
repo.init_schema()
|
||||||
|
# Escludi prompt_library (PromptLibrary dataclass non e' JSON-serializable);
|
||||||
|
# salva solo i nomi degli stili per reproducibility.
|
||||||
config_dict = {
|
config_dict = {
|
||||||
**cfg.__dict__,
|
**{k: v for k, v in cfg.__dict__.items() if k != "prompt_library"},
|
||||||
"db_path": str(cfg.db_path),
|
"db_path": str(cfg.db_path),
|
||||||
"model_tier": cfg.model_tier.value,
|
"model_tier": cfg.model_tier.value,
|
||||||
|
"prompt_library_styles": (
|
||||||
|
list(cfg.prompt_library.cognitive_styles)
|
||||||
|
if cfg.prompt_library is not None else None
|
||||||
|
),
|
||||||
}
|
}
|
||||||
run_id = repo.create_run(name=cfg.run_name, config=config_dict)
|
run_id = repo.create_run(name=cfg.run_name, config=config_dict)
|
||||||
|
|
||||||
@@ -100,7 +136,13 @@ def run_phase1(
|
|||||||
|
|
||||||
market = build_market_summary(train_ohlcv, symbol=cfg.symbol, timeframe=cfg.timeframe)
|
market = build_market_summary(train_ohlcv, symbol=cfg.symbol, timeframe=cfg.timeframe)
|
||||||
|
|
||||||
hypothesis_agent = HypothesisAgent(llm=llm)
|
# Propaga la libreria di stili al modulo mutation (cosi' mutate_cognitive_style
|
||||||
|
# pesca dai candidati coerenti col JSON della strategia in corso). Va FATTO
|
||||||
|
# PRIMA di istanziare HypothesisAgent (che la riceve in costruttore).
|
||||||
|
prompt_library = cfg.prompt_library or PromptLibrary.default()
|
||||||
|
set_cognitive_styles(prompt_library.cognitive_styles)
|
||||||
|
|
||||||
|
hypothesis_agent = HypothesisAgent(llm=llm, prompt_library=prompt_library)
|
||||||
falsification_agent = FalsificationAgent(
|
falsification_agent = FalsificationAgent(
|
||||||
fees_bp=cfg.fees_bp, n_trials_dsr=cfg.n_trials_dsr
|
fees_bp=cfg.fees_bp, n_trials_dsr=cfg.n_trials_dsr
|
||||||
)
|
)
|
||||||
@@ -113,7 +155,10 @@ def run_phase1(
|
|||||||
cost_tracker = CostTracker()
|
cost_tracker = CostTracker()
|
||||||
|
|
||||||
population = build_initial_population(
|
population = build_initial_population(
|
||||||
k=cfg.population_size, model_tier=cfg.model_tier, rng=rng
|
k=cfg.population_size,
|
||||||
|
model_tier=cfg.model_tier,
|
||||||
|
rng=rng,
|
||||||
|
prompt_library=prompt_library,
|
||||||
)
|
)
|
||||||
fitnesses: dict[str, float] = {}
|
fitnesses: dict[str, float] = {}
|
||||||
|
|
||||||
@@ -127,11 +172,20 @@ def run_phase1(
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
for gen in range(cfg.n_generations):
|
for gen in range(cfg.n_generations):
|
||||||
|
# Step 1: raccogli i genomi da valutare in questa generazione (esclude
|
||||||
|
# elite gia' presenti nella cache fitnesses) e lancia propose() in
|
||||||
|
# parallelo. La sezione DB-write resta serial sotto.
|
||||||
|
uncached = [g for g in population if g.id not in fitnesses]
|
||||||
|
proposals = _parallel_propose(
|
||||||
|
hypothesis_agent, uncached, market, cfg.llm_concurrency
|
||||||
|
)
|
||||||
|
proposal_by_id = {g.id: p for g, p in zip(uncached, proposals, strict=True)}
|
||||||
|
|
||||||
for genome in population:
|
for genome in population:
|
||||||
if genome.id in fitnesses:
|
if genome.id in fitnesses:
|
||||||
continue # elite gia' valutata in generazione precedente
|
continue # elite gia' valutata in generazione precedente
|
||||||
repo.save_genome(run_id=run_id, generation_idx=gen, genome=genome)
|
repo.save_genome(run_id=run_id, generation_idx=gen, genome=genome)
|
||||||
proposal = hypothesis_agent.propose(genome, market)
|
proposal = proposal_by_id[genome.id]
|
||||||
# Registra costo per OGNI completion (incluse retry).
|
# Registra costo per OGNI completion (incluse retry).
|
||||||
for completion in proposal.completions:
|
for completion in proposal.completions:
|
||||||
cost_record = cost_tracker.record(
|
cost_record = cost_tracker.record(
|
||||||
@@ -205,7 +259,7 @@ def run_phase1(
|
|||||||
cfg.fitness_combined_alpha * fit
|
cfg.fitness_combined_alpha * fit
|
||||||
+ (1.0 - cfg.fitness_combined_alpha) * fit_oos_inloop
|
+ (1.0 - cfg.fitness_combined_alpha) * fit_oos_inloop
|
||||||
)
|
)
|
||||||
except Exception: # noqa: BLE001
|
except Exception:
|
||||||
pass # fallback: usa solo IS
|
pass # fallback: usa solo IS
|
||||||
repo.save_evaluation(
|
repo.save_evaluation(
|
||||||
run_id=run_id,
|
run_id=run_id,
|
||||||
@@ -246,7 +300,7 @@ def run_phase1(
|
|||||||
# WFA re-eval: i top_k genomi (by fitness in-sample > 0) vengono rivalutati
|
# WFA re-eval: i top_k genomi (by fitness in-sample > 0) vengono rivalutati
|
||||||
# sul test_ohlcv. Le metriche OOS finiscono in evaluations.fitness_oos etc.
|
# sul test_ohlcv. Le metriche OOS finiscono in evaluations.fitness_oos etc.
|
||||||
if test_ohlcv is not None and len(test_ohlcv) >= 100:
|
if test_ohlcv is not None and len(test_ohlcv) >= 100:
|
||||||
from ..agents.hypothesis import _try_parse # noqa: PLC0415
|
from ..agents.hypothesis import _try_parse
|
||||||
|
|
||||||
all_evals = repo.list_evaluations(run_id)
|
all_evals = repo.list_evaluations(run_id)
|
||||||
top_evals = sorted(
|
top_evals = sorted(
|
||||||
@@ -261,7 +315,7 @@ def run_phase1(
|
|||||||
try:
|
try:
|
||||||
fals_oos = falsification_agent.evaluate(strategy, test_ohlcv)
|
fals_oos = falsification_agent.evaluate(strategy, test_ohlcv)
|
||||||
adv_oos = adversarial_agent.review(strategy, test_ohlcv)
|
adv_oos = adversarial_agent.review(strategy, test_ohlcv)
|
||||||
except Exception: # noqa: BLE001
|
except Exception:
|
||||||
continue
|
continue
|
||||||
fit_oos = compute_fitness(
|
fit_oos = compute_fitness(
|
||||||
fals_oos, adv_oos,
|
fals_oos, adv_oos,
|
||||||
|
|||||||
@@ -88,6 +88,29 @@ def _ind_atr_pct(df: pd.DataFrame, length: float) -> pd.Series:
|
|||||||
return _atr(df, int(length)) / df["close"]
|
return _atr(df, int(length)) / df["close"]
|
||||||
|
|
||||||
|
|
||||||
|
def _ind_sma_pct(df: pd.DataFrame, length: float) -> pd.Series:
|
||||||
|
# Deviazione frazionale del close dalla SMA: (close - sma) / sma.
|
||||||
|
# Range tipico +/- 0.1 (close +/- 10% dalla media). Uso ideale:
|
||||||
|
# sma_pct(50) > 0.05 -> "close 5% sopra la media a 50 barre"
|
||||||
|
# NB: non e' "sma/close" perche' quel valore (sempre ~1.0) e' inutile
|
||||||
|
# per confronti con literal frazionali.
|
||||||
|
sma = _sma(df["close"], int(length))
|
||||||
|
return (df["close"] - sma) / sma
|
||||||
|
|
||||||
|
|
||||||
|
def _ind_macd_pct(
|
||||||
|
df: pd.DataFrame,
|
||||||
|
fast: float = 12,
|
||||||
|
slow: float = 26,
|
||||||
|
signal: float = 9,
|
||||||
|
) -> pd.Series:
|
||||||
|
# MACD normalizzato come frazione del prezzo close: macd_value / close.
|
||||||
|
# Range tipico +/- 0.02. Uso: `macd_pct > 0` (momentum positivo) o
|
||||||
|
# `macd_pct > 0.005` (momentum positivo >= 0.5% del prezzo).
|
||||||
|
macd = _ind_macd(df, fast, slow, signal)
|
||||||
|
return macd / df["close"]
|
||||||
|
|
||||||
|
|
||||||
def _ind_realized_vol(df: pd.DataFrame, window: float) -> pd.Series:
|
def _ind_realized_vol(df: pd.DataFrame, window: float) -> pd.Series:
|
||||||
return _realized_vol(df["close"], int(window))
|
return _realized_vol(df["close"], int(window))
|
||||||
|
|
||||||
@@ -109,11 +132,13 @@ def _ind_macd(
|
|||||||
# against this map.
|
# against this map.
|
||||||
INDICATOR_FNS: dict[str, Any] = {
|
INDICATOR_FNS: dict[str, Any] = {
|
||||||
"sma": _ind_sma,
|
"sma": _ind_sma,
|
||||||
|
"sma_pct": _ind_sma_pct,
|
||||||
"rsi": _ind_rsi,
|
"rsi": _ind_rsi,
|
||||||
"atr": _ind_atr,
|
"atr": _ind_atr,
|
||||||
"atr_pct": _ind_atr_pct,
|
"atr_pct": _ind_atr_pct,
|
||||||
"realized_vol": _ind_realized_vol,
|
"realized_vol": _ind_realized_vol,
|
||||||
"macd": _ind_macd,
|
"macd": _ind_macd,
|
||||||
|
"macd_pct": _ind_macd_pct,
|
||||||
}
|
}
|
||||||
|
|
||||||
_TIME_FEATURE_FNS: dict[str, Callable[[pd.DatetimeIndex], pd.Series]] = {
|
_TIME_FEATURE_FNS: dict[str, Callable[[pd.DatetimeIndex], pd.Series]] = {
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ ACTION_VALUES: frozenset[str] = frozenset(
|
|||||||
KIND_VALUES: frozenset[str] = frozenset({"indicator", "feature", "literal"})
|
KIND_VALUES: frozenset[str] = frozenset({"indicator", "feature", "literal"})
|
||||||
|
|
||||||
KNOWN_INDICATORS: frozenset[str] = frozenset(
|
KNOWN_INDICATORS: frozenset[str] = frozenset(
|
||||||
{"sma", "rsi", "atr", "atr_pct", "macd", "realized_vol"}
|
{"sma", "sma_pct", "rsi", "atr", "atr_pct", "macd", "macd_pct", "realized_vol"}
|
||||||
)
|
)
|
||||||
KNOWN_FEATURES: frozenset[str] = frozenset(
|
KNOWN_FEATURES: frozenset[str] = frozenset(
|
||||||
{"open", "high", "low", "close", "volume",
|
{"open", "high", "low", "close", "volume",
|
||||||
|
|||||||
@@ -31,12 +31,14 @@ from .parser import (
|
|||||||
# Numero di parametri numerici accettati dopo il nome dell'indicatore.
|
# Numero di parametri numerici accettati dopo il nome dell'indicatore.
|
||||||
# (min, max) sui soli numeri. Indicatori non sono annidabili in Phase 1.
|
# (min, max) sui soli numeri. Indicatori non sono annidabili in Phase 1.
|
||||||
INDICATOR_ARITY: dict[str, tuple[int, int]] = {
|
INDICATOR_ARITY: dict[str, tuple[int, int]] = {
|
||||||
"sma": (1, 1), # length
|
"sma": (1, 1), # length (assoluto, unita' prezzo)
|
||||||
|
"sma_pct": (1, 1), # length: (close - sma)/sma, deviazione frazionale
|
||||||
"rsi": (1, 1), # length
|
"rsi": (1, 1), # length
|
||||||
"atr": (1, 1), # length (assoluto, unita' prezzo)
|
"atr": (1, 1), # length (assoluto, unita' prezzo)
|
||||||
"atr_pct": (1, 1), # length (frazione del close, per confronti con literal)
|
"atr_pct": (1, 1), # length (frazione del close, per confronti con literal)
|
||||||
"realized_vol": (1, 1), # window
|
"realized_vol": (1, 1), # window
|
||||||
"macd": (0, 3), # fast, slow, signal (tutti opzionali)
|
"macd": (0, 3), # fast, slow, signal (tutti opzionali)
|
||||||
|
"macd_pct": (0, 3), # macd/close, frazionale (per confronti con literal)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ dependencies = [
|
|||||||
"pyyaml>=6.0",
|
"pyyaml>=6.0",
|
||||||
"pyarrow>=18.0",
|
"pyarrow>=18.0",
|
||||||
"yfinance>=1.3.0",
|
"yfinance>=1.3.0",
|
||||||
|
"nicegui>=3.11.1",
|
||||||
|
"plotly>=5.24",
|
||||||
]
|
]
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
|
|||||||
@@ -108,7 +108,13 @@ def test_e2e_wfa_populates_fitness_oos(
|
|||||||
fake_llm,
|
fake_llm,
|
||||||
mocker,
|
mocker,
|
||||||
):
|
):
|
||||||
"""WFA: train_split=0.7 → top genomi devono avere fitness_oos popolato."""
|
"""WFA: train_split=0.7 → top genomi devono avere fitness_oos popolato.
|
||||||
|
|
||||||
|
Usa fitness v2 con hard-kill minimale (solo no_trades): il fixture sintetico
|
||||||
|
non produce strategie profittevoli, quindi i check aggressivi
|
||||||
|
fees_eat_alpha/negative_net_pnl azzererebbero tutti i genomi rendendo
|
||||||
|
inverificabile il wiring WFA.
|
||||||
|
"""
|
||||||
cfg = RunConfig(
|
cfg = RunConfig(
|
||||||
run_name="e2e-wfa-test",
|
run_name="e2e-wfa-test",
|
||||||
population_size=5,
|
population_size=5,
|
||||||
@@ -125,6 +131,7 @@ def test_e2e_wfa_populates_fitness_oos(
|
|||||||
db_path=tmp_path / "runs.db",
|
db_path=tmp_path / "runs.db",
|
||||||
wfa_train_split=0.7,
|
wfa_train_split=0.7,
|
||||||
wfa_top_k=3,
|
wfa_top_k=3,
|
||||||
|
fitness_hard_kill_findings=("no_trades",),
|
||||||
)
|
)
|
||||||
run_id = run_phase1(cfg, ohlcv=synthetic_ohlcv, llm=fake_llm)
|
run_id = run_phase1(cfg, ohlcv=synthetic_ohlcv, llm=fake_llm)
|
||||||
repo = Repository(db_path=tmp_path / "runs.db")
|
repo = Repository(db_path=tmp_path / "runs.db")
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import json
|
|||||||
import numpy as np
|
import numpy as np
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from multi_swarm_core.agents.adversarial import (
|
from multi_swarm_core.agents.adversarial import (
|
||||||
AdversarialAgent,
|
AdversarialAgent,
|
||||||
AdversarialReport,
|
AdversarialReport,
|
||||||
@@ -54,7 +53,10 @@ def test_degenerate_always_long_flagged(ohlcv: pd.DataFrame) -> None:
|
|||||||
assert any(f.name == "degenerate" and f.severity == Severity.HIGH for f in report.findings)
|
assert any(f.name == "degenerate" and f.severity == Severity.HIGH for f in report.findings)
|
||||||
|
|
||||||
|
|
||||||
def test_no_findings_on_reasonable_strategy(ohlcv: pd.DataFrame) -> None:
|
def test_rsi_mean_reversion_loses_money_on_synthetic_data(ohlcv: pd.DataFrame) -> None:
|
||||||
|
"""RSI mean-reversion sul fixture sintetico ha net negativo: deve firare
|
||||||
|
negative_net_pnl (deal-breaker). Conferma che il check cattura strategie
|
||||||
|
che perdono sul training, indipendentemente dal motivo (no edge / fees)."""
|
||||||
src = json.dumps(
|
src = json.dumps(
|
||||||
{
|
{
|
||||||
"rules": [
|
"rules": [
|
||||||
@@ -84,8 +86,59 @@ def test_no_findings_on_reasonable_strategy(ohlcv: pd.DataFrame) -> None:
|
|||||||
ast = parse_strategy(src)
|
ast = parse_strategy(src)
|
||||||
agent = AdversarialAgent()
|
agent = AdversarialAgent()
|
||||||
report = agent.review(ast, ohlcv)
|
report = agent.review(ast, ohlcv)
|
||||||
|
assert any(
|
||||||
|
f.name == "negative_net_pnl" and f.severity == Severity.HIGH
|
||||||
|
for f in report.findings
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_profitable_strategy_no_high_findings(
|
||||||
|
monkeypatch: pytest.MonkeyPatch, ohlcv: pd.DataFrame
|
||||||
|
) -> None:
|
||||||
|
"""Sanity test: una strategia con gross > 0 e fees << gross + n_trades ragionevole
|
||||||
|
+ signal misto non deve produrre nessun finding HIGH."""
|
||||||
|
n = 15
|
||||||
|
# entry=100 exit=110 gross=10 per trade, fees a 5bp -> 0.105 per trade
|
||||||
|
# totali: gross=150, fees=1.575 -> net=+148.4
|
||||||
|
fake_trades = [
|
||||||
|
_make_trade(
|
||||||
|
ohlcv.index[i * 30],
|
||||||
|
ohlcv.index[i * 30 + 1],
|
||||||
|
entry_price=100.0,
|
||||||
|
exit_price=110.0,
|
||||||
|
)
|
||||||
|
for i in range(n)
|
||||||
|
]
|
||||||
|
# 50/50 LONG/FLAT per evitare degenerate/flat_too_long/time_in_market.
|
||||||
|
fake_signals = pd.Series(
|
||||||
|
[Side.LONG if i % 2 == 0 else Side.FLAT for i in range(len(ohlcv))],
|
||||||
|
index=ohlcv.index,
|
||||||
|
dtype=object,
|
||||||
|
)
|
||||||
|
|
||||||
|
def fake_run(self, ohlcv: pd.DataFrame, signals: pd.Series) -> BacktestResult: # type: ignore[no-untyped-def]
|
||||||
|
return BacktestResult(
|
||||||
|
equity_curve=pd.Series([0.0] * len(ohlcv), index=ohlcv.index, name="equity"),
|
||||||
|
returns=pd.Series([0.0] * len(ohlcv), index=ohlcv.index, name="returns"),
|
||||||
|
trades=fake_trades,
|
||||||
|
)
|
||||||
|
|
||||||
|
def fake_compile(strategy): # type: ignore[no-untyped-def]
|
||||||
|
return lambda df: fake_signals
|
||||||
|
|
||||||
|
monkeypatch.setattr(
|
||||||
|
"multi_swarm_core.agents.adversarial.BacktestEngine.run", fake_run
|
||||||
|
)
|
||||||
|
monkeypatch.setattr(
|
||||||
|
"multi_swarm_core.agents.adversarial.compile_strategy", fake_compile
|
||||||
|
)
|
||||||
|
|
||||||
|
ast = parse_strategy(_MINIMAL_STRATEGY_SRC)
|
||||||
|
report = AdversarialAgent().review(ast, ohlcv)
|
||||||
high_findings = [f for f in report.findings if f.severity == Severity.HIGH]
|
high_findings = [f for f in report.findings if f.severity == Severity.HIGH]
|
||||||
assert len(high_findings) == 0
|
assert high_findings == [], (
|
||||||
|
f"expected no HIGH findings, got: {[f.name for f in high_findings]}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_zero_trade_strategy_flagged(ohlcv: pd.DataFrame) -> None:
|
def test_zero_trade_strategy_flagged(ohlcv: pd.DataFrame) -> None:
|
||||||
@@ -383,6 +436,55 @@ def test_fees_eat_alpha_flagged(monkeypatch: pytest.MonkeyPatch,
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_negative_net_pnl_fires_on_negative_gross(
|
||||||
|
monkeypatch: pytest.MonkeyPatch, ohlcv: pd.DataFrame
|
||||||
|
) -> None:
|
||||||
|
"""gross_pnl < 0 (perdente direzionale) -> HIGH negative_net_pnl.
|
||||||
|
fees_eat_alpha NON deve firare perche' la sua condizione richiede gross > 0.
|
||||||
|
"""
|
||||||
|
n = 15
|
||||||
|
# entry=100 exit=95 gross=-5 per trade (LONG perdente)
|
||||||
|
fake_trades = [
|
||||||
|
_make_trade(
|
||||||
|
ohlcv.index[i * 30],
|
||||||
|
ohlcv.index[i * 30 + 1],
|
||||||
|
entry_price=100.0,
|
||||||
|
exit_price=95.0,
|
||||||
|
)
|
||||||
|
for i in range(n)
|
||||||
|
]
|
||||||
|
fake_signals = pd.Series(
|
||||||
|
[Side.LONG if i % 2 == 0 else Side.FLAT for i in range(len(ohlcv))],
|
||||||
|
index=ohlcv.index,
|
||||||
|
dtype=object,
|
||||||
|
)
|
||||||
|
|
||||||
|
def fake_run(self, ohlcv, signals): # type: ignore[no-untyped-def]
|
||||||
|
return BacktestResult(
|
||||||
|
equity_curve=pd.Series([0.0] * len(ohlcv), index=ohlcv.index, name="equity"),
|
||||||
|
returns=pd.Series([0.0] * len(ohlcv), index=ohlcv.index, name="returns"),
|
||||||
|
trades=fake_trades,
|
||||||
|
)
|
||||||
|
|
||||||
|
def fake_compile(strategy): # type: ignore[no-untyped-def]
|
||||||
|
return lambda df: fake_signals
|
||||||
|
|
||||||
|
monkeypatch.setattr(
|
||||||
|
"multi_swarm_core.agents.adversarial.BacktestEngine.run", fake_run
|
||||||
|
)
|
||||||
|
monkeypatch.setattr(
|
||||||
|
"multi_swarm_core.agents.adversarial.compile_strategy", fake_compile
|
||||||
|
)
|
||||||
|
|
||||||
|
ast = parse_strategy(_MINIMAL_STRATEGY_SRC)
|
||||||
|
report = AdversarialAgent().review(ast, ohlcv)
|
||||||
|
assert any(
|
||||||
|
f.name == "negative_net_pnl" and f.severity == Severity.HIGH
|
||||||
|
for f in report.findings
|
||||||
|
)
|
||||||
|
assert not any(f.name == "fees_eat_alpha" for f in report.findings)
|
||||||
|
|
||||||
|
|
||||||
def test_time_in_market_too_high_flagged(monkeypatch: pytest.MonkeyPatch,
|
def test_time_in_market_too_high_flagged(monkeypatch: pytest.MonkeyPatch,
|
||||||
ohlcv: pd.DataFrame) -> None:
|
ohlcv: pd.DataFrame) -> None:
|
||||||
"""Signal LONG per >80% delle bar -> HIGH time_in_market_too_high."""
|
"""Signal LONG per >80% delle bar -> HIGH time_in_market_too_high."""
|
||||||
|
|||||||
@@ -0,0 +1,160 @@
|
|||||||
|
"""Parity check: engine vettorializzato vs reference iterrows implementation.
|
||||||
|
|
||||||
|
Mantiene una copia inline del loop ``iterrows`` come reference per garantire
|
||||||
|
che la vettorizzazione produca esattamente gli stessi trades, equity_curve e
|
||||||
|
returns su input pseudocasuali, indipendentemente dal regime di prezzo.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import pandas as pd
|
||||||
|
import pytest
|
||||||
|
from multi_swarm_core.backtest.engine import BacktestEngine, BacktestResult
|
||||||
|
from multi_swarm_core.backtest.orders import Position, Side, Trade
|
||||||
|
|
||||||
|
|
||||||
|
def _legacy_run(
|
||||||
|
ohlcv: pd.DataFrame, signals: pd.Series, fees_bp: float = 5.0
|
||||||
|
) -> BacktestResult:
|
||||||
|
"""Reference implementation: il loop iterrows originale (pre-vectorize).
|
||||||
|
|
||||||
|
Mantenuto qui esclusivamente come oracolo per i test di parità.
|
||||||
|
"""
|
||||||
|
signals = signals.reindex(ohlcv.index).ffill().fillna(Side.FLAT)
|
||||||
|
shifted = [Side.FLAT, *list(signals.iloc[:-1])]
|
||||||
|
executed_side = pd.Series(shifted, index=ohlcv.index, dtype=object)
|
||||||
|
|
||||||
|
position: Position | None = None
|
||||||
|
position_entry_ts: pd.Timestamp | None = None
|
||||||
|
trades: list[Trade] = []
|
||||||
|
equity = 0.0
|
||||||
|
equity_history: list[float] = []
|
||||||
|
returns_history: list[float] = []
|
||||||
|
prev_equity = 0.0
|
||||||
|
|
||||||
|
for ts, row in ohlcv.iterrows():
|
||||||
|
target_side = executed_side.loc[ts]
|
||||||
|
current_side = position.side if position else Side.FLAT
|
||||||
|
if target_side != current_side:
|
||||||
|
if position is not None:
|
||||||
|
assert position_entry_ts is not None
|
||||||
|
trade = Trade(
|
||||||
|
entry_ts=position_entry_ts,
|
||||||
|
exit_ts=ts,
|
||||||
|
side=position.side,
|
||||||
|
size=position.size,
|
||||||
|
entry_price=position.entry_price,
|
||||||
|
exit_price=float(row["open"]),
|
||||||
|
fees_bp=fees_bp,
|
||||||
|
)
|
||||||
|
trades.append(trade)
|
||||||
|
equity += trade.net_pnl
|
||||||
|
position = None
|
||||||
|
position_entry_ts = None
|
||||||
|
if target_side in (Side.LONG, Side.SHORT):
|
||||||
|
position = Position(
|
||||||
|
side=target_side, entry_price=float(row["open"]), size=1.0
|
||||||
|
)
|
||||||
|
position_entry_ts = ts
|
||||||
|
mark = float(row["close"])
|
||||||
|
mtm = position.unrealized_pnl(mark) if position else 0.0
|
||||||
|
current_equity = equity + mtm
|
||||||
|
equity_history.append(current_equity)
|
||||||
|
returns_history.append(current_equity - prev_equity)
|
||||||
|
prev_equity = current_equity
|
||||||
|
if position is not None:
|
||||||
|
assert position_entry_ts is not None
|
||||||
|
last_ts = ohlcv.index[-1]
|
||||||
|
last_close = float(ohlcv["close"].iloc[-1])
|
||||||
|
trade = Trade(
|
||||||
|
entry_ts=position_entry_ts,
|
||||||
|
exit_ts=last_ts,
|
||||||
|
side=position.side,
|
||||||
|
size=position.size,
|
||||||
|
entry_price=position.entry_price,
|
||||||
|
exit_price=last_close,
|
||||||
|
fees_bp=fees_bp,
|
||||||
|
)
|
||||||
|
trades.append(trade)
|
||||||
|
equity += trade.net_pnl
|
||||||
|
equity_history[-1] = equity
|
||||||
|
if len(returns_history) >= 2:
|
||||||
|
returns_history[-1] = equity - equity_history[-2]
|
||||||
|
|
||||||
|
return BacktestResult(
|
||||||
|
equity_curve=pd.Series(equity_history, index=ohlcv.index, name="equity"),
|
||||||
|
returns=pd.Series(returns_history, index=ohlcv.index, name="returns"),
|
||||||
|
trades=trades,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _random_ohlcv(n: int, seed: int) -> pd.DataFrame:
|
||||||
|
rng = np.random.default_rng(seed)
|
||||||
|
rets = rng.normal(0.0, 0.01, size=n)
|
||||||
|
close = 100.0 * np.exp(np.cumsum(rets))
|
||||||
|
idx = pd.date_range("2024-01-01", periods=n, freq="1h", tz="UTC")
|
||||||
|
return pd.DataFrame(
|
||||||
|
{
|
||||||
|
"open": close * (1 + rng.normal(0, 0.001, n)),
|
||||||
|
"high": close * 1.005,
|
||||||
|
"low": close * 0.995,
|
||||||
|
"close": close,
|
||||||
|
"volume": rng.uniform(1.0, 100.0, n),
|
||||||
|
},
|
||||||
|
index=idx,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _random_signals(n: int, seed: int, p_change: float = 0.1) -> pd.Series:
|
||||||
|
"""Segnali con persistenza: ad ogni bar con prob p_change cambia stato."""
|
||||||
|
rng = np.random.default_rng(seed + 999)
|
||||||
|
states = [Side.LONG, Side.SHORT, Side.FLAT]
|
||||||
|
out: list[Side] = [rng.choice(states)]
|
||||||
|
for _ in range(1, n):
|
||||||
|
out.append(rng.choice(states) if rng.random() < p_change else out[-1])
|
||||||
|
idx = pd.date_range("2024-01-01", periods=n, freq="1h", tz="UTC")
|
||||||
|
return pd.Series(out, index=idx, dtype=object)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("seed", [0, 1, 42, 123, 999])
|
||||||
|
def test_vectorized_equals_legacy(seed: int) -> None:
|
||||||
|
df = _random_ohlcv(500, seed)
|
||||||
|
signals = _random_signals(500, seed)
|
||||||
|
engine = BacktestEngine(fees_bp=5.0)
|
||||||
|
new = engine.run(df, signals)
|
||||||
|
ref = _legacy_run(df, signals, fees_bp=5.0)
|
||||||
|
|
||||||
|
pd.testing.assert_series_equal(
|
||||||
|
new.equity_curve, ref.equity_curve, rtol=1e-9, atol=1e-9
|
||||||
|
)
|
||||||
|
pd.testing.assert_series_equal(
|
||||||
|
new.returns, ref.returns, rtol=1e-9, atol=1e-9
|
||||||
|
)
|
||||||
|
assert len(new.trades) == len(ref.trades)
|
||||||
|
for nt, rt in zip(new.trades, ref.trades, strict=True):
|
||||||
|
assert nt.entry_ts == rt.entry_ts
|
||||||
|
assert nt.exit_ts == rt.exit_ts
|
||||||
|
assert nt.side == rt.side
|
||||||
|
assert nt.entry_price == pytest.approx(rt.entry_price, abs=1e-12)
|
||||||
|
assert nt.exit_price == pytest.approx(rt.exit_price, abs=1e-12)
|
||||||
|
assert nt.net_pnl == pytest.approx(rt.net_pnl, abs=1e-12)
|
||||||
|
|
||||||
|
|
||||||
|
def test_vectorized_handles_position_still_open_at_end() -> None:
|
||||||
|
"""Edge case: signal LONG fino all'ultimo bar (exit a close[-1] forced)."""
|
||||||
|
df = _random_ohlcv(100, seed=7)
|
||||||
|
signals = pd.Series([Side.LONG] * 100, index=df.index)
|
||||||
|
new = BacktestEngine(fees_bp=10.0).run(df, signals)
|
||||||
|
ref = _legacy_run(df, signals, fees_bp=10.0)
|
||||||
|
pd.testing.assert_series_equal(new.equity_curve, ref.equity_curve, atol=1e-9)
|
||||||
|
assert len(new.trades) == 1
|
||||||
|
assert new.trades[0].side == Side.LONG
|
||||||
|
|
||||||
|
|
||||||
|
def test_vectorized_zero_signals_no_trades() -> None:
|
||||||
|
df = _random_ohlcv(50, seed=3)
|
||||||
|
signals = pd.Series([Side.FLAT] * 50, index=df.index)
|
||||||
|
new = BacktestEngine().run(df, signals)
|
||||||
|
assert len(new.trades) == 0
|
||||||
|
assert (new.equity_curve == 0).all()
|
||||||
@@ -2,6 +2,7 @@ import json
|
|||||||
|
|
||||||
from multi_swarm_core.agents.hypothesis import HypothesisAgent, MarketSummary
|
from multi_swarm_core.agents.hypothesis import HypothesisAgent, MarketSummary
|
||||||
from multi_swarm_core.genome.hypothesis import HypothesisAgentGenome, ModelTier
|
from multi_swarm_core.genome.hypothesis import HypothesisAgentGenome, ModelTier
|
||||||
|
from multi_swarm_core.genome.prompt_library import PromptLibrary
|
||||||
from multi_swarm_core.llm.client import CompletionResult, EmptyCompletionError
|
from multi_swarm_core.llm.client import CompletionResult, EmptyCompletionError
|
||||||
|
|
||||||
|
|
||||||
@@ -248,3 +249,174 @@ def test_hypothesis_agent_returns_failed_proposal_on_only_empty_completions(mock
|
|||||||
assert "empty_completion" in proposal.parse_error
|
assert "empty_completion" in proposal.parse_error
|
||||||
# 3 tentativi tutti falliti.
|
# 3 tentativi tutti falliti.
|
||||||
assert fake_llm.complete.call_count == 3
|
assert fake_llm.complete.call_count == 3
|
||||||
|
|
||||||
|
|
||||||
|
def test_propose_renders_focus_block_for_style(mocker): # type: ignore[no-untyped-def]
|
||||||
|
"""Con PromptLibrary che ha focus_metrics, il LLM mock riceve 'Focus per la tua lente' nel user message."""
|
||||||
|
fake_llm = mocker.MagicMock()
|
||||||
|
fake_llm.complete.return_value = CompletionResult(
|
||||||
|
text=VALID_STRATEGY_JSON,
|
||||||
|
input_tokens=200,
|
||||||
|
output_tokens=80,
|
||||||
|
tier=ModelTier.C,
|
||||||
|
model="qwen",
|
||||||
|
)
|
||||||
|
# PromptLibrary con focus_metrics per physicist
|
||||||
|
lib = PromptLibrary(
|
||||||
|
styles={"physicist": "Pensa come un fisico."},
|
||||||
|
focus={"physicist": ["efficiency_ratio", "spectral_entropy", "hurst"]},
|
||||||
|
)
|
||||||
|
agent = HypothesisAgent(llm=fake_llm, prompt_library=lib)
|
||||||
|
proposal = agent.propose(make_genome(), make_summary())
|
||||||
|
assert proposal.strategy is not None
|
||||||
|
# Verifica che il user message contenga il focus block
|
||||||
|
call_kwargs = fake_llm.complete.call_args.kwargs
|
||||||
|
user_msg = call_kwargs["user"]
|
||||||
|
assert "Focus per la tua lente:" in user_msg
|
||||||
|
assert "efficiency_ratio=" in user_msg
|
||||||
|
assert "spectral_entropy=" in user_msg
|
||||||
|
assert "hurst=" in user_msg
|
||||||
|
|
||||||
|
|
||||||
|
def test_propose_no_focus_block_when_style_not_in_library(mocker): # type: ignore[no-untyped-def]
|
||||||
|
"""Stile senza focus_metrics → nessun focus block nel user message."""
|
||||||
|
fake_llm = mocker.MagicMock()
|
||||||
|
fake_llm.complete.return_value = CompletionResult(
|
||||||
|
text=VALID_STRATEGY_JSON,
|
||||||
|
input_tokens=200,
|
||||||
|
output_tokens=80,
|
||||||
|
tier=ModelTier.C,
|
||||||
|
model="qwen",
|
||||||
|
)
|
||||||
|
# PromptLibrary senza focus_metrics per physicist
|
||||||
|
lib = PromptLibrary(
|
||||||
|
styles={"physicist": "Pensa come un fisico."},
|
||||||
|
focus={},
|
||||||
|
)
|
||||||
|
agent = HypothesisAgent(llm=fake_llm, prompt_library=lib)
|
||||||
|
proposal = agent.propose(make_genome(), make_summary())
|
||||||
|
assert proposal.strategy is not None
|
||||||
|
call_kwargs = fake_llm.complete.call_args.kwargs
|
||||||
|
user_msg = call_kwargs["user"]
|
||||||
|
assert "Focus per la tua lente:" not in user_msg
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_system_prompt_includes_role_and_guidance_from_library(mocker): # type: ignore[no-untyped-def]
|
||||||
|
"""agent_role e pattern_guidance da PromptLibrary appaiono nel SYSTEM prompt."""
|
||||||
|
fake_llm = mocker.MagicMock()
|
||||||
|
fake_llm.complete.return_value = CompletionResult(
|
||||||
|
text=VALID_STRATEGY_JSON,
|
||||||
|
input_tokens=200,
|
||||||
|
output_tokens=80,
|
||||||
|
tier=ModelTier.C,
|
||||||
|
model="qwen",
|
||||||
|
)
|
||||||
|
lib = PromptLibrary(
|
||||||
|
styles={"physicist": "Pensa come un fisico."},
|
||||||
|
focus={},
|
||||||
|
agent_role="ROLE_X",
|
||||||
|
pattern_guidance="GUIDE_X",
|
||||||
|
)
|
||||||
|
agent = HypothesisAgent(llm=fake_llm, prompt_library=lib)
|
||||||
|
agent.propose(make_genome(), make_summary())
|
||||||
|
call_kwargs = fake_llm.complete.call_args.kwargs
|
||||||
|
system_msg = call_kwargs["system"]
|
||||||
|
assert "ROLE_X" in system_msg
|
||||||
|
assert "GUIDE_X" in system_msg
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_system_prompt_skips_empty_optional_sections(mocker): # type: ignore[no-untyped-def]
|
||||||
|
"""domain_warnings='' e pattern_guidance='' → sezioni opzionali assenti nel SYSTEM."""
|
||||||
|
fake_llm = mocker.MagicMock()
|
||||||
|
fake_llm.complete.return_value = CompletionResult(
|
||||||
|
text=VALID_STRATEGY_JSON,
|
||||||
|
input_tokens=200,
|
||||||
|
output_tokens=80,
|
||||||
|
tier=ModelTier.C,
|
||||||
|
model="qwen",
|
||||||
|
)
|
||||||
|
lib = PromptLibrary(
|
||||||
|
styles={"physicist": "Pensa come un fisico."},
|
||||||
|
focus={},
|
||||||
|
domain_warnings="",
|
||||||
|
pattern_guidance="",
|
||||||
|
)
|
||||||
|
agent = HypothesisAgent(llm=fake_llm, prompt_library=lib)
|
||||||
|
agent.propose(make_genome(), make_summary())
|
||||||
|
call_kwargs = fake_llm.complete.call_args.kwargs
|
||||||
|
system_msg = call_kwargs["system"]
|
||||||
|
assert "WARNING DI DOMINIO" not in system_msg
|
||||||
|
assert "PATTERN GUIDANCE" not in system_msg
|
||||||
|
|
||||||
|
|
||||||
|
def test_user_template_uses_instruction_from_library(mocker): # type: ignore[no-untyped-def]
|
||||||
|
"""instruction da PromptLibrary appare nel USER message; default non viene usato."""
|
||||||
|
fake_llm = mocker.MagicMock()
|
||||||
|
fake_llm.complete.return_value = CompletionResult(
|
||||||
|
text=VALID_STRATEGY_JSON,
|
||||||
|
input_tokens=200,
|
||||||
|
output_tokens=80,
|
||||||
|
tier=ModelTier.C,
|
||||||
|
model="qwen",
|
||||||
|
)
|
||||||
|
lib = PromptLibrary(
|
||||||
|
styles={"physicist": "Pensa come un fisico."},
|
||||||
|
focus={},
|
||||||
|
instruction="INSTR_X",
|
||||||
|
)
|
||||||
|
agent = HypothesisAgent(llm=fake_llm, prompt_library=lib)
|
||||||
|
agent.propose(make_genome(), make_summary())
|
||||||
|
call_kwargs = fake_llm.complete.call_args.kwargs
|
||||||
|
user_msg = call_kwargs["user"]
|
||||||
|
assert "INSTR_X" in user_msg
|
||||||
|
assert "Genera una strategia che cerchi anomalie sfruttabili in questo regime." not in user_msg
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_system_prompt_includes_anti_patterns_and_priorities(mocker): # type: ignore[no-untyped-def]
|
||||||
|
"""anti_patterns e output_priorities da PromptLibrary appaiono nel SYSTEM prompt."""
|
||||||
|
fake_llm = mocker.MagicMock()
|
||||||
|
fake_llm.complete.return_value = CompletionResult(
|
||||||
|
text=VALID_STRATEGY_JSON,
|
||||||
|
input_tokens=200,
|
||||||
|
output_tokens=80,
|
||||||
|
tier=ModelTier.C,
|
||||||
|
model="qwen",
|
||||||
|
)
|
||||||
|
lib = PromptLibrary(
|
||||||
|
styles={"physicist": "Pensa come un fisico."},
|
||||||
|
focus={},
|
||||||
|
anti_patterns="EVITA_X",
|
||||||
|
output_priorities="PRIORI_X",
|
||||||
|
)
|
||||||
|
agent = HypothesisAgent(llm=fake_llm, prompt_library=lib)
|
||||||
|
agent.propose(make_genome(), make_summary())
|
||||||
|
call_kwargs = fake_llm.complete.call_args.kwargs
|
||||||
|
system_msg = call_kwargs["system"]
|
||||||
|
assert "ANTI-PATTERN DA EVITARE" in system_msg
|
||||||
|
assert "EVITA_X" in system_msg
|
||||||
|
assert "PRIORITA' DI OUTPUT" in system_msg
|
||||||
|
assert "PRIORI_X" in system_msg
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_system_prompt_skips_anti_patterns_and_priorities_when_empty(mocker): # type: ignore[no-untyped-def]
|
||||||
|
"""anti_patterns='' e output_priorities='' -> sezioni opzionali assenti nel SYSTEM."""
|
||||||
|
fake_llm = mocker.MagicMock()
|
||||||
|
fake_llm.complete.return_value = CompletionResult(
|
||||||
|
text=VALID_STRATEGY_JSON,
|
||||||
|
input_tokens=200,
|
||||||
|
output_tokens=80,
|
||||||
|
tier=ModelTier.C,
|
||||||
|
model="qwen",
|
||||||
|
)
|
||||||
|
lib = PromptLibrary(
|
||||||
|
styles={"physicist": "Pensa come un fisico."},
|
||||||
|
focus={},
|
||||||
|
anti_patterns="",
|
||||||
|
output_priorities="",
|
||||||
|
)
|
||||||
|
agent = HypothesisAgent(llm=fake_llm, prompt_library=lib)
|
||||||
|
agent.propose(make_genome(), make_summary())
|
||||||
|
call_kwargs = fake_llm.complete.call_args.kwargs
|
||||||
|
system_msg = call_kwargs["system"]
|
||||||
|
assert "ANTI-PATTERN" not in system_msg
|
||||||
|
assert "PRIORITA' DI OUTPUT" not in system_msg
|
||||||
|
|||||||
@@ -31,3 +31,83 @@ def test_volatility_regime_high_for_volatile() -> None:
|
|||||||
)
|
)
|
||||||
s = build_market_summary(df, symbol="BTC/USDT", timeframe="1h")
|
s = build_market_summary(df, symbol="BTC/USDT", timeframe="1h")
|
||||||
assert s.volatility_regime in {"medium", "high"}
|
assert s.volatility_regime in {"medium", "high"}
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_summary_new_fields_populated() -> None:
|
||||||
|
"""I 6 nuovi campi devono essere float nei range attesi."""
|
||||||
|
idx = pd.date_range("2024-01-01", periods=600, freq="1h", tz="UTC")
|
||||||
|
np.random.seed(42)
|
||||||
|
close = 100 + np.cumsum(np.random.normal(0, 1, 600))
|
||||||
|
df = pd.DataFrame(
|
||||||
|
{"open": close, "high": close + 0.5, "low": close - 0.5, "close": close, "volume": 1.0},
|
||||||
|
index=idx,
|
||||||
|
)
|
||||||
|
s = build_market_summary(df, symbol="ETH/USDT", timeframe="1h")
|
||||||
|
|
||||||
|
# autocorr fields: float in [-1, 1]
|
||||||
|
assert isinstance(s.autocorr_lag1_recent, float)
|
||||||
|
assert isinstance(s.autocorr_lag1_baseline, float)
|
||||||
|
assert -1.0 <= s.autocorr_lag1_recent <= 1.0
|
||||||
|
assert -1.0 <= s.autocorr_lag1_baseline <= 1.0
|
||||||
|
|
||||||
|
# hurst: float in [0, 1]
|
||||||
|
assert isinstance(s.hurst_recent, float)
|
||||||
|
assert 0.0 <= s.hurst_recent <= 1.0
|
||||||
|
|
||||||
|
# vol_percentile: float in [0, 100]
|
||||||
|
assert isinstance(s.vol_percentile, float)
|
||||||
|
assert 0.0 <= s.vol_percentile <= 100.0
|
||||||
|
|
||||||
|
# seasonality fields: float in [0, 1]
|
||||||
|
assert isinstance(s.seasonality_hour, float)
|
||||||
|
assert isinstance(s.seasonality_dow, float)
|
||||||
|
assert 0.0 <= s.seasonality_hour <= 1.0
|
||||||
|
assert 0.0 <= s.seasonality_dow <= 1.0
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_summary_geometric_fields_populated() -> None:
|
||||||
|
"""I 7 nuovi campi geometrico-frattali devono essere non-default e nei range attesi."""
|
||||||
|
idx = pd.date_range("2024-01-01", periods=600, freq="1h", tz="UTC")
|
||||||
|
np.random.seed(5)
|
||||||
|
close = 100 + np.cumsum(np.random.normal(0, 1, 600))
|
||||||
|
df = pd.DataFrame(
|
||||||
|
{"open": close, "high": close + 0.5, "low": close - 0.5, "close": close, "volume": 1.0},
|
||||||
|
index=idx,
|
||||||
|
)
|
||||||
|
s = build_market_summary(df, symbol="BTC/USDT", timeframe="1h")
|
||||||
|
|
||||||
|
# efficiency_ratio: float in [0, 1]
|
||||||
|
assert isinstance(s.efficiency_ratio, float)
|
||||||
|
assert 0.0 <= s.efficiency_ratio <= 1.0
|
||||||
|
|
||||||
|
# tail_index_left/right: float in [1, 10]
|
||||||
|
assert isinstance(s.tail_index_left, float)
|
||||||
|
assert isinstance(s.tail_index_right, float)
|
||||||
|
assert 1.0 <= s.tail_index_left <= 10.0
|
||||||
|
assert 1.0 <= s.tail_index_right <= 10.0
|
||||||
|
|
||||||
|
# structural_uptrend: float in [0, 1]
|
||||||
|
assert isinstance(s.structural_uptrend, float)
|
||||||
|
assert 0.0 <= s.structural_uptrend <= 1.0
|
||||||
|
|
||||||
|
# compression: float > 0
|
||||||
|
assert isinstance(s.compression, float)
|
||||||
|
assert s.compression > 0.0
|
||||||
|
|
||||||
|
# spectral_entropy: float in [0, 1]
|
||||||
|
assert isinstance(s.spectral_entropy, float)
|
||||||
|
assert 0.0 <= s.spectral_entropy <= 1.0
|
||||||
|
|
||||||
|
# dominant_cycle: None or positive float
|
||||||
|
assert s.dominant_cycle is None or (isinstance(s.dominant_cycle, float) and s.dominant_cycle > 0)
|
||||||
|
|
||||||
|
# Verifica che i campi siano stati popolati (non tutti ai valori default)
|
||||||
|
defaults_only = (
|
||||||
|
s.efficiency_ratio == 0.0
|
||||||
|
and s.tail_index_left == 5.0
|
||||||
|
and s.tail_index_right == 5.0
|
||||||
|
and s.structural_uptrend == 0.5
|
||||||
|
and s.compression == 1.0
|
||||||
|
and s.spectral_entropy == 1.0
|
||||||
|
)
|
||||||
|
assert not defaults_only, "Tutti i campi geometrici sono rimasti ai valori default: calcolo non avvenuto"
|
||||||
|
|||||||
@@ -2,7 +2,20 @@ import numpy as np
|
|||||||
import pandas as pd
|
import pandas as pd
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from multi_swarm_core.metrics.basic import max_drawdown, sharpe_ratio, total_return
|
from multi_swarm_core.metrics.basic import (
|
||||||
|
autocorr_lag1,
|
||||||
|
compression_ratio,
|
||||||
|
efficiency_ratio_kaufman,
|
||||||
|
hurst_exponent,
|
||||||
|
max_drawdown,
|
||||||
|
seasonality_strength,
|
||||||
|
sharpe_ratio,
|
||||||
|
spectral_entropy_and_cycle,
|
||||||
|
structural_uptrend_score,
|
||||||
|
tail_index_hill,
|
||||||
|
total_return,
|
||||||
|
vol_percentile_historical,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_sharpe_zero_returns():
|
def test_sharpe_zero_returns():
|
||||||
@@ -38,3 +51,240 @@ def test_max_drawdown_known_curve():
|
|||||||
def test_total_return():
|
def test_total_return():
|
||||||
eq = pd.Series([100.0, 110.0, 105.0, 120.0])
|
eq = pd.Series([100.0, 110.0, 105.0, 120.0])
|
||||||
assert total_return(eq) == pytest.approx(0.20)
|
assert total_return(eq) == pytest.approx(0.20)
|
||||||
|
|
||||||
|
|
||||||
|
# --- New metrics tests ---
|
||||||
|
|
||||||
|
|
||||||
|
def test_autocorr_lag1_short_series():
|
||||||
|
"""Serie corta ritorna 0.0 senza errori."""
|
||||||
|
r = pd.Series([0.01])
|
||||||
|
assert autocorr_lag1(r) == 0.0
|
||||||
|
|
||||||
|
|
||||||
|
def test_autocorr_lag1_all_nan():
|
||||||
|
"""Serie di NaN ritorna 0.0."""
|
||||||
|
r = pd.Series([float("nan")] * 10)
|
||||||
|
assert autocorr_lag1(r) == 0.0
|
||||||
|
|
||||||
|
|
||||||
|
def test_autocorr_lag1_range():
|
||||||
|
"""Autocorrelazione sempre in [-1, 1]."""
|
||||||
|
np.random.seed(7)
|
||||||
|
r = pd.Series(np.random.normal(0, 0.01, 500))
|
||||||
|
val = autocorr_lag1(r)
|
||||||
|
assert -1.0 <= val <= 1.0
|
||||||
|
|
||||||
|
|
||||||
|
def test_autocorr_lag1_trending_positive():
|
||||||
|
"""Serie con autocorrelazione positiva artificiale deve dare val > 0."""
|
||||||
|
# costruiamo una serie con forte AR(1): x_t = 0.8*x_{t-1} + noise
|
||||||
|
np.random.seed(1)
|
||||||
|
n = 300
|
||||||
|
x = np.zeros(n)
|
||||||
|
for i in range(1, n):
|
||||||
|
x[i] = 0.8 * x[i - 1] + np.random.normal(0, 0.1)
|
||||||
|
r = pd.Series(x)
|
||||||
|
assert autocorr_lag1(r) > 0.5
|
||||||
|
|
||||||
|
|
||||||
|
def test_hurst_short_series():
|
||||||
|
"""Serie < 100 elementi ritorna 0.5 (random-walk default)."""
|
||||||
|
r = pd.Series(np.random.normal(0, 0.01, 50))
|
||||||
|
assert hurst_exponent(r) == pytest.approx(0.5)
|
||||||
|
|
||||||
|
|
||||||
|
def test_hurst_range():
|
||||||
|
"""Hurst sempre in [0, 1]."""
|
||||||
|
np.random.seed(3)
|
||||||
|
r = pd.Series(np.random.normal(0, 0.01, 500))
|
||||||
|
h = hurst_exponent(r)
|
||||||
|
assert 0.0 <= h <= 1.0
|
||||||
|
|
||||||
|
|
||||||
|
def test_hurst_random_walk_approx_half():
|
||||||
|
"""Random walk iid deve avere Hurst vicino a 0.5."""
|
||||||
|
np.random.seed(42)
|
||||||
|
r = pd.Series(np.random.normal(0, 0.01, 2000))
|
||||||
|
h = hurst_exponent(r)
|
||||||
|
assert 0.3 <= h <= 0.7
|
||||||
|
|
||||||
|
|
||||||
|
def test_vol_percentile_short_series():
|
||||||
|
"""Serie troppo corta ritorna 50.0."""
|
||||||
|
r = pd.Series(np.random.normal(0, 0.01, 10))
|
||||||
|
assert vol_percentile_historical(r) == pytest.approx(50.0)
|
||||||
|
|
||||||
|
|
||||||
|
def test_vol_percentile_range():
|
||||||
|
"""Percentile sempre in [0, 100]."""
|
||||||
|
np.random.seed(5)
|
||||||
|
r = pd.Series(np.random.normal(0, 0.01, 500))
|
||||||
|
p = vol_percentile_historical(r, current_window=24, ref_window=200)
|
||||||
|
assert 0.0 <= p <= 100.0
|
||||||
|
|
||||||
|
|
||||||
|
def test_vol_percentile_high_vol_at_end():
|
||||||
|
"""Vol molto alta alla fine deve dare percentile elevato."""
|
||||||
|
np.random.seed(9)
|
||||||
|
low_vol = np.random.normal(0, 0.001, 400)
|
||||||
|
high_vol = np.random.normal(0, 0.05, 100)
|
||||||
|
r = pd.Series(np.concatenate([low_vol, high_vol]))
|
||||||
|
p = vol_percentile_historical(r, current_window=24, ref_window=400)
|
||||||
|
assert p > 70.0
|
||||||
|
|
||||||
|
|
||||||
|
def test_seasonality_strength_no_datetimeindex():
|
||||||
|
"""Senza DatetimeIndex ritorna 0.0."""
|
||||||
|
r = pd.Series(np.random.normal(0, 0.01, 200))
|
||||||
|
assert seasonality_strength(r, by="hour") == 0.0
|
||||||
|
|
||||||
|
|
||||||
|
def test_seasonality_strength_short_series():
|
||||||
|
"""Serie < 50 elementi ritorna 0.0."""
|
||||||
|
idx = pd.date_range("2024-01-01", periods=30, freq="1h", tz="UTC")
|
||||||
|
r = pd.Series(np.random.normal(0, 0.01, 30), index=idx)
|
||||||
|
assert seasonality_strength(r, by="hour") == 0.0
|
||||||
|
|
||||||
|
|
||||||
|
def test_seasonality_strength_range():
|
||||||
|
"""Risultato sempre in [0, 1]."""
|
||||||
|
np.random.seed(11)
|
||||||
|
idx = pd.date_range("2024-01-01", periods=500, freq="1h", tz="UTC")
|
||||||
|
r = pd.Series(np.random.normal(0, 0.01, 500), index=idx)
|
||||||
|
val = seasonality_strength(r, by="hour")
|
||||||
|
assert 0.0 <= val <= 1.0
|
||||||
|
|
||||||
|
|
||||||
|
def test_seasonality_strength_dow_range():
|
||||||
|
"""Stagionalita dow sempre in [0, 1]."""
|
||||||
|
np.random.seed(13)
|
||||||
|
idx = pd.date_range("2024-01-01", periods=500, freq="1h", tz="UTC")
|
||||||
|
r = pd.Series(np.random.normal(0, 0.01, 500), index=idx)
|
||||||
|
val = seasonality_strength(r, by="dow")
|
||||||
|
assert 0.0 <= val <= 1.0
|
||||||
|
|
||||||
|
|
||||||
|
def test_seasonality_strength_invalid_by():
|
||||||
|
"""by non valido solleva ValueError."""
|
||||||
|
idx = pd.date_range("2024-01-01", periods=100, freq="1h", tz="UTC")
|
||||||
|
r = pd.Series(np.random.normal(0, 0.01, 100), index=idx)
|
||||||
|
with pytest.raises(ValueError, match="'minute'"):
|
||||||
|
seasonality_strength(r, by="minute")
|
||||||
|
|
||||||
|
|
||||||
|
# --- Geometric/fractal metrics tests ---
|
||||||
|
|
||||||
|
|
||||||
|
def test_efficiency_ratio_pure_trend():
|
||||||
|
"""Slope perfetto (prezzi linearmente crescenti) -> efficiency_ratio vicino a 1.0."""
|
||||||
|
prices = pd.Series(np.linspace(100.0, 200.0, 200))
|
||||||
|
er = efficiency_ratio_kaufman(prices, length=100)
|
||||||
|
assert er == pytest.approx(1.0, abs=1e-6)
|
||||||
|
|
||||||
|
|
||||||
|
def test_efficiency_ratio_random_walk():
|
||||||
|
"""Random walk iid -> efficiency_ratio basso (attorno a 0.1-0.4)."""
|
||||||
|
np.random.seed(42)
|
||||||
|
prices = pd.Series(100.0 + np.cumsum(np.random.normal(0, 1, 300)))
|
||||||
|
er = efficiency_ratio_kaufman(prices, length=100)
|
||||||
|
assert 0.0 <= er <= 0.6
|
||||||
|
|
||||||
|
|
||||||
|
def test_efficiency_ratio_short_series():
|
||||||
|
"""Serie troppo corta ritorna 0.0."""
|
||||||
|
prices = pd.Series([100.0, 101.0, 102.0])
|
||||||
|
assert efficiency_ratio_kaufman(prices, length=100) == 0.0
|
||||||
|
|
||||||
|
|
||||||
|
def test_tail_index_hill_left_right_separate():
|
||||||
|
"""tail_index_hill left e right ritornano float in [1, 10]."""
|
||||||
|
np.random.seed(7)
|
||||||
|
r = pd.Series(np.random.normal(0, 0.01, 500))
|
||||||
|
left = tail_index_hill(r, side="left")
|
||||||
|
right = tail_index_hill(r, side="right")
|
||||||
|
assert 1.0 <= left <= 10.0
|
||||||
|
assert 1.0 <= right <= 10.0
|
||||||
|
|
||||||
|
|
||||||
|
def test_tail_index_hill_invalid_side():
|
||||||
|
"""side non valido solleva ValueError."""
|
||||||
|
r = pd.Series(np.random.normal(0, 0.01, 200))
|
||||||
|
with pytest.raises(ValueError, match="'both'"):
|
||||||
|
tail_index_hill(r, side="both")
|
||||||
|
|
||||||
|
|
||||||
|
def test_tail_index_hill_short_series():
|
||||||
|
"""Serie < 50 ritorna default 5.0."""
|
||||||
|
r = pd.Series([0.01, -0.01, 0.02])
|
||||||
|
assert tail_index_hill(r, side="left") == pytest.approx(5.0)
|
||||||
|
assert tail_index_hill(r, side="right") == pytest.approx(5.0)
|
||||||
|
|
||||||
|
|
||||||
|
def test_structural_uptrend_pure_uptrend():
|
||||||
|
"""Prezzi costantemente crescenti -> structural_uptrend vicino a 1.0."""
|
||||||
|
prices = pd.Series(np.linspace(100.0, 200.0, 100))
|
||||||
|
score = structural_uptrend_score(prices, window=5)
|
||||||
|
assert score >= 0.5
|
||||||
|
|
||||||
|
|
||||||
|
def test_structural_uptrend_pure_downtrend():
|
||||||
|
"""Prezzi costantemente decrescenti -> structural_uptrend vicino a 0.0."""
|
||||||
|
prices = pd.Series(np.linspace(200.0, 100.0, 100))
|
||||||
|
score = structural_uptrend_score(prices, window=5)
|
||||||
|
assert score <= 0.5
|
||||||
|
|
||||||
|
|
||||||
|
def test_structural_uptrend_short_series():
|
||||||
|
"""Serie troppo corta ritorna 0.5 (neutro)."""
|
||||||
|
prices = pd.Series([100.0, 101.0, 99.0])
|
||||||
|
assert structural_uptrend_score(prices, window=5) == pytest.approx(0.5)
|
||||||
|
|
||||||
|
|
||||||
|
def test_compression_ratio_compress_then_expand():
|
||||||
|
"""Finestra recente compressa rispetto a ref -> ratio < 1."""
|
||||||
|
np.random.seed(1)
|
||||||
|
# Ref window: alta volatilita
|
||||||
|
ref_part = np.random.normal(0, 5.0, 200)
|
||||||
|
# Recent window: bassa volatilita
|
||||||
|
recent_part = np.random.normal(0, 0.2, 50)
|
||||||
|
prices = pd.Series(100.0 + np.cumsum(np.concatenate([ref_part, recent_part])))
|
||||||
|
ratio = compression_ratio(prices, recent_window=50, ref_window=200)
|
||||||
|
assert ratio < 1.0
|
||||||
|
|
||||||
|
|
||||||
|
def test_compression_ratio_short_series():
|
||||||
|
"""Serie troppo corta ritorna 1.0 (neutro)."""
|
||||||
|
prices = pd.Series([100.0, 101.0, 99.0])
|
||||||
|
assert compression_ratio(prices, recent_window=50, ref_window=200) == pytest.approx(1.0)
|
||||||
|
|
||||||
|
|
||||||
|
def test_spectral_entropy_pure_sine():
|
||||||
|
"""Seno puro -> entropy bassa e dominant_cycle ben definito."""
|
||||||
|
n = 256
|
||||||
|
t = np.arange(n)
|
||||||
|
cycle_period = 32 # barre
|
||||||
|
series = np.sin(2 * np.pi * t / cycle_period)
|
||||||
|
r = pd.Series(series)
|
||||||
|
entropy, cycle = spectral_entropy_and_cycle(r, length=256)
|
||||||
|
assert entropy < 0.6
|
||||||
|
assert cycle is not None
|
||||||
|
# Il ciclo dominante deve essere vicino a cycle_period
|
||||||
|
assert abs(cycle - cycle_period) <= 5.0
|
||||||
|
|
||||||
|
|
||||||
|
def test_spectral_entropy_white_noise():
|
||||||
|
"""Rumore bianco -> entropy alta e dominant_cycle = None."""
|
||||||
|
np.random.seed(99)
|
||||||
|
r = pd.Series(np.random.normal(0, 1, 512))
|
||||||
|
entropy, cycle = spectral_entropy_and_cycle(r, length=256)
|
||||||
|
assert entropy >= 0.6
|
||||||
|
assert cycle is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_spectral_entropy_short_series():
|
||||||
|
"""Serie troppo corta ritorna (1.0, None)."""
|
||||||
|
r = pd.Series([0.01, -0.01, 0.02])
|
||||||
|
entropy, cycle = spectral_entropy_and_cycle(r, length=256)
|
||||||
|
assert entropy == pytest.approx(1.0)
|
||||||
|
assert cycle is None
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
"""Test che `_parallel_propose` preservi l'ordine dei risultati e funzioni
|
||||||
|
sia in modalita' sequenziale (workers=1) che concorrente (workers>1).
|
||||||
|
|
||||||
|
Non vogliamo testare il vero `HypothesisAgent.propose()` (che fa chiamate
|
||||||
|
LLM); usiamo un dummy con una latenza simulata per validare ordine e parallelismo.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import time
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from multi_swarm_core.orchestrator.run import _parallel_propose
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class _FakeGenome:
|
||||||
|
id: str
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class _FakeProposal:
|
||||||
|
genome_id: str
|
||||||
|
|
||||||
|
|
||||||
|
class _FakeAgent:
|
||||||
|
"""Agent dummy: propose() dorme 50ms e ritorna un proposal con l'id del genome."""
|
||||||
|
|
||||||
|
def __init__(self, delay_s: float = 0.05) -> None:
|
||||||
|
self._delay = delay_s
|
||||||
|
self.call_count = 0
|
||||||
|
|
||||||
|
def propose(self, genome: _FakeGenome, market: Any) -> _FakeProposal:
|
||||||
|
time.sleep(self._delay)
|
||||||
|
self.call_count += 1
|
||||||
|
return _FakeProposal(genome_id=genome.id)
|
||||||
|
|
||||||
|
|
||||||
|
def test_parallel_propose_preserves_order_serial() -> None:
|
||||||
|
agent = _FakeAgent(delay_s=0.01)
|
||||||
|
genomes = [_FakeGenome(id=f"g{i}") for i in range(5)]
|
||||||
|
results = _parallel_propose(agent, genomes, market=None, n_workers=1)
|
||||||
|
assert [r.genome_id for r in results] == ["g0", "g1", "g2", "g3", "g4"]
|
||||||
|
assert agent.call_count == 5
|
||||||
|
|
||||||
|
|
||||||
|
def test_parallel_propose_preserves_order_concurrent() -> None:
|
||||||
|
agent = _FakeAgent(delay_s=0.05)
|
||||||
|
genomes = [_FakeGenome(id=f"g{i}") for i in range(8)]
|
||||||
|
results = _parallel_propose(agent, genomes, market=None, n_workers=4)
|
||||||
|
assert [r.genome_id for r in results] == [f"g{i}" for i in range(8)]
|
||||||
|
assert agent.call_count == 8
|
||||||
|
|
||||||
|
|
||||||
|
def test_parallel_propose_actually_parallelizes() -> None:
|
||||||
|
"""Wall time con 4 worker su 4 task da 100ms deve essere ~100ms, non ~400ms."""
|
||||||
|
agent = _FakeAgent(delay_s=0.1)
|
||||||
|
genomes = [_FakeGenome(id=f"g{i}") for i in range(4)]
|
||||||
|
t0 = time.time()
|
||||||
|
_parallel_propose(agent, genomes, market=None, n_workers=4)
|
||||||
|
elapsed = time.time() - t0
|
||||||
|
# serial sarebbe 0.4s; con 4 worker scendiamo a ~0.1s (max 0.2 per overhead).
|
||||||
|
assert elapsed < 0.2, f"expected <200ms with 4 workers, got {elapsed * 1000:.0f}ms"
|
||||||
|
|
||||||
|
|
||||||
|
def test_parallel_propose_handles_single_genome() -> None:
|
||||||
|
agent = _FakeAgent()
|
||||||
|
results = _parallel_propose(agent, [_FakeGenome(id="solo")], market=None, n_workers=8)
|
||||||
|
assert len(results) == 1
|
||||||
|
assert results[0].genome_id == "solo"
|
||||||
|
|
||||||
|
|
||||||
|
def test_parallel_propose_empty_input() -> None:
|
||||||
|
agent = _FakeAgent()
|
||||||
|
results = _parallel_propose(agent, [], market=None, n_workers=4)
|
||||||
|
assert results == []
|
||||||
|
assert agent.call_count == 0
|
||||||
@@ -0,0 +1,146 @@
|
|||||||
|
"""Unit tests per PromptLibrary v3.0 — nuovi campi top-level."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import tempfile
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from multi_swarm_core.genome.prompt_library import PromptLibrary, PromptLibraryError
|
||||||
|
|
||||||
|
|
||||||
|
def _write_json(data: dict, tmp_path: Path) -> Path:
|
||||||
|
p = tmp_path / "prompts.json"
|
||||||
|
p.write_text(json.dumps(data), encoding="utf-8")
|
||||||
|
return p
|
||||||
|
|
||||||
|
|
||||||
|
def test_from_json_loads_new_top_level_fields(tmp_path: Path) -> None:
|
||||||
|
"""from_json() legge agent_role, pattern_guidance, instruction, domain_warnings."""
|
||||||
|
data = {
|
||||||
|
"styles": {"physicist": {"directive": "Cerca leggi conservative."}},
|
||||||
|
"agent_role": "Sei un agente test.",
|
||||||
|
"pattern_guidance": "Forme di curva: trend, compressione.",
|
||||||
|
"instruction": "Genera qualcosa di utile.",
|
||||||
|
"domain_warnings": "Attenzione: mercato 24/7.",
|
||||||
|
}
|
||||||
|
lib = PromptLibrary.from_json(_write_json(data, tmp_path))
|
||||||
|
assert lib.agent_role == "Sei un agente test."
|
||||||
|
assert lib.pattern_guidance == "Forme di curva: trend, compressione."
|
||||||
|
assert lib.instruction == "Genera qualcosa di utile."
|
||||||
|
assert lib.domain_warnings == "Attenzione: mercato 24/7."
|
||||||
|
|
||||||
|
|
||||||
|
def test_from_json_defaults_new_fields_when_absent(tmp_path: Path) -> None:
|
||||||
|
"""from_json() usa stringa vuota come default per i 4 campi opzionali."""
|
||||||
|
data = {
|
||||||
|
"styles": {"physicist": {"directive": "Cerca leggi conservative."}},
|
||||||
|
}
|
||||||
|
lib = PromptLibrary.from_json(_write_json(data, tmp_path))
|
||||||
|
assert lib.agent_role == ""
|
||||||
|
assert lib.pattern_guidance == ""
|
||||||
|
assert lib.instruction == ""
|
||||||
|
assert lib.domain_warnings == ""
|
||||||
|
|
||||||
|
|
||||||
|
def test_default_provides_universal_fallbacks() -> None:
|
||||||
|
"""PromptLibrary.default() ha agent_role, pattern_guidance, instruction non vuoti; domain_warnings vuoto."""
|
||||||
|
lib = PromptLibrary.default()
|
||||||
|
assert lib.agent_role != ""
|
||||||
|
assert lib.pattern_guidance != ""
|
||||||
|
assert lib.instruction != ""
|
||||||
|
assert lib.domain_warnings == ""
|
||||||
|
|
||||||
|
|
||||||
|
def test_prompt_library_rejects_whitespace_only_fields() -> None:
|
||||||
|
"""Campi opzionali forniti ma solo whitespace sollevano PromptLibraryError."""
|
||||||
|
with pytest.raises(PromptLibraryError, match="agent_role"):
|
||||||
|
PromptLibrary(
|
||||||
|
styles={"physicist": "Cerca leggi."},
|
||||||
|
focus={},
|
||||||
|
agent_role=" ",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_prompt_library_accepts_empty_string_for_optional_fields() -> None:
|
||||||
|
"""Stringa vuota '' e' accettata per tutti i campi opzionali."""
|
||||||
|
lib = PromptLibrary(
|
||||||
|
styles={"physicist": "Cerca leggi."},
|
||||||
|
focus={},
|
||||||
|
agent_role="",
|
||||||
|
pattern_guidance="",
|
||||||
|
instruction="",
|
||||||
|
domain_warnings="",
|
||||||
|
)
|
||||||
|
assert lib.agent_role == ""
|
||||||
|
assert lib.domain_warnings == ""
|
||||||
|
|
||||||
|
|
||||||
|
def test_from_json_loads_anti_patterns_and_output_priorities(tmp_path: Path) -> None:
|
||||||
|
"""from_json() legge anti_patterns e output_priorities (v3.1)."""
|
||||||
|
data = {
|
||||||
|
"styles": {"physicist": {"directive": "Cerca leggi conservative."}},
|
||||||
|
"anti_patterns": "Evita overfitting.",
|
||||||
|
"output_priorities": "Robustezza > ottimalita.",
|
||||||
|
}
|
||||||
|
lib = PromptLibrary.from_json(_write_json(data, tmp_path))
|
||||||
|
assert lib.anti_patterns == "Evita overfitting."
|
||||||
|
assert lib.output_priorities == "Robustezza > ottimalita."
|
||||||
|
|
||||||
|
|
||||||
|
def test_strategy_crypto_directives_ascii_safe() -> None:
|
||||||
|
"""REGRESSION GUARD: nessuna directive contiene caratteri > U+007F.
|
||||||
|
|
||||||
|
v3.1 aveva regredito introducendo il carattere circa-uguale (U+2248) in 3 stili.
|
||||||
|
v3.2 ripristina ASCII-strict come invariante permanente.
|
||||||
|
"""
|
||||||
|
import importlib.resources
|
||||||
|
|
||||||
|
path = importlib.resources.files("strategy_crypto") / "prompts.json"
|
||||||
|
lib = PromptLibrary.from_json(str(path))
|
||||||
|
|
||||||
|
for style, directive in lib.styles.items():
|
||||||
|
non_ascii = [c for c in directive if ord(c) > 127]
|
||||||
|
assert not non_ascii, (
|
||||||
|
f"directive di {style!r} contiene caratteri non-ASCII: "
|
||||||
|
f"{non_ascii} (codepoints: {[hex(ord(c)) for c in non_ascii]})"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_strategy_crypto_directives_have_archetype_marker() -> None:
|
||||||
|
"""REGRESSION GUARD: ogni directive chiude con 'Archetipo dominante: ...'.
|
||||||
|
|
||||||
|
L'archetipo e' l'ancora semantica identitaria della lente; deve essere
|
||||||
|
presente per resistere alle riscritture di mutate_prompt_llm.
|
||||||
|
"""
|
||||||
|
import importlib.resources
|
||||||
|
|
||||||
|
path = importlib.resources.files("strategy_crypto") / "prompts.json"
|
||||||
|
lib = PromptLibrary.from_json(str(path))
|
||||||
|
|
||||||
|
for style, directive in lib.styles.items():
|
||||||
|
assert "Archetipo dominante:" in directive, (
|
||||||
|
f"directive di {style!r} manca del marker 'Archetipo dominante:'"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_strategy_crypto_directives_have_lookback_hint() -> None:
|
||||||
|
"""REGRESSION GUARD: ogni directive contiene un hint 'Lookback consigliato: X-Y barre'.
|
||||||
|
|
||||||
|
Il range numerico orienta il parametro evoluto lookback_window del genoma;
|
||||||
|
differenziato per stile per favorire diversita di scala temporale nella
|
||||||
|
popolazione iniziale.
|
||||||
|
"""
|
||||||
|
import re
|
||||||
|
import importlib.resources
|
||||||
|
|
||||||
|
path = importlib.resources.files("strategy_crypto") / "prompts.json"
|
||||||
|
lib = PromptLibrary.from_json(str(path))
|
||||||
|
|
||||||
|
pattern = re.compile(r"[Ll]ookback consigliato:\s*\d+\s*-\s*\d+", re.IGNORECASE)
|
||||||
|
for style, directive in lib.styles.items():
|
||||||
|
assert pattern.search(directive), (
|
||||||
|
f"directive di {style!r} manca dell'hint 'Lookback consigliato: X-Y'"
|
||||||
|
)
|
||||||
@@ -309,3 +309,84 @@ def test_atr_pct_in_strategy_eval(ohlcv: pd.DataFrame) -> None:
|
|||||||
# senza dead-branch (qualche match e' possibile in warmup).
|
# senza dead-branch (qualche match e' possibile in warmup).
|
||||||
non_warmup = signal.iloc[30:]
|
non_warmup = signal.iloc[30:]
|
||||||
assert (non_warmup == Side.FLAT).all() or (non_warmup == Side.LONG).any()
|
assert (non_warmup == Side.FLAT).all() or (non_warmup == Side.LONG).any()
|
||||||
|
|
||||||
|
|
||||||
|
def test_sma_pct_is_close_deviation_from_sma() -> None:
|
||||||
|
"""sma_pct = (close - sma) / sma: deviazione frazionale del close dalla SMA."""
|
||||||
|
from multi_swarm_core.protocol.compiler import _ind_sma, _ind_sma_pct
|
||||||
|
|
||||||
|
idx = pd.date_range("2024-01-01", periods=100, freq="1h", tz="UTC")
|
||||||
|
# Prezzo che cresce poi torna: sma_pct passa da +qualcosa a -qualcosa
|
||||||
|
close = np.concatenate([np.linspace(100, 120, 50), np.linspace(120, 95, 50)])
|
||||||
|
df = pd.DataFrame(
|
||||||
|
{"open": close, "high": close + 0.5, "low": close - 0.5, "close": close, "volume": 1.0},
|
||||||
|
index=idx,
|
||||||
|
)
|
||||||
|
|
||||||
|
sma = _ind_sma(df, 20)
|
||||||
|
sma_pct = _ind_sma_pct(df, 20)
|
||||||
|
expected = (df["close"] - sma) / sma
|
||||||
|
assert np.allclose(sma_pct.dropna(), expected.dropna())
|
||||||
|
# In salita: close > sma -> sma_pct positivo
|
||||||
|
assert (sma_pct.iloc[30:50] > 0).any()
|
||||||
|
# In discesa estesa: close < sma -> sma_pct negativo
|
||||||
|
assert (sma_pct.iloc[80:] < 0).any()
|
||||||
|
|
||||||
|
|
||||||
|
def test_macd_pct_is_macd_divided_by_close() -> None:
|
||||||
|
"""macd_pct = macd / close: momentum normalizzato al prezzo."""
|
||||||
|
from multi_swarm_core.protocol.compiler import _ind_macd, _ind_macd_pct
|
||||||
|
|
||||||
|
idx = pd.date_range("2024-01-01", periods=300, freq="1h", tz="UTC")
|
||||||
|
# Random walk realistico su crypto (~3000 USDT, vol ~30/bar): MACD ha
|
||||||
|
# ampiezza non trascurabile vs trend lineare puro.
|
||||||
|
rng = np.random.default_rng(42)
|
||||||
|
close = 3000.0 + np.cumsum(rng.standard_normal(300) * 30)
|
||||||
|
df = pd.DataFrame(
|
||||||
|
{"open": close, "high": close + 5, "low": close - 5, "close": close, "volume": 1.0},
|
||||||
|
index=idx,
|
||||||
|
)
|
||||||
|
|
||||||
|
macd = _ind_macd(df, 12, 26, 9)
|
||||||
|
macd_pct = _ind_macd_pct(df, 12, 26, 9)
|
||||||
|
# Identita' algebrica: macd_pct == macd / close
|
||||||
|
assert np.allclose(macd_pct.dropna(), (macd / df["close"]).dropna())
|
||||||
|
# macd_pct ha scala << 1 (frazione del prezzo, ordine 1e-3)
|
||||||
|
assert macd_pct.abs().mean() < 0.05
|
||||||
|
# macd assoluto e' >> macd_pct (rapporto = close ~3000)
|
||||||
|
ratio = macd.abs().mean() / max(macd_pct.abs().mean(), 1e-12)
|
||||||
|
assert 1000 < ratio < 5000
|
||||||
|
|
||||||
|
|
||||||
|
def test_sma_pct_and_macd_pct_in_validator() -> None:
|
||||||
|
"""Regression: i nuovi indicatori sono accettati dal validator."""
|
||||||
|
from multi_swarm_core.protocol.validator import validate_strategy
|
||||||
|
|
||||||
|
spec = {
|
||||||
|
"rules": [
|
||||||
|
{
|
||||||
|
"condition": {
|
||||||
|
"op": "and",
|
||||||
|
"args": [
|
||||||
|
{
|
||||||
|
"op": "gt",
|
||||||
|
"args": [
|
||||||
|
{"kind": "indicator", "name": "sma_pct", "params": [50]},
|
||||||
|
{"kind": "literal", "value": 0.05},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "gt",
|
||||||
|
"args": [
|
||||||
|
{"kind": "indicator", "name": "macd_pct", "params": [12, 26, 9]},
|
||||||
|
{"kind": "literal", "value": 0.005},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"action": "entry-long",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
strat = parse_strategy(json.dumps(spec))
|
||||||
|
validate_strategy(strat) # no exception
|
||||||
|
|||||||
@@ -18,3 +18,4 @@ build-backend = "hatchling.build"
|
|||||||
|
|
||||||
[tool.hatch.build.targets.wheel.force-include]
|
[tool.hatch.build.targets.wheel.force-include]
|
||||||
"strategy_crypto/strategies" = "strategy_crypto/strategies"
|
"strategy_crypto/strategies" = "strategy_crypto/strategies"
|
||||||
|
"strategy_crypto/prompts.json" = "strategy_crypto/prompts.json"
|
||||||
|
|||||||
@@ -1,3 +1,8 @@
|
|||||||
|
"""Paper-trading data access functions for the strategy_crypto dashboard.
|
||||||
|
|
||||||
|
Reads exclusively from strategy_crypto.db (paper_trading_* tables).
|
||||||
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import json
|
import json
|
||||||
@@ -7,53 +12,6 @@ from typing import Any
|
|||||||
|
|
||||||
import pandas as pd # type: ignore[import-untyped]
|
import pandas as pd # type: ignore[import-untyped]
|
||||||
|
|
||||||
from multi_swarm_core.persistence.repository import Repository
|
|
||||||
|
|
||||||
|
|
||||||
def get_repo(db_path: str | Path) -> Repository:
|
|
||||||
return Repository(db_path=db_path)
|
|
||||||
|
|
||||||
|
|
||||||
def list_runs_df(repo: Repository) -> pd.DataFrame:
|
|
||||||
return pd.DataFrame(repo.list_runs())
|
|
||||||
|
|
||||||
|
|
||||||
def get_run_overview(repo: Repository, run_id: str) -> dict[str, Any]:
|
|
||||||
run = repo.get_run(run_id)
|
|
||||||
return {
|
|
||||||
"name": run["name"],
|
|
||||||
"started_at": run["started_at"],
|
|
||||||
"completed_at": run["completed_at"],
|
|
||||||
"status": run["status"],
|
|
||||||
"total_cost_usd": run["total_cost_usd"],
|
|
||||||
"config": json.loads(run["config_json"]),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def generations_df(repo: Repository, run_id: str) -> pd.DataFrame:
|
|
||||||
return pd.DataFrame(repo.list_generations(run_id))
|
|
||||||
|
|
||||||
|
|
||||||
def evaluations_df(repo: Repository, run_id: str) -> pd.DataFrame:
|
|
||||||
return pd.DataFrame(repo.list_evaluations(run_id))
|
|
||||||
|
|
||||||
|
|
||||||
def genomes_df(
|
|
||||||
repo: Repository, run_id: str, generation_idx: int | None = None
|
|
||||||
) -> pd.DataFrame:
|
|
||||||
rows = repo.list_genomes(run_id, generation_idx)
|
|
||||||
flat: list[dict[str, Any]] = []
|
|
||||||
for r in rows:
|
|
||||||
payload = json.loads(r["payload_json"])
|
|
||||||
flat.append(
|
|
||||||
{
|
|
||||||
"id": r["id"],
|
|
||||||
"generation_idx": r["generation_idx"],
|
|
||||||
**payload,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
return pd.DataFrame(flat)
|
|
||||||
|
|
||||||
|
|
||||||
def _paper_conn(db_path: str | Path) -> sqlite3.Connection:
|
def _paper_conn(db_path: str | Path) -> sqlite3.Connection:
|
||||||
conn = sqlite3.connect(str(db_path))
|
conn = sqlite3.connect(str(db_path))
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
"""NiceGUI dashboard — port progressivo da Streamlit.
|
"""Strategy Crypto Dashboard — paper-trading page: /.
|
||||||
|
|
||||||
Avvio: ``uv run python -m strategy_crypto.frontend.nicegui_app``
|
Avvio: ``uv run python -m strategy_crypto.frontend.nicegui_app``
|
||||||
Default port 8080. Streamlit resta su 8501 durante la migrazione.
|
Default port 8080. Legge SOLO strategy_crypto.db (paper_trading_* tables).
|
||||||
|
|
||||||
Riusa ``dashboard.data`` (Repository helpers) senza modifiche al backend.
|
|
||||||
|
|
||||||
Palette "Neon Trading Dashboard" (ispirata screenshot 2026-05-11):
|
Palette "Neon Trading Dashboard" (ispirata screenshot 2026-05-11):
|
||||||
- BG: #0A0A0F (near-black con tinge blu)
|
- BG: #0A0A0F (near-black con tinge blu)
|
||||||
@@ -18,8 +16,6 @@ Palette "Neon Trading Dashboard" (ispirata screenshot 2026-05-11):
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import html
|
|
||||||
import json
|
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
@@ -29,12 +25,6 @@ import plotly.graph_objects as go # type: ignore[import-untyped]
|
|||||||
from nicegui import app, ui
|
from nicegui import app, ui
|
||||||
|
|
||||||
from strategy_crypto.frontend.data import (
|
from strategy_crypto.frontend.data import (
|
||||||
evaluations_df,
|
|
||||||
generations_df,
|
|
||||||
genomes_df,
|
|
||||||
get_repo,
|
|
||||||
get_run_overview,
|
|
||||||
list_runs_df,
|
|
||||||
paper_equity_df,
|
paper_equity_df,
|
||||||
paper_positions_df,
|
paper_positions_df,
|
||||||
paper_run_summary,
|
paper_run_summary,
|
||||||
@@ -42,840 +32,21 @@ from strategy_crypto.frontend.data import (
|
|||||||
paper_ticks_df,
|
paper_ticks_df,
|
||||||
paper_trades_df,
|
paper_trades_df,
|
||||||
)
|
)
|
||||||
|
from multi_swarm_core.dashboard.theme import (
|
||||||
|
COLOR_PRIMARY,
|
||||||
|
COLOR_SURFACE,
|
||||||
|
COLOR_SURFACE_2,
|
||||||
|
COLOR_TEXT,
|
||||||
|
COLOR_TEXT_MUTED,
|
||||||
|
_STATUS_BADGE,
|
||||||
|
_apply_theme,
|
||||||
|
_build_header,
|
||||||
|
)
|
||||||
|
|
||||||
# Dual-DB: GA core e paper strategy_crypto vivono in DB separati.
|
|
||||||
GA_DB_PATH = os.environ.get("GA_DB_PATH", "./state/runs.db")
|
|
||||||
PAPER_DB_PATH = os.environ.get("STRATEGY_CRYPTO_DB_PATH", "./state/strategy_crypto.db")
|
PAPER_DB_PATH = os.environ.get("STRATEGY_CRYPTO_DB_PATH", "./state/strategy_crypto.db")
|
||||||
# Subpath per Traefik: "" in dev, "/strategy_crypto_gui" in prod.
|
|
||||||
DASHBOARD_ROOT_PATH = os.environ.get("DASHBOARD_ROOT_PATH", "")
|
DASHBOARD_ROOT_PATH = os.environ.get("DASHBOARD_ROOT_PATH", "")
|
||||||
REFRESH_INTERVAL_S = 3.0
|
REFRESH_INTERVAL_S = 3.0
|
||||||
|
|
||||||
# --- Neon Trading Dashboard palette ---
|
|
||||||
COLOR_BG = "#0A0A0F"
|
|
||||||
COLOR_SURFACE = "#13131A"
|
|
||||||
COLOR_SURFACE_2 = "#1C1C26"
|
|
||||||
COLOR_BORDER = "rgba(255, 45, 135, 0.12)"
|
|
||||||
COLOR_BORDER_HOVER = "rgba(255, 45, 135, 0.45)"
|
|
||||||
COLOR_PRIMARY = "#FF2D87"
|
|
||||||
COLOR_SECONDARY = "#00D9FF"
|
|
||||||
COLOR_ACCENT = "#FFB800"
|
|
||||||
COLOR_SUCCESS = "#00E676"
|
|
||||||
COLOR_DANGER = "#FF3D60"
|
|
||||||
COLOR_TEXT = "#FFFFFF"
|
|
||||||
COLOR_TEXT_MUTED = "#7A7A8C"
|
|
||||||
|
|
||||||
_STATUS_BADGE: dict[str, tuple[str, str]] = {
|
|
||||||
"running": ("● running", "positive"),
|
|
||||||
"completed": ("✓ completed", "positive"),
|
|
||||||
"failed": ("✕ failed", "negative"),
|
|
||||||
}
|
|
||||||
|
|
||||||
_CUSTOM_CSS = f"""
|
|
||||||
<style>
|
|
||||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap');
|
|
||||||
|
|
||||||
html, body, .q-page, .q-card, .q-btn, .q-field, .q-table, .text-h4, .text-h6, .text-subtitle1, .text-caption, .text-body1, .nav-link, .brand, label, p, span, div {{
|
|
||||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
|
|
||||||
letter-spacing: -0.01em;
|
|
||||||
}}
|
|
||||||
.material-icons, .material-icons-outlined, .material-symbols-outlined, .q-icon, i.q-icon, i[class*="material"] {{
|
|
||||||
font-family: 'Material Icons' !important;
|
|
||||||
font-feature-settings: 'liga';
|
|
||||||
letter-spacing: normal !important;
|
|
||||||
}}
|
|
||||||
code, pre, .q-code, .nicegui-code {{ font-family: 'JetBrains Mono', 'Fira Code', monospace !important; font-size: 13.5px !important; }}
|
|
||||||
|
|
||||||
body, .q-page-container, .q-page {{
|
|
||||||
background: {COLOR_BG} !important;
|
|
||||||
color: {COLOR_TEXT};
|
|
||||||
background-image:
|
|
||||||
radial-gradient(ellipse 800px 400px at 20% 0%, rgba(255, 45, 135, 0.08) 0%, transparent 60%),
|
|
||||||
radial-gradient(ellipse 600px 400px at 80% 100%, rgba(0, 217, 255, 0.06) 0%, transparent 60%);
|
|
||||||
background-attachment: fixed;
|
|
||||||
}}
|
|
||||||
|
|
||||||
.q-card {{
|
|
||||||
background: {COLOR_SURFACE} !important;
|
|
||||||
color: {COLOR_TEXT} !important;
|
|
||||||
border: 1px solid {COLOR_BORDER};
|
|
||||||
border-radius: 14px !important;
|
|
||||||
box-shadow:
|
|
||||||
0 1px 2px rgba(0,0,0,0.5),
|
|
||||||
0 8px 24px rgba(0,0,0,0.25),
|
|
||||||
inset 0 1px 0 rgba(255,255,255,0.04);
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
position: relative;
|
|
||||||
overflow: hidden;
|
|
||||||
}}
|
|
||||||
.q-card::before {{
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
top: 0; left: 0; right: 0;
|
|
||||||
height: 1px;
|
|
||||||
background: linear-gradient(90deg, transparent, rgba(255, 45, 135, 0.4), transparent);
|
|
||||||
opacity: 0.5;
|
|
||||||
}}
|
|
||||||
.q-card:hover {{
|
|
||||||
border-color: rgba(255, 45, 135, 0.5);
|
|
||||||
box-shadow:
|
|
||||||
0 1px 2px rgba(0,0,0,0.5),
|
|
||||||
0 8px 32px rgba(255, 45, 135, 0.15),
|
|
||||||
inset 0 1px 0 rgba(255,255,255,0.05);
|
|
||||||
}}
|
|
||||||
|
|
||||||
.metric-card {{
|
|
||||||
padding: 20px 16px;
|
|
||||||
text-align: left;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 6px;
|
|
||||||
min-width: 140px;
|
|
||||||
}}
|
|
||||||
.metric-card .text-caption {{
|
|
||||||
color: {COLOR_TEXT_MUTED} !important;
|
|
||||||
font-size: 11px !important;
|
|
||||||
font-weight: 500 !important;
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.06em;
|
|
||||||
}}
|
|
||||||
.metric-card .text-h4 {{
|
|
||||||
color: {COLOR_TEXT} !important;
|
|
||||||
font-weight: 600 !important;
|
|
||||||
font-size: 26px !important;
|
|
||||||
line-height: 1.2 !important;
|
|
||||||
font-feature-settings: 'tnum';
|
|
||||||
}}
|
|
||||||
.metric-card.accent-cyan .text-h4 {{ color: {COLOR_PRIMARY} !important; }}
|
|
||||||
.metric-card.accent-purple .text-h4 {{ color: {COLOR_SECONDARY} !important; }}
|
|
||||||
.metric-card.accent-amber .text-h4 {{ color: {COLOR_ACCENT} !important; }}
|
|
||||||
.metric-card.accent-green .text-h4 {{ color: {COLOR_SUCCESS} !important; }}
|
|
||||||
|
|
||||||
.q-header {{
|
|
||||||
background: rgba(10, 10, 15, 0.75) !important;
|
|
||||||
backdrop-filter: blur(20px) saturate(180%);
|
|
||||||
-webkit-backdrop-filter: blur(20px) saturate(180%);
|
|
||||||
border-bottom: 1px solid {COLOR_BORDER} !important;
|
|
||||||
box-shadow: 0 1px 0 rgba(255, 45, 135, 0.15) !important;
|
|
||||||
}}
|
|
||||||
|
|
||||||
.nav-link {{
|
|
||||||
color: {COLOR_TEXT_MUTED} !important;
|
|
||||||
padding: 8px 14px;
|
|
||||||
border-radius: 8px;
|
|
||||||
text-decoration: none;
|
|
||||||
font-size: 13.5px;
|
|
||||||
font-weight: 500;
|
|
||||||
transition: all 0.15s ease;
|
|
||||||
position: relative;
|
|
||||||
}}
|
|
||||||
.nav-link:hover {{
|
|
||||||
color: {COLOR_TEXT} !important;
|
|
||||||
background: {COLOR_SURFACE_2};
|
|
||||||
}}
|
|
||||||
.nav-link.active {{
|
|
||||||
color: {COLOR_PRIMARY} !important;
|
|
||||||
background: rgba(255, 45, 135, 0.08);
|
|
||||||
}}
|
|
||||||
.nav-link.active::after {{
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
bottom: -16px;
|
|
||||||
left: 14px;
|
|
||||||
right: 14px;
|
|
||||||
height: 2px;
|
|
||||||
background: {COLOR_PRIMARY};
|
|
||||||
border-radius: 2px 2px 0 0;
|
|
||||||
}}
|
|
||||||
|
|
||||||
.brand {{
|
|
||||||
color: {COLOR_TEXT};
|
|
||||||
font-weight: 700;
|
|
||||||
font-size: 15px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
}}
|
|
||||||
.brand-dot {{
|
|
||||||
width: 10px;
|
|
||||||
height: 10px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: {COLOR_PRIMARY};
|
|
||||||
box-shadow: 0 0 16px {COLOR_PRIMARY}, 0 0 4px {COLOR_PRIMARY};
|
|
||||||
animation: pulse-pink 2s ease-in-out infinite;
|
|
||||||
}}
|
|
||||||
@keyframes pulse-pink {{
|
|
||||||
0%, 100% {{ box-shadow: 0 0 16px {COLOR_PRIMARY}, 0 0 4px {COLOR_PRIMARY}; }}
|
|
||||||
50% {{ box-shadow: 0 0 24px {COLOR_PRIMARY}, 0 0 8px {COLOR_PRIMARY}; }}
|
|
||||||
}}
|
|
||||||
|
|
||||||
.q-linear-progress {{ height: 8px !important; border-radius: 6px !important; }}
|
|
||||||
.q-linear-progress__track {{ background: {COLOR_SURFACE_2} !important; }}
|
|
||||||
.q-linear-progress__model {{ border-radius: 6px !important; }}
|
|
||||||
|
|
||||||
.q-separator {{ background: {COLOR_BORDER} !important; }}
|
|
||||||
|
|
||||||
.q-field--outlined .q-field__control {{
|
|
||||||
background: {COLOR_SURFACE} !important;
|
|
||||||
border-radius: 8px !important;
|
|
||||||
}}
|
|
||||||
.q-field--outlined .q-field__control:before {{ border-color: {COLOR_BORDER} !important; }}
|
|
||||||
.q-field--outlined.q-field--focused .q-field__control:after {{ border-color: {COLOR_PRIMARY} !important; }}
|
|
||||||
.q-field__label {{ color: {COLOR_TEXT_MUTED} !important; }}
|
|
||||||
.q-field__native, .q-field__input {{ color: {COLOR_TEXT} !important; }}
|
|
||||||
|
|
||||||
.q-btn {{ border-radius: 8px !important; font-weight: 500 !important; text-transform: none !important; letter-spacing: 0 !important; }}
|
|
||||||
|
|
||||||
.q-table {{ background: transparent !important; color: {COLOR_TEXT} !important; }}
|
|
||||||
.q-table thead tr {{ background: {COLOR_SURFACE_2} !important; }}
|
|
||||||
.q-table th {{
|
|
||||||
color: {COLOR_TEXT_MUTED} !important;
|
|
||||||
font-size: 11px !important;
|
|
||||||
font-weight: 600 !important;
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.06em;
|
|
||||||
}}
|
|
||||||
.q-table tbody tr {{ transition: background 0.15s; }}
|
|
||||||
.q-table tbody tr:hover {{ background: rgba(255, 45, 135, 0.05) !important; }}
|
|
||||||
.q-table tbody tr.selected {{ background: rgba(255, 45, 135, 0.12) !important; }}
|
|
||||||
.q-table td {{ border-bottom: 1px solid {COLOR_BORDER} !important; font-feature-settings: 'tnum'; }}
|
|
||||||
|
|
||||||
.text-h6 {{ font-weight: 600 !important; letter-spacing: -0.015em !important; }}
|
|
||||||
.text-subtitle1 {{
|
|
||||||
color: {COLOR_TEXT_MUTED} !important;
|
|
||||||
font-size: 11px !important;
|
|
||||||
font-weight: 600 !important;
|
|
||||||
text-transform: uppercase !important;
|
|
||||||
letter-spacing: 0.08em !important;
|
|
||||||
margin-bottom: 8px !important;
|
|
||||||
}}
|
|
||||||
|
|
||||||
code, pre, .nicegui-code {{
|
|
||||||
background: #1A1A24 !important;
|
|
||||||
color: {COLOR_TEXT} !important;
|
|
||||||
border: 1px solid {COLOR_BORDER};
|
|
||||||
border-radius: 10px !important;
|
|
||||||
padding: 16px !important;
|
|
||||||
font-size: 13.5px !important;
|
|
||||||
line-height: 1.6 !important;
|
|
||||||
}}
|
|
||||||
.hljs {{ background: transparent !important; color: {COLOR_TEXT} !important; }}
|
|
||||||
.hljs-attr, .hljs-attribute {{ color: {COLOR_SECONDARY} !important; font-weight: 500; }}
|
|
||||||
.hljs-string {{ color: {COLOR_SUCCESS} !important; }}
|
|
||||||
.hljs-number, .hljs-literal {{ color: {COLOR_PRIMARY} !important; font-weight: 500; }}
|
|
||||||
.hljs-keyword, .hljs-built_in {{ color: {COLOR_ACCENT} !important; }}
|
|
||||||
.hljs-punctuation, .hljs-meta {{ color: {COLOR_TEXT_MUTED} !important; }}
|
|
||||||
.hljs-comment {{ color: {COLOR_TEXT_MUTED} !important; font-style: italic; }}
|
|
||||||
.hljs-name, .hljs-title {{ color: {COLOR_PRIMARY} !important; }}
|
|
||||||
|
|
||||||
/* Prism.js tokens (NiceGUI usa Prism per ui.code) */
|
|
||||||
.token.property, .token.attr-name, .token.tag {{ color: {COLOR_SECONDARY} !important; font-weight: 500; }}
|
|
||||||
.token.string, .token.url {{ color: {COLOR_SUCCESS} !important; }}
|
|
||||||
.token.number, .token.boolean, .token.null, .token.symbol {{ color: {COLOR_PRIMARY} !important; font-weight: 500; }}
|
|
||||||
.token.keyword, .token.constant, .token.builtin, .token.atrule {{ color: {COLOR_ACCENT} !important; }}
|
|
||||||
.token.punctuation, .token.operator {{ color: {COLOR_TEXT_MUTED} !important; }}
|
|
||||||
.token.comment {{ color: {COLOR_TEXT_MUTED} !important; font-style: italic; }}
|
|
||||||
.token.function, .token.class-name {{ color: {COLOR_PRIMARY} !important; }}
|
|
||||||
pre[class*="language-"], code[class*="language-"] {{
|
|
||||||
color: {COLOR_TEXT} !important;
|
|
||||||
text-shadow: none !important;
|
|
||||||
}}
|
|
||||||
|
|
||||||
.q-badge {{ border-radius: 6px !important; font-weight: 500 !important; padding: 4px 10px !important; font-size: 12px !important; }}
|
|
||||||
|
|
||||||
.config-block {{
|
|
||||||
background: #1A1A24;
|
|
||||||
color: {COLOR_TEXT};
|
|
||||||
border: 1px solid {COLOR_BORDER};
|
|
||||||
border-radius: 10px;
|
|
||||||
padding: 18px 20px;
|
|
||||||
font-family: 'JetBrains Mono', 'Fira Code', monospace;
|
|
||||||
font-size: 13.5px;
|
|
||||||
line-height: 1.7;
|
|
||||||
overflow-x: auto;
|
|
||||||
white-space: pre;
|
|
||||||
margin: 0;
|
|
||||||
}}
|
|
||||||
.config-block .cb-key {{ color: {COLOR_SECONDARY}; font-weight: 500; }}
|
|
||||||
.config-block .cb-string {{ color: {COLOR_SUCCESS}; }}
|
|
||||||
.config-block .cb-number {{ color: {COLOR_PRIMARY}; font-weight: 500; }}
|
|
||||||
.config-block .cb-bool {{ color: {COLOR_ACCENT}; }}
|
|
||||||
.config-block .cb-null {{ color: {COLOR_ACCENT}; font-style: italic; }}
|
|
||||||
.config-block .cb-punct {{ color: {COLOR_TEXT_MUTED}; }}
|
|
||||||
|
|
||||||
.raw-block {{
|
|
||||||
background: #1A1A24;
|
|
||||||
color: {COLOR_TEXT};
|
|
||||||
border: 1px solid {COLOR_BORDER};
|
|
||||||
border-radius: 10px;
|
|
||||||
padding: 18px 20px;
|
|
||||||
font-family: 'JetBrains Mono', 'Fira Code', monospace;
|
|
||||||
font-size: 13px;
|
|
||||||
line-height: 1.6;
|
|
||||||
overflow-x: auto;
|
|
||||||
white-space: pre-wrap;
|
|
||||||
word-break: break-word;
|
|
||||||
max-height: 400px;
|
|
||||||
overflow-y: auto;
|
|
||||||
margin: 0;
|
|
||||||
}}
|
|
||||||
</style>
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
def _json_to_html(obj: Any, indent: int = 0) -> str:
|
|
||||||
"""Render JSON con span colorati espliciti. Garantisce leggibilità ovunque."""
|
|
||||||
pad = " " * indent
|
|
||||||
inner_pad = " " * (indent + 1)
|
|
||||||
if isinstance(obj, dict):
|
|
||||||
if not obj:
|
|
||||||
return '<span class="cb-punct">{}</span>'
|
|
||||||
items = []
|
|
||||||
for k, v in obj.items():
|
|
||||||
key = f'<span class="cb-key">"{html.escape(str(k))}"</span>'
|
|
||||||
val = _json_to_html(v, indent + 1)
|
|
||||||
items.append(f"{inner_pad}{key}<span class=\"cb-punct\">:</span> {val}")
|
|
||||||
return ('<span class="cb-punct">{</span>\n'
|
|
||||||
+ '<span class="cb-punct">,</span>\n'.join(items)
|
|
||||||
+ f'\n{pad}<span class="cb-punct">}}</span>')
|
|
||||||
if isinstance(obj, list):
|
|
||||||
if not obj:
|
|
||||||
return '<span class="cb-punct">[]</span>'
|
|
||||||
items = [_json_to_html(x, indent + 1) for x in obj]
|
|
||||||
return ('<span class="cb-punct">[</span>\n'
|
|
||||||
+ '<span class="cb-punct">,</span>\n'.join(inner_pad + i for i in items)
|
|
||||||
+ f'\n{pad}<span class="cb-punct">]</span>')
|
|
||||||
if isinstance(obj, bool):
|
|
||||||
return f'<span class="cb-bool">{str(obj).lower()}</span>'
|
|
||||||
if obj is None:
|
|
||||||
return '<span class="cb-null">null</span>'
|
|
||||||
if isinstance(obj, (int, float)):
|
|
||||||
return f'<span class="cb-number">{obj}</span>'
|
|
||||||
return f'<span class="cb-string">"{html.escape(str(obj))}"</span>'
|
|
||||||
|
|
||||||
|
|
||||||
def _apply_theme() -> None:
|
|
||||||
ui.add_head_html(_CUSTOM_CSS)
|
|
||||||
ui.dark_mode().enable()
|
|
||||||
ui.colors(
|
|
||||||
primary=COLOR_PRIMARY,
|
|
||||||
secondary=COLOR_SECONDARY,
|
|
||||||
accent=COLOR_ACCENT,
|
|
||||||
dark=COLOR_BG,
|
|
||||||
dark_page=COLOR_BG,
|
|
||||||
positive=COLOR_SUCCESS,
|
|
||||||
negative=COLOR_DANGER,
|
|
||||||
info=COLOR_PRIMARY,
|
|
||||||
warning=COLOR_ACCENT,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _build_header(active: str) -> None:
|
|
||||||
with ui.header().classes("items-center justify-between q-px-lg q-py-md"):
|
|
||||||
with ui.row().classes("items-center gap-8"):
|
|
||||||
with ui.row().classes("items-center gap-2").classes("brand"):
|
|
||||||
ui.html('<span class="brand-dot"></span>')
|
|
||||||
ui.html('<span class="brand">Multi-Swarm <span style="color:'
|
|
||||||
+ COLOR_TEXT_MUTED + ';font-weight:400;">/ Coevolutivo</span></span>')
|
|
||||||
with ui.row().classes("items-center gap-1"):
|
|
||||||
for path, label in (
|
|
||||||
("/", "Overview"),
|
|
||||||
("/convergence", "Convergence"),
|
|
||||||
("/genomes", "Genomes"),
|
|
||||||
("/paper", "Paper"),
|
|
||||||
):
|
|
||||||
cls = "nav-link active" if active == path else "nav-link"
|
|
||||||
ui.link(label, path).classes(cls)
|
|
||||||
with ui.row().classes("items-center gap-3"):
|
|
||||||
ui.html(f'<span style="color:{COLOR_TEXT_MUTED};font-size:12px;'
|
|
||||||
f'font-family:JetBrains Mono,monospace;">'
|
|
||||||
f'⛁ {Path(GA_DB_PATH).resolve().name} + {Path(PAPER_DB_PATH).resolve().name}</span>')
|
|
||||||
|
|
||||||
|
|
||||||
def _runs_options() -> dict[str, str]:
|
|
||||||
repo = get_repo(GA_DB_PATH)
|
|
||||||
runs = list_runs_df(repo)
|
|
||||||
if runs.empty:
|
|
||||||
return {}
|
|
||||||
return {
|
|
||||||
row["id"]: f"{row['name']} — {row['status']} ({row['started_at'][:16]})"
|
|
||||||
for _, row in runs.iterrows()
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def _snapshot(run_id: str) -> dict[str, Any]:
|
|
||||||
repo = get_repo(GA_DB_PATH)
|
|
||||||
ov = get_run_overview(repo, run_id)
|
|
||||||
evals = evaluations_df(repo, run_id)
|
|
||||||
gens = generations_df(repo, run_id)
|
|
||||||
|
|
||||||
cfg = ov["config"]
|
|
||||||
pop_size = int(cfg.get("population_size", 0))
|
|
||||||
n_gens = int(cfg.get("n_generations", 0))
|
|
||||||
evals_total = max(pop_size * n_gens, 1)
|
|
||||||
evals_done = len(evals)
|
|
||||||
gens_done = int(gens["completed_at"].notna().sum()) if not gens.empty else 0
|
|
||||||
# runs.total_cost_usd è 0 finché complete_run non viene chiamato.
|
|
||||||
# Per le run in corso leggiamo la somma live da cost_records.
|
|
||||||
live_cost = float(repo.total_cost(run_id)) if ov["status"] == "running" else float(
|
|
||||||
ov["total_cost_usd"]
|
|
||||||
)
|
|
||||||
|
|
||||||
top_fit = float(evals["fitness"].max()) if evals_done else float("nan")
|
|
||||||
median_fit = float(evals["fitness"].median()) if evals_done else float("nan")
|
|
||||||
parse_success = (
|
|
||||||
100.0 * float(evals["parse_error"].isna().sum()) / evals_done if evals_done else 0.0
|
|
||||||
)
|
|
||||||
|
|
||||||
return {
|
|
||||||
"status": ov["status"],
|
|
||||||
"name": cfg.get("run_name", "—"),
|
|
||||||
"started_at": ov["started_at"],
|
|
||||||
"completed_at": ov["completed_at"] or "—",
|
|
||||||
"cost_usd": live_cost,
|
|
||||||
"pop_size": pop_size,
|
|
||||||
"n_gens": n_gens,
|
|
||||||
"evals_done": evals_done,
|
|
||||||
"evals_total": evals_total,
|
|
||||||
"gens_done": gens_done,
|
|
||||||
"top_fit": top_fit,
|
|
||||||
"median_fit": median_fit,
|
|
||||||
"parse_success": parse_success,
|
|
||||||
"config": cfg,
|
|
||||||
"gens_df": gens,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def _convergence_figure(gens_df: Any) -> go.Figure:
|
|
||||||
fig = go.Figure()
|
|
||||||
if gens_df.empty:
|
|
||||||
fig.add_annotation(
|
|
||||||
text="Nessuna generazione registrata", x=0.5, y=0.5, showarrow=False,
|
|
||||||
font={"color": COLOR_TEXT_MUTED, "size": 14},
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
fig.add_trace(
|
|
||||||
go.Scatter(
|
|
||||||
x=gens_df["generation_idx"], y=gens_df["fitness_max"],
|
|
||||||
name="max", mode="lines+markers",
|
|
||||||
line={"color": COLOR_PRIMARY, "width": 3, "shape": "spline", "smoothing": 0.6},
|
|
||||||
marker={"size": 9, "color": COLOR_PRIMARY,
|
|
||||||
"line": {"color": "#fff", "width": 1}},
|
|
||||||
fill="tozeroy",
|
|
||||||
fillcolor="rgba(255, 45, 135, 0.12)",
|
|
||||||
)
|
|
||||||
)
|
|
||||||
fig.add_trace(
|
|
||||||
go.Scatter(
|
|
||||||
x=gens_df["generation_idx"], y=gens_df["fitness_p90"],
|
|
||||||
name="p90", mode="lines+markers",
|
|
||||||
line={"color": COLOR_ACCENT, "width": 2, "dash": "dot", "shape": "spline"},
|
|
||||||
marker={"size": 7, "color": COLOR_ACCENT},
|
|
||||||
)
|
|
||||||
)
|
|
||||||
fig.add_trace(
|
|
||||||
go.Scatter(
|
|
||||||
x=gens_df["generation_idx"], y=gens_df["fitness_median"],
|
|
||||||
name="median", mode="lines+markers",
|
|
||||||
line={"color": COLOR_SECONDARY, "width": 2, "shape": "spline"},
|
|
||||||
marker={"size": 7, "color": COLOR_SECONDARY},
|
|
||||||
)
|
|
||||||
)
|
|
||||||
fig.update_layout(
|
|
||||||
template="plotly_dark",
|
|
||||||
paper_bgcolor=COLOR_SURFACE,
|
|
||||||
plot_bgcolor=COLOR_SURFACE,
|
|
||||||
font={"color": COLOR_TEXT},
|
|
||||||
xaxis={"title": "generation", "gridcolor": "rgba(148, 163, 184, 0.08)", "dtick": 1},
|
|
||||||
yaxis={"title": "fitness", "gridcolor": "rgba(148, 163, 184, 0.08)"},
|
|
||||||
title={"text": "Fitness convergence", "font": {"color": COLOR_TEXT, "size": 18}},
|
|
||||||
legend={"bgcolor": "rgba(19, 19, 26, 0.95)", "bordercolor": COLOR_PRIMARY, "borderwidth": 1},
|
|
||||||
margin={"l": 50, "r": 30, "t": 50, "b": 50},
|
|
||||||
)
|
|
||||||
return fig
|
|
||||||
|
|
||||||
|
|
||||||
def _entropy_figure(gens_df: Any) -> go.Figure:
|
|
||||||
fig = go.Figure()
|
|
||||||
if not gens_df.empty:
|
|
||||||
fig.add_trace(
|
|
||||||
go.Scatter(
|
|
||||||
x=gens_df["generation_idx"], y=gens_df["entropy"],
|
|
||||||
mode="lines+markers",
|
|
||||||
line={"color": COLOR_SECONDARY, "width": 3, "shape": "spline", "smoothing": 0.6},
|
|
||||||
marker={"size": 9, "color": COLOR_SECONDARY,
|
|
||||||
"line": {"color": "#fff", "width": 1}},
|
|
||||||
fill="tozeroy",
|
|
||||||
fillcolor="rgba(0, 217, 255, 0.12)",
|
|
||||||
name="entropy",
|
|
||||||
)
|
|
||||||
)
|
|
||||||
fig.add_hline(
|
|
||||||
y=0.5, line_dash="dash", line_color=COLOR_ACCENT,
|
|
||||||
annotation_text="gate threshold (0.5)",
|
|
||||||
annotation_font_color=COLOR_ACCENT,
|
|
||||||
)
|
|
||||||
fig.update_layout(
|
|
||||||
template="plotly_dark",
|
|
||||||
paper_bgcolor=COLOR_SURFACE,
|
|
||||||
plot_bgcolor=COLOR_SURFACE,
|
|
||||||
font={"color": COLOR_TEXT},
|
|
||||||
xaxis={"title": "generation", "gridcolor": "rgba(148, 163, 184, 0.08)", "dtick": 1},
|
|
||||||
yaxis={"title": "entropy", "gridcolor": "rgba(148, 163, 184, 0.08)"},
|
|
||||||
title={"text": "Diversity (fitness entropy)", "font": {"color": COLOR_TEXT, "size": 18}},
|
|
||||||
margin={"l": 50, "r": 30, "t": 50, "b": 50},
|
|
||||||
)
|
|
||||||
return fig
|
|
||||||
|
|
||||||
|
|
||||||
@ui.page("/")
|
|
||||||
def index() -> None:
|
|
||||||
_apply_theme()
|
|
||||||
_build_header(active="/")
|
|
||||||
|
|
||||||
options = _runs_options()
|
|
||||||
if not options:
|
|
||||||
ui.label("Nessuna run nel database.").classes("text-h5")
|
|
||||||
return
|
|
||||||
|
|
||||||
state: dict[str, Any] = {"run_id": next(iter(options))}
|
|
||||||
|
|
||||||
with ui.row().classes("w-full items-center gap-4 q-mb-md"):
|
|
||||||
select = ui.select(options=options, value=state["run_id"], label="Run").classes(
|
|
||||||
"flex-grow"
|
|
||||||
)
|
|
||||||
status_badge = ui.badge("…", color="primary").classes("text-body1 q-pa-sm")
|
|
||||||
ui.button("🔄 Refresh", on_click=lambda: refresh()).props("outline color=primary")
|
|
||||||
|
|
||||||
with ui.card().classes("w-full"):
|
|
||||||
ui.label("Progresso run").classes("text-subtitle1")
|
|
||||||
gen_label = ui.label("Generations: 0/0")
|
|
||||||
gen_bar = ui.linear_progress(0.0, show_value=False).props("size=20px color=primary")
|
|
||||||
eval_label = ui.label("Evaluations: 0/0 (0.0%)")
|
|
||||||
eval_bar = ui.linear_progress(0.0, show_value=False).props("size=20px color=accent")
|
|
||||||
|
|
||||||
with ui.row().classes("w-full gap-4"):
|
|
||||||
with ui.card().classes("flex-grow metric-card accent-cyan"):
|
|
||||||
ui.label("Top fitness").classes("text-caption")
|
|
||||||
top_lbl = ui.label("—").classes("text-h4")
|
|
||||||
with ui.card().classes("flex-grow metric-card accent-purple"):
|
|
||||||
ui.label("Median fitness").classes("text-caption")
|
|
||||||
median_lbl = ui.label("—").classes("text-h4")
|
|
||||||
with ui.card().classes("flex-grow metric-card accent-amber"):
|
|
||||||
ui.label("Parse success").classes("text-caption")
|
|
||||||
parse_lbl = ui.label("—").classes("text-h4")
|
|
||||||
with ui.card().classes("flex-grow metric-card accent-green"):
|
|
||||||
ui.label("Cost (USD)").classes("text-caption")
|
|
||||||
cost_lbl = ui.label("—").classes("text-h4")
|
|
||||||
|
|
||||||
with ui.row().classes("w-full gap-4 q-mt-md"):
|
|
||||||
started_lbl = ui.label("Started: —")
|
|
||||||
completed_lbl = ui.label("Completed: —")
|
|
||||||
ui.separator()
|
|
||||||
ui.label("Config").classes("text-subtitle1")
|
|
||||||
cfg_code = ui.html('<pre class="config-block"></pre>').classes("w-full")
|
|
||||||
|
|
||||||
def refresh() -> None:
|
|
||||||
run_id = select.value
|
|
||||||
if not run_id:
|
|
||||||
return
|
|
||||||
try:
|
|
||||||
s = _snapshot(run_id)
|
|
||||||
except Exception as e: # noqa: BLE001
|
|
||||||
ui.notify(f"Errore: {e}", type="negative")
|
|
||||||
return
|
|
||||||
|
|
||||||
text, color = _STATUS_BADGE.get(s["status"], (s["status"], "primary"))
|
|
||||||
status_badge.text = text
|
|
||||||
status_badge.props(f"color={color}")
|
|
||||||
|
|
||||||
gen_frac = min(s["gens_done"] / max(s["n_gens"], 1), 1.0)
|
|
||||||
eval_frac = min(s["evals_done"] / s["evals_total"], 1.0)
|
|
||||||
gen_bar.value = gen_frac
|
|
||||||
eval_bar.value = eval_frac
|
|
||||||
gen_label.text = f"Generations: {s['gens_done']}/{s['n_gens']}"
|
|
||||||
eval_label.text = (
|
|
||||||
f"Evaluations: {s['evals_done']}/{s['evals_total']} ({100 * eval_frac:.1f}%)"
|
|
||||||
)
|
|
||||||
|
|
||||||
top_lbl.text = f"{s['top_fit']:.4f}" if s["evals_done"] else "—"
|
|
||||||
median_lbl.text = f"{s['median_fit']:.4f}" if s["evals_done"] else "—"
|
|
||||||
parse_lbl.text = f"{s['parse_success']:.1f}%" if s["evals_done"] else "—"
|
|
||||||
cost_lbl.text = f"${s['cost_usd']:.4f}"
|
|
||||||
|
|
||||||
started_lbl.text = f"Started: {s['started_at']}"
|
|
||||||
completed_lbl.text = f"Completed: {s['completed_at']}"
|
|
||||||
cfg_code.content = f'<pre class="config-block">{_json_to_html(s["config"])}</pre>'
|
|
||||||
|
|
||||||
def on_select_change() -> None:
|
|
||||||
state["run_id"] = select.value
|
|
||||||
refresh()
|
|
||||||
|
|
||||||
select.on_value_change(on_select_change)
|
|
||||||
ui.timer(REFRESH_INTERVAL_S, refresh)
|
|
||||||
refresh()
|
|
||||||
|
|
||||||
|
|
||||||
@ui.page("/convergence")
|
|
||||||
def convergence() -> None:
|
|
||||||
_apply_theme()
|
|
||||||
_build_header(active="/convergence")
|
|
||||||
|
|
||||||
options = _runs_options()
|
|
||||||
if not options:
|
|
||||||
ui.label("Nessuna run nel database.").classes("text-h5")
|
|
||||||
return
|
|
||||||
|
|
||||||
state: dict[str, Any] = {"run_id": next(iter(options))}
|
|
||||||
|
|
||||||
with ui.row().classes("w-full items-center gap-4 q-mb-md"):
|
|
||||||
select = ui.select(options=options, value=state["run_id"], label="Run").classes(
|
|
||||||
"flex-grow"
|
|
||||||
)
|
|
||||||
gen_count_lbl = ui.label("Gens: 0/0").classes("text-body1").style(
|
|
||||||
f"color: {COLOR_PRIMARY}; font-weight: 600;"
|
|
||||||
)
|
|
||||||
ui.button("🔄 Refresh", on_click=lambda: refresh()).props("outline color=primary")
|
|
||||||
|
|
||||||
fitness_plot = ui.plotly(_convergence_figure(generations_df(get_repo(GA_DB_PATH), state["run_id"]))).classes("w-full")
|
|
||||||
entropy_plot = ui.plotly(_entropy_figure(generations_df(get_repo(GA_DB_PATH), state["run_id"]))).classes("w-full q-mt-md")
|
|
||||||
|
|
||||||
ui.separator()
|
|
||||||
ui.label("Tabella generazioni").classes("text-subtitle1 q-mt-md")
|
|
||||||
gens_table = ui.table(
|
|
||||||
columns=[
|
|
||||||
{"name": "generation_idx", "label": "gen", "field": "generation_idx", "sortable": True},
|
|
||||||
{"name": "n_genomes", "label": "n", "field": "n_genomes"},
|
|
||||||
{"name": "fitness_max", "label": "max", "field": "fitness_max"},
|
|
||||||
{"name": "fitness_p90", "label": "p90", "field": "fitness_p90"},
|
|
||||||
{"name": "fitness_median", "label": "median", "field": "fitness_median"},
|
|
||||||
{"name": "entropy", "label": "entropy", "field": "entropy"},
|
|
||||||
{"name": "completed_at", "label": "completed", "field": "completed_at"},
|
|
||||||
],
|
|
||||||
rows=[],
|
|
||||||
row_key="generation_idx",
|
|
||||||
).classes("w-full")
|
|
||||||
|
|
||||||
def refresh() -> None:
|
|
||||||
run_id = select.value
|
|
||||||
if not run_id:
|
|
||||||
return
|
|
||||||
try:
|
|
||||||
gens = generations_df(get_repo(GA_DB_PATH), run_id)
|
|
||||||
ov = get_run_overview(get_repo(GA_DB_PATH), run_id)
|
|
||||||
except Exception as e: # noqa: BLE001
|
|
||||||
ui.notify(f"Errore: {e}", type="negative")
|
|
||||||
return
|
|
||||||
|
|
||||||
n_gens = int(ov["config"].get("n_generations", 0))
|
|
||||||
gens_done = int(gens["completed_at"].notna().sum()) if not gens.empty else 0
|
|
||||||
gen_count_lbl.text = f"Gens: {gens_done}/{n_gens}"
|
|
||||||
|
|
||||||
fitness_plot.update_figure(_convergence_figure(gens))
|
|
||||||
entropy_plot.update_figure(_entropy_figure(gens))
|
|
||||||
|
|
||||||
if gens.empty:
|
|
||||||
gens_table.rows = []
|
|
||||||
else:
|
|
||||||
display_cols = [
|
|
||||||
"generation_idx", "n_genomes",
|
|
||||||
"fitness_max", "fitness_p90", "fitness_median",
|
|
||||||
"entropy", "completed_at",
|
|
||||||
]
|
|
||||||
gens_table.rows = [
|
|
||||||
{
|
|
||||||
col: (round(v, 6) if isinstance(v, float) else v)
|
|
||||||
for col, v in row.items()
|
|
||||||
if col in display_cols
|
|
||||||
}
|
|
||||||
for _, row in gens.iterrows()
|
|
||||||
]
|
|
||||||
gens_table.update()
|
|
||||||
|
|
||||||
def on_select_change() -> None:
|
|
||||||
state["run_id"] = select.value
|
|
||||||
refresh()
|
|
||||||
|
|
||||||
select.on_value_change(on_select_change)
|
|
||||||
ui.timer(REFRESH_INTERVAL_S, refresh)
|
|
||||||
refresh()
|
|
||||||
|
|
||||||
|
|
||||||
@ui.page("/genomes")
|
|
||||||
def genomes() -> None:
|
|
||||||
_apply_theme()
|
|
||||||
_build_header(active="/genomes")
|
|
||||||
|
|
||||||
options = _runs_options()
|
|
||||||
if not options:
|
|
||||||
ui.label("Nessuna run nel database.").classes("text-h5")
|
|
||||||
return
|
|
||||||
|
|
||||||
state: dict[str, Any] = {
|
|
||||||
"run_id": next(iter(options)),
|
|
||||||
"selected_gid": None,
|
|
||||||
"merged": None,
|
|
||||||
}
|
|
||||||
|
|
||||||
with ui.row().classes("w-full items-center gap-4 q-mb-md"):
|
|
||||||
select = ui.select(options=options, value=state["run_id"], label="Run").classes(
|
|
||||||
"flex-grow"
|
|
||||||
)
|
|
||||||
top_k_select = ui.select(
|
|
||||||
options={10: "Top 10", 25: "Top 25", 50: "Top 50"},
|
|
||||||
value=10,
|
|
||||||
label="Top K",
|
|
||||||
)
|
|
||||||
ui.button("🔄 Refresh", on_click=lambda: refresh()).props("outline color=primary")
|
|
||||||
|
|
||||||
ui.label("Top genomi per fitness").classes("text-subtitle1 q-mt-sm")
|
|
||||||
top_table = ui.table(
|
|
||||||
columns=[
|
|
||||||
{"name": "genome_id", "label": "id", "field": "genome_id", "align": "left"},
|
|
||||||
{"name": "fitness", "label": "fitness", "field": "fitness", "sortable": True},
|
|
||||||
{"name": "dsr", "label": "DSR", "field": "dsr"},
|
|
||||||
{"name": "sharpe", "label": "Sharpe", "field": "sharpe"},
|
|
||||||
{"name": "max_dd", "label": "max DD", "field": "max_dd"},
|
|
||||||
{"name": "n_trades", "label": "trades", "field": "n_trades"},
|
|
||||||
{"name": "cognitive_style", "label": "style", "field": "cognitive_style"},
|
|
||||||
{"name": "temperature", "label": "T", "field": "temperature"},
|
|
||||||
{"name": "lookback_window", "label": "lookback", "field": "lookback_window"},
|
|
||||||
],
|
|
||||||
rows=[],
|
|
||||||
row_key="genome_id",
|
|
||||||
selection="single",
|
|
||||||
).classes("w-full")
|
|
||||||
|
|
||||||
ui.separator().classes("q-my-md")
|
|
||||||
|
|
||||||
with ui.card().classes("w-full"):
|
|
||||||
ui.label("Ispezione genoma").classes("text-subtitle1")
|
|
||||||
detail_hint = ui.label("Seleziona un genoma dalla tabella sopra.").classes(
|
|
||||||
"text-caption"
|
|
||||||
).style(f"color: {COLOR_TEXT_MUTED};")
|
|
||||||
|
|
||||||
with ui.row().classes("w-full gap-4 q-mt-sm"):
|
|
||||||
with ui.card().classes("flex-grow metric-card accent-cyan"):
|
|
||||||
ui.label("fitness").classes("text-caption")
|
|
||||||
fit_lbl = ui.label("—").classes("text-h4")
|
|
||||||
with ui.card().classes("flex-grow metric-card accent-purple"):
|
|
||||||
ui.label("DSR").classes("text-caption")
|
|
||||||
dsr_lbl = ui.label("—").classes("text-h4")
|
|
||||||
with ui.card().classes("flex-grow metric-card accent-amber"):
|
|
||||||
ui.label("Sharpe").classes("text-caption")
|
|
||||||
sharpe_lbl = ui.label("—").classes("text-h4")
|
|
||||||
with ui.card().classes("flex-grow metric-card"):
|
|
||||||
ui.label("max DD").classes("text-caption")
|
|
||||||
dd_lbl = ui.label("—").classes("text-h4")
|
|
||||||
with ui.card().classes("flex-grow metric-card accent-green"):
|
|
||||||
ui.label("trades").classes("text-caption")
|
|
||||||
trades_lbl = ui.label("—").classes("text-h4")
|
|
||||||
with ui.card().classes("flex-grow metric-card"):
|
|
||||||
ui.label("style").classes("text-caption")
|
|
||||||
style_lbl = ui.label("—").classes("text-h4")
|
|
||||||
|
|
||||||
ui.label("System prompt").classes("text-subtitle1 q-mt-md")
|
|
||||||
prompt_code = ui.html('<pre class="raw-block">—</pre>').classes("w-full")
|
|
||||||
|
|
||||||
ui.label("Raw LLM output").classes("text-subtitle1 q-mt-md")
|
|
||||||
raw_code = ui.html('<pre class="raw-block">—</pre>').classes("w-full")
|
|
||||||
|
|
||||||
parse_error_lbl = ui.label("").classes("q-mt-sm").style(
|
|
||||||
"color: #FF6B6B; font-weight: 600;"
|
|
||||||
)
|
|
||||||
|
|
||||||
def _render_detail(row: dict[str, Any]) -> None:
|
|
||||||
detail_hint.text = f"Genoma: {row.get('genome_id', '—')}"
|
|
||||||
fit_lbl.text = f"{float(row.get('fitness', 0)):.4f}"
|
|
||||||
dsr_lbl.text = f"{float(row.get('dsr', 0)):.4f}"
|
|
||||||
sharpe_lbl.text = f"{float(row.get('sharpe', 0)):.3f}"
|
|
||||||
dd_lbl.text = f"{float(row.get('max_dd', 0)):.3f}"
|
|
||||||
trades_lbl.text = str(int(row.get("n_trades", 0)))
|
|
||||||
style_lbl.text = str(row.get("cognitive_style", "—"))
|
|
||||||
prompt_code.content = (
|
|
||||||
f'<pre class="raw-block">{html.escape(str(row.get("system_prompt", "—")))}</pre>'
|
|
||||||
)
|
|
||||||
raw_code.content = (
|
|
||||||
f'<pre class="raw-block">{html.escape(str(row.get("raw_text", "—") or "—"))}</pre>'
|
|
||||||
)
|
|
||||||
pe = row.get("parse_error")
|
|
||||||
parse_error_lbl.text = f"❌ Parse error: {pe}" if pe else ""
|
|
||||||
|
|
||||||
def refresh() -> None:
|
|
||||||
run_id = select.value
|
|
||||||
if not run_id:
|
|
||||||
return
|
|
||||||
try:
|
|
||||||
repo = get_repo(GA_DB_PATH)
|
|
||||||
evals = evaluations_df(repo, run_id)
|
|
||||||
gens = genomes_df(repo, run_id)
|
|
||||||
except Exception as e: # noqa: BLE001
|
|
||||||
ui.notify(f"Errore: {e}", type="negative")
|
|
||||||
return
|
|
||||||
|
|
||||||
if evals.empty:
|
|
||||||
top_table.rows = []
|
|
||||||
top_table.update()
|
|
||||||
return
|
|
||||||
|
|
||||||
merged = evals.merge(
|
|
||||||
gens, left_on="genome_id", right_on="id", how="left", suffixes=("", "_g")
|
|
||||||
)
|
|
||||||
state["merged"] = merged
|
|
||||||
|
|
||||||
k = int(top_k_select.value)
|
|
||||||
top = merged.sort_values("fitness", ascending=False).head(k)
|
|
||||||
|
|
||||||
rows = []
|
|
||||||
for _, r in top.iterrows():
|
|
||||||
rows.append(
|
|
||||||
{
|
|
||||||
"genome_id": str(r.get("genome_id", "—"))[:12] + "…",
|
|
||||||
"fitness": round(float(r.get("fitness", 0)), 4),
|
|
||||||
"dsr": round(float(r.get("dsr", 0)), 4),
|
|
||||||
"sharpe": round(float(r.get("sharpe", 0)), 3),
|
|
||||||
"max_dd": round(float(r.get("max_dd", 0)), 3),
|
|
||||||
"n_trades": int(r.get("n_trades", 0)),
|
|
||||||
"cognitive_style": str(r.get("cognitive_style", "—")),
|
|
||||||
"temperature": round(float(r.get("temperature", 0)), 2),
|
|
||||||
"lookback_window": int(r.get("lookback_window", 0)),
|
|
||||||
"_full_id": str(r.get("genome_id", "")),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
top_table.rows = rows
|
|
||||||
top_table.update()
|
|
||||||
|
|
||||||
sel = state.get("selected_gid")
|
|
||||||
if sel:
|
|
||||||
match = merged[merged["genome_id"] == sel]
|
|
||||||
if not match.empty:
|
|
||||||
_render_detail(match.iloc[0].to_dict())
|
|
||||||
|
|
||||||
def on_row_selected(e: Any) -> None:
|
|
||||||
rows = (e.args or {}).get("rows") or []
|
|
||||||
if not rows:
|
|
||||||
return
|
|
||||||
full_id = rows[0].get("_full_id")
|
|
||||||
if not full_id:
|
|
||||||
return
|
|
||||||
state["selected_gid"] = full_id
|
|
||||||
merged = state.get("merged")
|
|
||||||
if merged is None:
|
|
||||||
return
|
|
||||||
match = merged[merged["genome_id"] == full_id]
|
|
||||||
if not match.empty:
|
|
||||||
_render_detail(match.iloc[0].to_dict())
|
|
||||||
|
|
||||||
def on_select_change() -> None:
|
|
||||||
state["run_id"] = select.value
|
|
||||||
state["selected_gid"] = None
|
|
||||||
refresh()
|
|
||||||
|
|
||||||
select.on_value_change(on_select_change)
|
|
||||||
top_k_select.on_value_change(lambda _: refresh())
|
|
||||||
top_table.on("selection", on_row_selected)
|
|
||||||
ui.timer(REFRESH_INTERVAL_S, refresh)
|
|
||||||
refresh()
|
|
||||||
|
|
||||||
|
|
||||||
def _paper_runs_options(only_running: bool = False) -> dict[str, str]:
|
def _paper_runs_options(only_running: bool = False) -> dict[str, str]:
|
||||||
runs = paper_runs_df(PAPER_DB_PATH)
|
runs = paper_runs_df(PAPER_DB_PATH)
|
||||||
@@ -925,10 +96,15 @@ def _paper_equity_figure(eq_df: Any, initial_capital: float) -> go.Figure:
|
|||||||
return fig
|
return fig
|
||||||
|
|
||||||
|
|
||||||
@ui.page("/paper")
|
@ui.page("/")
|
||||||
def paper() -> None:
|
def paper() -> None:
|
||||||
_apply_theme()
|
_apply_theme()
|
||||||
_build_header(active="/paper")
|
_build_header(
|
||||||
|
active="/",
|
||||||
|
brand_subtitle="Strategy Crypto",
|
||||||
|
nav_items=[("/", "Paper")],
|
||||||
|
db_label=f"⛁ {Path(PAPER_DB_PATH).resolve().name}",
|
||||||
|
)
|
||||||
|
|
||||||
options = _paper_runs_options()
|
options = _paper_runs_options()
|
||||||
if not options:
|
if not options:
|
||||||
@@ -1087,7 +263,6 @@ def paper() -> None:
|
|||||||
def main() -> None:
|
def main() -> None:
|
||||||
app.on_startup(
|
app.on_startup(
|
||||||
lambda: print(
|
lambda: print(
|
||||||
f"GA DB: {Path(GA_DB_PATH).resolve()} | "
|
|
||||||
f"Paper DB: {Path(PAPER_DB_PATH).resolve()} | "
|
f"Paper DB: {Path(PAPER_DB_PATH).resolve()} | "
|
||||||
f"root_path: {DASHBOARD_ROOT_PATH or '/'}"
|
f"root_path: {DASHBOARD_ROOT_PATH or '/'}"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
{
|
||||||
|
"_comment": "Stili cognitivi e direttive del system_prompt per il GA di strategy_crypto. Modifica liberamente: cambia 'directive' di uno stile esistente o aggiungi nuove voci a 'styles'. Il nome dello stile (key) viene usato come 'cognitive_style' del genoma; la 'directive' diventa il system_prompt iniziale.",
|
||||||
|
"_schema": "3.2",
|
||||||
|
"_changelog": "v3.2 - Patch consolidamento: ripristinati 3 invarianti regrediti in v3.1 (ASCII-safe, archetipo dominante, hint lookback); voce attiva rinforzata; anti_patterns +2 (chattering, isteresi); output_priorities +1 (#1 coerenza con lente cognitiva); domain_warnings +1 frase (soglia seasonality 0.05); NEW _design_invariants metadata. Lunghezza directive 800-950 char (era 545-614 in v3.1, troppo snellite). v3.1 - Refactor contenuto post-diagnosi. v3.0 - Refactor compositore. v2.2 - Metriche geometrico-frattali. v2.1 - directive estese. v2.0 - Riprogettato per blind-generator GA.",
|
||||||
|
"_focus_metrics_design": "Le focus_metrics sono ENFASI per la lente, non filtri. Standardizzate a 4 per stile (cognitive budget). Evitano ridondanze con la sezione 'Regime recente' del USER_TEMPLATE.",
|
||||||
|
"_design_invariants": "Caratteristiche che future versioni DEVONO preservare: (1) ASCII-safe: nessun carattere > U+007F nelle directive (es. il carattere circa-uguale U+2248 era una regressione di v3.1, ripristinato in v3.2); (2) Archetipo dominante: ogni directive chiude con 'Archetipo dominante: <metafora>.' come ancora identitaria resistente a mutate_prompt_llm; (3) Lookback consigliato: ogni directive include un range numerico diversificato per stile per orientare il parametro evoluto lookback_window del genoma; (4) Metafora ancorante: la lente cognitiva e' descritta in forma 'Il mercato e ...' come prima frase; (5) Lunghezza directive: tra 800 e 950 char (sweet spot per robustezza a mutate_prompt_llm).",
|
||||||
|
|
||||||
|
"agent_role": "Sei un agente generatore di ipotesi di trading quantitativo per un sistema swarm coevolutivo specializzato in mercati crypto. Sei parte di una popolazione che esplora collettivamente lo spazio delle strategie: la diversita delle ipotesi e un asset critico per il sistema. Preferisci esplorare territori meno ovvi rispetto a quelli che la tua lente cognitiva renderebbe predicibili.",
|
||||||
|
|
||||||
|
"pattern_guidance": "Forme di curva da cercare:\n - Trend strutturale (direzione persistente con basso ritracciamento)\n - Compressione di volatilita (pre-breakout, energia accumulata)\n - Espansione di volatilita (regime di shock: momentum o cattura difensiva)\n - Mean reversion strutturale (deviazione eccessiva dalla media verso ritorno atteso)\n - Esaurimento direzionale (estremi di oscillatori, divergenza prezzo-momento)\n\nRipetibilita da sfruttare:\n - Eventi crossover ricorrenti su serie correlate\n - Cicli intra-day se la seasonality oraria e significativa\n - Cicli settimanali se la seasonality settimanale e significativa\n - Pattern doppio (top/bottom) con conferma su livello simile\n - Range breakout dopo periodo di compressione\n\nCerca pattern che si REPLICANO nei dati storici, non singoli eventi rari.",
|
||||||
|
|
||||||
|
"instruction": "Genera una strategia che cerchi anomalie sfruttabili in questo regime crypto.",
|
||||||
|
|
||||||
|
"domain_warnings": "Crypto trada 24/7 senza CME gap: non assumere chiusure settimanali. Tail pesanti e vol clustering esteso caratterizzano BTC/ETH: evita ipotesi gaussiane. NON tentare di inferire funding rate, news o eventi macro: non sono accessibili. Le statistiche fornite sono l'unica informazione su cui basarsi. Una seasonality_hour o seasonality_dow > 0 NON significa che la stagionalita sia significativa: usa la soglia 0.05 come gate minimo (sotto e' rumore statistico).",
|
||||||
|
|
||||||
|
"anti_patterns": "Evita: (1) basare strategia su singolo evento estremo (kurt outlier senza ricorrenza nelle 500 barre recenti); (2) usare piu di 4 condizioni in AND (sovra-fitting combinatorio, brittle a piccoli shift di regime); (3) confondere correlazione storica con causalita (autocorr o pattern temporali sono BIAS, non leggi); (4) assumere stazionarieta perfetta del regime (i delta 'recente vs baseline' indicano lo scostamento); (5) usare feature temporali (hour, dow, is_weekend) quando la seasonality corrispondente e' < 0.05 (rumore, no signal); (6) crossover tra indicatori dello stesso tipo con lookback vicini (chattering: oscilla tra entry/exit senza segnale netto); (7) soglie hard senza isteresi tra entry ed exit (genera chattering al confine; usa soglie diverse per entry vs exit, almeno 10-20% di gap).",
|
||||||
|
|
||||||
|
"output_priorities": "Quando emerge trade-off: (1) coerenza con la lente cognitiva: la strategia deve essere riconoscibile come emanata dal tuo stile (es. engineer = pochi gate robusti, psychologist = contrarian su estremi). E' il fondamento del design swarm: ipotesi omogeneizzate riducono la diversita della popolazione; (2) robustezza cross-regime > ottimalita su singolo regime; (3) semplicita interpretabile (2-3 condizioni con razionale chiaro) > complessita raffinata (5+ condizioni accordate); (4) selettivita (poche entry forti, alto SNR) > attivita (molte entry deboli); (5) condizioni con razionale meccanico > pattern statistici fortuiti.",
|
||||||
|
|
||||||
|
"styles": {
|
||||||
|
"physicist": {
|
||||||
|
"directive": "Il mercato e un sistema fisico con energia (std), simmetrie (skew) e memoria (autocorr). Leggi kurt come densita di eventi estremi (fat tails = fuori equilibrio), skew come forzante asimmetrica. AR(1) positivo molto sopra baseline = memoria coerente, costruisci ipotesi di momentum legittimo; Hurst > 0.55 conferma persistenza di scala su orizzonti multipli; vol_pct alto con kurt bassa = energia immagazzinata non ancora rilasciata, cattura il rilascio; efficiency_ratio elevato = movimento efficiente, sfrutta direzionalita; spectral_entropy bassa con dominant_cycle definito = modi armonici sfruttabili, combina con conferma sulla fase. Preferisci pattern coerenti su piu lookback rispetto a singoli eventi rumorosi. Diagnostica regimi simmetrici e rotture di simmetria. Lookback consigliato: 150-300 barre. Archetipo dominante: sistema fisico in equilibrio (o pre-rottura di simmetria).",
|
||||||
|
"focus_metrics": ["hurst", "dominant_cycle", "efficiency_ratio", "spectral_entropy"]
|
||||||
|
},
|
||||||
|
"biologist": {
|
||||||
|
"directive": "Il mercato e un ecosistema dove strategie competono per alpha finito. Skew negativo segnala predazione asimmetrica (vol-selling crowded subisce shock), positivo predatori che cacciano breakout. Kurt alta = eventi di estinzione o fioritura. AR(1) positivo persistente = una specie sta colonizzando la nicchia (overcrowding imminente, preferisci fade); Hurst > 0.55 con vol_pct basso = nicchia stabile (occupa con strategie direzionali); tail asimmetrico (left molto piu pesante di right) = predazione asimmetrica strutturale, costruisci contrarian sulla coda; structural_uptrend persistente = specie dominante stabile. Combina seasonality con uno o due gate di regime per evitare di sovrapporti a fasi gia mature. Cattura la coda opposta al consensus. Lookback consigliato: 80-200 barre. Archetipo dominante: ecosistema con dinamiche predator-prey e nicchie evolutive.",
|
||||||
|
"focus_metrics": ["tail_left", "tail_right", "structural_uptrend", "seasonality_hour"]
|
||||||
|
},
|
||||||
|
"historian": {
|
||||||
|
"directive": "Il mercato attraversa fasi cicliche che si ripetono in forma simile. Mean = drift strutturale, std = ampiezza ciclo, kurt alta + vol regime medium/high = fase tardiva (pre-transizione); kurt bassa + skew vicino a zero = fase di accumulazione o stabilita. AR(1) recente molto sopra baseline storica = regime accelera rispetto al normale, diagnostica se markup o distribuzione; Hurst > 0.55 con vol_pct alto = fase markup matura, costruisci ipotesi di mean reversion strutturale attesa; structural_uptrend sostenuto = fase di accumulo o markup attiva, sfrutta la direzionalita; compression < 1 = consolidamento pre-fase nuova, preferisci breakout direzionali a conferma di rottura. Identifica analogie tra il regime corrente e fasi tipiche (accumulazione, markup, distribuzione, markdown). Lookback consigliato: 200-500 barre. Archetipo dominante: ciclo storico ricorrente in fasi tipiche.",
|
||||||
|
"focus_metrics": ["autocorr_recent", "structural_uptrend", "compression", "hurst"]
|
||||||
|
},
|
||||||
|
"meteorologist": {
|
||||||
|
"directive": "La volatilita ha climi persistenti e fronti di transizione. Vol_regime + std + kurt definiscono il microclima: std bassa + kurt bassa = calma stabile (preferisci vendere vol con gate sicuri); std alta + kurt alta = tempesta (compra convexity o resta flat); std bassa + kurt alta = calma ingannevole pre-fronte (riduci esposizione). AR(1) recente sopra baseline = fronte persistente in arrivo, cattura la direzione del fronte; Hurst > 0.55 = sistema su scala lunga (ciclone), Hurst < 0.45 = turbolenza locale (no trend persistente, preferisci range-trading); vol_pct estremo = posizione nel ciclo seasonal, modula la size; compression < 1 = compressione vol pre-rilascio, posizionati per il breakout. Costruisci strategie con gate espliciti su vol che attivano logiche diverse. Lookback consigliato: 50-150 barre. Archetipo dominante: clima atmosferico con fronti e regimi persistenti.",
|
||||||
|
"focus_metrics": ["vol_pct", "compression", "tail_right", "dominant_cycle"]
|
||||||
|
},
|
||||||
|
"engineer": {
|
||||||
|
"directive": "Tratta ogni segnale come un sistema di controllo: serve SNR favorevole, causalita, robustezza. Std e il rumore di fondo. Kurt alta riduce affidabilita dei segnali medi (gli estremi dominano le statistiche). AR(1) > 0.05 con std contenuta = SNR favorevole, costruisci ipotesi di momentum filtrato; AR(1) vicino a zero = random walk, evita di costruire signal su questo; Hurst < 0.45 = filtro mean-reversion causale efficace, sfrutta con isteresi; efficiency_ratio < 0.2 = no signal, non costruire; spectral_entropy > 0.8 = white noise, regime non modellabile; tail_index < 2.5 = saturazione dei sensori, riduci leverage; seasonality < 0.05 = feature temporali sono rumore, NON usarle. Preferisci pattern semplici e tarabili: poche condizioni in AND, soglie con margine, isteresi entry/exit. Lookback consigliato: 60-120 barre. Archetipo dominante: sistema di controllo ingegneristico con SNR e robustezza.",
|
||||||
|
"focus_metrics": ["efficiency_ratio", "spectral_entropy", "tail_left", "autocorr_recent"]
|
||||||
|
},
|
||||||
|
"military_strategist": {
|
||||||
|
"directive": "Distingui campagna offensiva da campagna difensiva e adatta dottrina. Vol regime medium/low + skew positivo + kurt moderata = terreno favorevole all'attacco (costruisci entry direzionali su breakout o momentum). Vol regime high + kurt elevata = terreno ostile (difesa: posizioni limitate, exit rapide, gate restrittivi). AR(1) > 0 = vento alle spalle, carica con momentum; AR(1) negativo = imboscata possibile, preferisci contrarian; Hurst > 0.55 = posizione difendibile, hold trade; structural_uptrend > 0.7 = terreno occupato dall'avversario (decidi se attaccare o ritirarti); compression < 0.5 = preparazione attacco silenziosa, posizionati per breakout; vol_pct alta = artiglieria nemica attiva, ritirata. Concentrazione: poche condizioni forti. Sorpresa: contrarian su consensus estremo. Lookback consigliato: 100-200 barre. Archetipo dominante: stratega militare che bilancia offesa e difesa.",
|
||||||
|
"focus_metrics": ["structural_uptrend", "compression", "vol_pct", "tail_left"]
|
||||||
|
},
|
||||||
|
"psychologist": {
|
||||||
|
"directive": "Il mercato e folla con emozioni misurabili. Skew e kurt sono il termometro emotivo: skew neg + kurt alta = paura ricorrente (capitulation spikes, cattura il rimbalzo); skew pos + kurt alta = euforia (FOMO spikes, preferisci fade gli estremi al rialzo); skew vicino a zero + kurt bassa = apatia o range (gioca i bordi del range). AR(1) recente molto sopra baseline = euforia coordinata in corso, posizionati contro l'ultimo arrivato; Hurst > 0.55 = trance collettiva (trend trance, dura piu del razionale); tail_left pesante (Hill < 2.5) = paura sistemica strutturale, contrarian sulla capitulation; spectral_entropy alta = caos comportamentale, riduci dimensionalita del signal; vol_pct estremo = momentum emozionale puro, fade gli estremi. Sfrutta crossover di oscillatori in regimi razionali (kurt vicina a 3). Lookback consigliato: 50-120 barre. Archetipo dominante: psicologo del comportamento collettivo.",
|
||||||
|
"focus_metrics": ["tail_left", "tail_right", "autocorr_recent", "spectral_entropy"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
{
|
||||||
|
"rules": [
|
||||||
|
{
|
||||||
|
"condition": {
|
||||||
|
"op": "and",
|
||||||
|
"args": [
|
||||||
|
{
|
||||||
|
"op": "gt",
|
||||||
|
"args": [
|
||||||
|
{
|
||||||
|
"kind": "indicator",
|
||||||
|
"name": "realized_vol",
|
||||||
|
"params": [150]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "literal",
|
||||||
|
"value": 0.01
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "lt",
|
||||||
|
"args": [
|
||||||
|
{
|
||||||
|
"kind": "indicator",
|
||||||
|
"name": "atr_pct",
|
||||||
|
"params": [150]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "literal",
|
||||||
|
"value": 0.02
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "gt",
|
||||||
|
"args": [
|
||||||
|
{
|
||||||
|
"kind": "indicator",
|
||||||
|
"name": "sma_pct",
|
||||||
|
"params": [150]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "literal",
|
||||||
|
"value": 0.05
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"action": "entry-long"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"condition": {
|
||||||
|
"op": "and",
|
||||||
|
"args": [
|
||||||
|
{
|
||||||
|
"op": "lt",
|
||||||
|
"args": [
|
||||||
|
{
|
||||||
|
"kind": "indicator",
|
||||||
|
"name": "realized_vol",
|
||||||
|
"params": [150]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "literal",
|
||||||
|
"value": 0.005
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "gt",
|
||||||
|
"args": [
|
||||||
|
{
|
||||||
|
"kind": "indicator",
|
||||||
|
"name": "atr_pct",
|
||||||
|
"params": [150]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "literal",
|
||||||
|
"value": 0.03
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "lt",
|
||||||
|
"args": [
|
||||||
|
{
|
||||||
|
"kind": "indicator",
|
||||||
|
"name": "sma_pct",
|
||||||
|
"params": [150]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "literal",
|
||||||
|
"value": -0.05
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"action": "entry-short"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"condition": {
|
||||||
|
"op": "or",
|
||||||
|
"args": [
|
||||||
|
{
|
||||||
|
"op": "eq",
|
||||||
|
"args": [
|
||||||
|
{
|
||||||
|
"kind": "indicator",
|
||||||
|
"name": "sma_pct",
|
||||||
|
"params": [150]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "literal",
|
||||||
|
"value": 0.0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "lt",
|
||||||
|
"args": [
|
||||||
|
{
|
||||||
|
"kind": "indicator",
|
||||||
|
"name": "realized_vol",
|
||||||
|
"params": [150]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "literal",
|
||||||
|
"value": 0.001
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"action": "exit"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
{
|
||||||
|
"rules": [
|
||||||
|
{
|
||||||
|
"condition": {
|
||||||
|
"op": "and",
|
||||||
|
"args": [
|
||||||
|
{
|
||||||
|
"op": "gt",
|
||||||
|
"args": [
|
||||||
|
{
|
||||||
|
"kind": "indicator",
|
||||||
|
"name": "realized_vol",
|
||||||
|
"params": [
|
||||||
|
150
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "literal",
|
||||||
|
"value": 0.007
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "lt",
|
||||||
|
"args": [
|
||||||
|
{
|
||||||
|
"kind": "indicator",
|
||||||
|
"name": "atr_pct",
|
||||||
|
"params": [
|
||||||
|
150
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "literal",
|
||||||
|
"value": 0.0042
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"action": "entry-long"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"condition": {
|
||||||
|
"op": "or",
|
||||||
|
"args": [
|
||||||
|
{
|
||||||
|
"op": "crossunder",
|
||||||
|
"args": [
|
||||||
|
{
|
||||||
|
"kind": "indicator",
|
||||||
|
"name": "rsi",
|
||||||
|
"params": [
|
||||||
|
14
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "literal",
|
||||||
|
"value": 70.0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "gt",
|
||||||
|
"args": [
|
||||||
|
{
|
||||||
|
"kind": "indicator",
|
||||||
|
"name": "atr_pct",
|
||||||
|
"params": [
|
||||||
|
150
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "literal",
|
||||||
|
"value": 0.007
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"action": "exit"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
{
|
||||||
|
"rules": [
|
||||||
|
{
|
||||||
|
"condition": {
|
||||||
|
"op": "and",
|
||||||
|
"args": [
|
||||||
|
{
|
||||||
|
"op": "lt",
|
||||||
|
"args": [
|
||||||
|
{
|
||||||
|
"kind": "indicator",
|
||||||
|
"name": "rsi",
|
||||||
|
"params": [
|
||||||
|
14
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "literal",
|
||||||
|
"value": 30.0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "gt",
|
||||||
|
"args": [
|
||||||
|
{
|
||||||
|
"kind": "indicator",
|
||||||
|
"name": "atr_pct",
|
||||||
|
"params": [
|
||||||
|
14
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "literal",
|
||||||
|
"value": 0.01
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "lt",
|
||||||
|
"args": [
|
||||||
|
{
|
||||||
|
"kind": "indicator",
|
||||||
|
"name": "macd_pct",
|
||||||
|
"params": [
|
||||||
|
12,
|
||||||
|
26,
|
||||||
|
9
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "literal",
|
||||||
|
"value": -0.005
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"action": "entry-long"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"condition": {
|
||||||
|
"op": "or",
|
||||||
|
"args": [
|
||||||
|
{
|
||||||
|
"op": "gt",
|
||||||
|
"args": [
|
||||||
|
{
|
||||||
|
"kind": "indicator",
|
||||||
|
"name": "rsi",
|
||||||
|
"params": [
|
||||||
|
14
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "literal",
|
||||||
|
"value": 70.0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "lt",
|
||||||
|
"args": [
|
||||||
|
{
|
||||||
|
"kind": "indicator",
|
||||||
|
"name": "atr_pct",
|
||||||
|
"params": [
|
||||||
|
14
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "literal",
|
||||||
|
"value": 0.005
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "gt",
|
||||||
|
"args": [
|
||||||
|
{
|
||||||
|
"kind": "indicator",
|
||||||
|
"name": "macd_pct",
|
||||||
|
"params": [
|
||||||
|
12,
|
||||||
|
26,
|
||||||
|
9
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "literal",
|
||||||
|
"value": 0.005
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"action": "exit"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -897,32 +897,36 @@ wheels = [
|
|||||||
name = "multi-swarm-coevolutive"
|
name = "multi-swarm-coevolutive"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = { virtual = "." }
|
source = { virtual = "." }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "multi-swarm-core" },
|
||||||
|
{ name = "strategy-crypto" },
|
||||||
|
]
|
||||||
|
|
||||||
[package.dev-dependencies]
|
[package.dev-dependencies]
|
||||||
dev = [
|
dev = [
|
||||||
{ name = "multi-swarm-core" },
|
|
||||||
{ name = "mypy" },
|
{ name = "mypy" },
|
||||||
{ name = "pytest" },
|
{ name = "pytest" },
|
||||||
{ name = "pytest-asyncio" },
|
{ name = "pytest-asyncio" },
|
||||||
{ name = "pytest-mock" },
|
{ name = "pytest-mock" },
|
||||||
{ name = "responses" },
|
{ name = "responses" },
|
||||||
{ name = "ruff" },
|
{ name = "ruff" },
|
||||||
{ name = "strategy-crypto" },
|
|
||||||
{ name = "types-requests" },
|
{ name = "types-requests" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.metadata]
|
[package.metadata]
|
||||||
|
requires-dist = [
|
||||||
|
{ name = "multi-swarm-core", editable = "src/multi_swarm_core" },
|
||||||
|
{ name = "strategy-crypto", editable = "src/strategy_crypto" },
|
||||||
|
]
|
||||||
|
|
||||||
[package.metadata.requires-dev]
|
[package.metadata.requires-dev]
|
||||||
dev = [
|
dev = [
|
||||||
{ name = "multi-swarm-core", editable = "src/multi_swarm_core" },
|
|
||||||
{ name = "mypy", specifier = ">=1.13" },
|
{ name = "mypy", specifier = ">=1.13" },
|
||||||
{ name = "pytest", specifier = ">=8.3" },
|
{ name = "pytest", specifier = ">=8.3" },
|
||||||
{ name = "pytest-asyncio", specifier = ">=0.24" },
|
{ name = "pytest-asyncio", specifier = ">=0.24" },
|
||||||
{ name = "pytest-mock", specifier = ">=3.14" },
|
{ name = "pytest-mock", specifier = ">=3.14" },
|
||||||
{ name = "responses", specifier = ">=0.25" },
|
{ name = "responses", specifier = ">=0.25" },
|
||||||
{ name = "ruff", specifier = ">=0.7" },
|
{ name = "ruff", specifier = ">=0.7" },
|
||||||
{ name = "strategy-crypto", editable = "src/strategy_crypto" },
|
|
||||||
{ name = "types-requests", specifier = ">=2.32" },
|
{ name = "types-requests", specifier = ">=2.32" },
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -932,9 +936,11 @@ version = "0.1.0"
|
|||||||
source = { editable = "src/multi_swarm_core" }
|
source = { editable = "src/multi_swarm_core" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "httpx" },
|
{ name = "httpx" },
|
||||||
|
{ name = "nicegui" },
|
||||||
{ name = "numpy" },
|
{ name = "numpy" },
|
||||||
{ name = "openai" },
|
{ name = "openai" },
|
||||||
{ name = "pandas" },
|
{ name = "pandas" },
|
||||||
|
{ name = "plotly" },
|
||||||
{ name = "pyarrow" },
|
{ name = "pyarrow" },
|
||||||
{ name = "pydantic" },
|
{ name = "pydantic" },
|
||||||
{ name = "pydantic-settings" },
|
{ name = "pydantic-settings" },
|
||||||
@@ -949,9 +955,11 @@ dependencies = [
|
|||||||
[package.metadata]
|
[package.metadata]
|
||||||
requires-dist = [
|
requires-dist = [
|
||||||
{ name = "httpx", specifier = ">=0.28" },
|
{ name = "httpx", specifier = ">=0.28" },
|
||||||
|
{ name = "nicegui", specifier = ">=3.11.1" },
|
||||||
{ name = "numpy", specifier = ">=2.1" },
|
{ name = "numpy", specifier = ">=2.1" },
|
||||||
{ name = "openai", specifier = ">=1.55" },
|
{ name = "openai", specifier = ">=1.55" },
|
||||||
{ name = "pandas", specifier = ">=2.2" },
|
{ name = "pandas", specifier = ">=2.2" },
|
||||||
|
{ name = "plotly", specifier = ">=5.24" },
|
||||||
{ name = "pyarrow", specifier = ">=18.0" },
|
{ name = "pyarrow", specifier = ">=18.0" },
|
||||||
{ name = "pydantic", specifier = ">=2.9" },
|
{ name = "pydantic", specifier = ">=2.9" },
|
||||||
{ name = "pydantic-settings", specifier = ">=2.6" },
|
{ name = "pydantic-settings", specifier = ">=2.6" },
|
||||||
|
|||||||
Reference in New Issue
Block a user