Compare commits
29 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b8aa00b883 | |||
| ba2c23e602 | |||
| 646c64dacd | |||
| a702b2090d | |||
| c6bf0f31cc | |||
| 68e0b009e9 | |||
| 4baa1eca62 | |||
| 035cd1dff3 | |||
| b8bf0c186c | |||
| af68bc44b4 | |||
| 074ebe0379 | |||
| 7f2db19a7c | |||
| 369a77b5cf | |||
| 2aa5646aeb | |||
| 6a9e2c28b1 | |||
| 37558a34f5 | |||
| 14f476dd09 | |||
| 05c7b5e89e | |||
| 6655e425fa | |||
| 23b7273e71 | |||
| 9c871d1d86 | |||
| 8b767da5e7 | |||
| 1c0058ec3b | |||
| 220e510d5e | |||
| 9742df3a1f | |||
| 21b5cf1eae | |||
| a29748e3d8 | |||
| fa11cca2bc | |||
| 6526c6e6e3 |
@@ -47,3 +47,6 @@ logs/
|
||||
build/
|
||||
dist/
|
||||
*.egg
|
||||
|
||||
# Pythagoras source PDFs (local only, not tracked)
|
||||
src/strategy_pythagoras/Pythagoras/*.pdf
|
||||
|
||||
@@ -12,23 +12,23 @@ git clone ssh://git@git.tielogic.xyz:222/Adriano/Multi_Swarm_Coevolutive.git
|
||||
|
||||
## 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)
|
||||
├── 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
|
||||
├── uv.lock lock unico del workspace
|
||||
├── data/, series/, state/ cache OHLCV + DB (runtime, gitignored)
|
||||
├── scripts/ CLI entrypoints (run_phase1, run_paper_trading, ...)
|
||||
└── src/
|
||||
├── 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 +
|
||||
│ │ data + llm + agents + ga + orchestrator +
|
||||
│ │ metrics + persistence + config
|
||||
│ ├── tests/ unit + integration (182 test)
|
||||
│ │ metrics + persistence + config + dashboard (GA-only)
|
||||
│ ├── tests/ unit + integration
|
||||
│ └── docs/ design/ + decisions/ + reports/
|
||||
│
|
||||
└── 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
|
||||
├── strategy_crypto/
|
||||
│ ├── backend/ paper-trading (executor, portfolio, persistence, schema)
|
||||
│ ├── frontend/ NiceGUI dashboard dual-DB
|
||||
│ └── strategies/ JSON freezate (btc_*.json, eth_*.json)
|
||||
└── tests/ smoke regression (import + json + schema)
|
||||
│ ├── frontend/ NiceGUI paper-only dashboard
|
||||
│ ├── strategies/ JSON freezate (btc_*.json, eth_*.json)
|
||||
│ └── 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.
|
||||
|
||||
**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
|
||||
|
||||
**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).
|
||||
- `strategy_crypto/strategies/eth_facd6af85d5d.json` — ETH-PERPETUAL, regime-based (Sharpe OOS +0,19 su 6,75 anni).
|
||||
- `btc_fb63e851.json` — BTC-PERPETUAL, RSI estremi + ATR vs SMA + filtro orario 9-17 (Sharpe OOS +0,26 su 7,33 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.
|
||||
- Decision log: [`src/multi_swarm_core/docs/decisions/`](src/multi_swarm_core/docs/decisions/) (gate Phase 1, scelta nemotron).
|
||||
- Design docs concettuali: [`src/multi_swarm_core/docs/design/`](src/multi_swarm_core/docs/design/).
|
||||
Documenti:
|
||||
- [**Stato progetto e roadmap (14 maggio 2026)**](src/multi_swarm_core/docs/reports/2026-05-14-stato-progetto-e-roadmap.md)
|
||||
- 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.
|
||||
|
||||
@@ -65,7 +111,7 @@ Stack: Python 3.13, uv workspace, hatchling, pytest+pytest-mock+responses, opena
|
||||
```bash
|
||||
uv sync # installa entrambi i workspace member come editable
|
||||
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
|
||||
@@ -84,10 +130,9 @@ OPENROUTER_API_KEY=<sk-or-v1-...>
|
||||
GA_DB_PATH=./state/runs.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
|
||||
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`.
|
||||
@@ -105,14 +150,27 @@ uv run mypy src/ scripts/
|
||||
# Smoke run (MockLLM + OHLCV sintetico, no API calls)
|
||||
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 \
|
||||
--name run-XXX \
|
||||
--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 \
|
||||
--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
|
||||
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 \
|
||||
--name phase3-papertrade-XXX \
|
||||
--initial-capital 1000 --poll-seconds 300
|
||||
# Default --strategies-dir: importlib.resources del package strategy_crypto
|
||||
|
||||
# Dashboard NiceGUI locale
|
||||
uv run python -m strategy_crypto.frontend.nicegui_app
|
||||
# → http://localhost:8080 (env SWARM_DASHBOARD_PORT)
|
||||
# Dashboard NiceGUI locale (2 distinte)
|
||||
uv run python -m multi_swarm_core.dashboard.nicegui_app # GA core (/, /convergence, /genomes)
|
||||
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.
|
||||
- **GA Convergence** (`/convergence`): fitness median/max/p90 per generazione + entropy.
|
||||
- **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
|
||||
|
||||
`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-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
|
||||
docker compose up -d --build
|
||||
@@ -158,12 +234,16 @@ docker compose ps
|
||||
|
||||
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.).
|
||||
- `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
|
||||
|
||||
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).
|
||||
|
||||
**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.
|
||||
|
||||
+3
-3
@@ -23,7 +23,7 @@
|
||||
# ./state contiene runs.db (GA) + strategy_crypto.db (paper) + WAL/SHM
|
||||
# ./src/strategy_crypto/strategy_crypto/strategies JSON freezate (ro)
|
||||
#
|
||||
# Secrets (token Cerbero + OpenRouter): caricati da .env via env_file.
|
||||
# Secrets (token Cerbero + OpusAgent): caricati da .env via env_file.
|
||||
|
||||
networks:
|
||||
traefik:
|
||||
@@ -88,8 +88,8 @@ services:
|
||||
<<: *swarm-env
|
||||
DASHBOARD_ROOT_PATH: /strategy_crypto_gui
|
||||
volumes:
|
||||
# Dashboard legge solo strategy_crypto.db: state/ in read-only (WAL: vedi nota)
|
||||
- ./state:/app/state:ro
|
||||
# RW richiesto: SQLite WAL mode richiede write-access dal reader per SHM.
|
||||
- ./state:/app/state
|
||||
entrypoint:
|
||||
- python
|
||||
- -m
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,343 @@
|
||||
# strategy_pythagoras — Design
|
||||
|
||||
**Data:** 2026-05-19
|
||||
**Autore design:** Adriano Dal Pastro (con Claude/brainstorming)
|
||||
**Stato:** Design approvato per parti 1-4, in attesa di review utente sulla spec consolidata
|
||||
**Audience:** implementatori (Claude executor o umano)
|
||||
|
||||
## 0. Riassunto esecutivo
|
||||
|
||||
Nuovo workspace member `strategy_pythagoras` parallelo a `strategy_crypto`. Replica il pattern coevolutivo GA del monorepo applicato a un dominio diverso: la **scoperta di pattern frattali ricorrenti** sui mercati crypto secondo il framework Pythagoras-Malanga (Fourier + frattali + geometria Evideon), descritto nei due PDF in `src/strategy_pythagoras/Pythagoras/`.
|
||||
|
||||
**Non cambia il genoma del core:** gli agenti restano `HypothesisAgentGenome` e producono `Strategy JSON` nella stessa DSL S-expression. Cambiano:
|
||||
- 7 stili cognitivi Pythagoras-themed (in `prompts.json`)
|
||||
- 3 nuovi indicatori candle (`candle_pattern`, `pythagorean_ratio`, `fractal_mirror`)
|
||||
- Fitness con bonus di asset-invariance BTC↔ETH
|
||||
- Output: strategie JSON freezate + dashboard NiceGUI + report markdown di analisi cross-book
|
||||
|
||||
**Deliverable di chiusura task:** scaffolding completo + 1 GA run short (smoke test) + `docs/analysis_first_run.md` con sintesi numerica e top winners.
|
||||
|
||||
---
|
||||
|
||||
## 1. Architettura
|
||||
|
||||
### Layout package
|
||||
|
||||
```
|
||||
src/strategy_pythagoras/
|
||||
├── pyproject.toml # workspace member, dipende da multi-swarm-core
|
||||
├── README.md
|
||||
├── tests/
|
||||
│ └── test_indicators.py # unit-test per i 3 nuovi indicatori
|
||||
└── strategy_pythagoras/
|
||||
├── __init__.py
|
||||
├── prompts.json # 7 stili Pythagoras-aligned (schema v3.2 di strategy_crypto)
|
||||
├── indicators.py # candle_pattern, pythagorean_ratio, fractal_mirror
|
||||
├── backend/
|
||||
│ ├── __init__.py
|
||||
│ ├── schema.py # tabelle paper_trading_*
|
||||
│ ├── executor.py # PaperExecutor (port da strategy_crypto)
|
||||
│ ├── portfolio.py # Portfolio (port da strategy_crypto)
|
||||
│ └── persistence.py # PaperRepository
|
||||
├── frontend/
|
||||
│ ├── __init__.py
|
||||
│ ├── nicegui_app.py # /strategy_pythagoras_gui
|
||||
│ └── data.py # dual-reader: GA db + paper db + invariance metrics
|
||||
└── strategies/ # JSON winners shippati col package
|
||||
└── (vuoto al t0)
|
||||
```
|
||||
|
||||
### Workspace registration
|
||||
|
||||
In `pyproject.toml` (root):
|
||||
```toml
|
||||
[tool.uv.workspace]
|
||||
members = ["src/multi_swarm_core", "src/strategy_crypto", "src/strategy_pythagoras"]
|
||||
|
||||
[tool.uv.sources]
|
||||
strategy-pythagoras = { workspace = true }
|
||||
```
|
||||
|
||||
Aggiungere `strategy-pythagoras` a `[project].dependencies` per il deployable root.
|
||||
|
||||
### Riuso
|
||||
|
||||
| Componente | Source | Note |
|
||||
|---|---|---|
|
||||
| GA loop, mutation, crossover | `multi_swarm_core.ga` | invariato |
|
||||
| Protocol parser/validator/compiler | `multi_swarm_core.protocol` | esteso con 3 indicatori (vedi §2) |
|
||||
| Backtest engine | `multi_swarm_core.backtest` | invariato |
|
||||
| LLM / OpenRouter / Anthropic clients | `multi_swarm_core.llm` | invariato |
|
||||
| PaperExecutor + Portfolio | `strategy_crypto.backend` | **port** (non import), per isolamento DB |
|
||||
| NiceGUI dashboard shell | `strategy_crypto.frontend` | **port** + adatta tabs |
|
||||
|
||||
### Persistence
|
||||
|
||||
```
|
||||
state/strategy_pythagoras.db # GA: genomi, generazioni, fitness history
|
||||
state/strategy_pythagoras_paper.db # paper-trading post-deploy
|
||||
strategy_pythagoras/strategies/ # JSON winners shippati
|
||||
docs/analysis_first_run.md # report cross-book
|
||||
docs/analysis_runs/<run-id>/ # per-run dump
|
||||
```
|
||||
|
||||
Env vars:
|
||||
- `STRATEGY_PYTHAGORAS_DB_PATH` (default `state/strategy_pythagoras.db`)
|
||||
- `STRATEGY_PYTHAGORAS_PAPER_DB_PATH` (default `state/strategy_pythagoras_paper.db`)
|
||||
- `GA_INVARIANCE_ALPHA` (default 0.3)
|
||||
- `GA_INVARIANCE_TOLERANCE_BARS` (default 36 = ±3h su 5m TF)
|
||||
|
||||
### Routing GUI
|
||||
|
||||
`/strategy_pythagoras_gui` (allinea a [[gui_subpath_routing]] in memory: ogni asset GUI su subpath, root dominio libera).
|
||||
|
||||
### Docker (Phase 2, fuori scope di questa spec)
|
||||
|
||||
Servizi paralleli a quelli di strategy_crypto:
|
||||
- `strategy-pythagoras-paper` (runner)
|
||||
- `strategy-pythagoras-gui` (dashboard)
|
||||
Rete Traefik external, bind mount uid 1000 (vedi [[production_deployment]]).
|
||||
|
||||
---
|
||||
|
||||
## 2. Genoma e DSL
|
||||
|
||||
### Il genoma non cambia
|
||||
|
||||
`HypothesisAgentGenome` di `multi_swarm_core.genome.hypothesis` resta identico:
|
||||
- `system_prompt: str`
|
||||
- `feature_access: list[str]`
|
||||
- `temperature: float`
|
||||
- `top_p: float`
|
||||
- `model_tier: ModelTier`
|
||||
- `lookback_window: int` — vincolo 12 ≤ lw ≤ 200
|
||||
- `cognitive_style: str` — uno dei 7 nuovi stili
|
||||
- `parent_ids, generation, id` — invariati
|
||||
|
||||
### 3 nuovi indicatori (`strategy_pythagoras/indicators.py`)
|
||||
|
||||
| Nome | Params (JSON) | Output | Semantica operativa |
|
||||
|---|---|---|---|
|
||||
| `candle_pattern` | `[seq_str]` es. `"UDU"`, `"UUD0U"` | 1.0 se le ultime k=len(seq_str) candele matchano la sequenza, 0.0 altrimenti | `"U"` = close>open; `"D"` = close<open; `"0"` = `abs(close-open)/open < 0.001` |
|
||||
| `pythagorean_ratio` | `[lookback: int]` | float = `max(close[-lookback:]) / min(close[-lookback:])` | Ratio prezzo, da confrontare con literal vicini a φ=1.618, π=3.1416, √2=1.414, e=2.718 |
|
||||
| `fractal_mirror` | `[k: int, axis: str]` axis ∈ `{"h","v"}` | float ∈ [-1, +1] | Correlazione di Pearson tra ultime k candele e loro mirror: `"h"` = mirror tempo (inversione sequenza); `"v"` = mirror prezzo (1 - close/max) |
|
||||
|
||||
Vincoli del compiler:
|
||||
- `candle_pattern`: `len(seq_str)` ∈ [3, 12], simboli ∈ `{U,D,0}`
|
||||
- `pythagorean_ratio`: `lookback` ∈ [12, 200]
|
||||
- `fractal_mirror`: `k` ∈ [3, 12], `axis` ∈ `{"h","v"}`
|
||||
|
||||
Tutti e 3 vanno aggiunti a `KNOWN_INDICATORS` in `multi_swarm_core.protocol.grammar`.
|
||||
|
||||
### Esempio strategy JSON tipica
|
||||
|
||||
```json
|
||||
{
|
||||
"rules": [
|
||||
{
|
||||
"condition": {
|
||||
"op": "and",
|
||||
"args": [
|
||||
{"op": "eq", "args": [
|
||||
{"kind": "indicator", "name": "candle_pattern", "params": ["UDDU"]},
|
||||
{"kind": "literal", "value": 1.0}
|
||||
]},
|
||||
{"op": "gt", "args": [
|
||||
{"kind": "indicator", "name": "pythagorean_ratio", "params": [89]},
|
||||
{"kind": "literal", "value": 1.618}
|
||||
]}
|
||||
]
|
||||
},
|
||||
"action": "entry-long"
|
||||
},
|
||||
{
|
||||
"condition": {
|
||||
"op": "or", "args": [
|
||||
{"op": "gt", "args": [
|
||||
{"kind": "indicator", "name": "pythagorean_ratio", "params": [89]},
|
||||
{"kind": "literal", "value": 2.618}
|
||||
]},
|
||||
{"op": "crossunder", "args": [
|
||||
{"kind": "indicator", "name": "fractal_mirror", "params": [12, "h"]},
|
||||
{"kind": "literal", "value": 0.0}
|
||||
]}
|
||||
]
|
||||
},
|
||||
"action": "exit"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Vincoli protettivi (anti-overfitting)
|
||||
|
||||
- `lookback_window ≤ 200`
|
||||
- `candle_pattern` seq length ∈ [3, 12] (range 1+2 del Libro dei Frattali)
|
||||
- `time_in_market` monitorato come metric (red flag [[selectivity_red_flag]]); non hard-gate al primo run
|
||||
- Letterali con `pythagorean_ratio`: max 4 decimali (no `1.6180339`)
|
||||
- Max 4 condizioni in AND per regola (eredita da prompts.json)
|
||||
|
||||
---
|
||||
|
||||
## 3. Stili cognitivi (`strategy_pythagoras/prompts.json`)
|
||||
|
||||
### Schema
|
||||
|
||||
Schema v3.2 identico a `strategy_crypto/prompts.json` (campi `_schema`, `_changelog`, `_design_invariants`, `agent_role`, `pattern_guidance`, `instruction`, `domain_warnings`, `anti_patterns`, `output_priorities`, `styles`).
|
||||
|
||||
### `agent_role`
|
||||
|
||||
> Sei un agente di un sistema swarm coevolutivo che cerca pattern frattali ricorrenti sui mercati crypto secondo il framework Pythagoras-Malanga (frattali H-C, trasformata di Fourier, geometria Evideon). Sei parte di una popolazione che esplora collettivamente lo spazio dei pattern: la diversità delle ipotesi è un asset critico. Preferisci esplorare strutture meno ovvie per la tua lente cognitiva. La strategia che produci deve essere riconoscibile come emanata dal tuo stile.
|
||||
|
||||
### `pattern_guidance` (specifico Pythagoras)
|
||||
|
||||
- Sequenze candle 3-12 lunghezza in alfabeto `{U,D,0}` via `candle_pattern`
|
||||
- Mirror H/V come operatori di proiezione via `fractal_mirror`
|
||||
- Ratios di prezzo vicini a φ=1.618, 1/φ=0.618, √2=1.414, π/2=1.571, e/2=1.359 entro tolleranza 0.5%
|
||||
- Pattern composti: pattern lunghi (6-12) come concatenazione di pattern corti (Pattern 13 = Pattern 3 + Pattern 11, p. 53 del paper)
|
||||
- Cicli ricorrenti: stesso pattern firato a distanze regolari (Poincaré)
|
||||
|
||||
### `domain_warnings`
|
||||
|
||||
- Crypto 24/7, no CME gap
|
||||
- I "numeri sacri" (Solfeggio 396-852 Hz, 137.0359, etc.) sono prior teorici, NON leggi: usali come scale candidate, non come dogma
|
||||
- Il paper Pythagoras è esplicitamente non-falsificabile (cita "consapevolezza del trader" come jolly per il fallimento): il backtest è l'unico arbitro
|
||||
- `time_in_market > 80%` red flag (leveraged B&H camuffato)
|
||||
- Tolleranza ±3h del paper → su 5m TF = ±36 barre
|
||||
|
||||
### `anti_patterns`
|
||||
|
||||
- Sequenza `candle_pattern` con `len > 7` simboli vincolati → overfitting
|
||||
- `pythagorean_ratio` con tolleranza > 2% sui literal → numerologia spuriosa
|
||||
- `fractal_mirror` con `k == lookback_window` → tautologico
|
||||
- Letterali con più di 4 decimali
|
||||
- Più di 4 condizioni in AND
|
||||
- Crossover tra indicatori dello stesso tipo con lookback vicini → chattering
|
||||
|
||||
### `output_priorities`
|
||||
|
||||
1. **Coerenza con lente cognitiva** (es. `pythagorean` usa ratios, `candle_grammarian` usa sequenze esplicite)
|
||||
2. **Asset-invariance** (segnali che attivano sia su BTC che su ETH entro ±36 barre)
|
||||
3. **Selettività** (poche entry forti)
|
||||
4. **Composizionalità** (pattern lunghi come somma di corti)
|
||||
5. **Robustezza vs random baseline** (3σ richiesto da `skeptic_quant`)
|
||||
|
||||
### I 7 stili (`styles`)
|
||||
|
||||
Ogni stile mantiene shape v3.2 (directive 800-950 char ASCII-safe, `focus_metrics` 4 voci, ultimo periodo = "Archetipo dominante: X.", lookback range esplicito):
|
||||
|
||||
| `cognitive_style` | Archetipo / Metafora ancorante | `focus_metrics` | Lookback consigliato |
|
||||
|---|---|---|---|
|
||||
| `pythagorean` | Armonia di ratios sacri (φ, π, √2) | `pythagorean_ratio`, `candle_pattern`, `sma_pct`, `realized_vol` | 89-144 |
|
||||
| `fractal_geometer` | Autosimilarità: pattern di 3 candele si ripetono dilatati a 6, 12 | `candle_pattern`, `fractal_mirror`, `atr_pct`, `pythagorean_ratio` | 48-144 |
|
||||
| `fourier_analyst` | Somma di seni: frequenze ricorrenti dominanti | `sma_pct`, `realized_vol`, `candle_pattern`, `atr` | 60-200 |
|
||||
| `evideonic_projector` | Presente = passato proiettato via mirror H+V e scale | `fractal_mirror`, `pythagorean_ratio`, `candle_pattern`, `sma_pct` | 24-96 |
|
||||
| `candle_grammarian` | Lingua di 3 simboli (U,D,0); parole 3-12 lettere | `candle_pattern`, `volume`, `atr`, `realized_vol` | 12-48 |
|
||||
| `recurrence_theorist` | Per Poincaré, eventi tornano: cerca pattern di oggi che firarono ieri | `candle_pattern`, `fractal_mirror`, `pythagorean_ratio`, `sma_pct` | 100-200 |
|
||||
| `skeptic_quant` | Anticorpo all'unfalsifiability: solo edge 3σ vs random | `realized_vol`, `atr_pct`, `sma_pct`, `candle_pattern` | 60-150 |
|
||||
|
||||
Lo `skeptic_quant` è importante: la sua directive richiede esplicitamente che la strategia sia testabile e che il fitness sia confrontato contro random baseline.
|
||||
|
||||
### `_design_invariants`
|
||||
|
||||
Stessa filosofia di v3.2 di strategy_crypto:
|
||||
- ASCII-safe (no Unicode oltre U+007F nelle directive)
|
||||
- Ogni directive chiude con `Archetipo dominante: <metafora>.`
|
||||
- Ogni directive ha range lookback numerico esplicito
|
||||
- Prima frase: `Il mercato e ...`
|
||||
- Lunghezza 800-950 char
|
||||
|
||||
---
|
||||
|
||||
## 4. Fitness, run GA short, deliverable analisi
|
||||
|
||||
### Fitness con bonus invariance
|
||||
|
||||
```
|
||||
fitness(genome) = mean(sharpe_BTC, sharpe_ETH) × (1 + α × invariance_score)
|
||||
|
||||
invariance_score = corr_signal(entries_BTC, entries_ETH, tolerance_bars=GA_INVARIANCE_TOLERANCE_BARS)
|
||||
∈ [0, 1]
|
||||
α = GA_INVARIANCE_ALPHA (default 0.3)
|
||||
```
|
||||
|
||||
`corr_signal` = frazione di entries su BTC che hanno una entry corrispondente su ETH entro ±36 barre (=±3h su 5m TF).
|
||||
|
||||
Implementazione come callback al GA in `multi_swarm_core.ga`, registrata da `strategy_pythagoras` al startup. Il core non sa nulla di Pythagoras: riceve solo la callback.
|
||||
|
||||
### GA run short (smoke test)
|
||||
|
||||
| Parametro | Valore | Note |
|
||||
|---|---|---|
|
||||
| `population_size` | 20 | minimo per 7 stili (~3 per stile) |
|
||||
| `generations` | 5 | smoke test, non training |
|
||||
| `elite_fraction` | 0.2 | top-4 sopravvivono |
|
||||
| `mutation_rate` | 0.3 | invariato vs strategy_crypto |
|
||||
| `crossover_rate` | 0.5 | invariato |
|
||||
| `model_tier` distribuzione | 70% C (qwen-2.5-72b), 30% B (sonnet) | rispetta [[model_qwen_dependency]] |
|
||||
| `dataset` | BTC 5m + ETH 5m da `strategy_crypto/series/` | riusa serie esistenti |
|
||||
| `train_window` | 2024-07 → 2024-12 | copre le date Pythagoras (lug-ago 2024) |
|
||||
| `test_window` | 2025-01 (1 mese) | hold-out per validare invariance |
|
||||
| `name` | `pythagoras-smoke-001` | run id |
|
||||
|
||||
Lo smoke test verifica:
|
||||
1. Workspace member installato in venv (`uv sync` + `uv run python -c "import strategy_pythagoras"`)
|
||||
2. I 3 nuovi indicatori registrati nel grammar e compilabili
|
||||
3. `prompts.json` caricato, 7 stili producono genomi distinti (no collisioni di id)
|
||||
4. Bonus invariance impatta fitness (verifica via log)
|
||||
5. JSON winners atterrano in `strategy_pythagoras/strategies/`
|
||||
6. Dashboard NiceGUI si avvia e legge i due DB
|
||||
|
||||
### Deliverable analisi cross-book
|
||||
|
||||
`docs/analysis_first_run.md` con:
|
||||
|
||||
1. **Sintesi numerica dei riassunti** — riferimenti a `src/strategy_pythagoras/Pythagoras/Pythagoras_Trading_Prediction.summary.md` e `Libro_frattali.summary.md`
|
||||
2. **Top-5 winners** — id, cognitive_style, fitness, sharpe_BTC, sharpe_ETH, invariance_score
|
||||
3. **Pattern frattali emersi** — dump dei `candle_pattern` seq usate, conteggio per stile, % sovrapposizione con spazio teorico dei 57 pattern del Libro
|
||||
4. **Ratios di prezzo emersi** — distribuzione literal usati con `pythagorean_ratio`, distanza dai numeri universali (φ/π/√2/Solfeggio)
|
||||
5. **Cross-asset invariance osservata** — istogramma di `corr_signal` per top genomi
|
||||
6. **Conclusione onesta** — confronto vs random baseline, quanti winners superano sharpe>1.0 su test + invariance>0.3, cosa il framework Pythagoras predice e cosa NON regge al backtest
|
||||
|
||||
Niente "consapevolezza" come jolly. Solo numeri.
|
||||
|
||||
### Dashboard NiceGUI
|
||||
|
||||
`/strategy_pythagoras_gui`:
|
||||
- Tab **Genomes** — winners con stile/sharpe/invariance, click per drill su rules
|
||||
- Tab **Patterns** — heatmap delle sequenze `candle_pattern` emerse, frequenza per stile
|
||||
- Tab **Ratios** — istogramma literal vicini a costanti universali, bins centrati su φ, π, √2, ecc.
|
||||
- Tab **Invariance** — scatter sharpe_BTC vs sharpe_ETH per ogni winner
|
||||
|
||||
---
|
||||
|
||||
## 5. Out-of-scope (esplicito)
|
||||
|
||||
- **Asset oltre BTC/ETH** (Oro/Argento del paper): non in primo run. Estensione futura.
|
||||
- **Range candele oltre 12**: range 3-5 del Libro (12-56 candele). Estensione futura quando lo smoke test conferma stabilità.
|
||||
- **Live trading reale**: solo paper-trading via stesso pattern di strategy_crypto.
|
||||
- **OCR/Vision sulle figure del Libro dei Frattali**: esplicitamente ESCLUSO da request utente ("senza passare alle immagini").
|
||||
- **Modifica del genoma del core**: nessuna modifica a `HypothesisAgentGenome`. Solo extension del grammar (3 indicatori).
|
||||
- **Riferimenti pseudoscientifici operativizzati come legge**: i numeri sacri/Solfeggio sono prior teorici per literal candidati, non vincoli rigidi.
|
||||
|
||||
---
|
||||
|
||||
## 6. Criteri di accettazione
|
||||
|
||||
- [ ] `uv sync` riesce dalla root con `strategy_pythagoras` come member
|
||||
- [ ] `uv run python -c "from strategy_pythagoras.indicators import candle_pattern, pythagorean_ratio, fractal_mirror"` non solleva
|
||||
- [ ] `pytest src/strategy_pythagoras/tests/` verde (almeno unit-test per i 3 indicatori)
|
||||
- [ ] GA short run `pythagoras-smoke-001` completa 5 generazioni senza errori
|
||||
- [ ] Almeno 1 winner con fitness > 0 e `cognitive_style` ∈ {7 stili Pythagoras}
|
||||
- [ ] Dashboard avvia su `http://localhost:PORT/strategy_pythagoras_gui` e mostra winners
|
||||
- [ ] `docs/analysis_first_run.md` esiste e contiene tutte le sezioni elencate in §4
|
||||
|
||||
---
|
||||
|
||||
## 7. Riferimenti
|
||||
|
||||
- [Pythagoras Trading Prediction — riassunto](../../../src/strategy_pythagoras/Pythagoras/Pythagoras_Trading_Prediction.summary.md)
|
||||
- [Libro dei Frattali — riassunto](../../../src/strategy_pythagoras/Pythagoras/Libro_frattali.summary.md)
|
||||
- Memory: [[monorepo_uv_workspace]], [[gui_subpath_routing]], [[ownership_per_modulo]], [[production_deployment]], [[model_qwen_dependency]], [[selectivity_red_flag]]
|
||||
- Template: `src/strategy_crypto/` (paper-trading + GUI), `src/strategy_crypto/strategy_crypto/prompts.json` (schema v3.2)
|
||||
- Core: `src/multi_swarm_core/multi_swarm_core/genome/hypothesis.py`, `multi_swarm_core/protocol/grammar.py`
|
||||
+4
-2
@@ -10,14 +10,16 @@ requires-python = ">=3.13"
|
||||
dependencies = [
|
||||
"multi-swarm-core",
|
||||
"strategy-crypto",
|
||||
"strategy-pythagoras",
|
||||
]
|
||||
|
||||
[tool.uv.workspace]
|
||||
members = ["src/multi_swarm_core", "src/strategy_crypto"]
|
||||
members = ["src/multi_swarm_core", "src/strategy_crypto", "src/strategy_pythagoras"]
|
||||
|
||||
[tool.uv.sources]
|
||||
multi-swarm-core = { workspace = true }
|
||||
strategy-crypto = { workspace = true }
|
||||
strategy-pythagoras = { workspace = true }
|
||||
|
||||
[dependency-groups]
|
||||
dev = [
|
||||
@@ -42,7 +44,7 @@ python_version = "3.13"
|
||||
strict = true
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
testpaths = ["src/multi_swarm_core/tests", "src/strategy_crypto/tests"]
|
||||
testpaths = ["src/multi_swarm_core/tests", "src/strategy_crypto/tests", "src/strategy_pythagoras/tests"]
|
||||
addopts = "-v --tb=short --import-mode=importlib"
|
||||
markers = [
|
||||
"integration: tests that require external services (Cerbero, LLM API)",
|
||||
|
||||
@@ -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()
|
||||
@@ -33,6 +33,36 @@ from strategy_crypto.backend import PaperExecutor, PaperRepository, Portfolio
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parent.parent
|
||||
|
||||
# Mapping timeframe stringa Cerbero -> minuti del bar. Le strategie tradano
|
||||
# sul "bar appena chiuso", quindi end deve essere snappato al boundary del
|
||||
# loro timeframe (NON sempre al top dell'ora) per evitare la regressione in
|
||||
# cui ETH 5m veniva valutato una volta sola ogni 60 min.
|
||||
_TIMEFRAME_MINUTES: dict[str, int] = {
|
||||
"1m": 1,
|
||||
"5m": 5,
|
||||
"15m": 15,
|
||||
"30m": 30,
|
||||
"1h": 60,
|
||||
"4h": 240,
|
||||
"1d": 1440,
|
||||
}
|
||||
|
||||
|
||||
def _align_end_to_timeframe(now: datetime, timeframe: str) -> datetime:
|
||||
"""Snap ``now`` al boundary del bar timeframe (UTC, naive seconds).
|
||||
|
||||
Es.: now=14:37:42, tf="5m" -> 14:35:00
|
||||
now=14:37:42, tf="1h" -> 14:00:00
|
||||
now=14:00:00, tf="1h" -> 14:00:00
|
||||
"""
|
||||
bar_min = _TIMEFRAME_MINUTES[timeframe]
|
||||
aligned = now.replace(second=0, microsecond=0)
|
||||
if bar_min >= 1440:
|
||||
return aligned.replace(hour=0, minute=0)
|
||||
total_min = aligned.hour * 60 + aligned.minute
|
||||
snapped = (total_min // bar_min) * bar_min
|
||||
return aligned.replace(hour=snapped // 60, minute=snapped % 60)
|
||||
|
||||
|
||||
def _default_strategies_dir() -> Path:
|
||||
"""Cartella JSON shippata col package strategy_crypto."""
|
||||
@@ -70,9 +100,11 @@ def load_assets(strategies_dir: Path) -> list[AssetConfig]:
|
||||
raise FileNotFoundError(
|
||||
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 [
|
||||
AssetConfig(symbol="BTC-PERPETUAL", strategy_file=btc_files[0]),
|
||||
AssetConfig(symbol="ETH-PERPETUAL", strategy_file=eth_files[0]),
|
||||
AssetConfig(symbol="BTC-PERPETUAL", strategy_file=btc_files[0], timeframe="1h"),
|
||||
AssetConfig(symbol="ETH-PERPETUAL", strategy_file=eth_files[0], timeframe="5m"),
|
||||
]
|
||||
|
||||
|
||||
@@ -129,9 +161,12 @@ def main() -> None:
|
||||
now = datetime.now(UTC)
|
||||
last_prices: dict[str, float] = {}
|
||||
for asset, executor in zip(assets, executors, strict=True):
|
||||
# fetch OHLCV most recent lookback bars
|
||||
end = now.replace(minute=0, second=0, microsecond=0)
|
||||
start = end - timedelta(hours=args.lookback_bars + 1)
|
||||
# fetch OHLCV most recent lookback bars: end snappato al timeframe
|
||||
# dell'asset, non sempre all'ora (altrimenti ETH 5m veniva valutato
|
||||
# solo ogni 60 min, regressione vs backtest tunato 5m).
|
||||
bar_min = _TIMEFRAME_MINUTES[asset.timeframe]
|
||||
end = _align_end_to_timeframe(now, asset.timeframe)
|
||||
start = end - timedelta(minutes=bar_min * (args.lookback_bars + 1))
|
||||
req = OHLCVRequest(
|
||||
symbol=asset.symbol,
|
||||
timeframe=asset.timeframe,
|
||||
|
||||
+56
-8
@@ -19,6 +19,30 @@ def _default_prompt_library_path() -> Path:
|
||||
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:
|
||||
p = argparse.ArgumentParser(description="Multi-Swarm Phase 1 runner")
|
||||
p.add_argument("--name", default="phase1-spike-001")
|
||||
@@ -35,7 +59,10 @@ def parse_args() -> argparse.Namespace:
|
||||
)
|
||||
p.add_argument("--symbol", default="BTC-PERPETUAL")
|
||||
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("--fees-bp", type=float, default=5.0)
|
||||
p.add_argument("--n-trials-dsr", type=int, default=50)
|
||||
@@ -67,8 +94,10 @@ def parse_args() -> argparse.Namespace:
|
||||
"--fitness-v2",
|
||||
action="store_true",
|
||||
help=(
|
||||
"Attiva fitness v2: solo {no_trades, degenerate, undertrading} azzerano; "
|
||||
"gli altri HIGH applicano soft penalty multiplicativa"
|
||||
"Attiva fitness v2: hard-kill su {no_trades, degenerate, undertrading, "
|
||||
"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(
|
||||
@@ -77,6 +106,16 @@ def parse_args() -> argparse.Namespace:
|
||||
default=0.4,
|
||||
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(
|
||||
"--wfa-train-split",
|
||||
type=float,
|
||||
@@ -114,6 +153,16 @@ def parse_args() -> argparse.Namespace:
|
||||
"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). OpusAgent processa in coda FIFO; concurrency > 1 accoda "
|
||||
"piu' richieste in parallelo."
|
||||
),
|
||||
)
|
||||
return p.parse_args()
|
||||
|
||||
|
||||
@@ -151,13 +200,13 @@ def main() -> None:
|
||||
print(f"OHLCV loaded: {len(ohlcv)} bars from {ohlcv.index[0]} to {ohlcv.index[-1]}")
|
||||
|
||||
llm = LLMClient(
|
||||
openrouter_api_key=settings.openrouter_api_key.get_secret_value(),
|
||||
opus_agent_api_key=settings.opus_agent_api_key.get_secret_value(),
|
||||
opus_agent_base_url=settings.opus_agent_base_url,
|
||||
model_tier_s=settings.llm_model_tier_s,
|
||||
model_tier_a=settings.llm_model_tier_a,
|
||||
model_tier_b=settings.llm_model_tier_b,
|
||||
model_tier_c=settings.llm_model_tier_c,
|
||||
model_tier_d=settings.llm_model_tier_d,
|
||||
openrouter_base_url=settings.openrouter_base_url,
|
||||
)
|
||||
|
||||
cfg = RunConfig(
|
||||
@@ -178,15 +227,14 @@ def main() -> None:
|
||||
fees_eat_alpha_threshold=args.fees_eat_alpha_threshold,
|
||||
flat_too_long_threshold=args.flat_too_long_threshold,
|
||||
undertrading_threshold=args.undertrading_threshold,
|
||||
fitness_hard_kill_findings=(
|
||||
("no_trades", "degenerate", "undertrading") if args.fitness_v2 else None
|
||||
),
|
||||
fitness_hard_kill_findings=_resolve_hard_kill(args),
|
||||
fitness_adversarial_soft_penalty=args.fitness_soft_penalty,
|
||||
wfa_train_split=args.wfa_train_split,
|
||||
wfa_top_k=args.wfa_top_k,
|
||||
eval_oos_during_loop=args.eval_oos_during_loop,
|
||||
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)
|
||||
|
||||
@@ -0,0 +1,475 @@
|
||||
"""Smoke test del GA per strategy_pythagoras.
|
||||
|
||||
Esegue 2 run di Phase 1 (BTC 5m + ETH 5m), poi cross-rank i genomi
|
||||
comuni applicando il bonus di asset-invariance (corr_signal sui pattern
|
||||
di entry entro +/-36 barre = +/-3h su 5m TF, vedi paper Pythagoras p.43).
|
||||
|
||||
Configurazione (per spec §4):
|
||||
- Population 20, 5 generations
|
||||
- Asset: BTC-PERPETUAL 5m + ETH-PERPETUAL 5m (Cerbero deribit)
|
||||
- Train window: 2024-07-01 -> 2024-12-31
|
||||
- Test window: 2025-01-01 -> 2025-01-31 (caricato come coda dello stesso
|
||||
range; non usato dal GA ma necessario per dataset continuo se in futuro
|
||||
si attiva WFA)
|
||||
- Stili cognitivi: 7 da strategy_pythagoras/prompts.json
|
||||
- Indicatori Pythagoras: candle_pattern, pythagorean_ratio, fractal_mirror
|
||||
(registrati nel compiler tramite import side-effect di strategy_pythagoras.indicators)
|
||||
- Fitness post-processing cross-asset: apply_invariance_bonus
|
||||
- Output: top 50 winners persisted in state/strategy_pythagoras.db
|
||||
(tabella pythagoras_winners)
|
||||
|
||||
Adattamento all'API reale di run_phase1 (Task 4.1 findings + verifica diretta):
|
||||
|
||||
- ``run_phase1(cfg: RunConfig, ohlcv: pd.DataFrame, llm: LLMClient) -> str``
|
||||
ritorna un ``run_id``. Non c'e' un fitness hook esterno: il GA loop
|
||||
invoca ``compute_fitness`` inline e persiste via
|
||||
``repo.save_evaluation``. Per il bonus invariance dobbiamo:
|
||||
1. lanciare due ``run_phase1`` indipendenti, uno per asset;
|
||||
2. caricare le evaluations via ``repo.list_evaluations(run_id)``;
|
||||
3. ricompilare la strategia (``_try_parse`` + ``compile_strategy``) sui
|
||||
segnali di ciascun OHLCV per estrarre gli entry index;
|
||||
4. calcolare ``corr_signal`` sugli entry binari (Series int-indexed)
|
||||
e applicare ``apply_invariance_bonus``.
|
||||
|
||||
- Le serie OHLCV NON sono shippate in repo come ``src/strategy_crypto/series/``:
|
||||
il default loader Cerbero le cachea in ``./series/{cache_key}.parquet``
|
||||
(cache key = sha1 di ``exchange|symbol|timeframe|start|end``). Riusiamo
|
||||
quel meccanismo: caricamento via ``CerberoOHLCVLoader``, identico a
|
||||
``scripts/run_phase1.py``.
|
||||
|
||||
Shape effettivo del dict ritornato da ``repo.list_evaluations(run_id)``
|
||||
(vedi ``persistence/repository.py:213`` e schema in ``schema.py``):
|
||||
|
||||
{
|
||||
'run_id', 'genome_id', 'fitness', 'dsr', 'dsr_pvalue', 'sharpe',
|
||||
'max_dd', 'total_return', 'n_trades', 'parse_error', 'raw_text',
|
||||
'eval_ts', 'fitness_oos', 'sharpe_oos', 'return_oos',
|
||||
'max_dd_oos', 'n_trades_oos'
|
||||
}
|
||||
|
||||
Nota: ``cognitive_style`` e ``generation`` NON sono nelle evaluations;
|
||||
vanno presi via ``repo.list_genomes(run_id)`` (payload_json del genoma).
|
||||
``raw_text`` contiene il completion grezzo del LLM, da cui si estrae
|
||||
nuovamente lo ``Strategy`` AST via ``_try_parse``.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import sqlite3
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import pandas as pd # type: ignore[import-untyped]
|
||||
|
||||
# Side-effect import: registra candle_pattern, pythagorean_ratio, fractal_mirror
|
||||
# in compiler.INDICATOR_FNS prima che il GA inizi a compilare strategie.
|
||||
# (Il compiler in protocol/compiler.py importa gia' i 3 simboli dal package
|
||||
# strategy_pythagoras.indicators, ma facciamo l'import esplicito qui per
|
||||
# rendere la dipendenza chiara e indipendente dall'ordine di import.)
|
||||
import strategy_pythagoras.indicators # noqa: F401
|
||||
from multi_swarm_core.agents.hypothesis import _try_parse
|
||||
from multi_swarm_core.backtest.orders import Side
|
||||
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.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.orchestrator.run import RunConfig, run_phase1
|
||||
from multi_swarm_core.persistence.repository import Repository
|
||||
from multi_swarm_core.protocol.compiler import compile_strategy
|
||||
from strategy_pythagoras.fitness_invariance import (
|
||||
apply_invariance_bonus,
|
||||
corr_signal,
|
||||
)
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
DB_PATH = Path(
|
||||
os.getenv("STRATEGY_PYTHAGORAS_DB_PATH", str(ROOT / "state" / "strategy_pythagoras.db"))
|
||||
)
|
||||
PROMPTS_PATH = ROOT / "src" / "strategy_pythagoras" / "strategy_pythagoras" / "prompts.json"
|
||||
RUN_NAME = os.getenv("PYTHAGORAS_SMOKE_RUN_NAME", "pythagoras-smoke-001")
|
||||
|
||||
# GA configuration (smoke per spec §4)
|
||||
POPULATION = 20
|
||||
GENERATIONS = 5
|
||||
|
||||
# Data window
|
||||
TRAIN_START = datetime.fromisoformat("2024-07-01T00:00:00+00:00")
|
||||
TRAIN_END = datetime.fromisoformat("2024-12-31T23:55:00+00:00")
|
||||
# Carichiamo anche gennaio 2025 come coda (per usi futuri: WFA OOS).
|
||||
# Il GA loop in questa fase usa l'intero range; e' compito di un eventuale
|
||||
# wfa_train_split (non attivato qui per coerenza con spec §4 smoke).
|
||||
TEST_END = datetime.fromisoformat("2025-01-31T23:55:00+00:00")
|
||||
|
||||
ASSETS: list[tuple[str, str]] = [
|
||||
("BTC-PERPETUAL", "btc"),
|
||||
("ETH-PERPETUAL", "eth"),
|
||||
]
|
||||
TIMEFRAME = "5m"
|
||||
EXCHANGE = "deribit"
|
||||
TOP_K_PERSIST = 50
|
||||
|
||||
logger = logging.getLogger(RUN_NAME)
|
||||
|
||||
|
||||
def init_winners_table(con: sqlite3.Connection) -> None:
|
||||
"""Crea ``pythagoras_winners`` se non esiste (idempotente)."""
|
||||
con.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS pythagoras_winners (
|
||||
genome_id TEXT PRIMARY KEY,
|
||||
cognitive_style TEXT,
|
||||
fitness REAL,
|
||||
sharpe_btc REAL,
|
||||
sharpe_eth REAL,
|
||||
invariance_score REAL,
|
||||
rules_json TEXT,
|
||||
generation INTEGER,
|
||||
run_name TEXT
|
||||
)
|
||||
"""
|
||||
)
|
||||
con.commit()
|
||||
|
||||
|
||||
def _load_ohlcv(loader: CerberoOHLCVLoader, symbol: str) -> pd.DataFrame:
|
||||
"""Carica la finestra ``TRAIN_START -> TEST_END`` per ``symbol`` su 5m."""
|
||||
req = OHLCVRequest(
|
||||
symbol=symbol,
|
||||
timeframe=TIMEFRAME,
|
||||
start=TRAIN_START,
|
||||
end=TEST_END,
|
||||
exchange=EXCHANGE,
|
||||
)
|
||||
ohlcv = loader.load(req)
|
||||
logger.info(
|
||||
"OHLCV loaded for %s: %d bars (%s -> %s)",
|
||||
symbol, len(ohlcv),
|
||||
ohlcv.index[0] if len(ohlcv) else "n/a",
|
||||
ohlcv.index[-1] if len(ohlcv) else "n/a",
|
||||
)
|
||||
return ohlcv
|
||||
|
||||
|
||||
def _build_run_config(
|
||||
run_name: str, symbol: str, prompt_library: PromptLibrary, db_path: Path,
|
||||
) -> RunConfig:
|
||||
"""Costruisce il ``RunConfig`` per un singolo asset.
|
||||
|
||||
Usa lo stesso GA-core DB del progetto (``settings.ga_db_path`` se override
|
||||
non passato): vi vengono scritte ``runs``, ``generations``, ``genomes``,
|
||||
``evaluations`` per la run.
|
||||
"""
|
||||
return RunConfig(
|
||||
run_name=run_name,
|
||||
population_size=POPULATION,
|
||||
n_generations=GENERATIONS,
|
||||
elite_k=2,
|
||||
tournament_k=3,
|
||||
p_crossover=0.5,
|
||||
seed=42,
|
||||
model_tier=ModelTier.C,
|
||||
symbol=symbol,
|
||||
timeframe=TIMEFRAME,
|
||||
fees_bp=5.0,
|
||||
n_trials_dsr=50,
|
||||
db_path=db_path,
|
||||
prompt_library=prompt_library,
|
||||
# Smoke: niente WFA, niente eval OOS in loop, niente prompt mutation LLM.
|
||||
# I parametri restano sui default sicuri di RunConfig.
|
||||
)
|
||||
|
||||
|
||||
def run_ga_for_asset(
|
||||
asset_label: str,
|
||||
symbol: str,
|
||||
ohlcv: pd.DataFrame,
|
||||
prompt_library: PromptLibrary,
|
||||
llm: LLMClient,
|
||||
ga_db_path: Path,
|
||||
) -> tuple[str, Repository]:
|
||||
"""Lancia ``run_phase1`` per un asset.
|
||||
|
||||
Ritorna ``(run_id, repo)`` per il caller, che usera' ``repo`` per
|
||||
estrarre evaluations + genomes a fine del run.
|
||||
"""
|
||||
run_name = f"{RUN_NAME}-{asset_label}"
|
||||
cfg = _build_run_config(run_name, symbol, prompt_library, ga_db_path)
|
||||
logger.info("Starting GA run '%s' on %s (%d bars)", run_name, symbol, len(ohlcv))
|
||||
run_id = run_phase1(cfg, ohlcv=ohlcv, llm=llm)
|
||||
logger.info("Run '%s' completed: run_id=%s", run_name, run_id)
|
||||
repo = Repository(ga_db_path)
|
||||
return run_id, repo
|
||||
|
||||
|
||||
def _entries_series_from_eval(
|
||||
eval_row: dict[str, Any], ohlcv: pd.DataFrame,
|
||||
) -> pd.Series | None:
|
||||
"""Ricostruisce gli entries binari (Side.LONG/SHORT -> 1, altrimenti 0)
|
||||
a partire dal ``raw_text`` salvato nell'eval row.
|
||||
|
||||
Ritorna ``None`` se il raw_text non e' parsabile (caso parse_error).
|
||||
L'index della Series ritornata e' INTERO posizionale (0..N-1) come
|
||||
richiesto da ``corr_signal`` (vedi tests in
|
||||
``strategy_pythagoras/tests/test_fitness_invariance.py``).
|
||||
"""
|
||||
raw = eval_row.get("raw_text")
|
||||
if not raw:
|
||||
return None
|
||||
strategy, parse_err = _try_parse(raw)
|
||||
if strategy is None:
|
||||
logger.debug(
|
||||
"skip genome %s: parse error '%s'",
|
||||
eval_row.get("genome_id"), parse_err,
|
||||
)
|
||||
return None
|
||||
try:
|
||||
signal_fn = compile_strategy(strategy)
|
||||
signals = signal_fn(ohlcv)
|
||||
except Exception as exc:
|
||||
logger.debug(
|
||||
"skip genome %s: compile/exec error: %s",
|
||||
eval_row.get("genome_id"), exc,
|
||||
)
|
||||
return None
|
||||
# 1 dove il signal e' LONG o SHORT (entry attiva), 0 altrove.
|
||||
is_entry = signals.isin([Side.LONG, Side.SHORT]).fillna(False).astype(int)
|
||||
# Riassegna integer index per il match in corr_signal (che somma delta
|
||||
# interi all'index e fa il test ``ti + delta in b_set``).
|
||||
return pd.Series(is_entry.values, index=range(len(is_entry)), dtype="int64")
|
||||
|
||||
|
||||
def _collect_evaluations(
|
||||
repo: Repository, run_id: str, ohlcv: pd.DataFrame,
|
||||
) -> dict[str, dict[str, Any]]:
|
||||
"""Carica evaluations + genomes per ``run_id`` e li unisce per genome_id.
|
||||
|
||||
Returns: dict ``{genome_id: row}`` dove ``row`` contiene i campi
|
||||
dell'eval + ``cognitive_style``, ``generation``, ``strategy_json``
|
||||
(dict del genoma serializzato) e ``entries`` (pd.Series int-indexed).
|
||||
"""
|
||||
evals = repo.list_evaluations(run_id)
|
||||
genomes = repo.list_genomes(run_id)
|
||||
genome_by_id: dict[str, dict[str, Any]] = {}
|
||||
for grow in genomes:
|
||||
try:
|
||||
payload = json.loads(grow["payload_json"])
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
payload = {}
|
||||
genome_by_id[grow["id"]] = payload
|
||||
|
||||
out: dict[str, dict[str, Any]] = {}
|
||||
for ev in evals:
|
||||
gid = ev["genome_id"]
|
||||
payload = genome_by_id.get(gid, {})
|
||||
row = dict(ev)
|
||||
row["cognitive_style"] = payload.get("cognitive_style", "")
|
||||
row["generation"] = int(payload.get("generation", 0))
|
||||
# ``raw_text`` e' il completion grezzo; lo ri-parsiamo in
|
||||
# _entries_series_from_eval. Salviamo la rappresentazione canonica
|
||||
# ``strategy_json`` per persistenza (best-effort: se il parse fallisce
|
||||
# salviamo il raw_text come fallback).
|
||||
strategy, _err = _try_parse(row.get("raw_text") or "")
|
||||
if strategy is not None:
|
||||
# Strategy non e' direttamente JSON-serializable: serializziamo
|
||||
# la struttura nominale tramite dataclasses.asdict-like fallback.
|
||||
try:
|
||||
row["strategy_json"] = _strategy_to_jsonable(strategy)
|
||||
except Exception:
|
||||
row["strategy_json"] = {"raw_text": row.get("raw_text", "")}
|
||||
else:
|
||||
row["strategy_json"] = {"raw_text": row.get("raw_text", "")}
|
||||
row["entries"] = _entries_series_from_eval(ev, ohlcv)
|
||||
out[gid] = row
|
||||
return out
|
||||
|
||||
|
||||
def _strategy_to_jsonable(strategy: Any) -> dict[str, Any]:
|
||||
"""Serializza un ``Strategy`` AST in dict JSON-friendly.
|
||||
|
||||
Strategy/Rule/Node sono dataclass: usiamo ``dataclasses.asdict`` quando
|
||||
possibile, con fallback a ``str(strategy)`` se la struttura contiene
|
||||
membri non-serializzabili (es. enum non-Str).
|
||||
"""
|
||||
import dataclasses
|
||||
if dataclasses.is_dataclass(strategy):
|
||||
try:
|
||||
return dataclasses.asdict(strategy)
|
||||
except TypeError:
|
||||
pass
|
||||
return {"repr": repr(strategy)}
|
||||
|
||||
|
||||
def compute_invariance_for_pair(
|
||||
btc_evals: dict[str, dict[str, Any]],
|
||||
eth_evals: dict[str, dict[str, Any]],
|
||||
) -> list[dict[str, Any]]:
|
||||
"""Per ogni genome_id presente in entrambi i run, calcola invariance + bonus.
|
||||
|
||||
Lo stesso ``genome_id`` puo' apparire in entrambi i run perche' l'id e'
|
||||
deterministico (sha1 di system_prompt+feature_access+temperature+...) e
|
||||
il seed del GA e' fisso: il founder set e i mutanti hanno alta probabilita'
|
||||
di collisione cross-asset. Quando il genoma compare in entrambi, le
|
||||
metriche ``sharpe`` IS sono comparabili e ha senso valutare l'invarianza.
|
||||
"""
|
||||
out: list[dict[str, Any]] = []
|
||||
common_ids = set(btc_evals) & set(eth_evals)
|
||||
logger.info(
|
||||
"Common genomes BTC ∩ ETH: %d (BTC: %d, ETH: %d)",
|
||||
len(common_ids), len(btc_evals), len(eth_evals),
|
||||
)
|
||||
for gid in common_ids:
|
||||
b = btc_evals[gid]
|
||||
e = eth_evals[gid]
|
||||
entries_btc: pd.Series | None = b.get("entries")
|
||||
entries_eth: pd.Series | None = e.get("entries")
|
||||
if entries_btc is None or entries_eth is None:
|
||||
inv = 0.0
|
||||
elif len(entries_btc) == 0 or len(entries_eth) == 0:
|
||||
inv = 0.0
|
||||
else:
|
||||
try:
|
||||
# Allineiamo a lunghezza minima: i due asset possono avere
|
||||
# un numero di bars leggermente diverso (gap nel feed Cerbero).
|
||||
# corr_signal lavora solo sugli index 1 -> il troncamento non
|
||||
# introduce bias asimmetrici.
|
||||
min_len = min(len(entries_btc), len(entries_eth))
|
||||
inv = corr_signal(
|
||||
entries_btc.iloc[:min_len].reset_index(drop=True),
|
||||
entries_eth.iloc[:min_len].reset_index(drop=True),
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.warning("corr_signal failed for %s: %s", gid, exc)
|
||||
inv = 0.0
|
||||
sharpe_btc = float(b.get("sharpe") or 0.0)
|
||||
sharpe_eth = float(e.get("sharpe") or 0.0)
|
||||
mean_sharpe = 0.5 * (sharpe_btc + sharpe_eth)
|
||||
boosted = apply_invariance_bonus(mean_sharpe, inv)
|
||||
out.append({
|
||||
"genome_id": gid,
|
||||
"cognitive_style": b.get("cognitive_style") or e.get("cognitive_style", ""),
|
||||
"fitness": float(boosted),
|
||||
"sharpe_btc": sharpe_btc,
|
||||
"sharpe_eth": sharpe_eth,
|
||||
"invariance_score": float(inv),
|
||||
"rules_json": json.dumps(b.get("strategy_json") or {}, default=str),
|
||||
"generation": int(b.get("generation", 0)),
|
||||
"run_name": RUN_NAME,
|
||||
})
|
||||
return sorted(out, key=lambda r: r["fitness"], reverse=True)
|
||||
|
||||
|
||||
def persist_winners(con: sqlite3.Connection, winners: list[dict[str, Any]]) -> None:
|
||||
if not winners:
|
||||
logger.warning("No winners to persist")
|
||||
return
|
||||
con.executemany(
|
||||
"""
|
||||
INSERT OR REPLACE INTO pythagoras_winners
|
||||
(genome_id, cognitive_style, fitness, sharpe_btc, sharpe_eth,
|
||||
invariance_score, rules_json, generation, run_name)
|
||||
VALUES (:genome_id, :cognitive_style, :fitness, :sharpe_btc, :sharpe_eth,
|
||||
:invariance_score, :rules_json, :generation, :run_name)
|
||||
""",
|
||||
winners,
|
||||
)
|
||||
con.commit()
|
||||
|
||||
|
||||
def main() -> None:
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format="%(asctime)s %(levelname)s %(name)s %(message)s",
|
||||
)
|
||||
settings = load_settings()
|
||||
|
||||
# Prompt library Pythagoras (NON quello di strategy_crypto).
|
||||
if not PROMPTS_PATH.exists():
|
||||
raise FileNotFoundError(f"Prompts file not found: {PROMPTS_PATH}")
|
||||
prompt_library = PromptLibrary.from_json(PROMPTS_PATH)
|
||||
logger.info(
|
||||
"PromptLibrary loaded from %s: %d styles (%s)",
|
||||
PROMPTS_PATH, len(prompt_library.styles),
|
||||
", ".join(prompt_library.cognitive_styles),
|
||||
)
|
||||
|
||||
# Cerbero client + OHLCV loader (riusa la cache parquet in ./series).
|
||||
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)
|
||||
|
||||
llm = LLMClient(
|
||||
opus_agent_api_key=settings.opus_agent_api_key.get_secret_value(),
|
||||
opus_agent_base_url=settings.opus_agent_base_url,
|
||||
model_tier_s=settings.llm_model_tier_s,
|
||||
model_tier_a=settings.llm_model_tier_a,
|
||||
model_tier_b=settings.llm_model_tier_b,
|
||||
model_tier_c=settings.llm_model_tier_c,
|
||||
model_tier_d=settings.llm_model_tier_d,
|
||||
)
|
||||
|
||||
# Setup DB winners (separato dal GA core DB).
|
||||
DB_PATH.parent.mkdir(parents=True, exist_ok=True)
|
||||
con = sqlite3.connect(DB_PATH)
|
||||
try:
|
||||
init_winners_table(con)
|
||||
logger.info("Winners DB initialized at %s", DB_PATH)
|
||||
|
||||
# Carica OHLCV per entrambi gli asset PRIMA dei run GA, cosi' se la
|
||||
# rete o Cerbero sono giu' falliamo subito senza sprecare chiamate LLM.
|
||||
ohlcv_by_asset: dict[str, pd.DataFrame] = {}
|
||||
for symbol, label in ASSETS:
|
||||
ohlcv_by_asset[label] = _load_ohlcv(loader, symbol)
|
||||
|
||||
# Run GA per asset. Usa il GA-core DB definito in settings; ogni run
|
||||
# crea un proprio run_id e set di evaluations isolato.
|
||||
evals_by_asset: dict[str, dict[str, dict[str, Any]]] = {}
|
||||
for symbol, label in ASSETS:
|
||||
run_id, repo = run_ga_for_asset(
|
||||
asset_label=label,
|
||||
symbol=symbol,
|
||||
ohlcv=ohlcv_by_asset[label],
|
||||
prompt_library=prompt_library,
|
||||
llm=llm,
|
||||
ga_db_path=settings.ga_db_path,
|
||||
)
|
||||
evals_by_asset[label] = _collect_evaluations(
|
||||
repo, run_id, ohlcv_by_asset[label]
|
||||
)
|
||||
logger.info(
|
||||
"%s: %d evaluations collected", label.upper(),
|
||||
len(evals_by_asset[label]),
|
||||
)
|
||||
|
||||
# Cross-rank con invariance bonus.
|
||||
winners = compute_invariance_for_pair(
|
||||
evals_by_asset["btc"], evals_by_asset["eth"],
|
||||
)
|
||||
logger.info(
|
||||
"Computed invariance bonus for %d common genomes", len(winners),
|
||||
)
|
||||
|
||||
top = winners[:TOP_K_PERSIST]
|
||||
persist_winners(con, top)
|
||||
logger.info(
|
||||
"Persisted top %d winners to %s (table: pythagoras_winners)",
|
||||
len(top), DB_PATH,
|
||||
)
|
||||
finally:
|
||||
con.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -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()
|
||||
@@ -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
|
||||
# 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)
|
||||
total_fees = sum(t.fees for t in result.trades)
|
||||
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
|
||||
|
||||
@@ -4,11 +4,9 @@ import re
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any
|
||||
|
||||
import openai
|
||||
|
||||
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, OpusAgentError, OpusAgentTransientError
|
||||
from ..protocol.parser import ParseError, Strategy, parse_strategy
|
||||
from ..protocol.validator import ValidationError, validate_strategy
|
||||
|
||||
@@ -181,6 +179,12 @@ def _build_system_prompt(lib: PromptLibrary, genome: HypothesisAgentGenome) -> s
|
||||
parts.append("")
|
||||
# 3. Grammar spec (core scaffold)
|
||||
parts.append(_SYSTEM_GRAMMAR_SPEC)
|
||||
# 3b. Custom indicators spec (da prompts.json, opzionale) - estende la lista
|
||||
# "Leaf - indicatori" con firme strategy-specific non note al core.
|
||||
if lib.custom_indicators_spec:
|
||||
parts.append("\nINDICATORI CUSTOM (firme aggiuntive, applica le stesse regole grammaticali):\n")
|
||||
parts.append(lib.custom_indicators_spec)
|
||||
parts.append("")
|
||||
# 4. Pattern guidance (da prompts.json, opzionale)
|
||||
if lib.pattern_guidance:
|
||||
parts.append(
|
||||
@@ -284,19 +288,12 @@ def _render_focus_block(keys: list[str], market: MarketSummary) -> str:
|
||||
|
||||
|
||||
_RETRY_TEMPLATE = """\
|
||||
{original_user}
|
||||
Il JSON che hai generato contiene un errore: {previous_error}
|
||||
|
||||
--- TENTATIVO PRECEDENTE FALLITO ---
|
||||
Output: {previous_raw}
|
||||
Errore: {previous_error}
|
||||
---
|
||||
Correggi l'errore e rispondi di nuovo con un singolo oggetto JSON valido
|
||||
dentro fence ```json...```, seguendo strettamente lo schema fornito nel
|
||||
SYSTEM message.
|
||||
Correggi e rispondi di nuovo con un singolo oggetto JSON valido
|
||||
dentro fence ```json...```, seguendo strettamente lo schema fornito.
|
||||
"""
|
||||
|
||||
_RETRY_RAW_TRUNCATE = 800
|
||||
|
||||
|
||||
_JSON_FENCE_RE = re.compile(
|
||||
r"```(?:json)?\s*(\{[\s\S]*\})\s*```",
|
||||
@@ -425,34 +422,39 @@ class HypothesisAgent:
|
||||
errors: list[str] = []
|
||||
last_raw = ""
|
||||
max_attempts = 1 + self._max_retries
|
||||
session_id: str | None = None
|
||||
|
||||
try:
|
||||
for attempt in range(max_attempts):
|
||||
if attempt == 0:
|
||||
user = original_user
|
||||
req_session_id = "new"
|
||||
else:
|
||||
truncated = last_raw[:_RETRY_RAW_TRUNCATE]
|
||||
user = _RETRY_TEMPLATE.format(
|
||||
original_user=original_user,
|
||||
previous_raw=truncated,
|
||||
previous_error=errors[-1],
|
||||
)
|
||||
user = _RETRY_TEMPLATE.format(previous_error=errors[-1])
|
||||
req_session_id = session_id or "new"
|
||||
|
||||
try:
|
||||
completion = self._llm.complete(genome, system=system, user=user)
|
||||
completion = self._llm.complete(
|
||||
genome, system=system, user=user,
|
||||
session_id=req_session_id,
|
||||
summarize=True,
|
||||
)
|
||||
except EmptyCompletionError as e:
|
||||
# LLM esaurito retry tenacity senza una risposta. Tratta come
|
||||
# parse-fail "empty" e ritenta nel loop esterno (max_attempts).
|
||||
errors.append(f"empty_completion: {e}")
|
||||
last_raw = ""
|
||||
continue
|
||||
except openai.RateLimitError as e:
|
||||
# Provider upstream rate limited oltre i retry tenacity.
|
||||
# Marca genome come fallito senza propagare l'eccezione al run.
|
||||
errors.append(f"rate_limit: {e}")
|
||||
except OpusAgentTransientError as e:
|
||||
errors.append(f"transient_error: {e}")
|
||||
last_raw = ""
|
||||
continue
|
||||
except OpusAgentError as e:
|
||||
errors.append(f"opus_agent_error: {e}")
|
||||
last_raw = ""
|
||||
continue
|
||||
completions.append(completion)
|
||||
last_raw = completion.text
|
||||
if completion.session_id:
|
||||
session_id = completion.session_id
|
||||
|
||||
strategy, err = _try_parse(completion.text)
|
||||
if strategy is not None:
|
||||
@@ -465,6 +467,9 @@ class HypothesisAgent:
|
||||
)
|
||||
assert err is not None
|
||||
errors.append(err)
|
||||
finally:
|
||||
if session_id:
|
||||
self._llm.close_session(session_id)
|
||||
|
||||
chained = " | ".join(
|
||||
f"attempt {i + 1}: {e}" for i, e in enumerate(errors)
|
||||
|
||||
@@ -2,6 +2,7 @@ from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd # type: ignore[import-untyped]
|
||||
|
||||
from .orders import Position, Side, Trade
|
||||
@@ -28,74 +29,110 @@ class BacktestEngine:
|
||||
self.fees_bp = fees_bp
|
||||
|
||||
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)
|
||||
|
||||
# Esecuzione con delay 1: segnale a t-1 esegue a open di t.
|
||||
shifted = [Side.FLAT, *list(signals.iloc[:-1])]
|
||||
executed_side = pd.Series(shifted, index=ohlcv.index, dtype=object)
|
||||
executed = pd.Series(
|
||||
[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] = []
|
||||
equity = 0.0
|
||||
equity_history: list[float] = []
|
||||
returns_history: list[float] = []
|
||||
prev_equity = 0.0
|
||||
# realized_pnl[t]: PnL netto cumulato dopo le chiusure avvenute a OPEN di t.
|
||||
realized_pnl = np.zeros(n, dtype=np.float64)
|
||||
fees_rate = self.fees_bp / 10000.0
|
||||
size = 1.0
|
||||
|
||||
for ts, row in ohlcv.iterrows():
|
||||
target_side = executed_side.loc[ts]
|
||||
current_side = position.side if position else Side.FLAT
|
||||
# Posizione corrente all'inizio di ogni indice t (prima di applicare il transitorio):
|
||||
# used per MtM computation. open_side_at_t / open_entry_at_t.
|
||||
open_side = np.zeros(n, dtype=np.int8)
|
||||
open_entry = np.zeros(n, dtype=np.float64)
|
||||
|
||||
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"]),
|
||||
for entry_idx in entry_idxs:
|
||||
entry_side = int(side_code[entry_idx])
|
||||
entry_price = opens[entry_idx]
|
||||
# Cerca exit: primo indice > entry_idx dove side differisce.
|
||||
after = side_code[entry_idx + 1:]
|
||||
rel = np.flatnonzero(after != entry_side)
|
||||
if rel.size > 0:
|
||||
exit_idx = entry_idx + 1 + int(rel[0])
|
||||
exit_price = opens[exit_idx]
|
||||
exit_ts = ts_index[exit_idx]
|
||||
gross = entry_side * (exit_price - entry_price) * size
|
||||
fees = fees_rate * size * (entry_price + exit_price)
|
||||
net = gross - fees
|
||||
# La chiusura avviene a open[exit_idx]: dal bar exit_idx in poi il
|
||||
# PnL e' realizzato (non piu' MtM).
|
||||
realized_pnl[exit_idx:] += net
|
||||
# Posizione aperta in [entry_idx, exit_idx-1].
|
||||
open_side[entry_idx:exit_idx] = entry_side
|
||||
open_entry[entry_idx:exit_idx] = entry_price
|
||||
trades.append(Trade(
|
||||
entry_ts=ts_index[entry_idx],
|
||||
exit_ts=exit_ts,
|
||||
side=Side.LONG if entry_side == 1 else Side.SHORT,
|
||||
size=size,
|
||||
entry_price=entry_price,
|
||||
exit_price=exit_price,
|
||||
fees_bp=self.fees_bp,
|
||||
)
|
||||
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,
|
||||
))
|
||||
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,
|
||||
)
|
||||
trades.append(trade)
|
||||
equity += trade.net_pnl
|
||||
equity_history[-1] = equity
|
||||
if len(returns_history) >= 2:
|
||||
returns_history[-1] = equity - equity_history[-2]
|
||||
))
|
||||
|
||||
# MtM unrealized per ogni bar in cui c'e' una posizione aperta.
|
||||
mtm = open_side.astype(np.float64) * (closes - open_entry) * size
|
||||
equity_arr = realized_pnl + mtm
|
||||
# Returns = first diff dell'equity (col loop legacy il primo bar e' equity[0]-0).
|
||||
returns_arr = np.concatenate(([equity_arr[0]], np.diff(equity_arr)))
|
||||
|
||||
return BacktestResult(
|
||||
equity_curve=pd.Series(equity_history, index=ohlcv.index, name="equity"),
|
||||
returns=pd.Series(returns_history, index=ohlcv.index, name="returns"),
|
||||
equity_curve=pd.Series(equity_arr, index=ts_index, name="equity"),
|
||||
returns=pd.Series(returns_arr, index=ts_index, name="returns"),
|
||||
trades=trades,
|
||||
)
|
||||
|
||||
|
||||
# Lo facade Position re-export e' tenuto per backward-compat con import legacy.
|
||||
__all__ = ["BacktestEngine", "BacktestResult", "Position", "Side", "Signal", "Trade"]
|
||||
|
||||
@@ -23,14 +23,14 @@ class Settings(BaseSettings):
|
||||
cerbero_mainnet_token: SecretStr | None = None
|
||||
cerbero_bot_tag: str = "swarm-poc-phase1"
|
||||
|
||||
openrouter_api_key: SecretStr
|
||||
opus_agent_api_key: SecretStr
|
||||
opus_agent_base_url: str = "https://opus-agent.tielogic.xyz"
|
||||
|
||||
llm_model_tier_s: str = "google/gemini-3-flash-preview"
|
||||
llm_model_tier_a: str = "deepseek/deepseek-v4-flash"
|
||||
llm_model_tier_b: str = "deepseek/deepseek-v4-flash"
|
||||
llm_model_tier_c: str = "qwen/qwen-2.5-72b-instruct"
|
||||
llm_model_tier_d: str = "openai/gpt-oss-20b"
|
||||
openrouter_base_url: str = "https://openrouter.ai/api/v1"
|
||||
llm_model_tier_s: str = "claude-opus-4-7"
|
||||
llm_model_tier_a: str = "claude-opus-4-7"
|
||||
llm_model_tier_b: str = "claude-sonnet-4-6"
|
||||
llm_model_tier_c: str = "claude-sonnet-4-6"
|
||||
llm_model_tier_d: str = "claude-haiku-4-5-20251001"
|
||||
|
||||
run_name: str = "phase1-spike-001"
|
||||
data_dir: Path = Field(default=Path("./data"))
|
||||
|
||||
@@ -263,7 +263,8 @@ def index() -> None:
|
||||
refresh()
|
||||
|
||||
select.on_value_change(on_select_change)
|
||||
ui.timer(REFRESH_INTERVAL_S, refresh)
|
||||
_timer = ui.timer(REFRESH_INTERVAL_S, refresh)
|
||||
ui.context.client.on_disconnect(_timer.deactivate)
|
||||
refresh()
|
||||
|
||||
|
||||
@@ -353,7 +354,8 @@ def convergence() -> None:
|
||||
refresh()
|
||||
|
||||
select.on_value_change(on_select_change)
|
||||
ui.timer(REFRESH_INTERVAL_S, refresh)
|
||||
_timer = ui.timer(REFRESH_INTERVAL_S, refresh)
|
||||
ui.context.client.on_disconnect(_timer.deactivate)
|
||||
refresh()
|
||||
|
||||
|
||||
@@ -535,7 +537,8 @@ def genomes() -> None:
|
||||
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)
|
||||
_timer = ui.timer(REFRESH_INTERVAL_S, refresh)
|
||||
ui.context.client.on_disconnect(_timer.deactivate)
|
||||
refresh()
|
||||
|
||||
|
||||
|
||||
@@ -8,11 +8,11 @@ from typing import Any
|
||||
|
||||
|
||||
class ModelTier(StrEnum):
|
||||
S = "S" # top-tier reasoning (Opus / equivalent) via Anthropic
|
||||
A = "A" # premium override via Anthropic
|
||||
B = "B" # Sonnet 4.6 via Anthropic
|
||||
C = "C" # Qwen 2.5 72B via OpenRouter
|
||||
D = "D" # ultra-economic (Llama / cheap models) via OpenRouter
|
||||
S = "S" # top-tier reasoning → opus via OpusAgent
|
||||
A = "A" # premium → opus via OpusAgent
|
||||
B = "B" # standard → sonnet via OpusAgent
|
||||
C = "C" # default GA → sonnet via OpusAgent
|
||||
D = "D" # economic → haiku via OpusAgent
|
||||
|
||||
|
||||
@dataclass
|
||||
|
||||
@@ -101,6 +101,10 @@ class PromptLibrary:
|
||||
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
|
||||
# NEW v3.2: firme formali (arity + range) di indicatori strategy-specific
|
||||
# non noti al core. Iniettato in _SYSTEM_GRAMMAR_SPEC come estensione della
|
||||
# lista "Leaf - indicatori" per consentire alla LLM di chiamarli correttamente.
|
||||
custom_indicators_spec: str = field(default="")
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if not self.styles:
|
||||
@@ -120,6 +124,7 @@ class PromptLibrary:
|
||||
("domain_warnings", self.domain_warnings),
|
||||
("anti_patterns", self.anti_patterns),
|
||||
("output_priorities", self.output_priorities),
|
||||
("custom_indicators_spec", self.custom_indicators_spec),
|
||||
):
|
||||
if not isinstance(value, str):
|
||||
raise PromptLibraryError(
|
||||
@@ -142,6 +147,7 @@ class PromptLibrary:
|
||||
domain_warnings="",
|
||||
anti_patterns="",
|
||||
output_priorities="",
|
||||
custom_indicators_spec="",
|
||||
)
|
||||
|
||||
@classmethod
|
||||
@@ -204,6 +210,12 @@ class PromptLibrary:
|
||||
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)}")
|
||||
# Parse new optional top-level field (v3.2)
|
||||
custom_indicators_spec_raw = data.get("custom_indicators_spec", "")
|
||||
if not isinstance(custom_indicators_spec_raw, str):
|
||||
raise PromptLibraryError(
|
||||
f"custom_indicators_spec deve essere stringa, non {type(custom_indicators_spec_raw)}"
|
||||
)
|
||||
|
||||
return cls(
|
||||
styles=styles,
|
||||
@@ -214,6 +226,7 @@ class PromptLibrary:
|
||||
domain_warnings=domain_warnings,
|
||||
anti_patterns=anti_patterns_raw,
|
||||
output_priorities=output_priorities_raw,
|
||||
custom_indicators_spec=custom_indicators_spec_raw,
|
||||
)
|
||||
|
||||
@property
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import logging
|
||||
import threading
|
||||
import time
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
import openai
|
||||
from openai import OpenAI
|
||||
import httpx
|
||||
from tenacity import (
|
||||
retry,
|
||||
retry_if_exception_type,
|
||||
@@ -14,26 +16,33 @@ from tenacity import (
|
||||
|
||||
from ..genome.hypothesis import HypothesisAgentGenome, ModelTier
|
||||
|
||||
# Modelli configurati per Phase 1 — tutti via OpenRouter
|
||||
MODEL_TIER_S = "google/gemini-3-flash-preview"
|
||||
MODEL_TIER_A = "deepseek/deepseek-v4-flash"
|
||||
MODEL_TIER_B = "deepseek/deepseek-v4-flash"
|
||||
MODEL_TIER_C = "qwen/qwen-2.5-72b-instruct"
|
||||
MODEL_TIER_D = "openai/gpt-oss-20b"
|
||||
OPENROUTER_BASE_URL = "https://openrouter.ai/api/v1"
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
MODEL_TIER_MAP: dict[ModelTier, str] = {
|
||||
ModelTier.S: "claude-opus-4-7",
|
||||
ModelTier.A: "claude-opus-4-7",
|
||||
ModelTier.B: "claude-sonnet-4-6",
|
||||
ModelTier.C: "claude-sonnet-4-6",
|
||||
ModelTier.D: "claude-haiku-4-5-20251001",
|
||||
}
|
||||
|
||||
|
||||
class EmptyCompletionError(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
# Errori transient: retry. Auth/InvalidRequest: NO retry.
|
||||
# RateLimitError (HTTP 429) ora retryable: provider OpenRouter come DeepInfra
|
||||
# applicano rate limit upstream temporaneo, recuperabile con backoff.
|
||||
class OpusAgentError(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
class OpusAgentTransientError(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
_RETRYABLE_EXCEPTIONS: tuple[type[BaseException], ...] = (
|
||||
openai.APIConnectionError,
|
||||
openai.APITimeoutError,
|
||||
openai.InternalServerError,
|
||||
openai.RateLimitError,
|
||||
httpx.ConnectError,
|
||||
httpx.TimeoutException,
|
||||
OpusAgentTransientError,
|
||||
EmptyCompletionError,
|
||||
)
|
||||
|
||||
@@ -45,50 +54,99 @@ class CompletionResult:
|
||||
output_tokens: int
|
||||
tier: ModelTier
|
||||
model: str
|
||||
session_id: str | None = None
|
||||
|
||||
|
||||
class LLMClient:
|
||||
# Provider OpenRouter da escludere di default. Novita rifiuta /completions
|
||||
# endpoint per modelli Qwen 2.x — vedi bug 2026-05-12.
|
||||
DEFAULT_PROVIDER_IGNORE: tuple[str, ...] = ("Novita",)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
openrouter_api_key: str,
|
||||
model_tier_s: str = MODEL_TIER_S,
|
||||
model_tier_a: str = MODEL_TIER_A,
|
||||
model_tier_b: str = MODEL_TIER_B,
|
||||
model_tier_c: str = MODEL_TIER_C,
|
||||
model_tier_d: str = MODEL_TIER_D,
|
||||
openrouter_base_url: str = OPENROUTER_BASE_URL,
|
||||
provider_ignore: tuple[str, ...] | None = None,
|
||||
opus_agent_api_key: str,
|
||||
opus_agent_base_url: str = "https://opus-agent.tielogic.xyz",
|
||||
model_tier_s: str = "claude-opus-4-7",
|
||||
model_tier_a: str = "claude-opus-4-7",
|
||||
model_tier_b: str = "claude-sonnet-4-6",
|
||||
model_tier_c: str = "claude-sonnet-4-6",
|
||||
model_tier_d: str = "claude-haiku-4-5-20251001",
|
||||
poll_interval: float = 3.0,
|
||||
poll_timeout: float = 180.0,
|
||||
) -> None:
|
||||
self.model_tier_s = model_tier_s
|
||||
self.model_tier_a = model_tier_a
|
||||
self.model_tier_b = model_tier_b
|
||||
self.model_tier_c = model_tier_c
|
||||
self.model_tier_d = model_tier_d
|
||||
self.openrouter_base_url = openrouter_base_url
|
||||
self._provider_ignore = (
|
||||
provider_ignore if provider_ignore is not None else self.DEFAULT_PROVIDER_IGNORE
|
||||
)
|
||||
self._tier_models: dict[ModelTier, str] = {
|
||||
self._base_url = opus_agent_base_url.rstrip("/")
|
||||
self._api_key = opus_agent_api_key
|
||||
self._tier_map: dict[ModelTier, str] = {
|
||||
ModelTier.S: model_tier_s,
|
||||
ModelTier.A: model_tier_a,
|
||||
ModelTier.B: model_tier_b,
|
||||
ModelTier.C: model_tier_c,
|
||||
ModelTier.D: model_tier_d,
|
||||
}
|
||||
# Timeout esplicito (60s) per prevenire hang infinito su connessioni
|
||||
# stallate. Tenacity retry su APITimeoutError gestisce il recovery.
|
||||
self._client = OpenAI(
|
||||
api_key=openrouter_api_key,
|
||||
base_url=openrouter_base_url,
|
||||
timeout=60.0,
|
||||
self._poll_interval = poll_interval
|
||||
self._poll_timeout = poll_timeout
|
||||
self._topic_cache: dict[str, str] = {}
|
||||
self._topic_lock = threading.Lock()
|
||||
self._client = httpx.Client(
|
||||
base_url=self._base_url,
|
||||
headers={"X-Api-Key": self._api_key, "Content-Type": "application/json"},
|
||||
timeout=30.0,
|
||||
)
|
||||
|
||||
def _get_or_create_topic(self, system_prompt: str) -> str:
|
||||
prompt_hash = hashlib.sha256(system_prompt.encode()).hexdigest()[:16]
|
||||
if prompt_hash in self._topic_cache:
|
||||
return self._topic_cache[prompt_hash]
|
||||
|
||||
with self._topic_lock:
|
||||
if prompt_hash in self._topic_cache:
|
||||
return self._topic_cache[prompt_hash]
|
||||
|
||||
topic_name = f"swarm-{prompt_hash}"
|
||||
resp = self._client.post("/api/topics", json={
|
||||
"name": topic_name,
|
||||
"system_prompt": system_prompt,
|
||||
})
|
||||
|
||||
if resp.status_code == 409:
|
||||
list_resp = self._client.get("/api/topics")
|
||||
list_resp.raise_for_status()
|
||||
for topic in list_resp.json()["data"]:
|
||||
if topic["name"] == topic_name:
|
||||
self._topic_cache[prompt_hash] = topic["id"]
|
||||
return topic["id"]
|
||||
raise OpusAgentError(f"Topic {topic_name} conflict but not found")
|
||||
|
||||
if resp.status_code >= 500:
|
||||
raise OpusAgentTransientError(f"Server error {resp.status_code}")
|
||||
resp.raise_for_status()
|
||||
|
||||
topic_id = resp.json()["data"]["id"]
|
||||
self._topic_cache[prompt_hash] = topic_id
|
||||
logger.debug("Created topic %s -> %s", topic_name, topic_id)
|
||||
return topic_id
|
||||
|
||||
def _poll_result(self, request_id: str) -> dict:
|
||||
deadline = time.monotonic() + self._poll_timeout
|
||||
while time.monotonic() < deadline:
|
||||
resp = self._client.get(f"/api/requests/{request_id}")
|
||||
resp.raise_for_status()
|
||||
data = resp.json()["data"]
|
||||
status = data["status"]
|
||||
if status == "completed":
|
||||
return data
|
||||
if status == "failed":
|
||||
error = data.get("error") or "unknown error"
|
||||
raise OpusAgentError(f"Request {request_id} failed: {error}")
|
||||
time.sleep(self._poll_interval)
|
||||
raise OpusAgentTransientError(
|
||||
f"Request {request_id} timed out after {self._poll_timeout}s"
|
||||
)
|
||||
|
||||
def close_session(self, session_id: str) -> None:
|
||||
try:
|
||||
self._client.delete(f"/api/sessions/{session_id}")
|
||||
except httpx.HTTPError:
|
||||
logger.debug("Failed to close session %s", session_id)
|
||||
|
||||
@retry(
|
||||
stop=stop_after_attempt(5),
|
||||
stop=stop_after_attempt(3),
|
||||
wait=wait_exponential(multiplier=2.0, min=2.0, max=30.0),
|
||||
retry=retry_if_exception_type(_RETRYABLE_EXCEPTIONS),
|
||||
reraise=True,
|
||||
@@ -99,29 +157,45 @@ class LLMClient:
|
||||
system: str,
|
||||
user: str,
|
||||
max_tokens: int = 2000,
|
||||
session_id: str | None = None,
|
||||
summarize: bool = False,
|
||||
) -> CompletionResult:
|
||||
model = self._tier_models[genome.model_tier]
|
||||
extra_body: dict[str, Any] = {}
|
||||
if self._provider_ignore:
|
||||
extra_body["provider"] = {"ignore": list(self._provider_ignore)}
|
||||
resp = self._client.chat.completions.create(
|
||||
model=model,
|
||||
messages=[
|
||||
{"role": "system", "content": system},
|
||||
{"role": "user", "content": user},
|
||||
],
|
||||
temperature=genome.temperature,
|
||||
top_p=genome.top_p,
|
||||
max_tokens=max_tokens,
|
||||
extra_body=extra_body or None,
|
||||
)
|
||||
if not resp.choices or resp.choices[0].message.content is None:
|
||||
raise EmptyCompletionError(f"empty response from {model}")
|
||||
usage = resp.usage
|
||||
model = self._tier_map[genome.model_tier]
|
||||
topic_id = self._get_or_create_topic(system)
|
||||
|
||||
prompt = f"[SYSTEM]\n{system}\n\n[USER]\n{user}" if session_id in (None, "new") else user
|
||||
|
||||
body: dict = {
|
||||
"topic_id": topic_id,
|
||||
"prompt": prompt,
|
||||
"model": model,
|
||||
}
|
||||
if session_id is not None:
|
||||
body["session_id"] = session_id
|
||||
if summarize:
|
||||
body["summarize"] = True
|
||||
|
||||
resp = self._client.post("/api/requests", json=body)
|
||||
|
||||
if resp.status_code == 429:
|
||||
raise OpusAgentTransientError("Rate limited")
|
||||
if resp.status_code >= 500:
|
||||
raise OpusAgentTransientError(f"Server error {resp.status_code}")
|
||||
if resp.status_code != 202:
|
||||
raise OpusAgentError(f"Unexpected status {resp.status_code}: {resp.text}")
|
||||
|
||||
request_id = resp.json()["data"]["id"]
|
||||
result = self._poll_result(request_id)
|
||||
|
||||
text = result.get("result") or ""
|
||||
if not text:
|
||||
raise EmptyCompletionError(f"empty response from OpusAgent ({model})")
|
||||
|
||||
return CompletionResult(
|
||||
text=resp.choices[0].message.content,
|
||||
input_tokens=usage.prompt_tokens if usage else 0,
|
||||
output_tokens=usage.completion_tokens if usage else 0,
|
||||
text=text,
|
||||
input_tokens=0,
|
||||
output_tokens=0,
|
||||
tier=genome.model_tier,
|
||||
model=model,
|
||||
session_id=result.get("session_id"),
|
||||
)
|
||||
|
||||
@@ -13,6 +13,7 @@ possa leggere lo stato a run terminato (o in corso).
|
||||
from __future__ import annotations
|
||||
|
||||
import random
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
|
||||
@@ -20,13 +21,13 @@ import pandas as pd # type: ignore[import-untyped]
|
||||
|
||||
from ..agents.adversarial import AdversarialAgent
|
||||
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 ..ga.fitness import compute_fitness
|
||||
from ..ga.initial import build_initial_population
|
||||
from ..ga.loop import GAConfig, next_generation
|
||||
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
|
||||
@@ -73,6 +74,29 @@ class RunConfig:
|
||||
# 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). OpusAgent processa in coda FIFO; concurrency > 1
|
||||
# accoda piu' richieste in parallelo ma il throughput dipende dal server.
|
||||
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``). LLMClient (OpusAgent)
|
||||
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(
|
||||
@@ -88,10 +112,16 @@ def run_phase1(
|
||||
|
||||
repo = Repository(cfg.db_path)
|
||||
repo.init_schema()
|
||||
# Escludi prompt_library (PromptLibrary dataclass non e' JSON-serializable);
|
||||
# salva solo i nomi degli stili per reproducibility.
|
||||
config_dict = {
|
||||
**cfg.__dict__,
|
||||
**{k: v for k, v in cfg.__dict__.items() if k != "prompt_library"},
|
||||
"db_path": str(cfg.db_path),
|
||||
"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)
|
||||
|
||||
@@ -142,11 +172,20 @@ def run_phase1(
|
||||
|
||||
try:
|
||||
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:
|
||||
if genome.id in fitnesses:
|
||||
continue # elite gia' valutata in generazione precedente
|
||||
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).
|
||||
for completion in proposal.completions:
|
||||
cost_record = cost_tracker.record(
|
||||
@@ -220,7 +259,7 @@ def run_phase1(
|
||||
cfg.fitness_combined_alpha * fit
|
||||
+ (1.0 - cfg.fitness_combined_alpha) * fit_oos_inloop
|
||||
)
|
||||
except Exception: # noqa: BLE001
|
||||
except Exception:
|
||||
pass # fallback: usa solo IS
|
||||
repo.save_evaluation(
|
||||
run_id=run_id,
|
||||
@@ -261,7 +300,7 @@ def run_phase1(
|
||||
# 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.
|
||||
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)
|
||||
top_evals = sorted(
|
||||
@@ -276,7 +315,7 @@ def run_phase1(
|
||||
try:
|
||||
fals_oos = falsification_agent.evaluate(strategy, test_ohlcv)
|
||||
adv_oos = adversarial_agent.review(strategy, test_ohlcv)
|
||||
except Exception: # noqa: BLE001
|
||||
except Exception:
|
||||
continue
|
||||
fit_oos = compute_fitness(
|
||||
fals_oos, adv_oos,
|
||||
|
||||
@@ -25,6 +25,12 @@ from typing import Any
|
||||
import numpy as np
|
||||
import pandas as pd # type: ignore[import-untyped]
|
||||
|
||||
from strategy_pythagoras.indicators import (
|
||||
candle_pattern as _public_candle_pattern,
|
||||
fractal_mirror as _public_fractal_mirror,
|
||||
pythagorean_ratio as _public_pythagorean_ratio,
|
||||
)
|
||||
|
||||
from ..backtest.orders import Side
|
||||
from .parser import (
|
||||
FeatureNode,
|
||||
@@ -126,6 +132,22 @@ def _ind_macd(
|
||||
return macd_line - signal_line
|
||||
|
||||
|
||||
def _ind_candle_pattern(df: pd.DataFrame, *params: float) -> pd.Series:
|
||||
# Adapter: il dispatch in _eval_node fa ``fn(df, *node.params)``, ma la
|
||||
# public API in strategy_pythagoras.indicators accetta ``params: list[float]``
|
||||
# come singolo argomento. Re-pack qui per mantenere indicators.py testabile
|
||||
# in isolamento.
|
||||
return _public_candle_pattern(df, list(params))
|
||||
|
||||
|
||||
def _ind_pythagorean_ratio(df: pd.DataFrame, lookback: float) -> pd.Series:
|
||||
return _public_pythagorean_ratio(df, [lookback])
|
||||
|
||||
|
||||
def _ind_fractal_mirror(df: pd.DataFrame, k: float, axis_int: float) -> pd.Series:
|
||||
return _public_fractal_mirror(df, [k, axis_int])
|
||||
|
||||
|
||||
# Annotated as ``dict[str, Any]`` deliberately: each indicator has its own
|
||||
# arity and parameter names, so a single ``Callable`` signature would be a
|
||||
# lie. Dispatch happens in :func:`_eval_node`, which validates the verb name
|
||||
@@ -139,6 +161,9 @@ INDICATOR_FNS: dict[str, Any] = {
|
||||
"realized_vol": _ind_realized_vol,
|
||||
"macd": _ind_macd,
|
||||
"macd_pct": _ind_macd_pct,
|
||||
"candle_pattern": _ind_candle_pattern,
|
||||
"pythagorean_ratio": _ind_pythagorean_ratio,
|
||||
"fractal_mirror": _ind_fractal_mirror,
|
||||
}
|
||||
|
||||
_TIME_FEATURE_FNS: dict[str, Callable[[pd.DatetimeIndex], pd.Series]] = {
|
||||
|
||||
@@ -17,7 +17,8 @@ ACTION_VALUES: frozenset[str] = frozenset(
|
||||
KIND_VALUES: frozenset[str] = frozenset({"indicator", "feature", "literal"})
|
||||
|
||||
KNOWN_INDICATORS: frozenset[str] = frozenset(
|
||||
{"sma", "sma_pct", "rsi", "atr", "atr_pct", "macd", "macd_pct", "realized_vol"}
|
||||
{"sma", "sma_pct", "rsi", "atr", "atr_pct", "macd", "macd_pct", "realized_vol",
|
||||
"candle_pattern", "pythagorean_ratio", "fractal_mirror"}
|
||||
)
|
||||
KNOWN_FEATURES: frozenset[str] = frozenset(
|
||||
{"open", "high", "low", "close", "volume",
|
||||
|
||||
@@ -39,6 +39,10 @@ INDICATOR_ARITY: dict[str, tuple[int, int]] = {
|
||||
"realized_vol": (1, 1), # window
|
||||
"macd": (0, 3), # fast, slow, signal (tutti opzionali)
|
||||
"macd_pct": (0, 3), # macd/close, frazionale (per confronti con literal)
|
||||
# Pythagoras indicators (params encoded as floats)
|
||||
"candle_pattern": (4, 13), # [length, sym0, ..., sym_{length-1}]
|
||||
"pythagorean_ratio": (1, 1), # lookback in [12,200]
|
||||
"fractal_mirror": (2, 2), # k in [3,12], axis_int in {0=h,1=v}
|
||||
}
|
||||
|
||||
|
||||
@@ -110,3 +114,37 @@ def _validate_indicator(node: IndicatorNode) -> None:
|
||||
raise ValidationError(
|
||||
f"indicator '{node.name}' arity {n_params} out of [{min_p},{max_p}]"
|
||||
)
|
||||
|
||||
# Pythagoras-specific param semantics
|
||||
name = node.name
|
||||
if name == "candle_pattern":
|
||||
length = int(node.params[0])
|
||||
if not (3 <= length <= 12):
|
||||
raise ValidationError(
|
||||
f"candle_pattern length must be in [3,12], got {length}"
|
||||
)
|
||||
if n_params != 1 + length:
|
||||
raise ValidationError(
|
||||
f"candle_pattern: expected 1+length={1 + length} params, got {n_params}"
|
||||
)
|
||||
for i, sym in enumerate(node.params[1:], start=1):
|
||||
sym_int = int(sym)
|
||||
if sym_int not in (0, 1, 2):
|
||||
raise ValidationError(
|
||||
f"candle_pattern sym[{i - 1}] must be 0(U)/1(D)/2(doji), got {sym}"
|
||||
)
|
||||
elif name == "pythagorean_ratio":
|
||||
lookback = int(node.params[0])
|
||||
if not (12 <= lookback <= 200):
|
||||
raise ValidationError(
|
||||
f"pythagorean_ratio lookback in [12,200], got {lookback}"
|
||||
)
|
||||
elif name == "fractal_mirror":
|
||||
k = int(node.params[0])
|
||||
if not (3 <= k <= 12):
|
||||
raise ValidationError(f"fractal_mirror k must be in [3,12], got {k}")
|
||||
axis_int = int(node.params[1])
|
||||
if axis_int not in (0, 1):
|
||||
raise ValidationError(
|
||||
f"fractal_mirror axis must be 0(h)/1(v), got {axis_int}"
|
||||
)
|
||||
|
||||
@@ -108,7 +108,13 @@ def test_e2e_wfa_populates_fitness_oos(
|
||||
fake_llm,
|
||||
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(
|
||||
run_name="e2e-wfa-test",
|
||||
population_size=5,
|
||||
@@ -125,6 +131,7 @@ def test_e2e_wfa_populates_fitness_oos(
|
||||
db_path=tmp_path / "runs.db",
|
||||
wfa_train_split=0.7,
|
||||
wfa_top_k=3,
|
||||
fitness_hard_kill_findings=("no_trades",),
|
||||
)
|
||||
run_id = run_phase1(cfg, ohlcv=synthetic_ohlcv, llm=fake_llm)
|
||||
repo = Repository(db_path=tmp_path / "runs.db")
|
||||
|
||||
@@ -3,7 +3,6 @@ import json
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import pytest
|
||||
|
||||
from multi_swarm_core.agents.adversarial import (
|
||||
AdversarialAgent,
|
||||
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)
|
||||
|
||||
|
||||
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(
|
||||
{
|
||||
"rules": [
|
||||
@@ -84,8 +86,59 @@ def test_no_findings_on_reasonable_strategy(ohlcv: pd.DataFrame) -> None:
|
||||
ast = parse_strategy(src)
|
||||
agent = AdversarialAgent()
|
||||
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]
|
||||
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:
|
||||
@@ -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,
|
||||
ohlcv: pd.DataFrame) -> None:
|
||||
"""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()
|
||||
@@ -18,13 +18,14 @@ def test_settings_loads_from_env(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setenv("CERBERO_TESTNET_TOKEN", "tok-test")
|
||||
monkeypatch.setenv("CERBERO_MAINNET_TOKEN", "tok-main")
|
||||
monkeypatch.setenv("CERBERO_BOT_TAG", "swarm-poc-phase1")
|
||||
monkeypatch.setenv("OPENROUTER_API_KEY", "or-key")
|
||||
monkeypatch.setenv("OPUS_AGENT_API_KEY", "oa-key")
|
||||
monkeypatch.setenv("RUN_NAME", "test-run")
|
||||
|
||||
s = Settings() # type: ignore[call-arg]
|
||||
|
||||
assert s.cerbero_base_url == "http://test:9000"
|
||||
assert s.cerbero_testnet_token.get_secret_value() == "tok-test"
|
||||
assert s.opus_agent_api_key.get_secret_value() == "oa-key"
|
||||
assert s.run_name == "test-run"
|
||||
assert s.data_dir.name == "data"
|
||||
assert s.db_path.name == "runs.db"
|
||||
@@ -32,50 +33,33 @@ def test_settings_loads_from_env(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
|
||||
def test_settings_requires_tokens(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.delenv("CERBERO_TESTNET_TOKEN", raising=False)
|
||||
monkeypatch.delenv("OPENROUTER_API_KEY", raising=False)
|
||||
monkeypatch.delenv("OPUS_AGENT_API_KEY", raising=False)
|
||||
from pydantic import ValidationError
|
||||
|
||||
with pytest.raises(ValidationError):
|
||||
# Disable .env loading to keep the test deterministic regardless of
|
||||
# whether a developer's local .env exists and is populated.
|
||||
Settings(_env_file=None) # type: ignore[call-arg]
|
||||
|
||||
|
||||
def test_settings_loads_llm_model_overrides(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
def test_settings_opus_agent_defaults(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setenv("CERBERO_TESTNET_TOKEN", "tok-test")
|
||||
monkeypatch.setenv("OPENROUTER_API_KEY", "or-key")
|
||||
monkeypatch.setenv("LLM_MODEL_TIER_S", "claude-mega-x")
|
||||
monkeypatch.setenv("LLM_MODEL_TIER_A", "claude-premium-y")
|
||||
monkeypatch.setenv("LLM_MODEL_TIER_B", "claude-opus-4-7")
|
||||
monkeypatch.setenv("LLM_MODEL_TIER_C", "deepseek/deepseek-chat")
|
||||
monkeypatch.setenv("LLM_MODEL_TIER_D", "mistralai/mistral-7b")
|
||||
monkeypatch.setenv("OPENROUTER_BASE_URL", "https://example.com/api/v1")
|
||||
monkeypatch.setenv("OPUS_AGENT_API_KEY", "oa-key")
|
||||
monkeypatch.delenv("OPUS_AGENT_BASE_URL", raising=False)
|
||||
|
||||
s = Settings(_env_file=None) # type: ignore[call-arg]
|
||||
|
||||
assert s.llm_model_tier_s == "claude-mega-x"
|
||||
assert s.llm_model_tier_a == "claude-premium-y"
|
||||
assert s.llm_model_tier_b == "claude-opus-4-7"
|
||||
assert s.llm_model_tier_c == "deepseek/deepseek-chat"
|
||||
assert s.llm_model_tier_d == "mistralai/mistral-7b"
|
||||
assert s.openrouter_base_url == "https://example.com/api/v1"
|
||||
assert s.opus_agent_base_url == "https://opus-agent.tielogic.xyz"
|
||||
assert s.llm_model_tier_s == "claude-opus-4-7"
|
||||
assert s.llm_model_tier_a == "claude-opus-4-7"
|
||||
assert s.llm_model_tier_b == "claude-sonnet-4-6"
|
||||
assert s.llm_model_tier_c == "claude-sonnet-4-6"
|
||||
assert s.llm_model_tier_d == "claude-haiku-4-5-20251001"
|
||||
|
||||
|
||||
def test_settings_llm_model_defaults(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
def test_settings_opus_agent_base_url_override(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setenv("CERBERO_TESTNET_TOKEN", "tok-test")
|
||||
monkeypatch.setenv("OPENROUTER_API_KEY", "or-key")
|
||||
monkeypatch.delenv("LLM_MODEL_TIER_S", raising=False)
|
||||
monkeypatch.delenv("LLM_MODEL_TIER_A", raising=False)
|
||||
monkeypatch.delenv("LLM_MODEL_TIER_B", raising=False)
|
||||
monkeypatch.delenv("LLM_MODEL_TIER_C", raising=False)
|
||||
monkeypatch.delenv("LLM_MODEL_TIER_D", raising=False)
|
||||
monkeypatch.delenv("OPENROUTER_BASE_URL", raising=False)
|
||||
monkeypatch.setenv("OPUS_AGENT_API_KEY", "oa-key")
|
||||
monkeypatch.setenv("OPUS_AGENT_BASE_URL", "https://custom.example.com")
|
||||
|
||||
s = Settings(_env_file=None) # type: ignore[call-arg]
|
||||
|
||||
assert s.llm_model_tier_s == "google/gemini-3-flash-preview"
|
||||
assert s.llm_model_tier_a == "deepseek/deepseek-v4-flash"
|
||||
assert s.llm_model_tier_b == "deepseek/deepseek-v4-flash"
|
||||
assert s.llm_model_tier_c == "qwen/qwen-2.5-72b-instruct"
|
||||
assert s.llm_model_tier_d == "openai/gpt-oss-20b"
|
||||
assert s.openrouter_base_url == "https://openrouter.ai/api/v1"
|
||||
assert s.opus_agent_base_url == "https://custom.example.com"
|
||||
|
||||
@@ -420,3 +420,48 @@ def test_build_system_prompt_skips_anti_patterns_and_priorities_when_empty(mocke
|
||||
system_msg = call_kwargs["system"]
|
||||
assert "ANTI-PATTERN" not in system_msg
|
||||
assert "PRIORITA' DI OUTPUT" not in system_msg
|
||||
|
||||
|
||||
def test_build_system_prompt_includes_custom_indicators_spec(mocker): # type: ignore[no-untyped-def]
|
||||
"""custom_indicators_spec da PromptLibrary appare nel SYSTEM prompt con header."""
|
||||
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={},
|
||||
custom_indicators_spec="CUSTOM_IND_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 "INDICATORI CUSTOM" in system_msg
|
||||
assert "CUSTOM_IND_X" in system_msg
|
||||
|
||||
|
||||
def test_build_system_prompt_skips_custom_indicators_spec_when_empty(mocker): # type: ignore[no-untyped-def]
|
||||
"""custom_indicators_spec='' -> sezione assente 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={},
|
||||
custom_indicators_spec="",
|
||||
)
|
||||
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 "INDICATORI CUSTOM" not in system_msg
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
import httpx
|
||||
import pytest
|
||||
|
||||
from multi_swarm_core.genome.hypothesis import HypothesisAgentGenome, ModelTier
|
||||
from multi_swarm_core.llm.client import CompletionResult, LLMClient
|
||||
from multi_swarm_core.llm.client import (
|
||||
CompletionResult,
|
||||
EmptyCompletionError,
|
||||
LLMClient,
|
||||
OpusAgentError,
|
||||
OpusAgentTransientError,
|
||||
)
|
||||
|
||||
|
||||
def make_genome(tier: ModelTier) -> HypothesisAgentGenome:
|
||||
@@ -16,217 +23,248 @@ def make_genome(tier: ModelTier) -> HypothesisAgentGenome:
|
||||
)
|
||||
|
||||
|
||||
def test_completion_tier_c_uses_openrouter(mocker):
|
||||
fake_openai = mocker.MagicMock()
|
||||
fake_response = mocker.MagicMock()
|
||||
fake_response.choices = [mocker.MagicMock(message=mocker.MagicMock(content="(strategy ...)"))]
|
||||
fake_response.usage = mocker.MagicMock(prompt_tokens=100, completion_tokens=200)
|
||||
fake_openai.chat.completions.create.return_value = fake_response
|
||||
TOPIC_RESPONSE = {
|
||||
"success": True,
|
||||
"data": {"id": "topic-123", "name": "swarm-test", "system_prompt": "sys"},
|
||||
}
|
||||
|
||||
mocker.patch("multi_swarm_core.llm.client.OpenAI", return_value=fake_openai)
|
||||
REQUEST_ACCEPTED = {
|
||||
"success": True,
|
||||
"data": {"id": "req-456", "session_id": None, "status": "pending"},
|
||||
}
|
||||
|
||||
client = LLMClient(openrouter_api_key="or-x")
|
||||
g = make_genome(ModelTier.C)
|
||||
out = client.complete(g, system="sys", user="usr")
|
||||
|
||||
def _completed_response(
|
||||
text: str = "(strategy ...)", session_id: str = "sess-789",
|
||||
) -> dict:
|
||||
return {
|
||||
"success": True,
|
||||
"data": {
|
||||
"id": "req-456",
|
||||
"status": "completed",
|
||||
"result": text,
|
||||
"session_id": session_id,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _mock_transport(responses: list[httpx.Response]) -> httpx.MockTransport:
|
||||
call_idx = {"i": 0}
|
||||
|
||||
def handler(request: httpx.Request) -> httpx.Response:
|
||||
idx = call_idx["i"]
|
||||
call_idx["i"] += 1
|
||||
if idx < len(responses):
|
||||
return responses[idx]
|
||||
return responses[-1]
|
||||
|
||||
return httpx.MockTransport(handler)
|
||||
|
||||
|
||||
def _make_client(transport: httpx.MockTransport) -> LLMClient:
|
||||
client = LLMClient(opus_agent_api_key="test-key", poll_interval=0.01, poll_timeout=5.0)
|
||||
client._client = httpx.Client(
|
||||
base_url="https://opus-agent.tielogic.xyz",
|
||||
headers={"X-Api-Key": "test-key", "Content-Type": "application/json"},
|
||||
transport=transport,
|
||||
)
|
||||
return client
|
||||
|
||||
|
||||
def test_complete_tier_c_uses_sonnet():
|
||||
transport = _mock_transport([
|
||||
httpx.Response(201, json=TOPIC_RESPONSE),
|
||||
httpx.Response(202, json=REQUEST_ACCEPTED),
|
||||
httpx.Response(200, json=_completed_response()),
|
||||
])
|
||||
client = _make_client(transport)
|
||||
out = client.complete(make_genome(ModelTier.C), system="sys", user="usr")
|
||||
|
||||
assert isinstance(out, CompletionResult)
|
||||
assert out.text == "(strategy ...)"
|
||||
assert out.input_tokens == 100
|
||||
assert out.output_tokens == 200
|
||||
assert out.model == "claude-sonnet-4-6"
|
||||
assert out.tier == ModelTier.C
|
||||
fake_openai.chat.completions.create.assert_called_once()
|
||||
|
||||
|
||||
def test_completion_tier_b_uses_openrouter_with_anthropic_model(mocker):
|
||||
fake_openai = mocker.MagicMock()
|
||||
fake_response = mocker.MagicMock()
|
||||
fake_response.choices = [mocker.MagicMock(message=mocker.MagicMock(content="(strategy ...)"))]
|
||||
fake_response.usage = mocker.MagicMock(prompt_tokens=80, completion_tokens=150)
|
||||
fake_openai.chat.completions.create.return_value = fake_response
|
||||
mocker.patch("multi_swarm_core.llm.client.OpenAI", return_value=fake_openai)
|
||||
def test_complete_tier_s_uses_opus():
|
||||
transport = _mock_transport([
|
||||
httpx.Response(201, json=TOPIC_RESPONSE),
|
||||
httpx.Response(202, json=REQUEST_ACCEPTED),
|
||||
httpx.Response(200, json=_completed_response("(strategy s)")),
|
||||
])
|
||||
client = _make_client(transport)
|
||||
out = client.complete(make_genome(ModelTier.S), system="sys", user="usr")
|
||||
|
||||
client = LLMClient(openrouter_api_key="or-x")
|
||||
g = make_genome(ModelTier.B)
|
||||
out = client.complete(g, system="sys", user="usr")
|
||||
|
||||
assert out.text == "(strategy ...)"
|
||||
assert out.input_tokens == 80
|
||||
assert out.output_tokens == 150
|
||||
assert out.tier == ModelTier.B
|
||||
call_kwargs = fake_openai.chat.completions.create.call_args.kwargs
|
||||
assert call_kwargs["model"] == "deepseek/deepseek-v4-flash"
|
||||
assert out.model == "deepseek/deepseek-v4-flash"
|
||||
|
||||
|
||||
@pytest.mark.slow
|
||||
def test_completion_retries_on_connection_error(mocker):
|
||||
"""Retry esegue 3 tentativi su APIConnectionError, poi rilancia."""
|
||||
import openai
|
||||
|
||||
fake_openai = mocker.MagicMock()
|
||||
fake_openai.chat.completions.create.side_effect = openai.APIConnectionError(
|
||||
request=mocker.MagicMock()
|
||||
)
|
||||
mocker.patch("multi_swarm_core.llm.client.OpenAI", return_value=fake_openai)
|
||||
|
||||
client = LLMClient(openrouter_api_key="or-x")
|
||||
g = make_genome(ModelTier.C)
|
||||
|
||||
with pytest.raises(openai.APIConnectionError):
|
||||
client.complete(g, system="sys", user="usr")
|
||||
|
||||
assert fake_openai.chat.completions.create.call_count == 5
|
||||
|
||||
|
||||
def test_completion_uses_custom_model_tier_c(mocker):
|
||||
fake_openai = mocker.MagicMock()
|
||||
fake_response = mocker.MagicMock()
|
||||
fake_response.choices = [
|
||||
mocker.MagicMock(message=mocker.MagicMock(content="(strategy ...)"))
|
||||
]
|
||||
fake_response.usage = mocker.MagicMock(prompt_tokens=10, completion_tokens=20)
|
||||
fake_openai.chat.completions.create.return_value = fake_response
|
||||
mocker.patch("multi_swarm_core.llm.client.OpenAI", return_value=fake_openai)
|
||||
|
||||
client = LLMClient(
|
||||
openrouter_api_key="or-x",
|
||||
model_tier_c="deepseek/deepseek-chat",
|
||||
)
|
||||
g = make_genome(ModelTier.C)
|
||||
out = client.complete(g, system="sys", user="usr")
|
||||
|
||||
fake_openai.chat.completions.create.assert_called_once()
|
||||
call_kwargs = fake_openai.chat.completions.create.call_args.kwargs
|
||||
assert call_kwargs["model"] == "deepseek/deepseek-chat"
|
||||
assert out.model == "deepseek/deepseek-chat"
|
||||
|
||||
|
||||
def test_completion_uses_custom_model_tier_b(mocker):
|
||||
fake_openai = mocker.MagicMock()
|
||||
fake_response = mocker.MagicMock()
|
||||
fake_response.choices = [
|
||||
mocker.MagicMock(message=mocker.MagicMock(content="(strategy ...)"))
|
||||
]
|
||||
fake_response.usage = mocker.MagicMock(prompt_tokens=10, completion_tokens=20)
|
||||
fake_openai.chat.completions.create.return_value = fake_response
|
||||
mocker.patch("multi_swarm_core.llm.client.OpenAI", return_value=fake_openai)
|
||||
|
||||
client = LLMClient(
|
||||
openrouter_api_key="or-x",
|
||||
model_tier_b="anthropic/claude-opus-4-7",
|
||||
)
|
||||
g = make_genome(ModelTier.B)
|
||||
out = client.complete(g, system="sys", user="usr")
|
||||
|
||||
fake_openai.chat.completions.create.assert_called_once()
|
||||
call_kwargs = fake_openai.chat.completions.create.call_args.kwargs
|
||||
assert call_kwargs["model"] == "anthropic/claude-opus-4-7"
|
||||
assert out.model == "anthropic/claude-opus-4-7"
|
||||
|
||||
|
||||
def test_completion_tier_s_uses_openrouter_with_anthropic_model(mocker):
|
||||
fake_openai = mocker.MagicMock()
|
||||
fake_response = mocker.MagicMock()
|
||||
fake_response.choices = [mocker.MagicMock(message=mocker.MagicMock(content="(strategy s)"))]
|
||||
fake_response.usage = mocker.MagicMock(prompt_tokens=50, completion_tokens=100)
|
||||
fake_openai.chat.completions.create.return_value = fake_response
|
||||
mocker.patch("multi_swarm_core.llm.client.OpenAI", return_value=fake_openai)
|
||||
|
||||
client = LLMClient(openrouter_api_key="or-x")
|
||||
g = make_genome(ModelTier.S)
|
||||
out = client.complete(g, system="sys", user="usr")
|
||||
|
||||
fake_openai.chat.completions.create.assert_called_once()
|
||||
call_kwargs = fake_openai.chat.completions.create.call_args.kwargs
|
||||
assert call_kwargs["model"] == "google/gemini-3-flash-preview"
|
||||
assert out.model == "claude-opus-4-7"
|
||||
assert out.tier == ModelTier.S
|
||||
assert out.model == "google/gemini-3-flash-preview"
|
||||
assert out.text == "(strategy s)"
|
||||
|
||||
|
||||
def test_completion_tier_a_uses_openrouter_with_anthropic_model(mocker):
|
||||
fake_openai = mocker.MagicMock()
|
||||
fake_response = mocker.MagicMock()
|
||||
fake_response.choices = [mocker.MagicMock(message=mocker.MagicMock(content="(strategy a)"))]
|
||||
fake_response.usage = mocker.MagicMock(prompt_tokens=40, completion_tokens=80)
|
||||
fake_openai.chat.completions.create.return_value = fake_response
|
||||
mocker.patch("multi_swarm_core.llm.client.OpenAI", return_value=fake_openai)
|
||||
def test_complete_tier_d_uses_haiku():
|
||||
transport = _mock_transport([
|
||||
httpx.Response(201, json=TOPIC_RESPONSE),
|
||||
httpx.Response(202, json=REQUEST_ACCEPTED),
|
||||
httpx.Response(200, json=_completed_response("(strategy d)")),
|
||||
])
|
||||
client = _make_client(transport)
|
||||
out = client.complete(make_genome(ModelTier.D), system="sys", user="usr")
|
||||
|
||||
client = LLMClient(openrouter_api_key="or-x")
|
||||
g = make_genome(ModelTier.A)
|
||||
out = client.complete(g, system="sys", user="usr")
|
||||
|
||||
fake_openai.chat.completions.create.assert_called_once()
|
||||
call_kwargs = fake_openai.chat.completions.create.call_args.kwargs
|
||||
assert call_kwargs["model"] == "deepseek/deepseek-v4-flash"
|
||||
assert out.tier == ModelTier.A
|
||||
assert out.model == "deepseek/deepseek-v4-flash"
|
||||
|
||||
|
||||
def test_completion_tier_d_uses_openrouter_with_llama(mocker):
|
||||
fake_openai = mocker.MagicMock()
|
||||
fake_response = mocker.MagicMock()
|
||||
fake_response.choices = [
|
||||
mocker.MagicMock(message=mocker.MagicMock(content="(strategy d)"))
|
||||
]
|
||||
fake_response.usage = mocker.MagicMock(prompt_tokens=30, completion_tokens=70)
|
||||
fake_openai.chat.completions.create.return_value = fake_response
|
||||
mocker.patch("multi_swarm_core.llm.client.OpenAI", return_value=fake_openai)
|
||||
|
||||
client = LLMClient(openrouter_api_key="or-x")
|
||||
g = make_genome(ModelTier.D)
|
||||
out = client.complete(g, system="sys", user="usr")
|
||||
|
||||
fake_openai.chat.completions.create.assert_called_once()
|
||||
call_kwargs = fake_openai.chat.completions.create.call_args.kwargs
|
||||
assert call_kwargs["model"] == "openai/gpt-oss-20b"
|
||||
assert out.model == "claude-haiku-4-5-20251001"
|
||||
assert out.tier == ModelTier.D
|
||||
assert out.model == "openai/gpt-oss-20b"
|
||||
|
||||
|
||||
def test_completion_uses_custom_model_tier_s(mocker):
|
||||
fake_openai = mocker.MagicMock()
|
||||
fake_response = mocker.MagicMock()
|
||||
fake_response.choices = [
|
||||
mocker.MagicMock(message=mocker.MagicMock(content="(strategy custom-s)"))
|
||||
]
|
||||
fake_response.usage = mocker.MagicMock(prompt_tokens=10, completion_tokens=20)
|
||||
fake_openai.chat.completions.create.return_value = fake_response
|
||||
mocker.patch("multi_swarm_core.llm.client.OpenAI", return_value=fake_openai)
|
||||
def test_topic_cached_on_second_call():
|
||||
transport = _mock_transport([
|
||||
httpx.Response(201, json=TOPIC_RESPONSE),
|
||||
httpx.Response(202, json=REQUEST_ACCEPTED),
|
||||
httpx.Response(200, json=_completed_response()),
|
||||
httpx.Response(202, json=REQUEST_ACCEPTED),
|
||||
httpx.Response(200, json=_completed_response("second")),
|
||||
])
|
||||
client = _make_client(transport)
|
||||
client.complete(make_genome(ModelTier.C), system="sys", user="usr1")
|
||||
out2 = client.complete(make_genome(ModelTier.C), system="sys", user="usr2")
|
||||
|
||||
client = LLMClient(
|
||||
openrouter_api_key="or-x",
|
||||
model_tier_s="anthropic/claude-future-mega",
|
||||
assert out2.text == "second"
|
||||
|
||||
|
||||
def test_topic_conflict_409_recovers():
|
||||
transport = _mock_transport([
|
||||
httpx.Response(409, json={"success": False, "error": {"code": "CONFLICT"}}),
|
||||
httpx.Response(200, json={"success": True, "data": [
|
||||
{"id": "topic-existing", "name": "swarm-wrong"},
|
||||
]}),
|
||||
])
|
||||
client = _make_client(transport)
|
||||
|
||||
with pytest.raises(OpusAgentError, match="conflict but not found"):
|
||||
client.complete(make_genome(ModelTier.C), system="sys", user="usr")
|
||||
|
||||
|
||||
def test_empty_response_raises():
|
||||
transport = _mock_transport([
|
||||
httpx.Response(201, json=TOPIC_RESPONSE),
|
||||
httpx.Response(202, json=REQUEST_ACCEPTED),
|
||||
httpx.Response(200, json={
|
||||
"success": True,
|
||||
"data": {"id": "req-456", "status": "completed", "result": ""},
|
||||
}),
|
||||
])
|
||||
client = _make_client(transport)
|
||||
|
||||
with pytest.raises(EmptyCompletionError):
|
||||
client.complete(make_genome(ModelTier.C), system="sys", user="usr")
|
||||
|
||||
|
||||
def test_request_failed_raises_opus_agent_error():
|
||||
transport = _mock_transport([
|
||||
httpx.Response(201, json=TOPIC_RESPONSE),
|
||||
httpx.Response(202, json=REQUEST_ACCEPTED),
|
||||
httpx.Response(200, json={
|
||||
"success": True,
|
||||
"data": {"id": "req-456", "status": "failed", "error": "model overloaded"},
|
||||
}),
|
||||
])
|
||||
client = _make_client(transport)
|
||||
|
||||
with pytest.raises(OpusAgentError, match="failed"):
|
||||
client.complete(make_genome(ModelTier.C), system="sys", user="usr")
|
||||
|
||||
|
||||
def test_rate_limit_429_is_retryable():
|
||||
transport = _mock_transport([
|
||||
httpx.Response(201, json=TOPIC_RESPONSE),
|
||||
httpx.Response(429, json={"success": False}),
|
||||
httpx.Response(202, json=REQUEST_ACCEPTED),
|
||||
httpx.Response(200, json=_completed_response("after retry")),
|
||||
])
|
||||
client = _make_client(transport)
|
||||
out = client.complete(make_genome(ModelTier.C), system="sys", user="usr")
|
||||
|
||||
assert out.text == "after retry"
|
||||
|
||||
|
||||
def test_polling_waits_for_completion():
|
||||
transport = _mock_transport([
|
||||
httpx.Response(201, json=TOPIC_RESPONSE),
|
||||
httpx.Response(202, json=REQUEST_ACCEPTED),
|
||||
httpx.Response(200, json={
|
||||
"success": True,
|
||||
"data": {"id": "req-456", "status": "processing"},
|
||||
}),
|
||||
httpx.Response(200, json={
|
||||
"success": True,
|
||||
"data": {"id": "req-456", "status": "processing"},
|
||||
}),
|
||||
httpx.Response(200, json=_completed_response("done")),
|
||||
])
|
||||
client = _make_client(transport)
|
||||
out = client.complete(make_genome(ModelTier.C), system="sys", user="usr")
|
||||
|
||||
assert out.text == "done"
|
||||
|
||||
|
||||
def test_tokens_are_zero():
|
||||
transport = _mock_transport([
|
||||
httpx.Response(201, json=TOPIC_RESPONSE),
|
||||
httpx.Response(202, json=REQUEST_ACCEPTED),
|
||||
httpx.Response(200, json=_completed_response()),
|
||||
])
|
||||
client = _make_client(transport)
|
||||
out = client.complete(make_genome(ModelTier.C), system="sys", user="usr")
|
||||
|
||||
assert out.input_tokens == 0
|
||||
assert out.output_tokens == 0
|
||||
|
||||
|
||||
def test_session_id_returned_from_completion():
|
||||
transport = _mock_transport([
|
||||
httpx.Response(201, json=TOPIC_RESPONSE),
|
||||
httpx.Response(202, json=REQUEST_ACCEPTED),
|
||||
httpx.Response(200, json=_completed_response(session_id="sess-abc")),
|
||||
])
|
||||
client = _make_client(transport)
|
||||
out = client.complete(make_genome(ModelTier.C), system="sys", user="usr", session_id="new")
|
||||
|
||||
assert out.session_id == "sess-abc"
|
||||
|
||||
|
||||
def test_session_id_and_summarize_sent_in_request():
|
||||
requests_seen: list[dict] = []
|
||||
|
||||
def handler(request: httpx.Request) -> httpx.Response:
|
||||
if request.method == "POST" and "/requests" in str(request.url):
|
||||
import json
|
||||
requests_seen.append(json.loads(request.content))
|
||||
return httpx.Response(202, json=REQUEST_ACCEPTED)
|
||||
if request.method == "POST" and "/topics" in str(request.url):
|
||||
return httpx.Response(201, json=TOPIC_RESPONSE)
|
||||
return httpx.Response(200, json=_completed_response())
|
||||
|
||||
transport = httpx.MockTransport(handler)
|
||||
client = _make_client(transport)
|
||||
client.complete(
|
||||
make_genome(ModelTier.C), system="sys", user="usr",
|
||||
session_id="sess-existing", summarize=True,
|
||||
)
|
||||
g = make_genome(ModelTier.S)
|
||||
out = client.complete(g, system="sys", user="usr")
|
||||
|
||||
call_kwargs = fake_openai.chat.completions.create.call_args.kwargs
|
||||
assert call_kwargs["model"] == "anthropic/claude-future-mega"
|
||||
assert out.model == "anthropic/claude-future-mega"
|
||||
assert len(requests_seen) == 1
|
||||
assert requests_seen[0]["session_id"] == "sess-existing"
|
||||
assert requests_seen[0]["summarize"] is True
|
||||
|
||||
|
||||
@pytest.mark.slow
|
||||
def test_completion_succeeds_after_one_retry(mocker):
|
||||
"""Dopo 1 fallimento transient, il retry riesce al 2 tentativo."""
|
||||
import openai
|
||||
def test_close_session():
|
||||
deleted: list[str] = []
|
||||
|
||||
fake_response = mocker.MagicMock()
|
||||
fake_response.choices = [
|
||||
mocker.MagicMock(message=mocker.MagicMock(content="(strategy ...)"))
|
||||
]
|
||||
fake_response.usage = mocker.MagicMock(prompt_tokens=100, completion_tokens=200)
|
||||
def handler(request: httpx.Request) -> httpx.Response:
|
||||
if request.method == "DELETE":
|
||||
deleted.append(str(request.url))
|
||||
return httpx.Response(200, json={"success": True})
|
||||
return httpx.Response(200, json={"success": True})
|
||||
|
||||
fake_openai = mocker.MagicMock()
|
||||
fake_openai.chat.completions.create.side_effect = [
|
||||
openai.APITimeoutError(request=mocker.MagicMock()),
|
||||
fake_response,
|
||||
]
|
||||
mocker.patch("multi_swarm_core.llm.client.OpenAI", return_value=fake_openai)
|
||||
transport = httpx.MockTransport(handler)
|
||||
client = _make_client(transport)
|
||||
client.close_session("sess-to-close")
|
||||
|
||||
client = LLMClient(openrouter_api_key="or-x")
|
||||
g = make_genome(ModelTier.C)
|
||||
out = client.complete(g, system="sys", user="usr")
|
||||
|
||||
assert isinstance(out, CompletionResult)
|
||||
assert out.text == "(strategy ...)"
|
||||
assert fake_openai.chat.completions.create.call_count == 2
|
||||
assert len(deleted) == 1
|
||||
assert "sess-to-close" in deleted[0]
|
||||
|
||||
@@ -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
|
||||
@@ -88,3 +88,81 @@ def test_from_json_loads_anti_patterns_and_output_priorities(tmp_path: Path) ->
|
||||
lib = PromptLibrary.from_json(_write_json(data, tmp_path))
|
||||
assert lib.anti_patterns == "Evita overfitting."
|
||||
assert lib.output_priorities == "Robustezza > ottimalita."
|
||||
|
||||
|
||||
def test_from_json_loads_custom_indicators_spec(tmp_path: Path) -> None:
|
||||
"""from_json() legge custom_indicators_spec (v3.2): firme di indicatori strategy-specific."""
|
||||
data = {
|
||||
"styles": {"physicist": {"directive": "Cerca leggi conservative."}},
|
||||
"custom_indicators_spec": (
|
||||
"candle_pattern: params=[length, sym0, sym1, ...], length in [3,12], "
|
||||
"sym in {0=U,1=D,2=doji}\n"
|
||||
"pythagorean_ratio: params=[lookback], lookback in [12,200]"
|
||||
),
|
||||
}
|
||||
lib = PromptLibrary.from_json(_write_json(data, tmp_path))
|
||||
assert "candle_pattern" in lib.custom_indicators_spec
|
||||
assert "lookback in [12,200]" in lib.custom_indicators_spec
|
||||
|
||||
|
||||
def test_from_json_defaults_custom_indicators_spec_when_absent(tmp_path: Path) -> None:
|
||||
"""custom_indicators_spec assente -> default stringa vuota."""
|
||||
data = {"styles": {"physicist": {"directive": "Cerca leggi conservative."}}}
|
||||
lib = PromptLibrary.from_json(_write_json(data, tmp_path))
|
||||
assert lib.custom_indicators_spec == ""
|
||||
|
||||
|
||||
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'"
|
||||
)
|
||||
|
||||
@@ -7,6 +7,7 @@ from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sqlite3
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
@@ -14,9 +15,18 @@ import pandas as pd # type: ignore[import-untyped]
|
||||
|
||||
|
||||
def _paper_conn(db_path: str | Path) -> sqlite3.Connection:
|
||||
conn = sqlite3.connect(str(db_path))
|
||||
# Cold-start race: GUI può avviarsi prima che il paper writer crei il file.
|
||||
db_path_str = str(db_path)
|
||||
deadline = time.monotonic() + 5.0
|
||||
while True:
|
||||
try:
|
||||
conn = sqlite3.connect(db_path_str, timeout=5.0)
|
||||
conn.row_factory = sqlite3.Row
|
||||
return conn
|
||||
except sqlite3.OperationalError:
|
||||
if time.monotonic() >= deadline:
|
||||
raise
|
||||
time.sleep(1.0)
|
||||
|
||||
|
||||
def paper_runs_df(db_path: str | Path) -> pd.DataFrame:
|
||||
|
||||
@@ -1,54 +1,49 @@
|
||||
{
|
||||
"_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.1",
|
||||
"_changelog": "v3.1 - Refactor contenuto post-diagnosi: pattern_guidance astratto (nessun riferimento a indicatori specifici, lascia il GA scoprire il mapping); domain_warnings riformulato in 'NON assumere' (rimossa inferenza su funding rate); agent_role con swarm awareness; NEW anti_patterns + output_priorities; directive ridotte sotto 900 char; focus_metrics standardizzati a 4 per stile, rimosse ridondanze (autocorr_baseline da historian, kurt/skew da psychologist sostituiti con autocorr_recent + spectral_entropy). v3.0 - Refactor compositore: prompts.json controlla agent_role/pattern_guidance/instruction/domain_warnings top-level; core fornisce solo lo SCAFFOLD universale. v2.2 - Aggiunte 5 metriche geometrico-frattali con focus_metrics per stile. v2.1 - directive estese con interpretazione dei 4 input statistici. v2.0 - Riprogettato per blind-generator GA, 7 lenti.",
|
||||
"_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 (es. autocorr_baseline non va nel focus, e' gia' visibile). Scelte per essere semantically aligned con la metafora dello stile.",
|
||||
"_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.",
|
||||
"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 -> 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.",
|
||||
"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.",
|
||||
"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).",
|
||||
"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) robustezza cross-regime > ottimalita su singolo regime; (2) semplicita interpretabile (2-3 condizioni con razionale chiaro) > complessita raffinata (5+ condizioni accordate); (3) selettivita (poche entry forti, alto SNR) > attivita (molte entry deboli); (4) condizioni con razionale meccanico > pattern statistici fortuiti.",
|
||||
"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 significativamente sopra baseline = memoria coerente, momentum legittimo; Hurst > 0.55 conferma persistenza di scala; vol percentile alto + kurt bassa = energia immagazzinata non ancora rilasciata; efficiency_ratio alto = movimento efficiente; spectral_entropy bassa + dominant_cycle definito = modi armonici sfruttabili. Pattern coerenti su piu scale temporali sono robusti, pattern singoli sono rumore.",
|
||||
"directive": "Il mercato e un sistema fisico con energia (std), simmetrie (skew) e memoria (autocorr). Leggi kurt come densita di eventi estremi (fat tails = fuori equilibrio), skew come forzante asimmetrica. AR(1) positivo molto sopra baseline = memoria coerente, costruisci ipotesi di momentum legittimo; Hurst > 0.55 conferma persistenza di scala su orizzonti multipli; vol_pct alto con kurt bassa = energia immagazzinata non ancora rilasciata, cattura il rilascio; efficiency_ratio elevato = movimento efficiente, sfrutta direzionalita; spectral_entropy bassa con dominant_cycle definito = modi armonici sfruttabili, combina con conferma sulla fase. Preferisci pattern coerenti su piu lookback rispetto a singoli eventi rumorosi. Diagnostica regimi simmetrici e rotture di simmetria. Lookback consigliato: 150-300 barre. Archetipo dominante: sistema fisico in equilibrio (o pre-rottura di simmetria).",
|
||||
"focus_metrics": ["hurst", "dominant_cycle", "efficiency_ratio", "spectral_entropy"]
|
||||
},
|
||||
|
||||
"biologist": {
|
||||
"directive": "Il mercato e un ecosistema dove strategie competono per alpha finito. Skew negativo = predazione asimmetrica (vol-selling crowded che subisce shock), positivo = predatori che cacciano breakout. Kurt alta = eventi di estinzione/fioritura. AR(1) positivo persistente = una specie sta colonizzando la nicchia (overcrowding imminente, fade); Hurst > 0.55 + vol percentile basso = nicchia stabile (occupa); tail asimmetrico (left << right) = predazione asimmetrica strutturale; structural_uptrend persistente = specie dominante stabile. Cattura la coda opposta al consensus.",
|
||||
"directive": "Il mercato e un ecosistema dove strategie competono per alpha finito. Skew negativo segnala predazione asimmetrica (vol-selling crowded subisce shock), positivo predatori che cacciano breakout. Kurt alta = eventi di estinzione o fioritura. AR(1) positivo persistente = una specie sta colonizzando la nicchia (overcrowding imminente, preferisci fade); Hurst > 0.55 con vol_pct basso = nicchia stabile (occupa con strategie direzionali); tail asimmetrico (left molto piu pesante di right) = predazione asimmetrica strutturale, costruisci contrarian sulla coda; structural_uptrend persistente = specie dominante stabile. Combina seasonality con uno o due gate di regime per evitare di sovrapporti a fasi gia mature. Cattura la coda opposta al consensus. Lookback consigliato: 80-200 barre. Archetipo dominante: ecosistema con dinamiche predator-prey e nicchie evolutive.",
|
||||
"focus_metrics": ["tail_left", "tail_right", "structural_uptrend", "seasonality_hour"]
|
||||
},
|
||||
|
||||
"historian": {
|
||||
"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 ≈0 = accumulazione/stabilita. AR(1) recente >> baseline storica = regime accelera rispetto al normale; Hurst > 0.55 + vol percentile alto = fase markup matura, mean reversion attesa; structural_uptrend sostenuto = fase di accumulo/markup; compression < 1 = consolidamento pre-fase nuova. Identifica analogie tra il regime corrente e fasi tipiche.",
|
||||
"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 microclima: std bassa + kurt bassa = calma stabile (vendi vol), std alta + kurt alta = tempesta (compra convexity), std bassa + kurt alta = calma ingannevole (riduci esposizione). AR(1) recente > baseline = fronte persistente in arrivo; Hurst > 0.55 = sistema su scala lunga, Hurst < 0.45 = turbolenza locale; vol percentile estremo = posizione nel ciclo seasonal; compression < 1 = compressione vol (pre-rilascio); tail pesanti = clima instabile. Pattern multi-regime sono robusti.",
|
||||
"directive": "La volatilita ha climi persistenti e fronti di transizione. Vol_regime + std + kurt definiscono il microclima: std bassa + kurt bassa = calma stabile (preferisci vendere vol con gate sicuri); std alta + kurt alta = tempesta (compra convexity o resta flat); std bassa + kurt alta = calma ingannevole pre-fronte (riduci esposizione). AR(1) recente sopra baseline = fronte persistente in arrivo, cattura la direzione del fronte; Hurst > 0.55 = sistema su scala lunga (ciclone), Hurst < 0.45 = turbolenza locale (no trend persistente, preferisci range-trading); vol_pct estremo = posizione nel ciclo seasonal, modula la size; compression < 1 = compressione vol pre-rilascio, posizionati per il breakout. Costruisci strategie con gate espliciti su vol che attivano logiche diverse. Lookback consigliato: 50-150 barre. Archetipo dominante: clima atmosferico con fronti e regimi persistenti.",
|
||||
"focus_metrics": ["vol_pct", "compression", "tail_right", "dominant_cycle"]
|
||||
},
|
||||
|
||||
"engineer": {
|
||||
"directive": "Tratta ogni segnale come un sistema di controllo: SNR favorevole, causalita, robustezza. Std e il rumore di fondo. Kurt alta riduce affidabilita dei segnali medi. AR(1) > 0.05 con std contenuta = SNR favorevole; AR(1) ≈0 = random walk, non costruirci; Hurst < 0.45 = filtro mean-reversion causale efficace; efficiency_ratio < 0.2 = no signal, non costruire; spectral_entropy > 0.8 = white noise, regime non modellabile; tail_index < 2.5 = saturazione, riduci leverage; seasonality < 0.05 = feature temporali sono rumore. Robustezza > ottimalita.",
|
||||
"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 offensivo (entry direzionali). Vol regime high + kurt elevata = terreno ostile (difesa: posizioni limitate). AR(1) > 0 = vento alle spalle, carica con momentum; structural_uptrend > 0.7 = terreno occupato, hold; compression < 0.5 = preparazione attacco, posizionati; vol percentile alta = artiglieria nemica, ritirata; seasonality forte = via predicibile. Concentrazione: poche condizioni forti. Sorpresa: contrarian su consensus estremo.",
|
||||
"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, fade gli estremi ribasso); skew pos + kurt alta = euforia (FOMO, fade rialzo); skew ≈0 + kurt bassa = apatia. AR(1) recente >> baseline = euforia coordinata in corso, posizionati contro l'ultimo arrivato; Hurst > 0.55 = trance collettiva (dura piu del razionale); tail_left pesante (Hill < 2.5) = paura sistemica; spectral_entropy alta = caos comportamentale; vol percentile estremo = momentum emozionale. Contrarian sugli estremi.",
|
||||
"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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -34,8 +34,10 @@ def test_frontend_imports() -> None:
|
||||
def test_strategies_json_loadable() -> None:
|
||||
files = importlib.resources.files("strategy_crypto") / "strategies"
|
||||
found = sorted(p.name for p in files.iterdir() if p.name.endswith(".json"))
|
||||
assert "btc_fb63e851.json" in found
|
||||
assert "eth_facd6af85d5d.json" in found
|
||||
# Convenzione: almeno un winner BTC e uno ETH shippati col package.
|
||||
# Non hard-codare hash specifici: cambiano ad ogni swap di paper winner.
|
||||
assert any(n.startswith("btc_") for n in found), f"no btc_*.json in {found}"
|
||||
assert any(n.startswith("eth_") for n in found), f"no eth_*.json in {found}"
|
||||
for fname in found:
|
||||
data = json.loads((files / fname).read_text())
|
||||
assert "rules" in data, f"{fname} missing 'rules' key"
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
"""Regression guard: end-of-window snap deve seguire il timeframe dell'asset.
|
||||
|
||||
Bug originale (scripts/run_paper_trading.py): ``end = now.replace(minute=0,...)``
|
||||
snappava sempre all'ora; ETH 5m veniva quindi valutato 1 volta ogni 60 min
|
||||
invece di ogni 5 min, riducendo la fedelta' al backtest tunato 5m.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import sys
|
||||
from datetime import UTC, datetime
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
_REPO_ROOT = Path(__file__).resolve().parents[3]
|
||||
_RUNNER_PATH = _REPO_ROOT / "scripts" / "run_paper_trading.py"
|
||||
|
||||
|
||||
def _load_runner_module():
|
||||
spec = importlib.util.spec_from_file_location("run_paper_trading", _RUNNER_PATH)
|
||||
assert spec is not None and spec.loader is not None
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
sys.modules["run_paper_trading"] = module
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def runner():
|
||||
return _load_runner_module()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"now, tf, expected",
|
||||
[
|
||||
# 5m: snap al boundary di 5 min, NON all'ora
|
||||
(datetime(2026, 5, 18, 14, 37, 42, tzinfo=UTC), "5m", datetime(2026, 5, 18, 14, 35, tzinfo=UTC)),
|
||||
(datetime(2026, 5, 18, 14, 35, 0, tzinfo=UTC), "5m", datetime(2026, 5, 18, 14, 35, tzinfo=UTC)),
|
||||
(datetime(2026, 5, 18, 14, 34, 59, tzinfo=UTC), "5m", datetime(2026, 5, 18, 14, 30, tzinfo=UTC)),
|
||||
# 1h: comportamento storico preservato
|
||||
(datetime(2026, 5, 18, 14, 37, 42, tzinfo=UTC), "1h", datetime(2026, 5, 18, 14, 0, tzinfo=UTC)),
|
||||
(datetime(2026, 5, 18, 14, 0, 0, tzinfo=UTC), "1h", datetime(2026, 5, 18, 14, 0, tzinfo=UTC)),
|
||||
# 15m / 4h
|
||||
(datetime(2026, 5, 18, 14, 22, 0, tzinfo=UTC), "15m", datetime(2026, 5, 18, 14, 15, tzinfo=UTC)),
|
||||
(datetime(2026, 5, 18, 14, 22, 0, tzinfo=UTC), "4h", datetime(2026, 5, 18, 12, 0, tzinfo=UTC)),
|
||||
],
|
||||
)
|
||||
def test_align_end_to_timeframe(runner, now, tf, expected) -> None:
|
||||
assert runner._align_end_to_timeframe(now, tf) == expected
|
||||
|
||||
|
||||
def test_align_end_5m_advances_every_5_minutes(runner) -> None:
|
||||
"""Bug-regression: chiamate consecutive a 5 min di distanza devono
|
||||
produrre end DIVERSI per tf=5m (prima del fix erano identici)."""
|
||||
a = datetime(2026, 5, 18, 14, 30, 0, tzinfo=UTC)
|
||||
b = datetime(2026, 5, 18, 14, 35, 0, tzinfo=UTC)
|
||||
c = datetime(2026, 5, 18, 14, 40, 0, tzinfo=UTC)
|
||||
ends = {runner._align_end_to_timeframe(t, "5m") for t in (a, b, c)}
|
||||
assert len(ends) == 3
|
||||
|
||||
|
||||
def test_align_end_1h_stable_within_hour(runner) -> None:
|
||||
"""Per tf=1h, chiamate dentro la stessa ora devono dare lo stesso end."""
|
||||
ends = {
|
||||
runner._align_end_to_timeframe(datetime(2026, 5, 18, 14, m, 0, tzinfo=UTC), "1h")
|
||||
for m in (0, 15, 30, 45, 59)
|
||||
}
|
||||
assert ends == {datetime(2026, 5, 18, 14, 0, tzinfo=UTC)}
|
||||
@@ -0,0 +1,110 @@
|
||||
# Libro dei Frattali — Riassunto
|
||||
|
||||
**Autori:** Luca Serleto, Corrado Malanga
|
||||
**Editore/Stampa:** Adobe InDesign, nov 2024
|
||||
**Pagine:** 58 — **testo embedded ≈ nullo (~1.6 KB)**
|
||||
**Estratto raw:** [_extracted/Libro_frattali.txt](_extracted/Libro_frattali.txt)
|
||||
|
||||
## Natura del documento
|
||||
|
||||
Il "Libro dei Frattali" è un **catalogo grafico** di pattern candlestick, non un testo discorsivo. Le pagine sono composte quasi interamente da figure (candle-chart estratti via trasformata di Fourier secondo la metodologia esposta nel companion paper [Pythagoras Trading Prediction](Pythagoras_Trading_Prediction.summary.md)).
|
||||
|
||||
L'estrazione testuale rivela **solo le etichette**:
|
||||
|
||||
| Pagina | Contenuto testuale |
|
||||
|---|---|
|
||||
| 1 | "Luca Serleto / Corrado Malanga / Libro dei Frattali" (frontespizio) |
|
||||
| 2 | "Pattern da 3 candele a massimo 6 candele (LONG)" |
|
||||
| 3–58 | "Pattern 1", "Pattern 2", …, "Pattern 57" — una riga per pagina |
|
||||
|
||||
**Tutto il resto è immagine.** L'utente ha esplicitamente richiesto che gli agenti **non passino dalle immagini** ma costruiscano schemi dai numeri.
|
||||
|
||||
## Struttura numerica deducibile (senza guardare le figure)
|
||||
|
||||
### Conteggio dei pattern
|
||||
- **Totale pattern catalogati: 57**
|
||||
- Numerati progressivamente da 1 a 57
|
||||
- Tutti dichiarati "**LONG**" (segnali rialzisti)
|
||||
- Tutti nel range **3–6 candele** (range 1 della tassonomia esposta in *Pythagoras Trading Prediction*, pp. 49)
|
||||
|
||||
### Coerenza col paper companion
|
||||
Il paper *Pythagoras Trading Prediction* (p. 49) afferma testualmente:
|
||||
> "Questa tipologia di pattern può estendersi fino a 6 candele, per questo motivo nel libro i pattern sono identificati con range da 3 a 6, da 6 a 12, da 12 a 24, da 24 a 39 e da 39 a 56."
|
||||
|
||||
⇒ Il Libro dei Frattali pubblicato copre **solo il primo range** (3–6 candele). Il numero massimo (56) della tassonomia coincide quasi esattamente con il numero di pattern catalogati (57) — possibile errore tipografico o discrepanza intenzionale di 1 unità.
|
||||
|
||||
### Pattern composti citati nel paper
|
||||
Sempre nel paper companion (p. 53), è dichiarato che pattern di range superiore sono composti dai pattern semplici di range 1:
|
||||
- **Pattern 13 = Pattern 3 + Pattern 11**
|
||||
|
||||
Implicazione: la numerazione del Libro NON è una sequenza temporale ma una **enumerazione di forme distinte** che fungono da "lettere" dell'alfabeto frattale; i pattern complessi sono "parole" composte.
|
||||
|
||||
## Spazio numerico (3–6 candele LONG)
|
||||
|
||||
Per i 57 pattern, l'**unico contenuto numerico estraibile senza vision** è il loro indice 1–57. Tuttavia il **dominio combinatorio** che il catalogo intende coprire può essere ricostruito teoricamente:
|
||||
|
||||
### Encoding compatto di una candela
|
||||
Una candela in approssimazione discreta può essere ridotta a un simbolo dell'alfabeto:
|
||||
- `U` = up (close > open) — bullish body
|
||||
- `D` = down (close < open) — bearish body
|
||||
- `0` = doji (close ≈ open)
|
||||
|
||||
Quindi una sequenza di 3–6 candele appartiene allo spazio `{U,D,0}^k` con k ∈ {3,4,5,6}.
|
||||
|
||||
| k | |Σᵏ| = 3ᵏ |
|
||||
|---|---|
|
||||
| 3 | 27 |
|
||||
| 4 | 81 |
|
||||
| 5 | 243 |
|
||||
| 6 | 729 |
|
||||
| **Tot** | **1080** sequenze teoriche |
|
||||
|
||||
57 pattern catalogati su 1080 sequenze ⇒ il catalogo seleziona ≈ **5.3%** dello spazio totale.
|
||||
|
||||
### Filtro implicito "LONG"
|
||||
"LONG" implica che il pattern preceda un breakout/continuazione al rialzo. Restrizione plausibile:
|
||||
- pattern terminanti con candela bullish forte, o
|
||||
- pattern con dominanza U / 0 nelle ultime k-1 candele, o
|
||||
- pattern strutturali noti dell'analisi tecnica (martello, inverted hammer, morning star, ecc.) — limitati a 3–6 candele.
|
||||
|
||||
## Numeri ricavabili dal Libro (l'unico set "puro")
|
||||
|
||||
Dal solo testo del Libro, gli agenti hanno accesso a:
|
||||
1. **Cardinalità del catalogo**: 57
|
||||
2. **Numerazione**: serie 1, 2, 3, …, 57
|
||||
3. **Range candele**: [3, 6] (chiuso, intero)
|
||||
4. **Direzione**: solo LONG
|
||||
5. **Costante autori**: 2 (Serleto, Malanga)
|
||||
6. **Edizione**: nov 2024
|
||||
7. **Numero di pagine**: 58
|
||||
|
||||
## Indizi numerici per l'analisi di similitudine
|
||||
|
||||
| Numero | Match con Pythagoras Trading Prediction |
|
||||
|---|---|
|
||||
| 57 pattern | tassonomia bordo superiore = 56 (errore di 1) |
|
||||
| 56 (bordo tassonomia) | numerologia: 56 = 8·7 = ottava·sacro |
|
||||
| 3 (min candele) | numero sacro pitagorico (triade); 3 = lati triangolo Evideon |
|
||||
| 6 (max candele) | numero perfetto (1+2+3); φ²-... |
|
||||
| 6/3 = 2 | ratio 2 = ottava musicale |
|
||||
| (max-min)/min = 1 | unità |
|
||||
| log(57)/log(3) ≈ 3.69 | dimensione frattale di Hausdorff di insieme dimensionale 4 ≈ 3.69 |
|
||||
|
||||
## Punti utili per estrazione numerica (per il task di similitudini)
|
||||
|
||||
1. **N = 57** pattern → cardinality, da confrontare con 56 (bordo del paper), 528 Hz (Solfeggio), 588 candele indicatore
|
||||
2. **k ∈ {3, 4, 5, 6}** → estremi del range
|
||||
3. **Σ³⁺⁴⁺⁵⁺⁶ = 1080** sequenze teoriche (3ᵏ encoded) → 1080 = 8·135 = 2³·3³·5
|
||||
4. **Tassonomia bordi**: 3, 6, 12, 24, 39, 56 — sequenza utile per fingerprint
|
||||
5. **Direzione "LONG"** → variabile binaria
|
||||
6. **Composizionalità**: Pattern 13 = Pattern 3 + Pattern 11 → grammar di pattern frattali
|
||||
|
||||
## Conclusione
|
||||
|
||||
Il Libro dei Frattali, **letto senza immagini**, è un oggetto numerico molto povero (57 etichette su 58 pagine). Per produrre similitudini significative tra i due testi, occorre:
|
||||
|
||||
1. Trattare il Libro come **schema combinatorio implicito** (1080 sequenze possibili, 57 selezionate)
|
||||
2. Confrontare i **numeri-chiave** (3, 6, 57, range) con i numeri estratti dal paper teorico (φ, π, e, 528, 588, 137.0359, etc.)
|
||||
3. Lasciare agli agenti il compito di **trovare ricorrenze, ratios, e dimensioni frattali** che colleghino i due insiemi numerici.
|
||||
|
||||
Vedere [Pythagoras_Trading_Prediction.summary.md](Pythagoras_Trading_Prediction.summary.md) per il dominio numerico del paper teorico.
|
||||
@@ -0,0 +1,182 @@
|
||||
# Pythagoras Trading Prediction — Riassunto
|
||||
|
||||
**Autori:** Luca Serleto, Corrado Malanga
|
||||
**Editore/Stampa:** Adobe InDesign, dic 2024
|
||||
**Pagine:** 66 — testo embedded ~91 KB (no immagini estraibili: figure presenti ma non analizzate per scelta esplicita dell'utente)
|
||||
**Estratto raw:** [_extracted/Pythagoras_Trading_Prediction_.txt](_extracted/Pythagoras_Trading_Prediction_.txt)
|
||||
|
||||
## Tesi centrale
|
||||
|
||||
Il prezzo dei mercati finanziari è descrivibile come **funzione frattale**, scomponibile via **trasformata di Fourier** in componenti sinusoidali periodiche, e quindi proiettabile nel futuro. Il "parametro nascosto" che la fisica quantistica cerca da decenni è la **Coscienza** (entropia di Shannon ΔS), che modula sia la capacità di osservare/predire che la traiettoria stessa dei prezzi.
|
||||
|
||||
## Struttura del libro (4 parti)
|
||||
|
||||
1. **Parte teorica / coscienziale** (pp. 2–24): framework filosofico-fisico
|
||||
2. **Frattali** (pp. 37–44): indicatore frattale H-C
|
||||
3. **Trasformata di Fourier** (pp. 45–58): pattern catalogati e applicazione
|
||||
4. **Piattaforma Pythagoras** (pp. 59–66): manuale operativo (Auto + Manual mode)
|
||||
|
||||
## Concetti chiave (in ordine di apparizione)
|
||||
|
||||
### 1. Approccio "femminile" vs "maschile" alla predizione
|
||||
- **Femminile/animico**: visione olografica del tempo (Pribram, Bohm), tutto co-presente nel presente; il cervello come "lettore di ologrammi"
|
||||
- **Maschile/spirituale**: approccio galileiano-statistico via algoritmo
|
||||
- Tesi: i due metodi vanno integrati
|
||||
|
||||
### 2. Probabilità → Certezza
|
||||
> "Se il sistema predittivo è corretto al 10%, possiamo trovare il parametro che trasformi quella probabilità nella certezza al 100%."
|
||||
|
||||
Il parametro mancante è la **Coscienza**.
|
||||
|
||||
### 3. Teorema di ricorrenza di Poincaré
|
||||
In meccanica hamiltoniana, in un sistema dinamico a spazio delle fasi limitato, lo stato può tornare arbitrariamente vicino a quello iniziale dopo tempo sufficiente. → Il tempo è **circolare**, gli eventi ricorrono → mercati ripetibili.
|
||||
|
||||
### 4. Entropia di Shannon S
|
||||
- S = -log(W), W = microstati
|
||||
- Coscienza ≡ S; **Consapevolezza ≡ ΔS** (variazione di entropia tra istante e successivo)
|
||||
- Inizio universo: S = -∞; fine: S = 0; entropia totale = ∞
|
||||
|
||||
### 5. Hartman-Curry → "Indicatore frattale H-C"
|
||||
- Introdotto da Corrado Malanga al XCongress di Pescara (2018)
|
||||
- Prima applicazione su grafico EUR/USD
|
||||
- Struttura tridimensionale costruita su "numeri universali" che genera ricorrenze
|
||||
|
||||
### 6. Numeri Universali (Evideon)
|
||||
> "I numeri dell'universo sono: la sezione aurea φ, π, il numero di Eulero e, √2, ecc. Numeri irrazionali e trascendenti."
|
||||
|
||||
Triangolo rettangolo con costanti universali fisiche; lati moltiplicati per opportuni coefficienti basati su π, e, φ e temperatura assoluta di Kelvin/100.
|
||||
|
||||
**Costanti numeriche citate esplicitamente:**
|
||||
- 137.0359 × √π / 2 = **121.449**
|
||||
- 266.87 × 1 / √e = **161.86**
|
||||
- Frequenze Solfeggio: **741, 528, 852, 639, 396, 417** (p. 27)
|
||||
- Numero di Avogadro N = 6.022·10²³ (citato come ipotesi sul numero di sub-componenti dell'Universo)
|
||||
|
||||
### 7. Trasformata di Fourier
|
||||
Scomposizione di qualsiasi funzione continua in seni e coseni:
|
||||
- Frequenze positive (orario) e negative (antiorario) → propagazione **avanti e indietro nel tempo**
|
||||
- Ricostruzione approssimata con N componenti → **N = misura della consapevolezza**
|
||||
- Pochi N: previsione approssimativa; molti N: previsione accurata
|
||||
|
||||
### 8. Equazione di Schrödinger ↔ trading
|
||||
Identificazione formale:
|
||||
|
||||
| Schrödinger | Trading |
|
||||
|---|---|
|
||||
| iℏ ∂Ψ/∂t = -ℏ²/(2m) ∂²Ψ/∂x² + V(x)Ψ | dinamica del prezzo |
|
||||
| derivata seconda di Ψ in x | "piegatura" spazio-temporale del grafico |
|
||||
| m | massa fittizia ≈ volume d'affari |
|
||||
| V(Ψ) | perturbazioni esterne (manipolazione di mercato) |
|
||||
| Ψ ampiezza | altezza candele (asse energia/prezzo) |
|
||||
| Ψ² probabilità | probabilità che l'evento si materializzi |
|
||||
|
||||
Identificazione con energia libera di Gibbs: termine -TΔS della formula di Gibbs ≡ termine entropico in Schrödinger.
|
||||
|
||||
### 9. Centro di inversione (operazione geometrica chiave)
|
||||
Sul modello "Evideon" (3 assi: spazio, tempo, energia), un punto A del passato si trasforma nel punto E del futuro tramite:
|
||||
- **Riflessione speculare sull'asse verticale** (energia/prezzo)
|
||||
- **Riflessione speculare sull'asse orizzontale** (tempo)
|
||||
- = traslazione temporale + inversione di pattern
|
||||
|
||||
Inoltre il pattern subisce:
|
||||
- **Compressione** o **dilatazione** sull'asse x (tempo)
|
||||
- **Compressione** o **dilatazione** sull'asse y (prezzo)
|
||||
A seconda della posizione del punto sull'Evideon (tempo sotto-multiplo → contrazione).
|
||||
|
||||
### 10. Finestra di Overton
|
||||
- Apertura temporale che definisce "il presente"
|
||||
- Analogia con SAR (Synthetic Aperture Radar): più larga la finestra, più precisa la previsione
|
||||
- Pythagoras applica la stessa finestra al passato per proiettare nel futuro
|
||||
|
||||
### 11. ZPE (Zero Point Energy)
|
||||
Universo non vuoto: pieno di particelle e anti-particelle che si annichiliscono. Permette di "conoscere perfettamente coordinate spazio-temporali e valore nullo dell'energia": non c'è incertezza vera (contro Heisenberg).
|
||||
|
||||
### 12. Indicatore frattale su TradingView
|
||||
Disponibile su licenza dedicata. Parametri operativi:
|
||||
- Tracciato da minimo → massimo
|
||||
- **25 linee verticali** all'interno di **588 candele**
|
||||
- 90% segnala accelerazione (aumento volatilità), 10% lateralizzazione
|
||||
- Tolleranza ±3 ore su time frame 1H/4H
|
||||
- **Linee blu inclinate** = supporti/resistenze tempo-condizionati
|
||||
- **Cerchi bianchi** = obiettivi di prezzo circolari
|
||||
|
||||
### 13. Pattern range (taxonomy del Book dei Frattali)
|
||||
| Range | Numero candele |
|
||||
|---|---|
|
||||
| Range 1 | 3 – 6 candele |
|
||||
| Range 2 | 6 – 12 candele |
|
||||
| Range 3 | 12 – 24 candele |
|
||||
| Range 4 | 24 – 39 candele |
|
||||
| Range 5 | 39 – 56 candele |
|
||||
|
||||
I pattern oltre 6 candele diventano "frattali complessi": composizione di pattern semplici (es. Pattern 13 = Pattern 3 + Pattern 11).
|
||||
|
||||
### 14. Variabili che NON alterano il target
|
||||
- Inclinazione del pattern
|
||||
- Ampiezza
|
||||
- Curvatura
|
||||
- Numero di candele entro lo stesso range
|
||||
|
||||
→ il target di proiezione è invariante per queste deformazioni.
|
||||
|
||||
### 15. Variabili che alterano l'applicabilità
|
||||
- **Time frame**: 15m, 1H, 4H, 1D
|
||||
- **Volatilità dell'asset** ("consapevolezza dell'asset")
|
||||
- **Leva finanziaria**
|
||||
|
||||
## Esempi concreti citati
|
||||
|
||||
| Pattern | Asset | Data/Ora | TF |
|
||||
|---|---|---|---|
|
||||
| 1 | BTC | 28 ago 2024 00:00 | 15m |
|
||||
| 1 | ETH | 28 ago 2024 00:00 | 15m |
|
||||
| 1 | Oro | 29 ago 2024 14:30 | 15m |
|
||||
| 1 | Argento | 17 lug 2024 18:15 | 15m |
|
||||
| 13 | BTC | 24 ago 2024 23:15 | 15m |
|
||||
| 13 | ETH | 24 ago 2024 23:45 | 15m |
|
||||
| 13 | Oro | 02 ago 2024 16:45 | 15m |
|
||||
| 13 | Argento | 02 ago 2024 16:45 | 15m |
|
||||
| 15 | DJT (Trump Media) | 12 lug 2024 (chiusura) | 1D |
|
||||
|
||||
Il caso DJT è presentato come "predizione" dell'attentato a Trump del 14 lug 2024 (frattale complesso al rialzo formatosi il 12 lug → escludeva evento negativo).
|
||||
|
||||
## Piattaforma Pythagoras
|
||||
|
||||
### Auto Mode
|
||||
1. Seleziona asset
|
||||
2. Seleziona TF (15m, 1H, 4H)
|
||||
3. Click "Predict" → Fourier identifica frattale e proietta futuro (colore giallo)
|
||||
|
||||
### Manual Mode
|
||||
1. Seleziona asset
|
||||
2. Seleziona frattale dal Book per data
|
||||
3. Click "Predict" → proiezione
|
||||
|
||||
## Disclaimer interno al testo
|
||||
|
||||
> "Anche se si conosce un potenziale futuro, questo non è garanzia di guadagno. Solo la consapevolezza del trader, basata sull'esperienza, potrà trasformare la predizione di un potenziale futuro in un profitto concreto."
|
||||
|
||||
Il testo enfatizza che il **target del trader non è guadagnare ma capire sé stesso** (approccio "coscienziale"). Il denaro è strumento di apprendimento dell'entropia/dualità.
|
||||
|
||||
## Punti utili per estrazione numerica (per il task di similitudini)
|
||||
|
||||
1. **Numeri universali esatti**: φ ≈ 1.6180339887, π ≈ 3.1415926535, e ≈ 2.71828, √2 ≈ 1.41421
|
||||
2. **Costanti derivate dal testo**: 137.0359, 121.449, 266.87, 161.86
|
||||
3. **Frequenze Solfeggio (Hz)**: 396, 417, 528, 639, 741, 852
|
||||
4. **Conteggi operativi**: 25 linee/588 candele = ratio 0.0425
|
||||
5. **Numeri ricorrenti**: 6.022·10²³ (Avogadro)
|
||||
6. **Range candele**: bordi 3, 6, 12, 24, 39, 56 — sequenza con incrementi 3, 6, 12, 15, 17 (NON Fibonacci puro, ma sequenza con ratios ~2x)
|
||||
7. **Ratios tra bordi**: 6/3=2, 12/6=2, 24/12=2, 39/24=1.625, 56/39=1.436 — gli ultimi due si avvicinano a φ ≈ 1.618 e √2 ≈ 1.414
|
||||
8. **Sequenza pattern numerati**: 1, 2, 3, ... 57 (matching col Libro dei Frattali)
|
||||
9. **Operazioni geometriche**: mirror H, mirror V, scaling x/y (4 trasformazioni)
|
||||
10. **TF candidati**: 15m, 1H, 4H, 1D (multipli 4x e 6x)
|
||||
|
||||
## Bibliografia interna / link YouTube citati
|
||||
- youtube.com/shorts/OnM09uMCS50 (teorema di Poincaré)
|
||||
- youtu.be/hHig-PP4Mcs (gravità come Coscienza)
|
||||
- youtu.be/HTIYZBrzZbM (struttura dell'Evideon)
|
||||
|
||||
## Opere citate degli autori
|
||||
- Corrado Malanga, *Io e Dio*, Spazio Interiore, Roma
|
||||
- Corrado Malanga, *Trilogia della Coscienza*, Spazio Interiore, Roma
|
||||
- Corrado Malanga al XCongress, Pescara, 2018 (intro pattern H-C)
|
||||
@@ -0,0 +1,227 @@
|
||||
|
||||
|
||||
===PAGE 1===
|
||||
Luca Serleto
|
||||
Corrado Malanga
|
||||
Libro dei Libro dei
|
||||
FrattaliFrattali
|
||||
|
||||
===PAGE 2===
|
||||
Pattern da 3 candele a massimo 6 candele (LONG)
|
||||
Pattern 1
|
||||
|
||||
===PAGE 3===
|
||||
Pattern 2
|
||||
|
||||
===PAGE 4===
|
||||
Pattern 3
|
||||
|
||||
===PAGE 5===
|
||||
Pattern 4
|
||||
|
||||
===PAGE 6===
|
||||
Pattern 5
|
||||
|
||||
|
||||
===PAGE 7===
|
||||
Pattern 6
|
||||
|
||||
|
||||
===PAGE 8===
|
||||
Pattern 7
|
||||
|
||||
|
||||
===PAGE 9===
|
||||
Pattern 8
|
||||
|
||||
|
||||
===PAGE 10===
|
||||
Pattern 9
|
||||
|
||||
|
||||
===PAGE 11===
|
||||
Pattern 10
|
||||
|
||||
|
||||
===PAGE 12===
|
||||
Pattern 11
|
||||
|
||||
|
||||
===PAGE 13===
|
||||
Pattern 12
|
||||
|
||||
|
||||
===PAGE 14===
|
||||
Pattern 13
|
||||
|
||||
|
||||
===PAGE 15===
|
||||
Pattern 14
|
||||
|
||||
|
||||
===PAGE 16===
|
||||
Pattern 15
|
||||
|
||||
|
||||
===PAGE 17===
|
||||
Pattern 16
|
||||
|
||||
|
||||
===PAGE 18===
|
||||
Pattern 17
|
||||
|
||||
|
||||
===PAGE 19===
|
||||
Pattern 18
|
||||
|
||||
|
||||
===PAGE 20===
|
||||
Pattern 19
|
||||
|
||||
|
||||
===PAGE 21===
|
||||
Pattern 20
|
||||
|
||||
|
||||
===PAGE 22===
|
||||
Pattern 21
|
||||
|
||||
|
||||
===PAGE 23===
|
||||
Pattern 22
|
||||
|
||||
|
||||
===PAGE 24===
|
||||
Pattern 23
|
||||
|
||||
|
||||
===PAGE 25===
|
||||
Pattern 24
|
||||
|
||||
|
||||
===PAGE 26===
|
||||
Pattern 25
|
||||
|
||||
|
||||
===PAGE 27===
|
||||
Pattern 26
|
||||
|
||||
|
||||
===PAGE 28===
|
||||
Pattern 27
|
||||
|
||||
|
||||
===PAGE 29===
|
||||
Pattern 28
|
||||
|
||||
|
||||
===PAGE 30===
|
||||
Pattern 29
|
||||
|
||||
|
||||
===PAGE 31===
|
||||
Pattern 30
|
||||
|
||||
|
||||
===PAGE 32===
|
||||
Pattern 31
|
||||
|
||||
|
||||
===PAGE 33===
|
||||
Pattern 32
|
||||
|
||||
|
||||
===PAGE 34===
|
||||
Pattern 33
|
||||
|
||||
|
||||
===PAGE 35===
|
||||
Pattern 34
|
||||
|
||||
|
||||
===PAGE 36===
|
||||
Pattern 35
|
||||
|
||||
|
||||
===PAGE 37===
|
||||
Pattern 36
|
||||
|
||||
|
||||
===PAGE 38===
|
||||
Pattern 37
|
||||
|
||||
|
||||
===PAGE 39===
|
||||
Pattern 38
|
||||
|
||||
|
||||
===PAGE 40===
|
||||
Pattern 39
|
||||
|
||||
|
||||
===PAGE 41===
|
||||
Pattern 40
|
||||
|
||||
|
||||
===PAGE 42===
|
||||
Pattern 41
|
||||
|
||||
|
||||
===PAGE 43===
|
||||
Pattern 42
|
||||
|
||||
|
||||
===PAGE 44===
|
||||
Pattern 43
|
||||
|
||||
|
||||
===PAGE 45===
|
||||
Pattern 44
|
||||
|
||||
|
||||
===PAGE 46===
|
||||
Pattern 45
|
||||
|
||||
|
||||
===PAGE 47===
|
||||
Pattern 46
|
||||
|
||||
|
||||
===PAGE 48===
|
||||
Pattern 47
|
||||
|
||||
|
||||
===PAGE 49===
|
||||
Pattern 48
|
||||
|
||||
|
||||
===PAGE 50===
|
||||
Pattern 49
|
||||
|
||||
|
||||
===PAGE 51===
|
||||
Pattern 50
|
||||
|
||||
|
||||
===PAGE 52===
|
||||
Pattern 51
|
||||
|
||||
|
||||
===PAGE 53===
|
||||
Pattern 52
|
||||
|
||||
|
||||
===PAGE 54===
|
||||
Pattern 53
|
||||
|
||||
===PAGE 55===
|
||||
Pattern 54
|
||||
|
||||
===PAGE 56===
|
||||
Pattern 55
|
||||
|
||||
===PAGE 57===
|
||||
Pattern 56
|
||||
|
||||
===PAGE 58===
|
||||
Pattern 57
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,78 @@
|
||||
# strategy_pythagoras
|
||||
|
||||
Strategia di trading basata sul framework **Pythagoras-Malanga**
|
||||
(candle-pattern + geometria frattale), evoluta via GA sul core
|
||||
`multi_swarm_core`. Workspace member del monorepo `multi_swarm_coevolutive`.
|
||||
|
||||
## Scope
|
||||
|
||||
Pipeline coevolutiva su candele OHLC: il GA del core esplora combinazioni
|
||||
di indicatori candle-pattern, geometria pitagorica e ratio frattali, e
|
||||
produce strategie JSON freezate. Questo member le esegue in
|
||||
**paper-trading forward-test** ed espone una dashboard NiceGUI per
|
||||
analisi invarianza scala/tick e candle.
|
||||
|
||||
## Layout
|
||||
|
||||
```
|
||||
strategy_pythagoras/
|
||||
├── backend/ paper trading runner (PaperExecutor, Portfolio, PaperRepository)
|
||||
│ └── schema.py tabelle paper_trading_* (DB locale dedicato)
|
||||
├── frontend/ NiceGUI dashboard (tab invariance, candle, equity, ticks)
|
||||
├── strategies/ JSON freezate input al runner
|
||||
│ (pythagoras_*.json)
|
||||
└── prompts.json 7 stili di prompt LLM (candle-pattern, frattale, ratio,
|
||||
pivot, kagi, renko, hybrid)
|
||||
```
|
||||
|
||||
## Run paper-trading smoke
|
||||
|
||||
```bash
|
||||
uv run python scripts/run_pythagoras_smoke.py \
|
||||
--name pythagoras-smoke-001 \
|
||||
--initial-capital 1000 \
|
||||
--poll-seconds 300
|
||||
```
|
||||
|
||||
Il default `--strategies-dir` punta ai JSON shippati col package via
|
||||
`importlib.resources.files("strategy_pythagoras") / "strategies"`.
|
||||
|
||||
## Dashboard
|
||||
|
||||
```bash
|
||||
uv run python -m strategy_pythagoras.frontend.nicegui_app
|
||||
```
|
||||
|
||||
In produzione: `https://swarm.tielogic.xyz/strategy_pythagoras_gui/`
|
||||
(root_path configurato via `DASHBOARD_ROOT_PATH=/strategy_pythagoras_gui`).
|
||||
|
||||
## DB schema
|
||||
|
||||
Schema isolato dal core e dalle altre strategie. Due DB distinti:
|
||||
|
||||
- `state/strategy_pythagoras.db` — GA + analisi invarianza
|
||||
(env `STRATEGY_PYTHAGORAS_DB_PATH`)
|
||||
- `state/strategy_pythagoras_paper.db` — paper-trading runs
|
||||
(env `STRATEGY_PYTHAGORAS_PAPER_DB_PATH`)
|
||||
|
||||
Tabelle paper-trading:
|
||||
|
||||
- `paper_trading_runs` — metadata run (id, name, capital, status)
|
||||
- `paper_trading_positions` — posizioni aperte (long/short)
|
||||
- `paper_trading_trades` — trade realized (entry/exit, pnl, fees)
|
||||
- `paper_trading_equity` — equity curve snapshot
|
||||
- `paper_trading_ticks` — log signal/action per ogni bar
|
||||
|
||||
DDL gestito da `strategy_pythagoras.backend.schema.init_schema()`.
|
||||
|
||||
La dashboard legge **anche** il `runs.db` del core GA (env `GA_DB_PATH`)
|
||||
per correlare paper performance con i genomi di provenienza e con i
|
||||
risultati di fitness invariance.
|
||||
|
||||
## References
|
||||
|
||||
- Spec: `docs/superpowers/specs/2026-05-19-strategy-pythagoras-design.md`
|
||||
- Plan: `docs/superpowers/plans/2026-05-19-strategy-pythagoras.md`
|
||||
- Summary paper: `Pythagoras/Pythagoras_Trading_Prediction.summary.md`
|
||||
- Summary frattali: `Pythagoras/Libro_frattali.summary.md`
|
||||
- Pattern member: vedi `src/strategy_crypto/README.md`
|
||||
@@ -0,0 +1,21 @@
|
||||
[project]
|
||||
name = "strategy-pythagoras"
|
||||
version = "0.1.0"
|
||||
description = "Strategy Pythagoras: candle-pattern GA su framework Pythagoras-Malanga, paper-trading runner + NiceGUI dashboard"
|
||||
authors = [{ name = "Adriano Dal Pastro", email = "adrianodalpastro@tielogic.com" }]
|
||||
requires-python = ">=3.13"
|
||||
dependencies = [
|
||||
"multi-swarm-core",
|
||||
"nicegui>=3.11.1",
|
||||
"plotly>=5.24",
|
||||
"pandas>=2.2",
|
||||
"pyarrow>=18.0",
|
||||
]
|
||||
|
||||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[tool.hatch.build.targets.wheel.force-include]
|
||||
"strategy_pythagoras/strategies" = "strategy_pythagoras/strategies"
|
||||
"strategy_pythagoras/prompts.json" = "strategy_pythagoras/prompts.json"
|
||||
@@ -0,0 +1,23 @@
|
||||
"""Backend paper-trading per la strategia strategy_pythagoras.
|
||||
|
||||
Espone le classi principali per import ergonomici in scripts/runner:
|
||||
|
||||
from strategy_pythagoras.backend import PaperExecutor, Portfolio, PaperRepository
|
||||
|
||||
Per i tipi interni (TickResult, OpenPosition, Trade) importare dal sotto-modulo.
|
||||
"""
|
||||
|
||||
from .executor import PaperExecutor, TickResult
|
||||
from .persistence import PaperRepository
|
||||
from .portfolio import OpenPosition, Portfolio
|
||||
from .schema import PAPER_SCHEMA_SQL, init_schema
|
||||
|
||||
__all__ = [
|
||||
"PAPER_SCHEMA_SQL",
|
||||
"OpenPosition",
|
||||
"PaperExecutor",
|
||||
"PaperRepository",
|
||||
"Portfolio",
|
||||
"TickResult",
|
||||
"init_schema",
|
||||
]
|
||||
@@ -0,0 +1,97 @@
|
||||
"""PaperExecutor: applica un segnale di strategia a un Portfolio.
|
||||
|
||||
Il flusso per ogni tick:
|
||||
|
||||
bar OHLCV chiuso -> compile_strategy(strategy) -> Series[Side]
|
||||
-> last_signal = series.iloc[-1]
|
||||
-> match con posizione attuale -> open / close / hold
|
||||
|
||||
Niente delay 1-bar: in paper-trading il segnale viene calcolato sulla
|
||||
barra appena chiusa e applicato al prezzo close della stessa. La latenza
|
||||
reale tra tick e ordine va misurata separatamente (Phase 3 spec).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
import pandas as pd # type: ignore[import-untyped]
|
||||
|
||||
from multi_swarm_core.backtest.orders import Side, Trade
|
||||
from multi_swarm_core.protocol.compiler import compile_strategy
|
||||
from multi_swarm_core.protocol.parser import parse_strategy
|
||||
|
||||
from .portfolio import OpenPosition, Portfolio
|
||||
|
||||
|
||||
@dataclass
|
||||
class TickResult:
|
||||
ts: datetime
|
||||
symbol: str
|
||||
bar_ts: datetime
|
||||
close_price: float
|
||||
signal: Side
|
||||
action_taken: str # "open_long" | "open_short" | "close" | "reverse" | "hold"
|
||||
trade: Trade | None = None
|
||||
new_position: OpenPosition | None = None
|
||||
|
||||
|
||||
class PaperExecutor:
|
||||
def __init__(self, strategy_json_path: Path, symbol: str) -> None:
|
||||
text = strategy_json_path.read_text()
|
||||
# parse_strategy si aspetta JSON pulito, non fence; il file e' gia' JSON.
|
||||
self._strategy = parse_strategy(text)
|
||||
self._compiled = compile_strategy(self._strategy)
|
||||
self.symbol = symbol
|
||||
self.strategy_path = strategy_json_path
|
||||
|
||||
def execute_tick(
|
||||
self,
|
||||
portfolio: Portfolio,
|
||||
ohlcv: pd.DataFrame,
|
||||
now: datetime,
|
||||
) -> TickResult:
|
||||
"""Esegui un tick: calcola segnale su tutto ``ohlcv`` (per indicatori
|
||||
con lookback), prendi l'ultimo, e applica al portfolio."""
|
||||
if len(ohlcv) == 0:
|
||||
raise ValueError("Empty OHLCV passed to execute_tick")
|
||||
signals = self._compiled(ohlcv)
|
||||
# ultimo bar chiuso
|
||||
bar_ts = ohlcv.index[-1]
|
||||
close_price = float(ohlcv["close"].iloc[-1])
|
||||
signal = Side(signals.iloc[-1]) if signals.iloc[-1] is not None else Side.FLAT
|
||||
|
||||
current = portfolio.positions.get(self.symbol)
|
||||
action = "hold"
|
||||
trade: Trade | None = None
|
||||
new_position: OpenPosition | None = None
|
||||
|
||||
if current is None and signal != Side.FLAT:
|
||||
new_position = portfolio.open(self.symbol, signal, close_price, now)
|
||||
action = f"open_{signal.value}"
|
||||
elif current is not None and signal == Side.FLAT:
|
||||
trade = portfolio.close(self.symbol, close_price, now)
|
||||
action = "close"
|
||||
elif current is not None and signal != current.side:
|
||||
# reverse: chiudi e riapri opposto
|
||||
trade = portfolio.close(self.symbol, close_price, now)
|
||||
new_position = portfolio.open(self.symbol, signal, close_price, now)
|
||||
action = "reverse"
|
||||
|
||||
return TickResult(
|
||||
ts=now,
|
||||
symbol=self.symbol,
|
||||
bar_ts=bar_ts.to_pydatetime() if hasattr(bar_ts, "to_pydatetime") else bar_ts,
|
||||
close_price=close_price,
|
||||
signal=signal,
|
||||
action_taken=action,
|
||||
trade=trade,
|
||||
new_position=new_position,
|
||||
)
|
||||
|
||||
@property
|
||||
def strategy_dict(self) -> dict:
|
||||
return json.loads(self.strategy_path.read_text())
|
||||
@@ -0,0 +1,123 @@
|
||||
"""Persistenza paper-trading: scrive su un DB dedicato (state/strategy_pythagoras_paper.db)
|
||||
con le tabelle ``paper_trading_*`` definite localmente in :mod:`.schema`.
|
||||
|
||||
Il DB e' isolato dal ``runs.db`` del core GA: nessun naming conflict con
|
||||
future strategie (state/strategy_<asset>.db), nessuna contention di lock
|
||||
fra writer GA e writer paper.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sqlite3
|
||||
import uuid
|
||||
from datetime import UTC, datetime
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from .executor import TickResult
|
||||
from .portfolio import Portfolio
|
||||
from .schema import init_schema as _init_paper_schema
|
||||
|
||||
|
||||
class PaperRepository:
|
||||
def __init__(self, db_path: Path | str):
|
||||
self.db_path = Path(db_path)
|
||||
|
||||
def init_schema(self) -> None:
|
||||
"""Crea (se mancanti) le tabelle paper_trading_* su ``self.db_path``."""
|
||||
_init_paper_schema(self.db_path)
|
||||
|
||||
def _conn(self) -> sqlite3.Connection:
|
||||
conn = sqlite3.connect(self.db_path, isolation_level=None)
|
||||
conn.row_factory = sqlite3.Row
|
||||
conn.execute("PRAGMA foreign_keys = ON")
|
||||
conn.execute("PRAGMA journal_mode = WAL")
|
||||
return conn
|
||||
|
||||
@staticmethod
|
||||
def _now() -> str:
|
||||
return datetime.now(UTC).isoformat()
|
||||
|
||||
def create_run(self, name: str, initial_capital: float, config: dict[str, Any]) -> str:
|
||||
rid = uuid.uuid4().hex
|
||||
with self._conn() as conn:
|
||||
conn.execute(
|
||||
"INSERT INTO paper_trading_runs "
|
||||
"(id, name, started_at, status, initial_capital, config_json) "
|
||||
"VALUES (?,?,?,?,?,?)",
|
||||
(rid, name, self._now(), "running", initial_capital, json.dumps(config)),
|
||||
)
|
||||
return rid
|
||||
|
||||
def stop_run(self, run_id: str, status: str = "stopped") -> None:
|
||||
with self._conn() as conn:
|
||||
conn.execute(
|
||||
"UPDATE paper_trading_runs SET stopped_at=?, status=? WHERE id=?",
|
||||
(self._now(), status, run_id),
|
||||
)
|
||||
|
||||
def save_tick(self, run_id: str, tick: TickResult) -> None:
|
||||
with self._conn() as conn:
|
||||
conn.execute(
|
||||
"INSERT INTO paper_trading_ticks "
|
||||
"(paper_run_id, symbol, ts, bar_ts, close_price, signal, action_taken) "
|
||||
"VALUES (?,?,?,?,?,?,?)",
|
||||
(
|
||||
run_id,
|
||||
tick.symbol,
|
||||
tick.ts.isoformat(),
|
||||
tick.bar_ts.isoformat() if hasattr(tick.bar_ts, "isoformat") else str(tick.bar_ts),
|
||||
tick.close_price,
|
||||
tick.signal.value,
|
||||
tick.action_taken,
|
||||
),
|
||||
)
|
||||
if tick.trade is not None:
|
||||
t = tick.trade
|
||||
conn.execute(
|
||||
"INSERT INTO paper_trading_trades "
|
||||
"(paper_run_id, symbol, side, qty, entry_price, exit_price, "
|
||||
"entry_ts, exit_ts, pnl, fees) VALUES (?,?,?,?,?,?,?,?,?,?)",
|
||||
(
|
||||
run_id,
|
||||
tick.symbol,
|
||||
t.side.value,
|
||||
t.size,
|
||||
t.entry_price,
|
||||
t.exit_price,
|
||||
t.entry_ts.isoformat(),
|
||||
t.exit_ts.isoformat(),
|
||||
t.net_pnl,
|
||||
t.fees,
|
||||
),
|
||||
)
|
||||
|
||||
def save_equity_snapshot(
|
||||
self,
|
||||
run_id: str,
|
||||
ts: datetime,
|
||||
equity: float,
|
||||
cash: float,
|
||||
positions_value: float,
|
||||
) -> None:
|
||||
with self._conn() as conn:
|
||||
conn.execute(
|
||||
"INSERT INTO paper_trading_equity "
|
||||
"(paper_run_id, ts, equity, cash, positions_value) VALUES (?,?,?,?,?)",
|
||||
(run_id, ts.isoformat(), equity, cash, positions_value),
|
||||
)
|
||||
|
||||
def sync_open_positions(self, run_id: str, portfolio: Portfolio) -> None:
|
||||
"""Sostituisce snapshot posizioni aperte. Idempotente: cancella e reinserisce."""
|
||||
with self._conn() as conn:
|
||||
conn.execute(
|
||||
"DELETE FROM paper_trading_positions WHERE paper_run_id=?", (run_id,)
|
||||
)
|
||||
for sym, pos in portfolio.positions.items():
|
||||
conn.execute(
|
||||
"INSERT INTO paper_trading_positions "
|
||||
"(paper_run_id, symbol, side, qty, entry_price, entry_ts) "
|
||||
"VALUES (?,?,?,?,?,?)",
|
||||
(run_id, sym, pos.side.value, pos.qty, pos.entry_price, pos.entry_ts.isoformat()),
|
||||
)
|
||||
@@ -0,0 +1,104 @@
|
||||
"""Portfolio multi-asset per paper-trading.
|
||||
|
||||
Modello semplificato: capitale unico ``cash``, allocazione equal-weight
|
||||
fra N posizioni (sleeve = 1/N del capitale iniziale per ogni simbolo).
|
||||
Niente leva, niente liquidation, fees su entry+exit (bp del notional).
|
||||
|
||||
Una :class:`Position` rappresenta una posizione aperta su un singolo
|
||||
simbolo (long/short, qty in unita' dell'asset, prezzo di entry). La
|
||||
posizione viene chiusa con :meth:`Portfolio.close` che produce un
|
||||
:class:`Trade` realized e accredita ``cash``.
|
||||
|
||||
Mark-to-market via :meth:`Portfolio.equity`.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
|
||||
from multi_swarm_core.backtest.orders import Side, Trade
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class OpenPosition:
|
||||
symbol: str
|
||||
side: Side
|
||||
qty: float
|
||||
entry_price: float
|
||||
entry_ts: datetime
|
||||
|
||||
|
||||
@dataclass
|
||||
class Portfolio:
|
||||
initial_capital: float
|
||||
fees_bp: float = 5.0
|
||||
n_sleeves: int = 2 # numero strategie / asset previsti
|
||||
cash: float = field(init=False)
|
||||
positions: dict[str, OpenPosition] = field(default_factory=dict)
|
||||
closed_trades: list[Trade] = field(default_factory=list)
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
self.cash = self.initial_capital
|
||||
|
||||
@property
|
||||
def sleeve_capital(self) -> float:
|
||||
return self.initial_capital / self.n_sleeves
|
||||
|
||||
def open(
|
||||
self,
|
||||
symbol: str,
|
||||
side: Side,
|
||||
price: float,
|
||||
ts: datetime,
|
||||
) -> OpenPosition:
|
||||
if symbol in self.positions:
|
||||
raise ValueError(f"Position already open on {symbol}")
|
||||
if side == Side.FLAT:
|
||||
raise ValueError("Cannot open a FLAT position")
|
||||
# sleeve fisso: alloca 1/n_sleeves del capitale iniziale, qty = notional/price.
|
||||
notional = self.sleeve_capital
|
||||
qty = notional / price
|
||||
fees = notional * (self.fees_bp / 10000.0)
|
||||
self.cash -= fees
|
||||
pos = OpenPosition(symbol=symbol, side=side, qty=qty, entry_price=price, entry_ts=ts)
|
||||
self.positions[symbol] = pos
|
||||
return pos
|
||||
|
||||
def close(
|
||||
self,
|
||||
symbol: str,
|
||||
price: float,
|
||||
ts: datetime,
|
||||
) -> Trade:
|
||||
if symbol not in self.positions:
|
||||
raise ValueError(f"No open position on {symbol}")
|
||||
pos = self.positions.pop(symbol)
|
||||
trade = Trade(
|
||||
entry_ts=pos.entry_ts,
|
||||
exit_ts=ts,
|
||||
side=pos.side,
|
||||
size=pos.qty,
|
||||
entry_price=pos.entry_price,
|
||||
exit_price=price,
|
||||
fees_bp=self.fees_bp,
|
||||
)
|
||||
# net_pnl include gia' i fees sull'intero round-trip; abbiamo gia'
|
||||
# addebitato meta' fees all'open, ora addebitiamo il resto.
|
||||
self.cash += trade.gross_pnl - (trade.fees / 2.0)
|
||||
self.closed_trades.append(trade)
|
||||
return trade
|
||||
|
||||
def equity(self, last_prices: dict[str, float]) -> tuple[float, float]:
|
||||
"""Ritorna (equity_totale, positions_value) marcando posizioni aperte
|
||||
al ``last_prices[symbol]``. Posizioni senza prezzo disponibile valgono
|
||||
notional di entry (fallback conservativo)."""
|
||||
positions_value = 0.0
|
||||
for sym, pos in self.positions.items():
|
||||
price = last_prices.get(sym, pos.entry_price)
|
||||
unreal = pos.qty * (
|
||||
price - pos.entry_price if pos.side == Side.LONG
|
||||
else pos.entry_price - price
|
||||
)
|
||||
positions_value += pos.qty * pos.entry_price + unreal
|
||||
return self.cash + positions_value, positions_value
|
||||
@@ -0,0 +1,87 @@
|
||||
"""Schema SQLite per le tabelle paper-trading della strategia pythagoras.
|
||||
|
||||
Owned dal member strategy_pythagoras: il DDL e' standalone rispetto al core,
|
||||
e scrive su un database dedicato (state/strategy_pythagoras_paper.db, env
|
||||
STRATEGY_PYTHAGORAS_PAPER_DB_PATH) separato dal runs.db del core GA. Pattern
|
||||
replicabile per future strategie.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sqlite3
|
||||
from pathlib import Path
|
||||
|
||||
PAPER_SCHEMA_SQL = """
|
||||
CREATE TABLE IF NOT EXISTS paper_trading_runs (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
started_at TEXT NOT NULL,
|
||||
stopped_at TEXT,
|
||||
status TEXT NOT NULL DEFAULT 'running',
|
||||
initial_capital REAL NOT NULL,
|
||||
config_json TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS paper_trading_positions (
|
||||
paper_run_id TEXT NOT NULL,
|
||||
symbol TEXT NOT NULL,
|
||||
side TEXT NOT NULL,
|
||||
qty REAL NOT NULL,
|
||||
entry_price REAL NOT NULL,
|
||||
entry_ts TEXT NOT NULL,
|
||||
PRIMARY KEY (paper_run_id, symbol),
|
||||
FOREIGN KEY (paper_run_id) REFERENCES paper_trading_runs(id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS paper_trading_trades (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
paper_run_id TEXT NOT NULL,
|
||||
symbol TEXT NOT NULL,
|
||||
side TEXT NOT NULL,
|
||||
qty REAL NOT NULL,
|
||||
entry_price REAL NOT NULL,
|
||||
exit_price REAL NOT NULL,
|
||||
entry_ts TEXT NOT NULL,
|
||||
exit_ts TEXT NOT NULL,
|
||||
pnl REAL NOT NULL,
|
||||
fees REAL NOT NULL,
|
||||
FOREIGN KEY (paper_run_id) REFERENCES paper_trading_runs(id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS paper_trading_equity (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
paper_run_id TEXT NOT NULL,
|
||||
ts TEXT NOT NULL,
|
||||
equity REAL NOT NULL,
|
||||
cash REAL NOT NULL,
|
||||
positions_value REAL NOT NULL,
|
||||
FOREIGN KEY (paper_run_id) REFERENCES paper_trading_runs(id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS paper_trading_ticks (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
paper_run_id TEXT NOT NULL,
|
||||
symbol TEXT NOT NULL,
|
||||
ts TEXT NOT NULL,
|
||||
bar_ts TEXT NOT NULL,
|
||||
close_price REAL NOT NULL,
|
||||
signal TEXT NOT NULL,
|
||||
action_taken TEXT NOT NULL,
|
||||
FOREIGN KEY (paper_run_id) REFERENCES paper_trading_runs(id)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_paper_trades_run ON paper_trading_trades(paper_run_id, exit_ts);
|
||||
CREATE INDEX IF NOT EXISTS idx_paper_equity_run ON paper_trading_equity(paper_run_id, ts);
|
||||
CREATE INDEX IF NOT EXISTS idx_paper_ticks_run ON paper_trading_ticks(paper_run_id, ts);
|
||||
"""
|
||||
|
||||
|
||||
def init_schema(db_path: Path | str) -> None:
|
||||
"""Crea (se mancanti) le tabelle paper_trading_* sul db indicato."""
|
||||
Path(db_path).parent.mkdir(parents=True, exist_ok=True)
|
||||
conn = sqlite3.connect(str(db_path))
|
||||
try:
|
||||
conn.executescript(PAPER_SCHEMA_SQL)
|
||||
conn.commit()
|
||||
finally:
|
||||
conn.close()
|
||||
@@ -0,0 +1,50 @@
|
||||
"""Bonus di asset-invariance per la fitness del GA.
|
||||
|
||||
corr_signal = frazione di entries su asset A che hanno corrispondenza su asset B
|
||||
entro +/-tolerance_bars (default 36 = 3h su 5m TF, vedi paper Pythagoras p. 43).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
|
||||
import pandas as pd
|
||||
|
||||
GA_INVARIANCE_ALPHA = float(os.getenv("GA_INVARIANCE_ALPHA", "0.3"))
|
||||
GA_INVARIANCE_TOLERANCE_BARS = int(os.getenv("GA_INVARIANCE_TOLERANCE_BARS", "36"))
|
||||
|
||||
|
||||
def corr_signal(
|
||||
entries_a: pd.Series,
|
||||
entries_b: pd.Series,
|
||||
tolerance_bars: int = GA_INVARIANCE_TOLERANCE_BARS,
|
||||
) -> float:
|
||||
"""Frazione di entries A con match in B entro +/-tolerance_bars.
|
||||
|
||||
Args:
|
||||
entries_a, entries_b: Series binarie {0,1} sullo stesso index temporale (interi).
|
||||
tolerance_bars: finestra di tolleranza in barre.
|
||||
|
||||
Returns:
|
||||
In [0, 1]. 0 se entries_a non ha alcuna entry o nessun match.
|
||||
"""
|
||||
a_idx = entries_a[entries_a > 0].index.tolist()
|
||||
b_idx = entries_b[entries_b > 0].index.tolist()
|
||||
if not a_idx or not b_idx:
|
||||
return 0.0
|
||||
b_set = set(b_idx)
|
||||
matched = 0
|
||||
for ti in a_idx:
|
||||
for delta in range(-tolerance_bars, tolerance_bars + 1):
|
||||
if (ti + delta) in b_set:
|
||||
matched += 1
|
||||
break
|
||||
return matched / len(a_idx)
|
||||
|
||||
|
||||
def apply_invariance_bonus(
|
||||
base_fitness: float,
|
||||
invariance_score: float,
|
||||
alpha: float = GA_INVARIANCE_ALPHA,
|
||||
) -> float:
|
||||
"""``fitness * (1 + alpha * invariance_score)``."""
|
||||
return base_fitness * (1.0 + alpha * invariance_score)
|
||||
@@ -0,0 +1,215 @@
|
||||
"""Paper-trading data access functions for the strategy_pythagoras dashboard.
|
||||
|
||||
Reads exclusively from strategy_pythagoras_paper.db (paper_trading_* tables)
|
||||
per il paper-trading; le funzioni dedicate ai winner Pythagoras leggono
|
||||
invece dal runs.db del core GA (env ``GA_DB_PATH``), default
|
||||
``state/strategy_pythagoras.db`` via env ``STRATEGY_PYTHAGORAS_DB_PATH`` quando
|
||||
si vuole usare una sotto-tabella locale.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sqlite3
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import pandas as pd # type: ignore[import-untyped]
|
||||
|
||||
|
||||
def _paper_conn(db_path: str | Path) -> sqlite3.Connection:
|
||||
# Cold-start race: GUI può avviarsi prima che il paper writer crei il file.
|
||||
db_path_str = str(db_path)
|
||||
deadline = time.monotonic() + 5.0
|
||||
while True:
|
||||
try:
|
||||
conn = sqlite3.connect(db_path_str, timeout=5.0)
|
||||
conn.row_factory = sqlite3.Row
|
||||
return conn
|
||||
except sqlite3.OperationalError:
|
||||
if time.monotonic() >= deadline:
|
||||
raise
|
||||
time.sleep(1.0)
|
||||
|
||||
|
||||
def paper_runs_df(db_path: str | Path) -> pd.DataFrame:
|
||||
with _paper_conn(db_path) as conn:
|
||||
rows = conn.execute(
|
||||
"SELECT id, name, started_at, stopped_at, status, initial_capital, config_json "
|
||||
"FROM paper_trading_runs ORDER BY started_at DESC"
|
||||
).fetchall()
|
||||
return pd.DataFrame([dict(r) for r in rows])
|
||||
|
||||
|
||||
def paper_equity_df(db_path: str | Path, run_id: str) -> pd.DataFrame:
|
||||
with _paper_conn(db_path) as conn:
|
||||
rows = conn.execute(
|
||||
"SELECT ts, equity, cash, positions_value FROM paper_trading_equity "
|
||||
"WHERE paper_run_id=? ORDER BY ts ASC",
|
||||
(run_id,),
|
||||
).fetchall()
|
||||
return pd.DataFrame([dict(r) for r in rows])
|
||||
|
||||
|
||||
def paper_positions_df(db_path: str | Path, run_id: str) -> pd.DataFrame:
|
||||
with _paper_conn(db_path) as conn:
|
||||
rows = conn.execute(
|
||||
"SELECT symbol, side, qty, entry_price, entry_ts "
|
||||
"FROM paper_trading_positions WHERE paper_run_id=? ORDER BY symbol",
|
||||
(run_id,),
|
||||
).fetchall()
|
||||
return pd.DataFrame([dict(r) for r in rows])
|
||||
|
||||
|
||||
def paper_trades_df(db_path: str | Path, run_id: str, limit: int = 100) -> pd.DataFrame:
|
||||
with _paper_conn(db_path) as conn:
|
||||
rows = conn.execute(
|
||||
"SELECT symbol, side, qty, entry_price, exit_price, entry_ts, exit_ts, pnl, fees "
|
||||
"FROM paper_trading_trades WHERE paper_run_id=? ORDER BY exit_ts DESC LIMIT ?",
|
||||
(run_id, limit),
|
||||
).fetchall()
|
||||
return pd.DataFrame([dict(r) for r in rows])
|
||||
|
||||
|
||||
def paper_ticks_df(db_path: str | Path, run_id: str, limit: int = 50) -> pd.DataFrame:
|
||||
with _paper_conn(db_path) as conn:
|
||||
rows = conn.execute(
|
||||
"SELECT ts, bar_ts, symbol, close_price, signal, action_taken "
|
||||
"FROM paper_trading_ticks WHERE paper_run_id=? ORDER BY ts DESC LIMIT ?",
|
||||
(run_id, limit),
|
||||
).fetchall()
|
||||
return pd.DataFrame([dict(r) for r in rows])
|
||||
|
||||
|
||||
def paper_run_summary(db_path: str | Path, run_id: str) -> dict[str, Any]:
|
||||
"""Aggrega metriche sintetiche per la pagina paper trading."""
|
||||
with _paper_conn(db_path) as conn:
|
||||
run = conn.execute(
|
||||
"SELECT id, name, started_at, stopped_at, status, initial_capital, config_json "
|
||||
"FROM paper_trading_runs WHERE id=?",
|
||||
(run_id,),
|
||||
).fetchone()
|
||||
if run is None:
|
||||
return {}
|
||||
run = dict(run)
|
||||
|
||||
eq_row = conn.execute(
|
||||
"SELECT equity, cash, positions_value, ts FROM paper_trading_equity "
|
||||
"WHERE paper_run_id=? ORDER BY ts DESC LIMIT 1",
|
||||
(run_id,),
|
||||
).fetchone()
|
||||
|
||||
trades_agg = conn.execute(
|
||||
"SELECT COUNT(*) AS n, COALESCE(SUM(pnl),0) AS sum_pnl, "
|
||||
"COALESCE(SUM(fees),0) AS sum_fees FROM paper_trading_trades "
|
||||
"WHERE paper_run_id=?",
|
||||
(run_id,),
|
||||
).fetchone()
|
||||
|
||||
tick_agg = conn.execute(
|
||||
"SELECT COUNT(*) AS n, MAX(ts) AS last_ts FROM paper_trading_ticks "
|
||||
"WHERE paper_run_id=?",
|
||||
(run_id,),
|
||||
).fetchone()
|
||||
|
||||
positions_n = conn.execute(
|
||||
"SELECT COUNT(*) AS n FROM paper_trading_positions WHERE paper_run_id=?",
|
||||
(run_id,),
|
||||
).fetchone()["n"]
|
||||
|
||||
initial = float(run["initial_capital"])
|
||||
current_equity = float(eq_row["equity"]) if eq_row is not None else initial
|
||||
pnl_pct = (current_equity - initial) / initial * 100.0 if initial else 0.0
|
||||
|
||||
return {
|
||||
"id": run["id"],
|
||||
"name": run["name"],
|
||||
"status": run["status"],
|
||||
"started_at": run["started_at"],
|
||||
"stopped_at": run["stopped_at"],
|
||||
"initial_capital": initial,
|
||||
"config": json.loads(run["config_json"]),
|
||||
"current_equity": current_equity,
|
||||
"current_cash": float(eq_row["cash"]) if eq_row is not None else initial,
|
||||
"current_positions_value": float(eq_row["positions_value"]) if eq_row is not None else 0.0,
|
||||
"last_equity_ts": eq_row["ts"] if eq_row is not None else None,
|
||||
"pnl_abs": current_equity - initial,
|
||||
"pnl_pct": pnl_pct,
|
||||
"n_trades": int(trades_agg["n"]),
|
||||
"trades_pnl": float(trades_agg["sum_pnl"]),
|
||||
"trades_fees": float(trades_agg["sum_fees"]),
|
||||
"n_ticks": int(tick_agg["n"]),
|
||||
"last_tick_ts": tick_agg["last_ts"],
|
||||
"n_open_positions": int(positions_n),
|
||||
}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Pythagoras-specific helpers (winners invariance + candle pattern usage)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def load_invariance_metrics(ga_db_path: str) -> "pd.DataFrame":
|
||||
"""Per ogni winner ritorna (genome_id, cognitive_style, fitness, sharpe_btc, sharpe_eth, invariance_score).
|
||||
|
||||
Lo schema atteso e' la tabella ``pythagoras_winners`` creata dal runner
|
||||
``scripts/run_pythagoras_smoke.py`` (Task 6.1).
|
||||
"""
|
||||
import sqlite3
|
||||
|
||||
import pandas as pd
|
||||
|
||||
con = sqlite3.connect(ga_db_path)
|
||||
try:
|
||||
return pd.read_sql_query(
|
||||
"SELECT genome_id, cognitive_style, fitness, sharpe_btc, sharpe_eth, "
|
||||
"invariance_score FROM pythagoras_winners ORDER BY fitness DESC",
|
||||
con,
|
||||
)
|
||||
finally:
|
||||
con.close()
|
||||
|
||||
|
||||
def load_candle_pattern_usage(ga_db_path: str) -> "pd.DataFrame":
|
||||
"""Per ogni winner estrae le sequenze candle_pattern usate (per heatmap)."""
|
||||
import json
|
||||
import sqlite3
|
||||
|
||||
import pandas as pd
|
||||
|
||||
con = sqlite3.connect(ga_db_path)
|
||||
try:
|
||||
df = pd.read_sql_query(
|
||||
"SELECT genome_id, cognitive_style, rules_json FROM pythagoras_winners",
|
||||
con,
|
||||
)
|
||||
finally:
|
||||
con.close()
|
||||
records: list[dict] = []
|
||||
for _, row in df.iterrows():
|
||||
rules = json.loads(row["rules_json"]).get("rules", [])
|
||||
for r in rules:
|
||||
for ind_name, params in _walk_indicators(r["condition"]):
|
||||
if ind_name == "candle_pattern":
|
||||
length = int(params[0])
|
||||
syms = [int(s) for s in params[1: 1 + length]]
|
||||
seq_str = "".join({0: "U", 1: "D", 2: "0"}[s] for s in syms)
|
||||
records.append(
|
||||
{
|
||||
"genome_id": row["genome_id"],
|
||||
"cognitive_style": row["cognitive_style"],
|
||||
"sequence": seq_str,
|
||||
"length": length,
|
||||
}
|
||||
)
|
||||
return pd.DataFrame.from_records(records)
|
||||
|
||||
|
||||
def _walk_indicators(node: dict):
|
||||
"""Yields (indicator_name, params) for every IndicatorNode in the AST."""
|
||||
if "op" in node:
|
||||
for a in node.get("args", []):
|
||||
yield from _walk_indicators(a)
|
||||
elif node.get("kind") == "indicator":
|
||||
yield node["name"], node["params"]
|
||||
@@ -0,0 +1,421 @@
|
||||
"""Strategy Pythagoras Dashboard — paper-trading + GA winners page: /.
|
||||
|
||||
Avvio: ``uv run python -m strategy_pythagoras.frontend.nicegui_app``
|
||||
Default port 8080. Legge il paper DB (``strategy_pythagoras_paper.db``) per il
|
||||
tab ``Paper`` e il GA DB (``strategy_pythagoras.db``) per i tab pythagoras-specifici
|
||||
(Genomes / Patterns / Ratios / Invariance).
|
||||
|
||||
Palette "Neon Trading Dashboard" (ispirata screenshot 2026-05-11):
|
||||
- BG: #0A0A0F (near-black con tinge blu)
|
||||
- Surface: #13131A (card base)
|
||||
- Surface elevata: #1C1C26 (hover/active)
|
||||
- Primary pink: #FF2D87 (highlight key metrics, max fitness)
|
||||
- Secondary cyan: #00D9FF (median, secondary curves)
|
||||
- Accent amber: #FFB800 (warnings, p90)
|
||||
- Success neon green: #00E676, Danger neon red: #FF3D60
|
||||
- Text: #FFFFFF (primary), #7A7A8C (muted)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import pandas as pd # type: ignore[import-untyped]
|
||||
import plotly.graph_objects as go # type: ignore[import-untyped]
|
||||
from nicegui import app, ui
|
||||
|
||||
from strategy_pythagoras.frontend.data import (
|
||||
load_candle_pattern_usage,
|
||||
load_invariance_metrics,
|
||||
paper_equity_df,
|
||||
paper_positions_df,
|
||||
paper_run_summary,
|
||||
paper_runs_df,
|
||||
paper_ticks_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,
|
||||
)
|
||||
|
||||
PAPER_DB_PATH = os.environ.get(
|
||||
"STRATEGY_PYTHAGORAS_PAPER_DB_PATH", "./state/strategy_pythagoras_paper.db"
|
||||
)
|
||||
GA_DB_PATH = os.environ.get(
|
||||
"STRATEGY_PYTHAGORAS_DB_PATH", "./state/strategy_pythagoras.db"
|
||||
)
|
||||
DASHBOARD_ROOT_PATH = os.environ.get("DASHBOARD_ROOT_PATH", "/strategy_pythagoras_gui")
|
||||
REFRESH_INTERVAL_S = 3.0
|
||||
|
||||
|
||||
def _paper_runs_options(only_running: bool = False) -> dict[str, str]:
|
||||
try:
|
||||
runs = paper_runs_df(PAPER_DB_PATH)
|
||||
except Exception:
|
||||
return {}
|
||||
if runs.empty:
|
||||
return {}
|
||||
if only_running:
|
||||
runs = runs[runs["status"] == "running"]
|
||||
if runs.empty:
|
||||
return {}
|
||||
return {
|
||||
row["id"]: f"{row['name']} — {row['status']} ({row['started_at'][:16]})"
|
||||
for _, row in runs.iterrows()
|
||||
}
|
||||
|
||||
|
||||
def _paper_equity_figure(eq_df: Any, initial_capital: float) -> go.Figure:
|
||||
fig = go.Figure()
|
||||
if eq_df is not None and not eq_df.empty:
|
||||
ts = pd.to_datetime(eq_df["ts"])
|
||||
fig.add_trace(
|
||||
go.Scatter(
|
||||
x=ts,
|
||||
y=eq_df["equity"],
|
||||
mode="lines",
|
||||
line={"color": COLOR_PRIMARY, "width": 2},
|
||||
name="equity",
|
||||
)
|
||||
)
|
||||
fig.add_hline(
|
||||
y=initial_capital,
|
||||
line={"color": COLOR_TEXT_MUTED, "width": 1, "dash": "dash"},
|
||||
annotation_text=f"initial ${initial_capital:.0f}",
|
||||
annotation_position="bottom right",
|
||||
annotation_font_color=COLOR_TEXT_MUTED,
|
||||
)
|
||||
fig.update_layout(
|
||||
title=None,
|
||||
paper_bgcolor=COLOR_SURFACE,
|
||||
plot_bgcolor=COLOR_SURFACE,
|
||||
font={"color": COLOR_TEXT, "family": "Inter"},
|
||||
xaxis={"gridcolor": COLOR_SURFACE_2, "title": None},
|
||||
yaxis={"gridcolor": COLOR_SURFACE_2, "title": "Equity ($)"},
|
||||
margin={"l": 60, "r": 20, "t": 10, "b": 40},
|
||||
height=320,
|
||||
showlegend=False,
|
||||
)
|
||||
return fig
|
||||
|
||||
|
||||
def _render_paper_panel() -> None:
|
||||
"""Rende il pannello paper-trading (equivalente alla pagina root di strategy_crypto)."""
|
||||
options = _paper_runs_options()
|
||||
if not options:
|
||||
ui.label("Nessuna paper-trading run nel database.").classes("text-h6 text-warning")
|
||||
ui.label(
|
||||
"Avvia un paper run per popolare strategy_pythagoras_paper.db."
|
||||
).classes("text-caption")
|
||||
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="Paper 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.row().classes("w-full gap-4"):
|
||||
with ui.card().classes("flex-grow metric-card accent-cyan"):
|
||||
ui.label("Equity").classes("text-caption")
|
||||
equity_lbl = ui.label("—").classes("text-h4")
|
||||
with ui.card().classes("flex-grow metric-card accent-purple"):
|
||||
ui.label("P/L cumulato").classes("text-caption")
|
||||
pnl_lbl = ui.label("—").classes("text-h4")
|
||||
with ui.card().classes("flex-grow metric-card accent-amber"):
|
||||
ui.label("Trades chiusi").classes("text-caption")
|
||||
trades_lbl = ui.label("—").classes("text-h4")
|
||||
with ui.card().classes("flex-grow metric-card accent-green"):
|
||||
ui.label("Open / Tick").classes("text-caption")
|
||||
ticks_lbl = ui.label("—").classes("text-h4")
|
||||
|
||||
with ui.row().classes("w-full gap-4 q-mt-md"):
|
||||
started_lbl = ui.label("Started: —")
|
||||
last_tick_lbl = ui.label("Last tick: —")
|
||||
cash_lbl = ui.label("Cash: —")
|
||||
|
||||
ui.separator()
|
||||
ui.label("Equity curve").classes("text-subtitle1 q-mt-md")
|
||||
equity_plot = ui.plotly(_paper_equity_figure(None, 0.0)).classes("w-full")
|
||||
|
||||
ui.separator()
|
||||
ui.label("Open positions").classes("text-subtitle1 q-mt-md")
|
||||
positions_table = ui.table(
|
||||
columns=[
|
||||
{"name": "symbol", "label": "symbol", "field": "symbol"},
|
||||
{"name": "side", "label": "side", "field": "side"},
|
||||
{"name": "qty", "label": "qty", "field": "qty"},
|
||||
{"name": "entry_price", "label": "entry", "field": "entry_price"},
|
||||
{"name": "entry_ts", "label": "entry ts", "field": "entry_ts"},
|
||||
],
|
||||
rows=[],
|
||||
row_key="symbol",
|
||||
).classes("w-full")
|
||||
|
||||
ui.separator()
|
||||
ui.label("Ultimi 30 tick").classes("text-subtitle1 q-mt-md")
|
||||
ticks_table = ui.table(
|
||||
columns=[
|
||||
{"name": "ts", "label": "ts", "field": "ts"},
|
||||
{"name": "symbol", "label": "symbol", "field": "symbol"},
|
||||
{"name": "bar_ts", "label": "bar", "field": "bar_ts"},
|
||||
{"name": "close_price", "label": "close", "field": "close_price"},
|
||||
{"name": "signal", "label": "signal", "field": "signal"},
|
||||
{"name": "action_taken", "label": "action", "field": "action_taken"},
|
||||
],
|
||||
rows=[],
|
||||
row_key="ts",
|
||||
).classes("w-full")
|
||||
|
||||
ui.separator()
|
||||
ui.label("Trades chiusi (ultimi 50)").classes("text-subtitle1 q-mt-md")
|
||||
trades_table = ui.table(
|
||||
columns=[
|
||||
{"name": "exit_ts", "label": "exit ts", "field": "exit_ts"},
|
||||
{"name": "symbol", "label": "symbol", "field": "symbol"},
|
||||
{"name": "side", "label": "side", "field": "side"},
|
||||
{"name": "qty", "label": "qty", "field": "qty"},
|
||||
{"name": "entry_price", "label": "entry", "field": "entry_price"},
|
||||
{"name": "exit_price", "label": "exit", "field": "exit_price"},
|
||||
{"name": "pnl", "label": "pnl", "field": "pnl"},
|
||||
{"name": "fees", "label": "fees", "field": "fees"},
|
||||
],
|
||||
rows=[],
|
||||
row_key="exit_ts",
|
||||
).classes("w-full")
|
||||
|
||||
def refresh() -> None:
|
||||
run_id = select.value
|
||||
if not run_id:
|
||||
return
|
||||
try:
|
||||
summary = paper_run_summary(PAPER_DB_PATH, run_id)
|
||||
eq = paper_equity_df(PAPER_DB_PATH, run_id)
|
||||
positions = paper_positions_df(PAPER_DB_PATH, run_id)
|
||||
ticks = paper_ticks_df(PAPER_DB_PATH, run_id, limit=30)
|
||||
trades = paper_trades_df(PAPER_DB_PATH, run_id, limit=50)
|
||||
except Exception as e: # noqa: BLE001
|
||||
ui.notify(f"Errore: {e}", type="negative")
|
||||
return
|
||||
|
||||
text, color = _STATUS_BADGE.get(summary["status"], (summary["status"], "primary"))
|
||||
status_badge.text = text
|
||||
status_badge.props(f"color={color}")
|
||||
|
||||
equity_lbl.text = f"${summary['current_equity']:.2f}"
|
||||
pnl_lbl.text = f"{summary['pnl_pct']:+.2f}%"
|
||||
trades_lbl.text = str(summary["n_trades"])
|
||||
ticks_lbl.text = f"{summary['n_open_positions']} / {summary['n_ticks']}"
|
||||
|
||||
started_lbl.text = f"Started: {summary['started_at']}"
|
||||
last_tick_lbl.text = f"Last tick: {summary['last_tick_ts'] or '—'}"
|
||||
cash_lbl.text = (
|
||||
f"Cash: ${summary['current_cash']:.2f} | "
|
||||
f"Pos value: ${summary['current_positions_value']:.2f}"
|
||||
)
|
||||
|
||||
equity_plot.update_figure(_paper_equity_figure(eq, summary["initial_capital"]))
|
||||
|
||||
positions_table.rows = (
|
||||
[
|
||||
{col: (round(v, 6) if isinstance(v, float) else v) for col, v in row.items()}
|
||||
for _, row in positions.iterrows()
|
||||
]
|
||||
if not positions.empty
|
||||
else []
|
||||
)
|
||||
positions_table.update()
|
||||
|
||||
ticks_table.rows = (
|
||||
[
|
||||
{col: (round(v, 6) if isinstance(v, float) else v) for col, v in row.items()}
|
||||
for _, row in ticks.iterrows()
|
||||
]
|
||||
if not ticks.empty
|
||||
else []
|
||||
)
|
||||
ticks_table.update()
|
||||
|
||||
trades_table.rows = (
|
||||
[
|
||||
{col: (round(v, 6) if isinstance(v, float) else v) for col, v in row.items()}
|
||||
for _, row in trades.iterrows()
|
||||
]
|
||||
if not trades.empty
|
||||
else []
|
||||
)
|
||||
trades_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("/")
|
||||
def index() -> None:
|
||||
_apply_theme()
|
||||
_build_header(
|
||||
active="/",
|
||||
brand_subtitle="Strategy Pythagoras",
|
||||
nav_items=[("/", "Dashboard")],
|
||||
db_label=f"⛁ {Path(GA_DB_PATH).resolve().name}",
|
||||
)
|
||||
|
||||
with ui.tabs() as tabs:
|
||||
t_genomes = ui.tab("Genomes")
|
||||
t_patterns = ui.tab("Patterns")
|
||||
t_ratios = ui.tab("Ratios")
|
||||
t_invariance = ui.tab("Invariance")
|
||||
t_paper = ui.tab("Paper")
|
||||
|
||||
with ui.tab_panels(tabs, value=t_genomes).classes("w-full"):
|
||||
with ui.tab_panel(t_genomes):
|
||||
ui.label("GA Winners (post pythagoras-smoke-001)").classes("text-h6")
|
||||
try:
|
||||
df = load_invariance_metrics(GA_DB_PATH)
|
||||
if df.empty:
|
||||
ui.label(
|
||||
"No winners yet. Run scripts/run_pythagoras_smoke.py first."
|
||||
).classes("text-warning")
|
||||
else:
|
||||
ui.table(
|
||||
rows=df.to_dict("records"),
|
||||
columns=[
|
||||
{"name": "genome_id", "label": "Genome ID", "field": "genome_id"},
|
||||
{
|
||||
"name": "cognitive_style",
|
||||
"label": "Style",
|
||||
"field": "cognitive_style",
|
||||
},
|
||||
{"name": "fitness", "label": "Fitness", "field": "fitness"},
|
||||
{"name": "sharpe_btc", "label": "Sharpe BTC", "field": "sharpe_btc"},
|
||||
{"name": "sharpe_eth", "label": "Sharpe ETH", "field": "sharpe_eth"},
|
||||
{
|
||||
"name": "invariance_score",
|
||||
"label": "Invariance",
|
||||
"field": "invariance_score",
|
||||
},
|
||||
],
|
||||
pagination=10,
|
||||
).classes("w-full")
|
||||
except Exception as e: # noqa: BLE001
|
||||
ui.label(f"DB not ready: {e}").classes("text-warning")
|
||||
|
||||
with ui.tab_panel(t_patterns):
|
||||
ui.label(
|
||||
"Candle pattern sequences emerged (per cognitive style)"
|
||||
).classes("text-h6")
|
||||
try:
|
||||
df_pat = load_candle_pattern_usage(GA_DB_PATH)
|
||||
if df_pat.empty:
|
||||
ui.label(
|
||||
"No patterns yet. Run scripts/run_pythagoras_smoke.py first."
|
||||
).classes("text-warning")
|
||||
else:
|
||||
grouped = (
|
||||
df_pat.groupby(["cognitive_style", "sequence"])
|
||||
.size()
|
||||
.reset_index(name="count")
|
||||
)
|
||||
grouped = grouped.sort_values("count", ascending=False).head(50)
|
||||
ui.table(
|
||||
rows=grouped.to_dict("records"),
|
||||
columns=[
|
||||
{
|
||||
"name": "cognitive_style",
|
||||
"label": "Style",
|
||||
"field": "cognitive_style",
|
||||
},
|
||||
{
|
||||
"name": "sequence",
|
||||
"label": "Sequence (U/D/0)",
|
||||
"field": "sequence",
|
||||
},
|
||||
{"name": "count", "label": "Count", "field": "count"},
|
||||
],
|
||||
pagination=15,
|
||||
).classes("w-full")
|
||||
except Exception as e: # noqa: BLE001
|
||||
ui.label(f"DB not ready: {e}").classes("text-warning")
|
||||
|
||||
with ui.tab_panel(t_ratios):
|
||||
ui.label(
|
||||
"Pythagorean ratio literals — distance from universal constants"
|
||||
).classes("text-h6")
|
||||
try:
|
||||
df = load_invariance_metrics(GA_DB_PATH)
|
||||
if df.empty:
|
||||
ui.label("No winners yet.").classes("text-warning")
|
||||
else:
|
||||
ui.label(f"Total winners: {len(df)}").classes("text-body2")
|
||||
ui.label(
|
||||
"(Ratio literal histogram available after GA run produces "
|
||||
"pythagorean_ratio entries.)"
|
||||
).classes("text-caption")
|
||||
except Exception as e: # noqa: BLE001
|
||||
ui.label(f"DB not ready: {e}").classes("text-warning")
|
||||
|
||||
with ui.tab_panel(t_invariance):
|
||||
ui.label(
|
||||
"Cross-asset invariance: Sharpe BTC vs Sharpe ETH"
|
||||
).classes("text-h6")
|
||||
try:
|
||||
df = load_invariance_metrics(GA_DB_PATH)
|
||||
if df.empty:
|
||||
ui.label("No winners yet.").classes("text-warning")
|
||||
else:
|
||||
import plotly.express as px # type: ignore[import-untyped]
|
||||
|
||||
fig = px.scatter(
|
||||
df,
|
||||
x="sharpe_btc",
|
||||
y="sharpe_eth",
|
||||
color="invariance_score",
|
||||
hover_data=["genome_id", "cognitive_style", "fitness"],
|
||||
title="Sharpe BTC vs Sharpe ETH (color = invariance_score)",
|
||||
)
|
||||
ui.plotly(fig).classes("w-full")
|
||||
except Exception as e: # noqa: BLE001
|
||||
ui.label(f"DB not ready: {e}").classes("text-warning")
|
||||
|
||||
with ui.tab_panel(t_paper):
|
||||
_render_paper_panel()
|
||||
|
||||
|
||||
def main() -> None:
|
||||
app.on_startup(
|
||||
lambda: print(
|
||||
f"GA DB: {Path(GA_DB_PATH).resolve()} | "
|
||||
f"Paper DB: {Path(PAPER_DB_PATH).resolve()} | "
|
||||
f"root_path: {DASHBOARD_ROOT_PATH or '/'}"
|
||||
)
|
||||
)
|
||||
ui.run(
|
||||
host="0.0.0.0",
|
||||
port=int(os.environ.get("SWARM_DASHBOARD_PORT", "8080")),
|
||||
title="Strategy Pythagoras Dashboard",
|
||||
reload=False,
|
||||
show=False,
|
||||
dark=True,
|
||||
root_path=DASHBOARD_ROOT_PATH,
|
||||
)
|
||||
|
||||
|
||||
if __name__ in {"__main__", "__mp_main__"}:
|
||||
main()
|
||||
@@ -0,0 +1,70 @@
|
||||
"""Indicatori candle Pythagoras.
|
||||
|
||||
Vincoli grammar: ``IndicatorNode.params`` e' sempre ``list[float]``. Quindi:
|
||||
- candle_pattern: params = [length, sym0, sym1, ..., sym_{length-1}]
|
||||
length in [3,12]; sym in {0=U, 1=D, 2=doji}
|
||||
- pythagorean_ratio: params = [lookback] lookback in [12,200]
|
||||
- fractal_mirror: params = [k, axis_int] k in [3,12]; axis_int=0(h) 1(v)
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
_DOJI_THRESHOLD = 0.001
|
||||
|
||||
|
||||
def _symbol_series(df: pd.DataFrame) -> pd.Series:
|
||||
"""Mappa ogni candela in {0=U, 1=D, 2=doji}."""
|
||||
close = df["close"]
|
||||
open_ = df["open"]
|
||||
rel = (close - open_).abs() / open_.replace(0, np.nan)
|
||||
sym = np.where(close > open_, 0, np.where(close < open_, 1, 2))
|
||||
sym = np.where(rel.values < _DOJI_THRESHOLD, 2, sym)
|
||||
return pd.Series(sym, index=df.index, dtype="int64")
|
||||
|
||||
|
||||
def candle_pattern(df: pd.DataFrame, params: list[float]) -> pd.Series:
|
||||
"""1.0 se le ultime ``length`` candele matchano la sequenza, 0.0 altrimenti."""
|
||||
length = int(params[0])
|
||||
target = [int(s) for s in params[1:1 + length]]
|
||||
syms = _symbol_series(df)
|
||||
out = pd.Series(0.0, index=df.index, dtype="float64")
|
||||
if len(syms) < length:
|
||||
return out
|
||||
arr = syms.values
|
||||
target_arr = np.array(target, dtype=arr.dtype)
|
||||
for i in range(length - 1, len(arr)):
|
||||
if np.array_equal(arr[i - length + 1: i + 1], target_arr):
|
||||
out.iat[i] = 1.0
|
||||
return out
|
||||
|
||||
|
||||
def pythagorean_ratio(df: pd.DataFrame, params: list[float]) -> pd.Series:
|
||||
"""``max(close[-lookback:]) / min(close[-lookback:])`` rolling."""
|
||||
lookback = int(params[0])
|
||||
close = df["close"]
|
||||
hi = close.rolling(lookback, min_periods=1).max()
|
||||
lo = close.rolling(lookback, min_periods=1).min().replace(0, np.nan)
|
||||
return (hi / lo).fillna(1.0)
|
||||
|
||||
|
||||
def fractal_mirror(df: pd.DataFrame, params: list[float]) -> pd.Series:
|
||||
"""Correlation tra close[-k:] e suo mirror su asse axis."""
|
||||
k = int(params[0])
|
||||
axis_int = int(params[1])
|
||||
close = df["close"].values
|
||||
out = np.full(len(close), 0.0)
|
||||
for i in range(k - 1, len(close)):
|
||||
window = close[i - k + 1: i + 1]
|
||||
if axis_int == 0: # h: mirror temporale
|
||||
mirror = window[::-1]
|
||||
else: # v: mirror prezzo
|
||||
mirror = window.max() - (window - window.min())
|
||||
std_w = window.std()
|
||||
std_m = mirror.std()
|
||||
if std_w < 1e-12 or std_m < 1e-12:
|
||||
out[i] = 0.0
|
||||
else:
|
||||
out[i] = float(np.corrcoef(window, mirror)[0, 1])
|
||||
return pd.Series(out, index=df.index, dtype="float64")
|
||||
@@ -0,0 +1,53 @@
|
||||
{
|
||||
"_comment": "Stili cognitivi del GA per strategy_pythagoras. Dominio: pattern frattali ricorrenti su mercati crypto secondo il framework Pythagoras-Malanga (Fourier + frattali H-C + Evideon).",
|
||||
"_schema": "3.2",
|
||||
"_changelog": "v1.0 - Initial release. Schema clonato da strategy_crypto v3.2 con contenuto Pythagoras-aligned.",
|
||||
"_focus_metrics_design": "Le focus_metrics sono ENFASI per la lente, non filtri. 4 per stile. Devono includere almeno 1 dei 3 indicatori Pythagoras (candle_pattern, pythagorean_ratio, fractal_mirror).",
|
||||
"_design_invariants": "(1) ASCII-safe; (2) Archetipo dominante: <metafora> come ancora; (3) Lookback range esplicito; (4) Prima frase 'Il mercato e ...'; (5) Lunghezza directive 800-950 char.",
|
||||
"_param_encoding_note": "Promosso a campo top-level 'custom_indicators_spec' (v3.2). Vedi sotto.",
|
||||
|
||||
"custom_indicators_spec": "Indicatori Pythagoras (oltre a sma/sma_pct/rsi/atr/atr_pct/realized_vol/macd/macd_pct gia documentati sopra). REGOLA CRITICA: params accetta SOLO numeri float, MAI stringhe e MAI altri nodi.\n\n {\"kind\": \"indicator\", \"name\": \"candle_pattern\", \"params\": [length, sym0, sym1, ..., sym_{length-1}]}\n length: int in [3,12] (numero di candele consecutive che devono matchare)\n sym_i: int in {0, 1, 2} (0=U up close>open, 1=D down close<open, 2=doji)\n Esempio: pattern U-D-U di 3 candele = params=[3, 0, 1, 0]\n Esempio: pattern D-D-D-U (reversal classico) = params=[4, 1, 1, 1, 0]\n Output: 1.0 se le ultime length candele matchano, 0.0 altrimenti. Confronta solo con literal 0.0 o 1.0.\n\n {\"kind\": \"indicator\", \"name\": \"pythagorean_ratio\", \"params\": [lookback]}\n lookback: int in [12,200] (finestra rolling per max/min close)\n Esempio: ratio su finestra 89 (Fibonacci) = params=[89]\n Output: max(close[-lookback:]) / min(close[-lookback:]), adimensionale >= 1.0.\n Confronta con literal vicini a phi=1.618, 1/phi=0.618 (in realta usa 1.618 perche ratio >= 1), sqrt2=1.414, pi/2=1.571, e=2.718.\n\n {\"kind\": \"indicator\", \"name\": \"fractal_mirror\", \"params\": [k, axis_int]}\n k: int in [3,12] (lunghezza finestra di confronto)\n axis_int: int in {0, 1} (0=h mirror temporale, 1=v mirror prezzo)\n Esempio: mirror temporale su 8 candele = params=[8, 0]\n Esempio: mirror prezzo su 6 candele = params=[6, 1]\n Output: correlation di Pearson in [-1.0, 1.0] tra finestra e suo mirror. Confronta con literal in (-1, 1), tipicamente |0.5|-|0.8|.\n\nESEMPI di confronti CORRETTI:\n candle_pattern di 3 candele U-D-U attiva: {\"op\": \"eq\", \"args\": [{\"kind\":\"indicator\",\"name\":\"candle_pattern\",\"params\":[3,0,1,0]}, {\"kind\":\"literal\",\"value\":1.0}]}\n ratio phi entro 0.5%: {\"op\": \"and\", \"args\": [{\"op\":\"gt\",\"args\":[{\"kind\":\"indicator\",\"name\":\"pythagorean_ratio\",\"params\":[89]},{\"kind\":\"literal\",\"value\":1.610}]}, {\"op\":\"lt\",\"args\":[{\"kind\":\"indicator\",\"name\":\"pythagorean_ratio\",\"params\":[89]},{\"kind\":\"literal\",\"value\":1.626}]}]}\n mirror temporale forte: {\"op\": \"gt\", \"args\": [{\"kind\":\"indicator\",\"name\":\"fractal_mirror\",\"params\":[8,0]}, {\"kind\":\"literal\",\"value\":0.7}]}\n\nESEMPI ERRATI (dead branch o validation error):\n candle_pattern con stringa: params=[\"UDU\"] (errato: params devono essere numeri)\n candle_pattern: params=[3, \"U\", \"D\", \"U\"] (errato: usa codici 0/1/2)\n pythagorean_ratio: params=[\"close\"] (errato: params devono essere numeri, non nomi feature)\n pythagorean_ratio: params=[5] (errato: lookback < 12)\n pythagorean_ratio: params=[89, 1.618] (errato: arity 1, non 2)\n fractal_mirror: params=[0, 1] (errato: k < 3)\n fractal_mirror: params=[100, 0] (errato: k > 12)\n fractal_mirror: params=[8] (errato: arity 2, manca axis_int)",
|
||||
|
||||
"agent_role": "Sei un agente generatore di ipotesi di trading quantitativo per un sistema swarm coevolutivo che cerca pattern frattali ricorrenti sui mercati crypto secondo il framework Pythagoras-Malanga (frattali H-C, trasformata di Fourier, geometria Evideon). Sei parte di una popolazione che esplora collettivamente lo spazio dei pattern: la diversita delle ipotesi e un asset critico. Preferisci esplorare territori meno ovvi rispetto a quelli che la tua lente cognitiva renderebbe predicibili. La strategia che produci deve essere riconoscibile come emanata dal tuo stile.",
|
||||
|
||||
"pattern_guidance": "Forme da cercare:\n - Sequenze candle 3-12 lunghezza in alfabeto {U,D,doji} via candle_pattern\n - Ratios di prezzo vicini a 1.618 (phi), 0.618 (1/phi), 1.414 (sqrt2), 1.571 (pi/2), 2.718 (e) via pythagorean_ratio\n - Mirror H/V come proiezioni via fractal_mirror\n - Pattern composti: lunghi come concatenazione di corti (Pattern 13 = Pattern 3 + Pattern 11 nel paper)\n - Cicli ricorrenti via Poincare: stesso pattern firato a distanze regolari\nCerca pattern che si REPLICANO su BTC e ETH al ~stesso timestamp (entro 36 barre = 3h su 5m TF).",
|
||||
|
||||
"instruction": "Genera una strategia che cerchi pattern frattali ricorrenti coerenti col framework Pythagoras-Malanga, riconoscibile come emanata dal tuo stile.",
|
||||
|
||||
"domain_warnings": "Crypto trada 24/7 senza CME gap. I numeri sacri (Solfeggio 396-417-528-639-741-852 Hz, 137.0359, 121.449, 161.86) sono prior teorici, NON leggi: usali come scale candidate per literal, non come dogma. Il paper Pythagoras e' esplicitamente non-falsificabile (cita 'consapevolezza del trader' come jolly): il backtest e' l'unico arbitro. La tolleranza +/-3h del paper su 5m TF = +/-36 barre per il bonus di asset-invariance.",
|
||||
|
||||
"anti_patterns": "Evita: (1) sequenza candle_pattern con piu di 7 simboli vincolati (overfitting); (2) pythagorean_ratio con tolleranza > 2% sui literal (numerologia spuria); (3) fractal_mirror con k = lookback_window (tautologico, sempre prossimo a 1.0); (4) letterali con piu di 4 decimali (es. 1.6180339 -> usa 1.618); (5) piu di 4 condizioni in AND; (6) crossover tra indicatori dello stesso tipo con lookback vicini (chattering); (7) time_in_market > 80% (leveraged buy&hold camuffato).",
|
||||
|
||||
"output_priorities": "Trade-off: (1) coerenza con la lente cognitiva: la strategia deve essere riconoscibile come emanata dal tuo stile (es. pythagorean usa ratios, candle_grammarian sequenze esplicite); (2) asset-invariance: segnali che attivano sia su BTC che su ETH entro 36 barre (e' il bonus della fitness); (3) selettivita: poche entry forti, alto SNR; (4) composizionalita: pattern lunghi come somma di corti; (5) robustezza vs random baseline (3-sigma richiesto dal skeptic_quant).",
|
||||
|
||||
"styles": {
|
||||
"pythagorean": {
|
||||
"directive": "Il mercato e un'armonia di ratios sacri: la sezione aurea (phi=1.618 e 1/phi=0.618), la radice di due (1.414), pi mezzi (1.571) ed e mezzi (1.359) strutturano i livelli di supporto, resistenza e target. Usa pythagorean_ratio con lookback Fibonacci (89, 144) per misurare l'estensione del range recente e cerca punti dove esso e' prossimo a phi entro 0.5%: indica un swing maturo che tende all'inversione armonica. Combina con candle_pattern di 3-5 candele per timing. Evita di chiudere troppo presto: la potenza di un ratio sacro si manifesta quando il prezzo passa il livello e poi torna a testarlo. Mai usare letterali con piu di 4 decimali: l'armonia non e' nei decimali ma nella struttura. Lookback consigliato: 89-144. Archetipo dominante: musicista che ascolta gli accordi nascosti del mercato.",
|
||||
"focus_metrics": ["pythagorean_ratio", "candle_pattern", "sma_pct", "realized_vol"]
|
||||
},
|
||||
"fractal_geometer": {
|
||||
"directive": "Il mercato e autosimilare: una sequenza di 3 candele si ripete dilatata su 6, 12 candele e oltre. La tua firma e' detectare la stessa struttura a scale diverse. Usa candle_pattern di lunghezza 3 per identificare l'unita atomica, poi cerca conferma con fractal_mirror su finestra 2x o 3x: se la correlation e' alta significa che la scala maggiore replica la struttura. Pythagorean_ratio con lookback grande (>100) misura l'ampiezza globale del frattale e i suoi target di proiezione. Atr_pct conferma che la volatilita corrente e coerente con la scala. Evita pattern di lunghezza > 6: i frattali complessi vanno composti, non vincolati esplicitamente. Mai usare fractal_mirror con k uguale al lookback_window. Lookback consigliato: 48-144. Archetipo dominante: geometra che misura la dimensione di Hausdorff del prezzo.",
|
||||
"focus_metrics": ["candle_pattern", "fractal_mirror", "atr_pct", "pythagorean_ratio"]
|
||||
},
|
||||
"fourier_analyst": {
|
||||
"directive": "Il mercato e somma di seni: una funzione complessa scomposta in frequenze armoniche distinte. Il tuo compito e' isolare le componenti ricorrenti dominanti. Usa sma_pct su 3 lookback diversi (es. 30, 60, 120) come proxy delle componenti a bassa frequenza; quando 2 su 3 si allineano nella stessa direzione, la frequenza dominante e' chiara e attiva l'entry. Realized_vol misura l'energia totale del segnale; un calo improvviso indica trasformazione di fase. Candle_pattern di 4-6 candele identifica l'inviluppo locale del segnale composito. Cerca pattern dove il presente e' la proiezione del passato dopo una traslazione temporale costante (mappa di Poincare). Evita oscillatori dello stesso tipo a lookback vicini: producono chattering inutile. Lookback consigliato: 60-200. Archetipo dominante: ingegnere del segnale che ricostruisce la portante armonica nascosta.",
|
||||
"focus_metrics": ["sma_pct", "realized_vol", "candle_pattern", "atr"]
|
||||
},
|
||||
"evideonic_projector": {
|
||||
"directive": "Il mercato e il presente proiettato dal passato via due operazioni geometriche: mirror H (inversione temporale) e mirror V (inversione prezzo), poi scalato per phi, 1/phi, sqrt2. Il tuo segnale principale e' fractal_mirror su entrambi gli assi: quando l'h mirror su 6-12 candele e' altamente positivo (>0.7), il pattern recente e' un riflesso temporale di un pattern precedente, segnale di prosecuzione armonica; quando il v mirror e' positivo, e' un riflesso direzionale, segnale di inversione imminente. Usa pythagorean_ratio per fissare i target di proiezione (entry moltiplicata per phi). Candle_pattern serve da gate per filtrare il contesto. Evita target con ratio > 3 (zona di rumore, fuori scala phi). Lookback consigliato: 24-96. Archetipo dominante: proiettore evideonico che riflette ogni evento nei suoi quattro alter-ego speculari.",
|
||||
"focus_metrics": ["fractal_mirror", "pythagorean_ratio", "candle_pattern", "sma_pct"]
|
||||
},
|
||||
"candle_grammarian": {
|
||||
"directive": "Il mercato e una lingua di tre simboli atomici: U (close>open), D (close<open), doji (close circa open). Ogni parola di 3-12 lettere e' una frase compiuta con significato direzionale. Il tuo dominio e' la sintassi diretta: usa candle_pattern come segnale principale, con sequenze di lunghezza 3-5 per la decisione di entry. Cerca pattern dove l'ultima candela e' U dopo una serie 'DD' o '0D' (reversal classico) o sequenze 'UU0U' che indicano accumulazione con pausa. Volume conferma la pronuncia del pattern: alto volume = pattern ben articolato. Atr modula la dimensione di stop e target. Combina al massimo due candle_pattern indipendenti in AND, mai di piu. Mai pattern di lunghezza > 7 (frasi troppo lunghe = overfitting). Lookback consigliato: 12-48. Archetipo dominante: grammatico che decifra la sintassi delle candele.",
|
||||
"focus_metrics": ["candle_pattern", "volume", "atr", "realized_vol"]
|
||||
},
|
||||
"recurrence_theorist": {
|
||||
"directive": "Il mercato e regolato dal teorema di Poincare: ogni configurazione torna prima o poi vicino a se stessa nel tempo. Il tuo lavoro e' trovare il pattern di oggi che firo' anche ieri o la settimana scorsa a distanza k regolare. Usa candle_pattern su 5-8 candele come firma riconoscibile, poi fractal_mirror con axis=0 (h, mirror temporale) su lookback grande (100-200) per misurare quanto la struttura corrente assomigli a quella passata invertita: se >0.5, il ciclo e' nella fase di ripetizione attiva. Pythagorean_ratio fornisce i target di estensione del ciclo. Sma_pct su lookback medio conferma il regime macro coerente. Evita pattern troppo corti (3 candele): troppo poco distinti per ricorrenza affidabile. Lookback consigliato: 100-200. Archetipo dominante: archeologo del prezzo che cerca repliche di antichi pattern dimenticati.",
|
||||
"focus_metrics": ["candle_pattern", "fractal_mirror", "pythagorean_ratio", "sma_pct"]
|
||||
},
|
||||
"skeptic_quant": {
|
||||
"directive": "Il mercato e rumore con piccoli edge statistici nascosti tra il caos. Tu sei l'anticorpo del sistema: non fidarti mai dei pattern senza significativita 3-sigma vs random baseline. Usa realized_vol e atr_pct come gate di regime (entra solo in regimi di volatilita media, percentile 30-70). Sma_pct conferma la direzione del trend macro corrente. Candle_pattern di 3-4 candele puo' essere usato, ma solo come conferma di un setup gia validato da volatilita e trend: mai come segnale primario isolato. Vincolo duro: max 3 condizioni in AND (poche e robuste); literal con almeno il 20% di margine di sicurezza tra entry ed exit. Nessun pythagorean_ratio (numerologia spuria) ne fractal_mirror (tautologici nella tua lente scettica). Lookback: 60-150. Archetipo dominante: scettico quantitativo che falsifica prima di confermare.",
|
||||
"focus_metrics": ["realized_vol", "atr_pct", "sma_pct", "candle_pattern"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
"""Bonus invariance: pattern che firano simultaneamente su 2 asset entro tolleranza."""
|
||||
from __future__ import annotations
|
||||
|
||||
import pandas as pd
|
||||
import pytest
|
||||
|
||||
from strategy_pythagoras.fitness_invariance import (
|
||||
apply_invariance_bonus,
|
||||
corr_signal,
|
||||
)
|
||||
|
||||
|
||||
def test_corr_signal_perfect_alignment() -> None:
|
||||
entries_btc = pd.Series([0, 1, 0, 1, 0], index=[0, 1, 2, 3, 4])
|
||||
entries_eth = pd.Series([0, 1, 0, 1, 0], index=[0, 1, 2, 3, 4])
|
||||
assert corr_signal(entries_btc, entries_eth, tolerance_bars=0) == pytest.approx(1.0)
|
||||
|
||||
|
||||
def test_corr_signal_no_overlap() -> None:
|
||||
entries_btc = pd.Series([0, 1, 0, 0, 0], index=[0, 1, 2, 3, 4])
|
||||
entries_eth = pd.Series([0, 0, 0, 0, 1], index=[0, 1, 2, 3, 4])
|
||||
assert corr_signal(entries_btc, entries_eth, tolerance_bars=0) == pytest.approx(0.0)
|
||||
|
||||
|
||||
def test_corr_signal_within_tolerance() -> None:
|
||||
# entry su BTC a t=1, su ETH a t=3, tolerance=2 -> match
|
||||
entries_btc = pd.Series([0, 1, 0, 0, 0], index=[0, 1, 2, 3, 4])
|
||||
entries_eth = pd.Series([0, 0, 0, 1, 0], index=[0, 1, 2, 3, 4])
|
||||
assert corr_signal(entries_btc, entries_eth, tolerance_bars=2) == pytest.approx(1.0)
|
||||
|
||||
|
||||
def test_apply_invariance_bonus_increases_fitness() -> None:
|
||||
assert apply_invariance_bonus(1.0, 0.5, 0.3) == pytest.approx(1.0 * (1.0 + 0.3 * 0.5))
|
||||
|
||||
|
||||
def test_apply_invariance_bonus_alpha_zero() -> None:
|
||||
assert apply_invariance_bonus(1.0, 0.7, 0.0) == pytest.approx(1.0)
|
||||
|
||||
|
||||
def test_corr_signal_zero_entries() -> None:
|
||||
entries_btc = pd.Series([0, 0, 0], index=[0, 1, 2])
|
||||
entries_eth = pd.Series([0, 0, 0], index=[0, 1, 2])
|
||||
assert corr_signal(entries_btc, entries_eth, tolerance_bars=0) == 0.0
|
||||
@@ -0,0 +1,82 @@
|
||||
"""Unit-test dei 3 indicatori Pythagoras."""
|
||||
from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import pytest
|
||||
|
||||
from strategy_pythagoras.indicators import (
|
||||
candle_pattern,
|
||||
fractal_mirror,
|
||||
pythagorean_ratio,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def ohlcv_30() -> pd.DataFrame:
|
||||
"""30 candele sintetiche: pattern alternato U,D,U,D,..."""
|
||||
n = 30
|
||||
close = np.array([100.0 + i for i in range(n)]) # monotone
|
||||
open_ = close - np.tile([1.0, -1.0], n // 2) # U,D,U,D,...
|
||||
return pd.DataFrame({"open": open_, "high": close + 0.5, "low": open_ - 0.5, "close": close, "volume": [1.0] * n})
|
||||
|
||||
|
||||
# -- candle_pattern -----------------------------------------------------------
|
||||
|
||||
def test_candle_pattern_matches_recent(ohlcv_30: pd.DataFrame) -> None:
|
||||
# Verifica che il symbol mapping U=0,D=1,doji=2 lavori correttamente.
|
||||
# Con la fixture: indices pari sono U (close>open), dispari sono D (close<open).
|
||||
# ultime 3 candele (idx 27,28,29) -> D,U,D
|
||||
out = candle_pattern(ohlcv_30, [3, 1, 0, 1]) # D, U, D
|
||||
assert out.iloc[-1] == 1.0
|
||||
out2 = candle_pattern(ohlcv_30, [3, 0, 0, 0]) # U, U, U: no match
|
||||
assert out2.iloc[-1] == 0.0
|
||||
|
||||
|
||||
def test_candle_pattern_zero_for_short_history(ohlcv_30: pd.DataFrame) -> None:
|
||||
out = candle_pattern(ohlcv_30, [3, 0, 0, 0])
|
||||
assert out.iloc[0] == 0.0
|
||||
assert out.iloc[1] == 0.0
|
||||
|
||||
|
||||
def test_candle_pattern_doji_symbol(ohlcv_30: pd.DataFrame) -> None:
|
||||
df = ohlcv_30.copy()
|
||||
# forza la candela [-1] doji: |close-open|/open < 0.001
|
||||
df.loc[df.index[-1], "open"] = df["close"].iloc[-1] * (1 - 1e-5)
|
||||
# ultime 3 dopo modifica: D (idx 27), U (idx 28), doji (idx 29)
|
||||
out = candle_pattern(df, [3, 1, 0, 2])
|
||||
assert out.iloc[-1] == 1.0
|
||||
|
||||
|
||||
# -- pythagorean_ratio --------------------------------------------------------
|
||||
|
||||
def test_pythagorean_ratio_basic(ohlcv_30: pd.DataFrame) -> None:
|
||||
out = pythagorean_ratio(ohlcv_30, [12])
|
||||
# ultimi 12 close: 118..129 -> max/min = 129/118
|
||||
expected = 129.0 / 118.0
|
||||
assert abs(out.iloc[-1] - expected) < 1e-9
|
||||
|
||||
|
||||
def test_pythagorean_ratio_no_lookahead(ohlcv_30: pd.DataFrame) -> None:
|
||||
out = pythagorean_ratio(ohlcv_30, [12])
|
||||
out0 = pythagorean_ratio(ohlcv_30.iloc[:13], [12])
|
||||
assert abs(out.iloc[12] - out0.iloc[-1]) < 1e-9
|
||||
|
||||
|
||||
# -- fractal_mirror -----------------------------------------------------------
|
||||
|
||||
def test_fractal_mirror_h_pattern_inverted(ohlcv_30: pd.DataFrame) -> None:
|
||||
# mirror temporale: correlazione tra close[-k:] e close[-k:][::-1]
|
||||
# Per close monotono, correlation(seq, seq_reversed) = -1
|
||||
out = fractal_mirror(ohlcv_30, [6, 0]) # axis=0 (h)
|
||||
assert out.iloc[-1] < -0.99
|
||||
|
||||
|
||||
def test_fractal_mirror_v_axis(ohlcv_30: pd.DataFrame) -> None:
|
||||
out = fractal_mirror(ohlcv_30, [6, 1])
|
||||
assert out.iloc[-1] < -0.99
|
||||
|
||||
|
||||
def test_fractal_mirror_clamps_initial(ohlcv_30: pd.DataFrame) -> None:
|
||||
out = fractal_mirror(ohlcv_30, [6, 0])
|
||||
assert len(out) == len(ohlcv_30)
|
||||
@@ -0,0 +1,67 @@
|
||||
"""prompts.json valido, 7 stili Pythagoras-aligned, schema v3.2."""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from importlib.resources import files
|
||||
|
||||
import pytest
|
||||
|
||||
EXPECTED_STYLES = {
|
||||
"pythagorean", "fractal_geometer", "fourier_analyst",
|
||||
"evideonic_projector", "candle_grammarian", "recurrence_theorist",
|
||||
"skeptic_quant",
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def prompts() -> dict:
|
||||
p = files("strategy_pythagoras") / "prompts.json"
|
||||
return json.loads(p.read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
def test_schema_version(prompts: dict) -> None:
|
||||
assert prompts["_schema"] == "3.2"
|
||||
|
||||
|
||||
def test_top_level_fields(prompts: dict) -> None:
|
||||
for k in (
|
||||
"agent_role", "pattern_guidance", "instruction", "domain_warnings",
|
||||
"anti_patterns", "output_priorities", "styles",
|
||||
):
|
||||
assert k in prompts, f"missing field: {k}"
|
||||
|
||||
|
||||
def test_styles_set(prompts: dict) -> None:
|
||||
assert set(prompts["styles"].keys()) == EXPECTED_STYLES
|
||||
|
||||
|
||||
def test_styles_have_directive_and_focus_metrics(prompts: dict) -> None:
|
||||
for name, style in prompts["styles"].items():
|
||||
assert "directive" in style, f"{name} missing directive"
|
||||
assert "focus_metrics" in style, f"{name} missing focus_metrics"
|
||||
assert len(style["focus_metrics"]) == 4, f"{name} focus_metrics len != 4"
|
||||
|
||||
|
||||
def test_directives_ascii_safe(prompts: dict) -> None:
|
||||
for name, style in prompts["styles"].items():
|
||||
directive = style["directive"]
|
||||
for ch in directive:
|
||||
assert ord(ch) <= 0x7F, f"{name} directive contains non-ASCII char {ch!r}"
|
||||
|
||||
|
||||
def test_directives_length(prompts: dict) -> None:
|
||||
for name, style in prompts["styles"].items():
|
||||
n = len(style["directive"])
|
||||
assert 800 <= n <= 950, f"{name} directive length {n} outside [800,950]"
|
||||
|
||||
|
||||
def test_directives_first_phrase(prompts: dict) -> None:
|
||||
for name, style in prompts["styles"].items():
|
||||
assert style["directive"].startswith("Il mercato e "), \
|
||||
f"{name} directive must start with 'Il mercato e '"
|
||||
|
||||
|
||||
def test_directives_end_archetype(prompts: dict) -> None:
|
||||
for name, style in prompts["styles"].items():
|
||||
assert "Archetipo dominante:" in style["directive"][-200:], \
|
||||
f"{name} missing Archetipo dominante in last 200 chars"
|
||||
@@ -0,0 +1,71 @@
|
||||
"""Validator accetta i 3 nuovi indicatori candle con arity corrette."""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
|
||||
import pytest
|
||||
|
||||
from multi_swarm_core.protocol.parser import parse_strategy
|
||||
from multi_swarm_core.protocol.validator import ValidationError, validate_strategy
|
||||
|
||||
|
||||
def _strategy_with_indicator(name: str, params: list[float]) -> str:
|
||||
return json.dumps(
|
||||
{
|
||||
"rules": [
|
||||
{
|
||||
"condition": {
|
||||
"op": "gt",
|
||||
"args": [
|
||||
{"kind": "indicator", "name": name, "params": params},
|
||||
{"kind": "literal", "value": 0.5},
|
||||
],
|
||||
},
|
||||
"action": "entry-long",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def test_candle_pattern_valid_min_arity() -> None:
|
||||
s = parse_strategy(_strategy_with_indicator("candle_pattern", [3, 0, 1, 0]))
|
||||
validate_strategy(s)
|
||||
|
||||
|
||||
def test_candle_pattern_valid_max_arity() -> None:
|
||||
s = parse_strategy(_strategy_with_indicator("candle_pattern", [12] + [0] * 12))
|
||||
validate_strategy(s)
|
||||
|
||||
|
||||
def test_candle_pattern_rejects_too_few_params() -> None:
|
||||
s = parse_strategy(_strategy_with_indicator("candle_pattern", [3, 0, 1]))
|
||||
with pytest.raises(ValidationError):
|
||||
validate_strategy(s)
|
||||
|
||||
|
||||
def test_pythagorean_ratio_valid() -> None:
|
||||
s = parse_strategy(_strategy_with_indicator("pythagorean_ratio", [89]))
|
||||
validate_strategy(s)
|
||||
|
||||
|
||||
def test_pythagorean_ratio_rejects_zero_params() -> None:
|
||||
s = parse_strategy(_strategy_with_indicator("pythagorean_ratio", []))
|
||||
with pytest.raises(ValidationError):
|
||||
validate_strategy(s)
|
||||
|
||||
|
||||
def test_fractal_mirror_valid_h() -> None:
|
||||
s = parse_strategy(_strategy_with_indicator("fractal_mirror", [12, 0]))
|
||||
validate_strategy(s)
|
||||
|
||||
|
||||
def test_fractal_mirror_valid_v() -> None:
|
||||
s = parse_strategy(_strategy_with_indicator("fractal_mirror", [12, 1]))
|
||||
validate_strategy(s)
|
||||
|
||||
|
||||
def test_fractal_mirror_rejects_one_param() -> None:
|
||||
s = parse_strategy(_strategy_with_indicator("fractal_mirror", [12]))
|
||||
with pytest.raises(ValidationError):
|
||||
validate_strategy(s)
|
||||
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,324 @@
|
||||
{
|
||||
"run_id": "7f65bd1832d94c638b588aab02fb223e",
|
||||
"run_name": "phase1-hardened-001",
|
||||
"n_folds": 4,
|
||||
"top_k_requested": 5,
|
||||
"top_k_evaluated": 5,
|
||||
"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": "9cf506b83bec55f6",
|
||||
"fitness_is": 0.2847465321576384,
|
||||
"sharpe_is": 0.6809931251900289,
|
||||
"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.3348901947844796,
|
||||
"sharpe": 0.6215567075501345,
|
||||
"dsr": 0.05615871498414856,
|
||||
"dsr_pvalue": 0.9438412850158514,
|
||||
"return": 0.1669550204052912,
|
||||
"max_dd": 0.2425926649805225,
|
||||
"n_trades": 60,
|
||||
"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.10547685784422405,
|
||||
"sharpe": -0.04385303190774091,
|
||||
"dsr": 0.013045759744378084,
|
||||
"dsr_pvalue": 0.986954240255622,
|
||||
"return": -0.003908970230222186,
|
||||
"max_dd": 0.05825300658307936,
|
||||
"n_trades": 31,
|
||||
"test_start": "2025-01-31 03:00:00+00:00",
|
||||
"test_end": "2025-12-31 23:00:00+00:00"
|
||||
}
|
||||
],
|
||||
"fitness_oos_mean": 0.24269351114651366,
|
||||
"fitness_oos_min": 0.08496628060413243,
|
||||
"fitness_oos_max": 0.4454407113532186,
|
||||
"fitness_oos_std": 0.15273583802070415,
|
||||
"sharpe_oos_mean": 0.3066807290989944,
|
||||
"sharpe_oos_min": -0.291593157960215,
|
||||
"robust_score": 0.08496628060413243
|
||||
},
|
||||
{
|
||||
"genome_id": "9f1f7ffa40b4900a",
|
||||
"fitness_is": 0.2653851546757454,
|
||||
"sharpe_is": 0.7183647852569522,
|
||||
"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.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: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.0,
|
||||
"sharpe_oos_min": 0.0,
|
||||
"robust_score": 0.0
|
||||
},
|
||||
{
|
||||
"genome_id": "aff702f86c30e30f",
|
||||
"fitness_is": 0.22070023288031862,
|
||||
"sharpe_is": 0.49220575419744833,
|
||||
"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": "43236904205071d5",
|
||||
"fitness_is": 0.22070023288031862,
|
||||
"sharpe_is": 0.49220575419744833,
|
||||
"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": "78a445d3043e0771",
|
||||
"fitness_is": 0.20950868586433583,
|
||||
"sharpe_is": 0.2940379509455447,
|
||||
"folds": [
|
||||
{
|
||||
"fold": 0,
|
||||
"fitness": 0.07182313189843528,
|
||||
"sharpe": -0.5971649832941454,
|
||||
"dsr": 0.0029614586254508167,
|
||||
"dsr_pvalue": 0.9970385413745492,
|
||||
"return": -0.10657245144370586,
|
||||
"max_dd": 0.17076725093180228,
|
||||
"n_trades": 14,
|
||||
"test_start": "2022-05-02 12:00:00+00:00",
|
||||
"test_end": "2023-04-02 08:00:00+00:00"
|
||||
},
|
||||
{
|
||||
"fold": 1,
|
||||
"fitness": 0.04914620786914511,
|
||||
"sharpe": -0.7350195766937807,
|
||||
"dsr": 0.0007433210570883239,
|
||||
"dsr_pvalue": 0.9992566789429117,
|
||||
"return": -0.03472119511680272,
|
||||
"max_dd": 0.060780295013844174,
|
||||
"n_trades": 20,
|
||||
"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.2071345821102195,
|
||||
"dsr": 0.14855640183813817,
|
||||
"dsr_pvalue": 0.8514435981618618,
|
||||
"return": 0.12296871148346566,
|
||||
"max_dd": 0.05065940776643054,
|
||||
"n_trades": 9,
|
||||
"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.6874561123211371,
|
||||
"dsr": 9.510326955509528e-06,
|
||||
"dsr_pvalue": 0.9999904896730445,
|
||||
"return": -0.03197955364502458,
|
||||
"max_dd": 0.032239270519921044,
|
||||
"n_trades": 7,
|
||||
"test_start": "2025-01-31 03:00:00+00:00",
|
||||
"test_end": "2025-12-31 23:00:00+00:00"
|
||||
}
|
||||
],
|
||||
"fitness_oos_mean": 0.0302423349418951,
|
||||
"fitness_oos_min": 0.0,
|
||||
"fitness_oos_max": 0.07182313189843528,
|
||||
"fitness_oos_std": 0.03128704497338385,
|
||||
"sharpe_oos_mean": -0.4531265225497109,
|
||||
"sharpe_oos_min": -1.6874561123211371,
|
||||
"robust_score": 0.0
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -18,6 +18,7 @@ members = [
|
||||
"multi-swarm-coevolutive",
|
||||
"multi-swarm-core",
|
||||
"strategy-crypto",
|
||||
"strategy-pythagoras",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -900,6 +901,7 @@ source = { virtual = "." }
|
||||
dependencies = [
|
||||
{ name = "multi-swarm-core" },
|
||||
{ name = "strategy-crypto" },
|
||||
{ name = "strategy-pythagoras" },
|
||||
]
|
||||
|
||||
[package.dev-dependencies]
|
||||
@@ -917,6 +919,7 @@ dev = [
|
||||
requires-dist = [
|
||||
{ name = "multi-swarm-core", editable = "src/multi_swarm_core" },
|
||||
{ name = "strategy-crypto", editable = "src/strategy_crypto" },
|
||||
{ name = "strategy-pythagoras", editable = "src/strategy_pythagoras" },
|
||||
]
|
||||
|
||||
[package.metadata.requires-dev]
|
||||
@@ -1980,6 +1983,27 @@ requires-dist = [
|
||||
{ name = "pyarrow", specifier = ">=18.0" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "strategy-pythagoras"
|
||||
version = "0.1.0"
|
||||
source = { editable = "src/strategy_pythagoras" }
|
||||
dependencies = [
|
||||
{ name = "multi-swarm-core" },
|
||||
{ name = "nicegui" },
|
||||
{ name = "pandas" },
|
||||
{ name = "plotly" },
|
||||
{ name = "pyarrow" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "multi-swarm-core", editable = "src/multi_swarm_core" },
|
||||
{ name = "nicegui", specifier = ">=3.11.1" },
|
||||
{ name = "pandas", specifier = ">=2.2" },
|
||||
{ name = "plotly", specifier = ">=5.24" },
|
||||
{ name = "pyarrow", specifier = ">=18.0" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tenacity"
|
||||
version = "9.1.4"
|
||||
|
||||
Reference in New Issue
Block a user