From 7d766173a429cf2c8249aab4f8664fb71d8fde5e Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Fri, 15 May 2026 17:43:01 +0000 Subject: [PATCH 01/11] chore(structure): bootstrap scheletro src/strategy_crypto member Pre-condizione per la riorganizzazione in uv workspace: crea il layout member del nuovo pacchetto strategy_crypto con __init__.py vuoti. Il contenuto (backend, frontend, strategies, tests) arriva nelle fasi successive del piano di migrazione. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/strategy_crypto/strategy_crypto/__init__.py | 0 src/strategy_crypto/strategy_crypto/backend/__init__.py | 0 src/strategy_crypto/strategy_crypto/frontend/__init__.py | 0 src/strategy_crypto/tests/__init__.py | 0 4 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 src/strategy_crypto/strategy_crypto/__init__.py create mode 100644 src/strategy_crypto/strategy_crypto/backend/__init__.py create mode 100644 src/strategy_crypto/strategy_crypto/frontend/__init__.py create mode 100644 src/strategy_crypto/tests/__init__.py diff --git a/src/strategy_crypto/strategy_crypto/__init__.py b/src/strategy_crypto/strategy_crypto/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/strategy_crypto/strategy_crypto/backend/__init__.py b/src/strategy_crypto/strategy_crypto/backend/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/strategy_crypto/strategy_crypto/frontend/__init__.py b/src/strategy_crypto/strategy_crypto/frontend/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/strategy_crypto/tests/__init__.py b/src/strategy_crypto/tests/__init__.py new file mode 100644 index 0000000..e69de29 From b6539802e07e349a7bcc2475bca51dd24f24b309 Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Fri, 15 May 2026 17:43:48 +0000 Subject: [PATCH 02/11] =?UTF-8?q?refactor(layout):=20rename=20multi=5Fswar?= =?UTF-8?q?m=20=E2=86=92=20multi=5Fswarm=5Fcore=20con=20doppia=20nidificaz?= =?UTF-8?q?ione=20uv=20workspace?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - mv src/multi_swarm → src/multi_swarm_core/multi_swarm_core (member layout) - sed-replace globale degli import: from/import multi_swarm.* → multi_swarm_core.* - 115 occorrenze aggiornate in src/ scripts/ tests/ - multi_swarm_coevolutive (nome repo) preservato dal word boundary Pre-condizione per il setup uv workspace della Fase 3. Co-Authored-By: Claude Opus 4.7 (1M context) --- .omc/project-memory.json | 234 ++++++++++++++++++ .../798fb16b-8751-4e21-a558-26b5cea16bd8.json | 8 + .../d997b670-db00-45b4-a478-cdad4d7fd603.json | 8 + .../ed0d4e09-401a-47e4-b354-336908ba30c2.json | 8 + ...798fb16b-8751-4e21-a558-26b5cea16bd8.jsonl | 31 +++ ...c410f859-5a67-475c-a941-8e2c50ed3a11.jsonl | 6 + ...d997b670-db00-45b4-a478-cdad4d7fd603.jsonl | 16 ++ ...ed0d4e09-401a-47e4-b354-336908ba30c2.jsonl | 1 + .omc/state/hud-state.json | 6 + .omc/state/hud-stdin-cache.json | 1 + .omc/state/idle-notif-cooldown.json | 3 + .omc/state/last-tool-error.json | 7 + .omc/state/mission-state.json | 145 +++++++++++ .omc/state/subagent-tracking.json | 35 +++ scripts/backtest_strategy.py | 14 +- scripts/replay_strategies_window.py | 127 ++++++++++ scripts/run_paper_trading.py | 14 +- scripts/run_phase1.py | 12 +- scripts/smoke_run.py | 6 +- .../multi_swarm_core}/__init__.py | 0 .../multi_swarm_core}/agents/__init__.py | 0 .../multi_swarm_core}/agents/adversarial.py | 0 .../multi_swarm_core}/agents/falsification.py | 0 .../multi_swarm_core}/agents/hypothesis.py | 0 .../agents/market_summary.py | 0 .../multi_swarm_core}/backtest/__init__.py | 0 .../multi_swarm_core}/backtest/engine.py | 0 .../multi_swarm_core}/backtest/orders.py | 0 .../multi_swarm_core}/cerbero/__init__.py | 0 .../multi_swarm_core}/cerbero/client.py | 0 .../multi_swarm_core}/cerbero/tools.py | 0 .../multi_swarm_core}/config.py | 0 .../multi_swarm_core}/dashboard/__init__.py | 0 .../multi_swarm_core}/dashboard/data.py | 0 .../dashboard/nicegui_app.py | 4 +- .../multi_swarm_core}/data/__init__.py | 0 .../multi_swarm_core}/data/cerbero_ohlcv.py | 0 .../multi_swarm_core}/data/splits.py | 0 .../multi_swarm_core}/ga/__init__.py | 0 .../multi_swarm_core}/ga/fitness.py | 0 .../multi_swarm_core}/ga/initial.py | 0 .../multi_swarm_core}/ga/loop.py | 0 .../multi_swarm_core}/ga/selection.py | 0 .../multi_swarm_core}/ga/summary.py | 0 .../multi_swarm_core}/genome/__init__.py | 0 .../multi_swarm_core}/genome/crossover.py | 0 .../multi_swarm_core}/genome/hypothesis.py | 0 .../multi_swarm_core}/genome/mutation.py | 0 .../genome/mutation_prompt_llm.py | 0 .../multi_swarm_core}/llm/__init__.py | 0 .../multi_swarm_core}/llm/client.py | 0 .../multi_swarm_core}/llm/cost_tracker.py | 0 .../multi_swarm_core}/metrics/__init__.py | 0 .../multi_swarm_core}/metrics/basic.py | 0 .../multi_swarm_core}/metrics/diversity.py | 0 .../multi_swarm_core}/metrics/dsr.py | 0 .../orchestrator/__init__.py | 0 .../multi_swarm_core}/orchestrator/run.py | 0 .../paper_trading/__init__.py | 0 .../paper_trading/executor.py | 0 .../paper_trading/persistence.py | 2 +- .../paper_trading/portfolio.py | 0 .../multi_swarm_core}/persistence/__init__.py | 0 .../persistence/repository.py | 0 .../multi_swarm_core}/persistence/schema.py | 0 .../multi_swarm_core}/protocol/__init__.py | 0 .../multi_swarm_core}/protocol/compiler.py | 0 .../multi_swarm_core}/protocol/grammar.py | 0 .../multi_swarm_core}/protocol/parser.py | 0 .../multi_swarm_core}/protocol/validator.py | 0 .../multi_swarm_core}/py.typed | 0 tests/integration/test_e2e_minimal_run.py | 8 +- .../test_ga_loop_with_prompt_mutator.py | 4 +- tests/unit/test_adversarial.py | 36 +-- tests/unit/test_backtest_engine.py | 4 +- tests/unit/test_backtest_orders.py | 2 +- tests/unit/test_cerbero_client.py | 2 +- tests/unit/test_cerbero_ohlcv.py | 2 +- tests/unit/test_cerbero_tools.py | 2 +- tests/unit/test_config.py | 4 +- tests/unit/test_cost_tracker.py | 4 +- tests/unit/test_diversity.py | 2 +- tests/unit/test_falsification.py | 4 +- tests/unit/test_fitness.py | 6 +- tests/unit/test_ga_initial.py | 4 +- tests/unit/test_ga_loop.py | 4 +- tests/unit/test_ga_summary.py | 2 +- tests/unit/test_genome_crossover.py | 4 +- tests/unit/test_genome_hypothesis.py | 2 +- tests/unit/test_genome_mutation.py | 4 +- tests/unit/test_hypothesis_agent.py | 6 +- tests/unit/test_llm_client.py | 24 +- tests/unit/test_market_summary.py | 2 +- tests/unit/test_metrics_basic.py | 2 +- tests/unit/test_metrics_dsr.py | 2 +- tests/unit/test_mutation_dispatcher.py | 4 +- tests/unit/test_mutation_prompt_llm.py | 4 +- tests/unit/test_protocol_compiler.py | 6 +- tests/unit/test_protocol_parser.py | 4 +- tests/unit/test_protocol_validator.py | 4 +- tests/unit/test_repository.py | 4 +- tests/unit/test_selection.py | 4 +- tests/unit/test_splits.py | 2 +- 103 files changed, 746 insertions(+), 110 deletions(-) create mode 100644 .omc/project-memory.json create mode 100644 .omc/sessions/798fb16b-8751-4e21-a558-26b5cea16bd8.json create mode 100644 .omc/sessions/d997b670-db00-45b4-a478-cdad4d7fd603.json create mode 100644 .omc/sessions/ed0d4e09-401a-47e4-b354-336908ba30c2.json create mode 100644 .omc/state/agent-replay-798fb16b-8751-4e21-a558-26b5cea16bd8.jsonl create mode 100644 .omc/state/agent-replay-c410f859-5a67-475c-a941-8e2c50ed3a11.jsonl create mode 100644 .omc/state/agent-replay-d997b670-db00-45b4-a478-cdad4d7fd603.jsonl create mode 100644 .omc/state/agent-replay-ed0d4e09-401a-47e4-b354-336908ba30c2.jsonl create mode 100644 .omc/state/hud-state.json create mode 100644 .omc/state/hud-stdin-cache.json create mode 100644 .omc/state/idle-notif-cooldown.json create mode 100644 .omc/state/last-tool-error.json create mode 100644 .omc/state/mission-state.json create mode 100644 .omc/state/subagent-tracking.json create mode 100644 scripts/replay_strategies_window.py rename src/{multi_swarm => multi_swarm_core/multi_swarm_core}/__init__.py (100%) rename src/{multi_swarm => multi_swarm_core/multi_swarm_core}/agents/__init__.py (100%) rename src/{multi_swarm => multi_swarm_core/multi_swarm_core}/agents/adversarial.py (100%) rename src/{multi_swarm => multi_swarm_core/multi_swarm_core}/agents/falsification.py (100%) rename src/{multi_swarm => multi_swarm_core/multi_swarm_core}/agents/hypothesis.py (100%) rename src/{multi_swarm => multi_swarm_core/multi_swarm_core}/agents/market_summary.py (100%) rename src/{multi_swarm => multi_swarm_core/multi_swarm_core}/backtest/__init__.py (100%) rename src/{multi_swarm => multi_swarm_core/multi_swarm_core}/backtest/engine.py (100%) rename src/{multi_swarm => multi_swarm_core/multi_swarm_core}/backtest/orders.py (100%) rename src/{multi_swarm => multi_swarm_core/multi_swarm_core}/cerbero/__init__.py (100%) rename src/{multi_swarm => multi_swarm_core/multi_swarm_core}/cerbero/client.py (100%) rename src/{multi_swarm => multi_swarm_core/multi_swarm_core}/cerbero/tools.py (100%) rename src/{multi_swarm => multi_swarm_core/multi_swarm_core}/config.py (100%) rename src/{multi_swarm => multi_swarm_core/multi_swarm_core}/dashboard/__init__.py (100%) rename src/{multi_swarm => multi_swarm_core/multi_swarm_core}/dashboard/data.py (100%) rename src/{multi_swarm => multi_swarm_core/multi_swarm_core}/dashboard/nicegui_app.py (99%) rename src/{multi_swarm => multi_swarm_core/multi_swarm_core}/data/__init__.py (100%) rename src/{multi_swarm => multi_swarm_core/multi_swarm_core}/data/cerbero_ohlcv.py (100%) rename src/{multi_swarm => multi_swarm_core/multi_swarm_core}/data/splits.py (100%) rename src/{multi_swarm => multi_swarm_core/multi_swarm_core}/ga/__init__.py (100%) rename src/{multi_swarm => multi_swarm_core/multi_swarm_core}/ga/fitness.py (100%) rename src/{multi_swarm => multi_swarm_core/multi_swarm_core}/ga/initial.py (100%) rename src/{multi_swarm => multi_swarm_core/multi_swarm_core}/ga/loop.py (100%) rename src/{multi_swarm => multi_swarm_core/multi_swarm_core}/ga/selection.py (100%) rename src/{multi_swarm => multi_swarm_core/multi_swarm_core}/ga/summary.py (100%) rename src/{multi_swarm => multi_swarm_core/multi_swarm_core}/genome/__init__.py (100%) rename src/{multi_swarm => multi_swarm_core/multi_swarm_core}/genome/crossover.py (100%) rename src/{multi_swarm => multi_swarm_core/multi_swarm_core}/genome/hypothesis.py (100%) rename src/{multi_swarm => multi_swarm_core/multi_swarm_core}/genome/mutation.py (100%) rename src/{multi_swarm => multi_swarm_core/multi_swarm_core}/genome/mutation_prompt_llm.py (100%) rename src/{multi_swarm => multi_swarm_core/multi_swarm_core}/llm/__init__.py (100%) rename src/{multi_swarm => multi_swarm_core/multi_swarm_core}/llm/client.py (100%) rename src/{multi_swarm => multi_swarm_core/multi_swarm_core}/llm/cost_tracker.py (100%) rename src/{multi_swarm => multi_swarm_core/multi_swarm_core}/metrics/__init__.py (100%) rename src/{multi_swarm => multi_swarm_core/multi_swarm_core}/metrics/basic.py (100%) rename src/{multi_swarm => multi_swarm_core/multi_swarm_core}/metrics/diversity.py (100%) rename src/{multi_swarm => multi_swarm_core/multi_swarm_core}/metrics/dsr.py (100%) rename src/{multi_swarm => multi_swarm_core/multi_swarm_core}/orchestrator/__init__.py (100%) rename src/{multi_swarm => multi_swarm_core/multi_swarm_core}/orchestrator/run.py (100%) rename src/{multi_swarm => multi_swarm_core/multi_swarm_core}/paper_trading/__init__.py (100%) rename src/{multi_swarm => multi_swarm_core/multi_swarm_core}/paper_trading/executor.py (100%) rename src/{multi_swarm => multi_swarm_core/multi_swarm_core}/paper_trading/persistence.py (98%) rename src/{multi_swarm => multi_swarm_core/multi_swarm_core}/paper_trading/portfolio.py (100%) rename src/{multi_swarm => multi_swarm_core/multi_swarm_core}/persistence/__init__.py (100%) rename src/{multi_swarm => multi_swarm_core/multi_swarm_core}/persistence/repository.py (100%) rename src/{multi_swarm => multi_swarm_core/multi_swarm_core}/persistence/schema.py (100%) rename src/{multi_swarm => multi_swarm_core/multi_swarm_core}/protocol/__init__.py (100%) rename src/{multi_swarm => multi_swarm_core/multi_swarm_core}/protocol/compiler.py (100%) rename src/{multi_swarm => multi_swarm_core/multi_swarm_core}/protocol/grammar.py (100%) rename src/{multi_swarm => multi_swarm_core/multi_swarm_core}/protocol/parser.py (100%) rename src/{multi_swarm => multi_swarm_core/multi_swarm_core}/protocol/validator.py (100%) rename src/{multi_swarm => multi_swarm_core/multi_swarm_core}/py.typed (100%) diff --git a/.omc/project-memory.json b/.omc/project-memory.json new file mode 100644 index 0000000..0da305b --- /dev/null +++ b/.omc/project-memory.json @@ -0,0 +1,234 @@ +{ + "version": "1.0.0", + "lastScanned": 1778863584729, + "projectRoot": "/opt/docker/multi_swarm_coevolutive", + "techStack": { + "languages": [ + { + "name": "Python", + "version": null, + "confidence": "high", + "markers": [ + "pyproject.toml" + ] + } + ], + "frameworks": [ + { + "name": "pytest", + "version": null, + "category": "testing" + } + ], + "packageManager": null, + "runtime": null + }, + "build": { + "buildCommand": null, + "testCommand": "pytest", + "lintCommand": "ruff check", + "devCommand": null, + "scripts": {} + }, + "conventions": { + "namingStyle": null, + "importStyle": null, + "testPattern": null, + "fileOrganization": null + }, + "structure": { + "isMonorepo": false, + "workspaces": [], + "mainDirectories": [ + "docs", + "scripts", + "src", + "tests" + ], + "gitBranches": { + "defaultBranch": "main", + "branchingStrategy": null + } + }, + "customNotes": [], + "directoryMap": { + "data": { + "path": "data", + "purpose": "Data files", + "fileCount": 0, + "lastAccessed": 1778863584681, + "keyFiles": [] + }, + "docs": { + "path": "docs", + "purpose": "Documentation", + "fileCount": 0, + "lastAccessed": 1778863584688, + "keyFiles": [] + }, + "scripts": { + "path": "scripts", + "purpose": "Build/utility scripts", + "fileCount": 6, + "lastAccessed": 1778863584699, + "keyFiles": [ + "__init__.py", + "backtest_strategy.py", + "replay_strategies_window.py", + "run_paper_trading.py", + "run_phase1.py" + ] + }, + "series": { + "path": "series", + "purpose": null, + "fileCount": 1, + "lastAccessed": 1778863584703, + "keyFiles": [ + "f4a6ed332c35c4a2.parquet" + ] + }, + "src": { + "path": "src", + "purpose": "Source code", + "fileCount": 0, + "lastAccessed": 1778863584706, + "keyFiles": [] + }, + "state": { + "path": "state", + "purpose": null, + "fileCount": 3, + "lastAccessed": 1778863584707, + "keyFiles": [ + "runs.db", + "runs.db-shm", + "runs.db-wal" + ] + }, + "strategies": { + "path": "strategies", + "purpose": null, + "fileCount": 2, + "lastAccessed": 1778863584707, + "keyFiles": [ + "btc_fb63e851.json", + "eth_facd6af85d5d.json" + ] + }, + "tests": { + "path": "tests", + "purpose": "Test files", + "fileCount": 1, + "lastAccessed": 1778863584710, + "keyFiles": [ + "__init__.py" + ] + } + }, + "hotPaths": [ + { + "path": "src/multi_swarm/paper_trading/persistence.py", + "accessCount": 2, + "lastAccessed": 1778864950928, + "type": "file" + }, + { + "path": "src/multi_swarm/paper_trading/executor.py", + "accessCount": 2, + "lastAccessed": 1778864951455, + "type": "file" + }, + { + "path": "src/multi_swarm/paper_trading/portfolio.py", + "accessCount": 2, + "lastAccessed": 1778864956467, + "type": "file" + }, + { + "path": "src/multi_swarm/paper_trading/__init__.py", + "accessCount": 2, + "lastAccessed": 1778864957276, + "type": "file" + }, + { + "path": "src/multi_swarm/dashboard/data.py", + "accessCount": 2, + "lastAccessed": 1778864958856, + "type": "file" + }, + { + "path": "src/multi_swarm/dashboard/__init__.py", + "accessCount": 2, + "lastAccessed": 1778864959235, + "type": "file" + }, + { + "path": "pyproject.toml", + "accessCount": 2, + "lastAccessed": 1778864960093, + "type": "file" + }, + { + "path": "docker-compose.yml", + "accessCount": 2, + "lastAccessed": 1778864960851, + "type": "file" + }, + { + "path": ".env.example", + "accessCount": 2, + "lastAccessed": 1778864961733, + "type": "file" + }, + { + "path": "Dockerfile", + "accessCount": 1, + "lastAccessed": 1778864536214, + "type": "file" + }, + { + "path": "src/multi_swarm/__init__.py", + "accessCount": 1, + "lastAccessed": 1778864537411, + "type": "file" + }, + { + "path": "README.md", + "accessCount": 1, + "lastAccessed": 1778864537500, + "type": "file" + }, + { + "path": "src/multi_swarm/dashboard/nicegui_app.py", + "accessCount": 1, + "lastAccessed": 1778864557898, + "type": "file" + }, + { + "path": "src/multi_swarm/persistence/schema.py", + "accessCount": 1, + "lastAccessed": 1778864949548, + "type": "file" + }, + { + "path": "src/multi_swarm/persistence/repository.py", + "accessCount": 1, + "lastAccessed": 1778864949987, + "type": "file" + }, + { + "path": "src/multi_swarm/config.py", + "accessCount": 1, + "lastAccessed": 1778864959813, + "type": "file" + }, + { + "path": "scripts/run_paper_trading.py", + "accessCount": 1, + "lastAccessed": 1778864961224, + "type": "file" + } + ], + "userDirectives": [] +} \ No newline at end of file diff --git a/.omc/sessions/798fb16b-8751-4e21-a558-26b5cea16bd8.json b/.omc/sessions/798fb16b-8751-4e21-a558-26b5cea16bd8.json new file mode 100644 index 0000000..52b5830 --- /dev/null +++ b/.omc/sessions/798fb16b-8751-4e21-a558-26b5cea16bd8.json @@ -0,0 +1,8 @@ +{ + "session_id": "798fb16b-8751-4e21-a558-26b5cea16bd8", + "ended_at": "2026-05-15T00:33:45.627Z", + "reason": "other", + "agents_spawned": 0, + "agents_completed": 0, + "modes_used": [] +} \ No newline at end of file diff --git a/.omc/sessions/d997b670-db00-45b4-a478-cdad4d7fd603.json b/.omc/sessions/d997b670-db00-45b4-a478-cdad4d7fd603.json new file mode 100644 index 0000000..573d026 --- /dev/null +++ b/.omc/sessions/d997b670-db00-45b4-a478-cdad4d7fd603.json @@ -0,0 +1,8 @@ +{ + "session_id": "d997b670-db00-45b4-a478-cdad4d7fd603", + "ended_at": "2026-05-15T14:30:53.277Z", + "reason": "other", + "agents_spawned": 1, + "agents_completed": 1, + "modes_used": [] +} \ No newline at end of file diff --git a/.omc/sessions/ed0d4e09-401a-47e4-b354-336908ba30c2.json b/.omc/sessions/ed0d4e09-401a-47e4-b354-336908ba30c2.json new file mode 100644 index 0000000..eed6e53 --- /dev/null +++ b/.omc/sessions/ed0d4e09-401a-47e4-b354-336908ba30c2.json @@ -0,0 +1,8 @@ +{ + "session_id": "ed0d4e09-401a-47e4-b354-336908ba30c2", + "ended_at": "2026-05-14T09:47:10.928Z", + "reason": "other", + "agents_spawned": 0, + "agents_completed": 0, + "modes_used": [] +} \ No newline at end of file diff --git a/.omc/state/agent-replay-798fb16b-8751-4e21-a558-26b5cea16bd8.jsonl b/.omc/state/agent-replay-798fb16b-8751-4e21-a558-26b5cea16bd8.jsonl new file mode 100644 index 0000000..0e9542e --- /dev/null +++ b/.omc/state/agent-replay-798fb16b-8751-4e21-a558-26b5cea16bd8.jsonl @@ -0,0 +1,31 @@ +{"t":0,"agent":"a682971","agent_type":"unknown","event":"agent_stop","success":true} +{"t":0,"agent":"a3e1e57","agent_type":"unknown","event":"agent_stop","success":true} +{"t":0,"agent":"ac6e465","agent_type":"unknown","event":"agent_stop","success":true} +{"t":0,"agent":"ae622fc","agent_type":"unknown","event":"agent_stop","success":true} +{"t":0,"agent":"a70edb8","agent_type":"unknown","event":"agent_stop","success":true} +{"t":0,"agent":"a0298bb","agent_type":"unknown","event":"agent_stop","success":true} +{"t":0,"agent":"a0a1dac","agent_type":"unknown","event":"agent_stop","success":true} +{"t":0,"agent":"a233182","agent_type":"unknown","event":"agent_stop","success":true} +{"t":0,"agent":"a36dbf7","agent_type":"unknown","event":"agent_stop","success":true} +{"t":0,"agent":"ad92a42","agent_type":"unknown","event":"agent_stop","success":true} +{"t":0,"agent":"a3d10f4","agent_type":"unknown","event":"agent_stop","success":true} +{"t":0,"agent":"a3a6023","agent_type":"unknown","event":"agent_stop","success":true} +{"t":0,"agent":"a0031d5","agent_type":"unknown","event":"agent_stop","success":true} +{"t":0,"agent":"a49c162","agent_type":"unknown","event":"agent_stop","success":true} +{"t":0,"agent":"a593b10","agent_type":"unknown","event":"agent_stop","success":true} +{"t":0,"agent":"a3936fc","agent_type":"unknown","event":"agent_stop","success":true} +{"t":0,"agent":"a547232","agent_type":"unknown","event":"agent_stop","success":true} +{"t":0,"agent":"a5b6003","agent_type":"unknown","event":"agent_stop","success":true} +{"t":0,"agent":"a70d843","agent_type":"unknown","event":"agent_stop","success":true} +{"t":0,"agent":"aec9396","agent_type":"unknown","event":"agent_stop","success":true} +{"t":0,"agent":"a339545","agent_type":"unknown","event":"agent_stop","success":true} +{"t":0,"agent":"acb249c","agent_type":"unknown","event":"agent_stop","success":true} +{"t":0,"agent":"acd29d1","agent_type":"unknown","event":"agent_stop","success":true} +{"t":0,"agent":"a8e0c37","agent_type":"unknown","event":"agent_stop","success":true} +{"t":0,"agent":"a8e001b","agent_type":"unknown","event":"agent_stop","success":true} +{"t":0,"agent":"ac6bdc5","agent_type":"unknown","event":"agent_stop","success":true} +{"t":0,"agent":"a69506c","agent_type":"unknown","event":"agent_stop","success":true} +{"t":0,"agent":"aacfb9e","agent_type":"unknown","event":"agent_stop","success":true} +{"t":0,"agent":"ac010a8","agent_type":"unknown","event":"agent_stop","success":true} +{"t":0,"agent":"a7f5511","agent_type":"unknown","event":"agent_stop","success":true} +{"t":0,"agent":"a5e7734","agent_type":"unknown","event":"agent_stop","success":true} diff --git a/.omc/state/agent-replay-c410f859-5a67-475c-a941-8e2c50ed3a11.jsonl b/.omc/state/agent-replay-c410f859-5a67-475c-a941-8e2c50ed3a11.jsonl new file mode 100644 index 0000000..c12eae9 --- /dev/null +++ b/.omc/state/agent-replay-c410f859-5a67-475c-a941-8e2c50ed3a11.jsonl @@ -0,0 +1,6 @@ +{"t":0,"agent":"aa06dbe","agent_type":"Explore","event":"agent_start","parent_mode":"none"} +{"t":0,"agent":"a62f5be","agent_type":"Explore","event":"agent_start","parent_mode":"none"} +{"t":0,"agent":"a62f5be","agent_type":"Explore","event":"agent_stop","success":true,"duration_ms":21379} +{"t":0,"agent":"aa06dbe","agent_type":"Explore","event":"agent_stop","success":true,"duration_ms":94545} +{"t":0,"agent":"a086f61","agent_type":"Plan","event":"agent_start","parent_mode":"none"} +{"t":0,"agent":"a086f61","agent_type":"Plan","event":"agent_stop","success":true,"duration_ms":202552} diff --git a/.omc/state/agent-replay-d997b670-db00-45b4-a478-cdad4d7fd603.jsonl b/.omc/state/agent-replay-d997b670-db00-45b4-a478-cdad4d7fd603.jsonl new file mode 100644 index 0000000..2456f80 --- /dev/null +++ b/.omc/state/agent-replay-d997b670-db00-45b4-a478-cdad4d7fd603.jsonl @@ -0,0 +1,16 @@ +{"t":0,"agent":"a0d4467","agent_type":"unknown","event":"agent_stop","success":true} +{"t":0,"agent":"a160b0e","agent_type":"unknown","event":"agent_stop","success":true} +{"t":0,"agent":"af3bbac","agent_type":"unknown","event":"agent_stop","success":true} +{"t":0,"agent":"a1ee6c9","agent_type":"unknown","event":"agent_stop","success":true} +{"t":0,"agent":"a4ad30d","agent_type":"unknown","event":"agent_stop","success":true} +{"t":0,"agent":"a90eb8d","agent_type":"unknown","event":"agent_stop","success":true} +{"t":0,"agent":"ae064b2","agent_type":"unknown","event":"agent_stop","success":true} +{"t":0,"agent":"a0c8984","agent_type":"unknown","event":"agent_stop","success":true} +{"t":0,"agent":"ad27588","agent_type":"unknown","event":"agent_stop","success":true} +{"t":0,"agent":"a523e02","agent_type":"unknown","event":"agent_stop","success":true} +{"t":0,"agent":"a2f3f5e","agent_type":"unknown","event":"agent_stop","success":true} +{"t":0,"agent":"ad7358f","agent_type":"unknown","event":"agent_stop","success":true} +{"t":0,"agent":"a70e03c","agent_type":"unknown","event":"agent_stop","success":true} +{"t":0,"agent":"a5535e6","agent_type":"Explore","event":"agent_start","parent_mode":"none"} +{"t":0,"agent":"a5535e6","agent_type":"Explore","event":"agent_stop","success":true,"duration_ms":316451} +{"t":0,"agent":"adffe1e","agent_type":"unknown","event":"agent_stop","success":true} diff --git a/.omc/state/agent-replay-ed0d4e09-401a-47e4-b354-336908ba30c2.jsonl b/.omc/state/agent-replay-ed0d4e09-401a-47e4-b354-336908ba30c2.jsonl new file mode 100644 index 0000000..55de032 --- /dev/null +++ b/.omc/state/agent-replay-ed0d4e09-401a-47e4-b354-336908ba30c2.jsonl @@ -0,0 +1 @@ +{"t":0,"agent":"a25cb13","agent_type":"unknown","event":"agent_stop","success":true} diff --git a/.omc/state/hud-state.json b/.omc/state/hud-state.json new file mode 100644 index 0000000..7ad50de --- /dev/null +++ b/.omc/state/hud-state.json @@ -0,0 +1,6 @@ +{ + "timestamp": "2026-05-15T16:46:57.759Z", + "backgroundTasks": [], + "sessionStartTimestamp": "2026-05-15T16:46:24.599Z", + "sessionId": "c410f859-5a67-475c-a941-8e2c50ed3a11" +} \ No newline at end of file diff --git a/.omc/state/hud-stdin-cache.json b/.omc/state/hud-stdin-cache.json new file mode 100644 index 0000000..521e847 --- /dev/null +++ b/.omc/state/hud-stdin-cache.json @@ -0,0 +1 @@ +{"session_id":"c410f859-5a67-475c-a941-8e2c50ed3a11","transcript_path":"/root/.claude/projects/-opt-docker-multi-swarm-coevolutive/c410f859-5a67-475c-a941-8e2c50ed3a11.jsonl","cwd":"/opt/docker/multi_swarm_coevolutive","effort":{"level":"xhigh"},"session_name":"monorepo-reorganization-split-revert","model":{"id":"claude-opus-4-7[1m]","display_name":"Opus 4.7 (1M context)"},"workspace":{"current_dir":"/opt/docker/multi_swarm_coevolutive","project_dir":"/opt/docker/multi_swarm_coevolutive","added_dirs":[]},"version":"2.1.142","output_style":{"name":"default"},"cost":{"total_cost_usd":12.174355699999994,"total_duration_ms":3442407,"total_api_duration_ms":1675477,"total_lines_added":750,"total_lines_removed":345},"context_window":{"total_input_tokens":212953,"total_output_tokens":8,"context_window_size":1000000,"current_usage":{"input_tokens":6,"output_tokens":8,"cache_creation_input_tokens":599,"cache_read_input_tokens":212348},"used_percentage":21,"remaining_percentage":79},"exceeds_200k_tokens":true,"fast_mode":false,"thinking":{"enabled":true},"rate_limits":{"five_hour":{"used_percentage":11,"resets_at":1778881200},"seven_day":{"used_percentage":21,"resets_at":1779138000}}} \ No newline at end of file diff --git a/.omc/state/idle-notif-cooldown.json b/.omc/state/idle-notif-cooldown.json new file mode 100644 index 0000000..89a79a5 --- /dev/null +++ b/.omc/state/idle-notif-cooldown.json @@ -0,0 +1,3 @@ +{ + "lastSentAt": "2026-05-15T17:22:55.330Z" +} \ No newline at end of file diff --git a/.omc/state/last-tool-error.json b/.omc/state/last-tool-error.json new file mode 100644 index 0000000..b381ffb --- /dev/null +++ b/.omc/state/last-tool-error.json @@ -0,0 +1,7 @@ +{ + "tool_name": "Bash", + "tool_input_preview": "{\"command\":\"find /opt/docker/multi_swarm_coevolutive -name \\\"config.py\\\" | xargs grep -l \\\"DATA_DIR\\\\|STRATEGIES\\\\|STATE\\\" 2>/dev/null\"}", + "error": "Exit code 123", + "timestamp": "2026-05-15T17:03:17.335Z", + "retry_count": 1 +} \ No newline at end of file diff --git a/.omc/state/mission-state.json b/.omc/state/mission-state.json new file mode 100644 index 0000000..7473ccc --- /dev/null +++ b/.omc/state/mission-state.json @@ -0,0 +1,145 @@ +{ + "updatedAt": "2026-05-15T17:12:22.595Z", + "missions": [ + { + "id": "session:d997b670-db00-45b4-a478-cdad4d7fd603:none", + "source": "session", + "name": "none", + "objective": "Session mission", + "createdAt": "2026-05-15T10:07:56.561Z", + "updatedAt": "2026-05-15T11:04:10.376Z", + "status": "done", + "workerCount": 1, + "taskCounts": { + "total": 1, + "pending": 0, + "blocked": 0, + "inProgress": 0, + "completed": 1, + "failed": 0 + }, + "agents": [ + { + "name": "Explore:a5535e6", + "role": "Explore", + "ownership": "a5535e676159dcf1c", + "status": "done", + "currentStep": null, + "latestUpdate": "completed", + "completedSummary": null, + "updatedAt": "2026-05-15T11:04:10.376Z" + } + ], + "timeline": [ + { + "id": "session-start:a5535e676159dcf1c:2026-05-15T10:07:56.561Z", + "at": "2026-05-15T10:07:56.561Z", + "kind": "update", + "agent": "Explore:a5535e6", + "detail": "started Explore:a5535e6", + "sourceKey": "session-start:a5535e676159dcf1c" + }, + { + "id": "session-stop:a5535e676159dcf1c:2026-05-15T10:13:13.012Z", + "at": "2026-05-15T10:13:13.012Z", + "kind": "completion", + "agent": "Explore:a5535e6", + "detail": "completed", + "sourceKey": "session-stop:a5535e676159dcf1c" + }, + { + "id": "session-stop:adffe1eae69f95602:2026-05-15T11:04:10.376Z", + "at": "2026-05-15T11:04:10.376Z", + "kind": "completion", + "agent": "Explore:a5535e6", + "detail": "completed", + "sourceKey": "session-stop:adffe1eae69f95602" + } + ] + }, + { + "id": "session:c410f859-5a67-475c-a941-8e2c50ed3a11:none", + "source": "session", + "name": "none", + "objective": "Session mission", + "createdAt": "2026-05-15T17:02:02.737Z", + "updatedAt": "2026-05-15T17:12:22.595Z", + "status": "done", + "workerCount": 3, + "taskCounts": { + "total": 3, + "pending": 0, + "blocked": 0, + "inProgress": 0, + "completed": 3, + "failed": 0 + }, + "agents": [ + { + "name": "Explore:aa06dbe", + "role": "Explore", + "ownership": "aa06dbecb11544c5e", + "status": "done", + "currentStep": null, + "latestUpdate": "completed", + "completedSummary": null, + "updatedAt": "2026-05-15T17:03:37.282Z" + }, + { + "name": "Explore:a62f5be", + "role": "Explore", + "ownership": "a62f5bee30bd7a6c1", + "status": "done", + "currentStep": null, + "latestUpdate": "completed", + "completedSummary": null, + "updatedAt": "2026-05-15T17:02:35.066Z" + }, + { + "name": "Plan:a086f61", + "role": "Plan", + "ownership": "a086f61a1fbb580be", + "status": "done", + "currentStep": null, + "latestUpdate": "completed", + "completedSummary": null, + "updatedAt": "2026-05-15T17:12:22.595Z" + } + ], + "timeline": [ + { + "id": "session-stop:a62f5bee30bd7a6c1:2026-05-15T17:02:35.066Z", + "at": "2026-05-15T17:02:35.066Z", + "kind": "completion", + "agent": "Explore:a62f5be", + "detail": "completed", + "sourceKey": "session-stop:a62f5bee30bd7a6c1" + }, + { + "id": "session-stop:aa06dbecb11544c5e:2026-05-15T17:03:37.282Z", + "at": "2026-05-15T17:03:37.282Z", + "kind": "completion", + "agent": "Explore:aa06dbe", + "detail": "completed", + "sourceKey": "session-stop:aa06dbecb11544c5e" + }, + { + "id": "session-start:a086f61a1fbb580be:2026-05-15T17:09:00.043Z", + "at": "2026-05-15T17:09:00.043Z", + "kind": "update", + "agent": "Plan:a086f61", + "detail": "started Plan:a086f61", + "sourceKey": "session-start:a086f61a1fbb580be" + }, + { + "id": "session-stop:a086f61a1fbb580be:2026-05-15T17:12:22.595Z", + "at": "2026-05-15T17:12:22.595Z", + "kind": "completion", + "agent": "Plan:a086f61", + "detail": "completed", + "sourceKey": "session-stop:a086f61a1fbb580be" + } + ] + } + ] +} \ No newline at end of file diff --git a/.omc/state/subagent-tracking.json b/.omc/state/subagent-tracking.json new file mode 100644 index 0000000..4fe1c6b --- /dev/null +++ b/.omc/state/subagent-tracking.json @@ -0,0 +1,35 @@ +{ + "agents": [ + { + "agent_id": "aa06dbecb11544c5e", + "agent_type": "Explore", + "started_at": "2026-05-15T17:02:02.737Z", + "parent_mode": "none", + "status": "completed", + "completed_at": "2026-05-15T17:03:37.282Z", + "duration_ms": 94545 + }, + { + "agent_id": "a62f5bee30bd7a6c1", + "agent_type": "Explore", + "started_at": "2026-05-15T17:02:13.687Z", + "parent_mode": "none", + "status": "completed", + "completed_at": "2026-05-15T17:02:35.066Z", + "duration_ms": 21379 + }, + { + "agent_id": "a086f61a1fbb580be", + "agent_type": "Plan", + "started_at": "2026-05-15T17:09:00.043Z", + "parent_mode": "none", + "status": "completed", + "completed_at": "2026-05-15T17:12:22.595Z", + "duration_ms": 202552 + } + ], + "total_spawned": 3, + "total_completed": 3, + "total_failed": 0, + "last_updated": "2026-05-15T17:12:22.696Z" +} \ No newline at end of file diff --git a/scripts/backtest_strategy.py b/scripts/backtest_strategy.py index 0475749..9964ec9 100644 --- a/scripts/backtest_strategy.py +++ b/scripts/backtest_strategy.py @@ -17,13 +17,13 @@ import math from datetime import datetime from pathlib import Path -from multi_swarm.agents.adversarial import AdversarialAgent -from multi_swarm.agents.falsification import FalsificationAgent -from multi_swarm.cerbero.client import CerberoClient -from multi_swarm.config import load_settings -from multi_swarm.data.cerbero_ohlcv import CerberoOHLCVLoader, OHLCVRequest -from multi_swarm.protocol.parser import parse_strategy -from multi_swarm.protocol.validator import validate_strategy +from multi_swarm_core.agents.adversarial import AdversarialAgent +from multi_swarm_core.agents.falsification import FalsificationAgent +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.protocol.parser import parse_strategy +from multi_swarm_core.protocol.validator import validate_strategy def main() -> None: diff --git a/scripts/replay_strategies_window.py b/scripts/replay_strategies_window.py new file mode 100644 index 0000000..defb526 --- /dev/null +++ b/scripts/replay_strategies_window.py @@ -0,0 +1,127 @@ +"""Replay diagnostico: per ciascuna strategia conta quanti bar avrebbero +soddisfatto le condizioni di ciascuna regola sull'ultimo `--days` di storico. + +Ouput tabellare per branch: total_bars, fires, fire_rate, primo/ultimo fire. +Esegue anche un backtest grezzo (entry-on-signal, exit-on-flat) per stimare +n_trades e total_return realistici nel periodo. + +Esempio: + docker compose exec multi-swarm-paper \ + python /app/scripts/replay_strategies_window.py --days 30 +""" + +from __future__ import annotations + +import argparse +import json +from datetime import UTC, datetime, timedelta +from pathlib import Path + +import pandas as pd + +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.protocol.compiler import _eval_node, compile_strategy +from multi_swarm_core.protocol.parser import parse_strategy + +PROJECT_ROOT = Path(__file__).resolve().parent.parent + + +def parse_args() -> argparse.Namespace: + p = argparse.ArgumentParser() + p.add_argument("--days", type=int, default=30) + p.add_argument("--strategies-dir", default=str(PROJECT_ROOT / "strategies")) + return p.parse_args() + + +def fetch_window(loader: CerberoOHLCVLoader, symbol: str, days: int) -> pd.DataFrame: + end = datetime.now(UTC).replace(minute=0, second=0, microsecond=0) + start = end - timedelta(days=days) + req = OHLCVRequest( + symbol=symbol, timeframe="1h", start=start, end=end, exchange="deribit" + ) + return loader._fetch(req) # noqa: SLF001 — bypass cache + + +def per_branch_fires(strategy_path: Path, ohlcv: pd.DataFrame) -> list[dict]: + raw = strategy_path.read_text() + parsed = parse_strategy(raw) + out = [] + for idx, rule in enumerate(parsed.rules): + cond_series = _eval_node(rule.condition, ohlcv).fillna(False).astype(bool) + n = int(cond_series.sum()) + first = ohlcv.index[cond_series.argmax()] if n > 0 else None + # last fire: argmax on reversed + last = ohlcv.index[len(cond_series) - 1 - cond_series[::-1].argmax()] if n > 0 else None + out.append({ + "branch_idx": idx, + "action": rule.action, + "fires": n, + "fire_rate_pct": round(100.0 * n / len(ohlcv), 2), + "first_fire": first, + "last_fire": last, + }) + return out + + +def quick_pnl(strategy_path: Path, ohlcv: pd.DataFrame, fees_bp: float = 5.0) -> dict: + """Approx: at each bar evaluate compiled signal series (long/short/flat), + apply position to next-bar return, charge fees on changes. No leverage.""" + raw = strategy_path.read_text() + parsed = parse_strategy(raw) + sig_fn = compile_strategy(parsed) + signals = sig_fn(ohlcv) # series of "long"/"short"/"flat" + # map to position: long=+1, short=-1, flat=0 + pos = signals.map({"long": 1, "short": -1, "flat": 0}).fillna(0).astype(int) + rets = ohlcv["close"].pct_change().fillna(0.0) + # next-bar execution: position decided at bar t applies to return t+1 -> shift + pnl = pos.shift(1).fillna(0) * rets + # fees on position changes + changes = pos.diff().abs().fillna(0).astype(int) + fee_per_change = fees_bp / 10_000.0 + pnl_after_fees = pnl - changes * fee_per_change + cum = (1 + pnl_after_fees).prod() - 1 + n_trades = int((changes > 0).sum()) + time_in_market = float((pos != 0).mean()) + return { + "n_trades": n_trades, + "total_return_pct": round(100.0 * float(cum), 3), + "time_in_market_pct": round(100.0 * time_in_market, 2), + } + + +def main() -> None: + args = parse_args() + 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) + + strategies_dir = Path(args.strategies_dir) + pairs = [ + ("BTC-PERPETUAL", sorted(strategies_dir.glob("btc_*.json"))[0]), + ("ETH-PERPETUAL", sorted(strategies_dir.glob("eth_*.json"))[0]), + ] + + for symbol, strat_path in pairs: + print(f"\n=== {symbol} strategy={strat_path.name} window={args.days}d ===") + ohlcv = fetch_window(loader, symbol, args.days) + print(f"bars: {len(ohlcv)} range: {ohlcv.index[0]} -> {ohlcv.index[-1]}") + print("\n-- per branch --") + for row in per_branch_fires(strat_path, ohlcv): + print(json.dumps(row, default=str)) + print("\n-- quick pnl (next-bar exec, fees=5bp) --") + print(json.dumps(quick_pnl(strat_path, ohlcv), default=str)) + + +if __name__ == "__main__": + main() diff --git a/scripts/run_paper_trading.py b/scripts/run_paper_trading.py index dccccad..6eb0d6a 100644 --- a/scripts/run_paper_trading.py +++ b/scripts/run_paper_trading.py @@ -25,13 +25,13 @@ from dataclasses import dataclass from datetime import UTC, datetime, timedelta from pathlib import Path -from multi_swarm.cerbero.client import CerberoClient -from multi_swarm.config import load_settings -from multi_swarm.data.cerbero_ohlcv import CerberoOHLCVLoader, OHLCVRequest -from multi_swarm.paper_trading.executor import PaperExecutor -from multi_swarm.paper_trading.persistence import PaperRepository -from multi_swarm.paper_trading.portfolio import Portfolio -from multi_swarm.persistence.repository import Repository +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.paper_trading.executor import PaperExecutor +from multi_swarm_core.paper_trading.persistence import PaperRepository +from multi_swarm_core.paper_trading.portfolio import Portfolio +from multi_swarm_core.persistence.repository import Repository PROJECT_ROOT = Path(__file__).resolve().parent.parent diff --git a/scripts/run_phase1.py b/scripts/run_phase1.py index c3ba992..45f0e39 100644 --- a/scripts/run_phase1.py +++ b/scripts/run_phase1.py @@ -3,12 +3,12 @@ from __future__ import annotations import argparse from datetime import datetime -from multi_swarm.cerbero.client import CerberoClient -from multi_swarm.config import load_settings -from multi_swarm.data.cerbero_ohlcv import CerberoOHLCVLoader, OHLCVRequest -from multi_swarm.genome.hypothesis import ModelTier -from multi_swarm.llm.client import LLMClient -from multi_swarm.orchestrator.run import RunConfig, run_phase1 +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.llm.client import LLMClient +from multi_swarm_core.orchestrator.run import RunConfig, run_phase1 def parse_args() -> argparse.Namespace: diff --git a/scripts/smoke_run.py b/scripts/smoke_run.py index 16da344..8c194d4 100644 --- a/scripts/smoke_run.py +++ b/scripts/smoke_run.py @@ -6,9 +6,9 @@ from pathlib import Path import numpy as np import pandas as pd # type: ignore[import-untyped] -from multi_swarm.genome.hypothesis import HypothesisAgentGenome, ModelTier -from multi_swarm.llm.client import CompletionResult -from multi_swarm.orchestrator.run import RunConfig, run_phase1 +from multi_swarm_core.genome.hypothesis import HypothesisAgentGenome, ModelTier +from multi_swarm_core.llm.client import CompletionResult +from multi_swarm_core.orchestrator.run import RunConfig, run_phase1 _MOCK_STRATEGY = json.dumps( { diff --git a/src/multi_swarm/__init__.py b/src/multi_swarm_core/multi_swarm_core/__init__.py similarity index 100% rename from src/multi_swarm/__init__.py rename to src/multi_swarm_core/multi_swarm_core/__init__.py diff --git a/src/multi_swarm/agents/__init__.py b/src/multi_swarm_core/multi_swarm_core/agents/__init__.py similarity index 100% rename from src/multi_swarm/agents/__init__.py rename to src/multi_swarm_core/multi_swarm_core/agents/__init__.py diff --git a/src/multi_swarm/agents/adversarial.py b/src/multi_swarm_core/multi_swarm_core/agents/adversarial.py similarity index 100% rename from src/multi_swarm/agents/adversarial.py rename to src/multi_swarm_core/multi_swarm_core/agents/adversarial.py diff --git a/src/multi_swarm/agents/falsification.py b/src/multi_swarm_core/multi_swarm_core/agents/falsification.py similarity index 100% rename from src/multi_swarm/agents/falsification.py rename to src/multi_swarm_core/multi_swarm_core/agents/falsification.py diff --git a/src/multi_swarm/agents/hypothesis.py b/src/multi_swarm_core/multi_swarm_core/agents/hypothesis.py similarity index 100% rename from src/multi_swarm/agents/hypothesis.py rename to src/multi_swarm_core/multi_swarm_core/agents/hypothesis.py diff --git a/src/multi_swarm/agents/market_summary.py b/src/multi_swarm_core/multi_swarm_core/agents/market_summary.py similarity index 100% rename from src/multi_swarm/agents/market_summary.py rename to src/multi_swarm_core/multi_swarm_core/agents/market_summary.py diff --git a/src/multi_swarm/backtest/__init__.py b/src/multi_swarm_core/multi_swarm_core/backtest/__init__.py similarity index 100% rename from src/multi_swarm/backtest/__init__.py rename to src/multi_swarm_core/multi_swarm_core/backtest/__init__.py diff --git a/src/multi_swarm/backtest/engine.py b/src/multi_swarm_core/multi_swarm_core/backtest/engine.py similarity index 100% rename from src/multi_swarm/backtest/engine.py rename to src/multi_swarm_core/multi_swarm_core/backtest/engine.py diff --git a/src/multi_swarm/backtest/orders.py b/src/multi_swarm_core/multi_swarm_core/backtest/orders.py similarity index 100% rename from src/multi_swarm/backtest/orders.py rename to src/multi_swarm_core/multi_swarm_core/backtest/orders.py diff --git a/src/multi_swarm/cerbero/__init__.py b/src/multi_swarm_core/multi_swarm_core/cerbero/__init__.py similarity index 100% rename from src/multi_swarm/cerbero/__init__.py rename to src/multi_swarm_core/multi_swarm_core/cerbero/__init__.py diff --git a/src/multi_swarm/cerbero/client.py b/src/multi_swarm_core/multi_swarm_core/cerbero/client.py similarity index 100% rename from src/multi_swarm/cerbero/client.py rename to src/multi_swarm_core/multi_swarm_core/cerbero/client.py diff --git a/src/multi_swarm/cerbero/tools.py b/src/multi_swarm_core/multi_swarm_core/cerbero/tools.py similarity index 100% rename from src/multi_swarm/cerbero/tools.py rename to src/multi_swarm_core/multi_swarm_core/cerbero/tools.py diff --git a/src/multi_swarm/config.py b/src/multi_swarm_core/multi_swarm_core/config.py similarity index 100% rename from src/multi_swarm/config.py rename to src/multi_swarm_core/multi_swarm_core/config.py diff --git a/src/multi_swarm/dashboard/__init__.py b/src/multi_swarm_core/multi_swarm_core/dashboard/__init__.py similarity index 100% rename from src/multi_swarm/dashboard/__init__.py rename to src/multi_swarm_core/multi_swarm_core/dashboard/__init__.py diff --git a/src/multi_swarm/dashboard/data.py b/src/multi_swarm_core/multi_swarm_core/dashboard/data.py similarity index 100% rename from src/multi_swarm/dashboard/data.py rename to src/multi_swarm_core/multi_swarm_core/dashboard/data.py diff --git a/src/multi_swarm/dashboard/nicegui_app.py b/src/multi_swarm_core/multi_swarm_core/dashboard/nicegui_app.py similarity index 99% rename from src/multi_swarm/dashboard/nicegui_app.py rename to src/multi_swarm_core/multi_swarm_core/dashboard/nicegui_app.py index cbd3fef..184cc83 100644 --- a/src/multi_swarm/dashboard/nicegui_app.py +++ b/src/multi_swarm_core/multi_swarm_core/dashboard/nicegui_app.py @@ -1,6 +1,6 @@ """NiceGUI dashboard — port progressivo da Streamlit. -Avvio: ``uv run python -m multi_swarm.dashboard.nicegui_app`` +Avvio: ``uv run python -m multi_swarm_core.dashboard.nicegui_app`` Default port 8080. Streamlit resta su 8501 durante la migrazione. Riusa ``dashboard.data`` (Repository helpers) senza modifiche al backend. @@ -28,7 +28,7 @@ import pandas as pd # type: ignore[import-untyped] import plotly.graph_objects as go # type: ignore[import-untyped] from nicegui import app, ui -from multi_swarm.dashboard.data import ( +from multi_swarm_core.dashboard.data import ( evaluations_df, generations_df, genomes_df, diff --git a/src/multi_swarm/data/__init__.py b/src/multi_swarm_core/multi_swarm_core/data/__init__.py similarity index 100% rename from src/multi_swarm/data/__init__.py rename to src/multi_swarm_core/multi_swarm_core/data/__init__.py diff --git a/src/multi_swarm/data/cerbero_ohlcv.py b/src/multi_swarm_core/multi_swarm_core/data/cerbero_ohlcv.py similarity index 100% rename from src/multi_swarm/data/cerbero_ohlcv.py rename to src/multi_swarm_core/multi_swarm_core/data/cerbero_ohlcv.py diff --git a/src/multi_swarm/data/splits.py b/src/multi_swarm_core/multi_swarm_core/data/splits.py similarity index 100% rename from src/multi_swarm/data/splits.py rename to src/multi_swarm_core/multi_swarm_core/data/splits.py diff --git a/src/multi_swarm/ga/__init__.py b/src/multi_swarm_core/multi_swarm_core/ga/__init__.py similarity index 100% rename from src/multi_swarm/ga/__init__.py rename to src/multi_swarm_core/multi_swarm_core/ga/__init__.py diff --git a/src/multi_swarm/ga/fitness.py b/src/multi_swarm_core/multi_swarm_core/ga/fitness.py similarity index 100% rename from src/multi_swarm/ga/fitness.py rename to src/multi_swarm_core/multi_swarm_core/ga/fitness.py diff --git a/src/multi_swarm/ga/initial.py b/src/multi_swarm_core/multi_swarm_core/ga/initial.py similarity index 100% rename from src/multi_swarm/ga/initial.py rename to src/multi_swarm_core/multi_swarm_core/ga/initial.py diff --git a/src/multi_swarm/ga/loop.py b/src/multi_swarm_core/multi_swarm_core/ga/loop.py similarity index 100% rename from src/multi_swarm/ga/loop.py rename to src/multi_swarm_core/multi_swarm_core/ga/loop.py diff --git a/src/multi_swarm/ga/selection.py b/src/multi_swarm_core/multi_swarm_core/ga/selection.py similarity index 100% rename from src/multi_swarm/ga/selection.py rename to src/multi_swarm_core/multi_swarm_core/ga/selection.py diff --git a/src/multi_swarm/ga/summary.py b/src/multi_swarm_core/multi_swarm_core/ga/summary.py similarity index 100% rename from src/multi_swarm/ga/summary.py rename to src/multi_swarm_core/multi_swarm_core/ga/summary.py diff --git a/src/multi_swarm/genome/__init__.py b/src/multi_swarm_core/multi_swarm_core/genome/__init__.py similarity index 100% rename from src/multi_swarm/genome/__init__.py rename to src/multi_swarm_core/multi_swarm_core/genome/__init__.py diff --git a/src/multi_swarm/genome/crossover.py b/src/multi_swarm_core/multi_swarm_core/genome/crossover.py similarity index 100% rename from src/multi_swarm/genome/crossover.py rename to src/multi_swarm_core/multi_swarm_core/genome/crossover.py diff --git a/src/multi_swarm/genome/hypothesis.py b/src/multi_swarm_core/multi_swarm_core/genome/hypothesis.py similarity index 100% rename from src/multi_swarm/genome/hypothesis.py rename to src/multi_swarm_core/multi_swarm_core/genome/hypothesis.py diff --git a/src/multi_swarm/genome/mutation.py b/src/multi_swarm_core/multi_swarm_core/genome/mutation.py similarity index 100% rename from src/multi_swarm/genome/mutation.py rename to src/multi_swarm_core/multi_swarm_core/genome/mutation.py diff --git a/src/multi_swarm/genome/mutation_prompt_llm.py b/src/multi_swarm_core/multi_swarm_core/genome/mutation_prompt_llm.py similarity index 100% rename from src/multi_swarm/genome/mutation_prompt_llm.py rename to src/multi_swarm_core/multi_swarm_core/genome/mutation_prompt_llm.py diff --git a/src/multi_swarm/llm/__init__.py b/src/multi_swarm_core/multi_swarm_core/llm/__init__.py similarity index 100% rename from src/multi_swarm/llm/__init__.py rename to src/multi_swarm_core/multi_swarm_core/llm/__init__.py diff --git a/src/multi_swarm/llm/client.py b/src/multi_swarm_core/multi_swarm_core/llm/client.py similarity index 100% rename from src/multi_swarm/llm/client.py rename to src/multi_swarm_core/multi_swarm_core/llm/client.py diff --git a/src/multi_swarm/llm/cost_tracker.py b/src/multi_swarm_core/multi_swarm_core/llm/cost_tracker.py similarity index 100% rename from src/multi_swarm/llm/cost_tracker.py rename to src/multi_swarm_core/multi_swarm_core/llm/cost_tracker.py diff --git a/src/multi_swarm/metrics/__init__.py b/src/multi_swarm_core/multi_swarm_core/metrics/__init__.py similarity index 100% rename from src/multi_swarm/metrics/__init__.py rename to src/multi_swarm_core/multi_swarm_core/metrics/__init__.py diff --git a/src/multi_swarm/metrics/basic.py b/src/multi_swarm_core/multi_swarm_core/metrics/basic.py similarity index 100% rename from src/multi_swarm/metrics/basic.py rename to src/multi_swarm_core/multi_swarm_core/metrics/basic.py diff --git a/src/multi_swarm/metrics/diversity.py b/src/multi_swarm_core/multi_swarm_core/metrics/diversity.py similarity index 100% rename from src/multi_swarm/metrics/diversity.py rename to src/multi_swarm_core/multi_swarm_core/metrics/diversity.py diff --git a/src/multi_swarm/metrics/dsr.py b/src/multi_swarm_core/multi_swarm_core/metrics/dsr.py similarity index 100% rename from src/multi_swarm/metrics/dsr.py rename to src/multi_swarm_core/multi_swarm_core/metrics/dsr.py diff --git a/src/multi_swarm/orchestrator/__init__.py b/src/multi_swarm_core/multi_swarm_core/orchestrator/__init__.py similarity index 100% rename from src/multi_swarm/orchestrator/__init__.py rename to src/multi_swarm_core/multi_swarm_core/orchestrator/__init__.py diff --git a/src/multi_swarm/orchestrator/run.py b/src/multi_swarm_core/multi_swarm_core/orchestrator/run.py similarity index 100% rename from src/multi_swarm/orchestrator/run.py rename to src/multi_swarm_core/multi_swarm_core/orchestrator/run.py diff --git a/src/multi_swarm/paper_trading/__init__.py b/src/multi_swarm_core/multi_swarm_core/paper_trading/__init__.py similarity index 100% rename from src/multi_swarm/paper_trading/__init__.py rename to src/multi_swarm_core/multi_swarm_core/paper_trading/__init__.py diff --git a/src/multi_swarm/paper_trading/executor.py b/src/multi_swarm_core/multi_swarm_core/paper_trading/executor.py similarity index 100% rename from src/multi_swarm/paper_trading/executor.py rename to src/multi_swarm_core/multi_swarm_core/paper_trading/executor.py diff --git a/src/multi_swarm/paper_trading/persistence.py b/src/multi_swarm_core/multi_swarm_core/paper_trading/persistence.py similarity index 98% rename from src/multi_swarm/paper_trading/persistence.py rename to src/multi_swarm_core/multi_swarm_core/paper_trading/persistence.py index 1f1a234..b58c620 100644 --- a/src/multi_swarm/paper_trading/persistence.py +++ b/src/multi_swarm_core/multi_swarm_core/paper_trading/persistence.py @@ -1,5 +1,5 @@ """Persistenza paper-trading: usa lo stesso ``runs.db`` con tabelle dedicate -``paper_trading_*`` (vedi :mod:`multi_swarm.persistence.schema`). +``paper_trading_*`` (vedi :mod:`multi_swarm_core.persistence.schema`). """ from __future__ import annotations diff --git a/src/multi_swarm/paper_trading/portfolio.py b/src/multi_swarm_core/multi_swarm_core/paper_trading/portfolio.py similarity index 100% rename from src/multi_swarm/paper_trading/portfolio.py rename to src/multi_swarm_core/multi_swarm_core/paper_trading/portfolio.py diff --git a/src/multi_swarm/persistence/__init__.py b/src/multi_swarm_core/multi_swarm_core/persistence/__init__.py similarity index 100% rename from src/multi_swarm/persistence/__init__.py rename to src/multi_swarm_core/multi_swarm_core/persistence/__init__.py diff --git a/src/multi_swarm/persistence/repository.py b/src/multi_swarm_core/multi_swarm_core/persistence/repository.py similarity index 100% rename from src/multi_swarm/persistence/repository.py rename to src/multi_swarm_core/multi_swarm_core/persistence/repository.py diff --git a/src/multi_swarm/persistence/schema.py b/src/multi_swarm_core/multi_swarm_core/persistence/schema.py similarity index 100% rename from src/multi_swarm/persistence/schema.py rename to src/multi_swarm_core/multi_swarm_core/persistence/schema.py diff --git a/src/multi_swarm/protocol/__init__.py b/src/multi_swarm_core/multi_swarm_core/protocol/__init__.py similarity index 100% rename from src/multi_swarm/protocol/__init__.py rename to src/multi_swarm_core/multi_swarm_core/protocol/__init__.py diff --git a/src/multi_swarm/protocol/compiler.py b/src/multi_swarm_core/multi_swarm_core/protocol/compiler.py similarity index 100% rename from src/multi_swarm/protocol/compiler.py rename to src/multi_swarm_core/multi_swarm_core/protocol/compiler.py diff --git a/src/multi_swarm/protocol/grammar.py b/src/multi_swarm_core/multi_swarm_core/protocol/grammar.py similarity index 100% rename from src/multi_swarm/protocol/grammar.py rename to src/multi_swarm_core/multi_swarm_core/protocol/grammar.py diff --git a/src/multi_swarm/protocol/parser.py b/src/multi_swarm_core/multi_swarm_core/protocol/parser.py similarity index 100% rename from src/multi_swarm/protocol/parser.py rename to src/multi_swarm_core/multi_swarm_core/protocol/parser.py diff --git a/src/multi_swarm/protocol/validator.py b/src/multi_swarm_core/multi_swarm_core/protocol/validator.py similarity index 100% rename from src/multi_swarm/protocol/validator.py rename to src/multi_swarm_core/multi_swarm_core/protocol/validator.py diff --git a/src/multi_swarm/py.typed b/src/multi_swarm_core/multi_swarm_core/py.typed similarity index 100% rename from src/multi_swarm/py.typed rename to src/multi_swarm_core/multi_swarm_core/py.typed diff --git a/tests/integration/test_e2e_minimal_run.py b/tests/integration/test_e2e_minimal_run.py index 32cec27..b9b7067 100644 --- a/tests/integration/test_e2e_minimal_run.py +++ b/tests/integration/test_e2e_minimal_run.py @@ -5,10 +5,10 @@ import numpy as np import pandas as pd import pytest -from multi_swarm.genome.hypothesis import ModelTier -from multi_swarm.llm.client import CompletionResult -from multi_swarm.orchestrator.run import RunConfig, run_phase1 -from multi_swarm.persistence.repository import Repository +from multi_swarm_core.genome.hypothesis import ModelTier +from multi_swarm_core.llm.client import CompletionResult +from multi_swarm_core.orchestrator.run import RunConfig, run_phase1 +from multi_swarm_core.persistence.repository import Repository @pytest.fixture diff --git a/tests/integration/test_ga_loop_with_prompt_mutator.py b/tests/integration/test_ga_loop_with_prompt_mutator.py index d1fc077..094e2e2 100644 --- a/tests/integration/test_ga_loop_with_prompt_mutator.py +++ b/tests/integration/test_ga_loop_with_prompt_mutator.py @@ -9,8 +9,8 @@ from __future__ import annotations import random from dataclasses import dataclass -from multi_swarm.ga.loop import GAConfig, next_generation -from multi_swarm.genome.hypothesis import HypothesisAgentGenome, ModelTier +from multi_swarm_core.ga.loop import GAConfig, next_generation +from multi_swarm_core.genome.hypothesis import HypothesisAgentGenome, ModelTier _PROMPT_TEMPLATES = ( "Strategia mean-reversion 1h. Entry long RSI(14) < 30 e close > SMA(50). Stop 2%.", diff --git a/tests/unit/test_adversarial.py b/tests/unit/test_adversarial.py index 3c0b663..f9b381b 100644 --- a/tests/unit/test_adversarial.py +++ b/tests/unit/test_adversarial.py @@ -4,14 +4,14 @@ import numpy as np import pandas as pd import pytest -from multi_swarm.agents.adversarial import ( +from multi_swarm_core.agents.adversarial import ( AdversarialAgent, AdversarialReport, Severity, ) -from multi_swarm.backtest.engine import BacktestResult -from multi_swarm.backtest.orders import Side, Trade -from multi_swarm.protocol.parser import parse_strategy +from multi_swarm_core.backtest.engine import BacktestResult +from multi_swarm_core.backtest.orders import Side, Trade +from multi_swarm_core.protocol.parser import parse_strategy @pytest.fixture @@ -178,10 +178,10 @@ def test_undertrading_under_10_is_high(monkeypatch: pytest.MonkeyPatch, return lambda df: fake_signals monkeypatch.setattr( - "multi_swarm.agents.adversarial.BacktestEngine.run", fake_run + "multi_swarm_core.agents.adversarial.BacktestEngine.run", fake_run ) monkeypatch.setattr( - "multi_swarm.agents.adversarial.compile_strategy", fake_compile + "multi_swarm_core.agents.adversarial.compile_strategy", fake_compile ) src = _MINIMAL_STRATEGY_SRC @@ -220,8 +220,8 @@ def test_undertrading_threshold_parametric(monkeypatch: pytest.MonkeyPatch, def fake_compile(strategy): # type: ignore[no-untyped-def] return lambda df: fake_signals - monkeypatch.setattr("multi_swarm.agents.adversarial.BacktestEngine.run", fake_run) - monkeypatch.setattr("multi_swarm.agents.adversarial.compile_strategy", fake_compile) + 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) # Default threshold 10: 15 trade NON killato @@ -269,10 +269,10 @@ def test_overtrading_with_tighter_threshold(monkeypatch: pytest.MonkeyPatch, return lambda df: fake_signals monkeypatch.setattr( - "multi_swarm.agents.adversarial.BacktestEngine.run", fake_run + "multi_swarm_core.agents.adversarial.BacktestEngine.run", fake_run ) monkeypatch.setattr( - "multi_swarm.agents.adversarial.compile_strategy", fake_compile + "multi_swarm_core.agents.adversarial.compile_strategy", fake_compile ) src = _MINIMAL_STRATEGY_SRC @@ -315,10 +315,10 @@ def test_flat_too_long_flagged(monkeypatch: pytest.MonkeyPatch, return lambda df: fake_signals monkeypatch.setattr( - "multi_swarm.agents.adversarial.BacktestEngine.run", fake_run + "multi_swarm_core.agents.adversarial.BacktestEngine.run", fake_run ) monkeypatch.setattr( - "multi_swarm.agents.adversarial.compile_strategy", fake_compile + "multi_swarm_core.agents.adversarial.compile_strategy", fake_compile ) src = _MINIMAL_STRATEGY_SRC @@ -367,10 +367,10 @@ def test_fees_eat_alpha_flagged(monkeypatch: pytest.MonkeyPatch, return lambda df: fake_signals monkeypatch.setattr( - "multi_swarm.agents.adversarial.BacktestEngine.run", fake_run + "multi_swarm_core.agents.adversarial.BacktestEngine.run", fake_run ) monkeypatch.setattr( - "multi_swarm.agents.adversarial.compile_strategy", fake_compile + "multi_swarm_core.agents.adversarial.compile_strategy", fake_compile ) src = _MINIMAL_STRATEGY_SRC @@ -413,10 +413,10 @@ def test_time_in_market_too_high_flagged(monkeypatch: pytest.MonkeyPatch, return lambda df: fake_signals monkeypatch.setattr( - "multi_swarm.agents.adversarial.BacktestEngine.run", fake_run + "multi_swarm_core.agents.adversarial.BacktestEngine.run", fake_run ) monkeypatch.setattr( - "multi_swarm.agents.adversarial.compile_strategy", fake_compile + "multi_swarm_core.agents.adversarial.compile_strategy", fake_compile ) src = _MINIMAL_STRATEGY_SRC @@ -461,10 +461,10 @@ def test_reasonable_balanced_strategy_not_flagged(monkeypatch: pytest.MonkeyPatc return lambda df: fake_signals monkeypatch.setattr( - "multi_swarm.agents.adversarial.BacktestEngine.run", fake_run + "multi_swarm_core.agents.adversarial.BacktestEngine.run", fake_run ) monkeypatch.setattr( - "multi_swarm.agents.adversarial.compile_strategy", fake_compile + "multi_swarm_core.agents.adversarial.compile_strategy", fake_compile ) src = _MINIMAL_STRATEGY_SRC diff --git a/tests/unit/test_backtest_engine.py b/tests/unit/test_backtest_engine.py index a3a8f9d..a764984 100644 --- a/tests/unit/test_backtest_engine.py +++ b/tests/unit/test_backtest_engine.py @@ -2,8 +2,8 @@ import numpy as np import pandas as pd import pytest -from multi_swarm.backtest.engine import BacktestEngine -from multi_swarm.backtest.orders import Side +from multi_swarm_core.backtest.engine import BacktestEngine +from multi_swarm_core.backtest.orders import Side @pytest.fixture diff --git a/tests/unit/test_backtest_orders.py b/tests/unit/test_backtest_orders.py index 9c0ee6a..51a3bf2 100644 --- a/tests/unit/test_backtest_orders.py +++ b/tests/unit/test_backtest_orders.py @@ -2,7 +2,7 @@ from datetime import UTC, datetime import pytest -from multi_swarm.backtest.orders import Order, Position, Side, Trade +from multi_swarm_core.backtest.orders import Order, Position, Side, Trade def test_order_validates_side() -> None: diff --git a/tests/unit/test_cerbero_client.py b/tests/unit/test_cerbero_client.py index 3027a6d..de724cb 100644 --- a/tests/unit/test_cerbero_client.py +++ b/tests/unit/test_cerbero_client.py @@ -1,7 +1,7 @@ import pytest import responses -from multi_swarm.cerbero.client import CerberoClient +from multi_swarm_core.cerbero.client import CerberoClient @responses.activate diff --git a/tests/unit/test_cerbero_ohlcv.py b/tests/unit/test_cerbero_ohlcv.py index 81ecf19..23cf90f 100644 --- a/tests/unit/test_cerbero_ohlcv.py +++ b/tests/unit/test_cerbero_ohlcv.py @@ -6,7 +6,7 @@ from pathlib import Path import pandas as pd import pytest -from multi_swarm.data.cerbero_ohlcv import CerberoOHLCVLoader, OHLCVRequest +from multi_swarm_core.data.cerbero_ohlcv import CerberoOHLCVLoader, OHLCVRequest @pytest.fixture diff --git a/tests/unit/test_cerbero_tools.py b/tests/unit/test_cerbero_tools.py index 06cfdb5..ef1193a 100644 --- a/tests/unit/test_cerbero_tools.py +++ b/tests/unit/test_cerbero_tools.py @@ -1,6 +1,6 @@ import pytest -from multi_swarm.cerbero.tools import CerberoTools +from multi_swarm_core.cerbero.tools import CerberoTools def test_tools_dispatch_sma(mocker): diff --git a/tests/unit/test_config.py b/tests/unit/test_config.py index f51ffb9..293103b 100644 --- a/tests/unit/test_config.py +++ b/tests/unit/test_config.py @@ -1,4 +1,4 @@ -"""Tests for multi_swarm.config.Settings. +"""Tests for multi_swarm_core.config.Settings. Note on .env isolation: The happy-path test relies on monkeypatch.setenv to provide values. @@ -10,7 +10,7 @@ absence of required env vars. This keeps the test deterministic both in CI import pytest -from multi_swarm.config import Settings +from multi_swarm_core.config import Settings def test_settings_loads_from_env(monkeypatch: pytest.MonkeyPatch) -> None: diff --git a/tests/unit/test_cost_tracker.py b/tests/unit/test_cost_tracker.py index 89fb69e..bfb89c5 100644 --- a/tests/unit/test_cost_tracker.py +++ b/tests/unit/test_cost_tracker.py @@ -1,5 +1,5 @@ -from multi_swarm.genome.hypothesis import ModelTier -from multi_swarm.llm.cost_tracker import CostTracker, estimate_cost +from multi_swarm_core.genome.hypothesis import ModelTier +from multi_swarm_core.llm.cost_tracker import CostTracker, estimate_cost def test_estimate_cost_tier_c(): diff --git a/tests/unit/test_diversity.py b/tests/unit/test_diversity.py index fbcbfb3..b0e759b 100644 --- a/tests/unit/test_diversity.py +++ b/tests/unit/test_diversity.py @@ -1,6 +1,6 @@ from __future__ import annotations -from multi_swarm.metrics.diversity import population_prompt_diversity +from multi_swarm_core.metrics.diversity import population_prompt_diversity def test_empty_or_single_prompt_zero_diversity() -> None: diff --git a/tests/unit/test_falsification.py b/tests/unit/test_falsification.py index 3d59173..81cd92f 100644 --- a/tests/unit/test_falsification.py +++ b/tests/unit/test_falsification.py @@ -4,8 +4,8 @@ import numpy as np import pandas as pd import pytest -from multi_swarm.agents.falsification import FalsificationAgent, FalsificationReport -from multi_swarm.protocol.parser import parse_strategy +from multi_swarm_core.agents.falsification import FalsificationAgent, FalsificationReport +from multi_swarm_core.protocol.parser import parse_strategy @pytest.fixture diff --git a/tests/unit/test_fitness.py b/tests/unit/test_fitness.py index 85df8d3..3f1cec7 100644 --- a/tests/unit/test_fitness.py +++ b/tests/unit/test_fitness.py @@ -1,8 +1,8 @@ from itertools import pairwise -from multi_swarm.agents.adversarial import AdversarialReport, Finding, Severity -from multi_swarm.agents.falsification import FalsificationReport -from multi_swarm.ga.fitness import compute_fitness +from multi_swarm_core.agents.adversarial import AdversarialReport, Finding, Severity +from multi_swarm_core.agents.falsification import FalsificationReport +from multi_swarm_core.ga.fitness import compute_fitness def make_falsification( diff --git a/tests/unit/test_ga_initial.py b/tests/unit/test_ga_initial.py index 00e11d8..bb71c2b 100644 --- a/tests/unit/test_ga_initial.py +++ b/tests/unit/test_ga_initial.py @@ -1,7 +1,7 @@ import random -from multi_swarm.ga.initial import build_initial_population -from multi_swarm.genome.hypothesis import ModelTier +from multi_swarm_core.ga.initial import build_initial_population +from multi_swarm_core.genome.hypothesis import ModelTier def test_initial_population_size(): diff --git a/tests/unit/test_ga_loop.py b/tests/unit/test_ga_loop.py index 6e52ebe..943b21c 100644 --- a/tests/unit/test_ga_loop.py +++ b/tests/unit/test_ga_loop.py @@ -1,7 +1,7 @@ import random -from multi_swarm.ga.loop import GAConfig, next_generation -from multi_swarm.genome.hypothesis import HypothesisAgentGenome, ModelTier +from multi_swarm_core.ga.loop import GAConfig, next_generation +from multi_swarm_core.genome.hypothesis import HypothesisAgentGenome, ModelTier def make(idx: int) -> HypothesisAgentGenome: diff --git a/tests/unit/test_ga_summary.py b/tests/unit/test_ga_summary.py index 55fb360..3d91d5d 100644 --- a/tests/unit/test_ga_summary.py +++ b/tests/unit/test_ga_summary.py @@ -2,7 +2,7 @@ import math import pytest -from multi_swarm.ga.summary import generation_summary +from multi_swarm_core.ga.summary import generation_summary def test_summary_basic_stats(): diff --git a/tests/unit/test_genome_crossover.py b/tests/unit/test_genome_crossover.py index 7477a14..bcb5cc3 100644 --- a/tests/unit/test_genome_crossover.py +++ b/tests/unit/test_genome_crossover.py @@ -1,7 +1,7 @@ import random -from multi_swarm.genome.crossover import uniform_crossover -from multi_swarm.genome.hypothesis import HypothesisAgentGenome, ModelTier +from multi_swarm_core.genome.crossover import uniform_crossover +from multi_swarm_core.genome.hypothesis import HypothesisAgentGenome, ModelTier def make(name: str) -> HypothesisAgentGenome: diff --git a/tests/unit/test_genome_hypothesis.py b/tests/unit/test_genome_hypothesis.py index 4948543..88668a2 100644 --- a/tests/unit/test_genome_hypothesis.py +++ b/tests/unit/test_genome_hypothesis.py @@ -1,4 +1,4 @@ -from multi_swarm.genome.hypothesis import HypothesisAgentGenome, ModelTier +from multi_swarm_core.genome.hypothesis import HypothesisAgentGenome, ModelTier def test_genome_creation_defaults(): diff --git a/tests/unit/test_genome_mutation.py b/tests/unit/test_genome_mutation.py index 41e5159..36a1382 100644 --- a/tests/unit/test_genome_mutation.py +++ b/tests/unit/test_genome_mutation.py @@ -2,8 +2,8 @@ import random import pytest -from multi_swarm.genome.hypothesis import HypothesisAgentGenome, ModelTier -from multi_swarm.genome.mutation import ( +from multi_swarm_core.genome.hypothesis import HypothesisAgentGenome, ModelTier +from multi_swarm_core.genome.mutation import ( COGNITIVE_STYLES, FEATURE_POOL, mutate_cognitive_style, diff --git a/tests/unit/test_hypothesis_agent.py b/tests/unit/test_hypothesis_agent.py index 23a3136..3477e60 100644 --- a/tests/unit/test_hypothesis_agent.py +++ b/tests/unit/test_hypothesis_agent.py @@ -1,8 +1,8 @@ import json -from multi_swarm.agents.hypothesis import HypothesisAgent, MarketSummary -from multi_swarm.genome.hypothesis import HypothesisAgentGenome, ModelTier -from multi_swarm.llm.client import CompletionResult, EmptyCompletionError +from multi_swarm_core.agents.hypothesis import HypothesisAgent, MarketSummary +from multi_swarm_core.genome.hypothesis import HypothesisAgentGenome, ModelTier +from multi_swarm_core.llm.client import CompletionResult, EmptyCompletionError def make_summary() -> MarketSummary: diff --git a/tests/unit/test_llm_client.py b/tests/unit/test_llm_client.py index 8514319..bd98178 100644 --- a/tests/unit/test_llm_client.py +++ b/tests/unit/test_llm_client.py @@ -1,7 +1,7 @@ import pytest -from multi_swarm.genome.hypothesis import HypothesisAgentGenome, ModelTier -from multi_swarm.llm.client import CompletionResult, LLMClient +from multi_swarm_core.genome.hypothesis import HypothesisAgentGenome, ModelTier +from multi_swarm_core.llm.client import CompletionResult, LLMClient def make_genome(tier: ModelTier) -> HypothesisAgentGenome: @@ -23,7 +23,7 @@ def test_completion_tier_c_uses_openrouter(mocker): fake_response.usage = mocker.MagicMock(prompt_tokens=100, completion_tokens=200) fake_openai.chat.completions.create.return_value = fake_response - mocker.patch("multi_swarm.llm.client.OpenAI", return_value=fake_openai) + mocker.patch("multi_swarm_core.llm.client.OpenAI", return_value=fake_openai) client = LLMClient(openrouter_api_key="or-x") g = make_genome(ModelTier.C) @@ -43,7 +43,7 @@ def test_completion_tier_b_uses_openrouter_with_anthropic_model(mocker): 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.llm.client.OpenAI", return_value=fake_openai) + mocker.patch("multi_swarm_core.llm.client.OpenAI", return_value=fake_openai) client = LLMClient(openrouter_api_key="or-x") g = make_genome(ModelTier.B) @@ -67,7 +67,7 @@ def test_completion_retries_on_connection_error(mocker): fake_openai.chat.completions.create.side_effect = openai.APIConnectionError( request=mocker.MagicMock() ) - mocker.patch("multi_swarm.llm.client.OpenAI", return_value=fake_openai) + mocker.patch("multi_swarm_core.llm.client.OpenAI", return_value=fake_openai) client = LLMClient(openrouter_api_key="or-x") g = make_genome(ModelTier.C) @@ -86,7 +86,7 @@ def test_completion_uses_custom_model_tier_c(mocker): ] fake_response.usage = mocker.MagicMock(prompt_tokens=10, completion_tokens=20) fake_openai.chat.completions.create.return_value = fake_response - mocker.patch("multi_swarm.llm.client.OpenAI", return_value=fake_openai) + mocker.patch("multi_swarm_core.llm.client.OpenAI", return_value=fake_openai) client = LLMClient( openrouter_api_key="or-x", @@ -109,7 +109,7 @@ def test_completion_uses_custom_model_tier_b(mocker): ] fake_response.usage = mocker.MagicMock(prompt_tokens=10, completion_tokens=20) fake_openai.chat.completions.create.return_value = fake_response - mocker.patch("multi_swarm.llm.client.OpenAI", return_value=fake_openai) + mocker.patch("multi_swarm_core.llm.client.OpenAI", return_value=fake_openai) client = LLMClient( openrouter_api_key="or-x", @@ -130,7 +130,7 @@ def test_completion_tier_s_uses_openrouter_with_anthropic_model(mocker): 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.llm.client.OpenAI", return_value=fake_openai) + mocker.patch("multi_swarm_core.llm.client.OpenAI", return_value=fake_openai) client = LLMClient(openrouter_api_key="or-x") g = make_genome(ModelTier.S) @@ -149,7 +149,7 @@ def test_completion_tier_a_uses_openrouter_with_anthropic_model(mocker): 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.llm.client.OpenAI", return_value=fake_openai) + mocker.patch("multi_swarm_core.llm.client.OpenAI", return_value=fake_openai) client = LLMClient(openrouter_api_key="or-x") g = make_genome(ModelTier.A) @@ -170,7 +170,7 @@ def test_completion_tier_d_uses_openrouter_with_llama(mocker): ] fake_response.usage = mocker.MagicMock(prompt_tokens=30, completion_tokens=70) fake_openai.chat.completions.create.return_value = fake_response - mocker.patch("multi_swarm.llm.client.OpenAI", return_value=fake_openai) + mocker.patch("multi_swarm_core.llm.client.OpenAI", return_value=fake_openai) client = LLMClient(openrouter_api_key="or-x") g = make_genome(ModelTier.D) @@ -191,7 +191,7 @@ def test_completion_uses_custom_model_tier_s(mocker): ] fake_response.usage = mocker.MagicMock(prompt_tokens=10, completion_tokens=20) fake_openai.chat.completions.create.return_value = fake_response - mocker.patch("multi_swarm.llm.client.OpenAI", return_value=fake_openai) + mocker.patch("multi_swarm_core.llm.client.OpenAI", return_value=fake_openai) client = LLMClient( openrouter_api_key="or-x", @@ -221,7 +221,7 @@ def test_completion_succeeds_after_one_retry(mocker): openai.APITimeoutError(request=mocker.MagicMock()), fake_response, ] - mocker.patch("multi_swarm.llm.client.OpenAI", return_value=fake_openai) + mocker.patch("multi_swarm_core.llm.client.OpenAI", return_value=fake_openai) client = LLMClient(openrouter_api_key="or-x") g = make_genome(ModelTier.C) diff --git a/tests/unit/test_market_summary.py b/tests/unit/test_market_summary.py index a89d640..a3b245e 100644 --- a/tests/unit/test_market_summary.py +++ b/tests/unit/test_market_summary.py @@ -1,7 +1,7 @@ import numpy as np import pandas as pd -from multi_swarm.agents.market_summary import build_market_summary +from multi_swarm_core.agents.market_summary import build_market_summary def test_build_summary_basic() -> None: diff --git a/tests/unit/test_metrics_basic.py b/tests/unit/test_metrics_basic.py index 8ed2626..7c67bae 100644 --- a/tests/unit/test_metrics_basic.py +++ b/tests/unit/test_metrics_basic.py @@ -2,7 +2,7 @@ import numpy as np import pandas as pd import pytest -from multi_swarm.metrics.basic import max_drawdown, sharpe_ratio, total_return +from multi_swarm_core.metrics.basic import max_drawdown, sharpe_ratio, total_return def test_sharpe_zero_returns(): diff --git a/tests/unit/test_metrics_dsr.py b/tests/unit/test_metrics_dsr.py index b6ed841..aa942c2 100644 --- a/tests/unit/test_metrics_dsr.py +++ b/tests/unit/test_metrics_dsr.py @@ -1,7 +1,7 @@ import numpy as np import pandas as pd -from multi_swarm.metrics.dsr import deflated_sharpe_ratio, expected_max_sharpe +from multi_swarm_core.metrics.dsr import deflated_sharpe_ratio, expected_max_sharpe def test_expected_max_sharpe_grows_with_n_trials(): diff --git a/tests/unit/test_mutation_dispatcher.py b/tests/unit/test_mutation_dispatcher.py index 8adb6cf..d314b78 100644 --- a/tests/unit/test_mutation_dispatcher.py +++ b/tests/unit/test_mutation_dispatcher.py @@ -4,8 +4,8 @@ import random from collections import Counter from dataclasses import dataclass -from multi_swarm.genome.hypothesis import HypothesisAgentGenome, ModelTier -from multi_swarm.genome.mutation import weighted_random_mutate +from multi_swarm_core.genome.hypothesis import HypothesisAgentGenome, ModelTier +from multi_swarm_core.genome.mutation import weighted_random_mutate _PROMPT = ( "Strategia mean-reversion 1h BTC. Entry long quando RSI(14) < 30 e " diff --git a/tests/unit/test_mutation_prompt_llm.py b/tests/unit/test_mutation_prompt_llm.py index 1a0eccd..c5abf37 100644 --- a/tests/unit/test_mutation_prompt_llm.py +++ b/tests/unit/test_mutation_prompt_llm.py @@ -3,8 +3,8 @@ from __future__ import annotations import random from dataclasses import dataclass -from multi_swarm.genome.hypothesis import HypothesisAgentGenome, ModelTier -from multi_swarm.genome.mutation_prompt_llm import ( +from multi_swarm_core.genome.hypothesis import HypothesisAgentGenome, ModelTier +from multi_swarm_core.genome.mutation_prompt_llm import ( MUTATION_INSTRUCTIONS, _extract_prompt, is_valid_prompt, diff --git a/tests/unit/test_protocol_compiler.py b/tests/unit/test_protocol_compiler.py index 8a92e4d..44c1289 100644 --- a/tests/unit/test_protocol_compiler.py +++ b/tests/unit/test_protocol_compiler.py @@ -6,9 +6,9 @@ import numpy as np import pandas as pd import pytest -from multi_swarm.backtest.orders import Side -from multi_swarm.protocol.compiler import compile_strategy -from multi_swarm.protocol.parser import parse_strategy +from multi_swarm_core.backtest.orders import Side +from multi_swarm_core.protocol.compiler import compile_strategy +from multi_swarm_core.protocol.parser import parse_strategy @pytest.fixture diff --git a/tests/unit/test_protocol_parser.py b/tests/unit/test_protocol_parser.py index 1005013..9451e92 100644 --- a/tests/unit/test_protocol_parser.py +++ b/tests/unit/test_protocol_parser.py @@ -2,7 +2,7 @@ import json import pytest -from multi_swarm.protocol.grammar import ( +from multi_swarm_core.protocol.grammar import ( ACTION_VALUES, ALL_OPS, COMPARATOR_OPS, @@ -10,7 +10,7 @@ from multi_swarm.protocol.grammar import ( KIND_VALUES, LOGICAL_OPS, ) -from multi_swarm.protocol.parser import ( +from multi_swarm_core.protocol.parser import ( FeatureNode, IndicatorNode, LiteralNode, diff --git a/tests/unit/test_protocol_validator.py b/tests/unit/test_protocol_validator.py index cd5d67e..13e663c 100644 --- a/tests/unit/test_protocol_validator.py +++ b/tests/unit/test_protocol_validator.py @@ -2,8 +2,8 @@ import json import pytest -from multi_swarm.protocol.parser import parse_strategy -from multi_swarm.protocol.validator import ValidationError, validate_strategy +from multi_swarm_core.protocol.parser import parse_strategy +from multi_swarm_core.protocol.validator import ValidationError, validate_strategy def _wrap(condition: dict, action: str = "entry-long") -> str: diff --git a/tests/unit/test_repository.py b/tests/unit/test_repository.py index e5e347c..8b4f3af 100644 --- a/tests/unit/test_repository.py +++ b/tests/unit/test_repository.py @@ -1,8 +1,8 @@ import json from pathlib import Path -from multi_swarm.genome.hypothesis import HypothesisAgentGenome, ModelTier -from multi_swarm.persistence.repository import Repository +from multi_swarm_core.genome.hypothesis import HypothesisAgentGenome, ModelTier +from multi_swarm_core.persistence.repository import Repository def make_genome(idx: int) -> HypothesisAgentGenome: diff --git a/tests/unit/test_selection.py b/tests/unit/test_selection.py index dca01d5..619590f 100644 --- a/tests/unit/test_selection.py +++ b/tests/unit/test_selection.py @@ -1,7 +1,7 @@ import random -from multi_swarm.ga.selection import elite_select, tournament_select -from multi_swarm.genome.hypothesis import HypothesisAgentGenome, ModelTier +from multi_swarm_core.ga.selection import elite_select, tournament_select +from multi_swarm_core.genome.hypothesis import HypothesisAgentGenome, ModelTier def make(idx: int) -> HypothesisAgentGenome: diff --git a/tests/unit/test_splits.py b/tests/unit/test_splits.py index c3151b7..df6383f 100644 --- a/tests/unit/test_splits.py +++ b/tests/unit/test_splits.py @@ -1,7 +1,7 @@ import pandas as pd import pytest -from multi_swarm.data.splits import expanding_walk_forward +from multi_swarm_core.data.splits import expanding_walk_forward @pytest.fixture From cd4c3131d9cb8a90ea036f803aeda9c946f2da47 Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Fri, 15 May 2026 17:45:53 +0000 Subject: [PATCH 03/11] feat(workspace): setup uv workspace con 2 member (multi-swarm-core + strategy-crypto) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Root pyproject.toml come workspace coordinator (no codice, dev deps + tool config) - src/multi_swarm_core/pyproject.toml: package core con dipendenze (pandas, openai, pydantic, ...) - src/strategy_crypto/pyproject.toml: package strategia con multi-swarm-core come workspace dep - Aggiunte env var GA_DB_PATH, STRATEGY_CRYPTO_DB_PATH, DASHBOARD_ROOT_PATH in .env.example - Patch Dockerfile (commento, label, healthcheck per workspace) - .gitignore: aggiunto .omc/ + state/*.db generico - Rigenerato uv.lock dal workspace uv tree mostra: strategy-crypto v0.1.0 ├── multi-swarm-core v0.1.0 ├── nicegui v3.12.0 └── ... Co-Authored-By: Claude Opus 4.7 (1M context) --- .env.example | 11 +- .gitignore | 7 + .omc/project-memory.json | 234 ---------- .../798fb16b-8751-4e21-a558-26b5cea16bd8.json | 8 - .../d997b670-db00-45b4-a478-cdad4d7fd603.json | 8 - .../ed0d4e09-401a-47e4-b354-336908ba30c2.json | 8 - ...798fb16b-8751-4e21-a558-26b5cea16bd8.jsonl | 31 -- ...c410f859-5a67-475c-a941-8e2c50ed3a11.jsonl | 6 - ...d997b670-db00-45b4-a478-cdad4d7fd603.jsonl | 16 - ...ed0d4e09-401a-47e4-b354-336908ba30c2.jsonl | 1 - .omc/state/hud-state.json | 6 - .omc/state/hud-stdin-cache.json | 1 - .omc/state/idle-notif-cooldown.json | 3 - .omc/state/last-tool-error.json | 7 - .omc/state/mission-state.json | 145 ------ .omc/state/subagent-tracking.json | 35 -- Dockerfile | 24 +- pyproject.toml | 39 +- src/multi_swarm_core/pyproject.toml | 25 + src/strategy_crypto/pyproject.toml | 20 + uv.lock | 435 ++++++++++-------- 21 files changed, 318 insertions(+), 752 deletions(-) delete mode 100644 .omc/project-memory.json delete mode 100644 .omc/sessions/798fb16b-8751-4e21-a558-26b5cea16bd8.json delete mode 100644 .omc/sessions/d997b670-db00-45b4-a478-cdad4d7fd603.json delete mode 100644 .omc/sessions/ed0d4e09-401a-47e4-b354-336908ba30c2.json delete mode 100644 .omc/state/agent-replay-798fb16b-8751-4e21-a558-26b5cea16bd8.jsonl delete mode 100644 .omc/state/agent-replay-c410f859-5a67-475c-a941-8e2c50ed3a11.jsonl delete mode 100644 .omc/state/agent-replay-d997b670-db00-45b4-a478-cdad4d7fd603.jsonl delete mode 100644 .omc/state/agent-replay-ed0d4e09-401a-47e4-b354-336908ba30c2.jsonl delete mode 100644 .omc/state/hud-state.json delete mode 100644 .omc/state/hud-stdin-cache.json delete mode 100644 .omc/state/idle-notif-cooldown.json delete mode 100644 .omc/state/last-tool-error.json delete mode 100644 .omc/state/mission-state.json delete mode 100644 .omc/state/subagent-tracking.json create mode 100644 src/multi_swarm_core/pyproject.toml create mode 100644 src/strategy_crypto/pyproject.toml diff --git a/.env.example b/.env.example index 5fb0f59..bf1dc0e 100644 --- a/.env.example +++ b/.env.example @@ -21,13 +21,20 @@ LLM_MODEL_TIER_D=openai/gpt-oss-20b RUN_NAME=phase1-spike-001 DATA_DIR=./data SERIES_DIR=./series -DB_PATH=./runs.db + +# Database paths (split per dominio): +# - GA_DB_PATH: tabelle GA universali (runs, generations, genomes, evaluations) +# - STRATEGY_CRYPTO_DB_PATH: tabelle paper_trading_* per la strategia crypto +GA_DB_PATH=./state/runs.db +STRATEGY_CRYPTO_DB_PATH=./state/strategy_crypto.db # Docker / Traefik (usati SOLO da docker-compose.yml) -# Dominio base: traefik espone la dashboard su swarm.${DOMAIN_NAME} +# Dominio base: traefik espone la dashboard su swarm.${DOMAIN_NAME}/strategy_crypto_gui DOMAIN_NAME=tielogic.xyz # Porta interna della NiceGUI dashboard (Traefik fa il TLS davanti) SWARM_DASHBOARD_PORT=8080 +# Subpath URL del dashboard NiceGUI (usato come root_path in produzione) +DASHBOARD_ROOT_PATH=/strategy_crypto_gui # Paper-trading runner — override del command nel compose (opzionali) PAPER_RUN_NAME=phase3-papertrade-prod diff --git a/.gitignore b/.gitignore index 50740a3..3d84c3e 100644 --- a/.gitignore +++ b/.gitignore @@ -24,6 +24,10 @@ venv/ *.key # Project artefacts (non versionati: troppo grandi o rigenerabili) +state/*.db +state/*.db-journal +state/*.db-wal +state/*.db-shm runs.db runs.db-journal runs.db-wal @@ -36,6 +40,9 @@ checkpoints/ logs/ *.log +# OMC state (auto-orchestration) +.omc/ + # Build / dist build/ dist/ diff --git a/.omc/project-memory.json b/.omc/project-memory.json deleted file mode 100644 index 0da305b..0000000 --- a/.omc/project-memory.json +++ /dev/null @@ -1,234 +0,0 @@ -{ - "version": "1.0.0", - "lastScanned": 1778863584729, - "projectRoot": "/opt/docker/multi_swarm_coevolutive", - "techStack": { - "languages": [ - { - "name": "Python", - "version": null, - "confidence": "high", - "markers": [ - "pyproject.toml" - ] - } - ], - "frameworks": [ - { - "name": "pytest", - "version": null, - "category": "testing" - } - ], - "packageManager": null, - "runtime": null - }, - "build": { - "buildCommand": null, - "testCommand": "pytest", - "lintCommand": "ruff check", - "devCommand": null, - "scripts": {} - }, - "conventions": { - "namingStyle": null, - "importStyle": null, - "testPattern": null, - "fileOrganization": null - }, - "structure": { - "isMonorepo": false, - "workspaces": [], - "mainDirectories": [ - "docs", - "scripts", - "src", - "tests" - ], - "gitBranches": { - "defaultBranch": "main", - "branchingStrategy": null - } - }, - "customNotes": [], - "directoryMap": { - "data": { - "path": "data", - "purpose": "Data files", - "fileCount": 0, - "lastAccessed": 1778863584681, - "keyFiles": [] - }, - "docs": { - "path": "docs", - "purpose": "Documentation", - "fileCount": 0, - "lastAccessed": 1778863584688, - "keyFiles": [] - }, - "scripts": { - "path": "scripts", - "purpose": "Build/utility scripts", - "fileCount": 6, - "lastAccessed": 1778863584699, - "keyFiles": [ - "__init__.py", - "backtest_strategy.py", - "replay_strategies_window.py", - "run_paper_trading.py", - "run_phase1.py" - ] - }, - "series": { - "path": "series", - "purpose": null, - "fileCount": 1, - "lastAccessed": 1778863584703, - "keyFiles": [ - "f4a6ed332c35c4a2.parquet" - ] - }, - "src": { - "path": "src", - "purpose": "Source code", - "fileCount": 0, - "lastAccessed": 1778863584706, - "keyFiles": [] - }, - "state": { - "path": "state", - "purpose": null, - "fileCount": 3, - "lastAccessed": 1778863584707, - "keyFiles": [ - "runs.db", - "runs.db-shm", - "runs.db-wal" - ] - }, - "strategies": { - "path": "strategies", - "purpose": null, - "fileCount": 2, - "lastAccessed": 1778863584707, - "keyFiles": [ - "btc_fb63e851.json", - "eth_facd6af85d5d.json" - ] - }, - "tests": { - "path": "tests", - "purpose": "Test files", - "fileCount": 1, - "lastAccessed": 1778863584710, - "keyFiles": [ - "__init__.py" - ] - } - }, - "hotPaths": [ - { - "path": "src/multi_swarm/paper_trading/persistence.py", - "accessCount": 2, - "lastAccessed": 1778864950928, - "type": "file" - }, - { - "path": "src/multi_swarm/paper_trading/executor.py", - "accessCount": 2, - "lastAccessed": 1778864951455, - "type": "file" - }, - { - "path": "src/multi_swarm/paper_trading/portfolio.py", - "accessCount": 2, - "lastAccessed": 1778864956467, - "type": "file" - }, - { - "path": "src/multi_swarm/paper_trading/__init__.py", - "accessCount": 2, - "lastAccessed": 1778864957276, - "type": "file" - }, - { - "path": "src/multi_swarm/dashboard/data.py", - "accessCount": 2, - "lastAccessed": 1778864958856, - "type": "file" - }, - { - "path": "src/multi_swarm/dashboard/__init__.py", - "accessCount": 2, - "lastAccessed": 1778864959235, - "type": "file" - }, - { - "path": "pyproject.toml", - "accessCount": 2, - "lastAccessed": 1778864960093, - "type": "file" - }, - { - "path": "docker-compose.yml", - "accessCount": 2, - "lastAccessed": 1778864960851, - "type": "file" - }, - { - "path": ".env.example", - "accessCount": 2, - "lastAccessed": 1778864961733, - "type": "file" - }, - { - "path": "Dockerfile", - "accessCount": 1, - "lastAccessed": 1778864536214, - "type": "file" - }, - { - "path": "src/multi_swarm/__init__.py", - "accessCount": 1, - "lastAccessed": 1778864537411, - "type": "file" - }, - { - "path": "README.md", - "accessCount": 1, - "lastAccessed": 1778864537500, - "type": "file" - }, - { - "path": "src/multi_swarm/dashboard/nicegui_app.py", - "accessCount": 1, - "lastAccessed": 1778864557898, - "type": "file" - }, - { - "path": "src/multi_swarm/persistence/schema.py", - "accessCount": 1, - "lastAccessed": 1778864949548, - "type": "file" - }, - { - "path": "src/multi_swarm/persistence/repository.py", - "accessCount": 1, - "lastAccessed": 1778864949987, - "type": "file" - }, - { - "path": "src/multi_swarm/config.py", - "accessCount": 1, - "lastAccessed": 1778864959813, - "type": "file" - }, - { - "path": "scripts/run_paper_trading.py", - "accessCount": 1, - "lastAccessed": 1778864961224, - "type": "file" - } - ], - "userDirectives": [] -} \ No newline at end of file diff --git a/.omc/sessions/798fb16b-8751-4e21-a558-26b5cea16bd8.json b/.omc/sessions/798fb16b-8751-4e21-a558-26b5cea16bd8.json deleted file mode 100644 index 52b5830..0000000 --- a/.omc/sessions/798fb16b-8751-4e21-a558-26b5cea16bd8.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "session_id": "798fb16b-8751-4e21-a558-26b5cea16bd8", - "ended_at": "2026-05-15T00:33:45.627Z", - "reason": "other", - "agents_spawned": 0, - "agents_completed": 0, - "modes_used": [] -} \ No newline at end of file diff --git a/.omc/sessions/d997b670-db00-45b4-a478-cdad4d7fd603.json b/.omc/sessions/d997b670-db00-45b4-a478-cdad4d7fd603.json deleted file mode 100644 index 573d026..0000000 --- a/.omc/sessions/d997b670-db00-45b4-a478-cdad4d7fd603.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "session_id": "d997b670-db00-45b4-a478-cdad4d7fd603", - "ended_at": "2026-05-15T14:30:53.277Z", - "reason": "other", - "agents_spawned": 1, - "agents_completed": 1, - "modes_used": [] -} \ No newline at end of file diff --git a/.omc/sessions/ed0d4e09-401a-47e4-b354-336908ba30c2.json b/.omc/sessions/ed0d4e09-401a-47e4-b354-336908ba30c2.json deleted file mode 100644 index eed6e53..0000000 --- a/.omc/sessions/ed0d4e09-401a-47e4-b354-336908ba30c2.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "session_id": "ed0d4e09-401a-47e4-b354-336908ba30c2", - "ended_at": "2026-05-14T09:47:10.928Z", - "reason": "other", - "agents_spawned": 0, - "agents_completed": 0, - "modes_used": [] -} \ No newline at end of file diff --git a/.omc/state/agent-replay-798fb16b-8751-4e21-a558-26b5cea16bd8.jsonl b/.omc/state/agent-replay-798fb16b-8751-4e21-a558-26b5cea16bd8.jsonl deleted file mode 100644 index 0e9542e..0000000 --- a/.omc/state/agent-replay-798fb16b-8751-4e21-a558-26b5cea16bd8.jsonl +++ /dev/null @@ -1,31 +0,0 @@ -{"t":0,"agent":"a682971","agent_type":"unknown","event":"agent_stop","success":true} -{"t":0,"agent":"a3e1e57","agent_type":"unknown","event":"agent_stop","success":true} -{"t":0,"agent":"ac6e465","agent_type":"unknown","event":"agent_stop","success":true} -{"t":0,"agent":"ae622fc","agent_type":"unknown","event":"agent_stop","success":true} -{"t":0,"agent":"a70edb8","agent_type":"unknown","event":"agent_stop","success":true} -{"t":0,"agent":"a0298bb","agent_type":"unknown","event":"agent_stop","success":true} -{"t":0,"agent":"a0a1dac","agent_type":"unknown","event":"agent_stop","success":true} -{"t":0,"agent":"a233182","agent_type":"unknown","event":"agent_stop","success":true} -{"t":0,"agent":"a36dbf7","agent_type":"unknown","event":"agent_stop","success":true} -{"t":0,"agent":"ad92a42","agent_type":"unknown","event":"agent_stop","success":true} -{"t":0,"agent":"a3d10f4","agent_type":"unknown","event":"agent_stop","success":true} -{"t":0,"agent":"a3a6023","agent_type":"unknown","event":"agent_stop","success":true} -{"t":0,"agent":"a0031d5","agent_type":"unknown","event":"agent_stop","success":true} -{"t":0,"agent":"a49c162","agent_type":"unknown","event":"agent_stop","success":true} -{"t":0,"agent":"a593b10","agent_type":"unknown","event":"agent_stop","success":true} -{"t":0,"agent":"a3936fc","agent_type":"unknown","event":"agent_stop","success":true} -{"t":0,"agent":"a547232","agent_type":"unknown","event":"agent_stop","success":true} -{"t":0,"agent":"a5b6003","agent_type":"unknown","event":"agent_stop","success":true} -{"t":0,"agent":"a70d843","agent_type":"unknown","event":"agent_stop","success":true} -{"t":0,"agent":"aec9396","agent_type":"unknown","event":"agent_stop","success":true} -{"t":0,"agent":"a339545","agent_type":"unknown","event":"agent_stop","success":true} -{"t":0,"agent":"acb249c","agent_type":"unknown","event":"agent_stop","success":true} -{"t":0,"agent":"acd29d1","agent_type":"unknown","event":"agent_stop","success":true} -{"t":0,"agent":"a8e0c37","agent_type":"unknown","event":"agent_stop","success":true} -{"t":0,"agent":"a8e001b","agent_type":"unknown","event":"agent_stop","success":true} -{"t":0,"agent":"ac6bdc5","agent_type":"unknown","event":"agent_stop","success":true} -{"t":0,"agent":"a69506c","agent_type":"unknown","event":"agent_stop","success":true} -{"t":0,"agent":"aacfb9e","agent_type":"unknown","event":"agent_stop","success":true} -{"t":0,"agent":"ac010a8","agent_type":"unknown","event":"agent_stop","success":true} -{"t":0,"agent":"a7f5511","agent_type":"unknown","event":"agent_stop","success":true} -{"t":0,"agent":"a5e7734","agent_type":"unknown","event":"agent_stop","success":true} diff --git a/.omc/state/agent-replay-c410f859-5a67-475c-a941-8e2c50ed3a11.jsonl b/.omc/state/agent-replay-c410f859-5a67-475c-a941-8e2c50ed3a11.jsonl deleted file mode 100644 index c12eae9..0000000 --- a/.omc/state/agent-replay-c410f859-5a67-475c-a941-8e2c50ed3a11.jsonl +++ /dev/null @@ -1,6 +0,0 @@ -{"t":0,"agent":"aa06dbe","agent_type":"Explore","event":"agent_start","parent_mode":"none"} -{"t":0,"agent":"a62f5be","agent_type":"Explore","event":"agent_start","parent_mode":"none"} -{"t":0,"agent":"a62f5be","agent_type":"Explore","event":"agent_stop","success":true,"duration_ms":21379} -{"t":0,"agent":"aa06dbe","agent_type":"Explore","event":"agent_stop","success":true,"duration_ms":94545} -{"t":0,"agent":"a086f61","agent_type":"Plan","event":"agent_start","parent_mode":"none"} -{"t":0,"agent":"a086f61","agent_type":"Plan","event":"agent_stop","success":true,"duration_ms":202552} diff --git a/.omc/state/agent-replay-d997b670-db00-45b4-a478-cdad4d7fd603.jsonl b/.omc/state/agent-replay-d997b670-db00-45b4-a478-cdad4d7fd603.jsonl deleted file mode 100644 index 2456f80..0000000 --- a/.omc/state/agent-replay-d997b670-db00-45b4-a478-cdad4d7fd603.jsonl +++ /dev/null @@ -1,16 +0,0 @@ -{"t":0,"agent":"a0d4467","agent_type":"unknown","event":"agent_stop","success":true} -{"t":0,"agent":"a160b0e","agent_type":"unknown","event":"agent_stop","success":true} -{"t":0,"agent":"af3bbac","agent_type":"unknown","event":"agent_stop","success":true} -{"t":0,"agent":"a1ee6c9","agent_type":"unknown","event":"agent_stop","success":true} -{"t":0,"agent":"a4ad30d","agent_type":"unknown","event":"agent_stop","success":true} -{"t":0,"agent":"a90eb8d","agent_type":"unknown","event":"agent_stop","success":true} -{"t":0,"agent":"ae064b2","agent_type":"unknown","event":"agent_stop","success":true} -{"t":0,"agent":"a0c8984","agent_type":"unknown","event":"agent_stop","success":true} -{"t":0,"agent":"ad27588","agent_type":"unknown","event":"agent_stop","success":true} -{"t":0,"agent":"a523e02","agent_type":"unknown","event":"agent_stop","success":true} -{"t":0,"agent":"a2f3f5e","agent_type":"unknown","event":"agent_stop","success":true} -{"t":0,"agent":"ad7358f","agent_type":"unknown","event":"agent_stop","success":true} -{"t":0,"agent":"a70e03c","agent_type":"unknown","event":"agent_stop","success":true} -{"t":0,"agent":"a5535e6","agent_type":"Explore","event":"agent_start","parent_mode":"none"} -{"t":0,"agent":"a5535e6","agent_type":"Explore","event":"agent_stop","success":true,"duration_ms":316451} -{"t":0,"agent":"adffe1e","agent_type":"unknown","event":"agent_stop","success":true} diff --git a/.omc/state/agent-replay-ed0d4e09-401a-47e4-b354-336908ba30c2.jsonl b/.omc/state/agent-replay-ed0d4e09-401a-47e4-b354-336908ba30c2.jsonl deleted file mode 100644 index 55de032..0000000 --- a/.omc/state/agent-replay-ed0d4e09-401a-47e4-b354-336908ba30c2.jsonl +++ /dev/null @@ -1 +0,0 @@ -{"t":0,"agent":"a25cb13","agent_type":"unknown","event":"agent_stop","success":true} diff --git a/.omc/state/hud-state.json b/.omc/state/hud-state.json deleted file mode 100644 index 7ad50de..0000000 --- a/.omc/state/hud-state.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "timestamp": "2026-05-15T16:46:57.759Z", - "backgroundTasks": [], - "sessionStartTimestamp": "2026-05-15T16:46:24.599Z", - "sessionId": "c410f859-5a67-475c-a941-8e2c50ed3a11" -} \ No newline at end of file diff --git a/.omc/state/hud-stdin-cache.json b/.omc/state/hud-stdin-cache.json deleted file mode 100644 index 521e847..0000000 --- a/.omc/state/hud-stdin-cache.json +++ /dev/null @@ -1 +0,0 @@ -{"session_id":"c410f859-5a67-475c-a941-8e2c50ed3a11","transcript_path":"/root/.claude/projects/-opt-docker-multi-swarm-coevolutive/c410f859-5a67-475c-a941-8e2c50ed3a11.jsonl","cwd":"/opt/docker/multi_swarm_coevolutive","effort":{"level":"xhigh"},"session_name":"monorepo-reorganization-split-revert","model":{"id":"claude-opus-4-7[1m]","display_name":"Opus 4.7 (1M context)"},"workspace":{"current_dir":"/opt/docker/multi_swarm_coevolutive","project_dir":"/opt/docker/multi_swarm_coevolutive","added_dirs":[]},"version":"2.1.142","output_style":{"name":"default"},"cost":{"total_cost_usd":12.174355699999994,"total_duration_ms":3442407,"total_api_duration_ms":1675477,"total_lines_added":750,"total_lines_removed":345},"context_window":{"total_input_tokens":212953,"total_output_tokens":8,"context_window_size":1000000,"current_usage":{"input_tokens":6,"output_tokens":8,"cache_creation_input_tokens":599,"cache_read_input_tokens":212348},"used_percentage":21,"remaining_percentage":79},"exceeds_200k_tokens":true,"fast_mode":false,"thinking":{"enabled":true},"rate_limits":{"five_hour":{"used_percentage":11,"resets_at":1778881200},"seven_day":{"used_percentage":21,"resets_at":1779138000}}} \ No newline at end of file diff --git a/.omc/state/idle-notif-cooldown.json b/.omc/state/idle-notif-cooldown.json deleted file mode 100644 index 89a79a5..0000000 --- a/.omc/state/idle-notif-cooldown.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "lastSentAt": "2026-05-15T17:22:55.330Z" -} \ No newline at end of file diff --git a/.omc/state/last-tool-error.json b/.omc/state/last-tool-error.json deleted file mode 100644 index b381ffb..0000000 --- a/.omc/state/last-tool-error.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "tool_name": "Bash", - "tool_input_preview": "{\"command\":\"find /opt/docker/multi_swarm_coevolutive -name \\\"config.py\\\" | xargs grep -l \\\"DATA_DIR\\\\|STRATEGIES\\\\|STATE\\\" 2>/dev/null\"}", - "error": "Exit code 123", - "timestamp": "2026-05-15T17:03:17.335Z", - "retry_count": 1 -} \ No newline at end of file diff --git a/.omc/state/mission-state.json b/.omc/state/mission-state.json deleted file mode 100644 index 7473ccc..0000000 --- a/.omc/state/mission-state.json +++ /dev/null @@ -1,145 +0,0 @@ -{ - "updatedAt": "2026-05-15T17:12:22.595Z", - "missions": [ - { - "id": "session:d997b670-db00-45b4-a478-cdad4d7fd603:none", - "source": "session", - "name": "none", - "objective": "Session mission", - "createdAt": "2026-05-15T10:07:56.561Z", - "updatedAt": "2026-05-15T11:04:10.376Z", - "status": "done", - "workerCount": 1, - "taskCounts": { - "total": 1, - "pending": 0, - "blocked": 0, - "inProgress": 0, - "completed": 1, - "failed": 0 - }, - "agents": [ - { - "name": "Explore:a5535e6", - "role": "Explore", - "ownership": "a5535e676159dcf1c", - "status": "done", - "currentStep": null, - "latestUpdate": "completed", - "completedSummary": null, - "updatedAt": "2026-05-15T11:04:10.376Z" - } - ], - "timeline": [ - { - "id": "session-start:a5535e676159dcf1c:2026-05-15T10:07:56.561Z", - "at": "2026-05-15T10:07:56.561Z", - "kind": "update", - "agent": "Explore:a5535e6", - "detail": "started Explore:a5535e6", - "sourceKey": "session-start:a5535e676159dcf1c" - }, - { - "id": "session-stop:a5535e676159dcf1c:2026-05-15T10:13:13.012Z", - "at": "2026-05-15T10:13:13.012Z", - "kind": "completion", - "agent": "Explore:a5535e6", - "detail": "completed", - "sourceKey": "session-stop:a5535e676159dcf1c" - }, - { - "id": "session-stop:adffe1eae69f95602:2026-05-15T11:04:10.376Z", - "at": "2026-05-15T11:04:10.376Z", - "kind": "completion", - "agent": "Explore:a5535e6", - "detail": "completed", - "sourceKey": "session-stop:adffe1eae69f95602" - } - ] - }, - { - "id": "session:c410f859-5a67-475c-a941-8e2c50ed3a11:none", - "source": "session", - "name": "none", - "objective": "Session mission", - "createdAt": "2026-05-15T17:02:02.737Z", - "updatedAt": "2026-05-15T17:12:22.595Z", - "status": "done", - "workerCount": 3, - "taskCounts": { - "total": 3, - "pending": 0, - "blocked": 0, - "inProgress": 0, - "completed": 3, - "failed": 0 - }, - "agents": [ - { - "name": "Explore:aa06dbe", - "role": "Explore", - "ownership": "aa06dbecb11544c5e", - "status": "done", - "currentStep": null, - "latestUpdate": "completed", - "completedSummary": null, - "updatedAt": "2026-05-15T17:03:37.282Z" - }, - { - "name": "Explore:a62f5be", - "role": "Explore", - "ownership": "a62f5bee30bd7a6c1", - "status": "done", - "currentStep": null, - "latestUpdate": "completed", - "completedSummary": null, - "updatedAt": "2026-05-15T17:02:35.066Z" - }, - { - "name": "Plan:a086f61", - "role": "Plan", - "ownership": "a086f61a1fbb580be", - "status": "done", - "currentStep": null, - "latestUpdate": "completed", - "completedSummary": null, - "updatedAt": "2026-05-15T17:12:22.595Z" - } - ], - "timeline": [ - { - "id": "session-stop:a62f5bee30bd7a6c1:2026-05-15T17:02:35.066Z", - "at": "2026-05-15T17:02:35.066Z", - "kind": "completion", - "agent": "Explore:a62f5be", - "detail": "completed", - "sourceKey": "session-stop:a62f5bee30bd7a6c1" - }, - { - "id": "session-stop:aa06dbecb11544c5e:2026-05-15T17:03:37.282Z", - "at": "2026-05-15T17:03:37.282Z", - "kind": "completion", - "agent": "Explore:aa06dbe", - "detail": "completed", - "sourceKey": "session-stop:aa06dbecb11544c5e" - }, - { - "id": "session-start:a086f61a1fbb580be:2026-05-15T17:09:00.043Z", - "at": "2026-05-15T17:09:00.043Z", - "kind": "update", - "agent": "Plan:a086f61", - "detail": "started Plan:a086f61", - "sourceKey": "session-start:a086f61a1fbb580be" - }, - { - "id": "session-stop:a086f61a1fbb580be:2026-05-15T17:12:22.595Z", - "at": "2026-05-15T17:12:22.595Z", - "kind": "completion", - "agent": "Plan:a086f61", - "detail": "completed", - "sourceKey": "session-stop:a086f61a1fbb580be" - } - ] - } - ] -} \ No newline at end of file diff --git a/.omc/state/subagent-tracking.json b/.omc/state/subagent-tracking.json deleted file mode 100644 index 4fe1c6b..0000000 --- a/.omc/state/subagent-tracking.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "agents": [ - { - "agent_id": "aa06dbecb11544c5e", - "agent_type": "Explore", - "started_at": "2026-05-15T17:02:02.737Z", - "parent_mode": "none", - "status": "completed", - "completed_at": "2026-05-15T17:03:37.282Z", - "duration_ms": 94545 - }, - { - "agent_id": "a62f5bee30bd7a6c1", - "agent_type": "Explore", - "started_at": "2026-05-15T17:02:13.687Z", - "parent_mode": "none", - "status": "completed", - "completed_at": "2026-05-15T17:02:35.066Z", - "duration_ms": 21379 - }, - { - "agent_id": "a086f61a1fbb580be", - "agent_type": "Plan", - "started_at": "2026-05-15T17:09:00.043Z", - "parent_mode": "none", - "status": "completed", - "completed_at": "2026-05-15T17:12:22.595Z", - "duration_ms": 202552 - } - ], - "total_spawned": 3, - "total_completed": 3, - "total_failed": 0, - "last_updated": "2026-05-15T17:12:22.696Z" -} \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 472842b..cd5cace 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,13 +1,15 @@ # syntax=docker/dockerfile:1.7 # -# Multi-Swarm Coevolutive — immagine unica usata da due servizi: -# * paper-trading runner (scripts/run_paper_trading.py) -# * Streamlit dashboard (src/multi_swarm/dashboard/streamlit_app.py) +# Multi-Swarm Coevolutive — immagine unica usata da due servizi del compose: +# * paper-trading runner (scripts/run_paper_trading.py) +# * NiceGUI dashboard (strategy_crypto.frontend.nicegui_app) # -# Builder stage: risolve uv.lock con `uv sync --frozen --no-dev` e produce -# un venv in /app/.venv. Runtime stage: copia solo /app + scripts/ e gira -# come utente non-root. data/, series/, strategies/, state/ sono bind -# mount dal compose, quindi non finiscono nell'immagine. +# uv workspace: pyproject root coordina due member packages +# (multi-swarm-core + strategy-crypto). Il `uv sync --frozen` installa +# entrambi come editable nella venv del builder. +# Runtime stage: copia solo /app + scripts/ e gira come utente non-root. +# data/, series/, state/ sono bind mount dal compose; strategies/ è +# bind-mounted dal path src/strategy_crypto/strategy_crypto/strategies. FROM python:3.13-slim AS builder RUN apt-get update && apt-get install -y --no-install-recommends \ @@ -21,7 +23,7 @@ RUN uv sync --frozen --no-dev FROM python:3.13-slim AS runtime -LABEL org.opencontainers.image.title="multi-swarm" \ +LABEL org.opencontainers.image.title="multi-swarm-coevolutive" \ org.opencontainers.image.version="0.1.0" \ org.opencontainers.image.source="https://git.tielogic.xyz/Adriano/Multi_Swarm_Coevolutive" @@ -43,10 +45,10 @@ RUN useradd -m -u 1000 app \ && chown -R app:app /app USER app -# Healthcheck di default: import del package — i servizi reali lo -# sovrascrivono nel compose (streamlit /_stcore/health). +# Healthcheck di default: import dei due package del workspace. +# I servizi reali lo sovrascrivono nel compose (es. NiceGUI HTTP). HEALTHCHECK --interval=60s --timeout=5s --retries=3 --start-period=10s \ - CMD python -c "import multi_swarm" || exit 1 + CMD python -c "import multi_swarm_core, strategy_crypto" || exit 1 # Nessun CMD di default: il compose specifica entrypoint/command # per ognuno dei due servizi. diff --git a/pyproject.toml b/pyproject.toml index 1c4e07e..a7cc6a9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,26 +1,16 @@ [project] -name = "multi-swarm" +name = "multi-swarm-coevolutive" version = "0.1.0" -description = "Multi-Swarm Coevolutive PoC trading swarm — Phase 1 lean spike" +description = "Multi-Swarm Coevolutive: monorepo workspace (core + strategie)" authors = [{ name = "Adriano Dal Pastro", email = "adrianodalpastro@tielogic.com" }] requires-python = ">=3.13" -dependencies = [ - "pandas>=2.2", - "numpy>=2.1", - "scipy>=1.14", - "pydantic>=2.9", - "pydantic-settings>=2.6", - "sqlmodel>=0.0.22", - "openai>=1.55", - "httpx>=0.28", - "requests>=2.32", - "tenacity>=9.0", - "pyyaml>=6.0", - "plotly>=5.24", - "pyarrow>=18.0", - "nicegui>=3.11.1", - "yfinance>=1.3.0", -] + +[tool.uv.workspace] +members = ["src/multi_swarm_core", "src/strategy_crypto"] + +[tool.uv.sources] +multi-swarm-core = { workspace = true } +strategy-crypto = { workspace = true } [dependency-groups] dev = [ @@ -31,15 +21,10 @@ dev = [ "ruff>=0.7", "mypy>=1.13", "types-requests>=2.32", + "multi-swarm-core", + "strategy-crypto", ] -[build-system] -requires = ["hatchling"] -build-backend = "hatchling.build" - -[tool.hatch.build.targets.wheel] -packages = ["src/multi_swarm"] - [tool.ruff] line-length = 100 target-version = "py313" @@ -52,7 +37,7 @@ python_version = "3.13" strict = true [tool.pytest.ini_options] -testpaths = ["tests"] +testpaths = ["src/multi_swarm_core/tests", "src/strategy_crypto/tests"] addopts = "-v --tb=short" markers = [ "integration: tests that require external services (Cerbero, LLM API)", diff --git a/src/multi_swarm_core/pyproject.toml b/src/multi_swarm_core/pyproject.toml new file mode 100644 index 0000000..866c942 --- /dev/null +++ b/src/multi_swarm_core/pyproject.toml @@ -0,0 +1,25 @@ +[project] +name = "multi-swarm-core" +version = "0.1.0" +description = "Multi-Swarm Coevolutive core: GA, genome, protocol, backtest, cerbero, data, llm, persistence" +authors = [{ name = "Adriano Dal Pastro", email = "adrianodalpastro@tielogic.com" }] +requires-python = ">=3.13" +dependencies = [ + "pandas>=2.2", + "numpy>=2.1", + "scipy>=1.14", + "pydantic>=2.9", + "pydantic-settings>=2.6", + "sqlmodel>=0.0.22", + "openai>=1.55", + "httpx>=0.28", + "requests>=2.32", + "tenacity>=9.0", + "pyyaml>=6.0", + "pyarrow>=18.0", + "yfinance>=1.3.0", +] + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" diff --git a/src/strategy_crypto/pyproject.toml b/src/strategy_crypto/pyproject.toml new file mode 100644 index 0000000..a3b54c7 --- /dev/null +++ b/src/strategy_crypto/pyproject.toml @@ -0,0 +1,20 @@ +[project] +name = "strategy-crypto" +version = "0.1.0" +description = "Strategy crypto: paper-trading runner + NiceGUI dashboard, consuma multi-swarm-core" +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_crypto/strategies" = "strategy_crypto/strategies" diff --git a/uv.lock b/uv.lock index f773f5a..73b728b 100644 --- a/uv.lock +++ b/uv.lock @@ -3,16 +3,23 @@ revision = 3 requires-python = ">=3.13" resolution-markers = [ "python_full_version >= '3.15' and sys_platform == 'win32'", - "python_full_version == '3.14.*' and sys_platform == 'win32'", "python_full_version >= '3.15' and sys_platform == 'emscripten'", - "python_full_version == '3.14.*' and sys_platform == 'emscripten'", "python_full_version >= '3.15' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.14.*' and sys_platform == 'win32'", + "python_full_version == '3.14.*' and sys_platform == 'emscripten'", "python_full_version == '3.14.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version < '3.14' and sys_platform == 'win32'", "python_full_version < '3.14' and sys_platform == 'emscripten'", "python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", ] +[manifest] +members = [ + "multi-swarm-coevolutive", + "multi-swarm-core", + "strategy-crypto", +] + [[package]] name = "aiofiles" version = "25.1.0" @@ -143,40 +150,40 @@ wheels = [ [[package]] name = "ast-serialize" -version = "0.3.0" +version = "0.4.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a9/9d/912fefab0e30aee6a3af8a62bbea4a81b29afa4ba2c973d31170620a26de/ast_serialize-0.3.0.tar.gz", hash = "sha256:1bc3ca09a63a021376527c4e938deedd11d11d675ce850e6f9c7487f5889992b", size = 60689, upload-time = "2026-04-30T23:24:48.104Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e2/1f/50f241d4e01fe75f4bba6a209edd4047c4b26acf70992ff885fd161f79cb/ast_serialize-0.4.0.tar.gz", hash = "sha256:74e4e634ab82d1466acf0be27043178570b98ebeaa3165f9240a6fad4c286471", size = 60687, upload-time = "2026-05-14T22:44:38.251Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6f/57/a54d4de491d6cdd7a4e4b0952cc3ca9f60dcefa7b5fb48d6d492debe1649/ast_serialize-0.3.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:3a867927df59f76a18dc1d874a0b2c079b42c58972dca637905576deb0912e14", size = 1182966, upload-time = "2026-04-30T23:23:57.376Z" }, - { url = "https://files.pythonhosted.org/packages/ee/9e/a5db014bb0f91b209236b57c429389e31290c0093532b8436d577699b2fa/ast_serialize-0.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a6fb063bf040abf8321e7b8113a0554eda445ffc508aa51287f8808886a5ae22", size = 1171316, upload-time = "2026-04-30T23:23:59.63Z" }, - { url = "https://files.pythonhosted.org/packages/15/59/fd55133e478c4326f60a11df02573bf7ccb2ac685810b50f1803d0f68053/ast_serialize-0.3.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5075cd8482573d743586779e5f9b652a015e37d4e95132d7e5a9bc5c8f483d8f", size = 1232234, upload-time = "2026-04-30T23:24:01.168Z" }, - { url = "https://files.pythonhosted.org/packages/cc/79/0ca1d26357ecb4a697d74d00b73ef3137f24c140424125393a0de820eb09/ast_serialize-0.3.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:41560b27794f4553b0f77811e9fb325b77db4a2b39018d437e09932275306e66", size = 1233437, upload-time = "2026-04-30T23:24:03.151Z" }, - { url = "https://files.pythonhosted.org/packages/53/3e/7078ec94dd6e124b8e028ac77016a4f13c83fa1c145790f2e68f3816998b/ast_serialize-0.3.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b967c01ca74909c5d90e0fe4393401e2cc5da5ebd9a6262a19e45ffd3757dec8", size = 1440188, upload-time = "2026-04-30T23:24:04.717Z" }, - { url = "https://files.pythonhosted.org/packages/21/16/cca7195ef55a012f8013c3442afa91d287a0a36dcf88b480b262475135b3/ast_serialize-0.3.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:424ebb8f46cd993f7cec4009d119312d8433dd90e6b0df0499cd2c91bdcc5af9", size = 1254211, upload-time = "2026-04-30T23:24:06.18Z" }, - { url = "https://files.pythonhosted.org/packages/a0/0f/f3d4dfae67dee6580534361a6343367d34217e7d25cff858bd1d8f03b8ed/ast_serialize-0.3.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d14b1d566b56e2ee70b11fec1de7e0b94ec7cd83717ec7d189967841a361190e", size = 1255973, upload-time = "2026-04-30T23:24:07.772Z" }, - { url = "https://files.pythonhosted.org/packages/14/41/55fbfe02c42f40fbe3e74eda167d977d555ff720ce1abfa08515236efd88/ast_serialize-0.3.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7ba30b18735f047ec11103d1ab92f4789cf1fea1e0dc89b04a2f5a0632fd79de", size = 1298629, upload-time = "2026-04-30T23:24:09.4Z" }, - { url = "https://files.pythonhosted.org/packages/28/36/7d2501cacc7989fb8504aa9da2a2022a174200a59d4e6639de4367a57fdd/ast_serialize-0.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e6ea0754cb7b0f682ebb005ffb0d18f8d17993490d9c289863cd69cacc4ab8df", size = 1408435, upload-time = "2026-04-30T23:24:11.013Z" }, - { url = "https://files.pythonhosted.org/packages/03/e7/54e3b469c3fa0bf9cd532fa643d1d33b73303f8d70beac3e366b68dd64b7/ast_serialize-0.3.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:a0c5aa1073a5ba7b2abaa4b54abe8b8d75c4d1e2d54a2ff70b0ca6222fea5728", size = 1508174, upload-time = "2026-04-30T23:24:12.635Z" }, - { url = "https://files.pythonhosted.org/packages/b5/2a/9b9621865b02c60539e26d9b114a312b4fa46aa703e33e79317174bfea21/ast_serialize-0.3.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:4e52650d834c1ea7791969a361de2c54c13b2fb4c519ec79445fa8b9021a147d", size = 1502354, upload-time = "2026-04-30T23:24:14.186Z" }, - { url = "https://files.pythonhosted.org/packages/34/dd/f138bc5c43b0c414fdd12eefe15677839323078b6e75301ad7f96cd26d45/ast_serialize-0.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:15bd6af3f136c61dae27805eb6b8f3269e85a545c4c27ffe9e530ead78d2b36d", size = 1450504, upload-time = "2026-04-30T23:24:16.076Z" }, - { url = "https://files.pythonhosted.org/packages/68/cf/97ef9e1c315601db74365955c8edd3292e3055500d6317602815dbdf08ae/ast_serialize-0.3.0-cp314-cp314t-win32.whl", hash = "sha256:d188bfe37b674b49708497683051d4b571366a668799c9b8e8a94513694969d9", size = 1058662, upload-time = "2026-04-30T23:24:17.535Z" }, - { url = "https://files.pythonhosted.org/packages/f8/d6/e2c3483c31580fdb623f92ad38d2f856cde4b9205a3e6bd84760f3de7d82/ast_serialize-0.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:5832c2fdf8f8a6cf682b4cfcf677f5eaf39b4ddbc490f5480cfccdd1e7ce8fa1", size = 1100349, upload-time = "2026-04-30T23:24:18.992Z" }, - { url = "https://files.pythonhosted.org/packages/ab/89/29abcb1fe18a429cda60c6e0bbd1d6e90499339842a2f548d7567542357e/ast_serialize-0.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:670f177188d128fb7f9f15b5ad0e1b553d22c34e3f584dcb83eb8077600437f0", size = 1072895, upload-time = "2026-04-30T23:24:20.706Z" }, - { url = "https://files.pythonhosted.org/packages/bc/93/72abad83966ed6235647c9f956417dc1e17e997696388521910e3d1fa3f4/ast_serialize-0.3.0-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:2ec2fafa5e4313cc8feed96e436ebe19ac7bc6fa41fbc2827e826c48b9e4c3a9", size = 1190024, upload-time = "2026-04-30T23:24:22.486Z" }, - { url = "https://files.pythonhosted.org/packages/85/4f/eb88584b2f0234e581762011208ca203252bf6c98e59b4769daa571f3576/ast_serialize-0.3.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:ef6d3c08b7b4cd29b48410338e134764a00e76d25841eb02c1084e868c888ecc", size = 1178633, upload-time = "2026-04-30T23:24:24.35Z" }, - { url = "https://files.pythonhosted.org/packages/56/51/cf1ec1ff3e616373d0dcbd5fad502e0029dc541f13ab642259762a7d127f/ast_serialize-0.3.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3d841424f41b886e98044abc80769c14a956e6e5ccd5fb5b0d9f5ead72be18a4", size = 1241351, upload-time = "2026-04-30T23:24:25.987Z" }, - { url = "https://files.pythonhosted.org/packages/0d/44/68fcf50478cf1093f2d423f034ae06453122c8b415d8e21a44668eca485d/ast_serialize-0.3.0-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d21453734ad39367ede5d37efe4f59f830ce1c09f432fc72a90e368f77a4a3e7", size = 1239582, upload-time = "2026-04-30T23:24:27.808Z" }, - { url = "https://files.pythonhosted.org/packages/9d/c1/a6c9fa284eceb5fc6f21347e968445a051d7ca2c4d34e6a04314646dbcee/ast_serialize-0.3.0-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f5e110cdce2a347e1dd987529c88ef54d26f67848dce3eba1b3b2cc2cf085c94", size = 1448853, upload-time = "2026-04-30T23:24:29.534Z" }, - { url = "https://files.pythonhosted.org/packages/23/5f/8ad3829a09e4e8c5328a53ce7d4711d660944e3e164c5f6abcc2c8f27167/ast_serialize-0.3.0-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b6e23a98e57560a055f5c4b68700a0fd5ce483d2814c23140b3638c7f5d1e61", size = 1262204, upload-time = "2026-04-30T23:24:31.482Z" }, - { url = "https://files.pythonhosted.org/packages/25/13/44aa28d97f10e25247e8576b5f6b2795d4fa1a80acc88acc942c508d06f7/ast_serialize-0.3.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c1c9e763d70293d65ce1e1ea8c943140c68d0953f0268c7ee0998f2e07f77dd0", size = 1266458, upload-time = "2026-04-30T23:24:33.088Z" }, - { url = "https://files.pythonhosted.org/packages/d8/58/b3a8be3777cd3744324fd5cec0d80d37cd96fc7cbb0fb010e03dff1e870f/ast_serialize-0.3.0-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4388a1796c228f1ce5c391426f7d21a0003ad3b47f677dbeded9bd1a85c7209f", size = 1308700, upload-time = "2026-04-30T23:24:34.657Z" }, - { url = "https://files.pythonhosted.org/packages/13/03/f8312d6b57f5471a9dc7946f22b8798a1fc296d38c25766223aacadec42c/ast_serialize-0.3.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:5283cdcc0c64c3d8b9b688dc6aaa012d9c0cf1380a7f774a6bae6a1c01b3205a", size = 1416724, upload-time = "2026-04-30T23:24:36.562Z" }, - { url = "https://files.pythonhosted.org/packages/50/5d/13fc3789a7abac00559da2e2e9f386db4612aa1f84fc53d09bf714c37545/ast_serialize-0.3.0-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:f5ef88cc5842a5d7a6ac09dc0d5fc2c98f5d276c1f076f866d55047ce886785b", size = 1515441, upload-time = "2026-04-30T23:24:38.018Z" }, - { url = "https://files.pythonhosted.org/packages/eb/b9/7ab43fc7a23b1f970281093228f5f79bed6edeed7a3e672bde6d7a832a58/ast_serialize-0.3.0-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:cc14bf402bdc0978594ecce783793de2c7470cd4f5cd7eb286ca97ed8ff7cba9", size = 1510522, upload-time = "2026-04-30T23:24:39.798Z" }, - { url = "https://files.pythonhosted.org/packages/56/ec/d75fc2b788d319f1fad77c14156896f31afdfc68af85b505e5bdebcb9592/ast_serialize-0.3.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:11eae0cf1b7b3e0678133cc2daa974ea972caf02eb4b3aa062af6fa9acd52c57", size = 1460917, upload-time = "2026-04-30T23:24:41.305Z" }, - { url = "https://files.pythonhosted.org/packages/95/74/f99c81193a2725911e1911ae567ed27c2f2419332c7f3537366f9d238cac/ast_serialize-0.3.0-cp39-abi3-win32.whl", hash = "sha256:2db3dd99de5e6a5a11d7dda73de8750eb6e5baaf25245adf7bdcfe64b6108ae2", size = 1067804, upload-time = "2026-04-30T23:24:43.091Z" }, - { url = "https://files.pythonhosted.org/packages/16/81/76af00c47daa151e89f98ae21fbbcb2840aaa9f5766579c4da76a3c57188/ast_serialize-0.3.0-cp39-abi3-win_amd64.whl", hash = "sha256:a2cd125adccf7969470621905d302750cd25951f22ea430d9a25b7be031e5549", size = 1105561, upload-time = "2026-04-30T23:24:44.578Z" }, - { url = "https://files.pythonhosted.org/packages/bd/46/d3ec57ad500f598d1554bd14ce4df615960549ab2844961bc4e1f5fbd174/ast_serialize-0.3.0-cp39-abi3-win_arm64.whl", hash = "sha256:0dd00da29985f15f50dc35728b7e1e7c84507bccfea1d9914738530f1c72238a", size = 1077165, upload-time = "2026-04-30T23:24:46.377Z" }, + { url = "https://files.pythonhosted.org/packages/b0/85/232631c59b5ca7152c08f026e9a46f47d852298acff74edd04a1fc1d0005/ast_serialize-0.4.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:a6f26937ce0293aafbece0e39019e020369a5a70486ff4088227f0cc888844a9", size = 1182685, upload-time = "2026-05-14T22:43:40.205Z" }, + { url = "https://files.pythonhosted.org/packages/5d/5e/4838d4d3ddc4425555601467d4e2a565e4340899e45feee4e32c80fbc911/ast_serialize-0.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:074032142777e3e6091977dc3c5146a8ca58ae6825b7f64e9a0b604153ddabd8", size = 1173113, upload-time = "2026-05-14T22:43:41.937Z" }, + { url = "https://files.pythonhosted.org/packages/22/fc/d622b19fc1c79a62028ec17f4ad4323177af25b174d32b07c84d61ef9d47/ast_serialize-0.4.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:404f3462b4532e13a70b8849bba241dbd82e30043ff58d98c7e762fd925b116a", size = 1234117, upload-time = "2026-05-14T22:43:43.977Z" }, + { url = "https://files.pythonhosted.org/packages/d5/b5/72f8c8659da0b64562e6d97f852d5c2022c74577df27c922e1e7065039ce/ast_serialize-0.4.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:97c55336e16f5c4ca2bde7be94cca4b8f7d665d64f7008925a82e02707ba14ac", size = 1231703, upload-time = "2026-05-14T22:43:46.064Z" }, + { url = "https://files.pythonhosted.org/packages/7b/98/ccc51ee4f90f97a1ed0a0848bd4c9d77a80969849db8a262b7d2970a6a15/ast_serialize-0.4.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:732b4ef76adcb0f298a7d18c4558336d83b1384f9ae0c7eaa1dc8d031b0a4390", size = 1441574, upload-time = "2026-05-14T22:43:47.784Z" }, + { url = "https://files.pythonhosted.org/packages/6a/ce/668c4efe79e09c9cc97a4d0a1c29e61fe6f78857fe1e57c086772af55f89/ast_serialize-0.4.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b3db87c4772097c0782250bcd550d66b1189a8c889793c7bcf153f4fee70005c", size = 1254040, upload-time = "2026-05-14T22:43:49.879Z" }, + { url = "https://files.pythonhosted.org/packages/3d/be/38b27bc2909b7236939801ca9f0d97cdc6198da4f435a81658e0db506fdb/ast_serialize-0.4.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:43729a5e369ebbe7750635c0c206bc616fcd36e703cb9c4497d6b4df0291ee64", size = 1257847, upload-time = "2026-05-14T22:43:51.607Z" }, + { url = "https://files.pythonhosted.org/packages/68/df/360ebccc361235c167a8be2a0476870cb9ef44c42413bf1289b885684052/ast_serialize-0.4.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:91d3786f3929786cdc4eeedfd110abb4603e7f6c1390c5af398f333a947b742d", size = 1298683, upload-time = "2026-05-14T22:43:53.606Z" }, + { url = "https://files.pythonhosted.org/packages/51/5c/7d5e0b4d47aafa1600c19e3670f962f81a9bf3da1bc25a1382529a447cf3/ast_serialize-0.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7fba7315fd4bd87cb5560792709f6e66e0606402d362c0a38dd32dfb66ba6066", size = 1409438, upload-time = "2026-05-14T22:43:55.316Z" }, + { url = "https://files.pythonhosted.org/packages/b5/3d/8875b2f1af3ec1539b88ff193dfbfa5573084ef7fcab27ea4cd09b6dc829/ast_serialize-0.4.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:4db9769d57deb5545ce56ebbbbe3436dcc0ae2688ce14c295cd14e106624ece7", size = 1507922, upload-time = "2026-05-14T22:43:56.959Z" }, + { url = "https://files.pythonhosted.org/packages/55/c3/5ec6927eb493ece7ba64263cdc556be889e0c62a013b1851bbe674a0dcda/ast_serialize-0.4.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:dcd04f85a29deb80400e8987cfaceb9907140f763453cbffdbd6ff36f1b32c12", size = 1502817, upload-time = "2026-05-14T22:43:59.081Z" }, + { url = "https://files.pythonhosted.org/packages/9d/c8/40cb818a08396b1f34d6189c0c42aec917dd331e11fb7c3b870cc61b795a/ast_serialize-0.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:905fc11940831454d93589bd7ce2acb6a5eb01c2936156f751d2a21087c98cd3", size = 1454318, upload-time = "2026-05-14T22:44:01.377Z" }, + { url = "https://files.pythonhosted.org/packages/74/d5/d51494b60cc52f4792be5ddc951631cddb17a2990154634549abdbdbb5bf/ast_serialize-0.4.0-cp314-cp314t-win32.whl", hash = "sha256:3bdde2c4570143791f636aed4e3ef868f5b46eb90a18f8d5c41dd045aab08bef", size = 1060098, upload-time = "2026-05-14T22:44:03.265Z" }, + { url = "https://files.pythonhosted.org/packages/7a/c9/b0086257c79ff95743a3621448a01fc71b234ae359d3d54cda383aa43939/ast_serialize-0.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:6551d55b8607b97a7755683d743200b398c61a0b71a11b7f00c89c335a11d0f4", size = 1101015, upload-time = "2026-05-14T22:44:05.055Z" }, + { url = "https://files.pythonhosted.org/packages/3d/6d/3dfddef4990fda47745af6615a3e51c4de711eda56c3a8072a0d8b6181c7/ast_serialize-0.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:7234ff086cb152ea2a3b7ef895b5ebeb6d80779df049d5c6431c8e3536d5b03c", size = 1074495, upload-time = "2026-05-14T22:44:07.186Z" }, + { url = "https://files.pythonhosted.org/packages/be/d5/044c5f995ef75807a0effb56fc288cfdedeeb571222450fb6f7d94fd52f1/ast_serialize-0.4.0-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:dcded5056d9f3d201df7833082c07ebcbc566ffc3d4105c9fc9fe278fa086ecb", size = 1189800, upload-time = "2026-05-14T22:44:09.333Z" }, + { url = "https://files.pythonhosted.org/packages/a9/5a/52163557789d59a8197c10912ab4a1791c9143731ba0e3d9283ac0791db6/ast_serialize-0.4.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:bd50d201098aae0d202805fe9606c0545492f69a3ec4403337e32c54ad29fc41", size = 1181713, upload-time = "2026-05-14T22:44:11.286Z" }, + { url = "https://files.pythonhosted.org/packages/2c/c3/678ce3b6cb594b01c361da87f6c5679d26c1dae1583a082a8cd190e7232e/ast_serialize-0.4.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6615b39cd747967c3aabe68bf3f5f26748e823cc6b474ddc1510ed188a824149", size = 1243258, upload-time = "2026-05-14T22:44:13.345Z" }, + { url = "https://files.pythonhosted.org/packages/3d/dd/4810fbeb81c47b7e4e65db15ca65c71330efc59b460bd10c12338dc6012e/ast_serialize-0.4.0-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:91362c0a9fdf1c344b7f50a5b0508b11a0732102998fbd754a191f7187e77031", size = 1239226, upload-time = "2026-05-14T22:44:15.811Z" }, + { url = "https://files.pythonhosted.org/packages/28/38/13a88d90b664c009ed208346ec2ed248b0ab2cb0b582ae467acaa7f44fa4/ast_serialize-0.4.0-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:70d9c5d527bbfa69bd3c7d17dac11fb6781e36186a434a06d7d5892e0b2f88f9", size = 1448867, upload-time = "2026-05-14T22:44:17.99Z" }, + { url = "https://files.pythonhosted.org/packages/4c/19/a069dba1a634b703bf07fb49df8f7e3c04e9ba8ef3f0d9f4495f72630f92/ast_serialize-0.4.0-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4738790cf54d8b416de992b87ee567056980bc82134d52458bd4985f389d1658", size = 1264135, upload-time = "2026-05-14T22:44:19.8Z" }, + { url = "https://files.pythonhosted.org/packages/2d/4c/76ec4279fecd7e78b60c3c99321f944c43cd11e5ff09c952746f5f9c0f4c/ast_serialize-0.4.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:faa008dccfcb793ae9101325e4d6d026caaa5d845c2182f03749c759834b0a3a", size = 1269060, upload-time = "2026-05-14T22:44:21.894Z" }, + { url = "https://files.pythonhosted.org/packages/33/c5/9230ef7481e5cb63b93a1f7738e959586202b081caf32b8bc5d9f673ef56/ast_serialize-0.4.0-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1c5245228e65d38cb48e1251f0ca71b0fa417e527141491e8c92f740e8e2d121", size = 1309654, upload-time = "2026-05-14T22:44:23.725Z" }, + { url = "https://files.pythonhosted.org/packages/b9/54/7d7397528d181ad68e476e0c81aa3ceff7d1f1b5c7fa958d6be28628ef16/ast_serialize-0.4.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:8f5153e9c44a02e61f4042c5f9249d2e8a759773d621a0b2f445a899e536e181", size = 1418855, upload-time = "2026-05-14T22:44:25.415Z" }, + { url = "https://files.pythonhosted.org/packages/b8/8f/87d6428adaa0986b817404f09329b64f8d2614cfe061ebf4951b4a7e0d19/ast_serialize-0.4.0-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:1e1fb90def261f6a0db885876f7e1a49ad2dbac38ad9f2f62dba2f9543af16e7", size = 1516040, upload-time = "2026-05-14T22:44:27.535Z" }, + { url = "https://files.pythonhosted.org/packages/b5/bb/5aaa41a21314c8b0d6dee54867b16535682c6660dd28cac64dba1380062d/ast_serialize-0.4.0-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:cf2ff7b654c8e95143e20f5d75878cbb78b65b928b26c4d58ef71cdba9d6d981", size = 1511450, upload-time = "2026-05-14T22:44:29.522Z" }, + { url = "https://files.pythonhosted.org/packages/87/16/cc729b5bb4b21da99db1379266cc367512e82ba10f9b3300a6f3e9941325/ast_serialize-0.4.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:90fc5c0d35a22f1a92dd33635508626d50f8fc64deb897c23e78e666a60804c9", size = 1463654, upload-time = "2026-05-14T22:44:31.265Z" }, + { url = "https://files.pythonhosted.org/packages/43/97/7198321b0244d011093387b41affea934d58bda08d59a2adfde72976b6c4/ast_serialize-0.4.0-cp39-abi3-win32.whl", hash = "sha256:9ecd6a1fc1b86f1f4e8ae206759b6319c10019706b3496b01b54d02b9b2cd918", size = 1068636, upload-time = "2026-05-14T22:44:33.189Z" }, + { url = "https://files.pythonhosted.org/packages/10/09/3b868f6d8df4bbe452903a5e0e039ebcec9ea0045f1a77951546205097e8/ast_serialize-0.4.0-cp39-abi3-win_amd64.whl", hash = "sha256:79c8d015c771c8bfdb1208003b227b27c40034790a2c29c09f2317a041825ce2", size = 1107137, upload-time = "2026-05-14T22:44:35.304Z" }, + { url = "https://files.pythonhosted.org/packages/fd/78/9387dffccdc55a12734f83aaccc4a987404a217a2a12a1920d8d4585950b/ast_serialize-0.4.0-cp39-abi3-win_arm64.whl", hash = "sha256:1026f565a7ab846337c630909089b3346a2fe417bf1552b1581ab01852137407", size = 1079199, upload-time = "2026-05-14T22:44:36.816Z" }, ] [[package]] @@ -500,9 +507,7 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0c/58/fc576f99037ce19c5aa16628e4c3226b6d1419f72a62c79f5f40576e6eb3/greenlet-3.5.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:5a5ed18de6a0f6cc7087f1563f6bd93fc7df1c19165ca01e9bde5a5dc281d106", size = 285066, upload-time = "2026-04-27T12:23:05.033Z" }, { url = "https://files.pythonhosted.org/packages/4a/ba/b28ddbe6bfad6a8ac196ef0e8cff37bc65b79735995b9e410923fffeeb70/greenlet-3.5.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a717fbc46d8a354fa675f7c1e813485b6ba3885f9bef0cd56e5ba27d758ff5b", size = 604414, upload-time = "2026-04-27T12:52:42.358Z" }, { url = "https://files.pythonhosted.org/packages/09/06/4b69f8f0b67603a8be2790e55107a190b376f2627fe0eaf5695d85ffb3cd/greenlet-3.5.0-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ddc090c5c1792b10246a78e8c2163ebbe04cf877f9d785c230a7b27b39ad038e", size = 617349, upload-time = "2026-04-27T12:59:43.32Z" }, - { url = "https://files.pythonhosted.org/packages/6a/15/a643b4ecd09969e30b8a150d5919960caae0abe4f5af75ab040b1ab85e78/greenlet-3.5.0-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4964101b8585c144cbda5532b1aa644255126c08a265dae90c16e7a0e63aaa9d", size = 623234, upload-time = "2026-04-27T13:02:40.611Z" }, { url = "https://files.pythonhosted.org/packages/8a/17/a3918541fd0ddefe024a69de6d16aa7b46d36ac19562adaa63c7fa180eff/greenlet-3.5.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2094acd54b272cb6eae8c03dd87b3fa1820a4cef18d6889c378d503500a1dc13", size = 613927, upload-time = "2026-04-27T12:25:30.28Z" }, - { url = "https://files.pythonhosted.org/packages/77/18/3b13d5ef1275b0ffaf933b05efa21408ac4ca95823c7411d79682e4fdcff/greenlet-3.5.0-cp313-cp313-manylinux_2_39_riscv64.whl", hash = "sha256:7022615368890680e67b9965d33f5773aade330d5343bbe25560135aaa849eae", size = 425243, upload-time = "2026-04-27T13:05:15.689Z" }, { url = "https://files.pythonhosted.org/packages/ee/e1/bd0af6213c7dd33175d8a462d4c1fe1175124ebed4855bc1475a5b5242c2/greenlet-3.5.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5e05ba267789ea87b5a155cf0e810b1ab88bf18e9e8740813945ceb8ee4350ba", size = 1570893, upload-time = "2026-04-27T12:53:29.483Z" }, { url = "https://files.pythonhosted.org/packages/9b/2a/0789702f864f5382cb476b93d7a9c823c10472658102ccd65f415747d2e2/greenlet-3.5.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0ecec963079cd58cbd14723582384f11f166fd58883c15dcbfb342e0bc9b5846", size = 1636060, upload-time = "2026-04-27T12:25:28.845Z" }, { url = "https://files.pythonhosted.org/packages/b2/8f/22bf9df92bbff0eb07842b60f7e63bf7675a9742df628437a9f02d09137f/greenlet-3.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:728d9667d8f2f586644b748dbd9bb67e50d6a9381767d1357714ea6825bb3bf5", size = 238740, upload-time = "2026-04-27T12:24:01.341Z" }, @@ -510,9 +515,7 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/94/5e/a70f31e3e8d961c4ce589c15b28e4225d63704e431a23932a3808cbcc867/greenlet-3.5.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:f35807464c4c58c55f0d31dfa83c541a5615d825c2fe3d2b95360cf7c4e3c0a8", size = 285564, upload-time = "2026-04-27T12:23:08.555Z" }, { url = "https://files.pythonhosted.org/packages/af/a6/046c0a28e21833e4086918218cfb3d8bed51c075a1b700f20b9d7861c0f4/greenlet-3.5.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:55fa7ea52771be44af0de27d8b80c02cd18c2c3cddde6c847ecebdf72418b6a1", size = 651166, upload-time = "2026-04-27T12:52:43.644Z" }, { url = "https://files.pythonhosted.org/packages/47/f8/4af27f71c5ff32a7fbc516adb46370d9c4ae2bc7bd3dc7d066ac542b4b15/greenlet-3.5.0-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a97e4821aa710603f94de0da25f25096454d78ffdace5dc77f3a006bc01abba3", size = 663792, upload-time = "2026-04-27T12:59:44.93Z" }, - { url = "https://files.pythonhosted.org/packages/fb/89/2dadb89793c37ee8b4c237857188293e9060dc085f19845c292e00f8e091/greenlet-3.5.0-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bf2d8a80bec89ab46221ae45c5373d5ba0bd36c19aa8508e85c6cd7e5106cd37", size = 668086, upload-time = "2026-04-27T13:02:42.314Z" }, { url = "https://files.pythonhosted.org/packages/a3/59/1bd6d7428d6ed9106efbb8c52310c60fd04f6672490f452aeaa3829aa436/greenlet-3.5.0-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8f52a464e4ed91780bdfbbdd2b97197f3accaa629b98c200f4dffada759f3ae7", size = 660933, upload-time = "2026-04-27T12:25:33.276Z" }, - { url = "https://files.pythonhosted.org/packages/82/35/75722be7e26a2af4cbd2dc35b0ed382dacf9394b7e75551f76ed1abe87f2/greenlet-3.5.0-cp314-cp314-manylinux_2_39_riscv64.whl", hash = "sha256:1bae92a1dd94c5f9d9493c3a212dd874c202442047cf96446412c862feca83a2", size = 470799, upload-time = "2026-04-27T13:05:17.094Z" }, { url = "https://files.pythonhosted.org/packages/83/e4/b903e5a5fae1e8a28cdd32a0cfbfd560b668c25b692f67768822ddc5f40f/greenlet-3.5.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:762612baf1161ccb8437c0161c668a688223cba28e1bf038f4eb47b13e39ccdf", size = 1618401, upload-time = "2026-04-27T12:53:31.062Z" }, { url = "https://files.pythonhosted.org/packages/0e/e3/5ec408a329acb854fb607a122e1ee5fb3ff649f9a97952948a90803c0d8e/greenlet-3.5.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:57a43c6079a89713522bc4bcb9f75070ecf5d3dbad7792bfe42239362cbf2a16", size = 1682038, upload-time = "2026-04-27T12:25:31.838Z" }, { url = "https://files.pythonhosted.org/packages/91/20/6b165108058767ee643c55c5c4904d591a830ee2b3c7dbd359828fbc829f/greenlet-3.5.0-cp314-cp314-win_amd64.whl", hash = "sha256:3bc59be3945ae9750b9e7d45067d01ae3fe90ea5f9ade99239dabdd6e28a5033", size = 239835, upload-time = "2026-04-27T12:24:54.136Z" }, @@ -520,9 +523,7 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/78/a8/4522939255bb5409af4e87132f915446bf3622c2c292d14d3c38d128ae82/greenlet-3.5.0-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:a10a732421ab4fec934783ce3e54763470d0181db6e3468f9103a275c3ed1853", size = 293614, upload-time = "2026-04-27T12:24:12.874Z" }, { url = "https://files.pythonhosted.org/packages/15/5e/8744c52e2c027b5a8772a01561934c8835f869733e101f62075c60430340/greenlet-3.5.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7fc391b1566f2907d17aaebe78f8855dc45675159a775fcf9e61f8ee0078e87f", size = 650723, upload-time = "2026-04-27T12:52:45.412Z" }, { url = "https://files.pythonhosted.org/packages/00/ef/7b4c39c03cf46ceca512c5d3f914afd85aa30b2cc9a93015b0dd73e4be6c/greenlet-3.5.0-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:680bd0e7ad5e8daa8a4aa89f68fd6adc834b8a8036dc256533f7e08f4a4b01f7", size = 656529, upload-time = "2026-04-27T12:59:46.295Z" }, - { url = "https://files.pythonhosted.org/packages/5f/5c/0602239503b124b70e39355cbdb39361ecfe65b87a5f2f63752c32f5286f/greenlet-3.5.0-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1aa4ce8debcd4ea7fb2e150f3036588c41493d1d52c43538924ae1819003f4ce", size = 657015, upload-time = "2026-04-27T13:02:43.973Z" }, { url = "https://files.pythonhosted.org/packages/0b/b5/c7768f352f5c010f92064d0063f987e7dc0cd290a6d92a34109015ce4aa1/greenlet-3.5.0-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ddb36c7d6c9c0a65f18c7258634e0c416c6ab59caac8c987b96f80c2ebda0112", size = 654364, upload-time = "2026-04-27T12:25:35.64Z" }, - { url = "https://files.pythonhosted.org/packages/38/51/8699f865f125dc952384cb432b0f7138aa4d8f2969a7d12d0df5b94d054d/greenlet-3.5.0-cp314-cp314t-manylinux_2_39_riscv64.whl", hash = "sha256:728a73687e39ae9ca34e4694cbf2f049d3fbc7174639468d0f67200a97d8f9e2", size = 488275, upload-time = "2026-04-27T13:05:18.28Z" }, { url = "https://files.pythonhosted.org/packages/ef/d0/079ebe12e4b1fc758857ce5be1a5e73f06870f2101e52611d1e71925ce54/greenlet-3.5.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e5ddf316ced87539144621453c3aef229575825fe60c604e62bedc4003f372b2", size = 1614204, upload-time = "2026-04-27T12:53:32.618Z" }, { url = "https://files.pythonhosted.org/packages/6d/89/6c2fb63df3596552d20e58fb4d96669243388cf680cff222758812c7bfaa/greenlet-3.5.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:4a448128607be0de65342dc9b31be7f948ef4cc0bc8832069350abefd310a8f2", size = 1675480, upload-time = "2026-04-27T12:25:34.168Z" }, { url = "https://files.pythonhosted.org/packages/15/32/77ee8a6c1564fc345a491a4e85b3bf360e4cf26eac98c4532d2fdb96e01f/greenlet-3.5.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d60097128cb0a1cab9ea541186ea13cd7b847b8449a7787c2e2350da0cb82d86", size = 245324, upload-time = "2026-04-27T12:24:40.295Z" }, @@ -589,11 +590,11 @@ wheels = [ [[package]] name = "idna" -version = "3.13" +version = "3.15" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ce/cc/762dfb036166873f0059f3b7de4565e1b5bc3d6f28a414c13da27e442f99/idna-3.13.tar.gz", hash = "sha256:585ea8fe5d69b9181ec1afba340451fba6ba764af97026f92a91d4eef164a242", size = 194210, upload-time = "2026-04-22T16:42:42.314Z" } +sdist = { url = "https://files.pythonhosted.org/packages/82/77/7b3966d0b9d1d31a36ddf1746926a11dface89a83409bf1483f0237aa758/idna-3.15.tar.gz", hash = "sha256:ca962446ea538f7092a95e057da437618e886f4d349216d2b1e294abfdb65fdc", size = 199245, upload-time = "2026-05-12T22:45:57.011Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5d/13/ad7d7ca3808a898b4612b6fe93cde56b53f3034dcde235acb1f0e1df24c6/idna-3.13-py3-none-any.whl", hash = "sha256:892ea0cde124a99ce773decba204c5552b69c3c67ffd5f232eb7696135bc8bb3", size = 68629, upload-time = "2026-04-22T16:42:40.909Z" }, + { url = "https://files.pythonhosted.org/packages/d2/23/408243171aa9aaba178d3e2559159c24c1171a641aa83b67bdd3394ead8e/idna-3.15-py3-none-any.whl", hash = "sha256:048adeaf8c2d788c40fee287673ccaa74c24ffd8dcf09ffa555a2fbb59f10ac8", size = 72340, upload-time = "2026-05-12T22:45:55.733Z" }, ] [[package]] @@ -691,49 +692,49 @@ wheels = [ [[package]] name = "librt" -version = "0.10.0" +version = "0.11.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/39/cb/c1945e506893b5b8577fb45a60c80e3ffe4a82092a04a6f29b0b951d9a24/librt-0.10.0.tar.gz", hash = "sha256:1aba1e8aa4e3307a7be68a74149545fde7451964dc0235a8bec5704a17bdda42", size = 191799, upload-time = "2026-05-05T16:31:23.535Z" } +sdist = { url = "https://files.pythonhosted.org/packages/40/08/9e7f6b5d2b5bed6ad055cdd5925f192bb403a51280f86b56554d9d0699a2/librt-0.11.0.tar.gz", hash = "sha256:075dc3ef4458a278e0195cbf6ac9d38808d9b906c5a6c7f7f79c3888276a3fb1", size = 200139, upload-time = "2026-05-10T18:17:25.138Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e5/29/681a75c82f4cc90d29e4b257a3299b79fe13fe927a04c57b8109d70b6957/librt-0.10.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f0ede79d682e73f91c1b599a76d78b7464b9b5d213754cedb13372d9df36e596", size = 77299, upload-time = "2026-05-05T16:30:00.209Z" }, - { url = "https://files.pythonhosted.org/packages/62/24/0c7ca445a55d04be79cac19819437fd094782347fa116f6681844fa6143e/librt-0.10.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e0ba0b131fdb336c8b9c948e397f4a7e649d0f783b529f07b647bf4961df392e", size = 79930, upload-time = "2026-05-05T16:30:01.555Z" }, - { url = "https://files.pythonhosted.org/packages/fe/1f/1e2b8f6443ef9e9a81e89486ca70e22f3684f93db003ce6eaefc3d0839b9/librt-0.10.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2728117da2afb96fb957768725ee43dc9a2d73b031e02da424b818a3cdd3a275", size = 246195, upload-time = "2026-05-05T16:30:03.261Z" }, - { url = "https://files.pythonhosted.org/packages/74/61/9dc9e03de0439ad84c1c240aac8b747f12c90cb797ea6042f7bdb8d3410f/librt-0.10.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:723ba80594c49cdf0584196fc430752262605dc9449902fc9bd3d9b79976cb77", size = 234951, upload-time = "2026-05-05T16:30:04.881Z" }, - { url = "https://files.pythonhosted.org/packages/55/f4/635223117d7590875bca441275065a3bf491203ad4208bd1cc3ffd90c5a1/librt-0.10.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7292edaaca294a61a978c53a3c7d6130d099b0dfbc8f0a65916cdc6b891b9852", size = 262768, upload-time = "2026-05-05T16:30:06.638Z" }, - { url = "https://files.pythonhosted.org/packages/e5/66/b04152d0cd8b6ca2b428a8bd3230343230c35ed304a932f35b5375f2f828/librt-0.10.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:89fe9d539f2c10a1666633eeeac507ce95dd06d9ecc58de3c6390dba156a3d3a", size = 255075, upload-time = "2026-05-05T16:30:08.216Z" }, - { url = "https://files.pythonhosted.org/packages/35/1e/25bac4c7f2ca36f0e612cade186970683cf79153d96beccc3a11a9e19b97/librt-0.10.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4efa7b9587503fa5b67f40593302b9c8836d211d222ff9f7cafe67be5f8f0b10", size = 268559, upload-time = "2026-05-05T16:30:10.1Z" }, - { url = "https://files.pythonhosted.org/packages/18/54/4601faab35b6632a13200faa146ca62bfd111ffbe2568be430d65c89493a/librt-0.10.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:22dc982ef59df0136df36092ccbdbb570ced8aafb33e49585739b2f1de1c13b6", size = 261753, upload-time = "2026-05-05T16:30:11.912Z" }, - { url = "https://files.pythonhosted.org/packages/1b/cf/39f4023509e94fade8b074666fa3292db9cb6b34ea5dcbe7af53df9fca1d/librt-0.10.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:6f2e5f3606253a84cea719c94a3bb1c54487b5d617d0254d46e0920d8a06be3f", size = 264055, upload-time = "2026-05-05T16:30:13.465Z" }, - { url = "https://files.pythonhosted.org/packages/8e/00/40247209fc46a8e308a91412d5206aedf8efb667ee89eb625820106a5c2f/librt-0.10.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:40884bfaa1e29f6b6a9be255007d8f359bfc9e61d68bdef8ed3158bfcbc95df9", size = 286190, upload-time = "2026-05-05T16:30:15.073Z" }, - { url = "https://files.pythonhosted.org/packages/d8/6e/5566beb94431a985abe1787af5ef86e087750172ff9d0bbf20f93e88132d/librt-0.10.0-cp313-cp313-win32.whl", hash = "sha256:3cd34cd8254eba756660bff6c2da91278248184301054fe3e4feb073bdd49b14", size = 62949, upload-time = "2026-05-05T16:30:16.503Z" }, - { url = "https://files.pythonhosted.org/packages/d0/c2/3ea3301d6c8dff51d39dbe8ed75db3dc92896947d4afb5eeadf821c1e67f/librt-0.10.0-cp313-cp313-win_amd64.whl", hash = "sha256:7baac5313e2d8dce1386f97777a8d03ab28f5fe1e780b3b9ac2ee7544551fedc", size = 71152, upload-time = "2026-05-05T16:30:17.766Z" }, - { url = "https://files.pythonhosted.org/packages/3c/de/5d49cb92cadcbc77d3abc27b93fd6030ed8437487dde2eae38cab5e6704d/librt-0.10.0-cp313-cp313-win_arm64.whl", hash = "sha256:afc5b4406c8e2515698d922a5c7823a009312835ea58196671fff40e35cb8166", size = 61336, upload-time = "2026-05-05T16:30:19.021Z" }, - { url = "https://files.pythonhosted.org/packages/6a/64/7165e08108cc185a13a9c069f0685e6ef92e70e07fddf7edf5e7348c6316/librt-0.10.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:f09588a30e6a22ec624090d72a3ab1a6d4d5485c3ed739603e76aa3c16efa688", size = 76794, upload-time = "2026-05-05T16:30:20.392Z" }, - { url = "https://files.pythonhosted.org/packages/ae/ef/bf8613febf651b90c5222ee79dea5ae58d4cc2b544df69d3033424448934/librt-0.10.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:131ade118d12bd7a0adc4e655474a553f1b76cf78385868885944d21d51e45e0", size = 79662, upload-time = "2026-05-05T16:30:22.025Z" }, - { url = "https://files.pythonhosted.org/packages/b6/67/9eddd165c1d8397bdf99b38bf12b5a55b3def5035b49eedb49f2775d1430/librt-0.10.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b8b9ab28e40d011c373a189eae900c916e66d6fbecf7983e9e4883089ee085ef", size = 242390, upload-time = "2026-05-05T16:30:23.51Z" }, - { url = "https://files.pythonhosted.org/packages/10/d1/d95da80334501866cd37004ab5d7483220d05862fab4b5405394f0264f0d/librt-0.10.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:67c39bb30da73bae1f293d1ed8bc2f8f6642649dd0928d3600aeff3041ac23d6", size = 232603, upload-time = "2026-05-05T16:30:25.198Z" }, - { url = "https://files.pythonhosted.org/packages/0c/fa/e6d64d28718bc1be4e1736fcb037ca1c4dfca927e7167df75a7d5215665e/librt-0.10.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8c3273c6b774614f093c8927c2bf1b077d0fefde988fe98f46a333734e5597ab", size = 259187, upload-time = "2026-05-05T16:30:26.772Z" }, - { url = "https://files.pythonhosted.org/packages/72/3f/3fdb77e7f937dad59cfd76b720be7e7643400ec76b2da35befab8d66ba30/librt-0.10.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9dd7c1b86a4baa583ab5db977484b93a2c474e69e96ef3e9538387ea54229cb9", size = 251846, upload-time = "2026-05-05T16:30:28.56Z" }, - { url = "https://files.pythonhosted.org/packages/18/ca/f4d49133dd86a6f55d79eca30bf412fa722f511a9abe67f62f57aa64e66a/librt-0.10.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a77385c5a202e831149f7ad03be9e67cf80e957e52c614e83dcb822c95222eb8", size = 264936, upload-time = "2026-05-05T16:30:30.491Z" }, - { url = "https://files.pythonhosted.org/packages/de/66/a8df2fbadc1f6c1827a096d11c40175bd526133480bd3bc88ec64a03d257/librt-0.10.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:c6a5eafa74b5655bad59886138ed68426f098a6beb8cb95a71f2cc3cd8bb33fe", size = 258699, upload-time = "2026-05-05T16:30:32.002Z" }, - { url = "https://files.pythonhosted.org/packages/bb/73/1e3c83613fe05451bb969e27b68a573d177f08d5f63533cc29fec0989658/librt-0.10.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:1fc93d0439204c50ab4d1512611ce2c206f1b369b419f69c7c27c761561e3291", size = 259825, upload-time = "2026-05-05T16:30:35.077Z" }, - { url = "https://files.pythonhosted.org/packages/09/24/5e2f926ee9d3ef348d9339526d7062abb5c44d8419e3179528c01d78c102/librt-0.10.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:79e713c178bc7a744adfbee6b4619a288eecc0c914da2a9313a20255abe2f0cf", size = 282548, upload-time = "2026-05-05T16:30:36.639Z" }, - { url = "https://files.pythonhosted.org/packages/fc/7d/3e89ed6ad0162561fa8bef9df3195e24263104c955713cd0237d3711fad2/librt-0.10.0-cp314-cp314-win32.whl", hash = "sha256:2eba9d955a68c41d9f326be3da42f163ec3518b7ab20f1c826224e7bed71e0bf", size = 58970, upload-time = "2026-05-05T16:30:38.183Z" }, - { url = "https://files.pythonhosted.org/packages/76/25/579e731c94a7086a268bfa3e7a4945cd47836bebd3cbf3faeafd2e7eaef9/librt-0.10.0-cp314-cp314-win_amd64.whl", hash = "sha256:cbfaf7f5145e9917f5d18bffa298eff6a19d74e7b8b11dabdca95785befe8dbf", size = 67260, upload-time = "2026-05-05T16:30:39.804Z" }, - { url = "https://files.pythonhosted.org/packages/6e/f8/235822b7ae0b2334f12ee18bcf2476d07924077a5efeea57dbe927704be2/librt-0.10.0-cp314-cp314-win_arm64.whl", hash = "sha256:8d6d385d1969849a6b1397114df22714b6ded917bada98668e3e974dc663477e", size = 57156, upload-time = "2026-05-05T16:30:41.412Z" }, - { url = "https://files.pythonhosted.org/packages/9f/e3/9b919cbf1e8eb770bf91bb7df28125e0f1daf4587169afefd95402636e9a/librt-0.10.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:6c3a82d3bd32631ef5c79922dfc028520c9ad840255979ab4d908271818039ee", size = 79150, upload-time = "2026-05-05T16:30:42.761Z" }, - { url = "https://files.pythonhosted.org/packages/6a/f5/72a944aa3bc3498169a168087eff58ca48b58bf1b704e59d091fd30739f3/librt-0.10.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d64cc66005dc324c9bb1fa3fc2841f529002f6eb15966d55e46d430f56955a6a", size = 82304, upload-time = "2026-05-05T16:30:44.082Z" }, - { url = "https://files.pythonhosted.org/packages/9c/e3/fcc290a33e295019759472dfa794d204e43504b276ac65eab7fd9da20ea3/librt-0.10.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9bb562cd28c88cd2c6a9a6c78f99dc39348d6b16c94adc25de0e574acf1176e9", size = 272556, upload-time = "2026-05-05T16:30:45.497Z" }, - { url = "https://files.pythonhosted.org/packages/fd/54/546975e4c997573885e7f040a05012f8838e06fb12b0c3c1fbb76254e9d7/librt-0.10.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:b809aa2854d019c28773b03605df22adc675ee4f3f4402d673581313e8906119", size = 256941, upload-time = "2026-05-05T16:30:47.059Z" }, - { url = "https://files.pythonhosted.org/packages/70/8c/f1d03401571b331653acddbd4e8cd955c06d945241dd08b25192fac0d04b/librt-0.10.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cc15acabdd519bd4176fdadc2119e5e3093485d86f89138daf47e5b4cedb983a", size = 285855, upload-time = "2026-05-05T16:30:48.86Z" }, - { url = "https://files.pythonhosted.org/packages/0c/08/62cf80ff046c339faf56718b3a940244d4beb70f1c6407289b5830ec11e9/librt-0.10.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b1b2d835307d08ddadd94568e2369648ec9173bd3eea6d7f52a1abe717c81f98", size = 275321, upload-time = "2026-05-05T16:30:50.63Z" }, - { url = "https://files.pythonhosted.org/packages/d9/ea/da5918d4070362e9a4d2ee9cd34f9dc84902daad8fd4275f8504a727ff4e/librt-0.10.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d261c6a2f93335a5167887fb0223e8b98ffce20ee3fde242e8e58a37ece6d0e5", size = 293993, upload-time = "2026-05-05T16:30:52.577Z" }, - { url = "https://files.pythonhosted.org/packages/c9/8d/68b6086bed1fcdc314c640ea04e31e52d18052e08059fa595409d66a51a9/librt-0.10.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:e2ffd44963f8e7f68995504d90f9881d64e94dc1d8e310039b9526108fc0c0f7", size = 284254, upload-time = "2026-05-05T16:30:55.086Z" }, - { url = "https://files.pythonhosted.org/packages/06/c8/b810f1d84ec34a5a7ed93d7b510ab04164d75fbdf23088d5c3fbe6b08357/librt-0.10.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:5f285f6455ed495791c4d8630e5af732960adea93cac4c893d15619f2eae53e8", size = 284925, upload-time = "2026-05-05T16:30:56.728Z" }, - { url = "https://files.pythonhosted.org/packages/5a/00/3c82d4158c5a2c62528b8fccce65a8c9ad700e480e86f9389387435089a5/librt-0.10.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f6034ff52e663d34c7b82ef2aa2f94ad7c1d939e2368e63b06844bc4d127d2e1", size = 307830, upload-time = "2026-05-05T16:30:58.377Z" }, - { url = "https://files.pythonhosted.org/packages/99/3a/9c635ac3e8a00383ff689161d3eac8a30b3b2ddc711b40471e6b8983ea29/librt-0.10.0-cp314-cp314t-win32.whl", hash = "sha256:657860fd877fba6a241ea088ef99f63ca819945d3c715265da670bad56c37ebe", size = 60147, upload-time = "2026-05-05T16:31:00.293Z" }, - { url = "https://files.pythonhosted.org/packages/dc/e8/6f65f3e565d4ac212cddddd552eacc8035ffdf941ca0ad6fe945a211d41f/librt-0.10.0-cp314-cp314t-win_amd64.whl", hash = "sha256:56ded2d66010203a0cb5af063b609e3f079531a0e5e576d618dece859fd2e1af", size = 68649, upload-time = "2026-05-05T16:31:01.778Z" }, - { url = "https://files.pythonhosted.org/packages/51/78/a0705a67cacd81e5fa01a5035b3adbdfbb43a7b8d4bd27e2b282ae61baf2/librt-0.10.0-cp314-cp314t-win_arm64.whl", hash = "sha256:1ee63f30abf18ed4830fdbaf87b2b6f4bba1e198d46085c314edde4045e56715", size = 58247, upload-time = "2026-05-05T16:31:03.191Z" }, + { url = "https://files.pythonhosted.org/packages/82/61/e59168d4d0bf2bf90f4f0caf7a001bfc60254c3af4586013b04dc3ef517b/librt-0.11.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:78dc31f7fdfe9c9d0eb0e8f42d139db230e826415bbcabd9f0e9faaaee909894", size = 144119, upload-time = "2026-05-10T18:16:11.771Z" }, + { url = "https://files.pythonhosted.org/packages/61/fd/caa1d60b12f7dd79ccea23054e06eeaebe266a5f52c40a6b651069200ce5/librt-0.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fa475675db22290c3158e1d42326d0f5a65f04f44a0e68c3630a25b53560fb9c", size = 143565, upload-time = "2026-05-10T18:16:13.334Z" }, + { url = "https://files.pythonhosted.org/packages/b8/a9/dc744f5c2b4978d48db970be29f22716d3413d28b14ad99740817315cf2c/librt-0.11.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:621db29691044bdeda22e789e482e1b0f3a985d90e3426c9c6d17606416205ea", size = 485395, upload-time = "2026-05-10T18:16:14.729Z" }, + { url = "https://files.pythonhosted.org/packages/8f/21/7f8e97a1e4dae952a5a95948f6f8507a173bc1e669f54340bba6ca1ca31b/librt-0.11.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:a9010e2ed5b3a9e158c5fd966b3ab7e834bb3d3aacc8f66c91dd4b57a3799230", size = 479383, upload-time = "2026-05-10T18:16:16.321Z" }, + { url = "https://files.pythonhosted.org/packages/a6/6d/d8ee9c114bebf2c50e29ec2aa940826fccb62a645c3e4c18760987d0e16d/librt-0.11.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7c39513d8b7477a2e1ed8c43fc21c524e8d5a0f8d4e8b7b074dbdbe7820a08e2", size = 513010, upload-time = "2026-05-10T18:16:17.647Z" }, + { url = "https://files.pythonhosted.org/packages/f0/43/0b5708af2bd30a46400e72ba6bdaa8f066f15fb9a688527e34220e8d6c06/librt-0.11.0-cp313-cp313-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7aef3cf1d5af86e770ab04bfd993dfc4ae8b8c17f66fb77dd4a7d50de7bbb1a3", size = 508433, upload-time = "2026-05-10T18:16:19.309Z" }, + { url = "https://files.pythonhosted.org/packages/4a/50/356187247d09013490481033183b3532b58acf8028bcb34b2b56a375c9b2/librt-0.11.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:557183ddc36babe46b27dd60facbd5adb4492181a5be887587d57cda6e092f21", size = 522595, upload-time = "2026-05-10T18:16:20.642Z" }, + { url = "https://files.pythonhosted.org/packages/40/e7/c6ac4240899c7f3248079d5a9900debe0dadb3fdeaf856684c987105ba47/librt-0.11.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:83d3e1f72bd42f6c5c0b7daec530c3f829bd02db42c70b8ddf0c2d90a2459930", size = 527255, upload-time = "2026-05-10T18:16:22.352Z" }, + { url = "https://files.pythonhosted.org/packages/eb/b5/a81322dbeedeeaf9c1ee6f001734d28a09d8383ac9e6779bc24bbd0743c6/librt-0.11.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:4ce1f21fbe589bc1afd7872dece84fb0e1144f794a288e58a10d2c54a55c43be", size = 516847, upload-time = "2026-05-10T18:16:23.627Z" }, + { url = "https://files.pythonhosted.org/packages/ae/66/6e6323787d592b55204a42595ff1102da5115601b53a7e9ddebc889a6da5/librt-0.11.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:970b09f7044ea2b64c9da42fd3d335666518cfd1c6e8a182c95da73d0214b41e", size = 553920, upload-time = "2026-05-10T18:16:25.025Z" }, + { url = "https://files.pythonhosted.org/packages/9c/21/623f8ca230857102066d9ca8c6c1734995908c4d0d1bee7bb2ef0021cb33/librt-0.11.0-cp313-cp313-win32.whl", hash = "sha256:78fddc31cd4d3caa897ad5d31f856b1faadc9474021ad6cb182b9018793e254e", size = 101898, upload-time = "2026-05-10T18:16:26.649Z" }, + { url = "https://files.pythonhosted.org/packages/b3/1d/b4ebd44dd723f768469007515cb92251e0ae286c94c140f374801140fa74/librt-0.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:8ca8aa88751a775870b764e93bad5135385f563cb8dcee399abf034ea4d3cb47", size = 119812, upload-time = "2026-05-10T18:16:27.859Z" }, + { url = "https://files.pythonhosted.org/packages/3b/e4/b2f4ca7965ca373b491cdb4bc25cdb30c1649ca81a8782056a83850292a9/librt-0.11.0-cp313-cp313-win_arm64.whl", hash = "sha256:96f044bb325fd9cf1a723015638c219e9143f0dfbc0ca54c565df2b7fc748b44", size = 103448, upload-time = "2026-05-10T18:16:29.066Z" }, + { url = "https://files.pythonhosted.org/packages/29/eb/dbce197da4e227779e56b5735f2decc3eb36e55a1cdbf1bd65d6639d76c1/librt-0.11.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4a017a95e5837dc15a8c5661d60e05daa96b90908b1aa6b7acdf443cd25c8ebd", size = 143345, upload-time = "2026-05-10T18:16:30.674Z" }, + { url = "https://files.pythonhosted.org/packages/76/a3/254bebd0c11c8ba684018efb8006ff22e466abce445215cca6c778e7d9de/librt-0.11.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b1ecbd9819deccc39b7542bf4d2a740d8a620694d39989e58661d3763458f8d4", size = 143131, upload-time = "2026-05-10T18:16:32.037Z" }, + { url = "https://files.pythonhosted.org/packages/f1/3f/f77d6122d21ac7bf6ae8a7dfced1bd2a7ac545d3273ebdcaf8042f6d619f/librt-0.11.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7da327dacd7be8f8ec36547373550744a3cc0e536d54665cd83f8bcd961200e8", size = 477024, upload-time = "2026-05-10T18:16:33.493Z" }, + { url = "https://files.pythonhosted.org/packages/ac/0a/2c996dadebaa7d9bbbd43ef2d4f3e66b6da545f838a41694ef6172cebec8/librt-0.11.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:0dc56b1f8d06e60db362cc3fdae206681817f86ce4725d34511473487f12a34b", size = 474221, upload-time = "2026-05-10T18:16:34.864Z" }, + { url = "https://files.pythonhosted.org/packages/0a/7e/f5d92af8486b8272c23b3e686b46ff72d89c8169585eb61eef01a2ac7147/librt-0.11.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:05fb8fb2ab90e21c8d12ea240d744ad514da9baf381ebfa70d91d20d21713175", size = 505174, upload-time = "2026-05-10T18:16:36.705Z" }, + { url = "https://files.pythonhosted.org/packages/af/1a/cb0734fe86398eb33193ab753b7326255c74cac5eb09e76b9b16536e7adb/librt-0.11.0-cp314-cp314-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cae74872be221df4374d10fec61f93ed1513b9546ea84f2c0bf73ab3e9bd0b03", size = 497216, upload-time = "2026-05-10T18:16:38.418Z" }, + { url = "https://files.pythonhosted.org/packages/18/06/094820f91558b66e29943c0ec41c9914f460f48dd51fc503c3101e10842d/librt-0.11.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:32bcc918c0148eb7e3d57385125bac7e5f9e4359d05f07448b09f6f778c2f31c", size = 513921, upload-time = "2026-05-10T18:16:39.848Z" }, + { url = "https://files.pythonhosted.org/packages/0b/c2/00de9018871a282f530cacb457d5ec0428f6ac7e6fedde9aff7468d9fb04/librt-0.11.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:f9743fc99135d5f78d2454435615f6dec0473ca507c26ce9d92b10b562a280d3", size = 520850, upload-time = "2026-05-10T18:16:41.471Z" }, + { url = "https://files.pythonhosted.org/packages/51/9d/64631832348fd1834fb3a61b996434edddaaf25a31d03b0a76273159d2cf/librt-0.11.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:5ba067f4aadae8fda802d91d2124c90c42195ff32d9161d3549e6d05cfe26f96", size = 504237, upload-time = "2026-05-10T18:16:43.15Z" }, + { url = "https://files.pythonhosted.org/packages/a5/ec/ae5525eb16edc827a044e7bb8777a455ff95d4bca9379e7e6bddd7383647/librt-0.11.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:de3bf945454d032f9e390b85c4072e0a0570bf825421c8be0e71209fa65e1abe", size = 546261, upload-time = "2026-05-10T18:16:44.408Z" }, + { url = "https://files.pythonhosted.org/packages/5a/09/adce371f27ca039411da9659f7430fcc2ba6cd0c7b3e4467a0f091be7fa9/librt-0.11.0-cp314-cp314-win32.whl", hash = "sha256:d2277a05f6dcb9fd13db9566aac4fabd68c3ea1ea46ee5567d4eef8efa495a2f", size = 96965, upload-time = "2026-05-10T18:16:46.039Z" }, + { url = "https://files.pythonhosted.org/packages/d6/ee/8ac720d98548f173c7ce2e632a7ca94673f74cacd5c8162a84af5b35958a/librt-0.11.0-cp314-cp314-win_amd64.whl", hash = "sha256:ab73e8db5e3f564d812c1f5c3a175930a5f9bc96ccb5e3b22a34d7858b401cf7", size = 115151, upload-time = "2026-05-10T18:16:47.133Z" }, + { url = "https://files.pythonhosted.org/packages/94/20/c900cf14efeb09b6bef2b2dff20779f73464b97fd58d1c6bccc379588ae3/librt-0.11.0-cp314-cp314-win_arm64.whl", hash = "sha256:aea3caa317752e3a466fa8af45d91ee0ea8c7fdd96e42b0a8dd9b76a7931eba1", size = 98850, upload-time = "2026-05-10T18:16:48.597Z" }, + { url = "https://files.pythonhosted.org/packages/0c/71/944bfe4b64e12abffcd3c15e1cce07f72f3d55655083786285f4dedeb532/librt-0.11.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:d1b36540d7aaf9b9101b3a6f376c8d8e9f7a9aec93ed05918f2c69d493ffef72", size = 151138, upload-time = "2026-05-10T18:16:49.839Z" }, + { url = "https://files.pythonhosted.org/packages/b6/10/99e64a5c86989357fda078c8143c533389585f6473b7439172dd8f3b3b2d/librt-0.11.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:efbb343ab2ce3540f4ecbe6315d677ed70f37cd9a72b1e58066c918ca83acbaa", size = 151976, upload-time = "2026-05-10T18:16:51.062Z" }, + { url = "https://files.pythonhosted.org/packages/21/31/5072ad880946d83e5ea4147d6d018c78eefce85b77819b19bdd0ee229435/librt-0.11.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa0dd688aab3f7914d3e6e5e3554978e0383312fb8e771d84be008a35b9ee548", size = 557927, upload-time = "2026-05-10T18:16:52.632Z" }, + { url = "https://files.pythonhosted.org/packages/5e/8d/70b5fb7cfbab60edbe7381614ab985da58e144fbf465c86d44c95f43cdca/librt-0.11.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:f5fb36b8c6c63fdcbb1d526d94c0d1331610d43f4118cc1beb4efef4f3faacb2", size = 539698, upload-time = "2026-05-10T18:16:53.934Z" }, + { url = "https://files.pythonhosted.org/packages/fa/a3/ba3495a0b3edbd24a4cae0d1d3c64f39a9fc45d06e812101289b50c1a619/librt-0.11.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4a9a237d13addb93715b6fee74023d5ee3469b53fce527626c0e088aa585805f", size = 577162, upload-time = "2026-05-10T18:16:55.589Z" }, + { url = "https://files.pythonhosted.org/packages/f7/db/36e25fb81f99937ff1b96612a1dc9fd66f039cb9cc3aee12c01fac31aab9/librt-0.11.0-cp314-cp314t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5ddd17bd87b2c56ddd60e546a7984a2e64c4e8eab92fb4cf3830a48ad5469d51", size = 566494, upload-time = "2026-05-10T18:16:56.975Z" }, + { url = "https://files.pythonhosted.org/packages/33/0d/3f622b47f0b013eeb9cf4cc07ae9bfe378d832a4eec998b2b209fe84244d/librt-0.11.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bd43992b4473d42f12ff9e68326079f0696d9d4e6000e8f39a0238d482ba6ee2", size = 596858, upload-time = "2026-05-10T18:16:58.374Z" }, + { url = "https://files.pythonhosted.org/packages/a9/02/71b90bc93039c46a2000651f6ad60122b114c8f54c4ad306e0e96f5b75ad/librt-0.11.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:f8e3e8056dd674e279741485e2e512d6e9a751c7455809d0114e6ebf8d781085", size = 590318, upload-time = "2026-05-10T18:16:59.676Z" }, + { url = "https://files.pythonhosted.org/packages/04/04/418cb3f75621e2b761fb1ab0f017f4d70a1a72a6e7c74ee4f7e8d198c2f3/librt-0.11.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:c1f708d8ae9c56cf38a903c44297243d2ec83fd82b396b977e0144a3e76217e3", size = 575115, upload-time = "2026-05-10T18:17:01.007Z" }, + { url = "https://files.pythonhosted.org/packages/cc/2c/5a2183ac58dd911f26b5d7e7d7d8f1d87fcecdddd99d6c12169a258ff62c/librt-0.11.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0add982e0e7b9fc14cf4b33789d5f13f66581889b88c2f58099f6ce8f92617bd", size = 617918, upload-time = "2026-05-10T18:17:02.682Z" }, + { url = "https://files.pythonhosted.org/packages/15/1f/dc6771a52592a4451be6effa200cbfc9cec61e4393d3033d81a9d307961d/librt-0.11.0-cp314-cp314t-win32.whl", hash = "sha256:2b481d846ac894c4e8403c5fd0e87c5d11d6499e404b474602508a224ff531c8", size = 103562, upload-time = "2026-05-10T18:17:03.99Z" }, + { url = "https://files.pythonhosted.org/packages/62/4a/7d1415567027286a75ba1093ec4aca11f073e0f559c530cf3e0a757ad55c/librt-0.11.0-cp314-cp314t-win_amd64.whl", hash = "sha256:28edb433edde181112a908c78907af28f964eabc15f4dd16c9d66c834302677c", size = 124327, upload-time = "2026-05-10T18:17:05.465Z" }, + { url = "https://files.pythonhosted.org/packages/ce/62/b40b382fa0c66fee1478073eb8db352a4a6beda4a1adccf1df911d8c289c/librt-0.11.0-cp314-cp314t-win_arm64.whl", hash = "sha256:dee008f20b542e3cd162ba338a7f9ec0f6d23d395f66fe8aeeec3c9d067ea253", size = 102572, upload-time = "2026-05-10T18:17:06.809Z" }, ] [[package]] @@ -893,16 +894,47 @@ wheels = [ ] [[package]] -name = "multi-swarm" +name = "multi-swarm-coevolutive" version = "0.1.0" -source = { editable = "." } +source = { virtual = "." } + +[package.dev-dependencies] +dev = [ + { name = "multi-swarm-core" }, + { name = "mypy" }, + { name = "pytest" }, + { name = "pytest-asyncio" }, + { name = "pytest-mock" }, + { name = "responses" }, + { name = "ruff" }, + { name = "strategy-crypto" }, + { name = "types-requests" }, +] + +[package.metadata] + +[package.metadata.requires-dev] +dev = [ + { name = "multi-swarm-core", editable = "src/multi_swarm_core" }, + { name = "mypy", specifier = ">=1.13" }, + { name = "pytest", specifier = ">=8.3" }, + { name = "pytest-asyncio", specifier = ">=0.24" }, + { name = "pytest-mock", specifier = ">=3.14" }, + { name = "responses", specifier = ">=0.25" }, + { name = "ruff", specifier = ">=0.7" }, + { name = "strategy-crypto", editable = "src/strategy_crypto" }, + { name = "types-requests", specifier = ">=2.32" }, +] + +[[package]] +name = "multi-swarm-core" +version = "0.1.0" +source = { editable = "src/multi_swarm_core" } dependencies = [ { name = "httpx" }, - { name = "nicegui" }, { name = "numpy" }, { name = "openai" }, { name = "pandas" }, - { name = "plotly" }, { name = "pyarrow" }, { name = "pydantic" }, { name = "pydantic-settings" }, @@ -914,25 +946,12 @@ dependencies = [ { name = "yfinance" }, ] -[package.dev-dependencies] -dev = [ - { name = "mypy" }, - { name = "pytest" }, - { name = "pytest-asyncio" }, - { name = "pytest-mock" }, - { name = "responses" }, - { name = "ruff" }, - { name = "types-requests" }, -] - [package.metadata] requires-dist = [ { name = "httpx", specifier = ">=0.28" }, - { name = "nicegui", specifier = ">=3.11.1" }, { name = "numpy", specifier = ">=2.1" }, { name = "openai", specifier = ">=1.55" }, { name = "pandas", specifier = ">=2.2" }, - { name = "plotly", specifier = ">=5.24" }, { name = "pyarrow", specifier = ">=18.0" }, { name = "pydantic", specifier = ">=2.9" }, { name = "pydantic-settings", specifier = ">=2.6" }, @@ -944,17 +963,6 @@ requires-dist = [ { name = "yfinance", specifier = ">=1.3.0" }, ] -[package.metadata.requires-dev] -dev = [ - { name = "mypy", specifier = ">=1.13" }, - { name = "pytest", specifier = ">=8.3" }, - { name = "pytest-asyncio", specifier = ">=0.24" }, - { name = "pytest-mock", specifier = ">=3.14" }, - { name = "responses", specifier = ">=0.25" }, - { name = "ruff", specifier = ">=0.7" }, - { name = "types-requests", specifier = ">=2.32" }, -] - [[package]] name = "multidict" version = "6.7.1" @@ -1047,7 +1055,7 @@ wheels = [ [[package]] name = "mypy" -version = "2.0.0" +version = "2.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "ast-serialize" }, @@ -1056,30 +1064,30 @@ dependencies = [ { name = "pathspec" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/cf/dc/7e6d49f04fca40b9dd5c752a51a432ffe67fb45200702bc9eee0cb4bbb26/mypy-2.0.0.tar.gz", hash = "sha256:1a9e3900ac5c40f1fe813506c7739da6e6f0eab2729067ebd94bfb0bbba53532", size = 3869036, upload-time = "2026-05-06T19:26:43.22Z" } +sdist = { url = "https://files.pythonhosted.org/packages/82/15/cca9d88503549ed6fedeaa1d448cdddd542ee8a490232d732e278036fbf2/mypy-2.1.0.tar.gz", hash = "sha256:81e76ad12c2d804512e9b13240d1588316531bfba07558286078bfbce9613633", size = 3898359, upload-time = "2026-05-11T18:37:36.237Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e5/c6/996a1e535e5d0d597c3b1460fc962733091f885f312e749350eb2ac10965/mypy-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9ef5f581b61240d1cc629b12f8df6565ed6ffac0d82ed745eef7833222ab50b9", size = 14737259, upload-time = "2026-05-06T19:20:23.081Z" }, - { url = "https://files.pythonhosted.org/packages/94/c5/0f9460e26b77f434bd53f47d1ce32a3cd4580c92a5331fa5dfc059f9421a/mypy-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:20e3470a165dbc249bdfbe8d1c5172727ef22688cffc279f8c3aa264ab9d4d9a", size = 13538377, upload-time = "2026-05-06T19:21:08.804Z" }, - { url = "https://files.pythonhosted.org/packages/b2/3e/8ea2f8dd1e5c9c279fb3c28193bdb850adf4d3d8172880abad829eced609/mypy-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:224ba142eee8b4d65d4db657cb1fc22abec30b135ded6ab297302ba1f62e505d", size = 13914264, upload-time = "2026-05-06T19:24:12.875Z" }, - { url = "https://files.pythonhosted.org/packages/be/ce/78bd3b8520f676acee9dab48ea71473e68f6d5cf14b59fbd800bea50a92b/mypy-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2e879ad8a03908ff74d15e8a9b42bf049918e6798d52c011011f1873d0b5877e", size = 14926761, upload-time = "2026-05-06T19:20:12.846Z" }, - { url = "https://files.pythonhosted.org/packages/61/ef/b52fa340522da3d22e669117c3b83155c2660f7cdc035856958fbfffb224/mypy-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:65c5c15bcbd18d6fe927cc55c459597a3517d69cc3123f067be3b020010e115e", size = 15157014, upload-time = "2026-05-06T19:25:49.78Z" }, - { url = "https://files.pythonhosted.org/packages/7a/0c/dde7614250c6d017936c7aa3bb63b9b52c7cfd298d3f1be9be45f307870b/mypy-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:d1a068acd7c9fb77e9f8923f1556f2f49d6d7895821121b8d97fa5642b9c52f5", size = 11067049, upload-time = "2026-05-06T19:21:16.116Z" }, - { url = "https://files.pythonhosted.org/packages/27/ec/1d6af4830a94a285442db19caa02f160cc1a255e4f324eec5458e6c2bafb/mypy-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:ef9d96da1ddffbc21f27d3939319b6846d12393baa17c4d2f3e81e040e73ce2c", size = 9967903, upload-time = "2026-05-06T19:22:15.52Z" }, - { url = "https://files.pythonhosted.org/packages/ce/2c/6fefe954207860aed6eeb91776795e64a257d3ce0360862288984ce121f5/mypy-2.0.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:c918c64e8ce36557851b0347f84eb12f1965d3a06813c36df253eb0c0afd1d82", size = 14729633, upload-time = "2026-05-06T19:24:53.383Z" }, - { url = "https://files.pythonhosted.org/packages/23/d6/d336f5b820af189eb0390cce21de62d264c0a4e64713dfbe81bfc4fc7739/mypy-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:301f1a8ccc7d79b542ee218b28bb49443a83e194eb3d10da63ff1649e5aa5d34", size = 13559524, upload-time = "2026-05-06T19:22:24.906Z" }, - { url = "https://files.pythonhosted.org/packages/af/a6/d7bb54fde1770f0484e5fbdbdce37a41e95ed0a1cd493ec60ead111e356c/mypy-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fdf4ef489d44ce350bac3fd699907834e551d4c934e9cc862ef201215ab1558d", size = 13936018, upload-time = "2026-05-06T19:25:02.992Z" }, - { url = "https://files.pythonhosted.org/packages/7d/ba/5be51316b91e6a6bf6e3a8adb3de500e7e1fb5bf9491743b8cbc81a34a2c/mypy-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9cde2d0989f912fc850890f727d0d76495e7a6c5bdd9912a1efdb64952b4398d", size = 14910712, upload-time = "2026-05-06T19:25:21.83Z" }, - { url = "https://files.pythonhosted.org/packages/b7/37/e2c8c3b373e20ebfb66e6c83a99027fd67df4ec43b08879f74e822d2dc4c/mypy-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:cdf05693c231a14fe37dbfce192a3a1372c26a833af4a80f550547742952e719", size = 15141499, upload-time = "2026-05-06T19:20:50.924Z" }, - { url = "https://files.pythonhosted.org/packages/12/36/07756f933e00416d912e35878cfcf89a593a3350a885691c0bb85ae0226a/mypy-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:73aee2da33a2237e66cbe84a94780e53599847e86bb3aa7b93e405e8cd9905f2", size = 11240511, upload-time = "2026-05-06T19:21:32.39Z" }, - { url = "https://files.pythonhosted.org/packages/70/05/79ac1f20f2397353f3845f7b8bb5d8006cda7c8ef9092f04f9de3c6135f2/mypy-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:1f6dcd8f39971f41edab2728c877c4ac8b50ad3c387ff2770423b79a05d23910", size = 10149336, upload-time = "2026-05-06T19:22:08.383Z" }, - { url = "https://files.pythonhosted.org/packages/53/e0/0db84e0ebbad6e99e566c68e4b465784f2a2294f7719e8db9d509ef23087/mypy-2.0.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:a04e980b9275c76159da66c6e1723c7798306f9802b31bdaf9358d0c84030ce8", size = 15797362, upload-time = "2026-05-06T19:22:00.835Z" }, - { url = "https://files.pythonhosted.org/packages/0a/a4/14cc0768164dd53bec48aa41a20270b18df9bf72aa5054278bf133608315/mypy-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:33f9cf4825469b2bc73c53ba55f6d9a9b4cdb60f9e6e228745581520f29b8771", size = 14635914, upload-time = "2026-05-06T19:23:43.675Z" }, - { url = "https://files.pythonhosted.org/packages/08/48/d866a3e23b4dc5974c77d9cf65a435bf22de01a84dd4620917950e233960/mypy-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:191675c3c7dc2a5c7722a035a6909c277f14046c5e4e02aa5fbf65f8524f08ad", size = 15270866, upload-time = "2026-05-06T19:22:34.756Z" }, - { url = "https://files.pythonhosted.org/packages/71/eb/de9ef94958eb2078a6b908ceb247757dc384d3a238d3bd6ed7d81de5eaf8/mypy-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c3d26c4321a3b06fc9f04c741e0733af693f82d823f8e64e47b2e63b7f19fa84", size = 16093131, upload-time = "2026-05-06T19:23:56.541Z" }, - { url = "https://files.pythonhosted.org/packages/ad/07/0ab2c1a9d26e90942612724cbd5788f16b7810c5dd39bfcf79286c6c4524/mypy-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:bbcbc4d5917ca6ce12de70e051de7f533e3bf92d548b41a38a2232a6fe356525", size = 16330685, upload-time = "2026-05-06T19:21:42.037Z" }, - { url = "https://files.pythonhosted.org/packages/a6/8f/46f85d1371a5be642dad263828118ae1efd536d91d8bd2000c68acff3920/mypy-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:dbc6ba6d40572ae49268531565793a8f07eac7fc65ad76d482c9b4c8765b6043", size = 12752017, upload-time = "2026-05-06T19:22:44.002Z" }, - { url = "https://files.pythonhosted.org/packages/7a/e6/94ca48800cac19eb28a58188a768aaec0d16cac0f373915f073058ab0855/mypy-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:77926029dfcb7e1a3ecb0acb2ddbb24ca36be03f7d623e1759ad5376be8f6c01", size = 10527097, upload-time = "2026-05-06T19:20:58.973Z" }, - { url = "https://files.pythonhosted.org/packages/5c/14/fd0694aa594d6e9f9fd16ce821be2eff295197a273262ef56ddcc1388d68/mypy-2.0.0-py3-none-any.whl", hash = "sha256:8a92b2be3146b4fa1f062af7eb05574cbf3e6eb8e1f14704af1075423144e4e5", size = 2673434, upload-time = "2026-05-06T19:26:32.856Z" }, + { url = "https://files.pythonhosted.org/packages/6e/dd/c7191469c777f07689c032a8f7326e393ea34c92d6d76eb7ce5ba57ea66d/mypy-2.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:35aac3bb114e03888f535d5eb51b8bafbb3266586b599da1940f9b1be3ec5bd5", size = 14852174, upload-time = "2026-05-11T18:31:38.929Z" }, + { url = "https://files.pythonhosted.org/packages/55/8c/aed55408879043d72bb9135f4d0d19a02b886dd569631e113e3d2706cb8d/mypy-2.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8de55a8c861f2a49331f807be98d90caeceeef520bde13d43a160207f8af613e", size = 13651542, upload-time = "2026-05-11T18:36:04.636Z" }, + { url = "https://files.pythonhosted.org/packages/3a/8e/f371a824b1f1fa8ea6e3dbb8703d232977d572be2329554a3bc4d960302f/mypy-2.1.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5fdf2941a07434af755837d9880f7d7d25f1dacb1af9dcd4b9b66f2220a3024e", size = 14033929, upload-time = "2026-05-11T18:35:55.742Z" }, + { url = "https://files.pythonhosted.org/packages/94/21/f54be870d6dd53a82c674407e0f8eed7174b05ec78d42e5abd7b42e84fd5/mypy-2.1.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e195b817c13f02352a9c124301f9f30f078405444679b6753c1b96b6eed37285", size = 15039200, upload-time = "2026-05-11T18:33:10.281Z" }, + { url = "https://files.pythonhosted.org/packages/17/99/bf21748626a40ce59fd29a39386ab46afec88b7bd2f0fa6c3a97c995523f/mypy-2.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5431d42af987ebd92ba2f71d45c85ed41d8e6ca9f5fd209a69f68f707d2469e5", size = 15272690, upload-time = "2026-05-11T18:32:07.205Z" }, + { url = "https://files.pythonhosted.org/packages/d6/d7/9e90d2cf47100bea550ed2bc7b0d4de3a62181d84d5e37da0003e8462637/mypy-2.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:767fe8c66dc3e01e19e1737d4c38ebefead16125e1b8e58ad421903b376f5c65", size = 11147435, upload-time = "2026-05-11T18:33:56.477Z" }, + { url = "https://files.pythonhosted.org/packages/ec/46/e5c449e858798e35ffc90946282a27c62a77be743fe17480e4977374eb91/mypy-2.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:ecfe70d43775ab99562ab128ce49854a362044c9f894961f68f898c23cb7429d", size = 10035052, upload-time = "2026-05-11T18:32:30.049Z" }, + { url = "https://files.pythonhosted.org/packages/b0/ca/b279a672e874aedd5498ae25f722dacc8aa86bbffb939b3f97cbb1cf6686/mypy-2.1.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:7354c5a7f69d9345c3d6e69921d57088eea3ddeeb6b20d34c1b3855b02c36ec2", size = 14848422, upload-time = "2026-05-11T18:35:45.984Z" }, + { url = "https://files.pythonhosted.org/packages/27/e6/3efe56c631d959b9b4454e208b0ac4b7f4f58b404c89f8bec7b49efdfc21/mypy-2.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:49890d4f76ac9e06ec117f9e09f3174da70a620a0c300953d8595c926e80947f", size = 13677374, upload-time = "2026-05-11T18:36:57.188Z" }, + { url = "https://files.pythonhosted.org/packages/84/7f/8107ea87a44fd1f1b59882442f033c9c3488c127201b1d1d15f1cbd6022e/mypy-2.1.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:761be68e023ef5d94678772396a8af1220030f80837a3afd8d0aef3b419666f4", size = 14055743, upload-time = "2026-05-11T18:35:18.361Z" }, + { url = "https://files.pythonhosted.org/packages/51/4d/b6d34db183133b83761b9199a82d31557cdbb70a380d8c3b3438e11882a3/mypy-2.1.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c90345fc182dc363b891350457ec69c35140858538f38b4540845afcc32b1aef", size = 15020937, upload-time = "2026-05-11T18:34:59.618Z" }, + { url = "https://files.pythonhosted.org/packages/ff/d7/f08360c691d758acb02f45022c34d98b92892f4ea756644e1000d4b9f3d8/mypy-2.1.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b84802e7b5a6daf1f5e15bc9fcd7ddae77be13981ffab037f1c67bb84d67d135", size = 15253371, upload-time = "2026-05-11T18:36:41.081Z" }, + { url = "https://files.pythonhosted.org/packages/67/1b/09460a13719530a19bce27bd3bc8449e83569dd2ba7faf51c9c3c30c0b61/mypy-2.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:022c771234936ceac541ebaf836fe9e2abeb3f5e09aff21588fe543ff006fe21", size = 11326429, upload-time = "2026-05-11T18:34:13.526Z" }, + { url = "https://files.pythonhosted.org/packages/40/62/75dbf0f82f7b6680340efc614af29dd0b3c17b8a4f1cd09b8bd2fd6bc814/mypy-2.1.0-cp314-cp314-win_arm64.whl", hash = "sha256:498207db725cec88829a6a5c2fc771205fd043719ef98bc49aba8fb9fc4e6d57", size = 10218799, upload-time = "2026-05-11T18:32:23.491Z" }, + { url = "https://files.pythonhosted.org/packages/b2/66/caca04ed7d972fb6eb6dd1ccd6df1de5c38fae8c5b3dc1c4e8e0d85ee6b9/mypy-2.1.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7d5e5cad0efeba72b93cd17490cc0d69c5ac9ca132994fe3fb0314808aeeb83e", size = 15923458, upload-time = "2026-05-11T18:35:28.64Z" }, + { url = "https://files.pythonhosted.org/packages/ed/52/2d90cbe49d014b13ed7ff337930c30bad35893fe38a1e4641e756bb62191/mypy-2.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ff715050c127d724fd260a2e666e7747fdd83511c0c47d449d98238970aef780", size = 14757697, upload-time = "2026-05-11T18:36:14.208Z" }, + { url = "https://files.pythonhosted.org/packages/ac/37/d98f4a14e081b238992d0ed96b6d39c7cc0148c9699eb71eaa68629665ea/mypy-2.1.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:82208da9e09414d520e912d3e462d454854bed0810b71540bb016dcbca7308fd", size = 15405638, upload-time = "2026-05-11T18:33:48.249Z" }, + { url = "https://files.pythonhosted.org/packages/a3/c2/15c46613b24a84fad2aea1248bf9619b99c2767ae9071fe224c179a0b7d4/mypy-2.1.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e79ebc1b904b84f0310dff7469655a9c36c7a68bddb37bdd42b67a332df61d08", size = 16215852, upload-time = "2026-05-11T18:32:50.296Z" }, + { url = "https://files.pythonhosted.org/packages/5c/90/9c16a57f482c76d25f6379762b56bbf65c711d8158cf271fb2802cfb0640/mypy-2.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e583edc957cfb0deb142079162ae826f58449b116c1d442f2d91c69d9fced081", size = 16452695, upload-time = "2026-05-11T18:33:38.182Z" }, + { url = "https://files.pythonhosted.org/packages/0f/4c/215a4eeb63cacc5f17f516691ea7285d11e249802b942476bff15922a314/mypy-2.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b33b6cd332695bba180d55e717a79d3038e479a2c49cc5eb3d53603409b9a5d7", size = 12866622, upload-time = "2026-05-11T18:34:39.945Z" }, + { url = "https://files.pythonhosted.org/packages/4b/50/1043e1db5f455ffe4c9ab22747cd8ca2bc492b1e4f4e21b130a44ee2b217/mypy-2.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:4f910fe825376a7b66ef7ca8c98e5a149e8cd64c19ae71d84047a74ee060d4e6", size = 10610798, upload-time = "2026-05-11T18:36:31.444Z" }, + { url = "https://files.pythonhosted.org/packages/0d/2a/13ca1f292f6db1b98ff495ef3467736b331621c5917cad984b7043e7348d/mypy-2.1.0-py3-none-any.whl", hash = "sha256:a663814603a5c563fb87a4f96fb473eeb30d1f5a4885afcf44f9db000a366289", size = 2693302, upload-time = "2026-05-11T18:31:29.246Z" }, ] [[package]] @@ -1102,7 +1110,7 @@ wheels = [ [[package]] name = "nicegui" -version = "3.11.1" +version = "3.12.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiofiles" }, @@ -1131,9 +1139,9 @@ dependencies = [ { name = "uvicorn", extra = ["standard"] }, { name = "watchfiles" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ec/5f/c03d3d9253c3e67b560749ad344dd74de61bba3c35788a029ad9ad0f7a4f/nicegui-3.11.1.tar.gz", hash = "sha256:d6d176c458a012fff926faf196f6cb3eb86c22203eea4aa335a4d0162291e92c", size = 19231328, upload-time = "2026-04-25T20:00:57.052Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d3/10/48a16e8c2a2d4c1b1efdcb9c3898a50db3d016fab86a972e3fe083bce206/nicegui-3.12.0.tar.gz", hash = "sha256:f90d091a2b4855e090a9bad8d8ad10396799f7c7792a0846447a11aea396ead3", size = 19602588, upload-time = "2026-05-12T14:22:02.317Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/33/44/350aed50d86abbd192cfc4e4b7bd3d7ba7c50a7cde7dc9e9a7c57f8799a8/nicegui-3.11.1-py3-none-any.whl", hash = "sha256:7e0e63e86dc35ece491aa5dd00a0ff8c28b443b0d58e48b0f425690e1578cc54", size = 19801682, upload-time = "2026-04-25T20:01:01.802Z" }, + { url = "https://files.pythonhosted.org/packages/c7/3d/222879aa74d30cd7fe057df5638623b9818de535064e0c8f02117e0b69ce/nicegui-3.12.0-py3-none-any.whl", hash = "sha256:1bdc6ef423f091afddc05ea3bcf5beac1b3c3d27081b0df243aaccf03e612434", size = 20186344, upload-time = "2026-05-12T14:22:06.794Z" }, ] [[package]] @@ -1254,46 +1262,46 @@ wheels = [ [[package]] name = "pandas" -version = "3.0.2" +version = "3.0.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy" }, { name = "python-dateutil" }, { name = "tzdata", marker = "sys_platform == 'emscripten' or sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/da/99/b342345300f13440fe9fe385c3c481e2d9a595ee3bab4d3219247ac94e9a/pandas-3.0.2.tar.gz", hash = "sha256:f4753e73e34c8d83221ba58f232433fca2748be8b18dbca02d242ed153945043", size = 4645855, upload-time = "2026-03-31T06:48:30.816Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f8/87/4341c6252d1c47b08768c3d25ac487362bf403f0313ddae4a2a26c9b1b4c/pandas-3.0.3.tar.gz", hash = "sha256:696a4a00a2a2a35d4e5deb3fc946641b96c944f02230e4f76137fe35d806c4fc", size = 4651414, upload-time = "2026-05-11T18:54:29.21Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/bf/ca/3e639a1ea6fcd0617ca4e8ca45f62a74de33a56ae6cd552735470b22c8d3/pandas-3.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b5918ba197c951dec132b0c5929a00c0bf05d5942f590d3c10a807f6e15a57d3", size = 10321105, upload-time = "2026-03-31T06:46:57.327Z" }, - { url = "https://files.pythonhosted.org/packages/0b/77/dbc82ff2fb0e63c6564356682bf201edff0ba16c98630d21a1fb312a8182/pandas-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d606a041c89c0a474a4702d532ab7e73a14fe35c8d427b972a625c8e46373668", size = 9864088, upload-time = "2026-03-31T06:46:59.935Z" }, - { url = "https://files.pythonhosted.org/packages/5c/2b/341f1b04bbca2e17e13cd3f08c215b70ef2c60c5356ef1e8c6857449edc7/pandas-3.0.2-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:710246ba0616e86891b58ab95f2495143bb2bc83ab6b06747c74216f583a6ac9", size = 10369066, upload-time = "2026-03-31T06:47:02.792Z" }, - { url = "https://files.pythonhosted.org/packages/12/c5/cbb1ffefb20a93d3f0e1fdcda699fb84976210d411b008f97f48bf6ce27e/pandas-3.0.2-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5d3cfe227c725b1f3dff4278b43d8c784656a42a9325b63af6b1492a8232209e", size = 10876780, upload-time = "2026-03-31T06:47:06.205Z" }, - { url = "https://files.pythonhosted.org/packages/98/fe/2249ae5e0a69bd0ddf17353d0a5d26611d70970111f5b3600cdc8be883e7/pandas-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c3b723df9087a9a9a840e263ebd9f88b64a12075d1bf2ea401a5a42f254f084d", size = 11375181, upload-time = "2026-03-31T06:47:09.383Z" }, - { url = "https://files.pythonhosted.org/packages/de/64/77a38b09e70b6464883b8d7584ab543e748e42c1b5d337a2ee088e0df741/pandas-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a3096110bf9eac0070b7208465f2740e2d8a670d5cb6530b5bb884eca495fd39", size = 11928899, upload-time = "2026-03-31T06:47:12.686Z" }, - { url = "https://files.pythonhosted.org/packages/5e/52/42855bf626868413f761addd574acc6195880ae247a5346477a4361c3acb/pandas-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:07a10f5c36512eead51bc578eb3354ad17578b22c013d89a796ab5eee90cd991", size = 9746574, upload-time = "2026-03-31T06:47:15.64Z" }, - { url = "https://files.pythonhosted.org/packages/88/39/21304ae06a25e8bf9fc820d69b29b2c495b2ae580d1e143146c309941760/pandas-3.0.2-cp313-cp313-win_arm64.whl", hash = "sha256:5fdbfa05931071aba28b408e59226186b01eb5e92bea2ab78b65863ca3228d84", size = 9047156, upload-time = "2026-03-31T06:47:18.595Z" }, - { url = "https://files.pythonhosted.org/packages/72/20/7defa8b27d4f330a903bb68eea33be07d839c5ea6bdda54174efcec0e1d2/pandas-3.0.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:dbc20dea3b9e27d0e66d74c42b2d0c1bed9c2ffe92adea33633e3bedeb5ac235", size = 10756238, upload-time = "2026-03-31T06:47:22.012Z" }, - { url = "https://files.pythonhosted.org/packages/e9/95/49433c14862c636afc0e9b2db83ff16b3ad92959364e52b2955e44c8e94c/pandas-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b75c347eff42497452116ce05ef461822d97ce5b9ff8df6edacb8076092c855d", size = 10408520, upload-time = "2026-03-31T06:47:25.197Z" }, - { url = "https://files.pythonhosted.org/packages/3b/f8/462ad2b5881d6b8ec8e5f7ed2ea1893faa02290d13870a1600fe72ad8efc/pandas-3.0.2-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d1478075142e83a5571782ad007fb201ed074bdeac7ebcc8890c71442e96adf7", size = 10324154, upload-time = "2026-03-31T06:47:28.097Z" }, - { url = "https://files.pythonhosted.org/packages/0a/65/d1e69b649cbcddda23ad6e4c40ef935340f6f652a006e5cbc3555ac8adb3/pandas-3.0.2-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5880314e69e763d4c8b27937090de570f1fb8d027059a7ada3f7f8e98bdcb677", size = 10714449, upload-time = "2026-03-31T06:47:30.85Z" }, - { url = "https://files.pythonhosted.org/packages/47/a4/85b59bc65b8190ea3689882db6cdf32a5003c0ccd5a586c30fdcc3ffc4fc/pandas-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b5329e26898896f06035241a626d7c335daa479b9bbc82be7c2742d048e41172", size = 11338475, upload-time = "2026-03-31T06:47:34.026Z" }, - { url = "https://files.pythonhosted.org/packages/1e/c4/bc6966c6e38e5d9478b935272d124d80a589511ed1612a5d21d36f664c68/pandas-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:81526c4afd31971f8b62671442a4b2b51e0aa9acc3819c9f0f12a28b6fcf85f1", size = 11786568, upload-time = "2026-03-31T06:47:36.941Z" }, - { url = "https://files.pythonhosted.org/packages/e8/74/09298ca9740beed1d3504e073d67e128aa07e5ca5ca2824b0c674c0b8676/pandas-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:7cadd7e9a44ec13b621aec60f9150e744cfc7a3dd32924a7e2f45edff31823b0", size = 10488652, upload-time = "2026-03-31T06:47:40.612Z" }, - { url = "https://files.pythonhosted.org/packages/bb/40/c6ea527147c73b24fc15c891c3fcffe9c019793119c5742b8784a062c7db/pandas-3.0.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:db0dbfd2a6cdf3770aa60464d50333d8f3d9165b2f2671bcc299b72de5a6677b", size = 10326084, upload-time = "2026-03-31T06:47:43.834Z" }, - { url = "https://files.pythonhosted.org/packages/95/25/bdb9326c3b5455f8d4d3549fce7abcf967259de146fe2cf7a82368141948/pandas-3.0.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0555c5882688a39317179ab4a0ed41d3ebc8812ab14c69364bbee8fb7a3f6288", size = 9914146, upload-time = "2026-03-31T06:47:46.67Z" }, - { url = "https://files.pythonhosted.org/packages/8d/77/3a227ff3337aa376c60d288e1d61c5d097131d0ac71f954d90a8f369e422/pandas-3.0.2-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:01f31a546acd5574ef77fe199bc90b55527c225c20ccda6601cf6b0fd5ed597c", size = 10444081, upload-time = "2026-03-31T06:47:49.681Z" }, - { url = "https://files.pythonhosted.org/packages/15/88/3cdd54fa279341afa10acf8d2b503556b1375245dccc9315659f795dd2e9/pandas-3.0.2-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:deeca1b5a931fdf0c2212c8a659ade6d3b1edc21f0914ce71ef24456ca7a6535", size = 10897535, upload-time = "2026-03-31T06:47:53.033Z" }, - { url = "https://files.pythonhosted.org/packages/06/9d/98cc7a7624f7932e40f434299260e2917b090a579d75937cb8a57b9d2de3/pandas-3.0.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0f48afd9bb13300ffb5a3316973324c787054ba6665cda0da3fbd67f451995db", size = 11446992, upload-time = "2026-03-31T06:47:56.193Z" }, - { url = "https://files.pythonhosted.org/packages/9a/cd/19ff605cc3760e80602e6826ddef2824d8e7050ed80f2e11c4b079741dc3/pandas-3.0.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6c4d8458b97a35717b62469a4ea0e85abd5ed8687277f5ccfc67f8a5126f8c53", size = 11968257, upload-time = "2026-03-31T06:47:59.137Z" }, - { url = "https://files.pythonhosted.org/packages/db/60/aba6a38de456e7341285102bede27514795c1eaa353bc0e7638b6b785356/pandas-3.0.2-cp314-cp314-win_amd64.whl", hash = "sha256:b35d14bb5d8285d9494fe93815a9e9307c0876e10f1e8e89ac5b88f728ec8dcf", size = 9865893, upload-time = "2026-03-31T06:48:02.038Z" }, - { url = "https://files.pythonhosted.org/packages/08/71/e5ec979dd2e8a093dacb8864598c0ff59a0cee0bbcdc0bfec16a51684d4f/pandas-3.0.2-cp314-cp314-win_arm64.whl", hash = "sha256:63d141b56ef686f7f0d714cfb8de4e320475b86bf4b620aa0b7da89af8cbdbbb", size = 9188644, upload-time = "2026-03-31T06:48:05.045Z" }, - { url = "https://files.pythonhosted.org/packages/f1/6c/7b45d85db19cae1eb524f2418ceaa9d85965dcf7b764ed151386b7c540f0/pandas-3.0.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:140f0cffb1fa2524e874dde5b477d9defe10780d8e9e220d259b2c0874c89d9d", size = 10776246, upload-time = "2026-03-31T06:48:07.789Z" }, - { url = "https://files.pythonhosted.org/packages/a8/3e/7b00648b086c106e81766f25322b48aa8dfa95b55e621dbdf2fdd413a117/pandas-3.0.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ae37e833ff4fed0ba352f6bdd8b73ba3ab3256a85e54edfd1ab51ae40cca0af8", size = 10424801, upload-time = "2026-03-31T06:48:10.897Z" }, - { url = "https://files.pythonhosted.org/packages/da/6e/558dd09a71b53b4008e7fc8a98ec6d447e9bfb63cdaeea10e5eb9b2dabe8/pandas-3.0.2-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4d888a5c678a419a5bb41a2a93818e8ed9fd3172246555c0b37b7cc27027effd", size = 10345643, upload-time = "2026-03-31T06:48:13.7Z" }, - { url = "https://files.pythonhosted.org/packages/be/e3/921c93b4d9a280409451dc8d07b062b503bbec0531d2627e73a756e99a82/pandas-3.0.2-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b444dc64c079e84df91baa8bf613d58405645461cabca929d9178f2cd392398d", size = 10743641, upload-time = "2026-03-31T06:48:16.659Z" }, - { url = "https://files.pythonhosted.org/packages/56/ca/fd17286f24fa3b4d067965d8d5d7e14fe557dd4f979a0b068ac0deaf8228/pandas-3.0.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:4544c7a54920de8eeacaa1466a6b7268ecfbc9bc64ab4dbb89c6bbe94d5e0660", size = 11361993, upload-time = "2026-03-31T06:48:19.475Z" }, - { url = "https://files.pythonhosted.org/packages/e4/a5/2f6ed612056819de445a433ca1f2821ac3dab7f150d569a59e9cc105de1d/pandas-3.0.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:734be7551687c00fbd760dc0522ed974f82ad230d4a10f54bf51b80d44a08702", size = 11815274, upload-time = "2026-03-31T06:48:22.695Z" }, - { url = "https://files.pythonhosted.org/packages/00/2f/b622683e99ec3ce00b0854bac9e80868592c5b051733f2cf3a868e5fea26/pandas-3.0.2-cp314-cp314t-win_amd64.whl", hash = "sha256:57a07209bebcbcf768d2d13c9b78b852f9a15978dac41b9e6421a81ad4cdd276", size = 10888530, upload-time = "2026-03-31T06:48:25.806Z" }, - { url = "https://files.pythonhosted.org/packages/cb/2b/f8434233fab2bd66a02ec014febe4e5adced20e2693e0e90a07d118ed30e/pandas-3.0.2-cp314-cp314t-win_arm64.whl", hash = "sha256:5371b72c2d4d415d08765f32d689217a43227484e81b2305b52076e328f6f482", size = 9455341, upload-time = "2026-03-31T06:48:28.418Z" }, + { url = "https://files.pythonhosted.org/packages/c5/90/62d8302883c44308c477e222c3daf7c813a34c8e96985882fbd53d964352/pandas-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:67b3b64c11910cfa29f4e94a14d3bff9ee693b6fc76055e7cad549cee0aec5fa", size = 10331071, upload-time = "2026-05-11T18:52:58.838Z" }, + { url = "https://files.pythonhosted.org/packages/7f/ae/6a6493c783a101f165e4356953ba3c74d6f77f0042fa7d753da9dfbb640c/pandas-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:39436b377d56d2a2e52d0395bdbee171f01068e99af5250509aceeb929f765c7", size = 9875690, upload-time = "2026-05-11T18:53:01.431Z" }, + { url = "https://files.pythonhosted.org/packages/62/7c/5df8e9f56c69a2769fbe9382a5ef8f2658c007e376434e1e2cbb57ad895f/pandas-3.0.3-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d4be06d68f9ddcfc645b87534911da79a8fbffc7573c80e0edcf42a5020624d8", size = 10381634, upload-time = "2026-05-11T18:53:04.393Z" }, + { url = "https://files.pythonhosted.org/packages/99/68/1237369725aa617bb358263d535803e3053fdbc593513ec5ed9c9896b5b6/pandas-3.0.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a4eeb6830daf35a71cc09649bd823e2b542dac246cdee9614c6e4bd65028cd6a", size = 10891243, upload-time = "2026-05-11T18:53:07.643Z" }, + { url = "https://files.pythonhosted.org/packages/25/93/77d108e8af7222b4a503ebde0e30215b1c2e4f8e53a526431890f22d5586/pandas-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1928e07221f82db493cd4af1e23c1bfca524a19a4699887975bff68f49a72bfb", size = 11388659, upload-time = "2026-05-11T18:53:10.634Z" }, + { url = "https://files.pythonhosted.org/packages/d0/bd/eff5b4399f332ac386c853f6cd2bd3fa2ca0061b9f36ecd9c4d7c4265649/pandas-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51b1fe551acb77dac643c6fda86084d8d446c10fe64b06a9cc29c4cc8540e7f2", size = 11942880, upload-time = "2026-05-11T18:53:13.536Z" }, + { url = "https://files.pythonhosted.org/packages/2c/20/559ace4200982c3887d0b86bfd0d856a2143ef8ddab63cc07934951a964c/pandas-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:a82d532a3351d435432cd913edbccaf8b8e01d4dd0e5ced5a8d2e8ecd94c7e44", size = 9757091, upload-time = "2026-05-11T18:53:16.306Z" }, + { url = "https://files.pythonhosted.org/packages/3a/66/69055a09fe200f29f922a3eeec4804611900b95f52d932ece3393c3c0c19/pandas-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:275c14e0fce14a2ec20eee474aecd305478ea3c1e6f6a9d8fe219a165542717e", size = 9057282, upload-time = "2026-05-11T18:53:18.768Z" }, + { url = "https://files.pythonhosted.org/packages/57/0e/efe801b0e6811e8e650cd21b7f2608e30f08a7067e2bf6e8752b0d56ee3c/pandas-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:46997386d528eb40376ecd6b033cf4a8a1e5282580f68f43de875b78cba2199d", size = 10767016, upload-time = "2026-05-11T18:53:21.227Z" }, + { url = "https://files.pythonhosted.org/packages/ea/dc/eb55135a1d5f0f0519f28da1f609a206d2cad1f9c35c32d51e38dd7261ae/pandas-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:261e308dfb22448384b7580cf719d2f998fe2966c92893c3e77d14008af1f066", size = 10420210, upload-time = "2026-05-11T18:53:23.982Z" }, + { url = "https://files.pythonhosted.org/packages/c6/3e/b1d5d955ce33ffecb407465a60bc32769d74fcf68224b7ae67ae11d4dea4/pandas-3.0.3-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dd1a5d1def6a46002e964510bdc67c368aa0951df5d1d9f8365336f5a1f490cd", size = 10336126, upload-time = "2026-05-11T18:53:26.731Z" }, + { url = "https://files.pythonhosted.org/packages/f5/76/a01261711ab60a22d71b862f0de20e4c504bf80457270ad8cb42110f6abc/pandas-3.0.3-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d72828c20c6d6e83e1e22a6a3b47b326b71664112fa9705dcbccfd7a39b62085", size = 10728051, upload-time = "2026-05-11T18:53:29.125Z" }, + { url = "https://files.pythonhosted.org/packages/e9/21/ea191195e587b18cf682e97f433f81b2d0fbe341380e80a3e0d6e4403c8e/pandas-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d26cbe1fcfc12e8fd900e2454163e466b2d3af84f7c75481df7683ffc073d870", size = 11350796, upload-time = "2026-05-11T18:53:32.056Z" }, + { url = "https://files.pythonhosted.org/packages/64/69/f0eaaf54939f0e8c6768fd06be9af2cef9b36048b96dfb9e1b2c685a807e/pandas-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:3e91cec1879ada0624fc3dc9953c5cbd60208e59c0db28f540c5d6d47502422f", size = 11799741, upload-time = "2026-05-11T18:53:34.985Z" }, + { url = "https://files.pythonhosted.org/packages/45/a4/865e0e510cae5fc2194de4db28be638952de942571ba9125934fd9c01d47/pandas-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:08d789b41f87e0905880e293cedf6197ce71fe67cc081358b1e148a491b9bd13", size = 10499958, upload-time = "2026-05-11T18:53:37.857Z" }, + { url = "https://files.pythonhosted.org/packages/86/54/effdcc3c0ff7a08037889200e148ebe94c16c4f653be078c7b3675955df1/pandas-3.0.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:3650109c0f22879df8bd6179ab9ee3d7f1d1d4e7e0094a3f0032d9f51e2e64ac", size = 10336065, upload-time = "2026-05-11T18:53:41.099Z" }, + { url = "https://files.pythonhosted.org/packages/68/10/bf2d6738d72748b961a3751ab89522d58c54efc36a8e1a12161216cd45cf/pandas-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:bab900348131a7db1f69a7309ef141fd5680f1487094193bcbbb61791573bf8f", size = 9926101, upload-time = "2026-05-11T18:53:43.515Z" }, + { url = "https://files.pythonhosted.org/packages/ae/e9/e35cf11c8a136e757b956f5f0efdcaa50aecde85ea055f1898dfc68262f3/pandas-3.0.3-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba7e08b9ac1d54569cd1e256e3668975ed624d6826f7b68df0342b012007bddb", size = 10457553, upload-time = "2026-05-11T18:53:46.394Z" }, + { url = "https://files.pythonhosted.org/packages/58/3b/1cdec6772bdbaf7b25dab360c59f03cadf05492dd724c6540af905389b07/pandas-3.0.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d71c63ae4ebdbf70209742096f1fc46a83a0613c99d4b23766cced9ff8cd62a", size = 10914065, upload-time = "2026-05-11T18:53:49.134Z" }, + { url = "https://files.pythonhosted.org/packages/c4/c2/1ef644445fcd72e3627bceec77e3560636f87ddce4ed841afe76b83b5bf9/pandas-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e3a2ec42c98ffa2565a67e08e218d06d72576d758d90facb7c00805194d8f360", size = 11459188, upload-time = "2026-05-11T18:53:52.527Z" }, + { url = "https://files.pythonhosted.org/packages/7e/49/4d8d4f42cbc9c4adc7a1870f269c02cbd6cd40d059622c06fb298addcbad/pandas-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:335f62418ed562cfc3c49e9e196375c28b729dcef8543abf4f9438e381bf3c76", size = 11982966, upload-time = "2026-05-11T18:53:55.043Z" }, + { url = "https://files.pythonhosted.org/packages/38/55/792619469bab9882d8bbd5865d45a72f6478762d04a9af4bf0d08c503e95/pandas-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:3c20a521bbb85902f79f7270c80a59e1b5452d96d170c034f207181870f97ac5", size = 9876755, upload-time = "2026-05-11T18:53:58.067Z" }, + { url = "https://files.pythonhosted.org/packages/2a/af/33c469653b0ba03b50c3a98192d4c07f0c75c66b263ceb097fce0ee97d31/pandas-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:a2d2dff8a04f3917b55ab3910c32990f8ddf7eceba114947838cefa976a68977", size = 9198658, upload-time = "2026-05-11T18:54:00.733Z" }, + { url = "https://files.pythonhosted.org/packages/a2/fa/b8c257bd76b8bd060c3a9151c1fca05e9b9c5e3af5d0f549c0356f6d143d/pandas-3.0.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:0d589105b3c14645af1738ff279b2995102d8f7a03b0a66dc8d95550eb513e04", size = 10787242, upload-time = "2026-05-11T18:54:03.564Z" }, + { url = "https://files.pythonhosted.org/packages/54/eb/f19206ffb0bf1919002969aa448b4702c6594845156a6f8050674855aac3/pandas-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:13fc1e853d9e04743d11ba75a985ccbc2a317fe07d8af61e445a6fd24dacd6a6", size = 10436369, upload-time = "2026-05-11T18:54:06.311Z" }, + { url = "https://files.pythonhosted.org/packages/fd/24/c7c39fb4fe22b71a0c2d78bf0c585c600092d85f94f086d2b3b2f6ca27e2/pandas-3.0.3-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:819959dab7bbd0049c15623fbac4e29a191b9528160a61fb1032242d8ced2d9c", size = 10358306, upload-time = "2026-05-11T18:54:09.085Z" }, + { url = "https://files.pythonhosted.org/packages/16/ec/dd2a9eb7fa1204df88c0864164e35b228ac581062ac612ba0a67fd812e4c/pandas-3.0.3-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:60ae316d3fd75d1858d450d0db0103ea2be3e7d4a95ec2f064f7e2ae63f7b028", size = 10758394, upload-time = "2026-05-11T18:54:11.956Z" }, + { url = "https://files.pythonhosted.org/packages/95/6e/00c61ea8e85b4f6d8d35e11852a1a4998fc7fafc91c6a602d1cc9c972d64/pandas-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bd3a518890b400d32f9023722dc9a9a5c969f00b415419a3c06c043f09bb5d7d", size = 11375717, upload-time = "2026-05-11T18:54:14.539Z" }, + { url = "https://files.pythonhosted.org/packages/31/89/8fc1c268969fac43688d65fd92e67df24bd128d53cb4d2eee534cd307399/pandas-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9c39be2d709d01fa972a0cabc522389fceca4f3969332ba25a7d6c5802cf976a", size = 11828897, upload-time = "2026-05-11T18:54:17.146Z" }, + { url = "https://files.pythonhosted.org/packages/56/3b/e7d20dea247a3e6dc0bd8a6953854afbedc03951def4e7371e05e7263e25/pandas-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4db8c527972a821cf5286b40ccc57642a39bc62e62022b42f99f8a67fca8c3a1", size = 10900855, upload-time = "2026-05-11T18:54:19.72Z" }, + { url = "https://files.pythonhosted.org/packages/0f/54/68a0978d1ef8502b8492099beaa6e7a0c1b32e3b5d4f677f5810cb08711c/pandas-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:b2c95f8bfc1ee412bf482605d7bfd30c12d1d26bd59fdd91efeef1d4718decb1", size = 9466464, upload-time = "2026-05-11T18:54:22.754Z" }, ] [[package]] @@ -1651,11 +1659,11 @@ wheels = [ [[package]] name = "python-multipart" -version = "0.0.27" +version = "0.0.28" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/69/9b/f23807317a113dc36e74e75eb265a02dd1a4d9082abc3c1064acd22997c4/python_multipart-0.0.27.tar.gz", hash = "sha256:9870a6a8c5a20a5bf4f07c017bd1489006ff8836cff097b6933355ee2b49b602", size = 44043, upload-time = "2026-04-27T10:51:26.649Z" } +sdist = { url = "https://files.pythonhosted.org/packages/82/54/a85eb421fbdd5007bc5af39d0f4ed9fa609e0fedbfdc2adcf0b34526870e/python_multipart-0.0.28.tar.gz", hash = "sha256:8550da197eac0f7ab748961fc9509b999fa2662ea25cef857f05249f6893c0f8", size = 45314, upload-time = "2026-05-10T11:05:16.596Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/99/78/4126abcbdbd3c559d43e0db7f7b9173fc6befe45d39a2856cc0b8ec2a5a6/python_multipart-0.0.27-py3-none-any.whl", hash = "sha256:6fccfad17a27334bd0193681b369f476eda3409f17381a2d65aa7df3f7275645", size = 29254, upload-time = "2026-04-27T10:51:24.997Z" }, + { url = "https://files.pythonhosted.org/packages/f3/a2/43bbc5860b5034e2af4ef99a0e04d726ff329c43e192ef3abaa8d7ecfce5/python_multipart-0.0.28-py3-none-any.whl", hash = "sha256:10faac07eb966c3f48dc415f9dee46c04cb10d58d30a35677db8027c825ed9b6", size = 29438, upload-time = "2026-05-10T11:05:15.052Z" }, ] [[package]] @@ -1723,7 +1731,7 @@ wheels = [ [[package]] name = "requests" -version = "2.33.1" +version = "2.34.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, @@ -1731,9 +1739,9 @@ dependencies = [ { name = "idna" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5f/a4/98b9c7c6428a668bf7e42ebb7c79d576a1c3c1e3ae2d47e674b468388871/requests-2.33.1.tar.gz", hash = "sha256:18817f8c57c6263968bc123d237e3b8b08ac046f5456bd1e307ee8f4250d3517", size = 134120, upload-time = "2026-03-30T16:09:15.531Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856, upload-time = "2026-05-14T19:25:27.735Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d7/8e/7540e8a2036f79a125c1d2ebadf69ed7901608859186c856fa0388ef4197/requests-2.33.1-py3-none-any.whl", hash = "sha256:4e6d1ef462f3626a1f0a0a9c42dd93c63bad33f9f1c1937509b8c5c8718ab56a", size = 64947, upload-time = "2026-03-30T16:09:13.83Z" }, + { url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" }, ] [[package]] @@ -1765,27 +1773,27 @@ wheels = [ [[package]] name = "ruff" -version = "0.15.12" +version = "0.15.13" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/99/43/3291f1cc9106f4c63bdce7a8d0df5047fe8422a75b091c16b5e9355e0b11/ruff-0.15.12.tar.gz", hash = "sha256:ecea26adb26b4232c0c2ca19ccbc0083a68344180bba2a600605538ce51a40a6", size = 4643852, upload-time = "2026-04-24T18:17:14.305Z" } +sdist = { url = "https://files.pythonhosted.org/packages/24/21/a7d5c126d5b557715ef81098f3db2fe20f622a039ff2e626af28d674ab80/ruff-0.15.13.tar.gz", hash = "sha256:f9d89f17f7ba7fb2ed42921f0df75da797a9a5d71bc39049e2c687cf2baf44b7", size = 4678180, upload-time = "2026-05-14T13:44:37.869Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c3/6e/e78ffb61d4686f3d96ba3df2c801161843746dcbcbb17a1e927d4829312b/ruff-0.15.12-py3-none-linux_armv6l.whl", hash = "sha256:f86f176e188e94d6bdbc09f09bfd9dc729059ad93d0e7390b5a73efe19f8861c", size = 10640713, upload-time = "2026-04-24T18:17:22.841Z" }, - { url = "https://files.pythonhosted.org/packages/ae/08/a317bc231fb9e7b93e4ef3089501e51922ff88d6936ce5cf870c4fe55419/ruff-0.15.12-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:e3bcd123364c3770b8e1b7baaf343cc99a35f197c5c6e8af79015c666c423a6c", size = 11069267, upload-time = "2026-04-24T18:17:30.105Z" }, - { url = "https://files.pythonhosted.org/packages/aa/a4/f828e9718d3dce1f5f11c39c4f65afd32783c8b2aebb2e3d259e492c47bd/ruff-0.15.12-py3-none-macosx_11_0_arm64.whl", hash = "sha256:fe87510d000220aa1ed530d4448a7c696a0cae1213e5ec30e5874287b66557b5", size = 10397182, upload-time = "2026-04-24T18:17:07.177Z" }, - { url = "https://files.pythonhosted.org/packages/71/e0/3310fc6d1b5e1fdea22bf3b1b807c7e187b581021b0d7d4514cccdb5fb71/ruff-0.15.12-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:84a1630093121375a3e2a95b4a6dc7b59e2b4ee76216e32d81aae550a832d002", size = 10758012, upload-time = "2026-04-24T18:16:55.759Z" }, - { url = "https://files.pythonhosted.org/packages/11/c1/a606911aee04c324ddaa883ae418f3569792fd3c4a10c50e0dd0a2311e1e/ruff-0.15.12-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fb129f40f114f089ebe0ca56c0d251cf2061b17651d464bb6478dc01e69f11f5", size = 10447479, upload-time = "2026-04-24T18:16:51.677Z" }, - { url = "https://files.pythonhosted.org/packages/9d/68/4201e8444f0894f21ab4aeeaee68aa4f10b51613514a20d80bd628d57e88/ruff-0.15.12-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b0c862b172d695db7598426b8af465e7e9ac00a3ea2a3630ee67eb82e366aaa6", size = 11234040, upload-time = "2026-04-24T18:17:16.529Z" }, - { url = "https://files.pythonhosted.org/packages/34/ff/8a6d6cf4ccc23fd67060874e832c18919d1557a0611ebef03fdb01fff11e/ruff-0.15.12-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2849ea9f3484c3aca43a82f484210370319e7170df4dfe4843395ddf6c57bc33", size = 12087377, upload-time = "2026-04-24T18:17:04.944Z" }, - { url = "https://files.pythonhosted.org/packages/85/f6/c669cf73f5152f623d34e69866a46d5e6185816b19fcd5b6dd8a2d299922/ruff-0.15.12-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9e77c7e51c07fe396826d5969a5b846d9cd4c402535835fb6e21ce8b28fef847", size = 11367784, upload-time = "2026-04-24T18:17:25.409Z" }, - { url = "https://files.pythonhosted.org/packages/e8/39/c61d193b8a1daaa8977f7dea9e8d8ba866e02ea7b65d32f6861693aa4c12/ruff-0.15.12-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:83b2f4f2f3b1026b5fb449b467d9264bf22067b600f7b6f41fc5958909f449d0", size = 11344088, upload-time = "2026-04-24T18:17:12.258Z" }, - { url = "https://files.pythonhosted.org/packages/c2/8d/49afab3645e31e12c590acb6d3b5b69d7aab5b81926dbaf7461f9441f37a/ruff-0.15.12-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:9ba3b8f1afd7e2e43d8943e55f249e13f9682fde09711644a6e7290eb4f3e339", size = 11271770, upload-time = "2026-04-24T18:17:02.457Z" }, - { url = "https://files.pythonhosted.org/packages/46/06/33f41fe94403e2b755481cdfb9b7ef3e4e0ed031c4581124658d935d52b4/ruff-0.15.12-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e852ba9fdc890655e1d78f2df1499efbe0e54126bd405362154a75e2bde159c5", size = 10719355, upload-time = "2026-04-24T18:17:27.648Z" }, - { url = "https://files.pythonhosted.org/packages/0d/59/18aa4e014debbf559670e4048e39260a85c7fcee84acfd761ac01e7b8d35/ruff-0.15.12-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:dd8aed930da53780d22fc70bdf84452c843cf64f8cb4eb38984319c24c5cd5fd", size = 10462758, upload-time = "2026-04-24T18:17:32.347Z" }, - { url = "https://files.pythonhosted.org/packages/25/e7/cc9f16fd0f3b5fddcbd7ec3d6ae30c8f3fde1047f32a4093a98d633c6570/ruff-0.15.12-py3-none-musllinux_1_2_i686.whl", hash = "sha256:01da3988d225628b709493d7dc67c3b9b12c0210016b08690ef9bd27970b262b", size = 10953498, upload-time = "2026-04-24T18:17:20.674Z" }, - { url = "https://files.pythonhosted.org/packages/72/7a/a9ba7f98c7a575978698f4230c5e8cc54bbc761af34f560818f933dafa0c/ruff-0.15.12-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:9cae0f92bd5700d1213188b31cd3bdd2b315361296d10b96b8e2337d3d11f53e", size = 11447765, upload-time = "2026-04-24T18:17:09.755Z" }, - { url = "https://files.pythonhosted.org/packages/ea/f9/0ae446942c846b8266059ad8a30702a35afae55f5cdc54c5adf8d7afdc27/ruff-0.15.12-py3-none-win32.whl", hash = "sha256:d0185894e038d7043ba8fd6aee7499ece6462dc0ea9f1e260c7451807c714c20", size = 10657277, upload-time = "2026-04-24T18:17:18.591Z" }, - { url = "https://files.pythonhosted.org/packages/33/f1/9614e03e1cdcbf9437570b5400ced8a720b5db22b28d8e0f1bda429f660d/ruff-0.15.12-py3-none-win_amd64.whl", hash = "sha256:c87a162d61ab3adca47c03f7f717c68672edec7d1b5499e652331780fe74950d", size = 11837758, upload-time = "2026-04-24T18:17:00.113Z" }, - { url = "https://files.pythonhosted.org/packages/c0/98/6beb4b351e472e5f4c4613f7c35a5290b8be2497e183825310c4c3a3984b/ruff-0.15.12-py3-none-win_arm64.whl", hash = "sha256:a538f7a82d061cee7be55542aca1d86d1393d55d81d4fcc314370f4340930d4f", size = 11120821, upload-time = "2026-04-24T18:16:57.979Z" }, + { url = "https://files.pythonhosted.org/packages/c6/61/11d458dc6ac22504fd8e237b29dfd40504c7fbbcc8930402cfe51a8e63ed/ruff-0.15.13-py3-none-linux_armv6l.whl", hash = "sha256:444b580fc72fd6887e650acd3e575e18cdc79dbcf42fb4030b491057921f61f8", size = 10738279, upload-time = "2026-05-14T13:44:18.7Z" }, + { url = "https://files.pythonhosted.org/packages/86/ca/caa871ee7be718c45256fada4e16a218ee3e33f0c4a46b729a60a24912e6/ruff-0.15.13-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6590d009e7cb7ebf36f83dbdd44a3fa48a0994ff6f1cdc1b08006abe58f98dc7", size = 11124798, upload-time = "2026-05-14T13:44:06.427Z" }, + { url = "https://files.pythonhosted.org/packages/d3/19/43f5f2e568dddde567fc41f8471f9432c09563e19d3e617a48cfa52f8f0a/ruff-0.15.13-py3-none-macosx_11_0_arm64.whl", hash = "sha256:1c26d2f66163deeb6e08d8b39fbbe983ce3c71cea06a6d7591cfd1421793c629", size = 10460761, upload-time = "2026-05-14T13:44:04.375Z" }, + { url = "https://files.pythonhosted.org/packages/99/df/cf938cd6de3003178f03ad7c1ea2a6c099468c03a35037985070b37e76be/ruff-0.15.13-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9dbd6f94b434f896308e4d57fb7bfde0d02b99f7a64b3bdab0fdfa6a864203a5", size = 10804451, upload-time = "2026-05-14T13:44:25.221Z" }, + { url = "https://files.pythonhosted.org/packages/c7/7d/5d0973129b154ded2225729169d7068f26b467760b146493fde138415f23/ruff-0.15.13-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bf3259f3be4d181bda591da5db2571aed6853c6a048157756448020bc6c5cd22", size = 10534285, upload-time = "2026-05-14T13:44:08.888Z" }, + { url = "https://files.pythonhosted.org/packages/1f/e3/6b999bbc66cd51e5f073842bc2a3995e99c5e0e72e16b15e7261f7abf57a/ruff-0.15.13-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae9c17e5eb4430c154e76abc25d79a318190f5a997f38fb6b114416c5319ffc9", size = 11312063, upload-time = "2026-05-14T13:44:11.274Z" }, + { url = "https://files.pythonhosted.org/packages/af/5a/642639e9f5db04f1e97fbd6e091c6fd20725bdf072fb114d00eefb9e6eb8/ruff-0.15.13-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2e2e39bff6c341f4b577a21b801326fab0b11847f48fcaa83f00a113c9b3cb55", size = 12183079, upload-time = "2026-05-14T13:44:01.634Z" }, + { url = "https://files.pythonhosted.org/packages/19/4c/7585735f6b53b0f12de13618b2f7d250a844f018822efc899df2e7b8295f/ruff-0.15.13-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e8d9a8e08013542e94d3220bc5b62cc3e5ef87c5f74bff367d3fac14fab013e6", size = 11440833, upload-time = "2026-05-14T13:43:59.043Z" }, + { url = "https://files.pythonhosted.org/packages/e8/31/bf1a0803d077e679cfeee5f2f67290a0fa79c7385b5d9a8c17b9db2c48f0/ruff-0.15.13-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc411dfebe5eebe55ce041c6ae080eb7668955e866daa2fbb16692a784f1c4ca", size = 11434486, upload-time = "2026-05-14T13:44:27.761Z" }, + { url = "https://files.pythonhosted.org/packages/e1/4e/62c9b999875d4f14db80f277c030578f5e249c9852d65b7ac7ad0b43c041/ruff-0.15.13-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:768494eb08b9cee54e2fd27969966f74db5a57f6eaa7a90fcb3306af34dfc4bd", size = 11385189, upload-time = "2026-05-14T13:44:13.704Z" }, + { url = "https://files.pythonhosted.org/packages/fc/89/7e959047a104df3eb12863447c110140191fc5b6c4f379ea2e803fcdb0e4/ruff-0.15.13-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:fb75f9a3a7e42ffe117d734494e6c5e5cb3565d66e12612cb63d0e572a41a5b6", size = 10781380, upload-time = "2026-05-14T13:43:56.734Z" }, + { url = "https://files.pythonhosted.org/packages/ff/52/5fd18f3b88cab63e88aa11516b3b4e1e5f720e5c330f8dbe5c26210f41f8/ruff-0.15.13-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8cb74dd33bb2f6613faf7fc03b660053b5ac4f80e706d5788c6335e2a8048d51", size = 10540605, upload-time = "2026-05-14T13:44:20.748Z" }, + { url = "https://files.pythonhosted.org/packages/e8/e0/9e35f338990d3e41a82875ff7053ffe97541dae81c9d02143177f381d572/ruff-0.15.13-py3-none-musllinux_1_2_i686.whl", hash = "sha256:7ef823f817fcd191dc934e984be9cf4094f808effa16f2542ad8e821ba02bbf2", size = 11036554, upload-time = "2026-05-14T13:44:16.256Z" }, + { url = "https://files.pythonhosted.org/packages/c2/13/070fb048c24080fba188f66371e2a92785be257ad02242066dc7255ac6e9/ruff-0.15.13-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:f345a13937bd7f09f6f5d19fa0721b0c103e00e7f62bc67089a8e5e037719e0b", size = 11528133, upload-time = "2026-05-14T13:44:22.808Z" }, + { url = "https://files.pythonhosted.org/packages/6b/8c/b1e1666aef7fc6555094d73ae6cd981701781ae85b97ceefc0eebd0b4668/ruff-0.15.13-py3-none-win32.whl", hash = "sha256:4044f94208b3b05ba0fc4a4abd0558cf4d6459bd18325eead7fd8cc66f909b41", size = 10721455, upload-time = "2026-05-14T13:44:35.697Z" }, + { url = "https://files.pythonhosted.org/packages/ab/a6/870a3e8a50590bb92be184ad928c2922f088b00d9dc5c5ec7b924ee08c22/ruff-0.15.13-py3-none-win_amd64.whl", hash = "sha256:7064884d442b7d477b4e7473d12da7f08851d2b1982763c5d3f388a19468a1a4", size = 11900409, upload-time = "2026-05-14T13:44:30.389Z" }, + { url = "https://files.pythonhosted.org/packages/9b/36/9c015cd052fca743dae8cb2aeb16b551444787467db42ceab0fc968865af/ruff-0.15.13-py3-none-win_arm64.whl", hash = "sha256:2471da9bd1068c8c064b5fd9c0c4b6dddffd6369cb1cd68b29993b1709ff1b21", size = 11179336, upload-time = "2026-05-14T13:44:33.026Z" }, ] [[package]] @@ -1943,6 +1951,27 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0b/c9/584bc9651441b4ba60cc4d557d8a547b5aff901af35bda3a4ee30c819b82/starlette-1.0.0-py3-none-any.whl", hash = "sha256:d3ec55e0bb321692d275455ddfd3df75fff145d009685eb40dc91fc66b03d38b", size = 72651, upload-time = "2026-03-22T18:29:45.111Z" }, ] +[[package]] +name = "strategy-crypto" +version = "0.1.0" +source = { editable = "src/strategy_crypto" } +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" @@ -1978,14 +2007,14 @@ wheels = [ [[package]] name = "types-requests" -version = "2.33.0.20260508" +version = "2.33.0.20260513" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c7/6b/eb226bdd61a982c9a03e02c657fb4ab001733506e6423906ac142331f2e3/types_requests-2.33.0.20260508.tar.gz", hash = "sha256:81b2ae5f0d20967714a6aa5ef9284c05570d7cb06b7de8f2a77b918b63ddd411", size = 23991, upload-time = "2026-05-08T04:50:56.818Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6d/f7/3228dd3794941bcb92ca6ca2045a6671a828ec0b47becbef23310bc45559/types_requests-2.33.0.20260513.tar.gz", hash = "sha256:bd845450e954e751373d5d33526742592f298808a3ee3bda7e858e46b839b57f", size = 24714, upload-time = "2026-05-13T05:39:23.481Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/cb/96/080db0afdf2c5cc5fe512b41354e8d114fe8f65e9510c56ff8dfd40216ce/types_requests-2.33.0.20260508-py3-none-any.whl", hash = "sha256:fa01459cca184229713df03709db46a905325906d27e042cd4fd7ea3d15d3400", size = 20722, upload-time = "2026-05-08T04:50:55.548Z" }, + { url = "https://files.pythonhosted.org/packages/51/f5/233a78be8367a9888de718f002fb27b1ea4be39471cd88aedeafceed872e/types_requests-2.33.0.20260513-py3-none-any.whl", hash = "sha256:d5a965f9d18b6e06b72039a69565de9027e58f36a7f709857da747fbe7521122", size = 21390, upload-time = "2026-05-13T05:39:22.262Z" }, ] [[package]] @@ -2029,15 +2058,15 @@ wheels = [ [[package]] name = "uvicorn" -version = "0.46.0" +version = "0.47.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, { name = "h11" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/1f/93/041fca8274050e40e6791f267d82e0e2e27dd165627bd640d3e0e378d877/uvicorn-0.46.0.tar.gz", hash = "sha256:fb9da0926999cc6cb22dc7cd71a94a632f078e6ae47ff683c5c420750fb7413d", size = 88758, upload-time = "2026-04-23T07:16:00.151Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/b1/8e7077a8641086aea449e1b5752a570f1b5906c64e0a33cd6d93b63a066b/uvicorn-0.47.0.tar.gz", hash = "sha256:7c9a0ea1a9414106bbab7324609c162d8fa0cdcdcb703060987269d77c7bb533", size = 90582, upload-time = "2026-05-14T18:16:54.455Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/31/a3/5b1562db76a5a488274b2332a97199b32d0442aca0ed193697fd47786316/uvicorn-0.46.0-py3-none-any.whl", hash = "sha256:bbebbcbed972d162afca128605223022bedd345b7bc7855ce66deb31487a9048", size = 70926, upload-time = "2026-04-23T07:15:58.355Z" }, + { url = "https://files.pythonhosted.org/packages/15/41/ac2dfdbc1f60c7af4f994c7a335cfa7040c01642b605d65f611cecc2a1e4/uvicorn-0.47.0-py3-none-any.whl", hash = "sha256:2c5715bc12d1892d84752049f400cd1c3cb018514967fdfeb97640443a6a9432", size = 71301, upload-time = "2026-05-14T18:16:51.762Z" }, ] [package.optional-dependencies] From 08f1585ab27f6128fd79e61e6541bc65a8455eed Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Fri, 15 May 2026 17:46:50 +0000 Subject: [PATCH 04/11] =?UTF-8?q?refactor(schema):=20split=20persistence?= =?UTF-8?q?=20schema=20=E2=80=94=20core=20GA=20+=20strategy=5Fcrypto=20pap?= =?UTF-8?q?er?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - multi_swarm_core/persistence/schema.py: rimuove tabelle paper_trading_* (5 CREATE TABLE + 3 indici idx_paper_*). Restano solo le tabelle GA universali (runs, generations, genomes, evaluations, cost_records, adversarial_findings). - strategy_crypto/backend/schema.py NEW: PAPER_SCHEMA_SQL standalone + init_schema() con mkdir parent, scrive su state/strategy_crypto.db. Ownership: ogni strategia possiede il proprio schema, isolato dal core. Pattern replicabile per strategy_forex, strategy_equity, ecc. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../multi_swarm_core/persistence/schema.py | 61 ------------- .../strategy_crypto/backend/schema.py | 86 +++++++++++++++++++ 2 files changed, 86 insertions(+), 61 deletions(-) create mode 100644 src/strategy_crypto/strategy_crypto/backend/schema.py diff --git a/src/multi_swarm_core/multi_swarm_core/persistence/schema.py b/src/multi_swarm_core/multi_swarm_core/persistence/schema.py index e9b6f11..3bcec0c 100644 --- a/src/multi_swarm_core/multi_swarm_core/persistence/schema.py +++ b/src/multi_swarm_core/multi_swarm_core/persistence/schema.py @@ -77,68 +77,7 @@ CREATE TABLE IF NOT EXISTS adversarial_findings ( FOREIGN KEY (run_id) REFERENCES runs(id) ); -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_evaluations_fitness ON evaluations(run_id, fitness DESC); CREATE INDEX IF NOT EXISTS idx_genomes_generation ON genomes(run_id, generation_idx); CREATE INDEX IF NOT EXISTS idx_cost_run ON cost_records(run_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); """ diff --git a/src/strategy_crypto/strategy_crypto/backend/schema.py b/src/strategy_crypto/strategy_crypto/backend/schema.py new file mode 100644 index 0000000..e912817 --- /dev/null +++ b/src/strategy_crypto/strategy_crypto/backend/schema.py @@ -0,0 +1,86 @@ +"""Schema SQLite per le tabelle paper-trading della strategia crypto. + +Owned dal member strategy_crypto: il DDL e' standalone rispetto al core, +e scrive su un database dedicato (state/strategy_crypto.db) 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() From b02be648315ebc05fcf8949a476eeb95f893bd1e Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Fri, 15 May 2026 17:54:31 +0000 Subject: [PATCH 05/11] =?UTF-8?q?refactor(paper=5Ftrading):=20move=20multi?= =?UTF-8?q?=5Fswarm=5Fcore/paper=5Ftrading=20=E2=86=92=20strategy=5Fcrypto?= =?UTF-8?q?/backend?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - git mv executor.py, portfolio.py, persistence.py al backend strategy_crypto - Cancellato src/multi_swarm_core/multi_swarm_core/paper_trading/ - Patch import: from ..backtest/protocol → from multi_swarm_core.backtest/protocol - persistence.py: * usa schema locale strategy_crypto.backend.schema (no piu' core) * docstring aggiornata: DB dedicato state/strategy_crypto.db isolato dal core * PaperRepository.init_schema() crea le tabelle paper_trading_* - backend/__init__.py: re-export pubblico (PaperExecutor, Portfolio, PaperRepository, ...) - config.py: * NEW: ga_db_path (alias DB_PATH legacy + GA_DB_PATH preferito) * NEW: strategy_crypto_db_path (STRATEGY_CRYPTO_DB_PATH) * db_path conserva come property proxy di ga_db_path per backcompat - Test: import in venv workspace OK; load_settings() resolve entrambi i path Co-Authored-By: Claude Opus 4.7 (1M context) --- .../multi_swarm_core/config.py | 21 +++++++++++++++-- .../paper_trading/__init__.py | 0 .../strategy_crypto/backend/__init__.py | 23 +++++++++++++++++++ .../strategy_crypto/backend}/executor.py | 7 +++--- .../strategy_crypto/backend}/persistence.py | 13 +++++++++-- .../strategy_crypto/backend}/portfolio.py | 2 +- 6 files changed, 58 insertions(+), 8 deletions(-) delete mode 100644 src/multi_swarm_core/multi_swarm_core/paper_trading/__init__.py rename src/{multi_swarm_core/multi_swarm_core/paper_trading => strategy_crypto/strategy_crypto/backend}/executor.py (94%) rename src/{multi_swarm_core/multi_swarm_core/paper_trading => strategy_crypto/strategy_crypto/backend}/persistence.py (88%) rename src/{multi_swarm_core/multi_swarm_core/paper_trading => strategy_crypto/strategy_crypto/backend}/portfolio.py (98%) diff --git a/src/multi_swarm_core/multi_swarm_core/config.py b/src/multi_swarm_core/multi_swarm_core/config.py index 0ea0ca2..2c20e0f 100644 --- a/src/multi_swarm_core/multi_swarm_core/config.py +++ b/src/multi_swarm_core/multi_swarm_core/config.py @@ -6,7 +6,7 @@ in the project root. Required secrets are validated at instantiation time. from pathlib import Path -from pydantic import Field, SecretStr +from pydantic import AliasChoices, Field, SecretStr from pydantic_settings import BaseSettings, SettingsConfigDict @@ -35,7 +35,24 @@ class Settings(BaseSettings): run_name: str = "phase1-spike-001" data_dir: Path = Field(default=Path("./data")) series_dir: Path = Field(default=Path("./series")) - db_path: Path = Field(default=Path("./runs.db")) + + # GA core DB (tabelle universali: runs, generations, genomes, evaluations, ...) + # Alias DB_PATH legacy mantenuto per backcompat (deprecato, rimuovere nei prossimi cicli). + ga_db_path: Path = Field( + default=Path("./state/runs.db"), + validation_alias=AliasChoices("GA_DB_PATH", "DB_PATH"), + ) + + # DB per la strategia crypto (tabelle paper_trading_*, isolato dal core) + strategy_crypto_db_path: Path = Field( + default=Path("./state/strategy_crypto.db"), + validation_alias="STRATEGY_CRYPTO_DB_PATH", + ) + + @property + def db_path(self) -> Path: + """Backcompat alias: legge ga_db_path. Deprecato — usare ga_db_path.""" + return self.ga_db_path def load_settings() -> Settings: diff --git a/src/multi_swarm_core/multi_swarm_core/paper_trading/__init__.py b/src/multi_swarm_core/multi_swarm_core/paper_trading/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/strategy_crypto/strategy_crypto/backend/__init__.py b/src/strategy_crypto/strategy_crypto/backend/__init__.py index e69de29..3d071d1 100644 --- a/src/strategy_crypto/strategy_crypto/backend/__init__.py +++ b/src/strategy_crypto/strategy_crypto/backend/__init__.py @@ -0,0 +1,23 @@ +"""Backend paper-trading per la strategia crypto. + +Espone le classi principali per import ergonomici in scripts/runner: + + from strategy_crypto.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", +] diff --git a/src/multi_swarm_core/multi_swarm_core/paper_trading/executor.py b/src/strategy_crypto/strategy_crypto/backend/executor.py similarity index 94% rename from src/multi_swarm_core/multi_swarm_core/paper_trading/executor.py rename to src/strategy_crypto/strategy_crypto/backend/executor.py index dbbd32c..524f26e 100644 --- a/src/multi_swarm_core/multi_swarm_core/paper_trading/executor.py +++ b/src/strategy_crypto/strategy_crypto/backend/executor.py @@ -20,9 +20,10 @@ from pathlib import Path import pandas as pd # type: ignore[import-untyped] -from ..backtest.orders import Side, Trade -from ..protocol.compiler import compile_strategy -from ..protocol.parser import parse_strategy +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 diff --git a/src/multi_swarm_core/multi_swarm_core/paper_trading/persistence.py b/src/strategy_crypto/strategy_crypto/backend/persistence.py similarity index 88% rename from src/multi_swarm_core/multi_swarm_core/paper_trading/persistence.py rename to src/strategy_crypto/strategy_crypto/backend/persistence.py index b58c620..7d08987 100644 --- a/src/multi_swarm_core/multi_swarm_core/paper_trading/persistence.py +++ b/src/strategy_crypto/strategy_crypto/backend/persistence.py @@ -1,5 +1,9 @@ -"""Persistenza paper-trading: usa lo stesso ``runs.db`` con tabelle dedicate -``paper_trading_*`` (vedi :mod:`multi_swarm_core.persistence.schema`). +"""Persistenza paper-trading: scrive su un DB dedicato (state/strategy_crypto.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_.db), nessuna contention di lock +fra writer GA e writer paper. """ from __future__ import annotations @@ -13,12 +17,17 @@ 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 diff --git a/src/multi_swarm_core/multi_swarm_core/paper_trading/portfolio.py b/src/strategy_crypto/strategy_crypto/backend/portfolio.py similarity index 98% rename from src/multi_swarm_core/multi_swarm_core/paper_trading/portfolio.py rename to src/strategy_crypto/strategy_crypto/backend/portfolio.py index 4b99bfa..8945958 100644 --- a/src/multi_swarm_core/multi_swarm_core/paper_trading/portfolio.py +++ b/src/strategy_crypto/strategy_crypto/backend/portfolio.py @@ -17,7 +17,7 @@ from __future__ import annotations from dataclasses import dataclass, field from datetime import datetime -from ..backtest.orders import Side, Trade +from multi_swarm_core.backtest.orders import Side, Trade @dataclass(frozen=True) From 37bf64012b84e3d9b88a0b6ce691983d1f1bfaa9 Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Fri, 15 May 2026 17:56:55 +0000 Subject: [PATCH 06/11] =?UTF-8?q?refactor(dashboard):=20move=20multi=5Fswa?= =?UTF-8?q?rm=5Fcore/dashboard=20=E2=86=92=20strategy=5Fcrypto/frontend?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - git mv data.py + nicegui_app.py al frontend strategy_crypto - Cancellato src/multi_swarm_core/multi_swarm_core/dashboard/ - Patch import in data.py: ..persistence → multi_swarm_core.persistence - Patch nicegui_app.py: * Import: multi_swarm_core.dashboard.data → strategy_crypto.frontend.data * Dual-DB: split DB_PATH in GA_DB_PATH + PAPER_DB_PATH (env separati) * Subpath routing: DASHBOARD_ROOT_PATH passato a ui.run(root_path=...) * Header dashboard mostra entrambi i DB * Docstring di avvio aggiornata: python -m strategy_crypto.frontend.nicegui_app * Title rinominato "Strategy Crypto Dashboard" - 6 occorrenze get_repo(DB_PATH) → get_repo(GA_DB_PATH) - 6 occorrenze paper_*_df(DB_PATH, ...) → paper_*_df(PAPER_DB_PATH, ...) Co-Authored-By: Claude Opus 4.7 (1M context) --- .../multi_swarm_core/dashboard/__init__.py | 0 .../strategy_crypto/frontend}/data.py | 2 +- .../strategy_crypto/frontend}/nicegui_app.py | 49 ++++++++++++------- 3 files changed, 31 insertions(+), 20 deletions(-) delete mode 100644 src/multi_swarm_core/multi_swarm_core/dashboard/__init__.py rename src/{multi_swarm_core/multi_swarm_core/dashboard => strategy_crypto/strategy_crypto/frontend}/data.py (98%) rename src/{multi_swarm_core/multi_swarm_core/dashboard => strategy_crypto/strategy_crypto/frontend}/nicegui_app.py (96%) diff --git a/src/multi_swarm_core/multi_swarm_core/dashboard/__init__.py b/src/multi_swarm_core/multi_swarm_core/dashboard/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/multi_swarm_core/multi_swarm_core/dashboard/data.py b/src/strategy_crypto/strategy_crypto/frontend/data.py similarity index 98% rename from src/multi_swarm_core/multi_swarm_core/dashboard/data.py rename to src/strategy_crypto/strategy_crypto/frontend/data.py index 33503ff..738a008 100644 --- a/src/multi_swarm_core/multi_swarm_core/dashboard/data.py +++ b/src/strategy_crypto/strategy_crypto/frontend/data.py @@ -7,7 +7,7 @@ from typing import Any import pandas as pd # type: ignore[import-untyped] -from ..persistence.repository import Repository +from multi_swarm_core.persistence.repository import Repository def get_repo(db_path: str | Path) -> Repository: diff --git a/src/multi_swarm_core/multi_swarm_core/dashboard/nicegui_app.py b/src/strategy_crypto/strategy_crypto/frontend/nicegui_app.py similarity index 96% rename from src/multi_swarm_core/multi_swarm_core/dashboard/nicegui_app.py rename to src/strategy_crypto/strategy_crypto/frontend/nicegui_app.py index 184cc83..eca9201 100644 --- a/src/multi_swarm_core/multi_swarm_core/dashboard/nicegui_app.py +++ b/src/strategy_crypto/strategy_crypto/frontend/nicegui_app.py @@ -1,6 +1,6 @@ """NiceGUI dashboard — port progressivo da Streamlit. -Avvio: ``uv run python -m multi_swarm_core.dashboard.nicegui_app`` +Avvio: ``uv run python -m strategy_crypto.frontend.nicegui_app`` Default port 8080. Streamlit resta su 8501 durante la migrazione. Riusa ``dashboard.data`` (Repository helpers) senza modifiche al backend. @@ -28,7 +28,7 @@ import pandas as pd # type: ignore[import-untyped] import plotly.graph_objects as go # type: ignore[import-untyped] from nicegui import app, ui -from multi_swarm_core.dashboard.data import ( +from strategy_crypto.frontend.data import ( evaluations_df, generations_df, genomes_df, @@ -43,7 +43,11 @@ from multi_swarm_core.dashboard.data import ( paper_trades_df, ) -DB_PATH = os.environ.get("DB_PATH", "./runs.db") +# Dual-DB: GA core e paper strategy_crypto vivono in DB separati. +GA_DB_PATH = os.environ.get("GA_DB_PATH", "./state/runs.db") +PAPER_DB_PATH = os.environ.get("STRATEGY_CRYPTO_DB_PATH", "./state/strategy_crypto.db") +# Subpath per Traefik: "" in dev, "/strategy_crypto_gui" in prod. +DASHBOARD_ROOT_PATH = os.environ.get("DASHBOARD_ROOT_PATH", "") REFRESH_INTERVAL_S = 3.0 # --- Neon Trading Dashboard palette --- @@ -384,11 +388,11 @@ def _build_header(active: str) -> None: with ui.row().classes("items-center gap-3"): ui.html(f'' - f'⛁ {Path(DB_PATH).resolve().name}') + f'⛁ {Path(GA_DB_PATH).resolve().name} + {Path(PAPER_DB_PATH).resolve().name}') def _runs_options() -> dict[str, str]: - repo = get_repo(DB_PATH) + repo = get_repo(GA_DB_PATH) runs = list_runs_df(repo) if runs.empty: return {} @@ -399,7 +403,7 @@ def _runs_options() -> dict[str, str]: def _snapshot(run_id: str) -> dict[str, Any]: - repo = get_repo(DB_PATH) + repo = get_repo(GA_DB_PATH) ov = get_run_overview(repo, run_id) evals = evaluations_df(repo, run_id) gens = generations_df(repo, run_id) @@ -632,8 +636,8 @@ def convergence() -> None: ) ui.button("🔄 Refresh", on_click=lambda: refresh()).props("outline color=primary") - fitness_plot = ui.plotly(_convergence_figure(generations_df(get_repo(DB_PATH), state["run_id"]))).classes("w-full") - entropy_plot = ui.plotly(_entropy_figure(generations_df(get_repo(DB_PATH), state["run_id"]))).classes("w-full q-mt-md") + fitness_plot = ui.plotly(_convergence_figure(generations_df(get_repo(GA_DB_PATH), state["run_id"]))).classes("w-full") + entropy_plot = ui.plotly(_entropy_figure(generations_df(get_repo(GA_DB_PATH), state["run_id"]))).classes("w-full q-mt-md") ui.separator() ui.label("Tabella generazioni").classes("text-subtitle1 q-mt-md") @@ -656,8 +660,8 @@ def convergence() -> None: if not run_id: return try: - gens = generations_df(get_repo(DB_PATH), run_id) - ov = get_run_overview(get_repo(DB_PATH), run_id) + gens = generations_df(get_repo(GA_DB_PATH), run_id) + ov = get_run_overview(get_repo(GA_DB_PATH), run_id) except Exception as e: # noqa: BLE001 ui.notify(f"Errore: {e}", type="negative") return @@ -801,7 +805,7 @@ def genomes() -> None: if not run_id: return try: - repo = get_repo(DB_PATH) + repo = get_repo(GA_DB_PATH) evals = evaluations_df(repo, run_id) gens = genomes_df(repo, run_id) except Exception as e: # noqa: BLE001 @@ -874,7 +878,7 @@ def genomes() -> None: def _paper_runs_options(only_running: bool = False) -> dict[str, str]: - runs = paper_runs_df(DB_PATH) + runs = paper_runs_df(PAPER_DB_PATH) if runs.empty: return {} if only_running: @@ -1014,11 +1018,11 @@ def paper() -> None: if not run_id: return try: - summary = paper_run_summary(DB_PATH, run_id) - eq = paper_equity_df(DB_PATH, run_id) - positions = paper_positions_df(DB_PATH, run_id) - ticks = paper_ticks_df(DB_PATH, run_id, limit=30) - trades = paper_trades_df(DB_PATH, run_id, limit=50) + 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 @@ -1081,14 +1085,21 @@ def paper() -> None: def main() -> None: - app.on_startup(lambda: print(f"DB: {Path(DB_PATH).resolve()}")) + 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="Multi-Swarm Dashboard", + title="Strategy Crypto Dashboard", reload=False, show=False, dark=True, + root_path=DASHBOARD_ROOT_PATH, ) From 2b5da4d1fc353447bb5eecefe87041e40e1d79c7 Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Fri, 15 May 2026 17:58:03 +0000 Subject: [PATCH 07/11] refactor(strategies): move JSON freezate sotto strategy_crypto + patch runner MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - git mv strategies/{btc,eth}_*.json → src/strategy_crypto/strategy_crypto/strategies/ - Cancellata directory root strategies/ (ora package data del member strategy_crypto) - I JSON sono inclusi nella wheel via force-include nel pyproject del member - scripts/run_paper_trading.py: * Import paper_trading.* → strategy_crypto.backend * Default --strategies-dir letto da importlib.resources.files('strategy_crypto') / 'strategies' * PaperRepository(settings.strategy_crypto_db_path) + init_schema() * Rimosso Repository(settings.db_path).init_schema() (GA init non e' responsabilita' del paper) - Verifica: importlib.resources trova i 2 JSON nel package Co-Authored-By: Claude Opus 4.7 (1M context) --- scripts/run_paper_trading.py | 21 ++++++++++--------- .../strategies}/btc_fb63e851.json | 0 .../strategies}/eth_facd6af85d5d.json | 0 3 files changed, 11 insertions(+), 10 deletions(-) rename {strategies => src/strategy_crypto/strategy_crypto/strategies}/btc_fb63e851.json (100%) rename {strategies => src/strategy_crypto/strategy_crypto/strategies}/eth_facd6af85d5d.json (100%) diff --git a/scripts/run_paper_trading.py b/scripts/run_paper_trading.py index 6eb0d6a..a31a581 100644 --- a/scripts/run_paper_trading.py +++ b/scripts/run_paper_trading.py @@ -20,6 +20,7 @@ Esempio: from __future__ import annotations import argparse +import importlib.resources import time from dataclasses import dataclass from datetime import UTC, datetime, timedelta @@ -28,14 +29,16 @@ from pathlib import Path 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.paper_trading.executor import PaperExecutor -from multi_swarm_core.paper_trading.persistence import PaperRepository -from multi_swarm_core.paper_trading.portfolio import Portfolio -from multi_swarm_core.persistence.repository import Repository +from strategy_crypto.backend import PaperExecutor, PaperRepository, Portfolio PROJECT_ROOT = Path(__file__).resolve().parent.parent +def _default_strategies_dir() -> Path: + """Cartella JSON shippata col package strategy_crypto.""" + return Path(str(importlib.resources.files("strategy_crypto") / "strategies")) + + @dataclass(frozen=True) class AssetConfig: symbol: str # es. "BTC-PERPETUAL" @@ -54,8 +57,8 @@ def parse_args() -> argparse.Namespace: p.add_argument("--lookback-bars", type=int, default=500, help="Quante bar fetchare per indicatori") p.add_argument( "--strategies-dir", - default=str(PROJECT_ROOT / "strategies"), - help="Cartella contenente btc_*.json e eth_*.json", + default=str(_default_strategies_dir()), + help="Cartella contenente btc_*.json e eth_*.json (default: package strategy_crypto/strategies)", ) return p.parse_args() @@ -77,9 +80,6 @@ def main() -> None: args = parse_args() settings = load_settings() - # Inizializza schema (idempotente). - Repository(settings.db_path).init_schema() - token = ( settings.cerbero_mainnet_token.get_secret_value() if settings.cerbero_mainnet_token @@ -105,7 +105,8 @@ def main() -> None: fees_bp=args.fees_bp, n_sleeves=len(assets), ) - repo = PaperRepository(settings.db_path) + repo = PaperRepository(settings.strategy_crypto_db_path) + repo.init_schema() config = { "assets": [ {"symbol": a.symbol, "strategy": a.strategy_file.name, "exchange": a.exchange} diff --git a/strategies/btc_fb63e851.json b/src/strategy_crypto/strategy_crypto/strategies/btc_fb63e851.json similarity index 100% rename from strategies/btc_fb63e851.json rename to src/strategy_crypto/strategy_crypto/strategies/btc_fb63e851.json diff --git a/strategies/eth_facd6af85d5d.json b/src/strategy_crypto/strategy_crypto/strategies/eth_facd6af85d5d.json similarity index 100% rename from strategies/eth_facd6af85d5d.json rename to src/strategy_crypto/strategy_crypto/strategies/eth_facd6af85d5d.json From 289df4b81fdef2b5bb20c08b78902eab8f550e60 Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Fri, 15 May 2026 17:59:28 +0000 Subject: [PATCH 08/11] refactor(layout): docs+tests core sotto modulo, cleanup superflui, README strategy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ownership per modulo: - Move docs/ root → src/multi_swarm_core/docs/{design,decisions,reports}/ * 00_documento_zero.md + coevolutive_swarm_system.md → docs/design/ * decisions/* → docs/decisions/ * reports/2026-05-14-stato-progetto-e-roadmap.md → docs/reports/ - Move tests/ root → src/multi_swarm_core/tests/ Cleanup superflui consumati (audit trail preservato in docs/decisions): - poc_trading_swarm.md (POC superato — Phase 3 attiva in prod) - docs/reports/2026-05-10-phase1-technical-report.md (superato dal 14-mag) - docs/superpowers/plans/*.md (3 file, plan consumati) - docs/superpowers/specs/*.md (2 file, spec consumate) - tests/unit/paper_trading/ (vuota, paper_trading e' migrato in strategy_crypto) - Directory docs/ root cancellata NEW: src/strategy_crypto/README.md — overview strategia (scope, layout, run, DB schema, pattern N strategie future) Root resta minima: README.md, pyproject.toml, docker-compose.yml, Dockerfile, .env*, uv.lock + data/series/state/scripts. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../2026-05-10-phase1-technical-report.md | 282 - .../plans/2026-05-09-phase1-lean-spike.md | 5282 ----------------- .../2026-05-11-mutate-prompt-llm-phase-2-5.md | 318 - .../plans/2026-05-11-temporal-features.md | 482 -- .../2026-05-09-decisione-strategica-design.md | 427 -- .../2026-05-11-temporal-features-design.md | 183 - poc_trading_swarm.md | 831 --- .../docs}/decisions/2026-05-10-gate-phase1.md | 0 .../2026-05-11-phase1-5-nemotron-run.md | 0 .../docs/design/00_documento_zero.md | 0 .../docs/design/coevolutive_swarm_system.md | 0 .../2026-05-14-stato-progetto-e-roadmap.md | 0 .../multi_swarm_core/tests}/__init__.py | 0 .../tests}/integration/__init__.py | 0 .../integration/test_e2e_minimal_run.py | 0 .../test_ga_loop_with_prompt_mutator.py | 0 .../multi_swarm_core/tests}/unit/__init__.py | 0 .../tests}/unit/test_adversarial.py | 0 .../tests}/unit/test_backtest_engine.py | 0 .../tests}/unit/test_backtest_orders.py | 0 .../tests}/unit/test_cerbero_client.py | 0 .../tests}/unit/test_cerbero_ohlcv.py | 0 .../tests}/unit/test_cerbero_tools.py | 0 .../tests}/unit/test_config.py | 0 .../tests}/unit/test_cost_tracker.py | 0 .../tests}/unit/test_diversity.py | 0 .../tests}/unit/test_falsification.py | 0 .../tests}/unit/test_fitness.py | 0 .../tests}/unit/test_ga_initial.py | 0 .../tests}/unit/test_ga_loop.py | 0 .../tests}/unit/test_ga_summary.py | 0 .../tests}/unit/test_genome_crossover.py | 0 .../tests}/unit/test_genome_hypothesis.py | 0 .../tests}/unit/test_genome_mutation.py | 0 .../tests}/unit/test_hypothesis_agent.py | 0 .../tests}/unit/test_llm_client.py | 0 .../tests}/unit/test_market_summary.py | 0 .../tests}/unit/test_metrics_basic.py | 0 .../tests}/unit/test_metrics_dsr.py | 0 .../tests}/unit/test_mutation_dispatcher.py | 0 .../tests}/unit/test_mutation_prompt_llm.py | 0 .../tests}/unit/test_protocol_compiler.py | 0 .../tests}/unit/test_protocol_parser.py | 0 .../tests}/unit/test_protocol_validator.py | 0 .../tests}/unit/test_repository.py | 0 .../tests}/unit/test_selection.py | 0 .../tests}/unit/test_splits.py | 0 src/strategy_crypto/README.md | 65 + tests/unit/paper_trading/__init__.py | 0 49 files changed, 65 insertions(+), 7805 deletions(-) delete mode 100644 docs/reports/2026-05-10-phase1-technical-report.md delete mode 100644 docs/superpowers/plans/2026-05-09-phase1-lean-spike.md delete mode 100644 docs/superpowers/plans/2026-05-11-mutate-prompt-llm-phase-2-5.md delete mode 100644 docs/superpowers/plans/2026-05-11-temporal-features.md delete mode 100644 docs/superpowers/specs/2026-05-09-decisione-strategica-design.md delete mode 100644 docs/superpowers/specs/2026-05-11-temporal-features-design.md delete mode 100644 poc_trading_swarm.md rename {docs => src/multi_swarm_core/docs}/decisions/2026-05-10-gate-phase1.md (100%) rename {docs => src/multi_swarm_core/docs}/decisions/2026-05-11-phase1-5-nemotron-run.md (100%) rename 00_documento_zero.md => src/multi_swarm_core/docs/design/00_documento_zero.md (100%) rename coevolutive_swarm_system.md => src/multi_swarm_core/docs/design/coevolutive_swarm_system.md (100%) rename {docs => src/multi_swarm_core/docs}/reports/2026-05-14-stato-progetto-e-roadmap.md (100%) rename {tests => src/multi_swarm_core/tests}/__init__.py (100%) rename {tests => src/multi_swarm_core/tests}/integration/__init__.py (100%) rename {tests => src/multi_swarm_core/tests}/integration/test_e2e_minimal_run.py (100%) rename {tests => src/multi_swarm_core/tests}/integration/test_ga_loop_with_prompt_mutator.py (100%) rename {tests => src/multi_swarm_core/tests}/unit/__init__.py (100%) rename {tests => src/multi_swarm_core/tests}/unit/test_adversarial.py (100%) rename {tests => src/multi_swarm_core/tests}/unit/test_backtest_engine.py (100%) rename {tests => src/multi_swarm_core/tests}/unit/test_backtest_orders.py (100%) rename {tests => src/multi_swarm_core/tests}/unit/test_cerbero_client.py (100%) rename {tests => src/multi_swarm_core/tests}/unit/test_cerbero_ohlcv.py (100%) rename {tests => src/multi_swarm_core/tests}/unit/test_cerbero_tools.py (100%) rename {tests => src/multi_swarm_core/tests}/unit/test_config.py (100%) rename {tests => src/multi_swarm_core/tests}/unit/test_cost_tracker.py (100%) rename {tests => src/multi_swarm_core/tests}/unit/test_diversity.py (100%) rename {tests => src/multi_swarm_core/tests}/unit/test_falsification.py (100%) rename {tests => src/multi_swarm_core/tests}/unit/test_fitness.py (100%) rename {tests => src/multi_swarm_core/tests}/unit/test_ga_initial.py (100%) rename {tests => src/multi_swarm_core/tests}/unit/test_ga_loop.py (100%) rename {tests => src/multi_swarm_core/tests}/unit/test_ga_summary.py (100%) rename {tests => src/multi_swarm_core/tests}/unit/test_genome_crossover.py (100%) rename {tests => src/multi_swarm_core/tests}/unit/test_genome_hypothesis.py (100%) rename {tests => src/multi_swarm_core/tests}/unit/test_genome_mutation.py (100%) rename {tests => src/multi_swarm_core/tests}/unit/test_hypothesis_agent.py (100%) rename {tests => src/multi_swarm_core/tests}/unit/test_llm_client.py (100%) rename {tests => src/multi_swarm_core/tests}/unit/test_market_summary.py (100%) rename {tests => src/multi_swarm_core/tests}/unit/test_metrics_basic.py (100%) rename {tests => src/multi_swarm_core/tests}/unit/test_metrics_dsr.py (100%) rename {tests => src/multi_swarm_core/tests}/unit/test_mutation_dispatcher.py (100%) rename {tests => src/multi_swarm_core/tests}/unit/test_mutation_prompt_llm.py (100%) rename {tests => src/multi_swarm_core/tests}/unit/test_protocol_compiler.py (100%) rename {tests => src/multi_swarm_core/tests}/unit/test_protocol_parser.py (100%) rename {tests => src/multi_swarm_core/tests}/unit/test_protocol_validator.py (100%) rename {tests => src/multi_swarm_core/tests}/unit/test_repository.py (100%) rename {tests => src/multi_swarm_core/tests}/unit/test_selection.py (100%) rename {tests => src/multi_swarm_core/tests}/unit/test_splits.py (100%) create mode 100644 src/strategy_crypto/README.md delete mode 100644 tests/unit/paper_trading/__init__.py diff --git a/docs/reports/2026-05-10-phase1-technical-report.md b/docs/reports/2026-05-10-phase1-technical-report.md deleted file mode 100644 index 4d20b35..0000000 --- a/docs/reports/2026-05-10-phase1-technical-report.md +++ /dev/null @@ -1,282 +0,0 @@ -# Phase 1 Lean Spike — Rapporto Tecnico - -**Autore**: Adriano Dal Pastro -**Data**: 10 maggio 2026 -**Versione**: 1.0 (finalizzato) -**Status**: ✅ Phase 1 chiusa, tutti 5 hard gate passati - -**Documenti correlati**: -- `docs/superpowers/specs/2026-05-09-decisione-strategica-design.md` (decisione strategica B3) -- `docs/superpowers/plans/2026-05-09-phase1-lean-spike.md` (piano implementativo) -- `docs/decisions/2026-05-10-gate-phase1.md` (decision memo finale) - ---- - -## 1. Setup sperimentale - -L'obiettivo della Phase 1 lean spike è dimostrare che il loop tecnico (LLM hypothesis → backtest falsification → adversarial check → GA selection) funziona end-to-end e produce output formalizzabile. I cinque hard gate definiti nello spec sez. 4.4 misurano feasibility, non alpha edge — quella è valutazione di Phase 2. - -### 1.1 Configurazione del run di riferimento - -Il run `phase1-real-005` (id `1c526996160446b18c0fb57d94874975`) è il primo a superare tutti i gate dopo 4 iterazioni di bug-fix (vedi sez. 3 del decision memo). - -| Parametro | Valore | -|---|---| -| Population size (K) | 20 | -| Generazioni | 10 | -| Elite k | 2 | -| Tournament k | 3 | -| Crossover probability | 0.5 | -| Random seed | 42 | -| Symbol | BTC-PERPETUAL (Deribit) | -| Timeframe | 1h | -| Range storico | 2024-01-01 → 2026-01-01 (2 anni, 17545 candele) | -| Fees backtest | 5 basis points | -| n_trials_dsr | 50 | -| Tier LLM dominante | C (qwen3-235b-a22b-2507 via OpenRouter) | -| Cerbero MCP endpoint | http://localhost:9001 (locale) | -| Durata wall-clock | 29 minuti | -| Costo LLM | $0.069 | - -### 1.2 Stack tecnologico - -Python 3.13, uv 0.10.9. Test framework: pytest + pytest-mock + responses. Persistence: sqlite3 + sqlmodel. Parsing strategia: `json.loads` con dataclass-based AST. Analytics: pandas + numpy + scipy. LLM: openai SDK con base URL OpenRouter (route unica per tutti i tier S/A/B/C/D). HTTP: requests + tenacity. Dashboard: streamlit + plotly + canvas HTML5 custom. - -### 1.3 Architettura del run - -L'orchestrator (`src/multi_swarm/orchestrator/run.py`, 184 righe) coordina la pipeline end-to-end: - -1. **OHLCV loading**: `CerberoOHLCVLoader` chiama `mcp-deribit/tools/get_historical` paginando in chunk da 4500 barre (cap soft Deribit ~5000). Cache parquet su sha1 della query — il run v5 ha riusato cache popolata dai run precedenti, fetch istantaneo. -2. **Market summary**: statistiche return (mean, std, skew, kurt) + classificazione regime volatilità. -3. **Initial population**: 20 genomi distribuiti uniformemente sui 6 cognitive style (physicist, biologist, historian, meteorologist, ecologist, engineer), temperature random in [0.7, 1.2], lookback random in {100, 150, 200, 300}. -4. **Per ogni generazione (10 totali)**: - - **Hypothesis**: chiamata LLM con prompt SYSTEM (regole grammar) + USER (market summary). Output JSON estratto via regex fence ```json. Se parse/validation fallisce: retry 1x con error message nel prompt utente. - - **Falsification**: AST compilato in `Callable[[df], Series[Side]]`, backtest event-driven con 1-bar exec delay, calcolo Sharpe + Deflated Sharpe (Bailey & López 2014, n_trials=50). - - **Adversarial**: 4 check euristici (no_trades, degenerate, overtrading, undertrading). - - **Fitness**: `0.5*dsr + 0.25*(tanh(sharpe)+1)` × `1/(1+max_dd)`, range [0, ~1]. Kill (=0) su zero trade o HIGH adversarial finding. - - **Next generation**: elitism 2 + tournament 3 + 50% crossover / 50% mutation. -5. **Persistence SQLite**: ogni genome, evaluation, cost_record, adversarial_finding, generation summary persistito con indici per query rapide della dashboard. - -### 1.4 Caveat metodologici noti - -- **In-sample**: il backtest in Phase 1 lean spike non usa walk-forward; tutto il range 2024-2026 viene usato sia per la generazione delle ipotesi sia per la loro valutazione. La sopravvivenza out-of-sample è esplicitamente fuori scope di Phase 1 (gate Phase 2 #2). -- **Compiler con indicatori built-in**: il compiler JSON-based (`src/multi_swarm/protocol/compiler.py`) calcola RSI, SMA, ATR, MACD, realized_vol localmente con pandas. `CerberoTools` è plumbed ma non chiamato durante l'esecuzione delle strategie — è disponibile per agenti future-tense ma il fitness Phase 1 dipende solo dagli indicatori locali. -- **RSI epsilon-floor**: il compiler ha un epsilon sul `roll_down` per evitare RSI=100 esatto su serie monotonicamente crescenti (artefatto matematico irrilevante su dati reali ma documentato). -- **Top-1 strategia con DSR marginale**: vedi sez. 3. - ---- - -## 2. Loop convergence - -### 2.1 Fitness per generazione - -| Gen | Median | Max | P90 | Entropy | -|---|---|---|---|---| -| 0 | 0.0001 | 0.0601 | 0.0165 | 0.588 | -| 1 | 0.0042 | 0.1893 | 0.0731 | 1.261 | -| 2 | 0.0188 | 0.3347 | 0.2039 | 1.333 | -| 3 | 0.0069 | 0.3347 | 0.3347 | 1.347 | -| 4 | 0.0910 | 0.3347 | 0.3347 | 1.415 | -| 5 | 0.0016 | 0.3347 | 0.3347 | 0.611 | -| 6 | 0.0040 | 0.3347 | 0.3347 | 0.886 | -| 7 | 0.0151 | 0.3347 | 0.3347 | 0.982 | -| 8 | 0.0066 | 0.3347 | 0.3347 | 0.746 | -| 9 | 0.0061 | 0.3347 | 0.3347 | 0.914 | - -### 2.2 Lettura - -**Convergenza tre-step iniziale**: gen 0→1→2 mostra crescita mediana 4x-50x (0.0001 → 0.0042 → 0.0188) e crescita max 3x-6x (0.06 → 0.19 → 0.33). Gate 1 PASS su questa finestra. - -**Plateau dell'elite da gen 2**: max stabile a 0.3347 per le restanti 7 generazioni — comportamento atteso con `elite_k=2` che preserva il top performer attraverso le generazioni. P90 si allinea al max da gen 3, segno che almeno 2 elite mantengono la top fitness. - -**Median oscillante**: dopo il picco a gen 4 (0.091), la median fluttua fra 0.0016 e 0.0151 nelle generazioni successive. Causa: turnover stocastico della popolazione (mutation + crossover) introduce genomi nuovi, alcuni dei quali parse correctly ma falliscono Adversarial (no_trades) e si attestano a fitness 0, abbassando la median. Non è regressione strutturale del GA. - -**Entropy**: oscilla 0.6-1.4 dopo gen 0, sempre sopra soglia 0.5 → diversità di fitness preservata anche durante plateau dell'elite. - ---- - -## 3. Top-5 genomi: ispezione qualitativa - -| Rank | Genome ID | Gen | Style | Fitness | DSR | Sharpe | Max DD | Trades | Temp | -|---|---|---|---|---|---|---|---|---|---| -| 1 | `696052b8...` | 2 | physicist | 0.3347 | 0.0021 | 0.381 | 0.0215 | 33 | 0.68 | -| 2 | `169376a2...` | 1 | engineer | 0.3347 | 0.0021 | 0.381 | 0.0215 | 33 | 0.78 | -| 3 | `eb0265ad...` | 3 | ecologist | 0.2453 | 0.0006 | −0.019 | 0.0011 | 1 | 1.14 | -| 4 | `38d4c1d9...` | 1 | engineer | 0.1893 | 0.0001 | −0.245 | 0.0028 | 1 | 0.82 | -| 5 | `3e355975...` | 1 | physicist | 0.1893 | 0.0001 | −0.245 | 0.0028 | 1 | 0.78 | - -### 3.1 Top-1 strategia (ispezione approfondita) - -**System prompt** (engineer): *"Cerca segnali con rapporto S/N favorevole, filtri causali, robustezza a perturbazioni di calibrazione."* - -**Strategia JSON** (3 regole, evaluation in ordine): - -- **LONG**: `SMA(10) crossover SMA(30)` AND `realized_vol(20) > 0.3%` AND `RSI(14) < 45`. -- **SHORT**: `SMA(10) crossunder SMA(30)` AND `realized_vol(20) > 0.3%` AND `RSI(14) > 55`. -- **EXIT**: (`RSI(14) > 70` AND `close crossover SMA(50)`) OR `realized_vol(20) < 0.1%`. - -**Lettura economica**: trend-following SMA-cross fast/slow modulato da filtro volatilità (entra solo quando il regime è abbastanza mosso, esce quando è troppo calmo) e filtro RSI come momentum confirmation (long solo se non già ipercomprato; short solo se non già ipervenduto). L'EXIT è sofisticato: esce su overbought confermato da break sopra MA50, OPPURE su collasso di volatilità. - -**Performance**: 33 trade su 17545 candele (1 trade ogni 532 candele = 1 ogni 22 giorni). Sharpe positivo modesto, max drawdown 2.15% (basso). DSR praticamente zero (0.0021) — il segnale non è statisticamente significativo dopo correzione multiple testing, perché 33 trade su 2 anni è sample piccolo. - -**Plausibilità**: pattern economicamente sensato, non casuale. Reminiscente di strategie trend-following classiche (Donchian, turtle-style) con filtri di regime. Lo stile cognitivo "engineer" (S/N favorable, filtri causali) si riflette nella struttura. - -### 3.2 Top-2/3/4/5 brevemente - -- Top-2 è una replica funzionale di Top-1 con metriche identiche. Plausibile elite duplicato o convergenza indipendente sulla stessa strategia (verifica per Phase 2: signal correlation fra duplicati). -- Top-3, 4, 5 hanno **1 trade ciascuno** su 2 anni. Sono "lucky shot": una posizione tenuta a lungo che casualmente termina con leggera vincita. Adversarial flagga MEDIUM `undertrading` ma non HIGH, quindi sopravvivono. La fitness function continua dà loro valore non-zero perché `tanh(sharpe)` è leggermente sopra 0.5 e penalty drawdown è quasi 1.0 (max_dd <0.5%). - -### 3.3 Ratio top-1 / median - -Median fitness su 98 evals: 0.0003. -Top-1 fitness: 0.3347. -**Ratio**: 1116x — Gate 3 soddisfatto con margine drammatico (soglia 1.5x). - ---- - -## 4. Parser failure modes - -### 4.1 Statistiche aggregate v5 - -- Evaluations totali: 98 -- Parse success: **98 (100.0%)** -- Parse failure: **0 (0.0%)** - -### 4.2 Confronto con iterazioni precedenti - -| Run | Grammar | Parse success | Note | -|---|---|---|---| -| v1 | S-expression | 33% | LLM nesta indicators non supportati | -| v4 | S-expression (con arity check post-fix) | 36% | 89 di 98 errori = `indicator nested` | -| v5 | **JSON Schema** | **100%** | Refactor commit `44eb643` | - -Il salto da 36% a 100% deriva interamente dal cambio di grammar. JSON è natively supported dal training dei modelli LLM moderni; S-expression è esotica e induce hallucination di sintassi creative. - -### 4.3 Retry-with-feedback (commit `d4fcb42`) - -Il sistema accetta 1 retry con error feedback. Nel run v5 il retry **non è mai stato usato** (zero retry per parse, dato il 100% di success). Il retry rimane comunque architetturalmente presente per Phase 2 / casi edge. - ---- - -## 5. Costi reali vs preventivo - -### 5.1 Breakdown costi LLM v5 - -| Tier | Calls | Input tokens | Output tokens | Cost USD | -|---|---|---|---|---| -| C (qwen3-235b) | 113 | 112369 | 60060 | $0.069 | - -### 5.2 Costo cumulativo Phase 1 (5 run, inclusi bug-fix iterations) - -| Run | Cost | Note | -|---|---|---| -| v1 (aborted) | $0.034 | 67% parse_error, max_dd bug | -| v2 (aborted) | $0.018 | macd 3 args, OHLCV cap discovery | -| v3 (aborted) | $0.015 | crash su indicator arity | -| v4 (completed FAIL) | $0.057 | 36% parse, fitness tutti 0 | -| v5 (completed PASS) | $0.069 | tutti gate passati | -| **Totale Phase 1** | **$0.193** | — | - -### 5.3 Confronto con preventivo - -- Preventivo originale (basato su pricing Anthropic Sonnet): $500-700. -- Spesa reale Phase 1 totale: **$0.19**. -- Deviazione: −99.97%. - -La differenza non è dovuta a underuse — il run v5 ha fatto 113 chiamate LLM = full saturazione del budget previsto di calls. È un cambio di ordine di grandezza nei prezzi dovuto al pricing aggressivo di OpenRouter per modelli open-weights (qwen3-235b è 7.5x più economico di Sonnet su input, 37x su output). Il preventivo originale era calibrato su Sonnet 4.6. - -### 5.4 Implicazioni per Phase 2 - -Il margine economico permette di pianificare Phase 2 con maggiore aggressività senza superare il cap ($700-1100): -- K=40 (×2), gen=15 (×1.5), tier mix 30% B / 70% C, ablation runs multiple. -- Estrapolazione lineare conservativa: $0.07 × 2 × 1.5 × ~3 (tier B factor) × 5 (ablation) = ~$3 totali. Possibile spingere a $30-50 senza preoccupazioni se serve per ablation più ricche. - -**Rischio cost-trap inverso**: tentazione di sovra-dimensionare Phase 2 perché "tanto costa nulla". Mantenere disciplina budget invariata — investire i $700 cap in PIÙ ablation, non in run più grandi. - ---- - -## 6. Diversity metrics - -### 6.1 Entropy fitness per generazione - -Vedi tabella sez. 2.1 colonna entropy. Mai sotto 0.5, picco a gen 4 (1.415). - -### 6.2 Cognitive style sopravvissuti gen 9 - -| Stile | Count gen 9 | Avg fitness | Note | -|---|---|---|---| -| engineer | 3 | 0.0 | Dominante numericamente ma fitness 0 (genomi recent, non valutati su elite) | -| physicist | 1 | 0.0598 | Solo presente nel top-K | -| historian | 1 | 0.0002 | — | -| biologist | 0 | — | Estinto | -| meteorologist | 0 | — | Estinto | -| ecologist | 0 | — | Estinto | - -**Lettura**: pressione selettiva ha eliminato 3 di 6 stili cognitivi alla generazione finale. Engineer è dominante numericamente, physicist domina nel valore (l'unico con fitness >0 della popolazione "live" gen 9). Phase 2 deve introdurre speciation esplicita per evitare questo collasso (minimum 2-3 specie protette). - -### 6.3 Trade distribution sui 98 evals - -| Categoria | n | % | -|---|---|---| -| Zero trade (HIGH no_trades, kill) | 42 | 42.9% | -| Undertrading (1-4 trade, MEDIUM) | 5 | 5.1% | -| Normal (5-100 trade) | 9 | 9.2% | -| Overtrading (>100 trade, NON flaggato) | 42 | 42.9% | - -**Issue identificato**: il 42.9% di overtrading non viene catturato dall'Adversarial perché la soglia attuale è `n_trades > n_bars/5 = 3509` — troppo alta per essere triggerata su 1000-2000 trade. Phase 2 dovrebbe abbassare a `n_bars/20 = 877` o usare metrica relativa al regime. - -### 6.4 Adversarial findings totali - -| Finding | Severity | Count | -|---|---|---| -| no_trades | HIGH | 42 | -| undertrading | MEDIUM | 5 | - -Niente `degenerate` né `overtrading` flaggato. Il primo è raro (richiede strategia sempre-LONG o sempre-SHORT puro), il secondo soffre della soglia troppo alta. - ---- - -## 7. Threats to validity - -Lista esplicita dei limiti metodologici da non sovra-interpretare: - -1. **In-sample fitting**: tutto il backtest è in-sample. Il top-1 ha Sharpe 0.38 ottenuto guardando i dati su cui è stato selezionato. Phase 2 (walk-forward + hold-out Q1-Q2 2026 intoccabile) misura overfitting reale. -2. **Tier C unico**: nessun confronto contro tier B/S. Possibile underperformance del LLM economico vs Sonnet/Opus. Phase 2 introduce ablation multi-tier. -3. **Adversarial hand-crafted**: 4 check euristici (no_trades, degenerate, overtrading, undertrading). Phase 2 introduce 5 prompt LLM-driven dedicati (data snooping, lookahead, regime fragility, crowding, transaction cost erosion). -4. **Fitness function v1**: lineare in DSR + tanh(Sharpe) normalizzato + drawdown moltiplicativa. Non multi-livello (per-team, anti-collusion). Phase 2 introduce. -5. **No speciation, no novelty bonus**: cognitive style scendono da 6 a 3 a gen 9. Phase 2 deve mitigare. -6. **DSR del top-1 = 0.0021**: il "successo" del Gate 3 è guidato da Sharpe (positivo modesto), non da significatività statistica vera. Senza walk-forward + multiple testing rigoroso, non si può affermare alpha edge. -7. **Top-3/4/5 sono "lucky shot" 1-trade**: la fitness function continua li promuove perché drawdown bassissimo + sharpe leggermente negativo, ma sono artefatti. Phase 2 promuove undertrading a HIGH se `n_trades < 10`. -8. **Cerbero/Deribit data quality**: nessuna detection di gap, outlier, exchange downtime. Da affrontare prima di forward-test (Phase 3). -9. **Cost predictability inverso**: Phase 2 deve resistere alla tentazione di sovra-dimensionare perché Phase 1 è costata $0.19. - ---- - -## 8. Conclusioni e implicazioni per Phase 2 - -**Hard gate sintesi**: ✅ 5 su 5 passati. - -**Decisione finale**: **GO Phase 2** (formalizzata nel decision memo). - -**Apprendimenti chiave per Phase 2**: - -1. **JSON >> S-expression** per grammar LLM-generated. Phase 2 non rivisita. -2. **Fitness continua è essenziale** per dare gradient al GA, ma può promuovere strategie degeneri (1-trade) che vanno killate diversamente. -3. **OpenRouter qwen3-235b** è sorprendentemente capace per generare strategie strutturate, dato un prompt schema-rigoroso. Tier B (Sonnet) potrebbe non essere necessario al 30% come pianificato; ablation Phase 2 misurerà il vero contributo. -4. **Cerbero MCP come single source of truth** funziona: paginazione, cache parquet, audit log integrati senza fragility. -5. **Bug-fix discovery via run reale** è efficiente: 4 cicli, ognuno ha esposto un problema specifico (max_dd math, macd arity, validator arity, fitness clamp, grammar choice). Phase 2 può aspettarsi pattern simile per nuove componenti (speciation edge cases, OOS overfitting, multi-tier dispatch). - -**Riusabilità del codebase Phase 1**: il design modulare (data, backtest, metrics, cerbero, protocol, genome, llm, agents, ga, persistence, orchestrator, dashboard) è riusabile direttamente. Estensioni Phase 2: -- `ga/speciation.py` (nuovo) — clustering cosine similarity prompt, quota tournament per specie. -- `ga/fitness.py` — versione v2 con novelty bonus + per-team aggregation. -- `orchestrator/run.py` — integrazione walk-forward. -- `agents/adversarial_llm.py` (nuovo) — 5 prompt LLM-driven. -- `baseline/random_forest.py` (nuovo) — RF baseline per benchmark. - -**Costo stimato Phase 2**: $3-15 (estrapolazione molto conservativa). Cap rimane $700-1100 invariato per disciplina. - -**Tempo stimato Phase 2**: 4-6 settimane di lavoro calendar, includendo i 3 aggiustamenti del decision memo (Adversarial soglie, speciation, walk-forward). - ---- - -*Documento finalizzato 10 maggio 2026. Versione 1.0.* diff --git a/docs/superpowers/plans/2026-05-09-phase1-lean-spike.md b/docs/superpowers/plans/2026-05-09-phase1-lean-spike.md deleted file mode 100644 index 7bad9c7..0000000 --- a/docs/superpowers/plans/2026-05-09-phase1-lean-spike.md +++ /dev/null @@ -1,5282 +0,0 @@ -# Phase 1 — Lean Spike Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Costruire il loop end-to-end del PoC Multi-Swarm Coevolutivo (Hypothesis swarm K=20 + Falsification + Adversarial hand-crafted, GA con tournament selection, backtest event-driven, fitness v0 DSR) e validare i 5 hard gate di Phase 1 definiti nello spec. - -**Architecture:** Python single-package `multi_swarm` con submoduli per responsabilità (data, backtest, metrics, cerbero, protocol, genome, llm, agents, ga, persistence, orchestrator, dashboard). Esecuzione sincrona single-thread, persistence SQLite, dataset cached in Parquet, GUI Streamlit multipage. Niente parallelismo in Phase 1 — performance non è obiettivo, validazione del loop sì. - -**Tech Stack:** Python 3.13 + uv; pytest+pytest-mock+responses per testing; ccxt per OHLCV; pydantic v2 per config; sqlite3+sqlmodel per persistence; sexpdata per S-expression parsing; pandas+numpy+scipy per analytics; anthropic + openai SDK (OpenAI SDK punta a OpenRouter per tier C); streamlit + plotly per dashboard. - -**Spec di riferimento:** `docs/superpowers/specs/2026-05-09-decisione-strategica-design.md` (sezione 4). - -**Convenzioni:** -- TDD su tutto il codice di logica. Test prima, implementazione minima, refactoring. -- Commit frequenti, uno per task completato (a volte uno per step se ha senso). -- Branch: `main`. Niente feature branch in Phase 1, troppo overhead per PoC singolo autore. -- Commit message: `feat:` `test:` `chore:` `fix:` `docs:` `refactor:` prefix. -- Nessun mock di Cerbero in test integrazione: usare istanza locale Docker (testnet token). -- Nessun mock di LLM in test e2e: chiamate reali a Qwen via OpenRouter, ma con popolazione 5 e generazioni 2 per contenere costi. - ---- - -## Task 1: Project skeleton e tooling - -**Files:** -- Create: `pyproject.toml` -- Create: `.env.example` -- Create: `README.md` -- Create: `src/multi_swarm/__init__.py` -- Create: `tests/__init__.py` - -- [ ] **Step 1: Creare `pyproject.toml`** - -```toml -[project] -name = "multi-swarm" -version = "0.1.0" -description = "Multi-Swarm Coevolutive PoC trading swarm — Phase 1 lean spike" -authors = [{ name = "Adriano Dal Pastro", email = "adrianodalpastro@tielogic.com" }] -requires-python = ">=3.13" -dependencies = [ - "ccxt>=4.4", - "pandas>=2.2", - "numpy>=2.1", - "scipy>=1.14", - "pydantic>=2.9", - "pydantic-settings>=2.6", - "sqlmodel>=0.0.22", - "sexpdata>=1.0.2", - "anthropic>=0.39", - "openai>=1.55", - "httpx>=0.28", - "tenacity>=9.0", - "pyyaml>=6.0", - "streamlit>=1.40", - "plotly>=5.24", - "pyarrow>=18.0", -] - -[dependency-groups] -dev = [ - "pytest>=8.3", - "pytest-mock>=3.14", - "pytest-asyncio>=0.24", - "responses>=0.25", - "ruff>=0.7", - "mypy>=1.13", -] - -[build-system] -requires = ["hatchling"] -build-backend = "hatchling.build" - -[tool.hatch.build.targets.wheel] -packages = ["src/multi_swarm"] - -[tool.ruff] -line-length = 100 -target-version = "py313" - -[tool.ruff.lint] -select = ["E", "F", "W", "I", "N", "UP", "B", "RUF"] - -[tool.mypy] -python_version = "3.13" -strict = true - -[tool.pytest.ini_options] -testpaths = ["tests"] -addopts = "-v --tb=short" -markers = [ - "integration: tests that require external services (Cerbero, LLM API)", - "slow: tests that take more than 5 seconds", -] -``` - -- [ ] **Step 2: Creare `.env.example`** - -```bash -# Cerbero MCP (locale durante Phase 1) -CERBERO_BASE_URL=http://localhost:9000 -CERBERO_TESTNET_TOKEN= -CERBERO_MAINNET_TOKEN= -CERBERO_BOT_TAG=swarm-poc-phase1 - -# LLM providers -OPENROUTER_API_KEY= -ANTHROPIC_API_KEY= - -# Run config -RUN_NAME=phase1-spike-001 -DATA_DIR=./data -SERIES_DIR=./series -DB_PATH=./runs.db -``` - -- [ ] **Step 3: Creare `README.md` minimale** - -```markdown -# Multi_Swarm_Coevolutive — Phase 1 - -Lean spike del PoC. Vedi `docs/superpowers/specs/2026-05-09-decisione-strategica-design.md` -per il razionale e `docs/superpowers/plans/2026-05-09-phase1-lean-spike.md` per il -piano implementativo. - -## Setup - -```bash -uv sync -cp .env.example .env # compilare token e API key -uv run pytest # verifica che tutto installi -``` - -## Cerbero locale - -Phase 1 backtest legge dataset OHLCV cached, ma alcune feature di indicatore -sono delegate a Cerbero. Avviare Cerbero locale prima di eseguire un run: - -```bash -cd /home/adriano/Documenti/Git_XYZ/CerberoSuite/Cerbero_mcp -docker compose up -d -``` - -## Comandi principali - -```bash -uv run pytest # tutti i test -uv run pytest tests/unit -v # solo unit -uv run pytest tests/integration -v -m integration # solo integration -uv run python scripts/run_phase1.py # run completo Phase 1 -uv run streamlit run src/multi_swarm/dashboard/streamlit_app.py -``` -``` - -- [ ] **Step 4: Creare `src/multi_swarm/__init__.py` e `tests/__init__.py`** - -```python -# src/multi_swarm/__init__.py -"""Multi_Swarm_Coevolutive — Phase 1 lean spike.""" - -__version__ = "0.1.0" -``` - -```python -# tests/__init__.py -``` - -- [ ] **Step 5: Sync dipendenze e verifica installazione** - -Run: `uv sync && uv run python -c "import multi_swarm; print(multi_swarm.__version__)"` -Expected: stampa `0.1.0` senza errori. - -- [ ] **Step 6: Commit** - -```bash -git add pyproject.toml .env.example README.md src/multi_swarm/__init__.py tests/__init__.py uv.lock -git commit -m "chore: project skeleton with uv + pyproject + deps" -``` - ---- - -## Task 2: Config loader (Pydantic settings) - -**Files:** -- Create: `src/multi_swarm/config.py` -- Test: `tests/unit/test_config.py` - -- [ ] **Step 1: Scrivere il test fallente** - -```python -# tests/unit/test_config.py -import os -from multi_swarm.config import Settings - - -def test_settings_loads_from_env(monkeypatch): - monkeypatch.setenv("CERBERO_BASE_URL", "http://test:9000") - 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("ANTHROPIC_API_KEY", "an-key") - monkeypatch.setenv("RUN_NAME", "test-run") - - s = Settings() - - assert s.cerbero_base_url == "http://test:9000" - assert s.cerbero_testnet_token == "tok-test" - assert s.run_name == "test-run" - assert s.data_dir.name == "data" - assert s.db_path.name == "runs.db" - - -def test_settings_requires_tokens(monkeypatch): - monkeypatch.delenv("CERBERO_TESTNET_TOKEN", raising=False) - monkeypatch.delenv("OPENROUTER_API_KEY", raising=False) - import pytest - from pydantic import ValidationError - - with pytest.raises(ValidationError): - Settings() -``` - -- [ ] **Step 2: Run test (deve fallire)** - -Run: `uv run pytest tests/unit/test_config.py -v` -Expected: FAIL — `ModuleNotFoundError: multi_swarm.config`. - -- [ ] **Step 3: Implementare `Settings`** - -```python -# src/multi_swarm/config.py -from pathlib import Path -from pydantic import Field, SecretStr -from pydantic_settings import BaseSettings, SettingsConfigDict - - -class Settings(BaseSettings): - model_config = SettingsConfigDict( - env_file=".env", - env_file_encoding="utf-8", - extra="ignore", - case_sensitive=False, - ) - - cerbero_base_url: str = "http://localhost:9000" - cerbero_testnet_token: SecretStr - cerbero_mainnet_token: SecretStr | None = None - cerbero_bot_tag: str = "swarm-poc-phase1" - - openrouter_api_key: SecretStr - anthropic_api_key: SecretStr | None = None - - run_name: str = "phase1-spike-001" - data_dir: Path = Field(default=Path("./data")) - series_dir: Path = Field(default=Path("./series")) - db_path: Path = Field(default=Path("./runs.db")) - - -def load_settings() -> Settings: - return Settings() -``` - -- [ ] **Step 4: Run test (deve passare)** - -Run: `uv run pytest tests/unit/test_config.py -v` -Expected: PASS entrambi. - -- [ ] **Step 5: Commit** - -```bash -git add src/multi_swarm/config.py tests/unit/test_config.py tests/unit/__init__.py -git commit -m "feat(config): pydantic settings loader from .env" -``` - ---- - -## Task 3: OHLCV loader (ccxt → parquet cache) - -**Files:** -- Create: `src/multi_swarm/data/__init__.py` -- Create: `src/multi_swarm/data/ohlcv_loader.py` -- Test: `tests/unit/test_ohlcv_loader.py` - -- [ ] **Step 1: Scrivere test fallente con mock ccxt** - -```python -# tests/unit/test_ohlcv_loader.py -from datetime import datetime, timezone -from pathlib import Path -import pandas as pd -import pytest -from multi_swarm.data.ohlcv_loader import OHLCVLoader, OHLCVRequest - - -@pytest.fixture -def sample_ohlcv_rows(): - base_ts = int(datetime(2024, 1, 1, tzinfo=timezone.utc).timestamp() * 1000) - rows = [] - for i in range(48): - rows.append([base_ts + i * 3600 * 1000, 40000 + i, 40100 + i, 39900 + i, 40050 + i, 100.0 + i]) - return rows - - -def test_loader_fetches_and_caches(tmp_path: Path, mocker, sample_ohlcv_rows): - fake_exchange = mocker.MagicMock() - fake_exchange.fetch_ohlcv.return_value = sample_ohlcv_rows - mocker.patch("multi_swarm.data.ohlcv_loader.ccxt.binance", return_value=fake_exchange) - - loader = OHLCVLoader(cache_dir=tmp_path) - req = OHLCVRequest( - symbol="BTC/USDT", - timeframe="1h", - start=datetime(2024, 1, 1, tzinfo=timezone.utc), - end=datetime(2024, 1, 3, tzinfo=timezone.utc), - ) - df = loader.load(req) - - assert isinstance(df, pd.DataFrame) - assert list(df.columns) == ["open", "high", "low", "close", "volume"] - assert len(df) == 48 - assert df.index.is_monotonic_increasing - cache_files = list(tmp_path.glob("*.parquet")) - assert len(cache_files) == 1 - - -def test_loader_uses_cache_on_second_call(tmp_path: Path, mocker, sample_ohlcv_rows): - fake_exchange = mocker.MagicMock() - fake_exchange.fetch_ohlcv.return_value = sample_ohlcv_rows - mocker.patch("multi_swarm.data.ohlcv_loader.ccxt.binance", return_value=fake_exchange) - - loader = OHLCVLoader(cache_dir=tmp_path) - req = OHLCVRequest( - symbol="BTC/USDT", - timeframe="1h", - start=datetime(2024, 1, 1, tzinfo=timezone.utc), - end=datetime(2024, 1, 3, tzinfo=timezone.utc), - ) - df1 = loader.load(req) - df2 = loader.load(req) - - assert fake_exchange.fetch_ohlcv.call_count == 2 # paginazione interna, non caching - pd.testing.assert_frame_equal(df1, df2) - # Seconda chiamata legge da cache, non chiama exchange - fake_exchange.fetch_ohlcv.reset_mock() - df3 = loader.load(req) - assert fake_exchange.fetch_ohlcv.call_count == 0 - pd.testing.assert_frame_equal(df1, df3) -``` - -- [ ] **Step 2: Run test (deve fallire)** - -Run: `uv run pytest tests/unit/test_ohlcv_loader.py -v` -Expected: FAIL — modulo non esistente. - -- [ ] **Step 3: Implementare `OHLCVLoader`** - -```python -# src/multi_swarm/data/__init__.py -``` - -```python -# src/multi_swarm/data/ohlcv_loader.py -from __future__ import annotations - -import hashlib -from dataclasses import dataclass -from datetime import datetime, timezone -from pathlib import Path - -import ccxt -import pandas as pd - - -@dataclass(frozen=True) -class OHLCVRequest: - symbol: str - timeframe: str - start: datetime - end: datetime - - def cache_key(self) -> str: - s = f"{self.symbol}|{self.timeframe}|{self.start.isoformat()}|{self.end.isoformat()}" - return hashlib.sha1(s.encode()).hexdigest()[:16] - - -class OHLCVLoader: - """Carica OHLCV via ccxt (Binance) e cachea in parquet.""" - - def __init__(self, cache_dir: Path, exchange_name: str = "binance"): - self.cache_dir = Path(cache_dir) - self.cache_dir.mkdir(parents=True, exist_ok=True) - self.exchange_name = exchange_name - - def load(self, req: OHLCVRequest) -> pd.DataFrame: - cache_file = self.cache_dir / f"{req.cache_key()}.parquet" - if cache_file.exists(): - return pd.read_parquet(cache_file) - - df = self._fetch_paginated(req) - df.to_parquet(cache_file) - return df - - def _fetch_paginated(self, req: OHLCVRequest) -> pd.DataFrame: - exchange = getattr(ccxt, self.exchange_name)({"enableRateLimit": True}) - timeframe_ms = exchange.parse_timeframe(req.timeframe) * 1000 - since = int(req.start.timestamp() * 1000) - end_ms = int(req.end.timestamp() * 1000) - all_rows: list[list[float]] = [] - limit = 1000 - - while since < end_ms: - rows = exchange.fetch_ohlcv(req.symbol, req.timeframe, since=since, limit=limit) - if not rows: - break - all_rows.extend(rows) - last_ts = rows[-1][0] - if last_ts <= since: - break - since = last_ts + timeframe_ms - if len(rows) < limit: - break - - df = pd.DataFrame(all_rows, columns=["ts", "open", "high", "low", "close", "volume"]) - df = df.drop_duplicates(subset=["ts"]).sort_values("ts") - df["ts"] = pd.to_datetime(df["ts"], unit="ms", utc=True) - df = df.set_index("ts") - df = df[(df.index >= req.start) & (df.index < req.end)] - return df[["open", "high", "low", "close", "volume"]].astype("float64") -``` - -- [ ] **Step 4: Run test (deve passare)** - -Run: `uv run pytest tests/unit/test_ohlcv_loader.py -v` -Expected: PASS entrambi. - -- [ ] **Step 5: Commit** - -```bash -git add src/multi_swarm/data/ tests/unit/test_ohlcv_loader.py -git commit -m "feat(data): OHLCV loader via ccxt with parquet cache" -``` - ---- - -## Task 4: Walk-forward expanding splits - -**Files:** -- Create: `src/multi_swarm/data/splits.py` -- Test: `tests/unit/test_splits.py` - -- [ ] **Step 1: Scrivere test fallente** - -```python -# tests/unit/test_splits.py -from datetime import datetime, timezone, timedelta -import pandas as pd -import pytest -from multi_swarm.data.splits import expanding_walk_forward, Split - - -@pytest.fixture -def daily_index(): - return pd.date_range("2024-01-01", "2024-12-31", freq="D", tz="UTC") - - -def test_expanding_split_count(daily_index: pd.DatetimeIndex): - splits = expanding_walk_forward( - daily_index, train_ratio=0.7, n_folds=4, min_train_days=30 - ) - assert len(splits) == 4 - - -def test_expanding_split_train_grows(daily_index: pd.DatetimeIndex): - splits = expanding_walk_forward( - daily_index, train_ratio=0.7, n_folds=4, min_train_days=30 - ) - train_lengths = [len(s.train_idx) for s in splits] - assert train_lengths == sorted(train_lengths) - assert train_lengths[0] < train_lengths[-1] - - -def test_no_overlap_train_test(daily_index: pd.DatetimeIndex): - splits = expanding_walk_forward( - daily_index, train_ratio=0.7, n_folds=4, min_train_days=30 - ) - for s in splits: - assert s.train_idx[-1] < s.test_idx[0] - - -def test_min_train_days_respected(): - idx = pd.date_range("2024-01-01", "2024-02-15", freq="D", tz="UTC") - splits = expanding_walk_forward(idx, train_ratio=0.7, n_folds=2, min_train_days=20) - for s in splits: - assert len(s.train_idx) >= 20 -``` - -- [ ] **Step 2: Run test (deve fallire)** - -Run: `uv run pytest tests/unit/test_splits.py -v` -Expected: FAIL — modulo non esistente. - -- [ ] **Step 3: Implementare splits** - -```python -# src/multi_swarm/data/splits.py -from __future__ import annotations - -from dataclasses import dataclass - -import pandas as pd - - -@dataclass(frozen=True) -class Split: - fold: int - train_idx: pd.DatetimeIndex - test_idx: pd.DatetimeIndex - - -def expanding_walk_forward( - index: pd.DatetimeIndex, - train_ratio: float = 0.7, - n_folds: int = 4, - min_train_days: int = 30, -) -> list[Split]: - """Genera split walk-forward expanding: train cresce, test è la finestra successiva. - - Esempio con n_folds=4, train_ratio=0.7: - fold 0: train [0..a0], test [a0..a0+(end-a0)/4] - fold 1: train [0..a1], test [a1..a1+(end-a1)/4] - ... - Il train iniziale parte da train_ratio dell'intervallo totale. - """ - if n_folds < 1: - raise ValueError("n_folds must be >= 1") - if not 0 < train_ratio < 1: - raise ValueError("train_ratio must be in (0,1)") - - total = len(index) - initial_train = int(total * train_ratio) - remaining = total - initial_train - fold_size = max(1, remaining // n_folds) - - splits: list[Split] = [] - for f in range(n_folds): - train_end = initial_train + f * fold_size - test_start = train_end - test_end = min(test_start + fold_size, total) - train_idx = index[:train_end] - test_idx = index[test_start:test_end] - if len(train_idx) < min_train_days or len(test_idx) == 0: - continue - splits.append(Split(fold=f, train_idx=train_idx, test_idx=test_idx)) - - return splits -``` - -- [ ] **Step 4: Run test (deve passare)** - -Run: `uv run pytest tests/unit/test_splits.py -v` -Expected: PASS tutti e 4. - -- [ ] **Step 5: Commit** - -```bash -git add src/multi_swarm/data/splits.py tests/unit/test_splits.py -git commit -m "feat(data): expanding walk-forward splits" -``` - ---- - -## Task 5: Backtest core dataclasses (Order, Position, Trade) - -**Files:** -- Create: `src/multi_swarm/backtest/__init__.py` -- Create: `src/multi_swarm/backtest/orders.py` -- Test: `tests/unit/test_backtest_orders.py` - -- [ ] **Step 1: Scrivere test fallente** - -```python -# tests/unit/test_backtest_orders.py -from datetime import datetime, timezone -import pytest -from multi_swarm.backtest.orders import Order, Side, Position, Trade - - -def test_order_validates_side(): - o = Order(ts=datetime(2024, 1, 1, tzinfo=timezone.utc), side=Side.LONG, size=1.0) - assert o.side == Side.LONG - - -def test_position_pnl_long(): - pos = Position(side=Side.LONG, entry_price=100.0, size=2.0) - assert pos.unrealized_pnl(110.0) == pytest.approx(20.0) - assert pos.unrealized_pnl(90.0) == pytest.approx(-20.0) - - -def test_position_pnl_short(): - pos = Position(side=Side.SHORT, entry_price=100.0, size=2.0) - assert pos.unrealized_pnl(110.0) == pytest.approx(-20.0) - assert pos.unrealized_pnl(90.0) == pytest.approx(20.0) - - -def test_trade_realized_pnl_with_fees(): - t = Trade( - entry_ts=datetime(2024, 1, 1, tzinfo=timezone.utc), - exit_ts=datetime(2024, 1, 2, tzinfo=timezone.utc), - side=Side.LONG, - size=1.0, - entry_price=100.0, - exit_price=110.0, - fees_bp=5.0, - ) - # gross 10, fees = 5bp * (100+110) = 0.005 * 210 = 1.05 - assert t.gross_pnl == pytest.approx(10.0) - assert t.fees == pytest.approx(0.105) - assert t.net_pnl == pytest.approx(9.895) -``` - -- [ ] **Step 2: Run test (deve fallire)** - -Run: `uv run pytest tests/unit/test_backtest_orders.py -v` -Expected: FAIL. - -- [ ] **Step 3: Implementare orders** - -```python -# src/multi_swarm/backtest/__init__.py -``` - -```python -# src/multi_swarm/backtest/orders.py -from __future__ import annotations - -from dataclasses import dataclass -from datetime import datetime -from enum import Enum - - -class Side(str, Enum): - LONG = "long" - SHORT = "short" - FLAT = "flat" - - -@dataclass(frozen=True) -class Order: - ts: datetime - side: Side - size: float - - -@dataclass(frozen=True) -class Position: - side: Side - entry_price: float - size: float - - def unrealized_pnl(self, current_price: float) -> float: - if self.side == Side.LONG: - return (current_price - self.entry_price) * self.size - if self.side == Side.SHORT: - return (self.entry_price - current_price) * self.size - return 0.0 - - -@dataclass(frozen=True) -class Trade: - entry_ts: datetime - exit_ts: datetime - side: Side - size: float - entry_price: float - exit_price: float - fees_bp: float = 5.0 - - @property - def gross_pnl(self) -> float: - if self.side == Side.LONG: - return (self.exit_price - self.entry_price) * self.size - return (self.entry_price - self.exit_price) * self.size - - @property - def fees(self) -> float: - notional_in = self.entry_price * self.size - notional_out = self.exit_price * self.size - return (self.fees_bp / 10000.0) * (notional_in + notional_out) - - @property - def net_pnl(self) -> float: - return self.gross_pnl - self.fees -``` - -- [ ] **Step 4: Run test (deve passare)** - -Run: `uv run pytest tests/unit/test_backtest_orders.py -v` -Expected: PASS. - -- [ ] **Step 5: Commit** - -```bash -git add src/multi_swarm/backtest/ tests/unit/test_backtest_orders.py -git commit -m "feat(backtest): Order/Position/Trade dataclasses with fees" -``` - ---- - -## Task 6: Backtest engine event-driven semplificato - -**Files:** -- Create: `src/multi_swarm/backtest/engine.py` -- Test: `tests/unit/test_backtest_engine.py` - -- [ ] **Step 1: Scrivere test fallente** - -```python -# tests/unit/test_backtest_engine.py -from datetime import datetime, timezone -import numpy as np -import pandas as pd -import pytest -from multi_swarm.backtest.engine import BacktestEngine, Signal -from multi_swarm.backtest.orders import Side - - -@pytest.fixture -def trending_ohlcv(): - idx = pd.date_range("2024-01-01", periods=100, freq="1h", tz="UTC") - close = np.linspace(100, 120, 100) - df = pd.DataFrame( - {"open": close, "high": close + 0.5, "low": close - 0.5, "close": close, "volume": 1.0}, - index=idx, - ) - return df - - -def test_engine_no_signals_zero_pnl(trending_ohlcv): - signals = pd.Series([Side.FLAT] * len(trending_ohlcv), index=trending_ohlcv.index) - engine = BacktestEngine(fees_bp=5.0) - result = engine.run(trending_ohlcv, signals) - assert result.equity_curve.iloc[-1] == pytest.approx(0.0) - assert len(result.trades) == 0 - - -def test_engine_long_in_uptrend_makes_profit(trending_ohlcv): - signals = pd.Series([Side.LONG] * len(trending_ohlcv), index=trending_ohlcv.index) - engine = BacktestEngine(fees_bp=5.0) - result = engine.run(trending_ohlcv, signals) - assert result.equity_curve.iloc[-1] > 0 - assert len(result.trades) == 1 - assert result.trades[0].side == Side.LONG - - -def test_engine_position_flips_on_side_change(trending_ohlcv): - half = len(trending_ohlcv) // 2 - signals = pd.Series( - [Side.LONG] * half + [Side.SHORT] * (len(trending_ohlcv) - half), - index=trending_ohlcv.index, - ) - engine = BacktestEngine(fees_bp=5.0) - result = engine.run(trending_ohlcv, signals) - assert len(result.trades) == 2 - assert result.trades[0].side == Side.LONG - assert result.trades[1].side == Side.SHORT - - -def test_engine_fees_are_subtracted(trending_ohlcv): - signals = pd.Series([Side.LONG] * len(trending_ohlcv), index=trending_ohlcv.index) - engine_no_fees = BacktestEngine(fees_bp=0.0) - engine_fees = BacktestEngine(fees_bp=10.0) - r1 = engine_no_fees.run(trending_ohlcv, signals) - r2 = engine_fees.run(trending_ohlcv, signals) - assert r1.equity_curve.iloc[-1] > r2.equity_curve.iloc[-1] -``` - -- [ ] **Step 2: Run test (deve fallire)** - -Run: `uv run pytest tests/unit/test_backtest_engine.py -v` -Expected: FAIL. - -- [ ] **Step 3: Implementare engine** - -```python -# src/multi_swarm/backtest/engine.py -from __future__ import annotations - -from dataclasses import dataclass -from typing import Literal - -import pandas as pd - -from .orders import Position, Side, Trade - - -Signal = Side # alias semantico - - -@dataclass(frozen=True) -class BacktestResult: - equity_curve: pd.Series - returns: pd.Series - trades: list[Trade] - - -class BacktestEngine: - """Engine event-driven sincrono: itera bar per bar, applica segnali con - delay di 1 bar (segnale a t → eseguito a t+1 open) per evitare lookahead. - - Position sizing: 1 unit per posizione. Fees applicati su entry+exit. - Niente leva, niente liquidation, niente funding (semplificazione Phase 1). - """ - - def __init__(self, fees_bp: float = 5.0): - self.fees_bp = fees_bp - - def run(self, ohlcv: pd.DataFrame, signals: pd.Series) -> BacktestResult: - signals = signals.reindex(ohlcv.index).ffill().fillna(Side.FLAT) - position: Position | None = None - trades: list[Trade] = [] - equity = 0.0 - equity_history: list[float] = [] - returns_history: list[float] = [] - prev_equity = 0.0 - - # Esecuzione con delay 1: segnale a t-1 esegue a open di t. - executed_side = pd.Series(Side.FLAT, index=ohlcv.index) - executed_side.iloc[1:] = signals.iloc[:-1].values - - 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: - trade = Trade( - entry_ts=position_entry_ts, - exit_ts=ts, - side=position.side, - size=position.size, - entry_price=position.entry_price, - exit_price=row["open"], - fees_bp=self.fees_bp, - ) - trades.append(trade) - equity += trade.net_pnl - position = None - if target_side in (Side.LONG, Side.SHORT): - position = Position(side=target_side, entry_price=row["open"], size=1.0) - position_entry_ts = ts - - mark = 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: - last_ts = ohlcv.index[-1] - last_close = ohlcv["close"].iloc[-1] - trade = Trade( - entry_ts=position_entry_ts, - exit_ts=last_ts, - side=position.side, - size=position.size, - entry_price=position.entry_price, - exit_price=last_close, - fees_bp=self.fees_bp, - ) - trades.append(trade) - equity += trade.net_pnl - equity_history[-1] = equity - if len(returns_history) >= 2: - returns_history[-1] = equity - equity_history[-2] - - return BacktestResult( - equity_curve=pd.Series(equity_history, index=ohlcv.index, name="equity"), - returns=pd.Series(returns_history, index=ohlcv.index, name="returns"), - trades=trades, - ) -``` - -- [ ] **Step 4: Run test (deve passare)** - -Run: `uv run pytest tests/unit/test_backtest_engine.py -v` -Expected: PASS tutti e 4. - -- [ ] **Step 5: Commit** - -```bash -git add src/multi_swarm/backtest/engine.py tests/unit/test_backtest_engine.py -git commit -m "feat(backtest): event-driven engine with 1-bar exec delay" -``` - ---- - -## Task 7: Metrics base (Sharpe, drawdown, returns) - -**Files:** -- Create: `src/multi_swarm/metrics/__init__.py` -- Create: `src/multi_swarm/metrics/basic.py` -- Test: `tests/unit/test_metrics_basic.py` - -- [ ] **Step 1: Scrivere test fallente** - -```python -# tests/unit/test_metrics_basic.py -import numpy as np -import pandas as pd -import pytest -from multi_swarm.metrics.basic import sharpe_ratio, max_drawdown, total_return - - -def test_sharpe_zero_returns(): - r = pd.Series([0.0] * 100) - assert sharpe_ratio(r, periods_per_year=8760) == 0.0 - - -def test_sharpe_positive_returns(): - np.random.seed(42) - r = pd.Series(np.random.normal(0.001, 0.01, 1000)) - s = sharpe_ratio(r, periods_per_year=8760) - assert s > 0 - - -def test_sharpe_negative_returns(): - np.random.seed(42) - r = pd.Series(np.random.normal(-0.001, 0.01, 1000)) - s = sharpe_ratio(r, periods_per_year=8760) - assert s < 0 - - -def test_max_drawdown_monotonic_up(): - eq = pd.Series([100.0, 105.0, 110.0, 115.0, 120.0]) - assert max_drawdown(eq) == pytest.approx(0.0) - - -def test_max_drawdown_known_curve(): - eq = pd.Series([100.0, 110.0, 90.0, 95.0, 105.0]) - # peak 110, trough 90, drawdown = (110-90)/110 ≈ 0.1818 - assert max_drawdown(eq) == pytest.approx(20.0 / 110.0) - - -def test_total_return(): - eq = pd.Series([100.0, 110.0, 105.0, 120.0]) - assert total_return(eq) == pytest.approx(0.20) -``` - -- [ ] **Step 2: Run test (deve fallire)** - -Run: `uv run pytest tests/unit/test_metrics_basic.py -v` -Expected: FAIL. - -- [ ] **Step 3: Implementare metrics base** - -```python -# src/multi_swarm/metrics/__init__.py -``` - -```python -# src/multi_swarm/metrics/basic.py -from __future__ import annotations - -import numpy as np -import pandas as pd - - -def sharpe_ratio(returns: pd.Series, periods_per_year: int = 8760, rf: float = 0.0) -> float: - """Sharpe annualizzato. periods_per_year=8760 per dati orari.""" - excess = returns - rf / periods_per_year - std = excess.std(ddof=1) - if std == 0 or np.isnan(std): - return 0.0 - return float(np.sqrt(periods_per_year) * excess.mean() / std) - - -def max_drawdown(equity: pd.Series) -> float: - """Max drawdown percentuale (positivo).""" - peak = equity.cummax() - dd = (peak - equity) / peak.replace(0, np.nan) - dd = dd.fillna(0.0) - return float(dd.max()) - - -def total_return(equity: pd.Series) -> float: - if equity.iloc[0] == 0: - return float(equity.iloc[-1]) - return float(equity.iloc[-1] / equity.iloc[0] - 1.0) -``` - -- [ ] **Step 4: Run test (deve passare)** - -Run: `uv run pytest tests/unit/test_metrics_basic.py -v` -Expected: PASS tutti e 6. - -- [ ] **Step 5: Commit** - -```bash -git add src/multi_swarm/metrics/ tests/unit/test_metrics_basic.py -git commit -m "feat(metrics): Sharpe + max drawdown + total return" -``` - ---- - -## Task 8: Deflated Sharpe Ratio (Bailey & López de Prado) - -**Files:** -- Create: `src/multi_swarm/metrics/dsr.py` -- Test: `tests/unit/test_metrics_dsr.py` - -- [ ] **Step 1: Scrivere test fallente** - -```python -# tests/unit/test_metrics_dsr.py -import numpy as np -import pandas as pd -import pytest -from multi_swarm.metrics.dsr import deflated_sharpe_ratio, expected_max_sharpe - - -def test_expected_max_sharpe_grows_with_n_trials(): - e1 = expected_max_sharpe(n_trials=1, sharpe_var=1.0) - e10 = expected_max_sharpe(n_trials=10, sharpe_var=1.0) - e100 = expected_max_sharpe(n_trials=100, sharpe_var=1.0) - assert e1 < e10 < e100 - - -def test_dsr_zero_when_sharpe_equals_expected_max(): - np.random.seed(0) - returns = pd.Series(np.random.normal(0, 0.01, 500)) - dsr, p = deflated_sharpe_ratio( - returns, n_trials=10, periods_per_year=8760, sharpe_var=0.0 - ) - # Con sharpe_var=0 e Sharpe stimato vicino a 0, p-value deve essere alto. - assert 0.0 <= p <= 1.0 - - -def test_dsr_significant_for_strong_sharpe(): - np.random.seed(42) - returns = pd.Series(np.random.normal(0.005, 0.005, 1000)) - dsr, p = deflated_sharpe_ratio( - returns, n_trials=5, periods_per_year=8760, sharpe_var=1.0 - ) - # Sharpe atteso > 0 e p-value basso - assert dsr > 0 - assert p < 0.5 -``` - -- [ ] **Step 2: Run test (deve fallire)** - -Run: `uv run pytest tests/unit/test_metrics_dsr.py -v` -Expected: FAIL. - -- [ ] **Step 3: Implementare DSR** - -```python -# src/multi_swarm/metrics/dsr.py -from __future__ import annotations - -import numpy as np -import pandas as pd -from scipy import stats - -from .basic import sharpe_ratio - - -EULER_MASCHERONI = 0.5772156649015329 - - -def expected_max_sharpe(n_trials: int, sharpe_var: float) -> float: - """E[max SR] su n_trials con varianza sharpe_var (Bailey & Lopez de Prado). - - Formula: sqrt(sharpe_var) * ((1-γ) * Φ⁻¹(1 - 1/N) + γ * Φ⁻¹(1 - 1/(N·e))) - dove γ è la costante di Eulero-Mascheroni. - """ - if n_trials < 2: - return 0.0 - e = np.e - z1 = stats.norm.ppf(1 - 1.0 / n_trials) - z2 = stats.norm.ppf(1 - 1.0 / (n_trials * e)) - return float(np.sqrt(sharpe_var) * ((1 - EULER_MASCHERONI) * z1 + EULER_MASCHERONI * z2)) - - -def deflated_sharpe_ratio( - returns: pd.Series, - n_trials: int, - periods_per_year: int = 8760, - sharpe_var: float = 1.0, - skewness: float | None = None, - kurtosis: float | None = None, -) -> tuple[float, float]: - """Deflated Sharpe Ratio (DSR) e p-value associato. - - Restituisce (DSR, p_value). p_value è la prob. che lo SR osservato sia - superiore al massimo atteso sotto null. p_value bassi (es. < 0.05) - indicano significatività dopo correzione per multiple testing. - """ - n = len(returns) - if n < 30: - return 0.0, 1.0 - - sr = sharpe_ratio(returns, periods_per_year=periods_per_year) - sr_period = sr / np.sqrt(periods_per_year) - - if skewness is None: - skewness = float(stats.skew(returns, bias=False)) - if kurtosis is None: - kurtosis = float(stats.kurtosis(returns, fisher=True, bias=False)) - - sr_expected_max = expected_max_sharpe(n_trials, sharpe_var) / np.sqrt(periods_per_year) - - denom = np.sqrt( - max( - (1 - skewness * sr_period + ((kurtosis - 1) / 4.0) * sr_period**2) / (n - 1), - 1e-12, - ) - ) - z = (sr_period - sr_expected_max) / denom - p_value = float(1.0 - stats.norm.cdf(z)) - dsr = float(stats.norm.cdf(z)) - return dsr, p_value -``` - -- [ ] **Step 4: Run test (deve passare)** - -Run: `uv run pytest tests/unit/test_metrics_dsr.py -v` -Expected: PASS tutti e 3. - -- [ ] **Step 5: Commit** - -```bash -git add src/multi_swarm/metrics/dsr.py tests/unit/test_metrics_dsr.py -git commit -m "feat(metrics): Deflated Sharpe Ratio (Bailey & Lopez de Prado)" -``` - ---- - -## Task 9: Cerbero HTTP client - -**Files:** -- Create: `src/multi_swarm/cerbero/__init__.py` -- Create: `src/multi_swarm/cerbero/client.py` -- Test: `tests/unit/test_cerbero_client.py` - -- [ ] **Step 1: Scrivere test fallente con `responses`** - -```python -# tests/unit/test_cerbero_client.py -import responses -from multi_swarm.cerbero.client import CerberoClient - - -@responses.activate -def test_call_tool_passes_bearer_and_bot_tag(): - responses.add( - responses.POST, - "http://test:9000/mcp-deribit/tools/get_iv_rank", - json={"iv_rank": 0.42}, - status=200, - ) - client = CerberoClient(base_url="http://test:9000", token="tok-xyz", bot_tag="swarm-poc-phase1") - result = client.call_tool("deribit", "get_iv_rank", {"symbol": "BTC-PERPETUAL"}) - assert result == {"iv_rank": 0.42} - req = responses.calls[0].request - assert req.headers["Authorization"] == "Bearer tok-xyz" - assert req.headers["X-Bot-Tag"] == "swarm-poc-phase1" - - -@responses.activate -def test_call_tool_raises_on_error(): - responses.add( - responses.POST, - "http://test:9000/mcp-deribit/tools/get_iv_rank", - json={"error": "bad"}, - status=400, - ) - client = CerberoClient(base_url="http://test:9000", token="tok-xyz", bot_tag="swarm-poc-phase1") - import pytest - with pytest.raises(RuntimeError): - client.call_tool("deribit", "get_iv_rank", {}) -``` - -- [ ] **Step 2: Run test (deve fallire)** - -Run: `uv run pytest tests/unit/test_cerbero_client.py -v` -Expected: FAIL. - -- [ ] **Step 3: Implementare client** - -```python -# src/multi_swarm/cerbero/__init__.py -``` - -```python -# src/multi_swarm/cerbero/client.py -from __future__ import annotations - -from typing import Any - -import httpx -from tenacity import retry, retry_if_exception_type, stop_after_attempt, wait_exponential - - -class CerberoClient: - """Client HTTP minimale verso Cerbero MCP unified server.""" - - def __init__( - self, - base_url: str, - token: str, - bot_tag: str, - timeout_seconds: float = 10.0, - ): - self.base_url = base_url.rstrip("/") - self.token = token - self.bot_tag = bot_tag - self._client = httpx.Client( - timeout=timeout_seconds, - headers={ - "Authorization": f"Bearer {token}", - "X-Bot-Tag": bot_tag, - "Content-Type": "application/json", - }, - ) - - def close(self) -> None: - self._client.close() - - def __enter__(self) -> CerberoClient: - return self - - def __exit__(self, *exc: object) -> None: - self.close() - - @retry( - stop=stop_after_attempt(3), - wait=wait_exponential(multiplier=0.5, min=0.5, max=4.0), - retry=retry_if_exception_type(httpx.TransportError), - reraise=True, - ) - def call_tool(self, exchange: str, tool: str, args: dict[str, Any]) -> Any: - url = f"{self.base_url}/mcp-{exchange}/tools/{tool}" - resp = self._client.post(url, json=args) - if resp.status_code >= 400: - raise RuntimeError(f"Cerbero {exchange}/{tool} returned {resp.status_code}: {resp.text}") - return resp.json() -``` - -- [ ] **Step 4: Run test (deve passare)** - -Run: `uv run pytest tests/unit/test_cerbero_client.py -v` -Expected: PASS. - -- [ ] **Step 5: Commit** - -```bash -git add src/multi_swarm/cerbero/ tests/unit/test_cerbero_client.py -git commit -m "feat(cerbero): HTTP client with bearer + bot-tag + retry" -``` - ---- - -## Task 10: Cerbero tools wrapper (indicatori usati da Phase 1) - -**Files:** -- Create: `src/multi_swarm/cerbero/tools.py` -- Test: `tests/unit/test_cerbero_tools.py` - -In Phase 1 gli agenti possono richiedere un sottoinsieme limitato di indicatori: SMA, RSI, ATR, MACD (technical), realized_vol (volatility), funding_rate (microstructure). Il wrapper espone una funzione Python per ognuno, mascherando il dettaglio HTTP. - -- [ ] **Step 1: Scrivere test fallente** - -```python -# tests/unit/test_cerbero_tools.py -import pytest -from multi_swarm.cerbero.tools import CerberoTools - - -def test_tools_dispatch_sma(mocker): - fake_client = mocker.MagicMock() - fake_client.call_tool.return_value = {"value": 100.0} - t = CerberoTools(fake_client) - out = t.sma(exchange="bybit", symbol="BTCUSDT", timeframe="1h", length=20) - fake_client.call_tool.assert_called_once_with( - "bybit", "sma", {"symbol": "BTCUSDT", "timeframe": "1h", "length": 20} - ) - assert out == {"value": 100.0} - - -def test_tools_dispatch_rsi(mocker): - fake_client = mocker.MagicMock() - fake_client.call_tool.return_value = {"value": 55.0} - t = CerberoTools(fake_client) - out = t.rsi(exchange="bybit", symbol="BTCUSDT", timeframe="1h", length=14) - fake_client.call_tool.assert_called_once_with( - "bybit", "rsi", {"symbol": "BTCUSDT", "timeframe": "1h", "length": 14} - ) - assert out == {"value": 55.0} - - -def test_tools_unknown_raises(mocker): - fake_client = mocker.MagicMock() - t = CerberoTools(fake_client) - with pytest.raises(AttributeError): - t.nonexistent_tool() # type: ignore[attr-defined] -``` - -- [ ] **Step 2: Run test (deve fallire)** - -Run: `uv run pytest tests/unit/test_cerbero_tools.py -v` -Expected: FAIL. - -- [ ] **Step 3: Implementare wrapper** - -```python -# src/multi_swarm/cerbero/tools.py -from __future__ import annotations - -from typing import Any - -from .client import CerberoClient - - -class CerberoTools: - """Sottoinsieme di tool MCP esposti agli agenti in Phase 1.""" - - def __init__(self, client: CerberoClient): - self._client = client - - def sma(self, exchange: str, symbol: str, timeframe: str, length: int) -> Any: - return self._client.call_tool( - exchange, "sma", {"symbol": symbol, "timeframe": timeframe, "length": length} - ) - - def rsi(self, exchange: str, symbol: str, timeframe: str, length: int = 14) -> Any: - return self._client.call_tool( - exchange, "rsi", {"symbol": symbol, "timeframe": timeframe, "length": length} - ) - - def atr(self, exchange: str, symbol: str, timeframe: str, length: int = 14) -> Any: - return self._client.call_tool( - exchange, "atr", {"symbol": symbol, "timeframe": timeframe, "length": length} - ) - - def macd(self, exchange: str, symbol: str, timeframe: str, fast: int = 12, slow: int = 26, signal: int = 9) -> Any: - return self._client.call_tool( - exchange, "macd", - {"symbol": symbol, "timeframe": timeframe, "fast": fast, "slow": slow, "signal": signal}, - ) - - def realized_vol(self, exchange: str, symbol: str, timeframe: str, window: int = 24) -> Any: - return self._client.call_tool( - exchange, "realized_vol", - {"symbol": symbol, "timeframe": timeframe, "window": window}, - ) - - def funding_rate(self, exchange: str, symbol: str) -> Any: - return self._client.call_tool(exchange, "funding_rate", {"symbol": symbol}) -``` - -- [ ] **Step 4: Run test (deve passare)** - -Run: `uv run pytest tests/unit/test_cerbero_tools.py -v` -Expected: PASS. - -- [ ] **Step 5: Commit** - -```bash -git add src/multi_swarm/cerbero/tools.py tests/unit/test_cerbero_tools.py -git commit -m "feat(cerbero): tools wrapper for Phase 1 indicator subset" -``` - ---- - -## Task 11: Protocollo S-expression — grammar e parser - -**Files:** -- Create: `src/multi_swarm/protocol/__init__.py` -- Create: `src/multi_swarm/protocol/grammar.py` -- Create: `src/multi_swarm/protocol/parser.py` -- Test: `tests/unit/test_protocol_parser.py` - -**Grammar Phase 1 (15 verbi)**: `entry-long`, `entry-short`, `exit`, `flat`, `when`, `and`, `or`, `not`, `gt`, `lt`, `eq`, `feature`, `indicator`, `crossover`, `crossunder`. - -Esempio strategia: -```lisp -(strategy - (when (and (gt (indicator rsi 14) 70.0) - (crossunder (feature close) (indicator sma 20))) - (entry-short)) - (when (lt (indicator rsi 14) 30.0) - (entry-long)) - (when (eq (indicator rsi 14) 50.0) - (exit))) -``` - -- [ ] **Step 1: Scrivere test fallente** - -```python -# tests/unit/test_protocol_parser.py -import pytest -from multi_swarm.protocol.parser import parse_strategy, ParseError -from multi_swarm.protocol.grammar import VERBS - - -def test_grammar_has_15_verbs(): - assert len(VERBS) == 15 - - -def test_parse_simple_strategy(): - src = "(strategy (when (gt (indicator rsi 14) 70.0) (entry-short)))" - ast = parse_strategy(src) - assert ast.kind == "strategy" - assert len(ast.rules) == 1 - rule = ast.rules[0] - assert rule.kind == "when" - assert rule.condition.kind == "gt" - assert rule.action.kind == "entry-short" - - -def test_parse_multiple_rules(): - src = """ - (strategy - (when (gt (indicator rsi 14) 70.0) (entry-short)) - (when (lt (indicator rsi 14) 30.0) (entry-long))) - """ - ast = parse_strategy(src) - assert len(ast.rules) == 2 - - -def test_parse_unknown_verb_raises(): - src = "(strategy (when (frobnicate 1 2) (entry-long)))" - with pytest.raises(ParseError): - parse_strategy(src) - - -def test_parse_malformed_raises(): - src = "(strategy (when" - with pytest.raises(ParseError): - parse_strategy(src) - - -def test_parse_empty_strategy_raises(): - src = "(strategy)" - with pytest.raises(ParseError): - parse_strategy(src) -``` - -- [ ] **Step 2: Run test (deve fallire)** - -Run: `uv run pytest tests/unit/test_protocol_parser.py -v` -Expected: FAIL. - -- [ ] **Step 3: Implementare grammar e parser** - -```python -# src/multi_swarm/protocol/__init__.py -``` - -```python -# src/multi_swarm/protocol/grammar.py -from __future__ import annotations - -VERBS: frozenset[str] = frozenset( - { - "entry-long", - "entry-short", - "exit", - "flat", - "when", - "and", - "or", - "not", - "gt", - "lt", - "eq", - "feature", - "indicator", - "crossover", - "crossunder", - } -) - -ACTION_VERBS: frozenset[str] = frozenset({"entry-long", "entry-short", "exit", "flat"}) -LOGICAL_VERBS: frozenset[str] = frozenset({"and", "or", "not"}) -COMPARATOR_VERBS: frozenset[str] = frozenset({"gt", "lt", "eq"}) -DATA_VERBS: frozenset[str] = frozenset({"feature", "indicator", "crossover", "crossunder"}) -``` - -```python -# src/multi_swarm/protocol/parser.py -from __future__ import annotations - -from dataclasses import dataclass, field -from typing import Any - -import sexpdata - -from .grammar import ( - ACTION_VERBS, - COMPARATOR_VERBS, - DATA_VERBS, - LOGICAL_VERBS, - VERBS, -) - - -class ParseError(Exception): - pass - - -@dataclass -class Node: - kind: str - args: list[Any] = field(default_factory=list) - - -@dataclass -class Rule: - kind: str # "when" - condition: Node - action: Node - - -@dataclass -class Strategy: - kind: str # "strategy" - rules: list[Rule] - - -def _to_node(token: Any) -> Node | float | int | str: - if isinstance(token, sexpdata.Symbol): - name = token.value() - return Node(kind=name, args=[]) - if isinstance(token, list): - if not token: - raise ParseError("Empty s-expression") - head = token[0] - if not isinstance(head, sexpdata.Symbol): - raise ParseError(f"Non-symbol head: {head!r}") - name = head.value() - if name not in VERBS and name != "strategy": - raise ParseError(f"Unknown verb: {name}") - return Node(kind=name, args=[_to_node(arg) for arg in token[1:]]) - return token - - -def parse_strategy(src: str) -> Strategy: - try: - parsed = sexpdata.loads(src) - except Exception as e: - raise ParseError(f"sexp parse error: {e}") from e - - if not isinstance(parsed, list) or not parsed: - raise ParseError("Top-level must be (strategy ...)") - head = parsed[0] - if not isinstance(head, sexpdata.Symbol) or head.value() != "strategy": - raise ParseError("Top-level must start with 'strategy'") - - raw_rules = parsed[1:] - if not raw_rules: - raise ParseError("Strategy must contain at least one rule") - - rules: list[Rule] = [] - for raw in raw_rules: - if not isinstance(raw, list) or len(raw) != 3: - raise ParseError(f"Rule must be (when ): {raw!r}") - head_r = raw[0] - if not isinstance(head_r, sexpdata.Symbol) or head_r.value() != "when": - raise ParseError(f"Rule must start with 'when': {raw!r}") - cond = _to_node(raw[1]) - action = _to_node(raw[2]) - if not isinstance(cond, Node): - raise ParseError(f"Condition must be a node: {cond!r}") - if not isinstance(action, Node): - raise ParseError(f"Action must be a node: {action!r}") - if action.kind not in ACTION_VERBS: - raise ParseError(f"Action must be one of {ACTION_VERBS}, got {action.kind}") - rules.append(Rule(kind="when", condition=cond, action=action)) - - return Strategy(kind="strategy", rules=rules) -``` - -- [ ] **Step 4: Run test (deve passare)** - -Run: `uv run pytest tests/unit/test_protocol_parser.py -v` -Expected: PASS tutti e 6. - -- [ ] **Step 5: Commit** - -```bash -git add src/multi_swarm/protocol/ tests/unit/test_protocol_parser.py -git commit -m "feat(protocol): S-expression grammar (15 verbs) + parser" -``` - ---- - -## Task 12: Protocollo — validator semantico - -**Files:** -- Create: `src/multi_swarm/protocol/validator.py` -- Test: `tests/unit/test_protocol_validator.py` - -Validator controlla che gli argomenti dei verbi abbiano tipi corretti (es. `gt` richiede 2 espressioni numeriche, `indicator` richiede un nome valido + length intero). - -- [ ] **Step 1: Scrivere test fallente** - -```python -# tests/unit/test_protocol_validator.py -import pytest -from multi_swarm.protocol.parser import parse_strategy -from multi_swarm.protocol.validator import validate_strategy, ValidationError - - -def test_valid_strategy_passes(): - src = "(strategy (when (gt (indicator rsi 14) 70.0) (entry-short)))" - ast = parse_strategy(src) - validate_strategy(ast) # no exception - - -def test_indicator_unknown_name_fails(): - src = "(strategy (when (gt (indicator wibble 14) 70.0) (entry-short)))" - ast = parse_strategy(src) - with pytest.raises(ValidationError, match="unknown indicator"): - validate_strategy(ast) - - -def test_indicator_wrong_arity_fails(): - src = "(strategy (when (gt (indicator rsi) 70.0) (entry-short)))" - ast = parse_strategy(src) - with pytest.raises(ValidationError): - validate_strategy(ast) - - -def test_comparator_wrong_arity_fails(): - src = "(strategy (when (gt 1.0) (entry-long)))" - ast = parse_strategy(src) - with pytest.raises(ValidationError): - validate_strategy(ast) - - -def test_feature_unknown_column_fails(): - src = "(strategy (when (gt (feature wibble) 100.0) (entry-long)))" - ast = parse_strategy(src) - with pytest.raises(ValidationError, match="unknown feature"): - validate_strategy(ast) -``` - -- [ ] **Step 2: Run test (deve fallire)** - -Run: `uv run pytest tests/unit/test_protocol_validator.py -v` -Expected: FAIL. - -- [ ] **Step 3: Implementare validator** - -```python -# src/multi_swarm/protocol/validator.py -from __future__ import annotations - -from .parser import Node, Rule, Strategy -from .grammar import COMPARATOR_VERBS, LOGICAL_VERBS - -KNOWN_INDICATORS: frozenset[str] = frozenset({"sma", "rsi", "atr", "macd", "realized_vol"}) -KNOWN_FEATURES: frozenset[str] = frozenset({"open", "high", "low", "close", "volume"}) - - -class ValidationError(Exception): - pass - - -def validate_strategy(strategy: Strategy) -> None: - for rule in strategy.rules: - _validate_node(rule.condition, expect_bool=True) - - -def _validate_node(node: Node, expect_bool: bool) -> None: - if node.kind in LOGICAL_VERBS: - if node.kind == "not": - if len(node.args) != 1: - raise ValidationError(f"'not' needs 1 arg, got {len(node.args)}") - _validate_node(node.args[0], expect_bool=True) - else: - if len(node.args) < 2: - raise ValidationError(f"'{node.kind}' needs >=2 args") - for a in node.args: - _validate_node(a, expect_bool=True) - return - - if node.kind in COMPARATOR_VERBS: - if len(node.args) != 2: - raise ValidationError(f"'{node.kind}' needs 2 args, got {len(node.args)}") - for a in node.args: - if isinstance(a, Node): - _validate_node(a, expect_bool=False) - return - - if node.kind in {"crossover", "crossunder"}: - if len(node.args) != 2: - raise ValidationError(f"'{node.kind}' needs 2 args") - for a in node.args: - if isinstance(a, Node): - _validate_node(a, expect_bool=False) - return - - if node.kind == "indicator": - if len(node.args) < 2: - raise ValidationError(f"'indicator' needs >=2 args (name, length)") - name_node = node.args[0] - if isinstance(name_node, Node): - ind_name = name_node.kind - else: - ind_name = str(name_node) - if ind_name not in KNOWN_INDICATORS: - raise ValidationError(f"unknown indicator: {ind_name}") - return - - if node.kind == "feature": - if len(node.args) != 1: - raise ValidationError(f"'feature' needs 1 arg") - feat_node = node.args[0] - if isinstance(feat_node, Node): - feat_name = feat_node.kind - else: - feat_name = str(feat_node) - if feat_name not in KNOWN_FEATURES: - raise ValidationError(f"unknown feature: {feat_name}") - return - - raise ValidationError(f"unexpected node kind in expression: {node.kind}") -``` - -- [ ] **Step 4: Run test (deve passare)** - -Run: `uv run pytest tests/unit/test_protocol_validator.py -v` -Expected: PASS tutti e 5. - -- [ ] **Step 5: Commit** - -```bash -git add src/multi_swarm/protocol/validator.py tests/unit/test_protocol_validator.py -git commit -m "feat(protocol): semantic validator for AST" -``` - ---- - -## Task 13: Protocollo — compiler AST → callable rules - -**Files:** -- Create: `src/multi_swarm/protocol/compiler.py` -- Test: `tests/unit/test_protocol_compiler.py` - -Il compiler trasforma l'AST in una funzione `(ohlcv_window: pd.DataFrame) -> Side` che dato uno snapshot di mercato restituisce la decisione di posizione. Gli indicatori sono calcolati da una libreria locale built-in (no Cerbero in compiler — Cerbero è chiamato dagli agenti per ispezione, non dal compiler che deve essere veloce e deterministico). - -- [ ] **Step 1: Scrivere test fallente** - -```python -# tests/unit/test_protocol_compiler.py -import numpy as np -import pandas as pd -import pytest -from multi_swarm.protocol.parser import parse_strategy -from multi_swarm.protocol.compiler import compile_strategy -from multi_swarm.backtest.orders import Side - - -@pytest.fixture -def ohlcv(): - idx = pd.date_range("2024-01-01", periods=200, freq="1h", tz="UTC") - close = np.linspace(100, 120, 200) - return pd.DataFrame( - {"open": close, "high": close + 0.5, "low": close - 0.5, "close": close, "volume": 1.0}, - index=idx, - ) - - -def test_compile_simple_long(ohlcv): - src = "(strategy (when (lt (indicator rsi 14) 100.0) (entry-long)))" - ast = parse_strategy(src) - fn = compile_strategy(ast) - signals = fn(ohlcv) - assert isinstance(signals, pd.Series) - assert (signals == Side.LONG).all() or (signals.dropna() == Side.LONG).all() - - -def test_compile_no_match_is_flat(ohlcv): - src = "(strategy (when (gt (indicator rsi 14) 1000.0) (entry-long)))" - ast = parse_strategy(src) - fn = compile_strategy(ast) - signals = fn(ohlcv) - assert (signals == Side.FLAT).any() - - -def test_compile_two_rules_priority(ohlcv): - src = """ - (strategy - (when (gt (feature close) 110.0) (entry-long)) - (when (lt (feature close) 105.0) (entry-short))) - """ - ast = parse_strategy(src) - fn = compile_strategy(ast) - signals = fn(ohlcv) - last = signals.iloc[-1] - assert last == Side.LONG # close finale è 120, regola 1 matcha -``` - -- [ ] **Step 2: Run test (deve fallire)** - -Run: `uv run pytest tests/unit/test_protocol_compiler.py -v` -Expected: FAIL. - -- [ ] **Step 3: Implementare compiler** - -```python -# src/multi_swarm/protocol/compiler.py -from __future__ import annotations - -from typing import Callable - -import numpy as np -import pandas as pd - -from ..backtest.orders import Side -from .parser import Node, Strategy - - -def _sma(s: pd.Series, length: int) -> pd.Series: - return s.rolling(length, min_periods=1).mean() - - -def _rsi(s: pd.Series, length: int) -> pd.Series: - delta = s.diff() - up = delta.clip(lower=0) - down = -delta.clip(upper=0) - roll_up = up.ewm(alpha=1.0 / length, adjust=False).mean() - roll_down = down.ewm(alpha=1.0 / length, adjust=False).mean() - rs = roll_up / roll_down.replace(0, np.nan) - return 100 - (100 / (1 + rs)) - - -def _atr(df: pd.DataFrame, length: int) -> pd.Series: - h_l = df["high"] - df["low"] - h_c = (df["high"] - df["close"].shift()).abs() - l_c = (df["low"] - df["close"].shift()).abs() - tr = pd.concat([h_l, h_c, l_c], axis=1).max(axis=1) - return tr.ewm(alpha=1.0 / length, adjust=False).mean() - - -def _realized_vol(s: pd.Series, window: int) -> pd.Series: - returns = s.pct_change() - return returns.rolling(window, min_periods=1).std() * np.sqrt(window) - - -INDICATOR_FNS: dict[str, Callable[..., pd.Series]] = { - "sma": lambda df, length: _sma(df["close"], length), - "rsi": lambda df, length: _rsi(df["close"], length), - "atr": lambda df, length: _atr(df, length), - "realized_vol": lambda df, length: _realized_vol(df["close"], length), - "macd": lambda df, fast=12, slow=26: ( - _sma(df["close"], fast) - _sma(df["close"], slow) - ), -} - - -def _eval_node(node: Node, df: pd.DataFrame) -> pd.Series: - if node.kind == "feature": - feat = node.args[0] - feat_name = feat.kind if isinstance(feat, Node) else str(feat) - return df[feat_name] - - if node.kind == "indicator": - name_node = node.args[0] - ind_name = name_node.kind if isinstance(name_node, Node) else str(name_node) - params = [a for a in node.args[1:] if not isinstance(a, Node)] - return INDICATOR_FNS[ind_name](df, *params) - - if node.kind == "gt": - a = _eval_node(node.args[0], df) if isinstance(node.args[0], Node) else _to_series(node.args[0], df) - b = _eval_node(node.args[1], df) if isinstance(node.args[1], Node) else _to_series(node.args[1], df) - return (a > b).astype(bool) - - if node.kind == "lt": - a = _eval_node(node.args[0], df) if isinstance(node.args[0], Node) else _to_series(node.args[0], df) - b = _eval_node(node.args[1], df) if isinstance(node.args[1], Node) else _to_series(node.args[1], df) - return (a < b).astype(bool) - - if node.kind == "eq": - a = _eval_node(node.args[0], df) if isinstance(node.args[0], Node) else _to_series(node.args[0], df) - b = _eval_node(node.args[1], df) if isinstance(node.args[1], Node) else _to_series(node.args[1], df) - return (a == b).astype(bool) - - if node.kind == "and": - result = pd.Series(True, index=df.index) - for a in node.args: - s = _eval_node(a, df) if isinstance(a, Node) else pd.Series(bool(a), index=df.index) - result &= s.fillna(False).astype(bool) - return result - - if node.kind == "or": - result = pd.Series(False, index=df.index) - for a in node.args: - s = _eval_node(a, df) if isinstance(a, Node) else pd.Series(bool(a), index=df.index) - result |= s.fillna(False).astype(bool) - return result - - if node.kind == "not": - a = node.args[0] - s = _eval_node(a, df) if isinstance(a, Node) else pd.Series(bool(a), index=df.index) - return (~s.fillna(False).astype(bool)) - - if node.kind == "crossover": - a = _eval_node(node.args[0], df) if isinstance(node.args[0], Node) else _to_series(node.args[0], df) - b = _eval_node(node.args[1], df) if isinstance(node.args[1], Node) else _to_series(node.args[1], df) - return ((a > b) & (a.shift() <= b.shift())).fillna(False).astype(bool) - - if node.kind == "crossunder": - a = _eval_node(node.args[0], df) if isinstance(node.args[0], Node) else _to_series(node.args[0], df) - b = _eval_node(node.args[1], df) if isinstance(node.args[1], Node) else _to_series(node.args[1], df) - return ((a < b) & (a.shift() >= b.shift())).fillna(False).astype(bool) - - raise RuntimeError(f"unsupported node in compiler: {node.kind}") - - -def _to_series(value: object, df: pd.DataFrame) -> pd.Series: - return pd.Series(float(value), index=df.index) # type: ignore[arg-type] - - -def _action_to_side(action: Node) -> Side: - return { - "entry-long": Side.LONG, - "entry-short": Side.SHORT, - "exit": Side.FLAT, - "flat": Side.FLAT, - }[action.kind] - - -def compile_strategy(strategy: Strategy) -> Callable[[pd.DataFrame], pd.Series]: - """Compila la strategy in una funzione df → Series[Side]. - - Le regole sono valutate in ordine; la prima che matcha vince per ogni timestamp. - Default Side.FLAT se nessuna regola matcha. - """ - - def fn(df: pd.DataFrame) -> pd.Series: - result = pd.Series(Side.FLAT, index=df.index, dtype=object) - already_set = pd.Series(False, index=df.index) - for rule in strategy.rules: - match = _eval_node(rule.condition, df) - target = _action_to_side(rule.action) - apply_mask = match & ~already_set - result[apply_mask] = target - already_set |= apply_mask - return result - - return fn -``` - -- [ ] **Step 4: Run test (deve passare)** - -Run: `uv run pytest tests/unit/test_protocol_compiler.py -v` -Expected: PASS tutti e 3. - -- [ ] **Step 5: Commit** - -```bash -git add src/multi_swarm/protocol/compiler.py tests/unit/test_protocol_compiler.py -git commit -m "feat(protocol): AST compiler to (df -> Series[Side]) signal fn" -``` - ---- - -## Task 14: Genome dataclass + serializzazione - -**Files:** -- Create: `src/multi_swarm/genome/__init__.py` -- Create: `src/multi_swarm/genome/hypothesis.py` -- Test: `tests/unit/test_genome_hypothesis.py` - -- [ ] **Step 1: Scrivere test fallente** - -```python -# tests/unit/test_genome_hypothesis.py -from multi_swarm.genome.hypothesis import HypothesisAgentGenome, ModelTier - - -def test_genome_creation_defaults(): - g = HypothesisAgentGenome( - system_prompt="Pensa come un fisico.", - feature_access=["close", "volume"], - temperature=0.9, - top_p=0.95, - model_tier=ModelTier.C, - lookback_window=200, - cognitive_style="physicist", - ) - assert g.id is not None - assert g.parent_ids == [] - assert g.generation == 0 - - -def test_genome_serialization_roundtrip(): - g = HypothesisAgentGenome( - system_prompt="Pensa come un biologo.", - feature_access=["close", "high", "low"], - temperature=1.1, - top_p=0.9, - model_tier=ModelTier.C, - lookback_window=300, - cognitive_style="biologist", - parent_ids=["abc"], - generation=5, - ) - payload = g.to_dict() - g2 = HypothesisAgentGenome.from_dict(payload) - assert g2.system_prompt == g.system_prompt - assert g2.feature_access == g.feature_access - assert g2.temperature == g.temperature - assert g2.parent_ids == g.parent_ids - assert g2.generation == g.generation - assert g2.id == g.id - - -def test_genome_id_is_deterministic_on_content(): - g1 = HypothesisAgentGenome( - system_prompt="X", feature_access=["close"], temperature=0.5, - top_p=0.9, model_tier=ModelTier.C, lookback_window=100, cognitive_style="x", - ) - g2 = HypothesisAgentGenome( - system_prompt="X", feature_access=["close"], temperature=0.5, - top_p=0.9, model_tier=ModelTier.C, lookback_window=100, cognitive_style="x", - ) - assert g1.id == g2.id -``` - -- [ ] **Step 2: Run test (deve fallire)** - -Run: `uv run pytest tests/unit/test_genome_hypothesis.py -v` -Expected: FAIL. - -- [ ] **Step 3: Implementare genome** - -```python -# src/multi_swarm/genome/__init__.py -``` - -```python -# src/multi_swarm/genome/hypothesis.py -from __future__ import annotations - -import hashlib -import json -from dataclasses import dataclass, field -from enum import Enum -from typing import Any - - -class ModelTier(str, Enum): - B = "B" # Sonnet 4.6 via Anthropic - C = "C" # Qwen 2.5 72B via OpenRouter - - -@dataclass -class HypothesisAgentGenome: - system_prompt: str - feature_access: list[str] - temperature: float - top_p: float - model_tier: ModelTier - lookback_window: int - cognitive_style: str - parent_ids: list[str] = field(default_factory=list) - generation: int = 0 - id: str = "" - - def __post_init__(self) -> None: - if not self.id: - self.id = self._compute_id() - - def _compute_id(self) -> str: - payload = { - "system_prompt": self.system_prompt, - "feature_access": sorted(self.feature_access), - "temperature": round(self.temperature, 4), - "top_p": round(self.top_p, 4), - "model_tier": self.model_tier.value, - "lookback_window": self.lookback_window, - "cognitive_style": self.cognitive_style, - } - s = json.dumps(payload, sort_keys=True) - return hashlib.sha1(s.encode()).hexdigest()[:16] - - def to_dict(self) -> dict[str, Any]: - return { - "id": self.id, - "system_prompt": self.system_prompt, - "feature_access": self.feature_access, - "temperature": self.temperature, - "top_p": self.top_p, - "model_tier": self.model_tier.value, - "lookback_window": self.lookback_window, - "cognitive_style": self.cognitive_style, - "parent_ids": self.parent_ids, - "generation": self.generation, - } - - @classmethod - def from_dict(cls, data: dict[str, Any]) -> HypothesisAgentGenome: - return cls( - system_prompt=data["system_prompt"], - feature_access=list(data["feature_access"]), - temperature=float(data["temperature"]), - top_p=float(data["top_p"]), - model_tier=ModelTier(data["model_tier"]), - lookback_window=int(data["lookback_window"]), - cognitive_style=data["cognitive_style"], - parent_ids=list(data.get("parent_ids", [])), - generation=int(data.get("generation", 0)), - id=data.get("id", ""), - ) -``` - -- [ ] **Step 4: Run test (deve passare)** - -Run: `uv run pytest tests/unit/test_genome_hypothesis.py -v` -Expected: PASS. - -- [ ] **Step 5: Commit** - -```bash -git add src/multi_swarm/genome/ tests/unit/test_genome_hypothesis.py -git commit -m "feat(genome): HypothesisAgentGenome with deterministic id and serde" -``` - ---- - -## Task 15: Genome — mutation operators - -**Files:** -- Create: `src/multi_swarm/genome/mutation.py` -- Test: `tests/unit/test_genome_mutation.py` - -Operatori di mutazione (uno selezionato casualmente per ogni mutazione): -1. `mutate_temperature`: ±0.1, clipped a [0.6, 1.3]. -2. `mutate_lookback`: ±50 bar, clipped a [50, 500]. -3. `mutate_feature_access`: aggiungi/rimuovi una feature da pool fissa. -4. `mutate_cognitive_style`: cambia da pool fissa di 6 stili. -5. `mutate_prompt_chunk`: l'LLM riscrive una parte del system_prompt (gestito altrove, per ora skip — solo placeholder). - -In Phase 1 mutiamo solo i campi numerici/discreti deterministicamente. Le mutazioni del prompt LLM sono delegate al modulo `agents` quando si chiama il "mutator agent". - -- [ ] **Step 1: Scrivere test fallente** - -```python -# tests/unit/test_genome_mutation.py -import random -import pytest -from multi_swarm.genome.hypothesis import HypothesisAgentGenome, ModelTier -from multi_swarm.genome.mutation import ( - mutate_temperature, - mutate_lookback, - mutate_feature_access, - mutate_cognitive_style, - FEATURE_POOL, - COGNITIVE_STYLES, -) - - -@pytest.fixture -def base_genome(): - return HypothesisAgentGenome( - system_prompt="x", - feature_access=["close"], - temperature=0.9, - top_p=0.95, - model_tier=ModelTier.C, - lookback_window=200, - cognitive_style="physicist", - ) - - -def test_mutate_temperature_within_bounds(base_genome): - rng = random.Random(0) - for _ in range(50): - new = mutate_temperature(base_genome, rng) - assert 0.6 <= new.temperature <= 1.3 - - -def test_mutate_lookback_within_bounds(base_genome): - rng = random.Random(0) - for _ in range(50): - new = mutate_lookback(base_genome, rng) - assert 50 <= new.lookback_window <= 500 - - -def test_mutate_feature_access_changes_set(base_genome): - rng = random.Random(0) - new = mutate_feature_access(base_genome, rng) - assert set(new.feature_access) != set(base_genome.feature_access) or len(FEATURE_POOL) == 1 - assert all(f in FEATURE_POOL for f in new.feature_access) - assert len(new.feature_access) >= 1 - - -def test_mutate_cognitive_style_uses_pool(base_genome): - rng = random.Random(0) - new = mutate_cognitive_style(base_genome, rng) - assert new.cognitive_style in COGNITIVE_STYLES - - -def test_mutation_preserves_lineage(base_genome): - rng = random.Random(0) - new = mutate_temperature(base_genome, rng) - assert base_genome.id in new.parent_ids - assert new.id != base_genome.id -``` - -- [ ] **Step 2: Run test (deve fallire)** - -Run: `uv run pytest tests/unit/test_genome_mutation.py -v` -Expected: FAIL. - -- [ ] **Step 3: Implementare mutazioni** - -```python -# src/multi_swarm/genome/mutation.py -from __future__ import annotations - -import random - -from .hypothesis import HypothesisAgentGenome, ModelTier - - -FEATURE_POOL: tuple[str, ...] = ("open", "high", "low", "close", "volume") - -COGNITIVE_STYLES: tuple[str, ...] = ( - "physicist", "biologist", "historian", "meteorologist", - "ecologist", "engineer", -) - - -def _clone_with(g: HypothesisAgentGenome, **overrides: object) -> HypothesisAgentGenome: - payload = g.to_dict() - payload.update(overrides) # type: ignore[arg-type] - payload.pop("id", None) - payload["parent_ids"] = list(g.parent_ids) + [g.id] - payload["generation"] = g.generation + 1 - return HypothesisAgentGenome.from_dict(payload) - - -def mutate_temperature(g: HypothesisAgentGenome, rng: random.Random) -> HypothesisAgentGenome: - delta = rng.choice([-0.1, 0.1]) - new_t = max(0.6, min(1.3, g.temperature + delta)) - return _clone_with(g, temperature=round(new_t, 4)) - - -def mutate_lookback(g: HypothesisAgentGenome, rng: random.Random) -> HypothesisAgentGenome: - delta = rng.choice([-50, 50]) - new_lb = max(50, min(500, g.lookback_window + delta)) - return _clone_with(g, lookback_window=new_lb) - - -def mutate_feature_access(g: HypothesisAgentGenome, rng: random.Random) -> HypothesisAgentGenome: - current = set(g.feature_access) - if len(current) == len(FEATURE_POOL): - op = "remove" - elif not current: - op = "add" - else: - op = rng.choice(["add", "remove"]) - - if op == "add": - candidates = [f for f in FEATURE_POOL if f not in current] - choice = rng.choice(candidates) - new_set = current | {choice} - else: - if len(current) <= 1: - return _clone_with(g) - choice = rng.choice(sorted(current)) - new_set = current - {choice} - - return _clone_with(g, feature_access=sorted(new_set)) - - -def mutate_cognitive_style(g: HypothesisAgentGenome, rng: random.Random) -> HypothesisAgentGenome: - candidates = [s for s in COGNITIVE_STYLES if s != g.cognitive_style] - new_style = rng.choice(candidates) - return _clone_with(g, cognitive_style=new_style) - - -MUTATION_OPS = (mutate_temperature, mutate_lookback, mutate_feature_access, mutate_cognitive_style) - - -def random_mutate(g: HypothesisAgentGenome, rng: random.Random) -> HypothesisAgentGenome: - op = rng.choice(MUTATION_OPS) - return op(g, rng) -``` - -- [ ] **Step 4: Run test (deve passare)** - -Run: `uv run pytest tests/unit/test_genome_mutation.py -v` -Expected: PASS. - -- [ ] **Step 5: Commit** - -```bash -git add src/multi_swarm/genome/mutation.py tests/unit/test_genome_mutation.py -git commit -m "feat(genome): deterministic mutation operators (numeric + categorical)" -``` - ---- - -## Task 16: Genome — crossover - -**Files:** -- Create: `src/multi_swarm/genome/crossover.py` -- Test: `tests/unit/test_genome_crossover.py` - -Crossover uniforme: per ogni campo prende valore da parent1 o parent2 con prob 0.5. system_prompt: scelta intera (no merging in Phase 1). - -- [ ] **Step 1: Scrivere test fallente** - -```python -# tests/unit/test_genome_crossover.py -import random -from multi_swarm.genome.hypothesis import HypothesisAgentGenome, ModelTier -from multi_swarm.genome.crossover import uniform_crossover - - -def make(name: str) -> HypothesisAgentGenome: - return HypothesisAgentGenome( - system_prompt=f"prompt-{name}", - feature_access=["close"] if name == "A" else ["close", "volume"], - temperature=0.7 if name == "A" else 1.1, - top_p=0.9, - model_tier=ModelTier.C, - lookback_window=100 if name == "A" else 300, - cognitive_style="physicist" if name == "A" else "biologist", - ) - - -def test_crossover_lineage(): - p1 = make("A") - p2 = make("B") - rng = random.Random(0) - child = uniform_crossover(p1, p2, rng) - assert sorted(child.parent_ids[-2:]) == sorted([p1.id, p2.id]) - assert child.generation == max(p1.generation, p2.generation) + 1 - - -def test_crossover_inherits_each_field_from_one_parent(): - p1 = make("A") - p2 = make("B") - rng = random.Random(0) - child = uniform_crossover(p1, p2, rng) - assert child.system_prompt in (p1.system_prompt, p2.system_prompt) - assert child.temperature in (p1.temperature, p2.temperature) - assert child.lookback_window in (p1.lookback_window, p2.lookback_window) - assert child.cognitive_style in (p1.cognitive_style, p2.cognitive_style) - - -def test_crossover_deterministic_with_same_seed(): - p1 = make("A") - p2 = make("B") - c1 = uniform_crossover(p1, p2, random.Random(42)) - c2 = uniform_crossover(p1, p2, random.Random(42)) - assert c1.to_dict() == c2.to_dict() -``` - -- [ ] **Step 2: Run test (deve fallire)** - -Run: `uv run pytest tests/unit/test_genome_crossover.py -v` -Expected: FAIL. - -- [ ] **Step 3: Implementare crossover** - -```python -# src/multi_swarm/genome/crossover.py -from __future__ import annotations - -import random - -from .hypothesis import HypothesisAgentGenome - - -def uniform_crossover( - p1: HypothesisAgentGenome, - p2: HypothesisAgentGenome, - rng: random.Random, -) -> HypothesisAgentGenome: - """Per ogni campo, eredita da p1 (prob 0.5) o p2.""" - - def pick(field: str) -> object: - return getattr(p1 if rng.random() < 0.5 else p2, field) - - payload = { - "system_prompt": pick("system_prompt"), - "feature_access": list(pick("feature_access")), # type: ignore[arg-type] - "temperature": pick("temperature"), - "top_p": pick("top_p"), - "model_tier": pick("model_tier").value if hasattr(pick("model_tier"), "value") else pick("model_tier"), # type: ignore[union-attr] - "lookback_window": pick("lookback_window"), - "cognitive_style": pick("cognitive_style"), - "parent_ids": [p1.id, p2.id], - "generation": max(p1.generation, p2.generation) + 1, - } - return HypothesisAgentGenome.from_dict(payload) -``` - -- [ ] **Step 4: Run test (deve passare)** - -Run: `uv run pytest tests/unit/test_genome_crossover.py -v` -Expected: PASS. - -- [ ] **Step 5: Commit** - -```bash -git add src/multi_swarm/genome/crossover.py tests/unit/test_genome_crossover.py -git commit -m "feat(genome): uniform crossover for hypothesis genomes" -``` - ---- - -## Task 17: LLM client (OpenRouter Qwen + Anthropic Sonnet) - -**Files:** -- Create: `src/multi_swarm/llm/__init__.py` -- Create: `src/multi_swarm/llm/client.py` -- Test: `tests/unit/test_llm_client.py` - -Wrapper unificato: `LLMClient.complete(genome, system, user) -> CompletionResult`. Sceglie tier da `genome.model_tier`. Per tier C usa OpenAI SDK con base_url = OpenRouter; per tier B usa anthropic SDK. - -- [ ] **Step 1: Scrivere test fallente** - -```python -# tests/unit/test_llm_client.py -from multi_swarm.genome.hypothesis import HypothesisAgentGenome, ModelTier -from multi_swarm.llm.client import LLMClient, CompletionResult - - -def make_genome(tier: ModelTier) -> HypothesisAgentGenome: - return HypothesisAgentGenome( - system_prompt="x", feature_access=["close"], temperature=0.9, top_p=0.95, - model_tier=tier, lookback_window=200, cognitive_style="physicist", - ) - - -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 - - mocker.patch("multi_swarm.llm.client.OpenAI", return_value=fake_openai) - - client = LLMClient(openrouter_api_key="or-x", anthropic_api_key=None) - g = make_genome(ModelTier.C) - out = client.complete(g, system="sys", user="usr") - - assert isinstance(out, CompletionResult) - assert out.text == "(strategy ...)" - assert out.input_tokens == 100 - assert out.output_tokens == 200 - assert out.tier == ModelTier.C - fake_openai.chat.completions.create.assert_called_once() - - -def test_completion_tier_b_uses_anthropic(mocker): - fake_anthropic = mocker.MagicMock() - fake_msg = mocker.MagicMock() - fake_msg.content = [mocker.MagicMock(text="(strategy ...)")] - fake_msg.usage = mocker.MagicMock(input_tokens=80, output_tokens=150) - fake_anthropic.messages.create.return_value = fake_msg - mocker.patch("multi_swarm.llm.client.Anthropic", return_value=fake_anthropic) - - client = LLMClient(openrouter_api_key="or-x", anthropic_api_key="an-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 -``` - -- [ ] **Step 2: Run test (deve fallire)** - -Run: `uv run pytest tests/unit/test_llm_client.py -v` -Expected: FAIL. - -- [ ] **Step 3: Implementare LLM client** - -```python -# src/multi_swarm/llm/__init__.py -``` - -```python -# src/multi_swarm/llm/client.py -from __future__ import annotations - -from dataclasses import dataclass - -from anthropic import Anthropic -from openai import OpenAI - -from ..genome.hypothesis import HypothesisAgentGenome, ModelTier - - -# Modelli configurati per Phase 1 -MODEL_TIER_C = "qwen/qwen-2.5-72b-instruct" # via OpenRouter -MODEL_TIER_B = "claude-sonnet-4-6" # via Anthropic -OPENROUTER_BASE_URL = "https://openrouter.ai/api/v1" - - -@dataclass(frozen=True) -class CompletionResult: - text: str - input_tokens: int - output_tokens: int - tier: ModelTier - model: str - - -class LLMClient: - def __init__( - self, - openrouter_api_key: str, - anthropic_api_key: str | None = None, - ): - self._openrouter = OpenAI(api_key=openrouter_api_key, base_url=OPENROUTER_BASE_URL) - self._anthropic = Anthropic(api_key=anthropic_api_key) if anthropic_api_key else None - - def complete( - self, - genome: HypothesisAgentGenome, - system: str, - user: str, - max_tokens: int = 2000, - ) -> CompletionResult: - if genome.model_tier == ModelTier.C: - resp = self._openrouter.chat.completions.create( - model=MODEL_TIER_C, - messages=[ - {"role": "system", "content": system}, - {"role": "user", "content": user}, - ], - temperature=genome.temperature, - top_p=genome.top_p, - max_tokens=max_tokens, - ) - return CompletionResult( - text=resp.choices[0].message.content or "", - input_tokens=resp.usage.prompt_tokens, - output_tokens=resp.usage.completion_tokens, - tier=ModelTier.C, - model=MODEL_TIER_C, - ) - - if self._anthropic is None: - raise RuntimeError("ANTHROPIC_API_KEY required for tier B genomes") - - msg = self._anthropic.messages.create( - model=MODEL_TIER_B, - system=system, - messages=[{"role": "user", "content": user}], - temperature=genome.temperature, - top_p=genome.top_p, - max_tokens=max_tokens, - ) - text = "".join(block.text for block in msg.content if hasattr(block, "text")) - return CompletionResult( - text=text, - input_tokens=msg.usage.input_tokens, - output_tokens=msg.usage.output_tokens, - tier=ModelTier.B, - model=MODEL_TIER_B, - ) -``` - -- [ ] **Step 4: Run test (deve passare)** - -Run: `uv run pytest tests/unit/test_llm_client.py -v` -Expected: PASS. - -- [ ] **Step 5: Commit** - -```bash -git add src/multi_swarm/llm/ tests/unit/test_llm_client.py -git commit -m "feat(llm): unified client for OpenRouter (Qwen) + Anthropic (Sonnet)" -``` - ---- - -## Task 18: Cost tracker - -**Files:** -- Create: `src/multi_swarm/llm/cost_tracker.py` -- Test: `tests/unit/test_cost_tracker.py` - -Pricing approssimativo Phase 1 (al token): -- tier C (Qwen 2.5 72B via OpenRouter): $0.40/M input, $0.40/M output -- tier B (Claude Sonnet 4.6): $3.00/M input, $15.00/M output - -- [ ] **Step 1: Scrivere test fallente** - -```python -# tests/unit/test_cost_tracker.py -from multi_swarm.genome.hypothesis import ModelTier -from multi_swarm.llm.cost_tracker import CostTracker, estimate_cost - - -def test_estimate_cost_tier_c(): - cost = estimate_cost(input_tokens=1_000_000, output_tokens=1_000_000, tier=ModelTier.C) - assert cost == 0.40 + 0.40 - - -def test_estimate_cost_tier_b(): - cost = estimate_cost(input_tokens=1_000_000, output_tokens=1_000_000, tier=ModelTier.B) - assert cost == 3.00 + 15.00 - - -def test_tracker_accumulates(): - t = CostTracker() - t.record(input_tokens=10_000, output_tokens=20_000, tier=ModelTier.C, run_id="r", agent_id="a") - t.record(input_tokens=5_000, output_tokens=15_000, tier=ModelTier.C, run_id="r", agent_id="b") - summary = t.summary() - assert summary["calls"] == 2 - assert summary["input_tokens"] == 15_000 - assert summary["output_tokens"] == 35_000 - assert summary["cost_usd"] > 0 - - -def test_tracker_per_tier_breakdown(): - t = CostTracker() - t.record(input_tokens=10_000, output_tokens=10_000, tier=ModelTier.C, run_id="r", agent_id="a") - t.record(input_tokens=10_000, output_tokens=10_000, tier=ModelTier.B, run_id="r", agent_id="b") - summary = t.summary() - assert "C" in summary["by_tier"] - assert "B" in summary["by_tier"] -``` - -- [ ] **Step 2: Run test (deve fallire)** - -Run: `uv run pytest tests/unit/test_cost_tracker.py -v` -Expected: FAIL. - -- [ ] **Step 3: Implementare cost tracker** - -```python -# src/multi_swarm/llm/cost_tracker.py -from __future__ import annotations - -from collections import defaultdict -from dataclasses import dataclass, field -from datetime import datetime, timezone -from typing import Any - -from ..genome.hypothesis import ModelTier - - -PRICE_PER_M_TOKENS: dict[ModelTier, dict[str, float]] = { - ModelTier.C: {"input": 0.40, "output": 0.40}, - ModelTier.B: {"input": 3.00, "output": 15.00}, -} - - -def estimate_cost(input_tokens: int, output_tokens: int, tier: ModelTier) -> float: - p = PRICE_PER_M_TOKENS[tier] - return (input_tokens / 1_000_000) * p["input"] + (output_tokens / 1_000_000) * p["output"] - - -@dataclass -class CostRecord: - ts: datetime - run_id: str - agent_id: str - tier: ModelTier - input_tokens: int - output_tokens: int - cost_usd: float - - -@dataclass -class CostTracker: - records: list[CostRecord] = field(default_factory=list) - - def record( - self, - input_tokens: int, - output_tokens: int, - tier: ModelTier, - run_id: str, - agent_id: str, - ) -> CostRecord: - cost = estimate_cost(input_tokens, output_tokens, tier) - rec = CostRecord( - ts=datetime.now(timezone.utc), - run_id=run_id, - agent_id=agent_id, - tier=tier, - input_tokens=input_tokens, - output_tokens=output_tokens, - cost_usd=cost, - ) - self.records.append(rec) - return rec - - def summary(self) -> dict[str, Any]: - by_tier: dict[str, dict[str, float]] = defaultdict( - lambda: {"calls": 0, "input_tokens": 0, "output_tokens": 0, "cost_usd": 0.0} - ) - for r in self.records: - t = r.tier.value - by_tier[t]["calls"] += 1 - by_tier[t]["input_tokens"] += r.input_tokens - by_tier[t]["output_tokens"] += r.output_tokens - by_tier[t]["cost_usd"] += r.cost_usd - return { - "calls": len(self.records), - "input_tokens": sum(r.input_tokens for r in self.records), - "output_tokens": sum(r.output_tokens for r in self.records), - "cost_usd": sum(r.cost_usd for r in self.records), - "by_tier": dict(by_tier), - } -``` - -- [ ] **Step 4: Run test (deve passare)** - -Run: `uv run pytest tests/unit/test_cost_tracker.py -v` -Expected: PASS. - -- [ ] **Step 5: Commit** - -```bash -git add src/multi_swarm/llm/cost_tracker.py tests/unit/test_cost_tracker.py -git commit -m "feat(llm): cost tracker with per-tier pricing and breakdown" -``` - ---- - -## Task 19: Hypothesis agent (LLM call → S-expr) - -**Files:** -- Create: `src/multi_swarm/agents/__init__.py` -- Create: `src/multi_swarm/agents/hypothesis.py` -- Test: `tests/unit/test_hypothesis_agent.py` - -L'Hypothesis agent prende un genome + un summary di mercato (statistiche di base sull'OHLCV training set) e produce una strategia S-expression. Il prompt template è fissato; il system_prompt del genoma viene iniettato nel system message; il summary di mercato e i feature accessibili sono iniettati nel user message. - -- [ ] **Step 1: Scrivere test fallente** - -```python -# tests/unit/test_hypothesis_agent.py -import pandas as pd -import numpy as np -from multi_swarm.genome.hypothesis import HypothesisAgentGenome, ModelTier -from multi_swarm.agents.hypothesis import HypothesisAgent, MarketSummary -from multi_swarm.llm.client import CompletionResult - - -def make_summary(): - return MarketSummary( - symbol="BTC/USDT", - timeframe="1h", - n_bars=1000, - return_mean=0.0001, - return_std=0.01, - skew=0.1, - kurtosis=3.5, - volatility_regime="high", - ) - - -def test_hypothesis_agent_calls_llm_and_parses(mocker): - fake_llm = mocker.MagicMock() - fake_llm.complete.return_value = CompletionResult( - text="(strategy (when (gt (indicator rsi 14) 70.0) (entry-short)))", - input_tokens=200, output_tokens=80, tier=ModelTier.C, model="qwen", - ) - g = HypothesisAgentGenome( - system_prompt="Pensa come un fisico.", feature_access=["close"], temperature=0.9, - top_p=0.95, model_tier=ModelTier.C, lookback_window=200, cognitive_style="physicist", - ) - agent = HypothesisAgent(llm=fake_llm) - proposal = agent.propose(g, make_summary()) - assert proposal.strategy is not None - assert proposal.raw_text.startswith("(strategy") - assert proposal.completion.input_tokens == 200 - fake_llm.complete.assert_called_once() - - -def test_hypothesis_agent_returns_none_on_parse_error(mocker): - fake_llm = mocker.MagicMock() - fake_llm.complete.return_value = CompletionResult( - text="this is not s-expression", - input_tokens=200, output_tokens=80, tier=ModelTier.C, model="qwen", - ) - g = HypothesisAgentGenome( - system_prompt="x", feature_access=["close"], temperature=0.9, - top_p=0.95, model_tier=ModelTier.C, lookback_window=200, cognitive_style="physicist", - ) - agent = HypothesisAgent(llm=fake_llm) - proposal = agent.propose(g, make_summary()) - assert proposal.strategy is None - assert proposal.parse_error is not None - - -def test_hypothesis_agent_extracts_sexp_from_markdown_fence(mocker): - fake_llm = mocker.MagicMock() - fake_llm.complete.return_value = CompletionResult( - text="Ecco la strategia:\n```lisp\n(strategy (when (lt (indicator rsi 14) 30.0) (entry-long)))\n```\nFatta.", - input_tokens=200, output_tokens=80, tier=ModelTier.C, model="qwen", - ) - g = HypothesisAgentGenome( - system_prompt="x", feature_access=["close"], temperature=0.9, - top_p=0.95, model_tier=ModelTier.C, lookback_window=200, cognitive_style="physicist", - ) - agent = HypothesisAgent(llm=fake_llm) - proposal = agent.propose(g, make_summary()) - assert proposal.strategy is not None -``` - -- [ ] **Step 2: Run test (deve fallire)** - -Run: `uv run pytest tests/unit/test_hypothesis_agent.py -v` -Expected: FAIL. - -- [ ] **Step 3: Implementare agent** - -```python -# src/multi_swarm/agents/__init__.py -``` - -```python -# src/multi_swarm/agents/hypothesis.py -from __future__ import annotations - -import re -from dataclasses import dataclass - -from ..genome.hypothesis import HypothesisAgentGenome -from ..llm.client import CompletionResult, LLMClient -from ..protocol.parser import ParseError, Strategy, parse_strategy -from ..protocol.validator import ValidationError, validate_strategy - - -@dataclass(frozen=True) -class MarketSummary: - symbol: str - timeframe: str - n_bars: int - return_mean: float - return_std: float - skew: float - kurtosis: float - volatility_regime: str - - -@dataclass(frozen=True) -class HypothesisProposal: - strategy: Strategy | None - raw_text: str - completion: CompletionResult - parse_error: str | None = None - - -SYSTEM_TEMPLATE = """\ -Sei un agente generatore di ipotesi di trading quantitativo per un sistema swarm. - -Il tuo stile cognitivo: {cognitive_style} -Direttiva personale: {system_prompt} - -Devi proporre una strategia di trading espressa nel linguaggio S-expression -con i seguenti verbi disponibili: - - Azioni: entry-long, entry-short, exit, flat - Logici: and, or, not - Comparatori: gt, lt, eq - Dati: feature, indicator, crossover, crossunder - -Indicatori disponibili: sma , rsi , atr , macd, realized_vol . -Feature disponibili: open, high, low, close, volume. - -Le regole sono valutate in ordine; la prima che matcha vince per ogni timestamp. -La default action se nessuna regola matcha è 'flat'. - -Rispondi SOLO con la S-expression in un fence ```lisp ... ```, senza prosa, -senza spiegazioni. Esempio formato: - -```lisp -(strategy - (when (gt (indicator rsi 14) 70.0) (entry-short)) - (when (lt (indicator rsi 14) 30.0) (entry-long))) -``` -""" - - -USER_TEMPLATE = """\ -Mercato: {symbol} timeframe {timeframe}, {n_bars} barre osservate. -Statistiche return: mean={return_mean:.5f}, std={return_std:.5f}, skew={skew:.3f}, kurt={kurtosis:.3f}. -Regime volatilità: {volatility_regime}. - -Feature accessibili dal tuo genoma: {feature_access}. -Lookback massimo che puoi usare nel ragionamento: {lookback_window} barre. - -Genera una strategia che cerchi anomalie sfruttabili in questo regime. -""" - - -_SEXP_FENCE_RE = re.compile(r"```(?:lisp|scheme|sexp)?\s*(\(strategy[\s\S]*?\))\s*```", re.MULTILINE) - - -def _extract_sexp(text: str) -> str | None: - m = _SEXP_FENCE_RE.search(text) - if m: - return m.group(1) - if text.strip().startswith("(strategy"): - return text.strip() - return None - - -class HypothesisAgent: - def __init__(self, llm: LLMClient): - self._llm = llm - - def propose( - self, - genome: HypothesisAgentGenome, - market: MarketSummary, - ) -> HypothesisProposal: - system = SYSTEM_TEMPLATE.format( - cognitive_style=genome.cognitive_style, - system_prompt=genome.system_prompt, - ) - user = USER_TEMPLATE.format( - symbol=market.symbol, - timeframe=market.timeframe, - n_bars=market.n_bars, - return_mean=market.return_mean, - return_std=market.return_std, - skew=market.skew, - kurtosis=market.kurtosis, - volatility_regime=market.volatility_regime, - feature_access=", ".join(genome.feature_access), - lookback_window=genome.lookback_window, - ) - - completion = self._llm.complete(genome, system=system, user=user) - - sexp = _extract_sexp(completion.text) - if sexp is None: - return HypothesisProposal( - strategy=None, raw_text=completion.text, completion=completion, - parse_error="no s-expression found in output", - ) - try: - ast = parse_strategy(sexp) - validate_strategy(ast) - return HypothesisProposal( - strategy=ast, raw_text=completion.text, completion=completion, - ) - except (ParseError, ValidationError) as e: - return HypothesisProposal( - strategy=None, raw_text=completion.text, completion=completion, - parse_error=str(e), - ) -``` - -- [ ] **Step 4: Run test (deve passare)** - -Run: `uv run pytest tests/unit/test_hypothesis_agent.py -v` -Expected: PASS. - -- [ ] **Step 5: Commit** - -```bash -git add src/multi_swarm/agents/ tests/unit/test_hypothesis_agent.py -git commit -m "feat(agents): hypothesis agent with prompt template + s-expr extraction" -``` - ---- - -## Task 20: Falsification agent (hand-crafted) - -**Files:** -- Create: `src/multi_swarm/agents/falsification.py` -- Test: `tests/unit/test_falsification.py` - -In Phase 1 il Falsification è completamente deterministic: prende una strategy AST, la compila, fa girare il backtest sul training set, calcola DSR + drawdown + altre metriche, restituisce un `FalsificationReport`. - -- [ ] **Step 1: Scrivere test fallente** - -```python -# tests/unit/test_falsification.py -from datetime import datetime, timezone -import numpy as np -import pandas as pd -import pytest -from multi_swarm.agents.falsification import FalsificationAgent, FalsificationReport -from multi_swarm.protocol.parser import parse_strategy - - -@pytest.fixture -def trending_ohlcv(): - idx = pd.date_range("2024-01-01", periods=500, freq="1h", tz="UTC") - close = 100 + np.cumsum(np.random.RandomState(0).normal(0.01, 1.0, 500)) - return pd.DataFrame( - {"open": close, "high": close + 0.5, "low": close - 0.5, "close": close, "volume": 1.0}, - index=idx, - ) - - -def test_falsification_returns_report(trending_ohlcv): - src = "(strategy (when (gt (indicator rsi 14) 70.0) (entry-short)) (when (lt (indicator rsi 14) 30.0) (entry-long)))" - ast = parse_strategy(src) - agent = FalsificationAgent(fees_bp=5.0, n_trials_dsr=20) - report = agent.evaluate(ast, trending_ohlcv) - assert isinstance(report, FalsificationReport) - assert isinstance(report.sharpe, float) - assert isinstance(report.dsr, float) - assert 0.0 <= report.dsr <= 1.0 - assert isinstance(report.max_drawdown, float) - assert isinstance(report.n_trades, int) - - -def test_falsification_zero_trades_returns_zero_metrics(trending_ohlcv): - src = "(strategy (when (gt (feature close) 1e9) (entry-long)))" - ast = parse_strategy(src) - agent = FalsificationAgent(fees_bp=5.0, n_trials_dsr=20) - report = agent.evaluate(ast, trending_ohlcv) - assert report.n_trades == 0 - assert report.sharpe == 0.0 -``` - -- [ ] **Step 2: Run test (deve fallire)** - -Run: `uv run pytest tests/unit/test_falsification.py -v` -Expected: FAIL. - -- [ ] **Step 3: Implementare falsification** - -```python -# src/multi_swarm/agents/falsification.py -from __future__ import annotations - -from dataclasses import dataclass - -import pandas as pd - -from ..backtest.engine import BacktestEngine -from ..metrics.basic import max_drawdown, sharpe_ratio, total_return -from ..metrics.dsr import deflated_sharpe_ratio -from ..protocol.compiler import compile_strategy -from ..protocol.parser import Strategy - - -@dataclass(frozen=True) -class FalsificationReport: - sharpe: float - dsr: float - dsr_pvalue: float - max_drawdown: float - total_return: float - n_trades: int - n_bars: int - - -class FalsificationAgent: - def __init__(self, fees_bp: float = 5.0, n_trials_dsr: int = 50): - self._engine = BacktestEngine(fees_bp=fees_bp) - self._n_trials_dsr = n_trials_dsr - - def evaluate(self, strategy: Strategy, ohlcv: pd.DataFrame) -> FalsificationReport: - signal_fn = compile_strategy(strategy) - signals = signal_fn(ohlcv) - result = self._engine.run(ohlcv, signals) - - if len(result.trades) == 0: - return FalsificationReport( - sharpe=0.0, dsr=0.0, dsr_pvalue=1.0, max_drawdown=0.0, - total_return=0.0, n_trades=0, n_bars=len(ohlcv), - ) - - sr = sharpe_ratio(result.returns, periods_per_year=8760) - dsr, p = deflated_sharpe_ratio( - result.returns, - n_trials=self._n_trials_dsr, - periods_per_year=8760, - sharpe_var=1.0, - ) - return FalsificationReport( - sharpe=sr, - dsr=dsr, - dsr_pvalue=p, - max_drawdown=max_drawdown(result.equity_curve + 1.0), # +1 evita div per 0 - total_return=total_return(result.equity_curve + 1.0), - n_trades=len(result.trades), - n_bars=len(ohlcv), - ) -``` - -- [ ] **Step 4: Run test (deve passare)** - -Run: `uv run pytest tests/unit/test_falsification.py -v` -Expected: PASS. - -- [ ] **Step 5: Commit** - -```bash -git add src/multi_swarm/agents/falsification.py tests/unit/test_falsification.py -git commit -m "feat(agents): hand-crafted falsification (compile→backtest→DSR)" -``` - ---- - -## Task 21: Adversarial agent (hand-crafted) - -**Files:** -- Create: `src/multi_swarm/agents/adversarial.py` -- Test: `tests/unit/test_adversarial.py` - -In Phase 1 l'Adversarial è hand-crafted con check euristici deterministic, no LLM. Verifica: -- `lookahead_check`: il numero di trade è coerente con i segnali (no trade su barra t senza segnale a t-1). -- `degenerate_check`: la strategia non è banale (es. sempre long, sempre flat). -- `trade_frequency_check`: troppi trade (>1 ogni 5 bar) = strategia rumorosa, flag warning. -- `single_trade_check`: 1-2 trade su 500 barre = lucky shot, flag warning. - -- [ ] **Step 1: Scrivere test fallente** - -```python -# tests/unit/test_adversarial.py -import numpy as np -import pandas as pd -import pytest -from multi_swarm.agents.adversarial import AdversarialAgent, AdversarialReport, Severity -from multi_swarm.protocol.parser import parse_strategy - - -@pytest.fixture -def ohlcv(): - idx = pd.date_range("2024-01-01", periods=500, freq="1h", tz="UTC") - close = 100 + np.cumsum(np.random.RandomState(0).normal(0.0, 1.0, 500)) - return pd.DataFrame( - {"open": close, "high": close + 0.5, "low": close - 0.5, "close": close, "volume": 1.0}, - index=idx, - ) - - -def test_degenerate_always_long_flagged(ohlcv): - src = "(strategy (when (gt (feature close) -1e9) (entry-long)))" - ast = parse_strategy(src) - agent = AdversarialAgent() - report = agent.review(ast, ohlcv) - assert any(f.name == "degenerate" and f.severity == Severity.HIGH for f in report.findings) - - -def test_no_findings_on_reasonable_strategy(ohlcv): - src = "(strategy (when (gt (indicator rsi 14) 70.0) (entry-short)) (when (lt (indicator rsi 14) 30.0) (entry-long)))" - ast = parse_strategy(src) - agent = AdversarialAgent() - report = agent.review(ast, ohlcv) - high_findings = [f for f in report.findings if f.severity == Severity.HIGH] - assert len(high_findings) == 0 - - -def test_zero_trade_strategy_flagged(ohlcv): - src = "(strategy (when (gt (feature close) 1e9) (entry-long)))" - ast = parse_strategy(src) - agent = AdversarialAgent() - report = agent.review(ast, ohlcv) - assert any(f.name == "no_trades" for f in report.findings) -``` - -- [ ] **Step 2: Run test (deve fallire)** - -Run: `uv run pytest tests/unit/test_adversarial.py -v` -Expected: FAIL. - -- [ ] **Step 3: Implementare adversarial** - -```python -# src/multi_swarm/agents/adversarial.py -from __future__ import annotations - -from dataclasses import dataclass, field -from enum import Enum - -import pandas as pd - -from ..backtest.engine import BacktestEngine -from ..backtest.orders import Side -from ..protocol.compiler import compile_strategy -from ..protocol.parser import Strategy - - -class Severity(str, Enum): - LOW = "low" - MEDIUM = "medium" - HIGH = "high" - - -@dataclass(frozen=True) -class Finding: - name: str - severity: Severity - detail: str - - -@dataclass -class AdversarialReport: - findings: list[Finding] = field(default_factory=list) - - -class AdversarialAgent: - def __init__(self, fees_bp: float = 5.0): - self._engine = BacktestEngine(fees_bp=fees_bp) - - def review(self, strategy: Strategy, ohlcv: pd.DataFrame) -> AdversarialReport: - signal_fn = compile_strategy(strategy) - signals = signal_fn(ohlcv) - result = self._engine.run(ohlcv, signals) - - report = AdversarialReport() - - if len(result.trades) == 0: - report.findings.append(Finding( - name="no_trades", severity=Severity.HIGH, - detail="Strategy never opens a position on training data", - )) - return report - - unique_signals = signals.unique() - if len(unique_signals) == 1 and unique_signals[0] in (Side.LONG, Side.SHORT): - report.findings.append(Finding( - name="degenerate", severity=Severity.HIGH, - detail=f"Strategy is always {unique_signals[0].value}, no real decision", - )) - - n_bars = len(ohlcv) - n_trades = len(result.trades) - if n_trades > n_bars / 5: - report.findings.append(Finding( - name="overtrading", severity=Severity.MEDIUM, - detail=f"{n_trades} trades on {n_bars} bars (>1 per 5 bars)", - )) - if n_trades < 5: - report.findings.append(Finding( - name="undertrading", severity=Severity.MEDIUM, - detail=f"only {n_trades} trades — likely lucky shot", - )) - - return report -``` - -- [ ] **Step 4: Run test (deve passare)** - -Run: `uv run pytest tests/unit/test_adversarial.py -v` -Expected: PASS. - -- [ ] **Step 5: Commit** - -```bash -git add src/multi_swarm/agents/adversarial.py tests/unit/test_adversarial.py -git commit -m "feat(agents): hand-crafted adversarial with heuristic checks" -``` - ---- - -## Task 22: Fitness function v0 - -**Files:** -- Create: `src/multi_swarm/ga/__init__.py` -- Create: `src/multi_swarm/ga/fitness.py` -- Test: `tests/unit/test_fitness.py` - -Fitness v0: `dsr - drawdown_penalty * max_drawdown`. Default `drawdown_penalty = 0.5`. Strategy con 0 trade = fitness 0 (non penalizzata negativamente, ma neutrale). - -- [ ] **Step 1: Scrivere test fallente** - -```python -# tests/unit/test_fitness.py -from multi_swarm.agents.falsification import FalsificationReport -from multi_swarm.agents.adversarial import AdversarialReport, Finding, Severity -from multi_swarm.ga.fitness import compute_fitness - - -def make_falsification(dsr=0.7, max_dd=0.2, n_trades=30): - return FalsificationReport( - sharpe=1.5, dsr=dsr, dsr_pvalue=0.05, max_drawdown=max_dd, - total_return=0.3, n_trades=n_trades, n_bars=500, - ) - - -def test_fitness_zero_trades_is_zero(): - f = make_falsification(n_trades=0) - a = AdversarialReport() - assert compute_fitness(f, a) == 0.0 - - -def test_fitness_increases_with_dsr(): - a = AdversarialReport() - f1 = make_falsification(dsr=0.5) - f2 = make_falsification(dsr=0.9) - assert compute_fitness(f2, a) > compute_fitness(f1, a) - - -def test_fitness_decreases_with_drawdown(): - a = AdversarialReport() - f1 = make_falsification(max_dd=0.1) - f2 = make_falsification(max_dd=0.4) - assert compute_fitness(f1, a) > compute_fitness(f2, a) - - -def test_fitness_zeroed_by_high_severity_finding(): - f = make_falsification() - a = AdversarialReport(findings=[Finding(name="degenerate", severity=Severity.HIGH, detail="x")]) - assert compute_fitness(f, a) == 0.0 -``` - -- [ ] **Step 2: Run test (deve fallire)** - -Run: `uv run pytest tests/unit/test_fitness.py -v` -Expected: FAIL. - -- [ ] **Step 3: Implementare fitness** - -```python -# src/multi_swarm/ga/__init__.py -``` - -```python -# src/multi_swarm/ga/fitness.py -from __future__ import annotations - -from ..agents.adversarial import AdversarialReport, Severity -from ..agents.falsification import FalsificationReport - - -def compute_fitness( - falsification: FalsificationReport, - adversarial: AdversarialReport, - drawdown_penalty: float = 0.5, -) -> float: - """Fitness v0 Phase 1. - - Logica: - 1. Se 0 trade → fitness 0. - 2. Se almeno un finding HIGH adversarial → fitness 0 (kill). - 3. Altrimenti: dsr - drawdown_penalty * max_drawdown, clamped a 0. - """ - if falsification.n_trades == 0: - return 0.0 - if any(f.severity == Severity.HIGH for f in adversarial.findings): - return 0.0 - raw = falsification.dsr - drawdown_penalty * falsification.max_drawdown - return max(0.0, float(raw)) -``` - -- [ ] **Step 4: Run test (deve passare)** - -Run: `uv run pytest tests/unit/test_fitness.py -v` -Expected: PASS. - -- [ ] **Step 5: Commit** - -```bash -git add src/multi_swarm/ga/ tests/unit/test_fitness.py -git commit -m "feat(ga): fitness v0 (DSR - dd_penalty * max_dd, kill on adversarial high)" -``` - ---- - -## Task 23: GA — tournament selection + elitism - -**Files:** -- Create: `src/multi_swarm/ga/selection.py` -- Test: `tests/unit/test_selection.py` - -- [ ] **Step 1: Scrivere test fallente** - -```python -# tests/unit/test_selection.py -import random -from multi_swarm.genome.hypothesis import HypothesisAgentGenome, ModelTier -from multi_swarm.ga.selection import tournament_select, elite_select - - -def make(idx: int) -> HypothesisAgentGenome: - return HypothesisAgentGenome( - system_prompt=f"p-{idx}", feature_access=["close"], temperature=0.9, - top_p=0.95, model_tier=ModelTier.C, lookback_window=100, cognitive_style="x", - ) - - -def test_tournament_picks_best_in_sample(): - population = [make(i) for i in range(10)] - fitnesses = {g.id: float(i) for i, g in enumerate(population)} - rng = random.Random(0) - winner = tournament_select(population, fitnesses, k=5, rng=rng) - assert isinstance(winner, HypothesisAgentGenome) - assert fitnesses[winner.id] >= 0.0 - - -def test_tournament_size_one_is_random(): - population = [make(i) for i in range(10)] - fitnesses = {g.id: float(i) for i, g in enumerate(population)} - rng = random.Random(0) - picks = [tournament_select(population, fitnesses, k=1, rng=rng) for _ in range(50)] - distinct = {p.id for p in picks} - assert len(distinct) > 1 - - -def test_elite_select_returns_top_k(): - population = [make(i) for i in range(10)] - fitnesses = {g.id: float(i) for i, g in enumerate(population)} - elites = elite_select(population, fitnesses, k=3) - elite_fitnesses = sorted([fitnesses[g.id] for g in elites], reverse=True) - assert elite_fitnesses == [9.0, 8.0, 7.0] -``` - -- [ ] **Step 2: Run test (deve fallire)** - -Run: `uv run pytest tests/unit/test_selection.py -v` -Expected: FAIL. - -- [ ] **Step 3: Implementare selection** - -```python -# src/multi_swarm/ga/selection.py -from __future__ import annotations - -import random - -from ..genome.hypothesis import HypothesisAgentGenome - - -def tournament_select( - population: list[HypothesisAgentGenome], - fitnesses: dict[str, float], - k: int, - rng: random.Random, -) -> HypothesisAgentGenome: - """Estrae k individui random e restituisce il migliore.""" - if k < 1: - raise ValueError("k must be >= 1") - if not population: - raise ValueError("empty population") - candidates = rng.sample(population, k=min(k, len(population))) - return max(candidates, key=lambda g: fitnesses.get(g.id, 0.0)) - - -def elite_select( - population: list[HypothesisAgentGenome], - fitnesses: dict[str, float], - k: int, -) -> list[HypothesisAgentGenome]: - """Restituisce i k genomi con fitness più alta.""" - sorted_pop = sorted(population, key=lambda g: fitnesses.get(g.id, 0.0), reverse=True) - return sorted_pop[:k] -``` - -- [ ] **Step 4: Run test (deve passare)** - -Run: `uv run pytest tests/unit/test_selection.py -v` -Expected: PASS. - -- [ ] **Step 5: Commit** - -```bash -git add src/multi_swarm/ga/selection.py tests/unit/test_selection.py -git commit -m "feat(ga): tournament selection + elitism" -``` - ---- - -## Task 24: GA — generation step (loop di una generazione) - -**Files:** -- Create: `src/multi_swarm/ga/loop.py` -- Test: `tests/unit/test_ga_loop.py` - -`step()`: dato (popolazione, fitnesses, RNG, config), produce la prossima popolazione tramite elitism + tournament selection + (mutation OR crossover) per riempire i restanti slot. - -- [ ] **Step 1: Scrivere test fallente** - -```python -# tests/unit/test_ga_loop.py -import random -from multi_swarm.genome.hypothesis import HypothesisAgentGenome, ModelTier -from multi_swarm.ga.loop import next_generation, GAConfig - - -def make(idx: int) -> HypothesisAgentGenome: - return HypothesisAgentGenome( - system_prompt=f"p-{idx}", feature_access=["close"], temperature=0.9, - top_p=0.95, model_tier=ModelTier.C, lookback_window=100, cognitive_style="x", - ) - - -def test_next_generation_size_preserved(): - population = [make(i) for i in range(20)] - fitnesses = {g.id: float(i) for i, g in enumerate(population)} - cfg = GAConfig(population_size=20, elite_k=2, tournament_k=3, p_crossover=0.5) - new_pop = next_generation(population, fitnesses, cfg, rng=random.Random(0)) - assert len(new_pop) == 20 - - -def test_next_generation_includes_elites(): - population = [make(i) for i in range(20)] - fitnesses = {g.id: float(i) for i, g in enumerate(population)} - cfg = GAConfig(population_size=20, elite_k=2, tournament_k=3, p_crossover=0.5) - new_pop = next_generation(population, fitnesses, cfg, rng=random.Random(0)) - elite_ids = {g.id for g in sorted(population, key=lambda g: fitnesses[g.id], reverse=True)[:2]} - new_ids = {g.id for g in new_pop} - assert elite_ids.issubset(new_ids) - - -def test_next_generation_increments_generation_for_offspring(): - population = [make(i) for i in range(20)] - fitnesses = {g.id: float(i) for i, g in enumerate(population)} - cfg = GAConfig(population_size=20, elite_k=2, tournament_k=3, p_crossover=0.5) - new_pop = next_generation(population, fitnesses, cfg, rng=random.Random(0)) - new_offspring = [g for g in new_pop if g.id not in {p.id for p in population}] - assert all(g.generation > 0 for g in new_offspring) -``` - -- [ ] **Step 2: Run test (deve fallire)** - -Run: `uv run pytest tests/unit/test_ga_loop.py -v` -Expected: FAIL. - -- [ ] **Step 3: Implementare loop** - -```python -# src/multi_swarm/ga/loop.py -from __future__ import annotations - -import random -from dataclasses import dataclass - -from ..genome.crossover import uniform_crossover -from ..genome.hypothesis import HypothesisAgentGenome -from ..genome.mutation import random_mutate -from .selection import elite_select, tournament_select - - -@dataclass(frozen=True) -class GAConfig: - population_size: int - elite_k: int - tournament_k: int - p_crossover: float - - -def next_generation( - population: list[HypothesisAgentGenome], - fitnesses: dict[str, float], - cfg: GAConfig, - rng: random.Random, -) -> list[HypothesisAgentGenome]: - new_pop: list[HypothesisAgentGenome] = list(elite_select(population, fitnesses, cfg.elite_k)) - - while len(new_pop) < cfg.population_size: - if rng.random() < cfg.p_crossover and len(population) >= 2: - p1 = tournament_select(population, fitnesses, cfg.tournament_k, rng) - p2 = tournament_select(population, fitnesses, cfg.tournament_k, rng) - child = uniform_crossover(p1, p2, rng) - else: - parent = tournament_select(population, fitnesses, cfg.tournament_k, rng) - child = random_mutate(parent, rng) - new_pop.append(child) - - return new_pop[: cfg.population_size] -``` - -- [ ] **Step 4: Run test (deve passare)** - -Run: `uv run pytest tests/unit/test_ga_loop.py -v` -Expected: PASS. - -- [ ] **Step 5: Commit** - -```bash -git add src/multi_swarm/ga/loop.py tests/unit/test_ga_loop.py -git commit -m "feat(ga): next_generation step (elitism + tournament + mutate/crossover)" -``` - ---- - -## Task 25: SQLite schema + repository - -**Files:** -- Create: `src/multi_swarm/persistence/__init__.py` -- Create: `src/multi_swarm/persistence/schema.py` -- Create: `src/multi_swarm/persistence/repository.py` -- Test: `tests/unit/test_repository.py` - -Schema essenziale Phase 1: -- `runs(id, name, started_at, completed_at, status, config_json, total_cost_usd)` -- `generations(run_id, generation_idx, started_at, completed_at, n_genomes, fitness_median, fitness_max, fitness_p90, entropy)` -- `genomes(id, run_id, generation_idx, payload_json)` -- `evaluations(genome_id, run_id, fitness, dsr, dsr_pvalue, sharpe, max_dd, total_return, n_trades, parse_error, raw_text, eval_ts)` -- `cost_records(id, run_id, agent_id, ts, tier, input_tokens, output_tokens, cost_usd)` -- `adversarial_findings(genome_id, run_id, name, severity, detail)` - -- [ ] **Step 1: Scrivere test fallente** - -```python -# tests/unit/test_repository.py -from pathlib import Path -import json -from multi_swarm.persistence.repository import Repository -from multi_swarm.genome.hypothesis import HypothesisAgentGenome, ModelTier - - -def make_genome(idx: int) -> HypothesisAgentGenome: - return HypothesisAgentGenome( - system_prompt=f"p-{idx}", feature_access=["close"], temperature=0.9, - top_p=0.95, model_tier=ModelTier.C, lookback_window=100, cognitive_style="x", - ) - - -def test_repository_creates_schema(tmp_path: Path): - repo = Repository(db_path=tmp_path / "runs.db") - repo.init_schema() - assert (tmp_path / "runs.db").exists() - - -def test_repository_create_run_and_get(tmp_path: Path): - repo = Repository(db_path=tmp_path / "runs.db") - repo.init_schema() - run_id = repo.create_run(name="phase1-test", config={"k": 20}) - run = repo.get_run(run_id) - assert run["name"] == "phase1-test" - assert json.loads(run["config_json"])["k"] == 20 - - -def test_repository_save_genome_and_evaluation(tmp_path: Path): - repo = Repository(db_path=tmp_path / "runs.db") - repo.init_schema() - run_id = repo.create_run(name="t", config={}) - g = make_genome(0) - repo.save_genome(run_id=run_id, generation_idx=0, genome=g) - repo.save_evaluation( - run_id=run_id, genome_id=g.id, fitness=0.5, dsr=0.7, dsr_pvalue=0.05, - sharpe=1.5, max_dd=0.2, total_return=0.3, n_trades=30, - parse_error=None, raw_text="(strategy ...)", - ) - evals = repo.list_evaluations(run_id) - assert len(evals) == 1 - assert evals[0]["fitness"] == 0.5 - - -def test_repository_save_generation_summary(tmp_path: Path): - repo = Repository(db_path=tmp_path / "runs.db") - repo.init_schema() - run_id = repo.create_run(name="t", config={}) - repo.save_generation_summary( - run_id=run_id, generation_idx=0, n_genomes=20, - fitness_median=0.3, fitness_max=0.8, fitness_p90=0.7, entropy=0.85, - ) - gens = repo.list_generations(run_id) - assert len(gens) == 1 - assert gens[0]["fitness_max"] == 0.8 -``` - -- [ ] **Step 2: Run test (deve fallire)** - -Run: `uv run pytest tests/unit/test_repository.py -v` -Expected: FAIL. - -- [ ] **Step 3: Implementare schema + repository** - -```python -# src/multi_swarm/persistence/__init__.py -``` - -```python -# src/multi_swarm/persistence/schema.py -SCHEMA_SQL = """ -CREATE TABLE IF NOT EXISTS runs ( - id TEXT PRIMARY KEY, - name TEXT NOT NULL, - started_at TEXT NOT NULL, - completed_at TEXT, - status TEXT NOT NULL DEFAULT 'running', - config_json TEXT NOT NULL, - total_cost_usd REAL NOT NULL DEFAULT 0.0 -); - -CREATE TABLE IF NOT EXISTS generations ( - run_id TEXT NOT NULL, - generation_idx INTEGER NOT NULL, - started_at TEXT, - completed_at TEXT, - n_genomes INTEGER NOT NULL, - fitness_median REAL NOT NULL, - fitness_max REAL NOT NULL, - fitness_p90 REAL NOT NULL, - entropy REAL NOT NULL, - PRIMARY KEY (run_id, generation_idx), - FOREIGN KEY (run_id) REFERENCES runs(id) -); - -CREATE TABLE IF NOT EXISTS genomes ( - id TEXT NOT NULL, - run_id TEXT NOT NULL, - generation_idx INTEGER NOT NULL, - payload_json TEXT NOT NULL, - PRIMARY KEY (id, run_id, generation_idx), - FOREIGN KEY (run_id) REFERENCES runs(id) -); - -CREATE TABLE IF NOT EXISTS evaluations ( - run_id TEXT NOT NULL, - genome_id TEXT NOT NULL, - fitness REAL NOT NULL, - dsr REAL NOT NULL, - dsr_pvalue REAL NOT NULL, - sharpe REAL NOT NULL, - max_dd REAL NOT NULL, - total_return REAL NOT NULL, - n_trades INTEGER NOT NULL, - parse_error TEXT, - raw_text TEXT, - eval_ts TEXT NOT NULL, - PRIMARY KEY (run_id, genome_id), - FOREIGN KEY (run_id) REFERENCES runs(id) -); - -CREATE TABLE IF NOT EXISTS cost_records ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - run_id TEXT NOT NULL, - agent_id TEXT NOT NULL, - ts TEXT NOT NULL, - tier TEXT NOT NULL, - input_tokens INTEGER NOT NULL, - output_tokens INTEGER NOT NULL, - cost_usd REAL NOT NULL, - FOREIGN KEY (run_id) REFERENCES runs(id) -); - -CREATE TABLE IF NOT EXISTS adversarial_findings ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - run_id TEXT NOT NULL, - genome_id TEXT NOT NULL, - name TEXT NOT NULL, - severity TEXT NOT NULL, - detail TEXT NOT NULL, - FOREIGN KEY (run_id) REFERENCES runs(id) -); - -CREATE INDEX IF NOT EXISTS idx_evaluations_fitness ON evaluations(run_id, fitness DESC); -CREATE INDEX IF NOT EXISTS idx_genomes_generation ON genomes(run_id, generation_idx); -CREATE INDEX IF NOT EXISTS idx_cost_run ON cost_records(run_id); -""" -``` - -```python -# src/multi_swarm/persistence/repository.py -from __future__ import annotations - -import json -import sqlite3 -import uuid -from datetime import datetime, timezone -from pathlib import Path -from typing import Any - -from ..genome.hypothesis import HypothesisAgentGenome -from .schema import SCHEMA_SQL - - -class Repository: - def __init__(self, db_path: Path | str): - self.db_path = Path(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 - - def init_schema(self) -> None: - self.db_path.parent.mkdir(parents=True, exist_ok=True) - with self._conn() as conn: - conn.executescript(SCHEMA_SQL) - - @staticmethod - def _now() -> str: - return datetime.now(timezone.utc).isoformat() - - # runs - def create_run(self, name: str, config: dict[str, Any]) -> str: - rid = uuid.uuid4().hex - with self._conn() as conn: - conn.execute( - "INSERT INTO runs (id, name, started_at, status, config_json) VALUES (?,?,?,?,?)", - (rid, name, self._now(), "running", json.dumps(config)), - ) - return rid - - def complete_run(self, run_id: str, total_cost: float, status: str = "completed") -> None: - with self._conn() as conn: - conn.execute( - "UPDATE runs SET completed_at=?, status=?, total_cost_usd=? WHERE id=?", - (self._now(), status, total_cost, run_id), - ) - - def get_run(self, run_id: str) -> dict[str, Any]: - with self._conn() as conn: - row = conn.execute("SELECT * FROM runs WHERE id=?", (run_id,)).fetchone() - if row is None: - raise KeyError(run_id) - return dict(row) - - def list_runs(self) -> list[dict[str, Any]]: - with self._conn() as conn: - rows = conn.execute("SELECT * FROM runs ORDER BY started_at DESC").fetchall() - return [dict(r) for r in rows] - - # generations - def save_generation_summary( - self, run_id: str, generation_idx: int, n_genomes: int, - fitness_median: float, fitness_max: float, fitness_p90: float, entropy: float, - ) -> None: - with self._conn() as conn: - conn.execute( - """INSERT OR REPLACE INTO generations - (run_id, generation_idx, completed_at, n_genomes, - fitness_median, fitness_max, fitness_p90, entropy) - VALUES (?,?,?,?,?,?,?,?)""", - (run_id, generation_idx, self._now(), n_genomes, - fitness_median, fitness_max, fitness_p90, entropy), - ) - - def list_generations(self, run_id: str) -> list[dict[str, Any]]: - with self._conn() as conn: - rows = conn.execute( - "SELECT * FROM generations WHERE run_id=? ORDER BY generation_idx", - (run_id,), - ).fetchall() - return [dict(r) for r in rows] - - # genomes - def save_genome(self, run_id: str, generation_idx: int, genome: HypothesisAgentGenome) -> None: - with self._conn() as conn: - conn.execute( - "INSERT OR REPLACE INTO genomes (id, run_id, generation_idx, payload_json) VALUES (?,?,?,?)", - (genome.id, run_id, generation_idx, json.dumps(genome.to_dict())), - ) - - def list_genomes(self, run_id: str, generation_idx: int | None = None) -> list[dict[str, Any]]: - with self._conn() as conn: - if generation_idx is None: - rows = conn.execute( - "SELECT * FROM genomes WHERE run_id=? ORDER BY generation_idx, id", (run_id,), - ).fetchall() - else: - rows = conn.execute( - "SELECT * FROM genomes WHERE run_id=? AND generation_idx=? ORDER BY id", - (run_id, generation_idx), - ).fetchall() - return [dict(r) for r in rows] - - # evaluations - def save_evaluation( - self, run_id: str, genome_id: str, fitness: float, dsr: float, dsr_pvalue: float, - sharpe: float, max_dd: float, total_return: float, n_trades: int, - parse_error: str | None, raw_text: str | None, - ) -> None: - with self._conn() as conn: - conn.execute( - """INSERT OR REPLACE INTO evaluations - (run_id, genome_id, fitness, dsr, dsr_pvalue, sharpe, max_dd, - total_return, n_trades, parse_error, raw_text, eval_ts) - VALUES (?,?,?,?,?,?,?,?,?,?,?,?)""", - (run_id, genome_id, fitness, dsr, dsr_pvalue, sharpe, max_dd, - total_return, n_trades, parse_error, raw_text, self._now()), - ) - - def list_evaluations(self, run_id: str) -> list[dict[str, Any]]: - with self._conn() as conn: - rows = conn.execute( - "SELECT * FROM evaluations WHERE run_id=? ORDER BY fitness DESC", - (run_id,), - ).fetchall() - return [dict(r) for r in rows] - - # cost - def save_cost_record( - self, run_id: str, agent_id: str, tier: str, - input_tokens: int, output_tokens: int, cost_usd: float, - ) -> None: - with self._conn() as conn: - conn.execute( - """INSERT INTO cost_records - (run_id, agent_id, ts, tier, input_tokens, output_tokens, cost_usd) - VALUES (?,?,?,?,?,?,?)""", - (run_id, agent_id, self._now(), tier, input_tokens, output_tokens, cost_usd), - ) - - def total_cost(self, run_id: str) -> float: - with self._conn() as conn: - row = conn.execute( - "SELECT COALESCE(SUM(cost_usd), 0.0) AS c FROM cost_records WHERE run_id=?", - (run_id,), - ).fetchone() - return float(row["c"]) - - # adversarial - def save_adversarial_finding( - self, run_id: str, genome_id: str, name: str, severity: str, detail: str, - ) -> None: - with self._conn() as conn: - conn.execute( - """INSERT INTO adversarial_findings - (run_id, genome_id, name, severity, detail) VALUES (?,?,?,?,?)""", - (run_id, genome_id, name, severity, detail), - ) - - def list_adversarial_findings(self, run_id: str) -> list[dict[str, Any]]: - with self._conn() as conn: - rows = conn.execute( - "SELECT * FROM adversarial_findings WHERE run_id=? ORDER BY id", (run_id,), - ).fetchall() - return [dict(r) for r in rows] -``` - -- [ ] **Step 4: Run test (deve passare)** - -Run: `uv run pytest tests/unit/test_repository.py -v` -Expected: PASS. - -- [ ] **Step 5: Commit** - -```bash -git add src/multi_swarm/persistence/ tests/unit/test_repository.py -git commit -m "feat(persistence): SQLite schema + repository for runs/genomes/evals/cost" -``` - ---- - -## Task 26: Generation summary utilities (entropy, percentili) - -**Files:** -- Create: `src/multi_swarm/ga/summary.py` -- Test: `tests/unit/test_ga_summary.py` - -Helper per calcolare metriche aggregate di una generazione: median, max, p90, entropy della distribuzione di fitness (binned). L'entropy serve come gate Phase 1 (#4 dello spec). - -- [ ] **Step 1: Scrivere test fallente** - -```python -# tests/unit/test_ga_summary.py -import math -import pytest -from multi_swarm.ga.summary import generation_summary - - -def test_summary_basic_stats(): - fitnesses = [0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9] - s = generation_summary(fitnesses, n_bins=5) - assert s["median"] == pytest.approx(0.45, abs=0.05) - assert s["max"] == pytest.approx(0.9) - assert 0.0 <= s["entropy"] <= math.log(5) + 0.01 - - -def test_summary_uniform_high_entropy(): - fitnesses = [0.1 * i for i in range(20)] - s_uniform = generation_summary(fitnesses, n_bins=5) - s_concentrated = generation_summary([0.5] * 20, n_bins=5) - assert s_uniform["entropy"] > s_concentrated["entropy"] - - -def test_summary_p90(): - fitnesses = list(range(100)) - s = generation_summary([float(x) for x in fitnesses], n_bins=10) - assert 88.0 <= s["p90"] <= 91.0 -``` - -- [ ] **Step 2: Run test (deve fallire)** - -Run: `uv run pytest tests/unit/test_ga_summary.py -v` -Expected: FAIL. - -- [ ] **Step 3: Implementare summary** - -```python -# src/multi_swarm/ga/summary.py -from __future__ import annotations - -import math - -import numpy as np - - -def generation_summary(fitnesses: list[float], n_bins: int = 10) -> dict[str, float]: - arr = np.asarray(fitnesses, dtype=float) - if arr.size == 0: - return {"median": 0.0, "max": 0.0, "p90": 0.0, "entropy": 0.0} - median = float(np.median(arr)) - fmax = float(np.max(arr)) - p90 = float(np.percentile(arr, 90)) - - if fmax > 0: - normalized = arr / fmax - else: - normalized = arr - - hist, _ = np.histogram(normalized, bins=n_bins, range=(0.0, 1.0)) - probs = hist / hist.sum() if hist.sum() > 0 else hist - entropy = float(-sum(p * math.log(p) for p in probs if p > 0)) - - return {"median": median, "max": fmax, "p90": p90, "entropy": entropy} -``` - -- [ ] **Step 4: Run test (deve passare)** - -Run: `uv run pytest tests/unit/test_ga_summary.py -v` -Expected: PASS. - -- [ ] **Step 5: Commit** - -```bash -git add src/multi_swarm/ga/summary.py tests/unit/test_ga_summary.py -git commit -m "feat(ga): generation summary stats (median/max/p90/entropy)" -``` - ---- - -## Task 27: Initial population generator - -**Files:** -- Create: `src/multi_swarm/ga/initial.py` -- Test: `tests/unit/test_ga_initial.py` - -Genera popolazione iniziale K=20: stili cognitivi distribuiti uniformemente sui 6 stili, temperature random in [0.7, 1.2], lookback random in {100, 200, 300}, prompt generati da template fissi per ogni stile cognitivo. - -- [ ] **Step 1: Scrivere test fallente** - -```python -# tests/unit/test_ga_initial.py -import random -from multi_swarm.ga.initial import build_initial_population -from multi_swarm.genome.hypothesis import ModelTier - - -def test_initial_population_size(): - pop = build_initial_population(k=20, model_tier=ModelTier.C, rng=random.Random(0)) - assert len(pop) == 20 - - -def test_initial_population_unique_ids(): - pop = build_initial_population(k=20, model_tier=ModelTier.C, rng=random.Random(0)) - ids = {g.id for g in pop} - assert len(ids) == 20 - - -def test_initial_population_covers_all_styles(): - pop = build_initial_population(k=12, model_tier=ModelTier.C, rng=random.Random(0)) - styles = {g.cognitive_style for g in pop} - assert len(styles) == 6 - - -def test_initial_population_generation_zero(): - pop = build_initial_population(k=20, model_tier=ModelTier.C, rng=random.Random(0)) - assert all(g.generation == 0 for g in pop) - assert all(g.parent_ids == [] for g in pop) -``` - -- [ ] **Step 2: Run test (deve fallire)** - -Run: `uv run pytest tests/unit/test_ga_initial.py -v` -Expected: FAIL. - -- [ ] **Step 3: Implementare initial** - -```python -# src/multi_swarm/ga/initial.py -from __future__ import annotations - -import random - -from ..genome.hypothesis import HypothesisAgentGenome, ModelTier -from ..genome.mutation import COGNITIVE_STYLES - - -STYLE_PROMPTS: dict[str, str] = { - "physicist": "Cerca leggi conservative, simmetrie, regimi di scala. Pensa in termini di flussi e potenziali.", - "biologist": "Cerca pattern adattivi, nicchie ecologiche, predator-prey dynamics tra partecipanti del mercato.", - "historian": "Cerca pattern ricorrenti su scale temporali multiple, analogie con regimi storici, mean reversion strutturali.", - "meteorologist": "Cerca regimi di volatilità che si autoalimentano, transizioni di stato come fronti, persistenza locale.", - "ecologist": "Cerca interazioni multi-asset, correlazioni cluster, segnali di stress sistemico nelle dinamiche di flusso.", - "engineer": "Cerca segnali con rapporto S/N favorevole, filtri causali, robustezza a perturbazioni di calibrazione.", -} - - -def build_initial_population( - k: int, - model_tier: ModelTier, - rng: random.Random, - feature_pool: tuple[str, ...] = ("close", "high", "low", "volume"), -) -> list[HypothesisAgentGenome]: - """Costruisce una popolazione iniziale K varia per stile cognitivo + parametri.""" - population: list[HypothesisAgentGenome] = [] - for i in range(k): - style = COGNITIVE_STYLES[i % len(COGNITIVE_STYLES)] - n_features = rng.randint(1, len(feature_pool)) - feats = sorted(rng.sample(feature_pool, k=n_features)) - g = HypothesisAgentGenome( - system_prompt=STYLE_PROMPTS[style], - feature_access=feats, - temperature=round(rng.uniform(0.7, 1.2), 2), - top_p=0.95, - model_tier=model_tier, - lookback_window=rng.choice([100, 150, 200, 300]), - cognitive_style=style, - ) - # Seed per garantire id univoco se duplicato (raro ma possibile) - while any(g.id == p.id for p in population): - g = HypothesisAgentGenome( - system_prompt=g.system_prompt + f" [seed-{i}-{rng.randint(0, 1_000_000)}]", - feature_access=g.feature_access, - temperature=g.temperature, - top_p=g.top_p, - model_tier=g.model_tier, - lookback_window=g.lookback_window, - cognitive_style=g.cognitive_style, - ) - population.append(g) - return population -``` - -- [ ] **Step 4: Run test (deve passare)** - -Run: `uv run pytest tests/unit/test_ga_initial.py -v` -Expected: PASS. - -- [ ] **Step 5: Commit** - -```bash -git add src/multi_swarm/ga/initial.py tests/unit/test_ga_initial.py -git commit -m "feat(ga): initial population generator with cognitive style coverage" -``` - ---- - -## Task 28: Market summary builder (statistiche per il prompt) - -**Files:** -- Create: `src/multi_swarm/agents/market_summary.py` -- Test: `tests/unit/test_market_summary.py` - -Calcola le statistiche del training set che vengono iniettate nel prompt dell'Hypothesis agent. - -- [ ] **Step 1: Scrivere test fallente** - -```python -# tests/unit/test_market_summary.py -import numpy as np -import pandas as pd -from multi_swarm.agents.market_summary import build_market_summary - - -def test_build_summary_basic(): - idx = pd.date_range("2024-01-01", periods=200, freq="1h", tz="UTC") - np.random.seed(0) - close = 100 + np.cumsum(np.random.normal(0, 1, 200)) - df = pd.DataFrame( - {"open": close, "high": close + 0.5, "low": close - 0.5, "close": close, "volume": 1.0}, - index=idx, - ) - s = build_market_summary(df, symbol="BTC/USDT", timeframe="1h") - assert s.symbol == "BTC/USDT" - assert s.timeframe == "1h" - assert s.n_bars == 200 - assert isinstance(s.return_mean, float) - assert isinstance(s.return_std, float) - assert s.volatility_regime in {"low", "medium", "high"} - - -def test_volatility_regime_high_for_volatile(): - idx = pd.date_range("2024-01-01", periods=200, freq="1h", tz="UTC") - np.random.seed(0) - close = 100 + np.cumsum(np.random.normal(0, 5.0, 200)) # alta vol - df = pd.DataFrame( - {"open": close, "high": close + 0.5, "low": close - 0.5, "close": close, "volume": 1.0}, - index=idx, - ) - s = build_market_summary(df, symbol="BTC/USDT", timeframe="1h") - assert s.volatility_regime in {"medium", "high"} -``` - -- [ ] **Step 2: Run test (deve fallire)** - -Run: `uv run pytest tests/unit/test_market_summary.py -v` -Expected: FAIL. - -- [ ] **Step 3: Implementare market summary** - -```python -# src/multi_swarm/agents/market_summary.py -from __future__ import annotations - -import numpy as np -import pandas as pd -from scipy import stats - -from .hypothesis import MarketSummary - - -def build_market_summary( - ohlcv: pd.DataFrame, symbol: str, timeframe: str, -) -> MarketSummary: - returns = ohlcv["close"].pct_change().dropna() - return_mean = float(returns.mean()) - return_std = float(returns.std(ddof=1)) - skew = float(stats.skew(returns, bias=False)) - kurt = float(stats.kurtosis(returns, fisher=True, bias=False)) - - if return_std < 0.005: - regime = "low" - elif return_std < 0.02: - regime = "medium" - else: - regime = "high" - - return MarketSummary( - symbol=symbol, - timeframe=timeframe, - n_bars=len(ohlcv), - return_mean=return_mean, - return_std=return_std, - skew=skew, - kurtosis=kurt, - volatility_regime=regime, - ) -``` - -- [ ] **Step 4: Run test (deve passare)** - -Run: `uv run pytest tests/unit/test_market_summary.py -v` -Expected: PASS. - -- [ ] **Step 5: Commit** - -```bash -git add src/multi_swarm/agents/market_summary.py tests/unit/test_market_summary.py -git commit -m "feat(agents): market summary builder for hypothesis prompt" -``` - ---- - -## Task 29: Run orchestrator (end-to-end loop) - -**Files:** -- Create: `src/multi_swarm/orchestrator/__init__.py` -- Create: `src/multi_swarm/orchestrator/run.py` -- Test: `tests/integration/test_e2e_minimal_run.py` - -L'orchestrator coordina: load OHLCV → build summary → init pop → per ogni gen: chiedi LLM, falsifica, adversarial, fitness → salva su DB → next_generation. Configurazione via dataclass `RunConfig`. - -- [ ] **Step 1: Scrivere test integration** - -```python -# tests/integration/__init__.py -``` - -```python -# tests/integration/test_e2e_minimal_run.py -import random -from datetime import datetime, timezone -from pathlib import Path -import pytest -import numpy as np -import pandas as pd -from multi_swarm.orchestrator.run import RunConfig, run_phase1 -from multi_swarm.genome.hypothesis import ModelTier -from multi_swarm.persistence.repository import Repository -from multi_swarm.llm.client import CompletionResult - - -@pytest.fixture -def synthetic_ohlcv(): - idx = pd.date_range("2024-01-01", periods=500, freq="1h", tz="UTC") - close = 100 + np.cumsum(np.random.RandomState(0).normal(0.01, 1.0, 500)) - return pd.DataFrame( - {"open": close, "high": close + 0.5, "low": close - 0.5, "close": close, "volume": 1.0}, - index=idx, - ) - - -@pytest.fixture -def fake_llm(mocker): - """LLM mock che ritorna sempre una strategia valida.""" - fake = mocker.MagicMock() - fake.complete.return_value = CompletionResult( - text="```lisp\n(strategy (when (gt (indicator rsi 14) 70.0) (entry-short)) (when (lt (indicator rsi 14) 30.0) (entry-long)))\n```", - input_tokens=200, output_tokens=80, tier=ModelTier.C, model="qwen", - ) - return fake - - -def test_e2e_minimal_run_completes(tmp_path: Path, synthetic_ohlcv, fake_llm, mocker): - cfg = RunConfig( - run_name="e2e-test", - population_size=5, - n_generations=2, - elite_k=1, - tournament_k=2, - p_crossover=0.5, - seed=42, - model_tier=ModelTier.C, - symbol="BTC/USDT", - timeframe="1h", - fees_bp=5.0, - n_trials_dsr=10, - db_path=tmp_path / "runs.db", - ) - - run_id = run_phase1(cfg, ohlcv=synthetic_ohlcv, llm=fake_llm) - - repo = Repository(db_path=tmp_path / "runs.db") - run = repo.get_run(run_id) - assert run["status"] == "completed" - gens = repo.list_generations(run_id) - assert len(gens) == 2 - evals = repo.list_evaluations(run_id) - assert len(evals) >= 5 # almeno una popolazione -``` - -- [ ] **Step 2: Run test (deve fallire)** - -Run: `uv run pytest tests/integration/test_e2e_minimal_run.py -v` -Expected: FAIL. - -- [ ] **Step 3: Implementare orchestrator** - -```python -# src/multi_swarm/orchestrator/__init__.py -``` - -```python -# src/multi_swarm/orchestrator/run.py -from __future__ import annotations - -import random -from dataclasses import dataclass, field -from pathlib import Path - -import pandas as pd - -from ..agents.adversarial import AdversarialAgent -from ..agents.falsification import FalsificationAgent -from ..agents.hypothesis import HypothesisAgent -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 HypothesisAgentGenome, ModelTier -from ..llm.client import LLMClient -from ..llm.cost_tracker import CostTracker -from ..persistence.repository import Repository - - -@dataclass -class RunConfig: - run_name: str - population_size: int = 20 - n_generations: int = 10 - elite_k: int = 2 - tournament_k: int = 3 - p_crossover: float = 0.5 - seed: int = 42 - model_tier: ModelTier = ModelTier.C - symbol: str = "BTC/USDT" - timeframe: str = "1h" - fees_bp: float = 5.0 - n_trials_dsr: int = 50 - db_path: Path = field(default_factory=lambda: Path("./runs.db")) - - -def run_phase1( - cfg: RunConfig, - ohlcv: pd.DataFrame, - llm: LLMClient, -) -> str: - rng = random.Random(cfg.seed) - - repo = Repository(cfg.db_path) - repo.init_schema() - run_id = repo.create_run(name=cfg.run_name, config=cfg.__dict__ | {"db_path": str(cfg.db_path)}) - - market = build_market_summary(ohlcv, symbol=cfg.symbol, timeframe=cfg.timeframe) - - hypothesis_agent = HypothesisAgent(llm=llm) - falsification_agent = FalsificationAgent(fees_bp=cfg.fees_bp, n_trials_dsr=cfg.n_trials_dsr) - adversarial_agent = AdversarialAgent(fees_bp=cfg.fees_bp) - cost_tracker = CostTracker() - - population = build_initial_population(k=cfg.population_size, model_tier=cfg.model_tier, rng=rng) - fitnesses: dict[str, float] = {} - - ga_cfg = GAConfig( - population_size=cfg.population_size, - elite_k=cfg.elite_k, - tournament_k=cfg.tournament_k, - p_crossover=cfg.p_crossover, - ) - - try: - for gen in range(cfg.n_generations): - for genome in population: - if genome.id in fitnesses: - continue # elite already evaluated - repo.save_genome(run_id=run_id, generation_idx=gen, genome=genome) - proposal = hypothesis_agent.propose(genome, market) - cost_record = cost_tracker.record( - input_tokens=proposal.completion.input_tokens, - output_tokens=proposal.completion.output_tokens, - tier=proposal.completion.tier, - run_id=run_id, - agent_id=genome.id, - ) - repo.save_cost_record( - run_id=run_id, agent_id=genome.id, tier=cost_record.tier.value, - input_tokens=cost_record.input_tokens, output_tokens=cost_record.output_tokens, - cost_usd=cost_record.cost_usd, - ) - - if proposal.strategy is None: - repo.save_evaluation( - run_id=run_id, genome_id=genome.id, fitness=0.0, - dsr=0.0, dsr_pvalue=1.0, sharpe=0.0, max_dd=0.0, - total_return=0.0, n_trades=0, - parse_error=proposal.parse_error, raw_text=proposal.raw_text, - ) - fitnesses[genome.id] = 0.0 - continue - - fals = falsification_agent.evaluate(proposal.strategy, ohlcv) - adv = adversarial_agent.review(proposal.strategy, ohlcv) - for finding in adv.findings: - repo.save_adversarial_finding( - run_id=run_id, genome_id=genome.id, - name=finding.name, severity=finding.severity.value, detail=finding.detail, - ) - fit = compute_fitness(fals, adv) - repo.save_evaluation( - run_id=run_id, genome_id=genome.id, fitness=fit, - dsr=fals.dsr, dsr_pvalue=fals.dsr_pvalue, sharpe=fals.sharpe, - max_dd=fals.max_drawdown, total_return=fals.total_return, - n_trades=fals.n_trades, parse_error=None, raw_text=proposal.raw_text, - ) - fitnesses[genome.id] = fit - - gen_fitnesses = [fitnesses[g.id] for g in population] - summary = generation_summary(gen_fitnesses, n_bins=10) - repo.save_generation_summary( - run_id=run_id, generation_idx=gen, n_genomes=len(population), - fitness_median=summary["median"], fitness_max=summary["max"], - fitness_p90=summary["p90"], entropy=summary["entropy"], - ) - - if gen < cfg.n_generations - 1: - population = next_generation(population, fitnesses, ga_cfg, rng) - - repo.complete_run(run_id, total_cost=repo.total_cost(run_id), status="completed") - return run_id - except Exception: - repo.complete_run(run_id, total_cost=repo.total_cost(run_id), status="failed") - raise -``` - -- [ ] **Step 4: Run test (deve passare)** - -Run: `uv run pytest tests/integration/test_e2e_minimal_run.py -v` -Expected: PASS. - -- [ ] **Step 5: Commit** - -```bash -git add src/multi_swarm/orchestrator/ tests/integration/ -git commit -m "feat(orchestrator): end-to-end Phase 1 runner with persistence" -``` - ---- - -## Task 30: Streamlit dashboard skeleton + Overview page - -**Files:** -- Create: `src/multi_swarm/dashboard/__init__.py` -- Create: `src/multi_swarm/dashboard/streamlit_app.py` -- Create: `src/multi_swarm/dashboard/data.py` -- Create: `src/multi_swarm/dashboard/pages/01_overview.py` -- Test: `tests/integration/test_streamlit_smoke.py` - -`data.py` espone funzioni di lettura per le pagine Streamlit; `streamlit_app.py` è la home; `pages/01_overview.py` mostra ultima run + stato + spesa. - -- [ ] **Step 1: Implementare data layer della dashboard** - -```python -# src/multi_swarm/dashboard/__init__.py -``` - -```python -# src/multi_swarm/dashboard/data.py -from __future__ import annotations - -import json -from pathlib import Path - -import pandas as pd - -from ..persistence.repository import Repository - - -def get_repo(db_path: str | Path) -> Repository: - return Repository(db_path=db_path) - - -def list_runs_df(repo: Repository) -> pd.DataFrame: - return pd.DataFrame(repo.list_runs()) - - -def get_run_overview(repo: Repository, run_id: str) -> dict: - run = repo.get_run(run_id) - return { - "name": run["name"], - "started_at": run["started_at"], - "completed_at": run["completed_at"], - "status": run["status"], - "total_cost_usd": run["total_cost_usd"], - "config": json.loads(run["config_json"]), - } - - -def generations_df(repo: Repository, run_id: str) -> pd.DataFrame: - return pd.DataFrame(repo.list_generations(run_id)) - - -def evaluations_df(repo: Repository, run_id: str) -> pd.DataFrame: - return pd.DataFrame(repo.list_evaluations(run_id)) - - -def genomes_df(repo: Repository, run_id: str, generation_idx: int | None = None) -> pd.DataFrame: - rows = repo.list_genomes(run_id, generation_idx) - flat = [] - for r in rows: - payload = json.loads(r["payload_json"]) - flat.append({ - "id": r["id"], "generation_idx": r["generation_idx"], - **payload, - }) - return pd.DataFrame(flat) -``` - -- [ ] **Step 2: Streamlit home page** - -```python -# src/multi_swarm/dashboard/streamlit_app.py -from __future__ import annotations - -import os -from pathlib import Path - -import streamlit as st - -st.set_page_config(page_title="Multi-Swarm Phase 1", layout="wide") -st.title("Multi-Swarm Coevolutivo — Phase 1 dashboard") -st.markdown(""" -Naviga le pagine nel menu a sinistra: -- **Overview**: ultima run e stato globale. -- **GA Convergence**: fitness per generazione. -- **Genomes**: top-K genomi e ispezione qualitativa. -""") - -db_path = os.environ.get("DB_PATH", "./runs.db") -st.session_state["db_path"] = db_path -st.caption(f"DB path: `{Path(db_path).resolve()}`") -``` - -- [ ] **Step 3: Pagina Overview** - -```python -# src/multi_swarm/dashboard/pages/01_overview.py -from __future__ import annotations - -import streamlit as st - -from multi_swarm.dashboard.data import get_repo, get_run_overview, list_runs_df - -st.title("Overview") - -db_path = st.session_state.get("db_path", "./runs.db") -repo = get_repo(db_path) - -runs = list_runs_df(repo) -if runs.empty: - st.info("Nessuna run nel database. Esegui `scripts/run_phase1.py` per generarne una.") - st.stop() - -st.subheader("Tutte le run") -st.dataframe(runs[["id", "name", "started_at", "completed_at", "status", "total_cost_usd"]]) - -selected = st.selectbox("Seleziona run per dettaglio", runs["id"].tolist()) -overview = get_run_overview(repo, selected) - -col1, col2, col3, col4 = st.columns(4) -col1.metric("Status", overview["status"]) -col2.metric("Cost (USD)", f"{overview['total_cost_usd']:.4f}") -col3.metric("Started", overview["started_at"]) -col4.metric("Completed", overview["completed_at"] or "—") - -st.subheader("Config") -st.json(overview["config"]) -``` - -- [ ] **Step 4: Smoke test (importabilità)** - -```python -# tests/integration/test_streamlit_smoke.py -import importlib - - -def test_streamlit_app_imports(): - # Check the modules import without exec'ing Streamlit's runtime - importlib.import_module("multi_swarm.dashboard.data") - - -def test_dashboard_data_helpers_signatures(): - from multi_swarm.dashboard import data - assert hasattr(data, "list_runs_df") - assert hasattr(data, "generations_df") - assert hasattr(data, "evaluations_df") - assert hasattr(data, "genomes_df") -``` - -- [ ] **Step 5: Run smoke test** - -Run: `uv run pytest tests/integration/test_streamlit_smoke.py -v` -Expected: PASS. - -- [ ] **Step 6: Commit** - -```bash -git add src/multi_swarm/dashboard/ tests/integration/test_streamlit_smoke.py -git commit -m "feat(dashboard): streamlit skeleton + Overview page + data layer" -``` - ---- - -## Task 31: Streamlit page — GA Convergence - -**Files:** -- Create: `src/multi_swarm/dashboard/pages/02_ga_convergence.py` - -- [ ] **Step 1: Implementare pagina** - -```python -# src/multi_swarm/dashboard/pages/02_ga_convergence.py -from __future__ import annotations - -import plotly.graph_objects as go -import streamlit as st - -from multi_swarm.dashboard.data import generations_df, get_repo, list_runs_df - -st.title("GA Convergence") - -db_path = st.session_state.get("db_path", "./runs.db") -repo = get_repo(db_path) - -runs = list_runs_df(repo) -if runs.empty: - st.info("Nessuna run.") - st.stop() - -selected = st.selectbox("Run", runs["id"].tolist()) -gens = generations_df(repo, selected) -if gens.empty: - st.warning("Nessuna generazione registrata per questa run.") - st.stop() - -fig = go.Figure() -fig.add_trace(go.Scatter(x=gens["generation_idx"], y=gens["fitness_median"], name="median", mode="lines+markers")) -fig.add_trace(go.Scatter(x=gens["generation_idx"], y=gens["fitness_max"], name="max", mode="lines+markers")) -fig.add_trace(go.Scatter(x=gens["generation_idx"], y=gens["fitness_p90"], name="p90", mode="lines+markers")) -fig.update_layout(xaxis_title="generation", yaxis_title="fitness", title="Fitness convergence") -st.plotly_chart(fig, use_container_width=True) - -st.subheader("Entropy") -fig2 = go.Figure() -fig2.add_trace(go.Scatter(x=gens["generation_idx"], y=gens["entropy"], mode="lines+markers")) -fig2.add_hline(y=0.5, line_dash="dash", annotation_text="gate threshold (0.5)") -fig2.update_layout(xaxis_title="generation", yaxis_title="entropy", title="Diversity (fitness entropy)") -st.plotly_chart(fig2, use_container_width=True) - -st.subheader("Tabella generazioni") -st.dataframe(gens) -``` - -- [ ] **Step 2: Smoke test (importabilità)** - -Run: `uv run python -c "import importlib; importlib.import_module('multi_swarm.dashboard.pages.02_ga_convergence')"` - -Note: Streamlit pages prefixed with digits possono essere problematici per import diretto. Per il test possiamo ridurre a verifica della pagina via filesystem. - -```bash -test -f src/multi_swarm/dashboard/pages/02_ga_convergence.py && echo OK -``` - -Expected: stampa `OK`. - -- [ ] **Step 3: Commit** - -```bash -git add src/multi_swarm/dashboard/pages/02_ga_convergence.py -git commit -m "feat(dashboard): GA convergence page (median/max/p90 + entropy)" -``` - ---- - -## Task 32: Streamlit page — Genomes (basic) - -**Files:** -- Create: `src/multi_swarm/dashboard/pages/03_genomes.py` - -- [ ] **Step 1: Implementare pagina** - -```python -# src/multi_swarm/dashboard/pages/03_genomes.py -from __future__ import annotations - -import streamlit as st - -from multi_swarm.dashboard.data import ( - evaluations_df, genomes_df, get_repo, list_runs_df, -) - -st.title("Genomes") - -db_path = st.session_state.get("db_path", "./runs.db") -repo = get_repo(db_path) - -runs = list_runs_df(repo) -if runs.empty: - st.info("Nessuna run.") - st.stop() - -selected = st.selectbox("Run", runs["id"].tolist()) -evals = evaluations_df(repo, selected) -genomes = genomes_df(repo, selected) - -if evals.empty: - st.warning("Nessuna evaluation.") - st.stop() - -merged = evals.merge(genomes, left_on="genome_id", right_on="id", how="left", suffixes=("", "_g")) -top = merged.sort_values("fitness", ascending=False).head(10) - -st.subheader("Top-10 genomi (per fitness)") -display_cols = [ - "genome_id", "fitness", "dsr", "sharpe", "max_dd", "n_trades", - "cognitive_style", "temperature", "lookback_window", "feature_access", -] -existing = [c for c in display_cols if c in top.columns] -st.dataframe(top[existing]) - -st.subheader("Ispezione genoma") -gid = st.selectbox("Seleziona genome_id", top["genome_id"].tolist()) -row = merged[merged["genome_id"] == gid].iloc[0] - -col1, col2 = st.columns(2) -with col1: - st.metric("fitness", f"{row['fitness']:.3f}") - st.metric("DSR", f"{row['dsr']:.3f}") - st.metric("Sharpe", f"{row['sharpe']:.3f}") -with col2: - st.metric("max DD", f"{row['max_dd']:.3f}") - st.metric("trades", int(row["n_trades"])) - st.metric("style", str(row.get("cognitive_style", "—"))) - -st.subheader("System prompt") -st.code(row.get("system_prompt", "—")) - -st.subheader("Raw LLM output") -st.code(row.get("raw_text", "—")) - -if row.get("parse_error"): - st.error(f"Parse error: {row['parse_error']}") -``` - -- [ ] **Step 2: Smoke check filesystem** - -Run: `test -f src/multi_swarm/dashboard/pages/03_genomes.py && echo OK` -Expected: stampa `OK`. - -- [ ] **Step 3: Commit** - -```bash -git add src/multi_swarm/dashboard/pages/03_genomes.py -git commit -m "feat(dashboard): Genomes page (top-10 + inspection)" -``` - ---- - -## Task 33: Script di entry point per Phase 1 - -**Files:** -- Create: `scripts/__init__.py` -- Create: `scripts/run_phase1.py` - -Lo script orchestra il run reale: carica OHLCV, costruisce LLMClient con API key da .env, esegue `run_phase1`. Configurabile via CLI args con argparse. - -- [ ] **Step 1: Implementare script** - -```python -# scripts/__init__.py -``` - -```python -# scripts/run_phase1.py -from __future__ import annotations - -import argparse -from datetime import datetime, timezone -from pathlib import Path - -from multi_swarm.config import load_settings -from multi_swarm.data.ohlcv_loader import OHLCVLoader, OHLCVRequest -from multi_swarm.genome.hypothesis import ModelTier -from multi_swarm.llm.client import LLMClient -from multi_swarm.orchestrator.run import RunConfig, run_phase1 - - -def parse_args() -> argparse.Namespace: - p = argparse.ArgumentParser(description="Multi-Swarm Phase 1 runner") - p.add_argument("--name", default="phase1-spike-001") - p.add_argument("--population-size", type=int, default=20) - p.add_argument("--n-generations", type=int, default=10) - p.add_argument("--elite-k", type=int, default=2) - p.add_argument("--tournament-k", type=int, default=3) - p.add_argument("--p-crossover", type=float, default=0.5) - p.add_argument("--seed", type=int, default=42) - p.add_argument("--symbol", default="BTC/USDT") - p.add_argument("--timeframe", default="1h") - p.add_argument("--start", default="2024-01-01T00:00:00+00:00") - p.add_argument("--end", default="2026-01-01T00:00:00+00:00") - p.add_argument("--fees-bp", type=float, default=5.0) - p.add_argument("--n-trials-dsr", type=int, default=50) - return p.parse_args() - - -def main() -> None: - args = parse_args() - settings = load_settings() - - loader = OHLCVLoader(cache_dir=settings.series_dir) - req = OHLCVRequest( - symbol=args.symbol, - timeframe=args.timeframe, - start=datetime.fromisoformat(args.start), - end=datetime.fromisoformat(args.end), - ) - ohlcv = loader.load(req) - 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(), - anthropic_api_key=( - settings.anthropic_api_key.get_secret_value() - if settings.anthropic_api_key else None - ), - ) - - cfg = RunConfig( - run_name=args.name, - population_size=args.population_size, - n_generations=args.n_generations, - elite_k=args.elite_k, - tournament_k=args.tournament_k, - p_crossover=args.p_crossover, - seed=args.seed, - model_tier=ModelTier.C, - symbol=args.symbol, - timeframe=args.timeframe, - fees_bp=args.fees_bp, - n_trials_dsr=args.n_trials_dsr, - db_path=settings.db_path, - ) - - run_id = run_phase1(cfg, ohlcv=ohlcv, llm=llm) - print(f"Run completed: {run_id}") - - -if __name__ == "__main__": - main() -``` - -- [ ] **Step 2: Verifica importabilità** - -Run: `uv run python -c "from scripts import run_phase1; print(run_phase1.__doc__ or 'ok')"` -Expected: stampa `ok`. - -- [ ] **Step 3: Commit** - -```bash -git add scripts/ -git commit -m "feat(scripts): Phase 1 runner CLI entry point" -``` - ---- - -## Task 34: Smoke run (popolazione minima, 1 generazione, dry data) - -**Files:** -- Create: `scripts/smoke_run.py` - -Smoke run usa OHLCV sintetico generato in memoria + popolazione 3 + 1 generazione. Niente API LLM reale: usa `MockLLMClient` che restituisce strategy fissa. Serve a validare che tutto il loop gira senza errori prima di spendere token reali. - -- [ ] **Step 1: Implementare smoke** - -```python -# scripts/smoke_run.py -from __future__ import annotations - -from pathlib import Path - -import numpy as np -import pandas as pd - -from multi_swarm.genome.hypothesis import HypothesisAgentGenome, ModelTier -from multi_swarm.llm.client import CompletionResult -from multi_swarm.orchestrator.run import RunConfig, run_phase1 - - -class MockLLMClient: - def complete( - self, genome: HypothesisAgentGenome, system: str, user: str, - max_tokens: int = 2000, - ) -> CompletionResult: - text = ( - "```lisp\n" - "(strategy" - " (when (gt (indicator rsi 14) 70.0) (entry-short))" - " (when (lt (indicator rsi 14) 30.0) (entry-long)))\n" - "```" - ) - return CompletionResult( - text=text, input_tokens=120, output_tokens=60, - tier=genome.model_tier, model="mock", - ) - - -def main() -> None: - idx = pd.date_range("2024-01-01", periods=1000, freq="1h", tz="UTC") - close = 100 + np.cumsum(np.random.RandomState(0).normal(0.01, 1.0, 1000)) - ohlcv = pd.DataFrame( - {"open": close, "high": close + 0.5, "low": close - 0.5, "close": close, "volume": 1.0}, - index=idx, - ) - cfg = RunConfig( - run_name="smoke", - population_size=3, - n_generations=1, - elite_k=1, - tournament_k=2, - p_crossover=0.5, - seed=0, - model_tier=ModelTier.C, - db_path=Path("./runs.db"), - ) - run_id = run_phase1(cfg, ohlcv=ohlcv, llm=MockLLMClient()) # type: ignore[arg-type] - print(f"Smoke run completed: {run_id}") - - -if __name__ == "__main__": - main() -``` - -- [ ] **Step 2: Run smoke** - -Run: `uv run python scripts/smoke_run.py` -Expected: stampa `Smoke run completed: `. File `runs.db` esiste con 3 genomi e 1 generazione. - -- [ ] **Step 3: Commit** - -```bash -git add scripts/smoke_run.py -git commit -m "feat(scripts): smoke run with mock LLM and synthetic OHLCV" -``` - ---- - -## Task 35: Validazione Streamlit dashboard via dataset reale dello smoke run - -**Files:** -- (no new files) - -- [ ] **Step 1: Avviare dashboard sul DB della smoke run** - -Run: `DB_PATH=./runs.db uv run streamlit run src/multi_swarm/dashboard/streamlit_app.py` -Expected: il browser apre `http://localhost:8501`. Le 3 pagine (Overview, GA Convergence, Genomes) mostrano dati senza errori. - -- [ ] **Step 2: Verifica visiva (lista da spuntare manualmente)** - -- [ ] Overview elenca la run "smoke" con status `completed` e cost > 0. -- [ ] GA Convergence mostra 1 punto per generazione 0 (sarebbero 1 punto su asse x). -- [ ] Genomes mostra 3 genomi nella tabella. -- [ ] Clic su un genome_id mostra system_prompt e raw_text. - -Se uno qualunque fallisce, fix prima di chiudere il task. Documenta eventuali bug in `docs/runs/`. - -- [ ] **Step 3: Stop dashboard, commit eventuali fix** - -```bash -# Solo se sono stati fatti fix -git add -A -git commit -m "fix(dashboard): correggere " -``` - ---- - -## Task 36: Run completo Phase 1 con LLM reale (K=20, 10 generazioni, OHLCV 2 anni) - -**Files:** -- Modify: nessuno (solo esecuzione) -- Create: `docs/runs/2026-MM-DD-phase1-run-001.md` - -Questo è l'**evento operativo** della Phase 1: il primo run reale. Pre-requisiti: -- Cerbero locale **non** strettamente necessario per Phase 1 (il compiler usa indicatori built-in). Avviare Cerbero solo se gli agenti vorranno chiamare tool MCP per ispezione, ma in Phase 1 il prompt non lo prevede esplicitamente. -- API key OpenRouter configurata in `.env`. -- Budget tracker attivato — monitorare la spesa durante il run. - -- [ ] **Step 1: Pre-flight check** - -```bash -uv run pytest # tutta la suite verde -uv run ruff check src/ tests/ # linter pulito -uv run mypy src/multi_swarm # type check pulito (ammessi ignore mirati documentati) -``` - -Expected: tutti verde. - -- [ ] **Step 2: Esegui run reale** - -```bash -uv run python scripts/run_phase1.py \ - --name phase1-run-001 \ - --population-size 20 \ - --n-generations 10 \ - --elite-k 2 \ - --tournament-k 3 \ - --p-crossover 0.5 \ - --seed 42 \ - --symbol BTC/USDT \ - --timeframe 1h \ - --start 2024-01-01T00:00:00+00:00 \ - --end 2026-01-01T00:00:00+00:00 -``` - -Expected: durata stimata 30-90 minuti, spesa stimata $40-90 (single run, una su 5-10 totali fino a fine Phase 1). - -**Monitoring**: in altra shell, controllare cumulato cost ogni 5 minuti via dashboard Overview, oppure: - -```bash -sqlite3 runs.db "SELECT total_cost_usd FROM runs WHERE name='phase1-run-001'" -``` - -Stop manuale (`Ctrl+C`) se la spesa cumulata supera $120 — sintomo di token output runaway. - -- [ ] **Step 3: Apri dashboard e ispeziona** - -Run: `DB_PATH=./runs.db uv run streamlit run src/multi_swarm/dashboard/streamlit_app.py` - -Verifica che: -- 10 generazioni siano presenti. -- 20 genomi per generazione, almeno 16 con `parse_error IS NULL`. -- Top-5 genomi abbiano DSR ragionevole (>0). - -- [ ] **Step 4: Documenta il run** - -Crea `docs/runs/2026-MM-DD-phase1-run-001.md` (sostituire MM-DD con la data effettiva) con: - -```markdown -# Phase 1 — Run 001 - -**Data**: -**Config**: K=20, 10 gen, seed=42, symbol BTC/USDT 1h, dataset 2024-2026. -**Costo finale**: $ -**Durata wall-clock**: - -## Risultati sintetici - -- Top fitness: -- Median fitness gen finale: -- Entropia gen finale: -- % parse success: % -- # genomi con DSR > 0.5: - -## Anomalie - -- (es. parse error frequenti su prompt cognitive_style "engineer", da investigare) - -## Learning - -- ... - -## Action items - -- ... -``` - -- [ ] **Step 5: Commit** - -```bash -git add docs/runs/ -git commit -m "docs(runs): Phase 1 run-001 report" -``` - ---- - -## Task 37: Decision memo Phase 1 (gate evaluation) - -**Files:** -- Create: `docs/decisions/2026-MM-DD-gate-phase1.md` - -Compilare il decision memo gate Phase 1 sulla base dei risultati del run-001 (eventualmente più run se serve aggregare). - -- [ ] **Step 1: Author pass — scrivere il memo** - -```markdown -# Gate Phase 1 — Decision Memo - -**Data**: -**Run analizzati**: phase1-run-001 [, phase1-run-002, ...] -**Spesa totale Phase 1**: $ di $700 cap (=%) -**Tempo speso Phase 1**: settimane - -## Hard gate evaluation - -| # | Gate | Soglia | Misura | Esito | -|---|------|--------|--------|-------| -| 1 | Loop converge (median ↑ ≥3 gen) | 3 gen consecutive crescita | | PASS/FAIL | -| 2 | Output formalizzabile | ≥80% parse success | % | PASS/FAIL | -| 3 | Tail superiore | top-5 DSR ≥ 1.5x median | | PASS/FAIL | -| 4 | Diversità non collassa | entropy > 0.5 a fine run | | PASS/FAIL | -| 5 | Cost predictability | spesa entro ±30% stima | % deviazione | PASS/FAIL | - -## Conclusione (author) - -PASS / FAIL con razionale numerico ancorato alla tabella sopra. - -## Aggiustamenti raccomandati per Phase 2 (se PASS) - -- ... - -## Pivot/stop raccomandato (se FAIL) - -- ... -``` - -- [ ] **Step 2: Review pass — adversarial review del memo** - -Scegli una delle 3 opzioni dello spec sez. 9.2: -- subagent Claude red-team con prompt esplicito -- collega umano -- timer 48h fresh-eyes pass - -Aggiungi al memo una sezione `## Review pass (red team)` con la critica e le contro-evidenze. - -- [ ] **Step 3: Sintesi finale e decisione** - -Aggiungi `## Decisione finale` con uno di: -- GO Phase 2 (specificare scope, eventuali aggiustamenti) -- ITERATE Phase 1 (specificare cosa cambiare e re-run) -- PIVOT (specificare nuovo dominio o nuovo approach) -- STOP (specificare razionale e learnings) - -- [ ] **Step 4: Commit** - -```bash -git add docs/decisions/ -git commit -m "docs(decisions): Phase 1 gate decision memo with author + review pass" -``` - ---- - -## Task 38: Report tecnico Phase 1 - -**Files:** -- Create: `docs/reports/2026-MM-DD-phase1-technical-report.md` - -Report ~5 pagine come da spec Sez. 4.5. Contenuti: -1. Setup sperimentale (config, dataset, periodo, seed). -2. Loop convergence (grafico fitness mediana / max / p90 per generazione, screenshot dashboard). -3. Top-5 genomi: ispezione qualitativa (system_prompt, parametri, strategia generata, performance). -4. Parser failure modes: tassonomia degli errori di parse osservati, suggerimenti per Phase 2. -5. Costi reali vs preventivo: breakdown per tier, per agent, identificare ottimizzazioni. -6. Diversity metrics: entropia per generazione, distinct cognitive_style sopravvissuti. - -- [ ] **Step 1: Generare grafici dalla dashboard** - -Procedura: aprire la dashboard, fare screenshot delle pagine GA Convergence e Genomes, salvarli in `docs/reports/figures/phase1/`. - -- [ ] **Step 2: Scrivere il report** - -Fornire il file con la struttura sopra. Usare prosa italiana piena (regola CLAUDE.md per public artifacts). - -- [ ] **Step 3: Commit** - -```bash -git add docs/reports/ -git commit -m "docs(reports): Phase 1 technical report" -``` - ---- - -## Self-review - -Dopo aver completato la stesura, rilettura del plan a freddo per verificare: - -**1. Spec coverage** -- Scope IN Phase 1 (spec sez. 4.1): - - Backtest engine event-driven 1h walk-forward 70/30 → Task 6 (engine), Task 4 (splits) ✓ - - Cerbero wrapper come tool layer → Task 9-10 ✓ - - Protocollo S-expr fisso 12-15 verbi → Task 11-13 ✓ - - Hypothesis Swarm K=20 tier C → Task 27 (initial) + Task 19 (agent) + Task 33 (run script) ✓ - - Falsification + Adversarial hand-crafted → Task 20-21 ✓ - - Fitness v0 (DSR + drawdown penalty) → Task 22 ✓ - - GA loop 8-12 generazioni, tournament + elitism → Task 23-24 + Task 33 (default 10 gen) ✓ -- Hard gates (spec sez. 4.4): - - 1 loop converge → Task 26 (summary helpers per misurare) + Task 37 (memo) ✓ - - 2 parser >80% → repository memorizza parse_error, Task 37 lo misura ✓ - - 3 tail superiore → query SQL su evaluations ✓ - - 4 entropy > 0.5 → Task 26 + Task 31 (dashboard mostra hline) ✓ - - 5 cost predictability → Task 18 (tracker) + Task 25 (DB) + Task 37 (memo) ✓ -- GUI Phase 1 (spec sez. 7.2): - - Overview ✓ Task 30 - - GA Convergence ✓ Task 31 - - Genomes basic ✓ Task 32 -- Deliverable Phase 1 (spec sez. 4.5): - - Codice testato ✓ tutti task con TDD - - Report tecnico ~5 pp ✓ Task 38 - - Decision memo ✓ Task 37 - -**2. Placeholder scan** -- Date YYYY-MM-DD lasciate da compilare nei task 36/37/38: questi sono naturalmente dipendenti dalla data di esecuzione, non sono placeholder di logica. Marcare come "compila al momento del run". -- Pricing LLM in Task 18 è approssimativo: aggiornare con valori reali se OpenRouter cambia tariffa (controllare a inizio run). -- Nessun TBD/TODO nel codice. - -**3. Type consistency** -- `HypothesisAgentGenome` interfaccia stabile in tutti i task (id, generation, parent_ids, model_tier). -- `Side` enum coerente: LONG/SHORT/FLAT in backtest, compiler, agents, dashboard. -- `Strategy`/`Rule`/`Node` AST consistenti fra parser → validator → compiler. -- `FalsificationReport` campi usati identici in fitness (Task 22) e repository (Task 25): `dsr`, `dsr_pvalue`, `sharpe`, `max_drawdown`, `total_return`, `n_trades`. ✓ -- `AdversarialReport.findings` usato da fitness e repository: `name`, `severity`, `detail` consistenti. ✓ -- `CompletionResult` campi `text`, `input_tokens`, `output_tokens`, `tier`, `model`: identici fra LLMClient (Task 17), CostTracker (Task 18), HypothesisAgent (Task 19), Orchestrator (Task 29). ✓ - -**4. Granularità** -- Task piccoli e atomici (3-5 step), 38 task totali → ~150-200 step. Coerente con stima 4-6 settimane full-time. -- Test integration Task 29 e Task 35-36 richiedono setup più grande, ma sono passi singoli con sub-checklist esplicita. - -Nessuna correzione necessaria. Il plan è pronto. - ---- - -## Execution handoff - -Plan completo salvato in `docs/superpowers/plans/2026-05-09-phase1-lean-spike.md`. - -**Due opzioni di esecuzione:** - -1. **Subagent-Driven (raccomandata)** — un fresh subagent per task, review fra task, iterazione rapida. -2. **Inline Execution** — task eseguiti in questa stessa sessione con checkpoint per review. - -Quale approccio? - diff --git a/docs/superpowers/plans/2026-05-11-mutate-prompt-llm-phase-2-5.md b/docs/superpowers/plans/2026-05-11-mutate-prompt-llm-phase-2-5.md deleted file mode 100644 index fcbbdd1..0000000 --- a/docs/superpowers/plans/2026-05-11-mutate-prompt-llm-phase-2-5.md +++ /dev/null @@ -1,318 +0,0 @@ -# `mutate_prompt_llm` — Phase 2.5 Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [x]`) syntax for tracking. - -**Status:** **TUTTI I 6 TASK COMPLETATI** (task 1-5 il 2026-05-11, task 6 il 2026-05-12). Mergiati su main. Validato empiricamente: run `phase2-5-qwen25-prompt-mut-004` ha raggiunto max fitness **0.1012** (+225% vs baseline `phase2-qwen25-control-001` 0.0311). Sweet spot weight=0.30 (curva U: weight=0.50 → regressione plateau 0.0311; weight=0.00 → baseline piatto). - -**Trigger Phase 2.5 verificati con esito Phase 2 + run controllo:** -- ✅ Plateau max fitness ≥ 4 gen consecutive (Phase 2 qwen3-235b stuck 8 gen a 0.0238; run controllo qwen-2.5-72b stuck 9 gen a 0.0311). -- ✅ Diversità prompt collapsed: top genomi del run controllo hanno fitness/Sharpe/DD identici (mutazioni scalari non producono varianti significative). -- ✗ Top quasi-fit ≥ 0.10 non raggiunto, ma 2/3 trigger sufficienti. - -**Decisione collaterale:** rollback tier C a `qwen/qwen-2.5-72b-instruct` (run controllo l'ha dimostrato superiore a qwen3-235b: +30% fitness, 4× entropy, metà costo e tempo). - -**Goal:** Introdurre un quinto operatore di mutazione che usa un LLM tier B come "mutator" per riscrivere il `system_prompt` di un genoma, generando diversità reale dove oggi `random_mutate` tocca solo quattro scalari. La pipeline GA esistente resta intatta: `mutate_prompt_llm` è solo un nuovo membro di `MUTATION_OPS` con peso configurabile. - -**Architecture:** Operatore puro come gli altri quattro (`mutate_temperature`, `mutate_lookback`, `mutate_feature_access`, `mutate_cognitive_style`). Riceve `parent_genome`, `llm_client`, `rng` e restituisce un child genome con `system_prompt` modificato. Il mutator LLM (tier B = `deepseek/deepseek-v4-flash`) riceve una mutation-instruction casuale tra sei tipi predefiniti (`tighten_threshold`, `swap_comparator`, `add_condition`, `remove_condition`, `change_timeframe`, `add_temporal_gate`) e produce un nuovo prompt vincolato a una mutazione "atomica". Il child viene validato (parser + adversarial dry-run); su fallimento si effettua fallback a `random_mutate`. Selezione probabilistica nel `random_mutate` dispatcher con peso configurabile (default 0.30) — i quattro operator scalari mantengono il 70% complessivo. - -**Tech Stack:** Python 3.13, `LLMClient` esistente (OpenAI SDK via OpenRouter), pytest + `pytest-mock`. Niente nuove dipendenze. - -**Spec di riferimento:** sezione "Meccanismo di mutazione" della conversazione `2026-05-11`, valutazione `mutate_prompt_llm` (questa pagina contiene la sintesi). - ---- - -## Trigger condition (quando attivare) - -Implementare e mergiare **solo se** uno dei seguenti è vero al termine di Phase 2: - -1. **Plateau evolutivo**: max fitness stagnante (Δ < 0.01) per ≥ 4 generazioni consecutive su `phase2-qwen3-001` o successori. -2. **Diversità prompt collassa**: media Levenshtein normalizzata fra i prompt della popolazione finale ≤ 0.15 (= popolazione clonata). -3. **Top genome problematico ma quasi-fit**: max fitness ≥ 0.10 ma adversarial finding HIGH ≥ 2 per il top, suggerendo che una mutazione mirata del prompt potrebbe "ripararlo". - -Se Phase 2 raggiunge max fitness ≥ 0.30 senza plateau, **non attivare** (la diversità random basta). - ---- - -## File map - -| File | Tipo | Responsabilità | -|------|------|----------------| -| `src/multi_swarm/genome/mutation_prompt_llm.py` | New | Operatore `mutate_prompt_llm` + helper `MUTATION_INSTRUCTIONS` + retry/fallback wrapper | -| `src/multi_swarm/genome/mutation.py` | Modify | Estendere `MUTATION_OPS` + introdurre dispatcher pesato `weighted_random_mutate` | -| `src/multi_swarm/ga/loop.py` | Modify | Sostituire `random_mutate(parent, rng)` con `weighted_random_mutate(parent, rng, llm_client, weights)` | -| `src/multi_swarm/orchestrator/run.py` | Modify | Aggiungere `mutator_tier: ModelTier = ModelTier.B` e `prompt_mutation_weight: float = 0.30` a `RunConfig`, passare `LLMClient` al loop GA | -| `src/multi_swarm/llm/cost_tracker.py` | Modify (minimo) | Loggare `mutation_call` separatamente da `hypothesis_call` per attribuzione costo | -| `src/multi_swarm/metrics/diversity.py` | New | Funzione `population_prompt_diversity` (Levenshtein normalizzata) — usata in trigger check + telemetry | -| `tests/unit/test_mutation_prompt_llm.py` | New | Test operator con mock `LLMClient` (success + validation fail + retry/fallback) | -| `tests/unit/test_mutation_dispatcher.py` | New | Test `weighted_random_mutate` rispetta i pesi | -| `tests/unit/test_diversity.py` | New | Test `population_prompt_diversity` su prompt identici/diversi | -| `tests/integration/test_ga_loop_with_prompt_mutator.py` | New | Loop end-to-end di 2 gen × 5 genomi con mock LLM, verifica diversità prompt cresce | - ---- - -## Task 1: Mutator instructions + operator stub - -**Files:** -- New: `src/multi_swarm/genome/mutation_prompt_llm.py` -- New: `tests/unit/test_mutation_prompt_llm.py` - -- [x] **Step 1.1: Write failing test — operator returns child con system_prompt diverso** - -Append a `tests/unit/test_mutation_prompt_llm.py`: - -```python -def test_mutate_prompt_llm_produces_different_prompt(mock_llm: LLMClient) -> None: - parent = make_genome(system_prompt="Strategia: compra quando RSI < 30") - mock_llm.respond_with("Strategia: compra quando RSI < 25 e ora >= 14") - child = mutate_prompt_llm(parent, mock_llm, rng=random.Random(0)) - assert child.system_prompt != parent.system_prompt - assert child.parent_ids == [*parent.parent_ids, parent.id] - assert child.generation == parent.generation + 1 -``` - -- [x] **Step 1.2: Implement `MUTATION_INSTRUCTIONS` constant** - -`mutation_prompt_llm.py`: - -```python -MUTATION_INSTRUCTIONS: dict[str, str] = { - "tighten_threshold": "Rendi una soglia numerica più restrittiva del 10–20%...", - "swap_comparator": "Inverti un comparator (gt ↔ lt, gte ↔ lte) mantenendo intent...", - "add_condition": "Aggiungi una condizione AND/OR alla rule più specifica...", - "remove_condition": "Rimuovi una condizione ridondante o debole...", - "change_timeframe": "Modifica una finestra rolling (lookback) di ±30%...", - "add_temporal_gate": "Aggiungi un gate temporale (hour, dow, is_weekend)...", -} -``` - -- [x] **Step 1.3: Implement `mutate_prompt_llm`** - -Firma: -```python -def mutate_prompt_llm( - g: HypothesisAgentGenome, - llm: LLMClient, - rng: random.Random, - mutator_tier: ModelTier = ModelTier.B, -) -> HypothesisAgentGenome: -``` - -Logica: -1. Scegli `instruction_key = rng.choice(list(MUTATION_INSTRUCTIONS))`. -2. Costruisci messaggio system + user con `MUTATION_INSTRUCTIONS[instruction_key]` + `g.system_prompt`. -3. Crea genoma temporaneo `mutator_genome` con `model_tier=mutator_tier`. -4. Chiama `llm.complete(mutator_genome, system, user, max_tokens=2000)`. -5. Estrai nuovo prompt da risposta (cerca blocco `...` o intero output). -6. Ritorna `_clone_with(g, system_prompt=new_prompt)` (riusa helper di `mutation.py`). - -- [x] **Step 1.4: Run test → green** - -```bash -uv run pytest tests/unit/test_mutation_prompt_llm.py::test_mutate_prompt_llm_produces_different_prompt -xvs -``` - ---- - -## Task 2: Validation + fallback - -**Files:** -- Modify: `src/multi_swarm/genome/mutation_prompt_llm.py` -- Append: `tests/unit/test_mutation_prompt_llm.py` - -- [x] **Step 2.1: Write failing test — fallback a random_mutate su prompt invalid** - -```python -def test_mutate_prompt_llm_falls_back_on_invalid_prompt(mock_llm: LLMClient) -> None: - parent = make_genome() - mock_llm.respond_with("garbage that does not parse") - child = mutate_prompt_llm(parent, mock_llm, rng=random.Random(0)) - # Garbage prompt deve fallback: child è prodotto da random_mutate, quindi - # system_prompt == parent.system_prompt (random_mutate tocca solo scalari) - assert child.system_prompt == parent.system_prompt - assert child.parent_ids == [*parent.parent_ids, parent.id] -``` - -- [x] **Step 2.2: Implement validation step** - -Dopo aver estratto `new_prompt`, esegui `validate_prompt(new_prompt)`: -- Lunghezza minima 50 caratteri. -- Contiene almeno una keyword fra `{rsi, sma, ema, atr, momentum, breakout, mean reversion, gt, lt, ...}`. -- Non identico a `parent.system_prompt` (Levenshtein > 0.05 normalizzata). - -Su fail → log warning + ritorna `random_mutate(g, rng)`. - -- [x] **Step 2.3: Write failing test — diversity guard** - -Mock LLM ritorna prompt identico al parent → `validate_prompt` rifiuta → fallback. - -- [x] **Step 2.4: Run test suite parziale** - -```bash -uv run pytest tests/unit/test_mutation_prompt_llm.py -xvs -``` - ---- - -## Task 3: Weighted dispatcher - -**Files:** -- Modify: `src/multi_swarm/genome/mutation.py` -- New: `tests/unit/test_mutation_dispatcher.py` - -- [x] **Step 3.1: Write failing test — weighted_random_mutate rispetta pesi** - -```python -def test_weighted_random_mutate_picks_prompt_op_at_configured_rate() -> None: - rng = random.Random(0) - weights = {"prompt": 1.0, "scalar": 0.0} # 100% prompt - counter = Counter() - for _ in range(100): - op_name = _pick_op_name(weights, rng) - counter[op_name] += 1 - assert counter["prompt"] == 100 -``` - -- [x] **Step 3.2: Implement `weighted_random_mutate`** - -```python -def weighted_random_mutate( - g: HypothesisAgentGenome, - rng: random.Random, - llm: LLMClient | None = None, - prompt_mutation_weight: float = 0.30, -) -> HypothesisAgentGenome: - if llm is not None and rng.random() < prompt_mutation_weight: - return mutate_prompt_llm(g, llm, rng) - return random_mutate(g, rng) -``` - -- [x] **Step 3.3: Test edge cases** - -- `llm=None` → sempre scalar mutation (backward compat). -- `prompt_mutation_weight=0.0` → sempre scalar. -- `prompt_mutation_weight=1.0` → sempre prompt (se llm presente). - ---- - -## Task 4: Integrazione GA loop - -**Files:** -- Modify: `src/multi_swarm/ga/loop.py` -- Modify: `src/multi_swarm/orchestrator/run.py` -- New: `tests/integration/test_ga_loop_with_prompt_mutator.py` - -- [x] **Step 4.1: Estendere `GAConfig`** - -```python -@dataclass(frozen=True) -class GAConfig: - population_size: int - elite_k: int - tournament_k: int - p_crossover: float - prompt_mutation_weight: float = 0.0 # default off → opt-in -``` - -- [x] **Step 4.2: Pass `LLMClient` in `next_generation`** - -```python -def next_generation( - population: list[HypothesisAgentGenome], - fitnesses: dict[str, float], - cfg: GAConfig, - rng: random.Random, - llm: LLMClient | None = None, -) -> list[HypothesisAgentGenome]: - ... - child = weighted_random_mutate(parent, rng, llm, cfg.prompt_mutation_weight) -``` - -- [x] **Step 4.3: Wire in orchestrator** - -`RunConfig.prompt_mutation_weight: float = 0.0` (default off). Quando attivo via CLI `--prompt-mutation-weight 0.30`, passare a `next_generation`. - -- [x] **Step 4.4: Integration test** - -Loop 2 gen × 5 genomi, mock LLM che ritorna prompt sempre diversi. Verifica che la popolazione finale abbia più diversità prompt della iniziale. - ---- - -## Task 5: Diversity metric - -**Files:** -- New: `src/multi_swarm/metrics/diversity.py` -- New: `tests/unit/test_diversity.py` - -- [x] **Step 5.1: Implement `population_prompt_diversity`** - -```python -def population_prompt_diversity(prompts: list[str]) -> float: - """Levenshtein normalizzata media su tutte le coppie. 0.0 = identici, 1.0 = totalmente diversi.""" -``` - -- [x] **Step 5.2: Test** - -Tre prompt identici → 0.0. Tre prompt totalmente diversi → ~1.0. - -- [x] **Step 5.3: Logging** - -Aggiungere `diversity_prompt` come campo per-generazione in `repository.save_generation` (richiede migration leggera). - ---- - -## Task 6: Cost attribution - -**Files:** -- Modify: `src/multi_swarm/llm/cost_tracker.py` -- Modify: tests esistenti - -- [x] **Step 6.1: Aggiungere `call_kind` a `CostRecord`** - -```python -@dataclass -class CostRecord: - ... - call_kind: str = "hypothesis" # "hypothesis" | "mutation" -``` - -- [x] **Step 6.2: Loggare separatamente in summary** - -`summary()["by_call_kind"]` con breakdown. - -- [x] **Step 6.3: Test compatibilità con record esistenti** - -Backward compat: record senza `call_kind` interpretati come `"hypothesis"`. - ---- - -## Verification end-to-end - -- [x] `uv run pytest -q` → 100% passa (157 + nuovi test). -- [x] `uv run python scripts/smoke_run.py` → completa con mock LLM. -- [x] **Run baseline B**: ripetere `phase2-qwen3-001` con `--prompt-mutation-weight 0.0` per controllo. -- [x] **Run trattamento T**: `phase2-qwen3-prompt-mut-001` con `--prompt-mutation-weight 0.30`. -- [x] Confronto: max fitness T > B + 20%, diversity_prompt(T) > diversity_prompt(B) + 30%. -- [x] Costo aggiuntivo run T ≤ $0.10 (sanity check). - ---- - -## Risks & mitigations - -| Rischio | Mitigazione | -|---------|-------------| -| Mode collapse mutator LLM | `mutation_instruction` scelta random + diversity guard Levenshtein | -| Prompt LLM-output non parsabile dal compiler | Validation step + fallback `random_mutate` | -| Costo runaway (loop infinito retry) | `max_tokens=2000`, no retry su validation fail | -| Bias condiviso con generator tier C | Mutator tier B = `deepseek-v4-flash`, famiglia diversa da Qwen3 | -| Variabili confuse con Phase 2 | Attivare **solo** dopo Phase 2 baseline; A/B isolato | - ---- - -## Cost estimate - -Pop = 20, gen = 10, mutation rate ~75% (5 elite + 15 children), prompt_mutation_weight = 0.30: -- ~45 chiamate LLM tier B aggiuntive per run. -- ~500 tok input + 200 tok output per call → 22.5k in + 9k out totali. -- 22.5k × $0.14/1M + 9k × $0.28/1M ≈ **$0.0057/run**. - -Trascurabile rispetto al budget run base (~$0.10). diff --git a/docs/superpowers/plans/2026-05-11-temporal-features.md b/docs/superpowers/plans/2026-05-11-temporal-features.md deleted file mode 100644 index ffa8f81..0000000 --- a/docs/superpowers/plans/2026-05-11-temporal-features.md +++ /dev/null @@ -1,482 +0,0 @@ -# Feature temporali nella grammatica Hypothesis — Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Aggiungere quattro feature temporali (`hour`, `dow`, `is_weekend`, `minute_of_hour`) alla grammatica delle strategie Hypothesis come `FeatureNode`, universalmente accessibili a ogni genoma e usabili con i comparator esistenti. - -**Architecture:** Estensione puramente additiva. La whitelist `KNOWN_FEATURES` in `protocol/grammar.py` cresce da 5 a 9 nomi. Il dispatcher di `FeatureNode` in `protocol/compiler.py` acquisisce un branch prioritario che mappa i nomi temporali a serie derivate da `df.index` (DatetimeIndex UTC). Il prompt template di `agents/hypothesis.py` riceve due esempi few-shot. Nessuna modifica a parser, mutation/crossover, genome dataclass. - -**Tech Stack:** Python 3.13, pandas (DatetimeIndex), pytest. Esecuzione via `uv run`. Repository: `/home/adriano/Documenti/Git_XYZ/Multi_Swarm_Coevolutive`. - -**Spec di riferimento:** `docs/superpowers/specs/2026-05-11-temporal-features-design.md` - ---- - -## File map - -| File | Tipo | Responsabilità | -|------|------|----------------| -| `src/multi_swarm/protocol/grammar.py` | Modify | Estendere `KNOWN_FEATURES` | -| `src/multi_swarm/protocol/compiler.py` | Modify | Aggiungere `_TIME_FEATURE_FNS` + branch in `_eval_node` | -| `src/multi_swarm/agents/hypothesis.py` | Modify | Estendere prompt template con sezione feature temporali + 2 esempi | -| `tests/unit/test_protocol_validator.py` | Modify | +2 test (accept/reject) | -| `tests/unit/test_protocol_compiler.py` | Modify | +5 test (4 feature + 1 integrazione) | - ---- - -## Task 1: Grammar extension + validator tests - -**Files:** -- Modify: `src/multi_swarm/protocol/grammar.py:21-23` -- Modify: `tests/unit/test_protocol_validator.py` (append) - -- [ ] **Step 1.1: Write failing test — validator accepts temporal features** - -Append to `tests/unit/test_protocol_validator.py`: - -```python -def test_validator_accepts_temporal_features() -> None: - for name in ("hour", "dow", "is_weekend", "minute_of_hour"): - src = _wrap( - { - "op": "gt", - "args": [ - {"kind": "feature", "name": name}, - {"kind": "literal", "value": 0}, - ], - } - ) - ast = parse_strategy(src) - validate_strategy(ast) # no exception - - -def test_validator_rejects_temporal_typo() -> None: - src = _wrap( - { - "op": "gt", - "args": [ - {"kind": "feature", "name": "weekday"}, - {"kind": "literal", "value": 0}, - ], - } - ) - ast = parse_strategy(src) - with pytest.raises(ValidationError, match="unknown feature"): - validate_strategy(ast) -``` - -- [ ] **Step 1.2: Run tests to verify they fail** - -Run: `uv run pytest tests/unit/test_protocol_validator.py::test_validator_accepts_temporal_features tests/unit/test_protocol_validator.py::test_validator_rejects_temporal_typo -v` -Expected: First test FAILs with `ValidationError: unknown feature: hour`. Second test PASSes already (weekday is unknown today too). - -- [ ] **Step 1.3: Extend `KNOWN_FEATURES` whitelist** - -Edit `src/multi_swarm/protocol/grammar.py`, lines 21-23: - -```python -KNOWN_FEATURES: frozenset[str] = frozenset( - {"open", "high", "low", "close", "volume", - "hour", "dow", "is_weekend", "minute_of_hour"} -) -``` - -- [ ] **Step 1.4: Run tests to verify both pass** - -Run: `uv run pytest tests/unit/test_protocol_validator.py -v` -Expected: All tests PASS (both new tests + all pre-existing ones). - -- [ ] **Step 1.5: Commit** - -```bash -git add src/multi_swarm/protocol/grammar.py tests/unit/test_protocol_validator.py -git commit -m "feat(protocol): extend KNOWN_FEATURES with temporal feature names" -``` - ---- - -## Task 2: Compiler — `hour` feature - -**Files:** -- Modify: `src/multi_swarm/protocol/compiler.py:135-137` -- Modify: `tests/unit/test_protocol_compiler.py` (append) - -- [ ] **Step 2.1: Write failing test for `hour`** - -Append to `tests/unit/test_protocol_compiler.py`: - -```python -def test_compile_hour_feature_returns_index_hour(ohlcv: pd.DataFrame) -> None: - src = json.dumps( - { - "rules": [ - { - "condition": { - "op": "gt", - "args": [ - {"kind": "feature", "name": "hour"}, - {"kind": "literal", "value": -1}, - ], - }, - "action": "entry-long", - } - ] - } - ) - ast = parse_strategy(src) - fn = compile_strategy(ast) - signal = fn(ohlcv) - # Tutte le righe hanno hour >= 0 > -1, quindi tutte entry-long - assert (signal == Side.LONG).all() -``` - -- [ ] **Step 2.2: Run test to verify it fails** - -Run: `uv run pytest tests/unit/test_protocol_compiler.py::test_compile_hour_feature_returns_index_hour -v` -Expected: FAIL with `KeyError: 'hour'` (df has no `hour` column, dispatcher falls into `df[name]`). - -- [ ] **Step 2.3: Add `_TIME_FEATURE_FNS` and dispatcher branch** - -Edit `src/multi_swarm/protocol/compiler.py`. Insert after line 108 (end of `INDICATOR_FNS`): - -```python -_TIME_FEATURE_FNS: dict[str, Callable[[pd.DatetimeIndex], pd.Series]] = { - "hour": lambda idx: pd.Series(idx.hour, index=idx, dtype="int64"), - "dow": lambda idx: pd.Series(idx.dayofweek, index=idx, dtype="int64"), - "is_weekend": lambda idx: pd.Series((idx.dayofweek >= 5).astype("int64"), index=idx), - "minute_of_hour": lambda idx: pd.Series(idx.minute, index=idx, dtype="int64"), -} -``` - -Then modify `_eval_node` at line 135-137. Replace: - -```python -def _eval_node(node: Node, df: pd.DataFrame) -> pd.Series: - if isinstance(node, FeatureNode): - return df[node.name] -``` - -With: - -```python -def _eval_node(node: Node, df: pd.DataFrame) -> pd.Series: - if isinstance(node, FeatureNode): - if node.name in _TIME_FEATURE_FNS: - return _TIME_FEATURE_FNS[node.name](df.index) - return df[node.name] -``` - -- [ ] **Step 2.4: Run test to verify it passes** - -Run: `uv run pytest tests/unit/test_protocol_compiler.py::test_compile_hour_feature_returns_index_hour -v` -Expected: PASS. - -- [ ] **Step 2.5: Commit** - -```bash -git add src/multi_swarm/protocol/compiler.py tests/unit/test_protocol_compiler.py -git commit -m "feat(protocol): dispatcher temporal features (hour) in compiler" -``` - ---- - -## Task 3: Compiler — `dow` and `is_weekend` tests - -**Files:** -- Modify: `tests/unit/test_protocol_compiler.py` (append) - -Nessuna modifica al sorgente: `_TIME_FEATURE_FNS` definito in Task 2 contiene già le quattro funzioni. Questi test verificano semantica e copertura. - -- [ ] **Step 3.1: Add `dow` test** - -Append to `tests/unit/test_protocol_compiler.py`: - -```python -def test_compile_dow_feature_monday_is_zero(ohlcv: pd.DataFrame) -> None: - # 2024-01-01 e' un lunedi -> dow=0; gating eq dow 0 deve dare LONG su monday only. - src = json.dumps( - { - "rules": [ - { - "condition": { - "op": "eq", - "args": [ - {"kind": "feature", "name": "dow"}, - {"kind": "literal", "value": 0}, - ], - }, - "action": "entry-long", - } - ] - } - ) - ast = parse_strategy(src) - fn = compile_strategy(ast) - signal = fn(ohlcv) - # ohlcv fixture: 200h da 2024-01-01 00:00 UTC -> primo lunedi e' bar 0..23 - monday_hours = signal[(signal.index.dayofweek == 0)] - other_hours = signal[(signal.index.dayofweek != 0)] - assert (monday_hours == Side.LONG).all() - assert (other_hours == Side.FLAT).all() -``` - -- [ ] **Step 3.2: Add `is_weekend` test** - -Append: - -```python -def test_compile_is_weekend_returns_zero_one(ohlcv: pd.DataFrame) -> None: - src = json.dumps( - { - "rules": [ - { - "condition": { - "op": "eq", - "args": [ - {"kind": "feature", "name": "is_weekend"}, - {"kind": "literal", "value": 1}, - ], - }, - "action": "entry-long", - } - ] - } - ) - ast = parse_strategy(src) - fn = compile_strategy(ast) - signal = fn(ohlcv) - weekend = signal[signal.index.dayofweek >= 5] - weekdays = signal[signal.index.dayofweek < 5] - assert (weekend == Side.LONG).all() - assert (weekdays == Side.FLAT).all() -``` - -- [ ] **Step 3.3: Run both tests** - -Run: `uv run pytest tests/unit/test_protocol_compiler.py::test_compile_dow_feature_monday_is_zero tests/unit/test_protocol_compiler.py::test_compile_is_weekend_returns_zero_one -v` -Expected: Both PASS. - -- [ ] **Step 3.4: Commit** - -```bash -git add tests/unit/test_protocol_compiler.py -git commit -m "test(protocol): compiler semantica dow + is_weekend" -``` - ---- - -## Task 4: Compiler — `minute_of_hour` test - -**Files:** -- Modify: `tests/unit/test_protocol_compiler.py` (append) - -- [ ] **Step 4.1: Add `minute_of_hour` test** - -Append: - -```python -def test_compile_minute_of_hour_zero_on_1h_timeframe(ohlcv: pd.DataFrame) -> None: - # Fixture ohlcv ha freq=1h, quindi tutti i minute_of_hour sono 0. - # gating eq minute_of_hour 0 -> LONG su TUTTE le righe. - src = json.dumps( - { - "rules": [ - { - "condition": { - "op": "eq", - "args": [ - {"kind": "feature", "name": "minute_of_hour"}, - {"kind": "literal", "value": 0}, - ], - }, - "action": "entry-long", - } - ] - } - ) - ast = parse_strategy(src) - fn = compile_strategy(ast) - signal = fn(ohlcv) - assert (signal == Side.LONG).all() -``` - -- [ ] **Step 4.2: Run test** - -Run: `uv run pytest tests/unit/test_protocol_compiler.py::test_compile_minute_of_hour_zero_on_1h_timeframe -v` -Expected: PASS. - -- [ ] **Step 4.3: Commit** - -```bash -git add tests/unit/test_protocol_compiler.py -git commit -m "test(protocol): compiler semantica minute_of_hour su 1h" -``` - ---- - -## Task 5: Compiler — integrazione con regola completa - -**Files:** -- Modify: `tests/unit/test_protocol_compiler.py` (append) - -- [ ] **Step 5.1: Add integration test** - -Append: - -```python -def test_rule_with_temporal_gating_compiles_and_executes(ohlcv: pd.DataFrame) -> None: - # Regola: entry-long se hour > 14 AND close > sma(20). - # close in fixture e' lineare crescente, quindi close > sma(20) e' True dopo warmup. - # entry-long deve apparire solo nelle bar con hour > 14. - src = json.dumps( - { - "rules": [ - { - "condition": { - "op": "and", - "args": [ - { - "op": "gt", - "args": [ - {"kind": "feature", "name": "hour"}, - {"kind": "literal", "value": 14}, - ], - }, - { - "op": "gt", - "args": [ - {"kind": "feature", "name": "close"}, - {"kind": "indicator", "name": "sma", "params": [20]}, - ], - }, - ], - }, - "action": "entry-long", - } - ] - } - ) - ast = parse_strategy(src) - fn = compile_strategy(ast) - signal = fn(ohlcv) - - # Bar con hour <= 14: mai LONG (gating temporale blocca). - morning = signal[signal.index.hour <= 14] - assert (morning == Side.FLAT).all() - - # Bar con hour > 14 e dopo warmup sma (>=20 bar dall'inizio): LONG. - afternoon_warm = signal[(signal.index.hour > 14) & (np.arange(len(signal)) >= 20)] - assert (afternoon_warm == Side.LONG).all() -``` - -- [ ] **Step 5.2: Run test** - -Run: `uv run pytest tests/unit/test_protocol_compiler.py::test_rule_with_temporal_gating_compiles_and_executes -v` -Expected: PASS. - -- [ ] **Step 5.3: Run full compiler + validator test suite to check regressions** - -Run: `uv run pytest tests/unit/test_protocol_compiler.py tests/unit/test_protocol_validator.py -v` -Expected: All tests PASS (pre-existing + new). Nessun test rotto. - -- [ ] **Step 5.4: Commit** - -```bash -git add tests/unit/test_protocol_compiler.py -git commit -m "test(protocol): integration test gating temporale + sma" -``` - ---- - -## Task 6: Update Hypothesis prompt - -**Files:** -- Modify: `src/multi_swarm/agents/hypothesis.py:84-85` - -- [ ] **Step 6.1: Edit prompt template** - -In `src/multi_swarm/agents/hypothesis.py`, alla riga 84-85 sostituire: - -```python -Leaf - feature OHLCV: - {{"kind": "feature", "name": "open|high|low|close|volume"}} -``` - -con: - -```python -Leaf - feature OHLCV: - {{"kind": "feature", "name": "open|high|low|close|volume"}} - -Leaf - feature TEMPORALI (sempre accessibili, UTC): - {{"kind": "feature", "name": "hour"}} // range 0-23 - {{"kind": "feature", "name": "dow"}} // range 0-6 (lun=0, dom=6) - {{"kind": "feature", "name": "is_weekend"}} // 0 o 1 - {{"kind": "feature", "name": "minute_of_hour"}} // range 0-59 - -Esempi di gating temporale: - // Solo durante la sessione US (14:00-22:00 UTC) - {{"op": "and", "args": [ - {{"op": "gt", "args": [{{"kind": "feature", "name": "hour"}}, {{"kind": "literal", "value": 14}}]}}, - {{"op": "lt", "args": [{{"kind": "feature", "name": "hour"}}, {{"kind": "literal", "value": 22}}]}} - ]}} - - // Solo nel weekend (sab+dom) - {{"op": "eq", "args": [{{"kind": "feature", "name": "is_weekend"}}, {{"kind": "literal", "value": 1}}]}} -``` - -- [ ] **Step 6.2: Run existing hypothesis tests to verify prompt format still valid** - -Run: `uv run pytest tests/unit/test_hypothesis_agent.py -v` -Expected: All tests PASS. Il template `{feature_access}` continua a funzionare perché non lo abbiamo toccato. - -- [ ] **Step 6.3: Commit** - -```bash -git add src/multi_swarm/agents/hypothesis.py -git commit -m "feat(hypothesis): aggiungi feature temporali al prompt con 2 esempi few-shot" -``` - ---- - -## Task 7: Smoke run end-to-end - -**Files:** -- Nessuna modifica al codice. - -Validazione che il loop intero giri con la grammatica estesa: carica OHLCV, genera 4 genomi, compila, backtesta, valuta DSR, applica Adversarial, persiste. - -- [ ] **Step 7.1: Run smoke script** - -Run: `uv run python -m scripts.smoke_run` -Expected: completamento senza eccezioni, output finale contenente `Smoke run completed`. - -- [ ] **Step 7.2: Inspect at least one generated genome for temporal feature usage** - -Run: - -```bash -LATEST=$(sqlite3 runs.db "SELECT id FROM runs WHERE name LIKE 'smoke%' ORDER BY started_at DESC LIMIT 1;") -sqlite3 runs.db "SELECT genome_id, substr(raw_text, 1, 600) FROM evaluations WHERE run_id='$LATEST' LIMIT 4;" -``` - -Expected output: 4 righe raw_text JSON. Almeno 1 dovrebbe contenere `"name": "hour"`, `"name": "dow"`, `"name": "is_weekend"`, o `"name": "minute_of_hour"`. Se 0/4 usano feature temporali, il prompt non è abbastanza eloquente — apri un follow-up per iterare il prompt (non bloccante per questa PR). - -- [ ] **Step 7.3: Push branch + open PR** - -```bash -git log --oneline -8 # verifica 6 commit dei Task 1-6 -git push origin HEAD -``` - -Aprire PR con titolo `feat: feature temporali nella grammatica Hypothesis` referenziando lo spec. - ---- - -## Self-review notes (autore del piano) - -- Tutti i 7 hard requirement dello spec (`grammar`, `compiler`, `prompt`, 4 feature, integration test, smoke, backward compat) sono coperti dai Task 1-7. -- Nessun placeholder `TBD`/`TODO`. -- Tipi consistenti: `_TIME_FEATURE_FNS` definito una volta in Task 2 e referenziato implicitamente dai tester nei Task 3-5 senza bisogno di re-definizione. -- Test pre-esistenti non vengono toccati; il Task 5 include `pytest` sull'intera suite del protocollo come regression check. -- Backward compat: `KNOWN_FEATURES` cresce, il branch OHLCV resta invariato → genomi vecchi restano validi senza migrazione DB. diff --git a/docs/superpowers/specs/2026-05-09-decisione-strategica-design.md b/docs/superpowers/specs/2026-05-09-decisione-strategica-design.md deleted file mode 100644 index 77df3dd..0000000 --- a/docs/superpowers/specs/2026-05-09-decisione-strategica-design.md +++ /dev/null @@ -1,427 +0,0 @@ -# Decisione Strategica PoC Multi-Swarm Coevolutivo — Design - -**Autore**: Adriano Dal Pastro -**Data**: 9 maggio 2026 -**Status**: Design strategico approvato per implementazione -**Versione**: 1.0 -**Documenti correlati**: -- `00_documento_zero.md` (framework concettuale) -- `coevolutive_swarm_system.md` (Filone A, sistema completo) -- `poc_trading_swarm.md` (Filone B, design PoC trading) - ---- - -## 1. Executive Summary - -Questo documento formalizza la decisione strategica su come avviare il progetto Multi-Swarm Coevolutivo. La scelta cade sulla variante **B3 — PoC trading incrementale a tre fasi con gate go/no-go**, una declinazione disciplinata dell'opzione Smart Spike (Filone B) descritta nel documento zero. - -La forma incrementale tripartita non sostituisce il design del PoC contenuto in `poc_trading_swarm.md`, ne organizza l'esecuzione in fasi successive con kill-switch numerici espliciti. Il principio guida è **applicare al progetto stesso la disciplina che il sistema dovrà applicare alle proprie ipotesi**: spegnere ciò che non funziona quando i dati lo dicono, senza sconti emotivi e senza giudizi soggettivi sui hard gate. - -**Vincoli operativi adottati**: - -| Dimensione | Valore | -|---|---| -| Obiettivo primario | Sistema produttivo che generi valore (trading reale, fase posteriore al PoC) | -| Dominio iniziale | Derivati crypto BTC/ETH | -| Tempo committed | Full-time, oltre 30h settimanali | -| Budget LLM cap | $2.200 hard, segmentato per fase | -| Capitale a rischio | $500-2.000 (solo nella Phase 3 forward-test mainnet) | -| Tempo calendario | 14-18 settimane atteso, 20 settimane hard cap | -| Setup tecnico di partenza | Cerbero_mcp operativo (multi-exchange, indicatori, audit, dual env) | - -**Esito atteso**: alla fine delle tre fasi, una decisione binaria documentata con razionale numerico fra (a) avviare il sistema completo Filone A con confidence empirica forte, (b) iterare la Phase 2 su debolezze identificate, (c) pivotare su un dominio diverso (offerte commerciali Tielogic, code review), oppure (d) chiudere il progetto con learnings registrati. - ---- - -## 2. Razionale della scelta strategica - -### 2.1 Le tre opzioni del documento zero, oggi - -Il documento zero presenta tre opzioni: A (Big Bet, sistema completo, 12-18 mesi), B (Smart Spike, PoC trading, 3-4 mesi), C (Research Dive, paper review più PoC minimo, 1-2 mesi). Alla luce dei vincoli operativi sopra elencati, la valutazione cambia rispetto al momento in cui il documento zero è stato scritto. - -L'opzione A è esclusa dal vincolo budget. Le stime conservative del Filone A indicano costi LLM nell'ordine di $10.000-30.000 anche solo per la fase iniziale, contro un cap committed di $2.200. Non è una questione di tempo: il tempo full-time c'è. È che il rapporto fra costo del run e disponibilità non lo rende sostenibile senza una validazione empirica preliminare. - -L'opzione C sotto-utilizza due asset materiali. Il primo è la disponibilità full-time, che rende il vincolo "non posso permettermi di costruire infrastruttura" non applicabile. Il secondo è Cerbero_mcp, già operativo: leggere paper per due settimane senza scrivere codice produttivo significherebbe lasciare ferma un'infrastruttura già pronta a essere wrappata come tool layer per agenti LLM. - -Resta l'opzione B. Il documento zero la indicava come scelta preferita; questo documento la conferma e la struttura. - -### 2.2 Perché B3 e non B1 o B2 - -Tre varianti di B sono state considerate. - -La variante **B1 (Lean / single-shot)** comprime il PoC in un'unica run con popolazione ridotta a 20-30 agenti e tier C unico. Coerente con il budget tight, ma rischiosa: la dimensione della popolazione è il principale moltiplicatore di diversità nel sistema, e una popolazione undersized rischia di produrre un falso negativo. Si decreterebbe il sistema "non funzionante" quando in realtà non gli abbiamo dato il numero di tentativi necessari. - -La variante **B2 (Canonical / come da documento)** segue fedelmente `poc_trading_swarm.md` con K=50, mix tier B/C, full set di ablation. Tecnicamente solida ma sfora il budget cap di un fattore 1.5-2x. Adottabile solo accettando di alzare il cap a $3-4K, decisione non giustificabile senza segnali empirici preliminari. - -La variante **B3 (Incrementale)**, scelta, articola il PoC in tre fasi sequenziali con cap budget per fase e gate decisionali quantitativi fra una fase e la successiva. Phase 1 valida il loop tecnico con popolazione minima e tier economico. Phase 2 esegue il PoC canonico solo se Phase 1 passa. Phase 3 forward-testa con capitale reale solo se Phase 2 passa. Il budget totale resta entro $2.2K e il rischio di falso negativo viene ridotto dal fatto che la popolazione completa di Phase 2 non viene mai tagliata: viene messa al lavoro solo dopo che il loop è stato validato. - -La struttura tripartita ha anche un beneficio non monetario: i deliverable di Phase 1 e Phase 2 valgono anche se le fasi successive falliscono. Il backtest engine, il GA harness, la Cerbero integration, la dashboard Streamlit, la pipeline DSR sono tutti riusabili in caso di pivot di dominio. Solo il capitale di Phase 3 è genuinamente "a rischio" come costo della validazione. - -### 2.3 Coerenza con la filosofia del progetto - -Il documento zero al §3.3 identifica come takeaway fondamentale di Renaissance la disciplina di spegnere strategie quando l'edge svanisce, senza decisioni emotive. Il design B3 applica questa disciplina al progetto stesso, prima ancora che al sistema. I gate sono numerici, le soglie sono fissate prima di vedere i dati, l'azione di stop è meccanica quando un hard gate fallisce. Non c'è spazio per "magari un'altra generazione" o "i risultati erano quasi ottimi": la decisione è già scritta nel design. - ---- - -## 3. Vincoli di terminazione globali - -Tre kill-switch globali sopra le singole fasi: - -1. **Cap budget LLM globale**: $2.200 hard. Spesa effettiva monitorata da pagina Overview della dashboard, contatore aggiornato dopo ogni batch. Sforamento previsto entro la fase corrente → riformulazione scope, non incremento cap. -2. **Cap tempo calendario**: 14-18 settimane attese, **20 settimane hard cap** dalla settimana 1 della Phase 1 alla decisione formale post-Phase 3. Sforamento previsto del cap → decisione anticipata sulla base dei dati raccolti, non estensione del cap. -3. **Hard gate falliti**: per costruzione, un hard gate fallito chiude la fase corrente e non apre quella successiva. La decisione fra stop, pivot, o iterazione è formalizzata nel decision memo della fase chiusa, non nel passaggio automatico alla successiva. - ---- - -## 4. Phase 1 — Lean Spike - -**Obiettivo**: dimostrare che il loop tecnico funziona end-to-end. Non si misura ancora alpha: si misura se il sistema gira come progettato, se l'output LLM è formalizzabile, se la GA converge, se i costi sono prevedibili. - -### 4.1 Scope IN - -- Infrastruttura backtest minima: dataset 2 anni (2024-2026) di OHLCV 1h BTC/ETH, engine event-driven semplificato (no microstructure, no slippage modelling complesso, fees fissi a 5 basis points), walk-forward expanding 70/30. -- Wrapper Cerbero come tool layer per agenti: i tool MCP esistenti (`indicators`, `vol_cone`, `oi_weighted_skew`, indicatori options/microstructure/stats) tradotti in funzioni callable dagli agenti. Niente reimplementazione, solo wrapping. -- Protocollo S-expression fisso: 12-15 verbi disegnati manualmente come da `poc_trading_swarm.md` §2.2. Nessuna evoluzione del protocollo in questa fase. -- Hypothesis Swarm con K=20 agenti, tier C unico (Qwen 2.5 72B via OpenRouter). Mutazione e crossover prompt come da documento PoC. Nessuna speciation, nessun novelty bonus. -- Falsification e Adversarial layer hand-crafted, un agente fisso per ognuno, prompt manuali. Tier B (Sonnet 4.6) chiamato solo per i top-5 candidati a fine generazione, per contenere costi. -- Fitness function v0: DSR in-sample con drawdown penalty. No multi-livello, no out-of-sample ancora. -- GA loop: 8-12 generazioni, tournament selection, elitism k=2. - -### 4.2 Scope OUT (esplicito) - -- Multi-tier ablation comparativa. -- Out-of-sample DSR e hold-out finale. -- Random Forest baseline. -- Speciation, novelty, diversity metrics. -- Forward-test con capitale reale. -- Domini diversi da BTC/ETH. - -### 4.3 Budget e tempo - -- LLM: $500-700. Stima base: 20 agenti × ~8.000 token output medi × 10 generazioni × pricing Qwen ≈ $300, più 50% di overhead per Adversarial, Falsification e iterazioni di sviluppo, totale stimato $500-700. -- Tempo: 4-6 settimane full-time. Settimane 1-2 backtest engine e Cerbero wrapper, settimana 3 GA infrastructure e parser S-expression, settimane 4-5 tuning e run completo, settimana 6 analisi e decisione gate. -- Capitale a rischio: zero. - -### 4.4 Gate go/no-go (tutti AND) - -Cinque hard gate: - -1. **Loop converge**: la fitness mediana della popolazione cresce per almeno tre generazioni consecutive prima di plateau. -2. **Output formalizzabile**: almeno l'80% delle proposte LLM passano il parser S-expression senza intervento manuale. -3. **Tail superiore esiste**: i top-5 genomi hanno DSR in-sample pari ad almeno 1.5x la mediana di popolazione, segnale che esiste struttura e non solo rumore. -4. **Diversità non collassa**: entropia della distribuzione di fitness in popolazione superiore a 0.5 a fine run, evita la convergenza monocoltura. -5. **Cost predictability**: spesa effettiva entro ±30% della stima preventivata. - -Anche un solo hard gate fallito chiude la Phase 1. Decisione successiva (pivot, ridiscussione design, stop) presa nel decision memo, non automaticamente in Phase 2. - -### 4.5 Deliverable Phase 1 - -- Codice testato (pytest): backtest engine, Cerbero wrapper, GA loop, protocol parser. -- Report tecnico (~5 pagine): loop convergence con grafici, ispezione qualitativa dei top-5 genomi, parser failure modes osservati, costi reali vs preventivo, diversity metrics. -- Decision memo: vai/non-vai a Phase 2 con eventuali aggiustamenti di scope. - ---- - -## 5. Phase 2 — Canonical PoC - -**Obiettivo**: rispondere ai cinque test del PoC originale (`poc_trading_swarm.md` §1) con popolazione e infrastruttura adeguate. Solo questa fase produce una vera misura dell'edge potenziale del sistema. - -### 5.1 Scope IN (in aggiunta a Phase 1) - -- Hypothesis Swarm K=40, scaling della popolazione a un livello canonico ridotto (K=50 documento → K=40 per disciplina budget). -- Tier mix B/C: circa 70% Qwen/DeepSeek (tier C), 30% Sonnet 4.6 (tier B). L'ablation comparativa misura il valore aggiunto del tier B. -- Speciation di base: clustering dei genomi per cosine similarity dei prompt più cognitive_style. Si mantengono almeno 3 specie attive, ognuna con quote di tournament protette. -- Novelty bonus: fitness composta α·DSR_OOS + β·novelty_score, dove la novelty è calcolata come distanza behavioural dei segnali rispetto a un archivio di elite. -- Walk-forward expanding più hold-out finale: training Q1-2024 a Q4-2025 in walk-forward, hold-out intoccabile Q1-Q2 2026. -- Random Forest baseline: feature engineering classico (returns multi-orizzonte, RSI/MACD/ATR, vol cone, funding rate, OI changes), classificazione long/flat/short su orizzonti 1d e 4h, valutato sulla stessa hold-out. -- Adversarial layer hand-crafted potenziato: cinque prompt distinti (data snooping, lookahead, regime fragility, crowding, transaction cost erosion) eseguiti sui top-10 candidati prima della valutazione OOS. -- Falsification con Deflated Sharpe Ratio (Bailey & López de Prado), correzione Bonferroni sul numero totale di ipotesi testate. -- Fitness multi-livello: per-agent (contributo DSR), per-team (DSR portfolio), diversity penalty per ridurre collusione. - -### 5.2 Scope OUT - -- Co-evolution del protocollo (Filone A). -- Forward-test con capitale reale (Phase 3). -- Speciation NEAT-style completa. -- Idiom emergence. -- Domini diversi da trading. - -### 5.3 Budget e tempo - -- LLM: $700-1.100. Dettaglio stimato: tier C ~$500, tier B ~$400, overhead per ablation iterativa ~$200. Range ampio per consentire più cicli di ablation se i primi risultati lo richiedono. -- Tempo: 4-6 settimane full-time. Settimana 1 porting K=40, speciation, novelty. Settimane 2-3 ablation runs (tier C only, tier B only, mix; con e senza speciation). Settimana 4 hold-out evaluation, RF baseline, Adversarial sweep. Settimane 5-6 analisi statistica, report, decisione gate. -- Capitale a rischio: zero. - -### 5.4 Gate go/no-go - -**Hard gate (tutti AND, altrimenti stop)**: - -1. **Significatività statistica**: top genoma su hold-out con DSR > 1.0 e p-value < 0.05 dopo correzione Bonferroni. -2. **Sopravvivenza regime change**: DSR hold-out almeno 0.5x del DSR walk-forward — limite contro overfitting catastrofico. -3. **Batte baseline**: top-3 genomi con Sharpe OOS superiore al Sharpe RF baseline OOS, effect size non trascurabile (Cohen's d > 0.3 sul rolling Sharpe). - -**Soft gate (informano, non killano)**: - -4. **Diversità**: almeno 3 specie distinte sopravvivono a fine run, top-3 genomi non identici per signal correlation (ρ < 0.7). -5. **Tier B aggiunge valore**: l'ablation mostra Δ Sharpe OOS misurabile per tier mix vs tier C only. In caso negativo, Phase 3 può girare tier C only e il decision memo ne prende nota. - -Hard gate passati → Phase 3. Hard gate falliti → stop o pivot. Soft gate falliti → Phase 3 con scope ridotto e annotazioni nel report. - -### 5.5 Deliverable Phase 2 - -- Codice testato: speciation, novelty, ablation harness, RF baseline, DSR pipeline, Adversarial battery. -- Report scientifico (~15-20 pagine): metodologia, risultati per ogni gate, ablation table, top-5 strategie ispezionate qualitativamente, threats to validity. -- Decision memo: vai/non-vai a Phase 3, scope di Phase 3 (capitale, exchange, leva, durata) calibrato sui risultati. - ---- - -## 6. Phase 3 — Forward-test mainnet - -**Obiettivo**: vedere se l'edge sopravvive in produzione. Anche il backtest più rigoroso ha bias inevitabili (look-ahead microscopico, slippage idealizzato, assenza di information leakage da fonti che non esistevano in periodo storico). Solo il forward-test live risponde alla domanda finale. - -### 6.1 Scope IN - -- Selezione strategie: top-3 genomi out-of-sample dalla Phase 2, dopo passaggio completo dell'Adversarial battery. Niente cherry-picking ex-post post-Phase 2. -- Capitale: $500-2.000 totali, distribuiti sui tre genomi con allocazione equal weight oppure risk-parity sulla volatilità OOS attesa, scelta motivata nel decision memo Phase 2. -- Exchange: scelta condizionata alle strategie selezionate. Default raccomandati Bybit (perp, fees competitive, liquidità BTC/ETH adeguata) oppure Hyperliquid (no KYC, transparent funding). Cerbero supporta entrambi nativamente. -- Leva: massimo 2x in forward-test. Anche se lo Sharpe OOS suggerisse di più, in fase di validazione la leva resta bassa per non confondere edge della strategia con leva. -- Durata: 6-8 settimane continue. Razionale: in crypto questa finestra copre tipicamente uno o due micro-regime change, sufficienti a stressare il modello senza catastrofi statistiche da campione troppo piccolo. -- Monitoring: dashboard giornaliera (sezione Live Monitor) con Sharpe live realized vs Sharpe OOS atteso, drawdown realtime, violation count (segnali generati ma non eseguiti per qualunque ragione), audit log Cerbero per ogni order. -- Decision triggers automatici: kill-switch se il drawdown live supera 1.5x il peggiore osservato in walk-forward; pause se Sharpe rolling 14d resta negativo per 14 giorni consecutivi. -- Adversarial post-mortem settimanale: l'agente Adversarial gira nuovamente sul signal log della settimana per identificare degradazione dell'edge (regime drift detection). - -### 6.2 Scope OUT - -- Capital scaling oltre $2.000 in Phase 3. Se i risultati promettono, la decisione di scaling è esplicitamente fuori dal PoC e viene presa dopo il decision memo finale. -- Multi-strategy portfolio rebalancing dinamico. Allocazione statica sui tre genomi. -- Hedging cross-exchange. Confonderebbe la lettura dell'edge della strategia. -- Aggiunta di nuovi genomi in corsa. I tre genomi sono fissati a inizio fase. - -### 6.3 Budget e tempo - -- LLM: $200-400. La popolazione GA non gira più, gli agenti sono richiamati solo per Adversarial post-mortem settimanale e per occasionali refresh quando i decision triggers scattano. -- Capitale a rischio: $500-2.000. Trattato come **costo della validazione**, non come investimento. Se il capitale va a zero, il dato che ne ricaviamo vale comunque. -- Tempo: 6-8 settimane di calendario, monitoring operativo circa 5h/settimana — non full-time. Le settimane libere sono allocate a documentazione finale, lavoro su Tielogic e altri progetti, prima esplorazione Filone A in caso di decisione GO. -- Costo infra extra: circa $10-30/mese VPS per dashboard più monitoring, in larga parte già coperto dal setup Hostinger esistente. - -### 6.4 Gate decisionale finale del PoC - -**Hard gate per "GO sistema completo / Filone A"**: - -1. **Edge sopravvive live**: Sharpe live realized almeno 0.5x dello Sharpe OOS atteso, su finestra di almeno 4 settimane. -2. **No catastrophic failure**: max drawdown live al massimo 1.5x del peggior drawdown walk-forward. -3. **Reproducibility**: almeno 2 dei 3 genomi performano in linea con previsione — la fortuna non si concentra su uno solo. - -**Soft gate (qualitativi, informano la decisione)**: - -4. **Audit Adversarial settimanali**: nessuna scoperta critica come lookahead nascosto emerso solo live, oppure data leakage da provider. -5. **Cost economy**: edge dopo costi reali (slippage effettivo, fees, funding) resta positivo. - -**Esiti possibili**: - -- Hard gate e soft gate tutti passati → GO Filone A con confidence empirica forte. Si apre la ridiscussione del budget e della roadmap del sistema completo, fuori dal perimetro di questo documento. -- Hard gate passati, soft gate falliti → iterazione Phase 2 mirata sulle debolezze identificate, no Filone A immediato. -- Hard gate falliti, ma senza catastrofi → l'idea regge concettualmente ma non scala live retail. Decision memo con due opzioni: pivot dominio (offerte Tielogic, code review) oppure chiusura del progetto. -- Hard gate falliti più drawdown catastrofico → l'idea non regge live. Stop del progetto. Documento di chiusura con learnings registrati per progetti futuri. - -### 6.5 Deliverable Phase 3 e finali - -- Report finale del PoC (~20-30 pagine): metodologia completa, risultati Phase 1+2+3, comparison Sharpe in-sample / OOS / live, ispezione qualitativa delle strategie, learnings, threats to validity confermati o respinti. -- Decision memo strategico: GO Filone A / iterate Phase 2 / pivot dominio / stop, con razionale quantitativo ancorato ai gate. -- Codebase pubblicabile (anche se repo privato): backtest, GA, Cerbero integration, speciation, DSR pipeline, RF baseline, monitoring dashboard, tutto documentato. - ---- - -## 7. GUI Streamlit incrementale - -Una dashboard è essenziale per ispezionare cosa fa il sistema, decidere i gate in modo informato, e produrre i grafici che entreranno nei report. - -### 7.1 Architettura - -- Tech stack: Streamlit single-app multi-page, dati letti da SQLite locale (`runs.db`) e Parquet per series numerici pesanti. -- SQLite per stato: genomi, generazioni, fitness, ablation results, adversarial findings, trade log. Schema relazionale stabile, query veloci, nessun DB server da gestire. -- Parquet per series: equity curves, signal time series, OHLCV. File-based, columnar, leggero. -- Auto-refresh ogni 10 secondi nella sola pagina Live Monitor (Phase 3). Sufficiente per uno scope a decisioni minute-level, non HFT. -- Single app, multipage: tutto sotto `dashboard/streamlit_app.py` più `pages/`. Deploy locale con `streamlit run`, niente VPS frontend. - -### 7.2 Pagine costruite incrementalmente - -**Phase 1 (3-4 giorni di lavoro)**: - -- *Overview*: ultima run, generazione corrente, stato (running/completed/failed), spesa LLM cumulata vs cap. -- *GA Convergence*: line plot fitness mediana / max / 90° percentile per generazione, distribuzione fitness ultima generazione (histogram), counter chiamate LLM e costo. -- *Genomes (basic)*: tabella top-10 genomi correnti con DSR, cognitive_style, temperature, lookback. Click su riga apre side panel con system_prompt completo, feature_access, lineage parent_id. - -**Phase 2 (9-12 giorni distribuiti)**: - -- *Genomes (avanzato)*: lineage tree interattivo (parent → children su 15 generazioni), speciation cluster view (UMAP/t-SNE su prompt embedding più parametri, colori per specie), filtri per specie / tier / cognitive_style. -- *Performance*: equity curve in-sample, walk-forward, OOS, hold-out per ogni genoma. Sharpe, DSR, drawdown. Trade distribution per regime di volatilità, asset, orario. Per-strategy e portfolio view. -- *Ablation*: confronto runs (tier C only, tier B only, mix) side-by-side. Δ Sharpe OOS, costo per percentile fitness, breakeven analysis. -- *Adversarial*: per ogni top-10 genoma, le cinque critiche (data snooping, lookahead, regime fragility, crowding, transaction cost). Click espande prompt completo, risposta LLM, decisione pass/fail con rationale. -- *RF Baseline*: Sharpe RF baseline OOS, feature importance, comparison vs top-3 swarm, Cohen's d effect size. - -**Phase 3 (4-5 giorni)**: - -- *Live Monitor*: P&L realtime per strategia e portfolio, equity curve da inizio Phase 3, drawdown rolling. Auto-refresh 10s. -- *Live vs OOS*: Sharpe live realized vs Sharpe OOS atteso (con confidence interval), gauge "edge sopravvive?", cumulative deviation tracker. -- *Triggers state*: stato kill-switch per strategia, distance from threshold, history pause/resume, audit log decisioni recenti. -- *Adversarial weekly*: report settimanale di regime drift detection, diff vs settimana precedente. - -### 7.3 Costi GUI - -- Effort totale: 16-21 giorni netti, distribuiti come da fasi sopra. -- LLM: zero (codice e visualizzazione Python locale). -- Infra: zero (esecuzione locale, SQLite locale, Parquet locale). - -### 7.4 Trade-off accettati - -- Refresh non realtime sub-secondo: accettabile per scope decisionale minute/hour. -- Niente login né multi-utente: dashboard personale solo locale. -- Niente alert push esterni in default: alert appaiono in dashboard. Per Phase 3 si può aggiungere webhook Telegram/email se necessario, mezza giornata extra di lavoro. -- Streamlit re-runs full-page on interaction: gestibile con `@st.cache_data` su query SQLite e dataset PoC di dimensioni contenute. - -### 7.5 Deliverable GUI - -- Codice `dashboard/` testato (smoke test e data layer test). -- Schema SQLite versionato con migrazioni semplici (Alembic light o script SQL). -- README con istruzioni `streamlit run` e descrizione di ogni pagina. - ---- - -## 8. Hardware e infrastruttura - -Principio: tutto locale più Cerbero come unico servizio remoto. Nessuna GPU, nessun cloud compute, nessun costo infra ricorrente significativo. - -### 8.1 Compute - -- Backtest e GA loop: PC locale Linux. Tutto CPU-bound, parallelizzabile su core (joblib o multiprocessing). Dataset 2 anni OHLCV 1h BTC+ETH circa 30-50 MB. Anche granularità 1m sarebbe sotto i 2 GB, gestibile. -- LLM: tutte chiamate via API esterne (OpenRouter per tier C, Anthropic per tier B). Nessun model locale, nessuna GPU. -- Streamlit dashboard: locale (`streamlit run` su `localhost:8501`). - -### 8.2 Cerbero_mcp - -Già configurato e operativo. Modalità d'uso durante PoC: - -- Locale via Docker compose durante development e Phase 1-2 (testnet only). Riduce latenza, niente costi VPS, debug più rapido. -- VPS Hostinger durante Phase 3 forward-test (mainnet). Già setup `/opt/cerbero-mcp` con `deploy-vps.sh` e branch V2.0.0. -- Token bearer: `TESTNET_TOKEN` per Phase 1-2 backtest replay, `MAINNET_TOKEN` solo per Phase 3. -- Bot tag dedicato per il PoC (`X-Bot-Tag: swarm-poc-`). L'audit log Cerbero traccia ogni call separatamente per fase, utile per ricostruzioni post-mortem. - -### 8.3 Storage - -- `runs.db` SQLite per stato GA, genomi, generazioni, fitness, adversarial, trade log. Backup giornaliero su disco esterno o cloud personale. -- `series/` Parquet per equity curves, signal time series, OHLCV cache. Versionato fuori da git (Git LFS o cartella esterna trackata in `.gitignore`). -- Audit log Cerbero: già JSONL con rotazione 30 giorni (`AUDIT_LOG_BACKUP_DAYS=30`). Per Phase 3 aumentare a 90 giorni per coprire intera fase più post-mortem. - -### 8.4 Networking - -- LLM API via OpenRouter (tier C) e Anthropic (tier B). Nessun setup speciale. -- Cerbero locale: porta 9000 default, nessuna esposizione pubblica. -- Cerbero VPS: già protetto da Traefik più bearer più allowlist IP. Nessun lavoro extra. - -### 8.5 Costi infra ricorrenti - -- VPS Hostinger: già pagato per altri progetti, costo marginale zero. -- Storage backup: trascurabile. -- Domini e TLS: già coperti (cerbero-mcp.tielogic.xyz). - -### 8.6 Decisioni hardware non bloccanti - -- Se la Phase 2 ablation richiedesse parallelizzazione massiva (ad esempio cento backtest concorrenti), valutare spot instance AWS o Hetzner. Probabilmente non necessario, le strategie a granularità 1h sono veloci da backtestare anche su CPU desktop. -- Backup off-site di `runs.db`: decisione di lifecycle, non bloccante per Phase 1. - ---- - -## 9. Cadenza review e disciplina autoriale - -Regola personale dell'autore: mai self-approve, separare author pass da review pass. Applicata sistematicamente ai gate del PoC. - -### 9.1 Cadenza working - -- Daily: lavoro full-time. Self-review informale a fine giornata, una riga nel commit message su cosa è andato e cosa no. -- Settimanale (venerdì): review formale con dump strutturato che include fitness convergence plot, top-5 genomi della settimana, spesa LLM accumulata vs cap di fase, eventuali findings Adversarial significativi, aggiornamento di `progress.md`. -- Bi-settimanale: snapshot completo più decision check sul fatto se siamo ancora on-track per il gate. Se due bi-weekly consecutivi mostrano off-track materiale, decisione anticipata di pivot o iterate, non attesa fino a fine fase. - -### 9.2 Gate review (decisione formale fine fase) - -Per ognuno dei tre gate (fine Phase 1, fine Phase 2, fine Phase 3): author pass e review pass separati. - -- **Author pass**: l'autore scrive il decision memo con tutti i numeri, gate per gate, conclusione raccomandata. -- **Review pass**: secondo passaggio con approccio adversarial. Tre opzioni equivalenti: - - Subagent Claude con prompt esplicitamente "red team" che riceve memo più dati grezzi e produce critica strutturata (cherry-picking, debolezze statistiche, omissioni). - - Collega umano disponibile, se esiste un contesto Tielogic adatto. - - Rilettura dopo 48 ore con timer, fresh eyes pass. -- **Sintesi**: solo dopo il review pass la decisione viene formalizzata e committata. - -### 9.3 Decision triggers oggettivi - -I gate hard di ogni fase sono numerici (DSR, p-value, drawdown ratio). La decisione GO/STOP è meccanica sui hard gate: - -- Hard gate fallito → STOP automatico, non in discussione. -- Hard gate passato → si valuta se i soft gate danno motivo di iterazione invece di procedere. - -Questa è la disciplina Renaissance applicata al progetto: niente "magari un'altra generazione" se il numero non lo dice. - -### 9.4 Documentazione del processo - -- `docs/runs/YYYY-MM-DD-phase{1,2,3}-run-N.md` per ogni run completato: configurazione, risultati, anomalie, learning. -- `docs/decisions/YYYY-MM-DD-gate-phase{1,2,3}.md` per ogni decisione gate: author pass, review pass, decisione finale, razionale. -- Questo documento (`docs/superpowers/specs/2026-05-09-decisione-strategica-design.md`) come north star strategico, aggiornato se cambiano vincoli o decisioni macro. - ---- - -## 10. Catena di ragionamento (tracciabilità) - -Per riferimento futuro, la sequenza logica che ha portato al design B3: - -1. Obiettivo dichiarato sistema produttivo che generi valore → esclude C (research dive senza output produttivo). -2. Dominio iniziale trading crypto → allinea il PoC al design già scritto in `poc_trading_swarm.md`. -3. Tempo full-time disponibile → scioglie il vincolo "non posso costruire infrastruttura", apre spazio per fasi sequenziali. -4. Budget LLM tight ($1-2K) → esclude A (Filone completo) e impone disciplina su B. -5. Setup Cerbero_mcp esistente → riduce settimane di plumbing, gli agenti chiamano tool MCP nativi. -6. Forward-test mainnet con capitale piccolo come parte del successo → richiede una Phase 3 dedicata, distinta dalla validazione statistica. -7. Disciplina "spegnere ciò che non funziona" → struttura tripartita con kill-switch numerici. -8. Mai self-approve → separazione author pass / review pass nei gate. -9. Necessità di ispezionare cosa fa il sistema → GUI Streamlit come componente orizzontale, non opzionale. - ---- - -## 11. Decisioni risolte e decisioni ancora aperte - -### 11.1 Risolte da questo documento - -- Opzione strategica: B3. -- Dominio iniziale: trading derivati crypto BTC/ETH. -- Numero di fasi: tre, con gate go/no-go fra una e l'altra. -- Budget cap globale: $2.200 LLM più $500-2.000 capitale a rischio Phase 3. -- Cap calendario: 18 settimane. -- Tier mix: solo C in Phase 1, mix B/C in Phase 2-3. -- Tech stack GUI: Streamlit più SQLite più Parquet. -- Infrastruttura: locale più Cerbero_mcp esistente. -- Cadenza review: settimanale, bi-settimanale per check, gate con author/review pass separati. - -### 11.2 Aperte (non bloccanti per Phase 1) - -- Allocazione capitale Phase 3 (equal weight vs risk-parity): decisione formalizzata nel decision memo Phase 2 sulla base dei risultati OOS. -- Exchange Phase 3 (Bybit vs Hyperliquid): scelta dipendente dalle strategie selezionate, decisa nel decision memo Phase 2. -- Approccio review pass (subagent vs umano vs fresh eyes): decisione tattica per gate, nessun lock-in. -- Webhook alert Telegram/email per Phase 3: opzionale, decidibile a inizio Phase 3. - -### 11.3 Esplicitamente fuori scope - -- Filone A (sistema completo) come fase corrente. Decisione su A presa solo dopo decision memo finale Phase 3. -- Filone C (applicazioni non-trading: offerte Tielogic, code review, doc Swagger). Possibile pivot in caso di hard gate falliti, non azione preventiva. -- Co-evolution del protocollo. Nessuna delle tre fasi PoC la include. -- Capital scaling oltre $2.000 in Phase 3. Decisione di scaling appartiene a una fase successiva al PoC. - ---- - -## 12. Prossimi passi - -Esecuzione di Phase 1. - -Il piano implementativo dettagliato di Phase 1 (settimana per settimana, task atomici, dipendenze, verifiche) sarà oggetto del documento successivo, da costruire con l'invocazione dello skill `writing-plans` su questo design. - ---- - -*Documento approvato il 9 maggio 2026. Versione 1.0. Aggiornare in caso di modifica dei vincoli operativi o di esiti di gate che richiedano revisione strategica complessiva.* diff --git a/docs/superpowers/specs/2026-05-11-temporal-features-design.md b/docs/superpowers/specs/2026-05-11-temporal-features-design.md deleted file mode 100644 index 4545ed0..0000000 --- a/docs/superpowers/specs/2026-05-11-temporal-features-design.md +++ /dev/null @@ -1,183 +0,0 @@ -# Feature temporali nella grammatica Hypothesis — Design - -**Data**: 11 maggio 2026 -**Status**: design approvato dall'operatore, pronto per writing-plans -**Scope target**: Phase 2 -**Riferimenti**: `docs/decisions/2026-05-11-phase1-5-nemotron-run.md` (memo che ha originato la discussione) - ---- - -## 1. Motivazione - -Le strategie LLM-generate da Phase 1 operano in modo time-blind: la grammatica espone solo OHLCV (`open`, `high`, `low`, `close`, `volume`) e indicatori tecnici (`sma`, `rsi`, `atr`, `macd`, `realized_vol`) calcolati sopra. Non esiste alcuna feature che permetta al genoma di condizionare il comportamento sull'orario o sul giorno della settimana. - -Questo è un limite strutturale rispetto a BTC-PERPETUAL su Cerbero, dove esistono effetti temporali sistematici: - -- apertura USA (14:30 UTC) e Europa (08:00 UTC) generano volatilità sistematica; -- apertura/chiusura settimanale crypto (Sabato/Domenica vs. resto della settimana) ha liquidità diversa e basis funding diverso; -- la sessione asiatica overnight presenta pattern di trend reversal noti. - -Il design seguente aggiunge alla grammatica quattro feature temporali — `hour`, `dow`, `is_weekend`, `minute_of_hour` — universalmente accessibili a ogni genoma, lasciando inalterati i meccanismi di mutation/crossover esistenti. - ---- - -## 2. Decisioni di design - -Le seguenti scelte sono state ratificate in fase di brainstorming. - -**Quattro feature, non una.** `hour` da sola coprirebbe l'80% dei casi, ma `dow` cattura un asse ortogonale (weekend effect) e `is_weekend` è una scorciatoia espressiva utile al LLM. `minute_of_hour` è incluso per disponibilità futura (timeframe 5m/15m in Phase 2+), inerte sui dati 1h attuali. - -**Accesso universale, non soggetto a `feature_access`.** Le feature temporali sono sempre disponibili a ogni genoma, indipendentemente dal subset OHLCV randomizzato in `ga/initial.py` e mutato da `mutate_feature_access`. Motivo: vogliamo che ogni genoma possa testarle; passarle attraverso `FEATURE_POOL` rischia di lasciarle inutilizzate in metà della popolazione e vanificare l'esperimento. Il prompt indica esplicitamente che sono "sempre accessibili", separate dalla sezione `{feature_access}` del template. - -**Riuso di `FeatureNode`, niente nuovo tipo AST.** Le feature temporali entrano nella stessa whitelist `KNOWN_FEATURES` di OHLCV e usano la stessa shape JSON `{"kind": "feature", "name": "..."}`. Il dispatcher in `compiler.py` discrimina per nome. Alternativa scartata: introdurre `TimeFeatureNode` separato. Avrebbe dato type-safety formale ma richiesto modifiche a parser, validator, JSON shape, prompt — costo eccessivo per beneficio puramente strutturale, dato che semanticamente "ora del giorno" e "prezzo close" sono entrambi attributi della riga. - -**Few-shot examples nel prompt.** L'istruzione minimale (solo nomi) lascia troppo spazio a interpretazioni errate (es. `dow=7` per domenica all'italiana, `hour` in fuso locale invece che UTC). Due esempi concreti — un gating intraday `gt hour 14 AND lt hour 22`, un gating settimanale `eq is_weekend 1` — fissano la semantica al costo di ~200 token addizionali per call. - -**Out-of-range non è errore di validazione.** Il LLM potrebbe emettere `gt hour 25` o `eq dow 7`. Il validator non li intercetta: tecnicamente sono `LiteralNode(value=...)` numerici legali. La condizione sarà semplicemente sempre falsa e l'Adversarial layer (`flat_too_long`, `no_trades`) sanzionerà i genomi che ne sono dipendenti. Aggiungere un check range esplicito sarebbe over-engineering per un caso che il sistema già gestisce. - ---- - -## 3. Architettura — modifiche file-by-file - -Cinque file toccati. Nessun nuovo modulo. - -### `src/multi_swarm/protocol/grammar.py` - -Estendere `KNOWN_FEATURES` da 5 a 9 nomi: - -```python -KNOWN_FEATURES: frozenset[str] = frozenset( - {"open", "high", "low", "close", "volume", - "hour", "dow", "is_weekend", "minute_of_hour"} -) -``` - -Nessun'altra modifica al file. Il validator legge da qui automaticamente. - -### `src/multi_swarm/protocol/compiler.py` - -Aggiungere un dizionario di derivazioni temporali ed estendere il dispatcher di `FeatureNode` con un branch prioritario: - -```python -_TIME_FEATURE_FNS: dict[str, Callable[[pd.DatetimeIndex], pd.Series]] = { - "hour": lambda idx: pd.Series(idx.hour, index=idx, dtype="int64"), - "dow": lambda idx: pd.Series(idx.dayofweek, index=idx, dtype="int64"), - "is_weekend": lambda idx: pd.Series((idx.dayofweek >= 5).astype("int64"), index=idx), - "minute_of_hour": lambda idx: pd.Series(idx.minute, index=idx, dtype="int64"), -} - -# nel branch FeatureNode di _eval_node: -if isinstance(node, FeatureNode): - if node.name in _TIME_FEATURE_FNS: - return _TIME_FEATURE_FNS[node.name](df.index) - return df[node.name] -``` - -Il branch OHLCV preesistente (`return df[node.name]`) resta invariato come fallback per i nomi non temporali. Si assume `df.index` di tipo `DatetimeIndex` UTC, già garantito da `CerberoOHLCVLoader`. - -### `src/multi_swarm/agents/hypothesis.py` - -Aggiungere nel prompt template, dopo la sezione "Leaf - feature OHLCV" (intorno a riga 84), una sezione "Leaf - feature TEMPORALI" con i quattro nomi, i loro range, e due esempi few-shot completi (gating sessione US, gating weekend). Mantenere la sezione separata da `{feature_access}` e dichiarare esplicitamente che le feature temporali sono "sempre accessibili". Contenuto preciso definito nella sezione 5 di questo spec. - -### `tests/protocol/test_compiler.py` - -Cinque test nuovi: - -1. `test_compile_hour_feature_returns_index_hour` — DataFrame 24-bar con index orario, `FeatureNode("hour")` restituisce serie `[0,1,...,23]`. -2. `test_compile_dow_feature_lunedi_is_zero` — verifica convenzione pandas (lunedì → 0, domenica → 6). -3. `test_compile_is_weekend_returns_zero_one` — sabato e domenica → 1, altri → 0. -4. `test_compile_minute_of_hour_zero_on_1h_timeframe` — su index 1h tutti gli output sono 0 (test di regressione del comportamento atteso). -5. `test_rule_with_temporal_gating_compiles_and_executes` — integrazione: regola `entry-long if hour > 14 AND close > sma(20)`, verifica che `Side.LONG` appaia solo nelle bar con `hour > 14`. - -### `tests/protocol/test_validator.py` - -Due test nuovi: - -1. `test_validator_accepts_temporal_features` — i quattro nuovi nomi non sollevano `ValidationError`. -2. `test_validator_rejects_temporal_typo` — `FeatureNode("weekday")` solleva `ValidationError`. - -Test esistenti non devono cambiare. L'aggiunta è puramente additiva. - ---- - -## 4. Contratto delle feature - -| Feature | Tipo | Range | Derivazione pandas | -|---------|------|-------|---------------------| -| `hour` | int64 | 0–23 | `df.index.hour` | -| `dow` | int64 | 0–6 (lun=0) | `df.index.dayofweek` | -| `is_weekend` | int64 | 0 o 1 | `(df.index.dayofweek >= 5).astype(int)` | -| `minute_of_hour` | int64 | 0–59 | `df.index.minute` | - -L'indice del DataFrame è UTC tz-aware per costruzione (`CerberoOHLCVLoader`). I valori temporali sono quindi in UTC, non in fuso locale italiano. Questa scelta è coerente con la convenzione di prezzi e timestamp del progetto e con la natura globale del mercato crypto. - -I confronti tipici emessi dal LLM saranno della forma `{"op": "gt", "args": [{"kind": "feature", "name": "hour"}, {"kind": "literal", "value": 14}]}`. Funzionano via broadcasting numpy senza modifiche a comparator o operator nodes. - ---- - -## 5. Frammento di prompt aggiunto - -Da inserire in `hypothesis.py` dopo l'attuale sezione "Leaf - feature OHLCV": - -```text -Leaf - feature TEMPORALI (sempre accessibili, UTC): - {{"kind": "feature", "name": "hour"}} range 0-23 - {{"kind": "feature", "name": "dow"}} range 0-6 (lun=0, dom=6) - {{"kind": "feature", "name": "is_weekend"}} 0 o 1 - {{"kind": "feature", "name": "minute_of_hour"}} range 0-59 - -Esempi di gating temporale: - // Solo durante la sessione US (14:00-22:00 UTC) - {{"op": "and", "args": [ - {{"op": "gt", "args": [{{"kind": "feature", "name": "hour"}}, {{"kind": "literal", "value": 14}}]}}, - {{"op": "lt", "args": [{{"kind": "feature", "name": "hour"}}, {{"kind": "literal", "value": 22}}]}} - ]}} - - // Solo nel weekend (sab+dom) - {{"op": "eq", "args": [{{"kind": "feature", "name": "is_weekend"}}, {{"kind": "literal", "value": 1}}]}} -``` - -Il blocco va inserito **prima** della frase corrente "Feature accessibili dal tuo genoma: {feature_access}", per chiarire che `{feature_access}` riguarda solo OHLCV mentre le temporali sono universali. - ---- - -## 6. Backward compatibility e impatto sui run esistenti - -Tutti i genomi esistenti nei `runs.db` storici (Phase 1, Phase 1.5 nemotron, Phase 1.5 grok in corso) usano solo feature OHLCV. Con la grammatica estesa restano validi: il validator continua ad accettarli, il compiler li gestisce nel branch OHLCV invariato. - -Non c'è quindi alcuna migrazione di dati. I run vecchi possono essere ri-letti dalla dashboard senza modifiche. La distinzione "run pre/post feature temporali" sarà tracciata implicitamente dalla data del commit di merge. - ---- - -## 7. Validazione end-to-end - -Dopo il merge dei cinque file, la procedura di validazione è: - -1. Esecuzione test suite completa (`uv run pytest`) — i 7 nuovi test devono passare, nessun test esistente deve rompersi. -2. `scripts/smoke_run.py` con `population_size=4, n_generations=1` per verificare che il loop end-to-end completi (caricamento OHLCV → generazione genome → compile → backtest → DSR → adversarial → persistenza). Tempo atteso ~2 minuti. -3. Ispezione manuale di almeno 1 genoma generato post-merge: verificare che il LLM abbia effettivamente usato almeno una feature temporale tra le sue regole. Se in 4 genomi nessuno usa feature temporali, ri-esaminare il prompt. - -Non è previsto un confronto ablation formale (con/senza feature temporali) in questo spec — è un'attività di Phase 2 separata che andrà pianificata in un proprio spec quando si avvierà il run di valutazione. - ---- - -## 8. Out of scope - -I seguenti elementi sono esplicitamente fuori dallo scope di questo spec e dovranno essere oggetto di design dedicato se desiderati: - -- **Feature temporali con segno periodico** (es. `sin_hour`, `cos_dow`): utili per regressioni continue, non per regole booleane GA-based. Skip. -- **Feature di sessione discreta** (es. `session=us|europe|asia`): derivabili componendo `hour` con comparator, non necessario aggiungere come feature primitiva. -- **Time-zone configurabile**: rimane fissa UTC. Cambiare implica refactor del loader OHLCV. -- **Validator range-check** (es. rifiutare `gt(dow, 6)`): sanzionato già dal loop GA via fitness e Adversarial. -- **Modifica del meccanismo `mutate_feature_access`**: invariato. Le feature temporali non entrano nel pool mutabile. -- **Indicatori temporali** (es. `time_since_last_high`): richiede stato persistente, fuori dal modello stateless attuale. - ---- - -## 9. Stima di sforzo - -Implementazione: ~120 LOC (60 di codice + 60 di test) in 5 file. Complessità bassa. - -TDD-driven: scrivere prima i 7 test, verificare che falliscano, poi aggiungere whitelist + dispatcher + prompt. Tempo stimato: 2-3 ore di lavoro continuo, validation smoke run inclusa. - -Costo prompt addizionale per call: ~200 token. Su un run da 200 call, ~40k token aggiuntivi → impatto economico trascurabile (<$0.05 con qualsiasi tier). diff --git a/poc_trading_swarm.md b/poc_trading_swarm.md deleted file mode 100644 index cc4d204..0000000 --- a/poc_trading_swarm.md +++ /dev/null @@ -1,831 +0,0 @@ -# PoC Trading Swarm — Validazione Strategica - -**Autore**: Adriano Dal Pastro -**Data**: Maggio 2026 -**Status**: Design document — pre-implementazione -**Versione**: 0.1 -**Documento correlato**: `coevolutive_swarm_system.md` (sistema completo) - ---- - -## 1. Razionale della deviazione - -Invece di committere 12-18 mesi al sistema co-evolutivo completo descritto nel documento principale, partiamo con un **proof-of-concept strategico** focalizzato su trading, con architettura semplificata, per validare empiricamente se l'idea di base funziona prima di investire nel sistema full. - -**Cosa il PoC valida**: -1. Lo swarm produce strategie che superano il null hypothesis statistico (Deflated Sharpe Ratio significativo)? -2. Le strategie sono qualitativamente diverse o cloni leggeri? -3. Le strategie sopravvivono al regime change out-of-sample? -4. Quanto del successo viene da modelli costosi vs economici (ablation multi-tier)? -5. Lo swarm batte una baseline statistica tradizionale (random forest + feature engineering)? - -**Tempo target**: 3-4 mesi a impegno significativo. -**Budget LLM target**: $2-4K. - -**Decisione post-PoC**: -- Se passa tutti i 5 test → procedere con sistema completo (documento principale) -- Se passa parzialmente → iterare sul PoC, identificare bottleneck -- Se non passa → riformulare. Forse l'edge degli LLM agents è in altri domini, non in pattern discovery numerico - ---- - -## 2. Architettura semplificata - -### 2.1 Cosa cambia rispetto al sistema completo - -| Aspetto | Sistema completo | PoC | -|------------------------|-----------------------------|------------------------------------| -| Popolazioni evolventi | 4 (3 layer + protocollo) | 1 (solo Hypothesis) | -| Hypothesis layer | Evolve via GA | Evolve via GA (K=50) | -| Falsification layer | Evolve via GA | Hand-crafted, 1 agente fisso | -| Adversarial layer | Evolve via GA | Hand-crafted, 1 agente fisso | -| Protocollo | Co-evolve | Fisso, designed manualmente | -| Domini di applicazione | Multipli | Solo trading BTC/ETH | -| Idiom emergence | Sì | No | -| Speciation | Sì | Versione semplificata (clustering base) | -| Tier multi-model | Sì (S/A/B/C/D) | Sì (semplificato a B/C principalmente) | -| Human-in-the-loop | Strutturato ogni 20 gen | Review settimanale informale | - -**Filosofia**: massimizzare apprendimento, minimizzare complessità implementativa. - -### 2.2 Schema architetturale - -``` - ┌───────────────────────────────────┐ - │ PROTOCOLLO FISSO (S-expression) │ - │ ~15 verbi designed manualmente │ - └─────────────┬─────────────────────┘ - │ - ▼ - ┌──────────────────────┐ - │ HYPOTHESIS SWARM │ ← UNICO layer che evolve - │ K=50 agenti │ - │ Tier mix: B/C │ - │ GA: tournament, │ - │ speciation base, │ - │ novelty bonus │ - └──────────┬───────────┘ - │ ipotesi formalizzate - ▼ - ┌──────────────────────┐ - │ FALSIFICATION (hand) │ ← FISSO - │ 1 agente Tier-B │ - │ Funzione: traduce │ - │ ipotesi in regole + │ - │ chiama backtest + │ - │ valuta con DSR │ - └──────────┬───────────┘ - │ strategie validate - ▼ - ┌──────────────────────┐ - │ ADVERSARIAL (hand) │ ← FISSO - │ 1 agente Tier-A │ - │ Funzione: red team │ - │ con checklist statica│ - │ (lookahead, regime, │ - │ crowding) │ - └──────────┬───────────┘ - │ strategie sopravvissute - ▼ - ┌──────────────────────┐ - │ FITNESS LOOP │ - │ Update agent_fitness│ - │ Selezione + GA │ - └──────────────────────┘ - │ - ▼ - Generazione N+1 -``` - ---- - -## 3. Le quattro trappole del backtesting su crypto - -Queste sono le killer specifiche del dominio. Vanno mitigate by design, non come afterthought. - -### 3.1 Look-ahead bias subdolo - -**Problema**: molti dati "storici" su crypto sono in realtà revisionati ex-post. -- Funding rates: spesso medie giornaliere ricalcolate, non valori real-time storici -- On-chain metrics (MVRV, NUPL, SOPR): formule che cambiano nel tempo, applicate retroattivamente -- Liste top-N token: survivorship bias massiccio -- Sentiment storico: ricostruzioni post-hoc, non disponibili in tempo reale a quei momenti - -**Mitigazione**: -- Ogni feature deve avere un campo `availability_lag`: quante ore dopo il timestamp T la feature era effettivamente disponibile -- Backtest engine rifiuta di usare feature prima del lag -- Documentazione esplicita di come/quando ogni feature è stata raccolta -- Preferire fonti che pubblicano archivi real-time (Kaiko, Tardis.dev, Amberdata) a quelle ricostruite - -### 3.2 Multiple testing su scala industriale - -**Problema**: con 10000 strategie testate, ~100 superano p<0.01 per puro caso. Senza correzione, il sistema produce sempre "vincitori" illusori. - -**Mitigazione**: -- **Deflated Sharpe Ratio (DSR)** come fitness primaria, non Sharpe naive -- Tracking del numero totale di strategie testate fino a generazione N (impatta DSR) -- Bonferroni-style correction quando si seleziona "top strategies" da reporting -- Hold-out set finale **mai toccato durante evoluzione** per validation finale - -### 3.3 Regime dependency - -**Problema**: BTC/ETH hanno regimi macro molto diversi. Una strategia che funziona 2018-2024 può essere semplicemente "long-only momentum con leva". OOS 2026 fallisce. - -**Periodi di regime distintivi**: -- 2017: bull mania retail (escluso dal dataset, troppo anomalo) -- 2018-2019: bear lungo -- 2020-2021: DeFi summer + bull istituzionale + COVID -- 2022: collapse cycle (LUNA, FTX, Celsius) -- 2023-2024: ripresa + ETF spot -- 2025-2026: post-ETF, regime nuovo - -**Mitigazione**: -- Walk-forward con **purged cross-validation** (López de Prado 2018) -- Train su finestra mobile, test su successiva, **gap di purging** in mezzo per evitare leakage -- Fitness penalizza strategie che funzionano solo su 1-2 regimi -- OOS finale obbligatoriamente su periodo diverso dal training - -### 3.4 Backtest ≠ live execution - -**Problema**: anche backtest perfetto ha gap col live. -- Slippage non lineare con size (su crypto particolarmente) -- Fees variabili (maker/taker, volume rebates) -- Funding rate sui perp può mangiarsi l'edge -- Liquidità evapora nei momenti che contano -- API outages, exchange downtime - -**Mitigazione**: -- Modello di slippage realistico (Almgren-Chriss o simile, non costante) -- Fees accurate (struttura tier per exchange) -- Funding payments simulati per posizioni perp -- Cap su size per evitare strategie che funzionano solo a $100, non a $100K - ---- - -## 4. Dataset Specification - -### 4.1 Coverage - -- **Asset**: BTC, ETH (focus iniziale) -- **Periodo**: 2018-01-01 → 2025-12-31 -- **Train/OOS split**: train 2018-2023, OOS validation 2024, OOS final hold-out 2025 -- **Frequenza base**: 1-hour bars (compromesso tra granularità e gestibilità) -- **Frequenze derivate**: 4h, 1d aggregate per features di lungo periodo - -### 4.2 Feature catalog - -**Price/Volume (sempre disponibili real-time, lag=0)**: -- OHLCV su 1h, 4h, 1d -- Returns log su orizzonti multipli -- Volatility realized (Garman-Klass, Parkinson, RV) -- Volume profile, VWAP - -**Derivatives (lag tipicamente 5-15min)**: -- Funding rates (Bybit, Binance, Hyperliquid, Deribit) -- Open Interest (per exchange e aggregato) -- Put/Call ratio (Deribit options) -- Implied volatility surface (Deribit) -- Skew, term structure -- Liquidations (volume e direzione) - -**On-chain (lag tipicamente 1-6 ore per finalization)**: -- Active addresses -- Transaction count + volume -- Exchange inflows/outflows (Glassnode-style) -- Miner flows -- Whale transactions (>$1M) -- MVRV, NUPL, SOPR (con cautela su revisionalità) - -**Macro context (lag variabile)**: -- DXY, gold, S&P 500, yield 10Y (per correlazioni) -- Crypto-specific: BTC dominance, ETH/BTC ratio, total market cap -- Stablecoin supply (USDT, USDC, DAI) - -**Sentiment (lag variabile, qualità incerta)**: -- Funding rate come proxy sentiment -- Open Interest variations come proxy speculation -- (Skip Twitter/social per ora — qualità storica troppo bassa) - -### 4.3 Data sources - -**Preferiti** (real-time archivi): -- Tardis.dev (derivatives, order book, trades — premium) -- Kaiko (institutional grade — premium) -- Amberdata (multi-source) - -**Backup gratuiti/cheap**: -- CCXT historical (OHLCV affidabile) -- Binance/Bybit/Deribit API direct (con cautela su gap) -- CoinGlass (derivatives aggregati, qualità media) -- Glassnode free tier (on-chain, limitato) - -**Da evitare**: -- Aggregatori che non documentano metodologia -- Source che hanno cambiato formula nel tempo senza versioning - -### 4.4 Storage - -```sql -CREATE TABLE features ( - timestamp TIMESTAMPTZ NOT NULL, - asset TEXT NOT NULL, - feature_name TEXT NOT NULL, - value DOUBLE PRECISION, - availability_lag_seconds INT NOT NULL, -- CRITICO - source TEXT, - version TEXT, -- per gestire cambi di metodologia - PRIMARY KEY (timestamp, asset, feature_name) -); - -CREATE INDEX idx_feat_time ON features (timestamp); -CREATE INDEX idx_feat_asset_name ON features (asset, feature_name); -``` - -Considerare TimescaleDB se le query temporali diventano colli di bottiglia. - ---- - -## 5. Backtest Engine - -### 5.1 Requisiti - -- **Determinismo**: stesso input, stesso output, sempre -- **Velocità**: target ≥100 backtest/sec su CPU normale (per gestire 50 agenti × 10 ipotesi/gen) -- **Anti-leakage by design**: rifiuta feature prima di availability_lag -- **Walk-forward integrato**: non opzionale, parte del flow base -- **Realistic execution**: slippage, fees, funding - -### 5.2 Architettura - -**Linguaggio**: Python+NumPy per il PoC. Rust+PyO3 solo se diventa bottleneck (probabile in fase scaling, non necessario per PoC). - -**API base**: -```python -class BacktestEngine: - def run(self, - strategy: StrategySpec, - features: FeatureSet, - time_range: tuple[datetime, datetime], - walk_forward_config: WFConfig) -> BacktestResult: - ... - - def run_with_dsr(self, - strategy: StrategySpec, - ...) -> tuple[BacktestResult, DSRStats]: - ... - -@dataclass -class StrategySpec: - entry_rules: list[Rule] # condizioni di ingresso - exit_rules: list[Rule] # condizioni di uscita - sizing: SizingRule # position sizing - instruments: list[str] # BTC, ETH, BTC-PERP, etc. - constraints: list[Constraint] # max leverage, max DD, etc. - -@dataclass -class BacktestResult: - pnl_curve: pd.Series - sharpe: float - sortino: float - max_drawdown: float - n_trades: int - win_rate: float - avg_holding_time: timedelta - fees_paid: float - funding_paid: float - slippage_cost: float - regime_breakdown: dict[str, float] # PnL per regime -``` - -### 5.3 Execution model - -**Slippage**: -``` -slippage = base_spread + impact_factor * (size / avg_volume_5min) ^ 0.5 -``` -Calibrato su book reale per BTC/ETH. Più alto in regimi di alta volatilità. - -**Fees**: -- Maker: 0.02% (tipico) -- Taker: 0.05% (tipico) -- Tier per volume non simulato nel PoC (assume tier base) - -**Funding** (per perp): -- Pagato/ricevuto ogni 8 ore -- Calcolato su mark price × position size × funding rate - -**Constraints automatiche**: -- Max leverage (configurabile, default 5x per il PoC) -- Margin call simulati realisticamente -- Liquidations forzate se margin scende sotto threshold - -### 5.4 Walk-forward purged CV - -``` -Time: |---------train-----------|gap|----test----|---next gap---|--next test--| -Window 1: [2018-01 ──────── 2020-06] [2020-07 ──── 2020-12] -Window 2: [2018-07 ──── 2021-06] [2021-07 ──── 2021-12] -Window 3: [2019-01 ── 2022-06] [2022-07 ── 2022-12] -... -``` - -- Training window: 30 mesi -- Gap (purging): 1 mese (evita leakage da event horizon) -- Test window: 6 mesi -- Step: 6 mesi -- Embargo: ulteriori 2 settimane post-test prima del prossimo training (López de Prado embargo) - ---- - -## 6. Hypothesis Swarm (l'unico layer che evolve) - -### 6.1 Genome design - -```python -@dataclass -class HypothesisAgentGenome: - # Cognizione - system_prompt: str - cognitive_style: str # "physicist", "biologist", "engineer", "trader_oldschool"... - - # Accesso a dati - feature_access: list[str] # subset delle feature disponibili - lookback_window_days: int # quanto storico vede - timeframes: list[str] # ["1h", "4h", "1d"] - - # Modello - model_tier: ModelTier # B o C principalmente nel PoC - temperature: float # 0.6 - 1.2 - - # Bias di output - strategy_type_preference: list[str] # ["mean_reversion", "momentum", "vol_arb", "cross_asset"] - timeframe_preference: str # "intraday", "swing", "position" - - # Tracking - parent_ids: list[UUID] - generation: int - species_id: UUID -``` - -### 6.2 Output format (nel protocollo fisso) - -``` -PROPOSE_STRATEGY( - id=#strat_47, - name="vol_skew_momentum", - - entry_conditions=[ - AND( - (deribit_skew_25d > rolling_mean(deribit_skew_25d, 30d) + 1.5*std), - (btc_funding_8h > 0.01), - (eth_funding_8h > btc_funding_8h) - ) - ], - - exit_conditions=[ - OR( - (skew normalized below mean), - (max_holding > 7d), - (drawdown > 0.03) - ) - ], - - sizing=KELLY_FRACTIONAL(fraction=0.25), - instruments=["ETH-PERP-HL"], - side="long", - - rationale="Skew elevato indica fear di puts, funding alto indica long crowding. Cross-section ETH/BTC funding suggerisce ETH outperformance attesa.", - - expected_regime="normal volatility, trending", - expected_failure_modes=["crash con depinning", "regime shift improvviso"] -) -``` - -### 6.3 Operatori genetici - -**Mutazione del system prompt**: -- LLM-as-mutator (Tier-B): "Modifica questo prompt cambiando un aspetto cognitivo, mantenendo intent" -- Probabilità: 60% per agente selezionato - -**Mutazione di feature_access**: -- Add/remove 1-3 feature random -- Probabilità: 30% - -**Mutazione di temperature**: -- Gaussiana, σ=0.1, clip [0.3, 1.4] -- Probabilità: 20% - -**Crossover**: -- 50/50 di feature_access da due parent -- Cognitive_style preso da parent migliore -- System prompt: crossover sezione-by-sezione (role/context/instructions/output_format) -- Probabilità: 50% per nuova generazione (resto è mutazione) - -**Mutazione di model_tier**: -- Rara (5%), perché impatta costo -- Solo C↔B, non promozioni a A senza fitness eccezionale - -### 6.4 Selection - -**Tournament selection**, k=5. - -**Speciation semplificata**: -- Embedding del system prompt via Voyage o OpenAI ada -- K-means con K=5-7 cluster -- Fitness sharing dentro cluster - -**Elitismo**: top 3 agenti per cluster sopravvivono non modificati. - -**Immigrazione**: ogni 10 generazioni, 5 nuovi agenti random vengono inseriti (anti-stagnazione). - ---- - -## 7. Falsification Agent (hand-crafted, fisso) - -### 7.1 Ruolo - -Prende ipotesi dal swarm, le esegue su backtest engine, riporta risultati con DSR e diagnostica. - -**NON** valuta soggettivamente. Esegue test deterministici e li interpreta. - -### 7.2 Modello - -Tier-B (Qwen Max o equivalente). Sufficiente per: -- Tradurre ipotesi in linguaggio naturale → StrategySpec strutturato -- Chiamare backtest engine -- Interpretare output numerico -- Riportare in protocollo - -### 7.3 System prompt template - -``` -Sei un agente di falsification rigoroso. Il tuo ruolo è testare ipotesi -trading senza pregiudizi e riportare risultati onesti. - -Per ogni ipotesi ricevuta: -1. Verifica che le entry/exit conditions siano formalizzabili in regole testabili -2. Verifica che le feature richieste rispettino availability_lag -3. Chiama il backtest engine con configurazione walk-forward standard -4. Calcola Deflated Sharpe Ratio considerando il numero di test fatti finora ({total_trials}) -5. Riporta breakdown per regime (bear/bull/sideways) -6. Identifica failure modes osservati nel backtest - -Output strict in protocollo S-expression. Nessuna interpretazione narrativa. -Se l'ipotesi non è formalizzabile, ritorna REJECT con motivo specifico. -``` - -### 7.4 Output format - -``` -REPORT_BACKTEST( - target=#strat_47, - - metrics=( - sharpe=1.34, - deflated_sharpe=0.87, - p_value=0.04, - max_drawdown=0.18, - n_trades=143, - avg_holding_h=42 - ), - - regime_performance=( - bear_2018=0.42, - bull_2020=2.1, - crash_2022=-0.85, - recovery_2023=1.1 - ), - - warnings=[ - "Performance dominated by 2020-2021 regime", - "DSR significant but multiple testing burden high (8400 strategies tested)" - ], - - verdict=PASS_WITH_WARNINGS // PASS / FAIL / PASS_WITH_WARNINGS -) -``` - ---- - -## 8. Adversarial Agent (hand-crafted, fisso) - -### 8.1 Ruolo - -Per ogni strategia che passa Falsification, applica una **checklist statica** di attacchi epistemici. - -Nel PoC NON evolve. Ha vocabolario fisso di attacchi noti dalla letteratura (López de Prado, Bailey, etc.). - -### 8.2 Modello - -Tier-A (Sonnet). Qui il reasoning conta — riconoscere lookahead bias subtle richiede capacità. - -### 8.3 Checklist di attacchi - -``` -1. Lookahead bias check - - Tutte le feature usate rispettano availability_lag? - - Qualche feature è "future-derived" subdolamente? - -2. Survivorship bias check - - La strategia usa universo di asset dinamico? - - Filtra solo asset sopravvissuti? - -3. Regime dependency check - - Performance concentrata in 1-2 regimi specifici? - - Cosa succede se rimuovi il regime migliore? - -4. Multiple testing severity - - DSR resta significativo dopo Bonferroni stretto? - - Confronto con random strategy baseline - -5. Crowding plausibility - - La logica è "ovvia"? Probabilmente già crowded - - Edge size ragionevole o sospettosamente alto? - -6. Implementation friction - - Slippage assumption realistica per la size? - - Funding payments inclusi correttamente? - - Trade frequency compatibile con execution reale? - -7. Feature stability - - Le feature critiche hanno stessa metodologia in tutto il periodo? - - Source provider ha cambiato formula? - -8. Statistical robustness - - Sharpe sensibile a rimozione top 5% trade? - - Performance robusta a perturbazioni piccole nei parametri? -``` - -### 8.4 Output - -``` -ADVERSARIAL_REVIEW( - target=#strat_47, - attacks_passed=[1, 2, 5, 6, 7], - attacks_failed=[3, 4, 8], - - critical_concerns=[ - CONCERN( - type=regime_dependency, - severity=high, - detail="60% of PnL from 2020-2021 bull regime. Removing it gives Sharpe 0.4." - ), - CONCERN( - type=multiple_testing, - severity=medium, - detail="DSR significant but only marginally (p=0.04). With 8400 trials, expect ~336 false positives at this level." - ) - ], - - verdict=REJECT // ACCEPT / ACCEPT_CONDITIONAL / REJECT -) -``` - ---- - -## 9. Fitness Function - -### 9.1 Agent fitness (per Hypothesis swarm) - -```python -def agent_fitness(agent: HypothesisAgentGenome, - episodes: list[Episode]) -> float: - - # Quality of contributions - accepted_strats = [e for e in episodes - if e.adversarial_verdict == "ACCEPT"] - conditional_strats = [e for e in episodes - if e.adversarial_verdict == "ACCEPT_CONDITIONAL"] - - quality_score = ( - len(accepted_strats) * 1.0 - + len(conditional_strats) * 0.3 - ) - - # Average DSR of accepted strategies - avg_dsr = np.mean([e.dsr for e in accepted_strats]) if accepted_strats else 0 - - # Cost penalty (proporzionale al tier usato) - cost_per_episode = TIER_COST[agent.model_tier] * agent.avg_tokens_per_episode - cost_penalty = cost_per_episode * COST_PENALTY_LAMBDA - - # Novelty bonus (semantica delle strategie prodotte) - novelty_score = compute_novelty(agent, all_agents_in_generation) - - # Diversity bonus (strategie diverse, non cloni) - diversity_score = compute_internal_diversity(agent.strategies) - - return ( - quality_score - + avg_dsr * DSR_WEIGHT - + novelty_score * NOVELTY_WEIGHT - + diversity_score * DIVERSITY_WEIGHT - - cost_penalty - ) -``` - -### 9.2 Pesi iniziali (da calibrare empiricamente) - -```python -DSR_WEIGHT = 2.0 # peso forte: vogliamo edge reale -NOVELTY_WEIGHT = 0.5 # bonus moderato per esplorazione -DIVERSITY_WEIGHT = 0.3 # leggero, evita cloni -COST_PENALTY_LAMBDA = 0.1 # da calibrare in base a budget -``` - -### 9.3 Anti-gaming - -- Tracking globale del numero di test fatti per DSR correction -- Cap su `quality_score` per evitare gaming via spam di ipotesi banali -- Adversarial feedback va in fitness (strategie REJECTED penalizzano, non solo non bonificano) -- Periodic audit umano: ogni 10 generazioni rivedere top 5 strategies, marcare "spurious" se gaming - ---- - -## 10. Baseline non-LLM (anti-illusion check) - -**Critico**: il PoC deve includere una baseline statistica tradizionale. - -### 10.1 Specifica baseline - -- **Metodo**: Random Forest + feature engineering manuale -- **Features**: stesso pool del swarm -- **Target**: returns N-bar future (predizione regression o classification) -- **Validation**: stesso walk-forward purged CV -- **Output**: trade signals → backtest engine identico - -### 10.2 Confronto - -| Metrica | Baseline RF | Swarm LLM | -|----------------------------|-------------|-----------| -| Best DSR found | ? | ? | -| Avg DSR top-10 strategies | ? | ? | -| N strategies passing adv. | ? | ? | -| Diversity (semantic) | ? | ? | -| Total cost | ~$50 | ~$3K | - -**Interpretazione**: -- Se Swarm batte significativamente Baseline → architettura LLM sta aggiungendo valore -- Se Swarm pareggia → costo non giustificato per discovery numerica pura -- Se Swarm perde → l'edge degli LLM agents è altrove (creatività su task non numerici, integrazione semi-strutturata) - -**Punto importante**: anche se Swarm perde su pattern discovery numerico, NON significa che il sistema co-evolutivo completo è inutile. Significa che la value-add è in altri domini (ipotesi macro, integrazione narrative, generalizzazione cross-domain). È informazione preziosa. - ---- - -## 11. Roadmap Implementativa (3-4 mesi) - -### Settimane 1-3: Setup & Dataset - -- Setup repo, environment, dependencies -- Data sourcing: identificare provider, sottoscrivere se necessario -- Data ingestion: pipeline dati storici BTC/ETH 2018-2025 -- Storage schema (PostgreSQL/TimescaleDB) -- Audit di availability_lag per ogni feature -- Sanity checks: confronto cross-source per validare integrity - -**Deliverable**: dataset completo annotato, query-able, con feature catalog. - -### Settimane 4-7: Backtest Engine - -- Implementazione walk-forward purged CV -- Slippage model (Almgren-Chriss) -- Fees + funding -- DSR computation -- Test di non-leakage (deliberately inject lookahead → engine deve catcharlo) -- Performance optimization (target ≥100 backtest/sec) - -**Deliverable**: backtest engine standalone testato. Strategie semplici (buy & hold, 50/200 SMA crossover) producono risultati noti. - -### Settimane 8-9: Agenti hand-crafted - -- Falsification agent (Tier-B) -- Adversarial agent (Tier-A) -- Protocol fisso (S-expression parser + 15 verbi) -- Message bus auditato semplificato -- Test end-to-end con ipotesi hand-crafted - -**Deliverable**: pipeline agente → falsification → adversarial → verdict, funzionante end-to-end con strategie inserite manualmente. - -### Settimane 10-12: Hypothesis Swarm + GA - -- Population manager (50 agenti) -- Operatori genetici (mutazione + crossover) -- Speciation con clustering embedding -- Fitness computation -- Tournament selection -- Evoluzione per 50 generazioni baseline - -**Deliverable**: swarm che evolve, fitness che migliora over generations, output di top strategies. - -### Settimane 13-14: Baseline + Comparison - -- Implementazione baseline Random Forest -- Run baseline su stesso dataset/timeframe -- Tabella comparison -- Analisi qualitative delle strategie prodotte da entrambi - -**Deliverable**: documento di confronto onesto. - -### Settimane 15-16: Analisi & Report - -- Run finale su hold-out set 2025 -- Analysis: ablation multi-tier, regime breakdown, diversity analysis -- Documentazione lessons learned -- Decision document: procedere con sistema completo, iterare PoC, o pivot - -**Deliverable**: report finale con risposte alle 5 domande di validazione iniziali. - ---- - -## 12. Costi PoC - -### 12.1 LLM costs - -| Fase | Calls stimate | Tokens stimati | Tier mix | Costo | -|-------------------------|---------------|-----------------|--------------|--------| -| Setup + agenti hand | ~500 | 2M | A/B | ~$50 | -| Test pipeline | ~2000 | 8M | B/C | ~$30 | -| GA run principale | 50 gen × 50 agent × 10 calls × 5K tok = 125M tok | mostly C, some B | ~$300-500 | -| Repeat runs (3x) | (per ablation, tuning) | | | ~$1000-1500 | -| Hold-out validation | ~1000 | 5M | A (judge) | ~$80 | -| **Totale** | | | | **$1500-2500** | - -Più conservativo se aggiungiamo iteration overhead: **budget $2-4K**. - -### 12.2 Infrastruttura - -- Compute (locale o cloud modesto): ~$200-400 totali -- Storage (data + logs): ~$50/mese -- Data subscription (Tardis o Kaiko per derivatives): variabile, $0-500/mese a seconda della qualità richiesta - -### 12.3 Tempo - -- Realistic estimate: **3-4 mesi** a 3-4 giorni/settimana effettivi -- Pessimistic estimate: 5-6 mesi se data sourcing risulta più complesso del previsto - ---- - -## 13. Rischi e Mitigazioni - -| Rischio | Probabilità | Impatto | Mitigazione | -|------------------------------------|-------------|---------|-----------------------------------------| -| Data sourcing più complesso | Alta | Medio | Start con CCXT + Binance free, upgrade dopo | -| Backtest engine ha bug subtle | Alta | Critico | Test deliberato di lookahead injection | -| Swarm non batte baseline RF | Media | Alto | È un risultato comunque, non fallimento | -| Costi over-budget | Media | Medio | Cap hard, monitor settimanale | -| Time over-budget (5+ mesi) | Alta | Medio | Milestone bi-settimanali, taglio scope se necessario | -| Convergenza prematura del swarm | Media | Alto | Speciation, novelty bonus, immigrazione | -| OpenRouter qualità inconsistente | Media | Basso | Fallback policy, monitor success rate | - ---- - -## 14. Decisioni Aperte - -Da risolvere prima di Fase 1: - -1. **Asset focus**: solo BTC, solo ETH, o entrambi? (suggerito: entrambi, per cross-asset features) -2. **Data provider primario**: pagare Tardis/Kaiko o partire con free sources? (suggerito: free per Fase 1, upgrade se necessario) -3. **Hardware**: locale o cloud? (suggerito: locale, dataset ~50-100GB gestibile) -4. **Tier-A budget**: quanto reservare a Sonnet per Adversarial? (suggerito: $300-500 cap) -5. **Frequency primaria**: 1h confermato o scendere a 15min? (suggerito: 1h, evita overfitting su microstructure) - ---- - -## 15. Cosa il PoC NON fa (e va bene) - -- NON evolve il protocollo (sistema completo) -- NON co-evolve Falsification e Adversarial (sistema completo) -- NON esplora idiom emergence (sistema completo) -- NON applica a domini non-trading (sistema completo) -- NON cerca breakthrough singoli, cerca **validazione architetturale** -- NON è un trading bot pronto per capitale reale (mai dopo PoC, paper trading prima) - -Il PoC è un **esperimento controllato**. Il sistema completo è il prodotto. Sono cose diverse, e va bene così. - ---- - -## 16. Decision Triggers - -Dopo PoC, queste sono le decisioni discrete: - -**GO al sistema completo se**: -- Swarm produce ≥5 strategie con DSR significativo dopo Adversarial -- Swarm batte baseline RF di ≥30% in best-DSR e diversity -- Strategie sopravvivono a OOS 2025 (regime change) -- Multi-tier ablation conferma valore (no significant loss usando 70% Tier-C) - -**ITERATE PoC se**: -- Risultati borderline (alcuni indicatori sì, altri no) -- Bug specifici identificabili (es. GA non diversifica → fix speciation) -- Architettura sembra giusta ma implementazione perfettibile - -**PIVOT se**: -- Swarm decisamente perde vs baseline su discovery numerica -- Ma: considerare pivot verso domini non-numerici (offerte commerciali, code review) -- O: considerare uso del swarm come *generator* di ipotesi macro/strutturali, non pattern numerici - ---- - -*Documento da aggiornare durante PoC. Questa è v0.1, scritta prima di qualunque implementazione.* -*Documento principale (sistema completo): `coevolutive_swarm_system.md`* diff --git a/docs/decisions/2026-05-10-gate-phase1.md b/src/multi_swarm_core/docs/decisions/2026-05-10-gate-phase1.md similarity index 100% rename from docs/decisions/2026-05-10-gate-phase1.md rename to src/multi_swarm_core/docs/decisions/2026-05-10-gate-phase1.md diff --git a/docs/decisions/2026-05-11-phase1-5-nemotron-run.md b/src/multi_swarm_core/docs/decisions/2026-05-11-phase1-5-nemotron-run.md similarity index 100% rename from docs/decisions/2026-05-11-phase1-5-nemotron-run.md rename to src/multi_swarm_core/docs/decisions/2026-05-11-phase1-5-nemotron-run.md diff --git a/00_documento_zero.md b/src/multi_swarm_core/docs/design/00_documento_zero.md similarity index 100% rename from 00_documento_zero.md rename to src/multi_swarm_core/docs/design/00_documento_zero.md diff --git a/coevolutive_swarm_system.md b/src/multi_swarm_core/docs/design/coevolutive_swarm_system.md similarity index 100% rename from coevolutive_swarm_system.md rename to src/multi_swarm_core/docs/design/coevolutive_swarm_system.md diff --git a/docs/reports/2026-05-14-stato-progetto-e-roadmap.md b/src/multi_swarm_core/docs/reports/2026-05-14-stato-progetto-e-roadmap.md similarity index 100% rename from docs/reports/2026-05-14-stato-progetto-e-roadmap.md rename to src/multi_swarm_core/docs/reports/2026-05-14-stato-progetto-e-roadmap.md diff --git a/tests/__init__.py b/src/multi_swarm_core/tests/__init__.py similarity index 100% rename from tests/__init__.py rename to src/multi_swarm_core/tests/__init__.py diff --git a/tests/integration/__init__.py b/src/multi_swarm_core/tests/integration/__init__.py similarity index 100% rename from tests/integration/__init__.py rename to src/multi_swarm_core/tests/integration/__init__.py diff --git a/tests/integration/test_e2e_minimal_run.py b/src/multi_swarm_core/tests/integration/test_e2e_minimal_run.py similarity index 100% rename from tests/integration/test_e2e_minimal_run.py rename to src/multi_swarm_core/tests/integration/test_e2e_minimal_run.py diff --git a/tests/integration/test_ga_loop_with_prompt_mutator.py b/src/multi_swarm_core/tests/integration/test_ga_loop_with_prompt_mutator.py similarity index 100% rename from tests/integration/test_ga_loop_with_prompt_mutator.py rename to src/multi_swarm_core/tests/integration/test_ga_loop_with_prompt_mutator.py diff --git a/tests/unit/__init__.py b/src/multi_swarm_core/tests/unit/__init__.py similarity index 100% rename from tests/unit/__init__.py rename to src/multi_swarm_core/tests/unit/__init__.py diff --git a/tests/unit/test_adversarial.py b/src/multi_swarm_core/tests/unit/test_adversarial.py similarity index 100% rename from tests/unit/test_adversarial.py rename to src/multi_swarm_core/tests/unit/test_adversarial.py diff --git a/tests/unit/test_backtest_engine.py b/src/multi_swarm_core/tests/unit/test_backtest_engine.py similarity index 100% rename from tests/unit/test_backtest_engine.py rename to src/multi_swarm_core/tests/unit/test_backtest_engine.py diff --git a/tests/unit/test_backtest_orders.py b/src/multi_swarm_core/tests/unit/test_backtest_orders.py similarity index 100% rename from tests/unit/test_backtest_orders.py rename to src/multi_swarm_core/tests/unit/test_backtest_orders.py diff --git a/tests/unit/test_cerbero_client.py b/src/multi_swarm_core/tests/unit/test_cerbero_client.py similarity index 100% rename from tests/unit/test_cerbero_client.py rename to src/multi_swarm_core/tests/unit/test_cerbero_client.py diff --git a/tests/unit/test_cerbero_ohlcv.py b/src/multi_swarm_core/tests/unit/test_cerbero_ohlcv.py similarity index 100% rename from tests/unit/test_cerbero_ohlcv.py rename to src/multi_swarm_core/tests/unit/test_cerbero_ohlcv.py diff --git a/tests/unit/test_cerbero_tools.py b/src/multi_swarm_core/tests/unit/test_cerbero_tools.py similarity index 100% rename from tests/unit/test_cerbero_tools.py rename to src/multi_swarm_core/tests/unit/test_cerbero_tools.py diff --git a/tests/unit/test_config.py b/src/multi_swarm_core/tests/unit/test_config.py similarity index 100% rename from tests/unit/test_config.py rename to src/multi_swarm_core/tests/unit/test_config.py diff --git a/tests/unit/test_cost_tracker.py b/src/multi_swarm_core/tests/unit/test_cost_tracker.py similarity index 100% rename from tests/unit/test_cost_tracker.py rename to src/multi_swarm_core/tests/unit/test_cost_tracker.py diff --git a/tests/unit/test_diversity.py b/src/multi_swarm_core/tests/unit/test_diversity.py similarity index 100% rename from tests/unit/test_diversity.py rename to src/multi_swarm_core/tests/unit/test_diversity.py diff --git a/tests/unit/test_falsification.py b/src/multi_swarm_core/tests/unit/test_falsification.py similarity index 100% rename from tests/unit/test_falsification.py rename to src/multi_swarm_core/tests/unit/test_falsification.py diff --git a/tests/unit/test_fitness.py b/src/multi_swarm_core/tests/unit/test_fitness.py similarity index 100% rename from tests/unit/test_fitness.py rename to src/multi_swarm_core/tests/unit/test_fitness.py diff --git a/tests/unit/test_ga_initial.py b/src/multi_swarm_core/tests/unit/test_ga_initial.py similarity index 100% rename from tests/unit/test_ga_initial.py rename to src/multi_swarm_core/tests/unit/test_ga_initial.py diff --git a/tests/unit/test_ga_loop.py b/src/multi_swarm_core/tests/unit/test_ga_loop.py similarity index 100% rename from tests/unit/test_ga_loop.py rename to src/multi_swarm_core/tests/unit/test_ga_loop.py diff --git a/tests/unit/test_ga_summary.py b/src/multi_swarm_core/tests/unit/test_ga_summary.py similarity index 100% rename from tests/unit/test_ga_summary.py rename to src/multi_swarm_core/tests/unit/test_ga_summary.py diff --git a/tests/unit/test_genome_crossover.py b/src/multi_swarm_core/tests/unit/test_genome_crossover.py similarity index 100% rename from tests/unit/test_genome_crossover.py rename to src/multi_swarm_core/tests/unit/test_genome_crossover.py diff --git a/tests/unit/test_genome_hypothesis.py b/src/multi_swarm_core/tests/unit/test_genome_hypothesis.py similarity index 100% rename from tests/unit/test_genome_hypothesis.py rename to src/multi_swarm_core/tests/unit/test_genome_hypothesis.py diff --git a/tests/unit/test_genome_mutation.py b/src/multi_swarm_core/tests/unit/test_genome_mutation.py similarity index 100% rename from tests/unit/test_genome_mutation.py rename to src/multi_swarm_core/tests/unit/test_genome_mutation.py diff --git a/tests/unit/test_hypothesis_agent.py b/src/multi_swarm_core/tests/unit/test_hypothesis_agent.py similarity index 100% rename from tests/unit/test_hypothesis_agent.py rename to src/multi_swarm_core/tests/unit/test_hypothesis_agent.py diff --git a/tests/unit/test_llm_client.py b/src/multi_swarm_core/tests/unit/test_llm_client.py similarity index 100% rename from tests/unit/test_llm_client.py rename to src/multi_swarm_core/tests/unit/test_llm_client.py diff --git a/tests/unit/test_market_summary.py b/src/multi_swarm_core/tests/unit/test_market_summary.py similarity index 100% rename from tests/unit/test_market_summary.py rename to src/multi_swarm_core/tests/unit/test_market_summary.py diff --git a/tests/unit/test_metrics_basic.py b/src/multi_swarm_core/tests/unit/test_metrics_basic.py similarity index 100% rename from tests/unit/test_metrics_basic.py rename to src/multi_swarm_core/tests/unit/test_metrics_basic.py diff --git a/tests/unit/test_metrics_dsr.py b/src/multi_swarm_core/tests/unit/test_metrics_dsr.py similarity index 100% rename from tests/unit/test_metrics_dsr.py rename to src/multi_swarm_core/tests/unit/test_metrics_dsr.py diff --git a/tests/unit/test_mutation_dispatcher.py b/src/multi_swarm_core/tests/unit/test_mutation_dispatcher.py similarity index 100% rename from tests/unit/test_mutation_dispatcher.py rename to src/multi_swarm_core/tests/unit/test_mutation_dispatcher.py diff --git a/tests/unit/test_mutation_prompt_llm.py b/src/multi_swarm_core/tests/unit/test_mutation_prompt_llm.py similarity index 100% rename from tests/unit/test_mutation_prompt_llm.py rename to src/multi_swarm_core/tests/unit/test_mutation_prompt_llm.py diff --git a/tests/unit/test_protocol_compiler.py b/src/multi_swarm_core/tests/unit/test_protocol_compiler.py similarity index 100% rename from tests/unit/test_protocol_compiler.py rename to src/multi_swarm_core/tests/unit/test_protocol_compiler.py diff --git a/tests/unit/test_protocol_parser.py b/src/multi_swarm_core/tests/unit/test_protocol_parser.py similarity index 100% rename from tests/unit/test_protocol_parser.py rename to src/multi_swarm_core/tests/unit/test_protocol_parser.py diff --git a/tests/unit/test_protocol_validator.py b/src/multi_swarm_core/tests/unit/test_protocol_validator.py similarity index 100% rename from tests/unit/test_protocol_validator.py rename to src/multi_swarm_core/tests/unit/test_protocol_validator.py diff --git a/tests/unit/test_repository.py b/src/multi_swarm_core/tests/unit/test_repository.py similarity index 100% rename from tests/unit/test_repository.py rename to src/multi_swarm_core/tests/unit/test_repository.py diff --git a/tests/unit/test_selection.py b/src/multi_swarm_core/tests/unit/test_selection.py similarity index 100% rename from tests/unit/test_selection.py rename to src/multi_swarm_core/tests/unit/test_selection.py diff --git a/tests/unit/test_splits.py b/src/multi_swarm_core/tests/unit/test_splits.py similarity index 100% rename from tests/unit/test_splits.py rename to src/multi_swarm_core/tests/unit/test_splits.py diff --git a/src/strategy_crypto/README.md b/src/strategy_crypto/README.md new file mode 100644 index 0000000..bd13def --- /dev/null +++ b/src/strategy_crypto/README.md @@ -0,0 +1,65 @@ +# strategy_crypto + +Strategia di test su asset crypto (BTC/ETH perpetual) basata sul core +`multi_swarm_core`. Workspace member del monorepo `multi_swarm_coevolutive`. + +## Scope + +Esegue **paper-trading forward-test** (Phase 3) di strategie JSON freezate, +prodotte dal pipeline evolutivo del core. Espone una dashboard NiceGUI +read-only per il monitoraggio in tempo reale. + +## Layout + +``` +strategy_crypto/ +├── backend/ paper trading runner (PaperExecutor, Portfolio, PaperRepository) +│ └── schema.py tabelle paper_trading_* (DB locale) +├── frontend/ NiceGUI dashboard (dual-DB reader: GA + paper) +└── strategies/ JSON freezate input al runner + (btc_*.json, eth_*.json) +``` + +## Run paper-trading + +```bash +uv run python scripts/run_paper_trading.py \ + --name phase3-papertrade-001 \ + --initial-capital 1000 \ + --poll-seconds 300 +``` + +Il default `--strategies-dir` punta ai JSON shippati col package via +`importlib.resources.files("strategy_crypto") / "strategies"`. + +## Dashboard + +```bash +uv run python -m strategy_crypto.frontend.nicegui_app +``` + +In produzione: `https://swarm.tielogic.xyz/strategy_crypto_gui/` (root_path +configurato via `DASHBOARD_ROOT_PATH=/strategy_crypto_gui`). + +## DB schema + +Schema isolato dal core in `state/strategy_crypto.db` (env +`STRATEGY_CRYPTO_DB_PATH`). Tabelle: + +- `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_crypto.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. + +## Pattern per future strategie + +`strategy_/` mantiene la stessa shape: `backend/`, `frontend/`, +`strategies/`, `tests/`, `docs/` (opzionale). DB dedicato `state/strategy_.db`. +Servizi Docker dedicati `strategy--paper` + `strategy--gui`, +GUI su `/strategy__gui`. diff --git a/tests/unit/paper_trading/__init__.py b/tests/unit/paper_trading/__init__.py deleted file mode 100644 index e69de29..0000000 From 8caa526727d3c6e0b68cacdb5027cb6be46a90a3 Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Fri, 15 May 2026 18:01:30 +0000 Subject: [PATCH 09/11] test(strategy_crypto): smoke regression + import-mode importlib per workspace MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit NEW src/strategy_crypto/tests/test_imports.py: - test_backend_imports — verifica re-export PaperExecutor/Portfolio/PaperRepository + schema - test_frontend_imports — strategy_crypto.frontend.{data,nicegui_app} importabili - test_strategies_json_loadable — i JSON sono in importlib.resources e ben formati - test_init_schema_creates_tables — PaperRepository.init_schema() crea 5 tabelle Fix pytest collection: add --import-mode=importlib agli addopts per evitare collisione dei due tests/__init__.py (core + strategy_crypto) sotto stesso nome module 'tests'. Verifica: uv run pytest → 186 passed (182 core + 4 strategy_crypto) Co-Authored-By: Claude Opus 4.7 (1M context) --- pyproject.toml | 2 +- src/strategy_crypto/tests/test_imports.py | 67 +++++++++++++++++++++++ 2 files changed, 68 insertions(+), 1 deletion(-) create mode 100644 src/strategy_crypto/tests/test_imports.py diff --git a/pyproject.toml b/pyproject.toml index a7cc6a9..756c244 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,7 +38,7 @@ strict = true [tool.pytest.ini_options] testpaths = ["src/multi_swarm_core/tests", "src/strategy_crypto/tests"] -addopts = "-v --tb=short" +addopts = "-v --tb=short --import-mode=importlib" markers = [ "integration: tests that require external services (Cerbero, LLM API)", "slow: tests that take more than 5 seconds", diff --git a/src/strategy_crypto/tests/test_imports.py b/src/strategy_crypto/tests/test_imports.py new file mode 100644 index 0000000..c606949 --- /dev/null +++ b/src/strategy_crypto/tests/test_imports.py @@ -0,0 +1,67 @@ +"""Smoke regression: il package strategy_crypto importa senza errori +e i JSON freezate sono accessibili via importlib.resources. + +Sostituisce la responsabilita' di un test di integrazione: serve solo +a catturare regressioni di import/packaging dopo refactor cross-modulo. +""" + +from __future__ import annotations + +import importlib.resources +import json + + +def test_backend_imports() -> None: + from strategy_crypto.backend import ( + PaperExecutor, + PaperRepository, + Portfolio, + ) + from strategy_crypto.backend.schema import PAPER_SCHEMA_SQL, init_schema + + assert PaperExecutor.__name__ == "PaperExecutor" + assert PaperRepository.__name__ == "PaperRepository" + assert Portfolio.__name__ == "Portfolio" + assert "CREATE TABLE" in PAPER_SCHEMA_SQL + assert callable(init_schema) + + +def test_frontend_imports() -> None: + import strategy_crypto.frontend.data # noqa: F401 + import strategy_crypto.frontend.nicegui_app # noqa: F401 + + +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 + for fname in found: + data = json.loads((files / fname).read_text()) + assert "rules" in data, f"{fname} missing 'rules' key" + assert isinstance(data["rules"], list) + + +def test_init_schema_creates_tables(tmp_path) -> None: + import sqlite3 + + from strategy_crypto.backend.schema import init_schema + + db = tmp_path / "paper.db" + init_schema(db) + conn = sqlite3.connect(str(db)) + try: + rows = conn.execute( + "SELECT name FROM sqlite_master WHERE type='table' AND name LIKE 'paper_%'" + ).fetchall() + names = {r[0] for r in rows} + finally: + conn.close() + expected = { + "paper_trading_runs", + "paper_trading_positions", + "paper_trading_trades", + "paper_trading_equity", + "paper_trading_ticks", + } + assert expected.issubset(names), f"missing tables: {expected - names}" From 30add359069505b017ff5b36cf5b8410b4c745c6 Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Fri, 15 May 2026 18:02:21 +0000 Subject: [PATCH 10/11] refactor(compose): rinomina servizi + Traefik subpath + dual-DB + bind strategies dal package MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rinomina servizi (era multi-swarm-paper / multi-swarm-dashboard): - strategy-crypto-paper — paper runner (container + service + image name) - strategy-crypto-gui — NiceGUI dashboard Routing Traefik: - Rule: Host(swarm.\${DOMAIN_NAME}) && PathPrefix(/strategy_crypto_gui) - NESSUN StripPrefix middleware: NiceGUI gestisce root_path internamente - URL prod: https://swarm.tielogic.xyz/strategy_crypto_gui/ - Root del dominio libera per future GUI di altre strategie Env block x-swarm-env aggiornato: - DB_PATH=/app/state/runs.db rimosso - GA_DB_PATH=/app/state/runs.db (universale GA) - STRATEGY_CRYPTO_DB_PATH=/app/state/strategy_crypto.db (paper crypto) - DASHBOARD_ROOT_PATH=/strategy_crypto_gui (passato a ui.run(root_path=...)) Volumi: - bind strategies dal package: ./src/strategy_crypto/strategy_crypto/strategies:/app/strategies:ro - image rinominata: multi-swarm:dev → multi-swarm-coevolutive:dev (allineato a wheel) Entrypoint dashboard: python -m strategy_crypto.frontend.nicegui_app (era multi_swarm.dashboard.nicegui_app) Verifica: docker compose config parse OK; 2 servizi presenti. NOTA OPERATIVA: in produzione aggiornare il file .env reale (non in repo) per allinearsi a GA_DB_PATH/STRATEGY_CRYPTO_DB_PATH/DASHBOARD_ROOT_PATH. Backcompat: DB_PATH legacy ancora letto come alias di GA_DB_PATH. Co-Authored-By: Claude Opus 4.7 (1M context) --- docker-compose.yml | 58 +++++++++++++++++++++++++--------------------- 1 file changed, 32 insertions(+), 26 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 7273350..7008000 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,25 +1,27 @@ # docker-compose.yml — Multi-Swarm Coevolutive # -# Due servizi che condividono la stessa immagine `multi-swarm:dev`: +# Due servizi della strategia crypto, condividono la stessa immagine +# `multi-swarm-coevolutive:dev` buildata dal Dockerfile root (uv workspace): # -# * multi-swarm-paper — paper-trading runner long-running -# (scripts/run_paper_trading.py) -# * multi-swarm-dashboard — Streamlit dashboard esposta da Traefik -# su https://swarm.${DOMAIN_NAME:-tielogic.xyz} +# * strategy-crypto-paper — paper-trading runner long-running +# (scripts/run_paper_trading.py) +# * strategy-crypto-gui — NiceGUI dashboard esposta da Traefik su +# https://swarm.${DOMAIN_NAME:-tielogic.xyz}/strategy_crypto_gui # # Entrambi joinano la rete external `traefik` cosi' il client Cerbero # risolve direttamente l'host `cerbero-mcp` (porta 9000) senza passare # dal gateway pubblico ne' dal TLS. # +# Pattern N strategie future: aggiungere strategy--paper + strategy--gui +# con PathPrefix(/strategy__gui) e DASHBOARD_ROOT_PATH dedicato. +# # Dati persistenti via bind mount dalla cartella del repo: -# ./data cache OHLCV intermedia -# ./series cache parquet per timeframe/symbol -# ./state contiene runs.db (+ WAL/SHM) -# ./strategies btc_*.json / eth_*.json letti dal paper runner +# ./data cache OHLCV intermedia +# ./series cache parquet per timeframe/symbol +# ./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. -# Le variabili sotto `environment:` sovrascrivono solo i valori che -# devono cambiare dentro il container (URL interno, path container). networks: traefik: @@ -31,15 +33,19 @@ x-swarm-env: &swarm-env # Override: path container per persistenza DATA_DIR: /app/data SERIES_DIR: /app/series - DB_PATH: /app/state/runs.db + # DB separati per dominio: + GA_DB_PATH: /app/state/runs.db + STRATEGY_CRYPTO_DB_PATH: /app/state/strategy_crypto.db + # Subpath sotto cui la dashboard NiceGUI e' esposta da Traefik + DASHBOARD_ROOT_PATH: /strategy_crypto_gui services: - multi-swarm-paper: + strategy-crypto-paper: build: context: . dockerfile: Dockerfile - image: multi-swarm:dev - container_name: multi-swarm-paper + image: multi-swarm-coevolutive:dev + container_name: strategy-crypto-paper restart: unless-stopped networks: [traefik] env_file: .env @@ -49,7 +55,7 @@ services: - ./data:/app/data - ./series:/app/series - ./state:/app/state - - ./strategies:/app/strategies:ro + - ./src/strategy_crypto/strategy_crypto/strategies:/app/strategies:ro # Niente HTTP da controllare: ci affidiamo a `restart: unless-stopped` # e ai log per la liveness del runner. command: @@ -64,26 +70,26 @@ services: labels: - com.centurylinklabs.watchtower.enable=true - multi-swarm-dashboard: - image: multi-swarm:dev + strategy-crypto-gui: + image: multi-swarm-coevolutive:dev build: context: . dockerfile: Dockerfile - container_name: multi-swarm-dashboard + container_name: strategy-crypto-gui restart: unless-stopped networks: [traefik] env_file: .env environment: <<: *swarm-env volumes: - # Dashboard legge solo runs.db: mount in read-only + # Dashboard legge entrambi i DB: state/ in read-only (WAL: vedi nota) - ./state:/app/state:ro - ./data:/app/data:ro - ./series:/app/series:ro entrypoint: - python - -m - - multi_swarm.dashboard.nicegui_app + - strategy_crypto.frontend.nicegui_app command: [] healthcheck: test: @@ -98,9 +104,9 @@ services: labels: - traefik.enable=true - traefik.docker.network=traefik - - "traefik.http.routers.multi-swarm-dashboard.rule=Host(`swarm.${DOMAIN_NAME:-tielogic.xyz}`)" - - traefik.http.routers.multi-swarm-dashboard.tls=true - - traefik.http.routers.multi-swarm-dashboard.entrypoints=websecure - - traefik.http.routers.multi-swarm-dashboard.tls.certresolver=mytlschallenge - - "traefik.http.services.multi-swarm-dashboard.loadbalancer.server.port=${SWARM_DASHBOARD_PORT:-8080}" + - "traefik.http.routers.strategy-crypto-gui.rule=Host(`swarm.${DOMAIN_NAME:-tielogic.xyz}`) && PathPrefix(`/strategy_crypto_gui`)" + - traefik.http.routers.strategy-crypto-gui.tls=true + - traefik.http.routers.strategy-crypto-gui.entrypoints=websecure + - traefik.http.routers.strategy-crypto-gui.tls.certresolver=mytlschallenge + - "traefik.http.services.strategy-crypto-gui.loadbalancer.server.port=${SWARM_DASHBOARD_PORT:-8080}" - com.centurylinklabs.watchtower.enable=true From 96bbd716ec4a6338c60e65f01c21e02b3f0a3bd4 Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Fri, 15 May 2026 18:03:31 +0000 Subject: [PATCH 11/11] docs(readme): allinea a monorepo unificato (workspace uv + strategy_crypto subpath) - Sezione "Layout monorepo (uv workspace)": ridisegna la struttura per chiarire i due workspace member (multi-swarm-core + strategy-crypto), DB separati, pattern per N strategie future. - Comandi aggiornati: paper runner ora importa da strategy_crypto.backend, dashboard via 'python -m strategy_crypto.frontend.nicegui_app'. - Backtest cmd punta al nuovo path strategie src/strategy_crypto/strategy_crypto/strategies/. - Variabili .env: GA_DB_PATH + STRATEGY_CRYPTO_DB_PATH + DASHBOARD_ROOT_PATH. Mantenuto nota backcompat su DB_PATH legacy. - Dashboard: nuova pagina /paper + URL prod /strategy_crypto_gui/. - Deploy: servizi rinominati strategy-crypto-paper / strategy-crypto-gui, bind strategies dal package, image rinominata multi-swarm-coevolutive:dev. - Rimossi link a doc cancellati (poc_trading_swarm.md, superpowers/, phase1-technical-report). - Doc rimanenti riposizionati sotto src/multi_swarm_core/docs/. Co-Authored-By: Claude Opus 4.7 (1M context) --- README.md | 211 ++++++++++++++++++++---------------------------------- 1 file changed, 77 insertions(+), 134 deletions(-) diff --git a/README.md b/README.md index 3622211..18bcc45 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Multi_Swarm_Coevolutive -Proof-of-concept di sistema co-evolutivo multi-agente per trading quantitativo. Un genetic algorithm fa evolvere una popolazione di agenti LLM (Hypothesis swarm) che generano strategie di trading espresse in JSON strutturato; un layer Falsification deterministico le backtesta su dati storici (default BTC-PERPETUAL Deribit) via Cerbero MCP; un layer Adversarial euristico le sottopone a red-team checks; la fitness combina Deflated Sharpe Ratio (Bailey & López 2014), Sharpe normalizzato e penalizzazione di drawdown, con opzioni v2 soft-kill e combined IS/OOS per Walk-Forward Validation. Il tutto è ispirato alla filosofia di Renaissance Technologies adattata a un contesto retail single-author con LLM agents. +Proof-of-concept di sistema co-evolutivo multi-agente per trading quantitativo. Un genetic algorithm fa evolvere una popolazione di agenti LLM (Hypothesis swarm) che generano strategie di trading espresse in JSON strutturato; un layer Falsification deterministico le backtesta su dati storici (default BTC-PERPETUAL Deribit) via Cerbero MCP; un layer Adversarial euristico le sottopone a red-team checks; la fitness combina Deflated Sharpe Ratio (Bailey & López 2014), Sharpe normalizzato e penalizzazione di drawdown, con opzioni v2 soft-kill e combined IS/OOS per Walk-Forward Validation. ## Repository @@ -10,99 +10,62 @@ Gitea Tielogic (privato, accesso SSH): 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: + +``` +multi_swarm_coevolutive/ repo root (workspace coordinator) +├── pyproject.toml workspace + dev deps + ruff/mypy/pytest +├── docker-compose.yml strategy-crypto-paper + strategy-crypto-gui +├── 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, ... + │ ├── multi_swarm_core/ GA + genome + protocol + backtest + cerbero + + │ │ data + llm + agents + ga + orchestrator + + │ │ metrics + persistence + config + │ ├── tests/ unit + integration (182 test) + │ └── docs/ design/ + decisions/ + reports/ + │ + └── strategy_crypto/ WORKSPACE MEMBER (wheel: strategy-crypto) + ├── pyproject.toml deps: multi-swarm-core (workspace) + nicegui + plotly + ├── 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) +``` + +**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_/` con lo stesso scheletro (`backend/`, `frontend/`, `strategies/`, `tests/`), DB dedicato `state/strategy_.db`, servizi Docker `strategy--paper` + `strategy--gui`, GUI su `/strategy__gui`. + ## Stato del progetto -**Phase 3 (paper-trading forward-test) in corso** dal 13 maggio 2026 su VPS. Runner `scripts/run_paper_trading.py` live 24/7 in Docker (`https://swarm.tielogic.xyz` per la dashboard) con 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: -- `strategies/btc_fb63e851.json` — BTC-PERPETUAL, true alpha hour-gated (RSI estremi + ATR vs SMA + filtro orario 9-17), Sharpe OOS +0,26 su 7,33 anni di storia. -- `strategies/eth_facd6af85d5d.json` — ETH-PERPETUAL, trend-following long-bias + vol regime, Sharpe OOS +0,19 su 6,75 anni. +- `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). -Phase 1 → 2.7 tutte chiuse (30 run GA, $3.74 cumulato LLM, cap originale $700 → margine 99%+). Vedi il documento di sintesi consolidato per il dettaglio: +Phase 1 → 2.7 chiuse (30 run GA, $3.74 cumulato LLM). -- [**Stato progetto e roadmap (14 maggio 2026)**](docs/reports/2026-05-14-stato-progetto-e-roadmap.md) — riepilogo di tutte le fasi, decisioni, caveat aperti, roadmap. +- [**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 chiave per fase: - -- [Decisione strategica](docs/superpowers/specs/2026-05-09-decisione-strategica-design.md) — perché Phase 1 prima, Phase 2 poi, Phase 3 forward-test. -- [Decision memo gate Phase 1](docs/decisions/2026-05-10-gate-phase1.md), [Technical report Phase 1](docs/reports/2026-05-10-phase1-technical-report.md), [Decision memo Phase 1.5 nemotron](docs/decisions/2026-05-11-phase1-5-nemotron-run.md). -- [Piano Phase 2.5 prompt-mutator](docs/superpowers/plans/2026-05-11-mutate-prompt-llm-phase-2-5.md), [Piano feature temporali](docs/superpowers/plans/2026-05-11-temporal-features.md). - -Documenti di contesto pre-implementazione: `00_documento_zero.md` (framework concettuale Renaissance → swarm), `coevolutive_swarm_system.md` (Filone A, sistema completo), `poc_trading_swarm.md` (Filone B, PoC trading). - -## Architettura - -``` -src/multi_swarm/ -├── config.py Settings Pydantic (.env) -├── data/ -│ ├── cerbero_ohlcv.py OHLCV loader via Cerbero MCP + cache parquet -│ └── splits.py Walk-forward expanding splits (Phase 2.6) -├── backtest/ -│ ├── orders.py Side/Order/Position/Trade -│ └── engine.py Event-driven backtest, 1-bar exec delay -├── metrics/ -│ ├── basic.py Sharpe, max drawdown, total return -│ ├── dsr.py Deflated Sharpe Ratio (Bailey & López 2014) -│ └── diversity.py Entropy/diversity metrics popolazione (Phase 2.5) -├── cerbero/ -│ ├── client.py HTTP client (bearer + bot-tag + retry tenacity) -│ └── tools.py Wrapper tool MCP (sma/rsi/atr/macd/realized_vol/funding) -├── protocol/ -│ ├── grammar.py Vocabolario operatori, indicatori, feature (incl. hour/dow/is_weekend) -│ ├── parser.py json.loads → AST dataclass tipizzato -│ ├── validator.py Arity checks, no-nesting indicators, whitelist -│ └── compiler.py AST → Callable[[df], Series[Side]] -├── genome/ -│ ├── hypothesis.py HypothesisAgentGenome (id deterministico) -│ ├── mutation.py 4 operatori scalari (temp, lookback, features, style) -│ ├── mutation_prompt_llm.py 5° operatore: riscrittura system_prompt via LLM tier B -│ └── crossover.py Uniform crossover -├── llm/ -│ ├── client.py Unified LLMClient via OpenRouter (tier S/A/B/C/D) -│ └── cost_tracker.py Pricing per tier, breakdown + call_kind tracking -├── agents/ -│ ├── hypothesis.py LLM call + JSON extract + retry-with-feedback -│ ├── falsification.py Compile → backtest → DSR -│ ├── adversarial.py Red-team heuristics (5 check HIGH parametrici via CLI) -│ └── market_summary.py Stats di mercato per il prompt -├── ga/ -│ ├── selection.py Tournament + elitism -│ ├── fitness.py v1 continua + v2 soft-kill + combined IS/OOS opt-in -│ ├── loop.py next_generation step -│ ├── summary.py median/max/p90/entropy per gen -│ └── initial.py Popolazione iniziale (6 cognitive style) -├── persistence/ -│ ├── schema.py SQLite DDL: 6 tabelle GA + 5 tabelle paper_trading_* -│ └── repository.py CRUD per runs/genomes/evals/cost/findings/gen_summary -├── paper_trading/ Phase 3 -│ ├── portfolio.py Multi-asset portfolio con sleeve uguali per asset -│ ├── executor.py PaperExecutor: carica strategia JSON, valuta ultimo bar -│ └── persistence.py PaperRepository (paper_trading_runs/ticks/equity/trades/positions) -├── orchestrator/ -│ └── run.py End-to-end pipeline GA + persistence -└── dashboard/ - ├── nicegui_app.py NiceGUI dashboard (overview / convergence / genomes) - └── data.py Lettura runs.db per le pagine -``` - -Stack: Python 3.13, uv, pytest+pytest-mock+responses, openai SDK (verso OpenRouter), requests+tenacity, pandas+numpy+scipy, sqlmodel+sqlite, nicegui+plotly, yfinance (test cross-asset non-crypto). - -CLI knobs accumulati (Phase 2.5 → 2.7): - -- `--prompt-mutation-weight FLOAT` (peso del 5° operatore, sweet spot 0.20-0.30) -- `--fees-eat-alpha-threshold FLOAT` (default 0.5, suggerito 0.7) -- `--flat-too-long-threshold FLOAT` (default 0.95) -- `--undertrading-threshold INT` (default 20) -- `--fitness-v2` + `--fitness-soft-penalty FLOAT` -- `--fitness-combined-alpha FLOAT` (multi-obiettivo IS/OOS) -- `--min-trades-threshold INT` (filtro OOS in WFA) +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. ## Setup ```bash -uv sync -cp .env.example .env # compilare CERBERO_*_TOKEN e OPENROUTER_API_KEY -uv run pytest # ~180 test attesi (unit + integration) +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) ``` ### Variabili .env richieste @@ -116,36 +79,27 @@ CERBERO_BOT_TAG=swarm-poc-phase1 # LLM provider (unico endpoint via OpenRouter) OPENROUTER_API_KEY= -OPENROUTER_BASE_URL=https://openrouter.ai/api/v1 -# Modelli per tier (default Phase 2.5+: qwen-2.5-72b per tier C, vedi .env.example per gli altri) -LLM_MODEL_TIER_C=qwen/qwen-2.5-72b-instruct +# DB paths (split per dominio: core GA vs paper della strategia) +GA_DB_PATH=./state/runs.db +STRATEGY_CRYPTO_DB_PATH=./state/strategy_crypto.db -# Deploy Docker (vedi sezione Deploy) +# Deploy Docker DOMAIN_NAME=tielogic.xyz SWARM_DASHBOARD_PORT=8080 +DASHBOARD_ROOT_PATH=/strategy_crypto_gui # subpath traefik per la dashboard ``` -### Cerbero MCP - -Tutti i fetch OHLCV passano da Cerbero MCP (sostituisce ccxt). In sviluppo locale: - -```bash -cd /home/adriano/Documenti/Git_XYZ/CerberoSuite/Cerbero_mcp -uv sync -uv run cerbero-mcp # ascolta su porta da .env (default 9001 se 9000 è occupato) -``` - -In produzione/integrazione: VPS `https://cerbero-mcp.tielogic.xyz` (richiede bearer) — o internal docker `http://cerbero-mcp:9000` se si gira nella stessa rete Traefik. +Backcompat: `DB_PATH` legacy continua a funzionare come alias di `GA_DB_PATH`. ## Comandi principali ```bash # Quality gates -uv run pytest # tutti i test -uv run pytest tests/unit -v # solo unit -uv run pytest tests/integration -v # solo integration (richiedono Cerbero + OpenRouter) -uv run ruff check src/ tests/ scripts/ +uv run pytest # tutti i test +uv run pytest src/multi_swarm_core/tests/unit -v # solo unit core +uv run pytest src/strategy_crypto/tests/ -v # smoke strategy_crypto +uv run ruff check src/ scripts/ uv run mypy src/ scripts/ # Smoke run (MockLLM + OHLCV sintetico, no API calls) @@ -160,67 +114,56 @@ uv run python scripts/run_phase1.py \ --population-size 20 --n-generations 10 \ --prompt-mutation-weight 0.30 --fitness-v2 -# Backtest standalone di una strategia JSON su range esteso +# Backtest standalone di una strategia JSON uv run python scripts/backtest_strategy.py \ - --strategy strategies/btc_fb63e851.json \ + --strategy src/strategy_crypto/strategy_crypto/strategies/btc_fb63e851.json \ --start 2018-09-01 --end 2026-01-01 # Paper-trading forward-test (Phase 3) 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 -DB_PATH=./runs.db uv run python -m multi_swarm.dashboard.nicegui_app +uv run python -m strategy_crypto.frontend.nicegui_app +# → http://localhost:8080 (env SWARM_DASHBOARD_PORT) ``` ## Dashboard -NiceGUI dashboard (dark/neon palette) su `http://localhost:8080` (override con env `SWARM_DASHBOARD_PORT`): +NiceGUI dashboard (dark palette) — **dual-DB reader** (GA + paper): -- **Overview** (`/`): lista runs, status, costo, metriche aggregate evaluations (parse success %, top fitness, median). -- **GA Convergence** (`/convergence`): fitness median/max/p90 per generazione, entropy con hline a soglia gate (0.5). -- **Genomes** (`/genomes`): top-K ordinati per fitness, click su riga per ispezione system_prompt + raw_text JSON strategy. +- **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 gira dentro Docker dietro Traefik su `https://swarm.${DOMAIN_NAME}` — vedi sezione Deploy. +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. ## Deploy -`docker-compose.yml` definisce due servizi che condividono la stessa immagine `multi-swarm:dev`: +`docker-compose.yml` definisce due servizi su immagine `multi-swarm-coevolutive:dev`: -- **`multi-swarm-paper`** — runner `scripts/run_paper_trading.py` long-running (`restart: unless-stopped`). -- **`multi-swarm-dashboard`** — NiceGUI esposta via Traefik su `https://swarm.${DOMAIN_NAME}`. +- **`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/`. -Entrambi joinano la rete external `traefik` per parlare direttamente con `cerbero-mcp:9000` senza giro pubblico+TLS. Persistenza via bind mount: - -- `./data/`, `./series/` — cache OHLCV (parquet) -- `./state/` — `runs.db` (+ WAL/SHM) -- `./strategies/` — `btc_*.json` / `eth_*.json` (read-only nel container) - -Bring-up: +Persistenza via bind mount: `./data/`, `./series/`, `./state/`. Le strategie JSON sono bind-mounted in read-only dal package: `./src/strategy_crypto/strategy_crypto/strategies/`. ```bash docker compose up -d --build -docker compose logs -f multi-swarm-paper # segui i tick -docker compose ps # stato +docker compose logs -f strategy-crypto-paper +docker compose ps ``` Note operative: - Le bind-mount dir devono essere `chown 1000:1000` (uid utente `app` nel container). -- Override del command paper-trading via env (`PAPER_RUN_NAME`, `PAPER_INITIAL_CAPITAL`, `PAPER_POLL_SECONDS`, ecc.) — vedi `.env.example`. -- `SWARM_DASHBOARD_PORT` controlla la porta interna del container (Traefik fa il TLS davanti). - -## Costi - -Costo cumulato LLM progetto a oggi: **≈ $3.74** su 30 run GA (Phase 1 → 2.7). Cap originale Phase 1: $700 → margine residuo abbondante. - -- Tier C (qwen-2.5-72b via OpenRouter): ~$0.40/1M token. -- Run base K=20 × 10gen ≈ $0.07. Con `--prompt-mutation-weight 0.30` overhead mutator 3-9%. -- **Phase 3 paper-trading**: $0 incrementali LLM (strategie fisse), solo costi Cerbero (servizio esistente). +- 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). ## Sviluppo Conventional commits con prefix `feat:` `fix:` `chore:` `docs:` `refactor:` `test:`. Body italiano. Footer `Co-Authored-By: Claude Opus 4.7 (1M context) ` su ogni commit collaborativo. -Branch attuale: `main`. Single-author retail R&D, nessun feature branch attivo. Ablation paralleli si gestiscono via CLI knobs sullo stesso branch. +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).