12 Commits

Author SHA1 Message Date
Adriano Dal Pastro 4d79e66d68 feat(api): esponi API library pubblica via __all__ in protocol/backtest/cerbero/data
Cristallizza la library surface che il nuovo repo figlio
Swarm_Strategy_Crypto consumerà via git submodule:
- multi_swarm.protocol.{compile,parse,validate}_strategy + AST nodes
- multi_swarm.backtest.{Side, Trade, Order, Position, BacktestEngine}
- multi_swarm.cerbero.CerberoClient
- multi_swarm.data.{CerberoOHLCVLoader, OHLCVRequest}

Cambi a queste signature dovranno essere considerati breaking
per il consumer downstream.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 10:33:52 +00:00
Adriano Dal Pastro 4b9cded966 fix(dashboard): leggi righe selezionate da e.args nel callback selection
Il callback registrato via top_table.on("selection", ...) riceve un
GenericEventArguments di NiceGUI, non un TableSelectionEventArguments,
quindi l'attributo .selection non esiste e il click su una riga della
tabella "Top genomi" generava AttributeError. Le righe selezionate
arrivano nel payload Quasar come e.args["rows"].

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 05:38:42 +00:00
Adriano Dal Pastro 14f130aa5a feat(dashboard): pagina /paper per monitoring forward-test
Nuova pagina NiceGUI "Paper" che legge le tabelle paper_trading_*:

- 4 metric card: Equity, P/L cumulato %, Trades chiusi, Open/Tick count
- Equity curve plotly con hline initial capital
- Tre tabelle: open positions, ultimi 30 tick (ts/bar/symbol/signal/action),
  trades chiusi (entry/exit/pnl/fees)
- Run selector dropdown + status badge + auto-refresh REFRESH_INTERVAL_S

dashboard/data.py: aggiunti 6 helper read-only su SQLite (paper_runs_df,
paper_equity_df, paper_positions_df, paper_trades_df, paper_ticks_df,
paper_run_summary). Connessione separata da Repository per usare
direttamente lo schema paper_trading_* senza passare per la classe di
write PaperRepository.

dashboard/nicegui_app.py: aggiunto import pandas (necessario per
to_datetime nell'equity curve), nav link "Paper" nell'header,
@ui.page("/paper") con helper _paper_runs_options + _paper_equity_figure.

Chiude il primo TODO della roadmap sez 10.1 ("Pagina dashboard
paper-trading").

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 19:52:02 +00:00
Adriano Dal Pastro b86dbdc9ee docs(readme): allinea a stato attuale (Phase 3, NiceGUI, Docker deploy)
- "Stato del progetto" riscritto: Phase 3 paper-trading in corso, link al
  report di sintesi del 14 maggio, strategie freezate BTC/ETH e costo
  cumulato $3.74 su 30 run GA.
- Architettura: aggiunti splits.py (WFA), diversity.py, mutation_prompt_llm.py
  (5° operatore), paper_trading/ (portfolio/executor/persistence); commenti
  su fitness.py/adversarial.py aggiornati a v2 soft-kill + 5 check HIGH.
- Nuova lista CLI knobs accumulati Phase 2.5 → 2.7.
- Setup: test count ~180, .env include DOMAIN_NAME e SWARM_DASHBOARD_PORT.
- Comandi: aggiunti backtest_strategy.py e run_paper_trading.py; esempio
  run_phase1.py ora usa --prompt-mutation-weight e --fitness-v2.
- Nuova sezione Deploy: docker-compose con due servizi su rete traefik
  external, bind-mount + chown 1000:1000, override paper via env.
- Costi: da Phase 1 only a cumulato $3.74 + Phase 3 LLM-free.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 13:45:25 +00:00
Adriano Dal Pastro a66f97fb0e docs(reports): chiudi item 10.3 port dashboard a NiceGUI
Aggiorna stato-progetto-e-roadmap riflettendo le modifiche del 14 maggio:
- diagramma architettura (sec 9): dashboard/ ora elenca solo nicegui_app.py
  + data.py; rimosso Streamlit legacy e aquarium.py
- item 10.3 "Port completo dashboard a NiceGUI": marcato [x] con
  riferimenti ai commit 03f723f (cleanup) e 8e5efde (deploy Docker
  via Traefik su swarm.tielogic.xyz). Annotata la scelta esplicita di
  non riportare l'Aquarium su NiceGUI.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 13:30:35 +00:00
Adriano Dal Pastro 073200313c docs(reports): stato progetto e roadmap al 14 maggio 2026
Riepilogo cumulato delle Phase 1 → 3: 30 run GA completate, $3.74 di
costo LLM, paper-trading runner operativo su BTC+ETH con strategie
freezate fb63e851 (BTC, true alpha hour-gated) e facd6af85d5d (ETH,
trend-following long-bias). Documento di sincronizzazione con caveat
aperti (varianza seed, dipendenza qwen-2.5-72b, Cerbero SPOF) e roadmap
suddivisa in completamento Phase 3, estensioni metodologiche, hardening
tecnico e documentazione.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 13:28:48 +00:00
Adriano Dal Pastro 03f723f7fc refactor(dashboard): switch a NiceGUI, rimuovi legacy Streamlit
NiceGUI è la dashboard ufficiale (port 8080, dark/neon palette, 3 pagine:
/, /convergence, /genomes). La porta è ora parametrica via env
SWARM_DASHBOARD_PORT, letta in ui.run() — Docker la usa anche per
healthcheck e label Traefik.

docker-compose.yml: entrypoint del servizio dashboard cambiato da
streamlit a python -m multi_swarm.dashboard.nicegui_app. Default porta
8080 ovunque (.env, .env.example, compose, healthcheck).

Rimossi i file legacy della vecchia GUI Streamlit:
- src/multi_swarm/dashboard/streamlit_app.py
- src/multi_swarm/dashboard/aquarium.py (helper canvas HTML5)
- src/multi_swarm/dashboard/pages/{01_overview,02_ga_convergence,
  03_genomes,04_aquarium}.py
- tests/integration/test_streamlit_smoke.py

pyproject.toml: rimossa la dep streamlit; uv.lock rigenerato (10 deps
transitive eliminate: pydeck, watchdog, jsonschema, pillow, ecc.).
README aggiornato (architettura, comando dashboard, sezione Dashboard
ora descrive NiceGUI con riferimento al deploy Docker via Traefik).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 12:15:11 +00:00
Adriano Dal Pastro 8e5efde219 feat(docker): deploy su VPS via traefik con cerbero-mcp interno
Dockerfile multi-stage (python:3.13-slim + uv) e docker-compose con due
servizi che condividono l'immagine:

- multi-swarm-paper: runner long-running scripts/run_paper_trading.py
- multi-swarm-dashboard: Streamlit su https://swarm.${DOMAIN_NAME}

Cerbero raggiunto via rete docker interna (http://cerbero-mcp:9000)
saltando il giro pubblico traefik+TLS. Persistenza via bind mount su
data/, series/, state/ (runs.db con WAL), strategies/ in read-only.

.env.example aggiornato con DOMAIN_NAME, SWARM_DASHBOARD_PORT (porta
interna parametrizzabile) e i PAPER_* per override del command.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 10:21:10 +00:00
Adriano 45f273f591 feat(phase-3): paper-trading runner BTC+ETH
Modulo paper_trading per forward-test virtuale Phase 3:
- Portfolio multi-asset equal-weight sleeve, fees bp su round-trip
- PaperExecutor compila strategia JSON e applica segnale a bar close
- PaperRepository persiste runs/ticks/trades/equity in runs.db
- CLI scripts/run_paper_trading.py: loop polling Cerbero, exec su nuovo bar

Strategie deployate:
- BTC fb63e851 (Sharpe OOS +0.50, mean rev RSI+ATR+hour gate)
- ETH facd6af85d5d (Sharpe OOS +0.19, trend vol regime + SMA50/200)

Capitale virtuale $1000 (sleeve $500 ciascuno), 2 settimane smoke.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 23:34:15 +02:00
Adriano 9d1ef8adcf chore(deps): aggiungi yfinance per test cross-asset non-crypto
Phase 2.7 portabilità: tentativo backtest top genome BTC/ETH su asset
tradFi via yfinance per stress-test universale del setup.
Esito documentato in memoria: yfinance 1h limitato a 730 giorni,
incompatibile con hour-gated strategy su 5+ anni.

Aggiunto .claude/ a .gitignore (scheduled_tasks.lock runtime artifact).
2026-05-13 23:27:53 +02:00
Adriano 67ae6ff74e feat(hypothesis): pattern guidance — forma curve + ripetibilità nel system prompt
Aggiunta sezione 'PATTERN GUIDANCE' nel SYSTEM_TEMPLATE che guida il LLM a
generare strategie basate non solo su threshold di indicatori isolati, ma
anche su:

- Forme di curva (trend asc/desc, compressione/espansione vol, mean reversion strutturale)
- Ripetibilità dell'andamento (crossover ricorrenti, pattern intraday/weekly, doppio top, range breakout)

La grammar JSON resta invariata (no nuove primitive); il prompt insegna al
LLM a comporre i nodi esistenti per approssimare pattern di chart analysis
classica. Obiettivo: spostare la generazione dalla soglia statica RSI<30
verso pattern shape-aware che si replicano nei dati storici.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 16:49:35 +02:00
Adriano 1a1dfb7a73 feat(fitness): multi-objective combined = alpha*IS + (1-alpha)*OOS opt-in
Aggiunti due meccanismi per selection multi-objective:

1) Helper compute_combined_fitness(fit_train, fit_oos, alpha):
   formula = alpha*IS + (1-alpha)*OOS, fallback a IS se OOS è None/NaN.

2) RunConfig.eval_oos_during_loop (default False) + fitness_combined_alpha
   (default 0.5). Quando True E wfa_train_split attivo, ogni genome con
   fitness IS > 0 viene rivalutato su test_ohlcv DURANTE il loop GA e la
   fitness usata per tournament_select/elite_select è quella combinata.
   2x costo backtest engine (richiede 2 evaluation per genome).

3) CLI flags --eval-oos-during-loop e --fitness-combined-alpha.

Motivazione: il run phase2-7-max7y-v2-wfa-001 ha mostrato che il top by
fitness_IS (4e1be9fa, ratio OOS 0.31) NON è il top per performance OOS reale
(634111992702, ratio 1.42, ret_OOS +105% / 2.2y). Selezionare durante GA
con combined fitness orienta l'evoluzione verso strategie OOS-robust by
design invece di filtrarle a posteriori.

Backward compat: default eval_oos_during_loop=False → comportamento
invariato per run senza il flag.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 16:47:53 +02:00
34 changed files with 2160 additions and 1589 deletions
+58
View File
@@ -0,0 +1,58 @@
# Git
.git
.gitignore
.gitattributes
# Python caches
__pycache__
*.py[cod]
*.egg-info
.venv
venv
.pytest_cache
.mypy_cache
.ruff_cache
# Editors / OS
.vscode
.idea
.DS_Store
*.swp
*.swo
# Secrets — montati via env_file nel compose, mai dentro l'immagine
.env
.env.*
!.env.example
*.pem
*.key
# Artefatti runtime — vivono come bind mount, non nell'immagine
runs.db
runs.db-journal
runs.db-wal
runs.db-shm
data/
series/
state/
*.parquet
*.feather
checkpoints/
logs/
*.log
# Build / dist
build/
dist/
# Docs grandi — non servono in immagine
docs/
*.md
!README.md
# Test — non servono in runtime (l'immagine non gira pytest)
tests/
# OMC / claude metadata
.omc/
.claude/
+13
View File
@@ -22,3 +22,16 @@ RUN_NAME=phase1-spike-001
DATA_DIR=./data
SERIES_DIR=./series
DB_PATH=./runs.db
# Docker / Traefik (usati SOLO da docker-compose.yml)
# Dominio base: traefik espone la dashboard su swarm.${DOMAIN_NAME}
DOMAIN_NAME=tielogic.xyz
# Porta interna della NiceGUI dashboard (Traefik fa il TLS davanti)
SWARM_DASHBOARD_PORT=8080
# Paper-trading runner — override del command nel compose (opzionali)
PAPER_RUN_NAME=phase3-papertrade-prod
PAPER_INITIAL_CAPITAL=1000
PAPER_FEES_BP=5.0
PAPER_POLL_SECONDS=300
PAPER_LOOKBACK_BARS=500
+1
View File
@@ -14,6 +14,7 @@ venv/
.DS_Store
*.swp
*.swo
.claude/
# Env / secrets
.env
+52
View File
@@ -0,0 +1,52 @@
# 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)
#
# 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.
FROM python:3.13-slim AS builder
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential curl \
&& rm -rf /var/lib/apt/lists/*
RUN pip install --no-cache-dir "uv>=0.5,<0.9"
WORKDIR /app
COPY pyproject.toml uv.lock README.md ./
COPY src ./src
RUN uv sync --frozen --no-dev
FROM python:3.13-slim AS runtime
LABEL org.opencontainers.image.title="multi-swarm" \
org.opencontainers.image.version="0.1.0" \
org.opencontainers.image.source="https://git.tielogic.xyz/Adriano/Multi_Swarm_Coevolutive"
RUN apt-get update && apt-get install -y --no-install-recommends \
ca-certificates \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY --from=builder /app /app
COPY scripts ./scripts
ENV PATH="/app/.venv/bin:$PATH" \
PYTHONUNBUFFERED=1 \
PYTHONDONTWRITEBYTECODE=1 \
PYTHONPATH=/app/src
RUN useradd -m -u 1000 app \
&& mkdir -p /app/data /app/series /app/state /app/strategies \
&& chown -R app:app /app
USER app
# Healthcheck di default: import del package — i servizi reali lo
# sovrascrivono nel compose (streamlit /_stcore/health).
HEALTHCHECK --interval=60s --timeout=5s --retries=3 --start-period=10s \
CMD python -c "import multi_swarm" || exit 1
# Nessun CMD di default: il compose specifica entrypoint/command
# per ognuno dei due servizi.
+105 -54
View File
@@ -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 BTC-PERPETUAL 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. 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. Il tutto è ispirato alla filosofia di Renaissance Technologies adattata a un contesto retail single-author con LLM agents.
## Repository
@@ -12,22 +12,22 @@ git clone ssh://git@git.tielogic.xyz:222/Adriano/Multi_Swarm_Coevolutive.git
## Stato del progetto
**Phase 1 (lean spike) completata** il 10 maggio 2026 con tutti i 5 hard gate passati (loop convergence, parse success 100%, top-5 ratio 1116x, entropy 0.914, costo $0.069 vs cap $700). Decisione strategica: **GO Phase 2** con tre aggiustamenti (Adversarial soglie più strette, speciation, walk-forward 70/30).
**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 1.5 (tactical hardening) in corso**: Adversarial layer rinforzato con soglie più strette (`overtrading` a `n_bars/20`, `undertrading` HIGH se `n<10`) e due nuovi check HIGH (`flat_too_long` se signal flat >95% bar, `fees_eat_alpha` se fees > 50% del gross PnL). Killa le strategie degeneri del run v5 (top-1 era flat 99.8% del tempo e ha sottoperformato BTC B&H di 103 punti percentuali).
- `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.
Documenti chiave:
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:
- [**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.
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.
- [Piano implementativo Phase 1](docs/superpowers/plans/2026-05-09-phase1-lean-spike.md) — 38 task TDD-driven.
- [Decision memo gate Phase 1](docs/decisions/2026-05-10-gate-phase1.md) — valutazione formale dei 5 hard gate.
- [Technical report Phase 1](docs/reports/2026-05-10-phase1-technical-report.md) — risultati, ispezione top genomi, threats to validity.
- [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 co-evolutivo LLM).
- `coevolutive_swarm_system.md` — design Filone A (sistema completo, 12-18 mesi).
- `poc_trading_swarm.md` — design Filone B (PoC trading, fonte di Phase 1).
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
@@ -36,63 +36,73 @@ 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
│ └── 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)
── 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
│ ├── 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 (temp, lookback, features, style)
│ ├── 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
│ └── 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 (no_trades/degenerate/over/under)
│ ├── 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: dsr + tanh(sharpe) × penalty(dd)
│ ├── 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 + 3 indici
│ ├── 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 + persistence
│ └── run.py End-to-end pipeline GA + persistence
└── dashboard/
├── streamlit_app.py Hub multipage
── data.py Lettura runs.db per le pagine
├── aquarium.py Helper canvas HTML5 (fish data + JS template)
└── pages/
├── 01_overview.py Run + metriche aggregate
├── 02_ga_convergence.py Fitness convergence + entropy plot
├── 03_genomes.py Top-10 + ispezione system_prompt
└── 04_aquarium.py Acquario 2D con click → info + lineage
├── 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, streamlit+plotly.
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)
## Setup
```bash
uv sync
cp .env.example .env # compilare CERBERO_*_TOKEN e OPENROUTER_API_KEY
uv run pytest # verifica che tutto installi (141 test attesi)
uv run pytest # ~180 test attesi (unit + integration)
```
### Variabili .env richieste
@@ -108,17 +118,17 @@ CERBERO_BOT_TAG=swarm-poc-phase1
OPENROUTER_API_KEY=<sk-or-v1-...>
OPENROUTER_BASE_URL=https://openrouter.ai/api/v1
# Modelli per tier (override dei default se serve)
LLM_MODEL_TIER_S=anthropic/claude-opus-4-7
LLM_MODEL_TIER_A=anthropic/claude-sonnet-4-6
LLM_MODEL_TIER_B=anthropic/claude-sonnet-4-6
# 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
LLM_MODEL_TIER_D=meta-llama/llama-3.3-70b-instruct
# Deploy Docker (vedi sezione Deploy)
DOMAIN_NAME=tielogic.xyz
SWARM_DASHBOARD_PORT=8080
```
### Cerbero MCP
Phase 1 fetcha OHLCV via Cerbero MCP (sostituisce ccxt). Avviare Cerbero locale prima di un run reale:
Tutti i fetch OHLCV passano da Cerbero MCP (sostituisce ccxt). In sviluppo locale:
```bash
cd /home/adriano/Documenti/Git_XYZ/CerberoSuite/Cerbero_mcp
@@ -126,50 +136,91 @@ uv sync
uv run cerbero-mcp # ascolta su porta da .env (default 9001 se 9000 è occupato)
```
In alternativa usare il VPS esistente `https://cerbero-mcp.tielogic.xyz` (richiede bearer).
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.
## Comandi principali
```bash
# Quality gates
uv run pytest # tutti i test (141 PASSED attesi)
uv run pytest # tutti i test
uv run pytest tests/unit -v # solo unit
uv run pytest tests/integration -v # solo integration
uv run pytest tests/integration -v # solo integration (richiedono Cerbero + OpenRouter)
uv run ruff check src/ tests/ scripts/
uv run mypy src/ scripts/
# Smoke run (MockLLM + OHLCV sintetico, no API calls)
uv run python scripts/smoke_run.py
# Run reale Phase 1 (Cerbero + OpenRouter, ~$0.07 per run K=20 10gen)
# Run reale Phase 1/2 (Cerbero + OpenRouter, ~$0.07 per run K=20 10gen)
uv run python scripts/run_phase1.py \
--name phase1-run-XXX \
--name run-XXX \
--exchange deribit --symbol BTC-PERPETUAL --timeframe 1h \
--start 2024-01-01T00:00:00+00:00 \
--end 2026-01-01T00:00:00+00:00 \
--population-size 20 --n-generations 10
--population-size 20 --n-generations 10 \
--prompt-mutation-weight 0.30 --fitness-v2
# Dashboard
DB_PATH=./runs.db uv run streamlit run src/multi_swarm/dashboard/streamlit_app.py
# Backtest standalone di una strategia JSON su range esteso
uv run python scripts/backtest_strategy.py \
--strategy 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
# Dashboard NiceGUI locale
DB_PATH=./runs.db uv run python -m multi_swarm.dashboard.nicegui_app
```
## Dashboard
Streamlit multipage su `http://localhost:8501` (override con `--server.port`):
NiceGUI dashboard (dark/neon palette) su `http://localhost:8080` (override con env `SWARM_DASHBOARD_PORT`):
- **Overview**: lista runs, status, costo, metriche aggregate evaluations (parse success %, top fitness, median).
- **GA Convergence**: fitness median/max/p90 per generazione, entropy con hline a soglia gate (0.5).
- **Genomes**: top-10 ordinati per fitness, click su row per ispezione system_prompt + raw_text JSON strategy.
- **Aquarium**: visualizzazione 2D canvas HTML5 con un pesce per agente; dimensione ∝ fitness, colore per cognitive_style, halo sui top-3, click su pesce → panel info completo + lineage BFS (parents → grandparents → ...).
- **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.
## Costi tipici Phase 1
In produzione gira dentro Docker dietro Traefik su `https://swarm.${DOMAIN_NAME}` — vedi sezione Deploy.
Tier C (qwen-2.5-72b via OpenRouter): ~$0.40/1M token. Run K=20 × 10gen ≈ $0.07. Phase 1 totale (5 run incluse iterazioni bug-fix): $0.19.
## Deploy
Per Phase 2 con tier mix B/C (Sonnet 4.6 = $3/$15 input/output) stima: $3-15 per ablation completa.
`docker-compose.yml` definisce due servizi che condividono la stessa immagine `multi-swarm: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}`.
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:
```bash
docker compose up -d --build
docker compose logs -f multi-swarm-paper # segui i tick
docker compose ps # stato
```
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).
## Sviluppo
Conventional commits con prefix `feat:` `fix:` `chore:` `docs:` `refactor:` `test:`. Body italiano. Footer `Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>` su ogni commit collaborativo.
Branch attuale: `main`. Nessun feature branch in Phase 1 (single author, lean spike). Phase 2 valuterà feature branch per ablation paralleli.
Branch attuale: `main`. Single-author retail R&D, nessun feature branch attivo. Ablation paralleli si gestiscono via CLI knobs sullo stesso branch.
+106
View File
@@ -0,0 +1,106 @@
# docker-compose.yml — Multi-Swarm Coevolutive
#
# Due servizi che condividono la stessa immagine `multi-swarm:dev`:
#
# * 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}
#
# 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.
#
# 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
#
# 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:
external: true
x-swarm-env: &swarm-env
# Override: rotta interna verso cerbero-mcp (no TLS, no traefik hop)
CERBERO_BASE_URL: http://cerbero-mcp:9000
# Override: path container per persistenza
DATA_DIR: /app/data
SERIES_DIR: /app/series
DB_PATH: /app/state/runs.db
services:
multi-swarm-paper:
build:
context: .
dockerfile: Dockerfile
image: multi-swarm:dev
container_name: multi-swarm-paper
restart: unless-stopped
networks: [traefik]
env_file: .env
environment:
<<: *swarm-env
volumes:
- ./data:/app/data
- ./series:/app/series
- ./state:/app/state
- ./strategies:/app/strategies:ro
# Niente HTTP da controllare: ci affidiamo a `restart: unless-stopped`
# e ai log per la liveness del runner.
command:
- python
- /app/scripts/run_paper_trading.py
- --name=${PAPER_RUN_NAME:-phase3-papertrade-prod}
- --initial-capital=${PAPER_INITIAL_CAPITAL:-1000}
- --fees-bp=${PAPER_FEES_BP:-5.0}
- --poll-seconds=${PAPER_POLL_SECONDS:-300}
- --lookback-bars=${PAPER_LOOKBACK_BARS:-500}
- --strategies-dir=/app/strategies
labels:
- com.centurylinklabs.watchtower.enable=true
multi-swarm-dashboard:
image: multi-swarm:dev
build:
context: .
dockerfile: Dockerfile
container_name: multi-swarm-dashboard
restart: unless-stopped
networks: [traefik]
env_file: .env
environment:
<<: *swarm-env
volumes:
# Dashboard legge solo runs.db: mount in read-only
- ./state:/app/state:ro
- ./data:/app/data:ro
- ./series:/app/series:ro
entrypoint:
- python
- -m
- multi_swarm.dashboard.nicegui_app
command: []
healthcheck:
test:
- "CMD"
- "python"
- "-c"
- "import os, urllib.request; urllib.request.urlopen(f'http://localhost:{os.environ.get(\"SWARM_DASHBOARD_PORT\",\"8080\")}/', timeout=3).close()"
interval: 30s
timeout: 5s
retries: 3
start_period: 30s
labels:
- traefik.enable=true
- traefik.docker.network=traefik
- "traefik.http.routers.multi-swarm-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}"
- com.centurylinklabs.watchtower.enable=true
@@ -0,0 +1,286 @@
# Multi-Swarm Coevolutivo — Stato del progetto e roadmap
*Data del documento: 14 maggio 2026 — branch `main` allineato a commit `45f273f`.*
Questo documento riepiloga l'intero percorso del proof-of-concept Multi-Swarm Coevolutive dalla Phase 1 (lean spike) fino allo stato corrente di entrata in Phase 3 (paper-trading forward-test). È inteso come punto di sincronizzazione per riprendere il lavoro: cosa è stato deciso, cosa ha funzionato, cosa no, e quali sono le prossime mosse plausibili.
---
## 1. Quadro sintetico
| Fase | Periodo | Stato | Esito |
|------|---------|-------|-------|
| **Phase 1** — lean spike | 9-10 maggio 2026 | ✅ chiusa | GO Phase 2 (5/5 hard gate) |
| **Phase 1.5** — adversarial hardening | 11 maggio 2026 | ✅ chiusa | NO-GO sulla combo nemotron, hardening conservato |
| **Phase 2** — feature temporali + qwen3-235b | 11 maggio 2026 | ✅ chiusa | NO-GO sul modello (rollback a qwen-2.5-72b) |
| **Phase 2.5** — LLM prompt mutator | 11-12 maggio 2026 | ✅ chiusa | Operator integrato, sweet spot weight 0.20-0.30 |
| **Phase 2.6** — Walk-Forward Validation | 12-13 maggio 2026 | ✅ chiusa | WFA 70/30 introdotta, min-trades parametrico |
| **Phase 2.7** — portabilità cross-asset (BTC/ETH/SOL) | 13 maggio 2026 | ✅ chiusa | BTC strong, ETH adequate, SOL failure |
| **Phase 3** — paper-trading forward-test | 13-14 maggio 2026 | 🟢 in corso | Runner BTC+ETH operativo, smoke OK |
Dal punto di vista del DB locale: 30 run GA completate, costo cumulato LLM **≈ $3.74**, due paper-trading run avviati (`phase3-smoke-001`, `phase3-papertrade-001`).
---
## 2. Phase 1 — lean spike (chiusa 10 maggio)
### Obiettivo
Validare end-to-end l'idea co-evolutiva: GA → popolazione di prompt LLM → strategie JSON → backtest deterministico → fitness → selezione. Cinque hard gate vincolanti.
### Risultato
Run di riferimento `phase1-real-005` su BTC-PERPETUAL Deribit 1h, 2024-01-01 → 2026-01-01, K=20, 10 generazioni, **costo $0.069 in 29 minuti**.
| Hard gate | Soglia | Misurato | Esito |
|-----------|--------|----------|-------|
| Loop convergence | median sale | 0.0001 → 0.0188 in 3 gen | ✓ |
| Parse success | ≥ 95% | 100% (98/98) post refactor JSON | ✓ |
| Top-5 vs median | ≥ 10× | 1116× | ✓ |
| Entropy fitness gen 9 | ≥ 0.5 | 0.914 | ✓ |
| Costo totale | ≤ $700 | $0.069 | ✓ |
Iterazione: 5 run prima del PASS, ognuna ha scoperto un bug strutturale (max_dd su equity assoluta, cap Cerbero 5000 candele, validator arity, switch grammar S-expr→JSON, fitness clip-to-0 troppo dura).
### Caveat critico
Il top-1 ha reso **+2.66% in 2 anni vs B&H BTC +106%**, essendo *flat* nel 99,8% del tempo. Conferma che la fitness v1 premiava "non-strategie" sicure invece di alpha vero. Da qui la Phase 1.5.
### Documenti chiave
- `docs/decisions/2026-05-10-gate-phase1.md` — decision memo.
- `docs/reports/2026-05-10-phase1-technical-report.md` — report tecnico.
---
## 3. Phase 1.5 — adversarial hardening (chiusa 11 maggio)
Quattro nuovi check `HIGH` aggiunti all'agente Adversarial per killare strategie degeneri:
1. `overtrading` ricalibrato `n_bars/20` (era `n_bars/5`).
2. `undertrading` promosso a HIGH se `n_trades < 10`.
3. `flat_too_long` (nuovo HIGH) — segnale flat > 95% bar.
4. `fees_eat_alpha` (nuovo HIGH) — `fees / |gross_pnl| > 0.5` con gross positivo.
5. `time_in_market_too_high` (nuovo HIGH) — segnale LONG||SHORT > 80% bar (kill leveraged-B&H camuffato).
**Run di test `phase1.5-nemotron-001`** (tier C nemotron, 2h26', $0.12) → **NO-GO**: max fitness 0.0215 stagnante, median 0 su 9 gen, top-5 con DSR=0 e Sharpe ≈ 1.1. I check Phase 1.5 funzionavano (98 findings emessi); il problema era il modello: prompt calibrato su qwen, nemotron produceva materiale qualitativamente più povero.
Bugfix collaterale (`9d0deb3`): `EmptyCompletionError` reso retryable + gestione `resp.usage=None` per provider `:free`.
---
## 4. Phase 2 — feature temporali + tier C qwen3-235b (chiusa 11 maggio)
Due lavori in parallelo, esiti opposti:
**4.1 Feature temporali in protocol layer**`KNOWN_FEATURES` esteso con `hour`, `dow`, `is_weekend`, `minute_of_hour`. Compiler dispatcher temporale (`9d1f97c`), validator parametrizzato, integration test gating temporale+SMA. Few-shot example nel prompt Hypothesis. **Successo strutturale**: tutte le top strategie successive sfruttano questo asset.
**4.2 Upgrade tier C a `qwen/qwen-2.5-72b-instruct` → `qwen3-235b-a22b`** — run `phase2-qwen3-001`: max fitness 0.0238 stuck per 8 gen, entropy 0.199 stuck per 7 gen, 4 dei 5 top genomi con fitness/Sharpe/DD identici. Il **run controllo** identico ma con qwen-2.5-72b: 0.0311 (+30%), median raggiunge top in 4 gen, entropy 0.85, ½ tempo e costo. **Rollback a qwen-2.5-72b** (`8ec45c5`).
Lezione consolidata: il prompt è calibrato sulla famiglia qwen-2.5; un modello "più nuovo / più grande" non è automaticamente meglio se il prompt non viene ricalibrato in parallelo.
---
## 5. Phase 2.5 — operator `mutate_prompt_llm` (chiusa 12 maggio)
Quinto operatore di mutazione che riscrive il `system_prompt` via LLM tier B (`deepseek-v4-flash`) anziché perturbare scalari. Sei istruzioni atomiche: `tighten_threshold`, `swap_comparator`, `add_condition`, `remove_condition`, `change_timeframe`, `add_temporal_gate`. Validation gate (lunghezza ≥ 50, keyword tecnica, diff Levenshtein > 5%) + fallback `random_mutate`. Dispatcher pesato `weighted_random_mutate` (CLI `--prompt-mutation-weight`, default 0.0).
### Sweet spot empirico (seed 42, pop 20, 10 gen)
| weight | max fit | median fin | Sharpe top | trades | verdetto |
|--------|---------|-----------|-----------|--------|----------|
| 0.00 | 0.0311 | 0.0000 | 1.08 | 274 | baseline |
| **0.30** | **0.1012** ⭐ | **0.0745** | **0.25** | 62 | sweet spot (ma seed-lucky) |
| 0.50 | 0.0311 | 0.0000 | 1.08 | 274 | regressione |
### Validazione robustezza
Confronti seed multipli (7, 99, 123) hanno mostrato che il **+225%** del run 004 era **outlier seed-specific**. Beneficio medio reale del prompt-mutator: **+1023%** sopra baseline. La leva più affidabile e seed-indipendente è risultata `fees_eat_alpha_threshold 0.7` (anziché 0.5): +23% stabile, Sharpe top 0.70 vs 1.08.
### Combo vincente (pop=30 + weight=0.30 + fees=0.7)
Run `pop30-combo-001`: max fitness 0.0459 (+48% vs control), **median finale = max** (convergenza ≥50% pop), Sharpe top 0.63, 226 trades. Mutator overhead ≈ 5,4% del costo totale.
### Cost attribution (Task 6)
`cost_records.call_kind` (`hypothesis` / `mutation`) attivo da `ba4eb09`. Permette breakdown costo per operatore: il prompt-mutator costa 3-9% del totale, trascurabile.
---
## 6. Phase 2.6 — Walk-Forward Validation (chiusa 13 maggio)
Aggiunte tre leve metodologiche:
- **WFA 70/30**: split temporale `train/OOS` con OOS intoccato durante GA, valutato solo a fine run.
- **`--min-trades-threshold`** parametrico: filtra survivors con n_trades insufficiente prima del ranking.
- **Fitness v2 soft-kill** (`cf42dd8`): solo `no_trades` + `degenerate` + `undertrading` azzerano hard. Altri HIGH applicano penalty moltiplicativa `1/(1+0.4·n)` (1 HIGH = 0,71×, 2 = 0,56×, 3 = 0,45×). CLI `--fitness-v2` + `--fitness-soft-penalty`.
- **Pattern guidance nel system prompt** (`67ae6ff`): forma curve attese + criterio ripetibilità.
- **Fitness multi-obiettivo** (`1a1dfb7`): `combined = α·IS + (1−α)·OOS` opt-in.
Effetto cumulativo: la pipeline produce strategie con migliore generalizzazione cross-split senza dover degradare le adversarial hard.
---
## 7. Phase 2.7 — backtest 7 anni e portabilità cross-asset (chiusa 13 maggio)
### 7.1 Validazione 7,33 anni su BTC
Backtest dei top genome scoperti sulle varie sotto-fasi sui **64.297 bar 1h** completi (2018-09-01 → 2026-01-01), fees 5 bp:
| Genome | Origine | Total P/L 7y | CAGR | Sharpe ann | MaxDD | Verdetto |
|--------|---------|--------------|------|-----------|-------|----------|
| `5226503a` | run004 outlier 2y bull | **310,69%** | wiped out | 0,155 | 280,9% | crash totale OOS |
| `e52604ba` | flat-ablation top 2y | 37,17% | 6,14% | 0,063 | 182,0% | SMA non generalizza |
| `ec06a3d4` | fitness-v2-combo top 2y | +142,51% | +12,85% | +0,229 | 64,9% | hour-gated regge |
| `4e1be9fa` | 7y-v2-WFA top IS | +67,60% | +7,30% | +0,240 | 79,1% | top IS ingannevole |
| `63411199` | 7y-v2-WFA top OOS | **+660,11%** | **+31,88%** | +0,238 | 77,1% | leveraged-B&H camuffato |
| **`fb63e851`** ⭐ | 7y multi-seed99 top OOS | +130,37% | +12,06% | **+0,264** | **54,8%** | **true alpha** |
Conclusioni:
- Il top by `fit_IS` è sistematicamente ingannevole su orizzonti lunghi.
- Pattern SMA-puri collassano cross-regime.
- Pattern *hour-gated* (filtri intraday) reggono cross-regime.
- `fb63e851` è il candidato più robusto: 4 AND × 2 rule × filtro intraday → attiva l'1-2% del tempo, Sharpe cross-regime più alto.
### 7.2 Portabilità BTC → ETH → SOL
Tre run **identici** (`population=30`, `n_gen=10`, `prompt_mutation_weight=0.30`, fitness v2, WFA 70/30, `fees_eat_alpha_threshold=0.7`, undertrading 20) su Deribit perpetuals.
| Asset | Storia | Top OOS Sharpe | Verdetto |
|-------|--------|----------------|----------|
| **BTC** | 7,33 y | `fb63e851` +0,50 OOS (+20,16%) | **STRONG** |
| **ETH** | 6,75 y | `facd6af85d5d` +0,19 OOS (+16,14%) | **ADEQUATE** |
| **SOL** | 3,00 y | **0 survivors / 247 evals** | **FAILURE** |
Pattern scoperti **divergenti**: BTC = mean reversion intraday contrarian; ETH = trend-following long-bias + vol regime. **Non esiste "una strategia universale"**: la metodologia (GA + WFA + adversarial v2) è portabile, il pattern no. SOL ha fallito per finestra dati troppo corta (3y) e regime bull-only post-FTX.
---
## 8. Phase 3 — paper-trading forward-test (in corso)
### Componenti implementati (`45f273f`)
Modulo `src/multi_swarm/paper_trading/`:
- `portfolio.py` — multi-asset portfolio con sleeve uguali per asset, fees in bp.
- `executor.py``PaperExecutor` carica una strategia JSON, compila, valuta l'ultimo bar.
- `persistence.py``PaperRepository` su SQLite (tabelle `paper_trading_runs`, `paper_trading_ticks`, `paper_trading_equity`, `paper_trading_trades`, `paper_trading_positions`).
Runner `scripts/run_paper_trading.py`:
- Loop poll OHLCV Cerbero ogni `--poll-seconds` (default 300).
- Riconosce *nuovo bar chiuso* confrontando ultimo timestamp; tick consecutivi su stesso bar = hold.
- Snapshot equity ogni tick.
- Supporta `--max-ticks N` per smoke test (0 = infinito).
Strategie freezate per il forward-test:
- `strategies/btc_fb63e851.json` — RSI estremi + ATR vs SMA + filtro orario 9-17.
- `strategies/eth_facd6af85d5d.json` — ATR + realized_vol + golden/death cross.
### Stato corrente
- Schema DB esteso e validato.
- Run smoke completato (`phase3-smoke-001`).
- Run live in corso (`phase3-papertrade-001`).
---
## 9. Architettura cumulata
```
src/multi_swarm/
├── config.py
├── data/{cerbero_ohlcv,splits}.py ← splits.py per WFA
├── backtest/{orders,engine}.py
├── metrics/{basic,dsr,diversity}.py ← diversity per Phase 2.5
├── cerbero/{client,tools}.py
├── protocol/{grammar,parser,validator,compiler}.py
│ └── KNOWN_FEATURES include hour/dow/is_weekend/minute_of_hour
├── genome/
│ ├── hypothesis.py
│ ├── mutation.py ← 4 operatori scalari
│ ├── mutation_prompt_llm.py ← Phase 2.5: 5° operatore LLM
│ └── crossover.py
├── llm/{client,cost_tracker}.py ← cost_kind tracking
├── agents/{hypothesis,falsification,adversarial,market_summary}.py
│ └── adversarial: 5 check HIGH parametrici (CLI knobs)
├── ga/
│ ├── selection.py
│ ├── fitness.py ← v1 + v2 soft-kill + combined IS/OOS
│ ├── loop.py
│ ├── summary.py
│ └── initial.py
├── persistence/{schema,repository}.py ← +tabelle paper_trading_*
├── paper_trading/ ← NEW Phase 3
│ ├── portfolio.py
│ ├── executor.py
│ └── persistence.py
├── orchestrator/run.py
└── dashboard/
├── nicegui_app.py ← unica GUI, porta parametrica via SWARM_DASHBOARD_PORT
└── data.py
```
CLI knobs accumulati per ablation:
- `--prompt-mutation-weight FLOAT` (Phase 2.5)
- `--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` (WFA OOS filter)
---
## 10. Cosa resta da fare
### 10.1 Phase 3 — completamento paper-trading
- [ ] **Definire criterio di STOP/GO Phase 3**: durata minima forward-test (es. 4-8 settimane), soglie sopravvivenza (Sharpe live > 50% del Sharpe OOS atteso, DD live < 1,5× DD OOS).
- [ ] **Pagina dashboard paper-trading**: estendere NiceGUI con tab live equity + open positions + tick log per `paper_trading_runs`. Oggi i dati esistono in DB ma non hanno UI dedicata.
- [ ] **Monitoring & alerting**: notifica se il runner si ferma (Cerbero down, processo killato). Considerare systemd unit o supervisor.
- [ ] **Robustezza fetch live**: oggi `loader._fetch(req)` bypassa la cache; aggiungere retry esplicito (oltre a quello tenacity già presente nel client) e log strutturato dei fallimenti per asset.
- [ ] **Confronto live vs OOS atteso**: script che a fine settimana confronta P/L, Sharpe rolling, hit rate vs i numeri del backtest 7y per individuare *regime mismatch* precoce.
### 10.2 Estensioni metodologiche
- [ ] **Multi-seed ensembling**: invece di scegliere un singolo top genome, valutare ensemble (mediana o weighted) dei top-K trovati con seed diversi sullo stesso asset. La varianza seed è il rischio numero uno (vedi sezione 5).
- [ ] **Asset universe expansion**: testare la metodologia su asset non-crypto (oro, forex EURUSD) per smentire l'ipotesi che funzioni solo perché BTC/ETH hanno alta volatilità. `yfinance` è già in dipendenze (`9d1ef8a`).
- [ ] **Fitness regime-aware**: oggi fitness è single-objective sull'intero train. Considerare fitness condizionata al regime (bull/bear/range) per favorire strategie con performance bilanciata cross-regime invece di top assoluto.
- [ ] **Phase 2.7 retry su SOL** con configurazione mirata: train più corto, undertrading_threshold ridotto, prompt few-shot di strategie short-vol-only. Verificare se è davvero il dato a fallire o se è la calibrazione.
### 10.3 Hardening tecnico
- [ ] **Cleanup zombie runs**: `phase2-6-flat-wfa-001` è ancora `failed` nel DB. Verificare che il flush di stato sia idempotente per tutti i path di crash.
- [x] **Port completo dashboard a NiceGUI** *(chiuso 14 maggio 2026, commit `03f723f`)*: Streamlit deprecata e rimossa insieme ai file legacy (`streamlit_app.py`, `aquarium.py`, `pages/0[1-4]_*.py`); dep `streamlit>=1.40` cancellata da `pyproject.toml` con 10 transitive (pydeck, watchdog, jsonschema, pillow, …). NiceGUI espone 3 pagine (`/`, `/convergence`, `/genomes`) su porta parametrica `SWARM_DASHBOARD_PORT` (default 8080). **Aquarium non riportata per scelta** (decisione utente: non più ritenuta utile). Deploy in produzione via Docker + Traefik su `https://swarm.tielogic.xyz` (compose `docker-compose.yml`, commit `8e5efde`).
- [ ] **Pruning DB**: dopo 30+ run la SQLite cresce. Aggiungere uno script di archiviazione/compressione delle run completate più vecchie di N giorni.
- [ ] **CI/test coverage**: i 180+ test girano localmente; non c'è ancora CI esterna (Gitea Actions o equivalente).
### 10.4 Documentazione e governance
- [ ] **Decision memo Phase 2.5 + Phase 2.6 + Phase 2.7** formalizzati come `docs/decisions/2026-05-1{2,3}-*.md` (esistono solo memory + commit message; manca il pendant pubblico dei due memo già esistenti per Phase 1 e Phase 1.5).
- [ ] **Phase 3 charter**: documento che fissa a priori cosa significherà "successo" o "fallimento" del forward-test, per evitare *moving goalposts* a posteriori.
- [ ] **Threats to validity update**: il memo Phase 1 ne elencava 6; integrarli con le scoperte successive (varianza seed, portabilità asset-specifica, divergenza pattern BTC/ETH).
---
## 11. Caveat e rischi aperti
1. **Varianza seed**: con seed diversi (7, 99, 123) sullo stesso identico setup il max fitness varia di un fattore 3-4×. Qualunque metrica single-seed è statisticamente debole; finché Phase 3 non raccoglie N≥5 forward-test indipendenti, il vantaggio del prompt-mutator resta nel rumore.
2. **Sharpe OOS positivi ma bassi**: BTC `+0,50` ed ETH `+0,19` sono migliori del coin-flip ma lontani dai target retail "investment-grade" (≥ 1,0). La metodologia è validata, l'alpha catturata è modesta.
3. **`time_in_market_too_high` come red-flag chiave**: `63411199` ha CAGR +31,88% ma esposizione 90% del tempo — è leveraged-B&H camuffato, non alpha. Phase 3 deve preferire `fb63e851` (selettività 1-2%) anche se ha return assoluto minore.
4. **Dipendenza dal modello qwen-2.5-72b**: rollback Phase 2 ha dimostrato che il prompt è calibrato su questa specifica famiglia. Se il modello venisse deprecato da OpenRouter, sarebbe necessario un giro di ricalibrazione prompt → rischio di operatività.
5. **Cerbero MCP come single point of failure**: tutti i fetch OHLCV passano da lì. Da considerare un fallback (ccxt o yfinance) almeno per il paper-trading.
---
## 12. Costi cumulati
- **Phase 1 (5 run iterazione)**: $0,19.
- **Phase 1.5 nemotron**: $0,12.
- **Phase 2 + 2.5 + 2.6 + 2.7**: ≈ $3,24 cumulati su 25+ run.
- **Totale LLM progetto a oggi**: ≈ **$3,74** (DB locale).
- **Phase 3 paper-trading**: $0 incrementali per LLM (le strategie sono fisse), solo costi Cerbero (incluso nel servizio esistente).
Resta amplissimo margine rispetto al cap originale Phase 1 di $700.
---
## 13. Riferimenti
- README.md — overview e setup.
- `docs/decisions/2026-05-10-gate-phase1.md`, `docs/decisions/2026-05-11-phase1-5-nemotron-run.md`.
- `docs/reports/2026-05-10-phase1-technical-report.md`.
- `docs/superpowers/specs/2026-05-09-decisione-strategica-design.md`, `docs/superpowers/specs/2026-05-11-temporal-features-design.md`.
- `docs/superpowers/plans/2026-05-09-phase1-lean-spike.md`, `docs/superpowers/plans/2026-05-11-mutate-prompt-llm-phase-2-5.md`, `docs/superpowers/plans/2026-05-11-temporal-features.md`.
- DB locale `runs.db` per dettaglio run-by-run.
---
*Prossimo checkpoint suggerito: rivedere questo documento al termine del primo ciclo completo di Phase 3 (≥ 2 settimane di forward-test continuo) per consolidare i risultati live e decidere GO/NO-GO verso un eventuale Phase 4 (capitale reale ridotto o estensione del universe).*
+1 -1
View File
@@ -16,10 +16,10 @@ dependencies = [
"requests>=2.32",
"tenacity>=9.0",
"pyyaml>=6.0",
"streamlit>=1.40",
"plotly>=5.24",
"pyarrow>=18.0",
"nicegui>=3.11.1",
"yfinance>=1.3.0",
]
[dependency-groups]
+202
View File
@@ -0,0 +1,202 @@
"""Paper-trading runner Phase 3 — forward-test virtuale BTC + ETH.
Loop infinito (o limitato via --max-ticks) che ogni ``--poll-seconds``:
1. fetch OHLCV 1h ultime ~500 barre via Cerbero
2. per ogni strategia: compile + esegui ultimo bar
3. apply segnale al portfolio multi-asset
4. snapshot equity in DB
I bar 1h chiudono al minuto :00. Il loop riconosce un "nuovo bar chiuso"
confrontando l'ultimo timestamp del DataFrame con quello dell'iterazione
precedente. Tick consecutivi su stesso bar = hold (no double-trade).
Esempio:
uv run python scripts/run_paper_trading.py \
--name phase3-papertrade-001 \
--initial-capital 1000 \
--max-ticks 336 # 2 settimane * 24 ore
"""
from __future__ import annotations
import argparse
import time
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
PROJECT_ROOT = Path(__file__).resolve().parent.parent
@dataclass(frozen=True)
class AssetConfig:
symbol: str # es. "BTC-PERPETUAL"
strategy_file: Path
exchange: str = "deribit"
timeframe: str = "1h"
def parse_args() -> argparse.Namespace:
p = argparse.ArgumentParser(description="Paper-trading runner Phase 3")
p.add_argument("--name", default="phase3-papertrade-001")
p.add_argument("--initial-capital", type=float, default=1000.0)
p.add_argument("--fees-bp", type=float, default=5.0)
p.add_argument("--poll-seconds", type=int, default=300, help="Polling interval (5min default)")
p.add_argument("--max-ticks", type=int, default=0, help="0 = infinito; per smoke test usa 1")
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",
)
return p.parse_args()
def load_assets(strategies_dir: Path) -> list[AssetConfig]:
btc_files = sorted(strategies_dir.glob("btc_*.json"))
eth_files = sorted(strategies_dir.glob("eth_*.json"))
if not btc_files or not eth_files:
raise FileNotFoundError(
f"Expected btc_*.json and eth_*.json in {strategies_dir}"
)
return [
AssetConfig(symbol="BTC-PERPETUAL", strategy_file=btc_files[0]),
AssetConfig(symbol="ETH-PERPETUAL", strategy_file=eth_files[0]),
]
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
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)
assets = load_assets(Path(args.strategies_dir))
executors: list[PaperExecutor] = [
PaperExecutor(strategy_json_path=a.strategy_file, symbol=a.symbol) for a in assets
]
print(f"Loaded {len(assets)} strategies:")
for a, ex in zip(assets, executors, strict=True):
print(f" {a.symbol}: {a.strategy_file.name} -> {len(ex._strategy.rules)} rules")
portfolio = Portfolio(
initial_capital=args.initial_capital,
fees_bp=args.fees_bp,
n_sleeves=len(assets),
)
repo = PaperRepository(settings.db_path)
config = {
"assets": [
{"symbol": a.symbol, "strategy": a.strategy_file.name, "exchange": a.exchange}
for a in assets
],
"fees_bp": args.fees_bp,
"poll_seconds": args.poll_seconds,
"lookback_bars": args.lookback_bars,
}
run_id = repo.create_run(
name=args.name, initial_capital=args.initial_capital, config=config
)
print(f"Paper run started: {run_id} ({args.name})")
print(f" initial_capital=${args.initial_capital:.2f}, sleeve=${portfolio.sleeve_capital:.2f}")
tick_count = 0
last_bars_seen: dict[str, datetime] = {}
try:
while args.max_ticks == 0 or tick_count < args.max_ticks:
now = datetime.now(UTC)
last_prices: dict[str, float] = {}
for asset, executor in zip(assets, executors, strict=True):
# fetch OHLCV most recent lookback bars
end = now.replace(minute=0, second=0, microsecond=0)
start = end - timedelta(hours=args.lookback_bars + 1)
req = OHLCVRequest(
symbol=asset.symbol,
timeframe=asset.timeframe,
start=start,
end=end,
exchange=asset.exchange,
)
# bypass cache for live data
try:
ohlcv = loader._fetch(req) # noqa: SLF001
except Exception as e: # noqa: BLE001
print(f"[{now.isoformat()}] {asset.symbol} fetch FAIL: {e}")
continue
if len(ohlcv) < 10:
print(f"[{now.isoformat()}] {asset.symbol} too few bars ({len(ohlcv)})")
continue
bar_ts = ohlcv.index[-1]
last_bar_dt = bar_ts.to_pydatetime() if hasattr(bar_ts, "to_pydatetime") else bar_ts
# skip se barra gia' processata in questo tick
if last_bars_seen.get(asset.symbol) == last_bar_dt:
last_prices[asset.symbol] = float(ohlcv["close"].iloc[-1])
continue
last_bars_seen[asset.symbol] = last_bar_dt
result = executor.execute_tick(portfolio, ohlcv, now)
repo.save_tick(run_id, result)
last_prices[asset.symbol] = result.close_price
if result.action_taken != "hold":
pnl_str = (
f"pnl=${result.trade.net_pnl:+.2f}" if result.trade else ""
)
print(
f"[{now.isoformat()}] {asset.symbol} bar={last_bar_dt} "
f"close={result.close_price:.2f} signal={result.signal.value} "
f"action={result.action_taken} {pnl_str}"
)
repo.sync_open_positions(run_id, portfolio)
eq, pos_val = portfolio.equity(last_prices)
repo.save_equity_snapshot(run_id, now, eq, portfolio.cash, pos_val)
tick_count += 1
print(
f"[{now.isoformat()}] tick={tick_count} "
f"equity=${eq:.2f} cash=${portfolio.cash:.2f} pos_val=${pos_val:.2f} "
f"open={list(portfolio.positions.keys())}"
)
if args.max_ticks > 0 and tick_count >= args.max_ticks:
break
time.sleep(args.poll_seconds)
repo.stop_run(run_id, status="completed")
except KeyboardInterrupt:
print("\nInterrupted by user")
repo.stop_run(run_id, status="interrupted")
except Exception as e: # noqa: BLE001
print(f"Run failed: {e}")
repo.stop_run(run_id, status="failed")
raise
print(f"Paper run {run_id} stopped after {tick_count} ticks")
print(f"Final equity: ${portfolio.equity({})[0]:.2f}")
print(f"Trades closed: {len(portfolio.closed_trades)}")
if __name__ == "__main__":
main()
+17
View File
@@ -81,6 +81,21 @@ def parse_args() -> argparse.Namespace:
default=5,
help="Walk-forward: quanti top genomi rivalutare OOS (default 5)",
)
p.add_argument(
"--eval-oos-during-loop",
action="store_true",
help=(
"Multi-objective: eval ogni genome anche su test_ohlcv durante "
"il loop e usa combined = alpha*IS + (1-alpha)*OOS per selection. "
"Richiede --wfa-train-split. 2x costo backtest engine."
),
)
p.add_argument(
"--fitness-combined-alpha",
type=float,
default=0.5,
help="Multi-objective: peso IS (1-alpha = OOS). 1.0=solo IS, 0.5=bilanciato, 0.0=solo OOS",
)
return p.parse_args()
@@ -144,6 +159,8 @@ def main() -> None:
fitness_adversarial_soft_penalty=args.fitness_soft_penalty,
wfa_train_split=args.wfa_train_split,
wfa_top_k=args.wfa_top_k,
eval_oos_during_loop=args.eval_oos_during_loop,
fitness_combined_alpha=args.fitness_combined_alpha,
)
run_id = run_phase1(cfg, ohlcv=ohlcv, llm=llm)
+19
View File
@@ -105,6 +105,25 @@ Esempi di gating temporale:
Leaf - letterale numerico:
{{"kind": "literal", "value": 70.0}}
PATTERN GUIDANCE (oltre agli indicatori, considera forma delle curve e ripetibilità):
Forme di curva:
- Trend ascendente: SMA(short) > SMA(long) E close > SMA(short)
- Trend discendente: SMA(short) < SMA(long) E close < SMA(short)
- Compressione di volatilità (pre-breakout): ATR(N) < soglia bassa
- Espansione di volatilità: ATR(N) > ATR(N*2) (vol corta > vol lunga)
- Mean reversion strutturale: |close - SMA(long)| eccessivo → reversal atteso
Ripetibilità dell'andamento:
- Eventi crossover/crossunder ricorrenti (golden/death cross, RSI cross zone)
- Pattern intra-day: usa 'hour' per sfruttare orari di forte volatilità ricorrente
- Pattern settimanali: usa 'dow' o 'is_weekend' per cicli mercato
- Doppio top approx: RSI > 70 + crossunder RSI 70 (1° picco), poi entro N bar
nuovo crossover RSI 70 a livello close simile → entry short
- Range breakout: close > SMA(N) con SMA(short) > SMA(long) (compressione + spinta)
Cerca pattern che si REPLICANO nei dati storici, non singoli eventi rari.
VINCOLI
- Gli indicator NON sono annidabili: 'params' accetta solo numeri, mai altri nodi.
- Le regole sono valutate in ordine; la prima che matcha vince per ogni timestamp.
+13
View File
@@ -0,0 +1,13 @@
"""Backtest layer: order/position/trade dataclasses + engine."""
from .engine import BacktestEngine, BacktestResult
from .orders import Order, Position, Side, Trade
__all__ = [
"BacktestEngine",
"BacktestResult",
"Order",
"Position",
"Side",
"Trade",
]
+5
View File
@@ -0,0 +1,5 @@
"""Cerbero data-provider HTTP client."""
from .client import CerberoClient
__all__ = ["CerberoClient"]
-590
View File
@@ -1,590 +0,0 @@
"""Aquarium 2D visualization helpers.
Builds fish records (with full genome attributes + ancestor lineage) and
renders a self-contained HTML/JS canvas animation, embeddable in Streamlit
via ``streamlit.components.v1.html``. Includes a click handler that opens
an info panel showing genome details and BFS ancestor levels.
"""
from __future__ import annotations
import json
import math
from typing import Any
import pandas as pd # type: ignore[import-untyped]
# Color palette per cognitive style. Default fallback for unknown styles is grey.
STYLE_COLORS: dict[str, str] = {
"physicist": "#4cc9f0",
"biologist": "#52b788",
"historian": "#e76f51",
"meteorologist": "#ffd166",
"ecologist": "#a78bfa",
"engineer": "#fb6f92",
}
DEFAULT_COLOR: str = "#94a3b8"
def _is_nan(v: Any) -> bool:
try:
return bool(pd.isna(v))
except (TypeError, ValueError):
return False
def _safe_float(v: Any, default: float = 0.0) -> float:
if v is None or _is_nan(v):
return default
try:
return float(v)
except (TypeError, ValueError):
return default
def _safe_int(v: Any, default: int = 0) -> int:
if v is None or _is_nan(v):
return default
try:
return int(v)
except (TypeError, ValueError):
return default
def _safe_str(v: Any, default: str = "") -> str:
if v is None or _is_nan(v):
return default
return str(v)
def _safe_list(v: Any) -> list[Any]:
if v is None:
return []
if isinstance(v, list):
return list(v)
# pandas may store python lists in object cells; if it's e.g. a numpy array,
# falling back to list() is fine. NaN scalar is excluded by _is_nan.
if _is_nan(v):
return []
try:
return list(v)
except TypeError:
return []
def build_lineage_index(
genomes_df: pd.DataFrame, evals_df: pd.DataFrame
) -> dict[str, dict[str, Any]]:
"""Build ``{genome_id: attrs}`` for every genome in the run.
``genomes_df`` must come from ``genomes_df(repo, run_id)`` (no gen filter):
columns include ``id``, ``generation_idx``, ``system_prompt``,
``feature_access``, ``temperature``, ``top_p``, ``model_tier``,
``lookback_window``, ``cognitive_style``, ``parent_ids``, ``generation``.
``evals_df`` must come from ``evaluations_df(repo, run_id)``: columns
include ``genome_id``, ``fitness``, ``dsr``, ``sharpe``, ``max_dd``,
``n_trades``.
"""
if genomes_df.empty:
return {}
if evals_df is None or evals_df.empty:
merged = genomes_df.copy()
for col in ("fitness", "dsr", "sharpe", "max_dd", "n_trades"):
if col not in merged.columns:
merged[col] = 0.0 if col != "n_trades" else 0
else:
merged = genomes_df.merge(
evals_df,
left_on="id",
right_on="genome_id",
how="left",
suffixes=("", "_eval"),
)
index: dict[str, dict[str, Any]] = {}
for _, row in merged.iterrows():
gid = _safe_str(row.get("id"))
if not gid:
continue
# ``generation`` is the genome's evolutionary generation (from payload).
# If absent, fall back to ``generation_idx`` (column added by the
# repository). Defensive: both may be missing in edge cases.
gen_val: Any = row.get("generation")
if gen_val is None or _is_nan(gen_val):
gen_val = row.get("generation_idx", 0)
index[gid] = {
"id": gid,
"generation": _safe_int(gen_val, 0),
"fitness": _safe_float(row.get("fitness"), 0.0),
"dsr": _safe_float(row.get("dsr"), 0.0),
"sharpe": _safe_float(row.get("sharpe"), 0.0),
"max_dd": _safe_float(row.get("max_dd"), 0.0),
"n_trades": _safe_int(row.get("n_trades"), 0),
"cognitive_style": _safe_str(row.get("cognitive_style"), ""),
"system_prompt": _safe_str(row.get("system_prompt"), ""),
"temperature": _safe_float(row.get("temperature"), 0.0),
"lookback_window": _safe_int(row.get("lookback_window"), 0),
"feature_access": _safe_list(row.get("feature_access")),
"model_tier": _safe_str(row.get("model_tier"), ""),
"parent_ids": _safe_list(row.get("parent_ids")),
}
return index
def trace_ancestors(
genome_id: str,
lineage_index: dict[str, dict[str, Any]],
max_levels: int = 5,
) -> list[list[dict[str, Any]]]:
"""BFS over ``parent_ids`` returning levels of ancestors.
``levels[0]`` = direct parents, ``levels[1]`` = grandparents, etc. Each
entry is a small dict (no ``system_prompt``, to keep JSON payload light):
``{id, generation, fitness, cognitive_style}``. Cycles are guarded via a
``seen`` set; missing parents (not in this run) are stubbed with sentinel
values so the lineage display still renders the relationship.
"""
levels: list[list[dict[str, Any]]] = []
root = lineage_index.get(genome_id, {})
current_ids: list[str] = list(root.get("parent_ids", []))
seen: set[str] = {genome_id}
for _ in range(max_levels):
if not current_ids:
break
level_entries: list[dict[str, Any]] = []
next_ids: list[str] = []
for pid in current_ids:
if pid in seen:
continue
seen.add(pid)
entry = lineage_index.get(pid)
if entry is None:
level_entries.append(
{
"id": pid,
"generation": -1,
"fitness": 0.0,
"cognitive_style": "",
}
)
continue
level_entries.append(
{
"id": entry["id"],
"generation": entry["generation"],
"fitness": entry["fitness"],
"cognitive_style": entry["cognitive_style"],
}
)
next_ids.extend(entry.get("parent_ids", []))
if not level_entries:
break
levels.append(level_entries)
current_ids = next_ids
return levels
def build_fish_dataset(
active_df: pd.DataFrame,
lineage_index: dict[str, dict[str, Any]] | None = None,
max_lineage_levels: int = 5,
) -> list[dict[str, Any]]:
"""Build full fish records for each active genome.
For every row in ``active_df`` the matching entry in ``lineage_index`` is
looked up by ``genome_id`` (or ``id``) and attached together with the BFS
ancestor levels. Rows whose id is not in the index are skipped.
Backward-compat: if ``lineage_index`` is ``None`` (legacy call site, e.g.
test fixtures with simple merged DataFrames) we synthesize a minimal
lineage from ``active_df`` itself so the function still returns useful
fish records.
"""
if active_df.empty:
return []
if lineage_index is None:
# Legacy path: build a tiny index from the active df only.
synth: dict[str, dict[str, Any]] = {}
for _, row in active_df.iterrows():
gid = _safe_str(row.get("genome_id") or row.get("id"))
if not gid:
continue
fitness_val = _safe_float(row.get("fitness"), float("nan"))
if math.isnan(fitness_val):
continue
synth[gid] = {
"id": gid,
"generation": _safe_int(row.get("generation"), 0),
"fitness": fitness_val,
"dsr": _safe_float(row.get("dsr"), 0.0),
"sharpe": _safe_float(row.get("sharpe"), 0.0),
"max_dd": _safe_float(row.get("max_dd"), 0.0),
"n_trades": _safe_int(row.get("n_trades"), 0),
"cognitive_style": _safe_str(row.get("cognitive_style"), "unknown"),
"system_prompt": _safe_str(row.get("system_prompt"), ""),
"temperature": _safe_float(row.get("temperature"), 0.0),
"lookback_window": _safe_int(row.get("lookback_window"), 0),
"feature_access": _safe_list(row.get("feature_access")),
"model_tier": _safe_str(row.get("model_tier"), ""),
"parent_ids": _safe_list(row.get("parent_ids")),
}
lineage_index = synth
fish: list[dict[str, Any]] = []
for _, row in active_df.iterrows():
gid = _safe_str(row.get("genome_id") or row.get("id"))
if not gid:
continue
attrs = lineage_index.get(gid)
if attrs is None:
continue
if math.isnan(attrs.get("fitness", 0.0)):
continue
ancestors = trace_ancestors(gid, lineage_index, max_lineage_levels)
record = {**attrs, "ancestors": ancestors}
fish.append(record)
return fish
def build_aquarium_html(
fish: list[dict[str, Any]],
canvas_w: int = 1000,
canvas_h: int = 600,
) -> str:
"""Build the self-contained HTML/JS string for the aquarium canvas.
The output embeds a click handler: tapping a fish opens an info panel
(top-right of the canvas) showing its genome attributes and BFS ancestor
levels. Labels are no longer rendered on the canvas itself.
"""
fish_json = json.dumps(fish)
palette_json = json.dumps(STYLE_COLORS)
default_color = DEFAULT_COLOR
# All braces inside <style>/<script> are escaped to literals using {{ }}
# so we can use Python f-string substitution for the few JSON payloads.
return f"""
<div style="position:relative;width:100%;height:{canvas_h}px;">
<canvas id="aquarium" width="{canvas_w}" height="{canvas_h}"
style="width:100%;height:{canvas_h}px;border-radius:12px;
background:linear-gradient(180deg,#0a2540 0%,#1a4d80 100%);
display:block;cursor:pointer;"></canvas>
<div id="fish-info-panel" style="
position:absolute;
top:12px;
right:12px;
width:340px;
max-height:580px;
overflow-y:auto;
background:rgba(8,16,32,0.92);
color:#e2e8f0;
border-radius:10px;
padding:14px 16px;
font-family:system-ui,-apple-system,sans-serif;
font-size:12px;
line-height:1.5;
border:1px solid rgba(255,255,255,0.1);
backdrop-filter:blur(6px);
-webkit-backdrop-filter:blur(6px);
display:none;
z-index:10;
">
<div id="fish-info-content"></div>
<button id="fish-info-close" style="
position:absolute;top:8px;right:10px;
background:transparent;color:#94a3b8;border:none;
cursor:pointer;font-size:16px;
">&times;</button>
</div>
</div>
<script>
(function() {{
const FISH_DATA = {fish_json};
const STYLE_COLORS = {palette_json};
const DEFAULT_COLOR = {json.dumps(default_color)};
const canvas = document.getElementById('aquarium');
if (!canvas) return;
const ctx = canvas.getContext('2d');
const W = canvas.width;
const H = canvas.height;
const panel = document.getElementById('fish-info-panel');
const panelContent = document.getElementById('fish-info-content');
const closeBtn = document.getElementById('fish-info-close');
if (closeBtn) {{
closeBtn.addEventListener('click', function() {{
panel.style.display = 'none';
}});
}}
// Normalize fitness for sizing.
let maxFit = 0.0;
for (const f of FISH_DATA) {{
if (typeof f.fitness === 'number' && f.fitness > maxFit) maxFit = f.fitness;
}}
function lerp(a, b, t) {{ return a + (b - a) * t; }}
function radiusFor(fitness) {{
if (maxFit <= 0) return 8;
const t = Math.max(0.05, Math.min(1.0, fitness / maxFit));
return lerp(8, 40, t);
}}
function colorFor(style) {{
return STYLE_COLORS[style] || DEFAULT_COLOR;
}}
// Init fish state. Each entry keeps a reference to the original data dict
// so the click handler can show full attributes + ancestors.
const fishState = FISH_DATA.map(function(f, idx) {{
const r = radiusFor(f.fitness);
return {{
data: f,
color: colorFor(f.cognitive_style),
radius: r,
x: Math.random() * (W - 2 * r) + r,
y: Math.random() * (H - 2 * r) + r,
vx: (Math.random() - 0.5) * 1.5,
vy: (Math.random() - 0.5) * 1.0,
rank: idx,
}};
}});
// Bubbles for ambience.
const N_BUBBLES = 25;
const bubbles = Array.from({{length: N_BUBBLES}}, function() {{ return {{
x: Math.random() * W,
y: Math.random() * H,
r: 1 + Math.random() * 3,
vy: 0.3 + Math.random() * 0.7,
}}; }});
function drawBubble(b) {{
ctx.beginPath();
ctx.arc(b.x, b.y, b.r, 0, Math.PI * 2);
ctx.fillStyle = 'rgba(255,255,255,0.18)';
ctx.fill();
}}
function updateBubble(b) {{
b.y -= b.vy;
if (b.y < -10) {{
b.y = H + 5;
b.x = Math.random() * W;
}}
}}
function drawFish(f) {{
const facingLeft = f.vx < 0;
ctx.save();
ctx.translate(f.x, f.y);
if (facingLeft) ctx.scale(-1, 1);
// Halo for top-3 fish.
if (f.rank < 3) {{
const grad = ctx.createRadialGradient(0, 0, f.radius * 0.5, 0, 0, f.radius * 2.0);
grad.addColorStop(0, f.color + 'aa');
grad.addColorStop(1, f.color + '00');
ctx.fillStyle = grad;
ctx.beginPath();
ctx.arc(0, 0, f.radius * 2.0, 0, Math.PI * 2);
ctx.fill();
}}
// Body (ellipse).
ctx.fillStyle = f.color;
ctx.beginPath();
ctx.ellipse(0, 0, f.radius, f.radius * 0.6, 0, 0, Math.PI * 2);
ctx.fill();
// Tail.
ctx.beginPath();
ctx.moveTo(-f.radius, 0);
ctx.lineTo(-f.radius * 1.6, -f.radius * 0.5);
ctx.lineTo(-f.radius * 1.6, f.radius * 0.5);
ctx.closePath();
ctx.fill();
// Eye.
ctx.fillStyle = '#ffffff';
ctx.beginPath();
ctx.arc(f.radius * 0.45, -f.radius * 0.15, Math.max(1.5, f.radius * 0.12), 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#1a1a1a';
ctx.beginPath();
ctx.arc(f.radius * 0.50, -f.radius * 0.15, Math.max(0.8, f.radius * 0.06), 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}}
function updateFish(f) {{
f.vx += (Math.random() - 0.5) * 0.05;
f.vy += (Math.random() - 0.5) * 0.05;
const speed = Math.hypot(f.vx, f.vy);
const maxSpeed = 1.5;
if (speed > maxSpeed) {{
f.vx = (f.vx / speed) * maxSpeed;
f.vy = (f.vy / speed) * maxSpeed;
}}
f.x += f.vx;
f.y += f.vy;
if (f.x < f.radius) {{ f.x = f.radius; f.vx = -f.vx; }}
if (f.x > W - f.radius) {{ f.x = W - f.radius; f.vx = -f.vx; }}
if (f.y < f.radius) {{ f.y = f.radius; f.vy = -f.vy; }}
if (f.y > H - f.radius) {{ f.y = H - f.radius; f.vy = -f.vy; }}
}}
function frame() {{
ctx.clearRect(0, 0, W, H);
ctx.strokeStyle = 'rgba(255,255,255,0.04)';
ctx.lineWidth = 1;
for (let i = 0; i < 6; i++) {{
const y = (H / 6) * i + (Date.now() / 50 % (H / 6));
ctx.beginPath();
ctx.moveTo(0, y);
ctx.lineTo(W, y);
ctx.stroke();
}}
for (const b of bubbles) {{
updateBubble(b);
drawBubble(b);
}}
for (const f of fishState) {{
updateFish(f);
drawFish(f);
}}
requestAnimationFrame(frame);
}}
// CLICK HANDLER: hit-test in canvas pixel space (account for CSS scaling).
canvas.addEventListener('click', function(e) {{
const rect = canvas.getBoundingClientRect();
const scaleX = canvas.width / rect.width;
const scaleY = canvas.height / rect.height;
const cx = (e.clientX - rect.left) * scaleX;
const cy = (e.clientY - rect.top) * scaleY;
let best = null;
let bestDist = Infinity;
for (const f of fishState) {{
const dx = cx - f.x;
const dy = cy - f.y;
const d = Math.sqrt(dx * dx + dy * dy);
const hit = Math.max(f.radius + 6, 14);
if (d < hit && d < bestDist) {{
best = f;
bestDist = d;
}}
}}
if (best) showFishInfo(best);
}});
const ROW_STYLE = 'display:flex;justify-content:space-between;'
+ 'padding:2px 0;border-bottom:1px solid rgba(255,255,255,0.05);';
const PROMPT_STYLE = 'margin-top:10px;padding:8px;'
+ 'background:rgba(255,255,255,0.04);border-radius:6px;'
+ 'font-size:11px;font-style:italic;color:#cbd5e1;';
const ANC_HEAD_STYLE = 'margin:14px 0 6px 0;color:#94a3b8;'
+ 'text-transform:uppercase;font-size:10px;letter-spacing:1px;';
const ANC_ROW_STYLE = 'display:flex;align-items:center;padding:4px 6px;'
+ 'margin-bottom:2px;background:rgba(255,255,255,0.03);'
+ 'border-radius:4px;border-left:3px solid ';
const NO_ANC_STYLE = 'margin-top:10px;font-size:10px;color:#64748b;';
const DASH = '\\u2014';
function metricRow(label, value) {{
return '<div style="' + ROW_STYLE + '">'
+ '<span style="color:#94a3b8;">' + label + '</span>'
+ '<span style="color:#e2e8f0;">' + value + '</span></div>';
}}
function escapeHtml(s) {{
const div = document.createElement('div');
div.appendChild(document.createTextNode(String(s)));
return div.innerHTML;
}}
function fmt(v, dp) {{
if (v === null || v === undefined || typeof v !== 'number' || isNaN(v)) {{
return DASH;
}}
return v.toFixed(dp);
}}
function showFishInfo(fish) {{
const data = fish.data;
const styleColor = STYLE_COLORS[data.cognitive_style] || DEFAULT_COLOR;
let html = '';
const idShort = String(data.id || '').slice(0, 8);
html += '<h4 style="margin:0 0 10px 0;color:' + styleColor + ';">';
html += escapeHtml(idShort)
+ ' <span style="color:#94a3b8;font-weight:normal;font-size:11px;">'
+ 'gen ' + escapeHtml(data.generation) + '</span>';
html += '</h4>';
html += metricRow('fitness', fmt(data.fitness, 3));
html += metricRow('DSR', fmt(data.dsr, 3));
html += metricRow('Sharpe', fmt(data.sharpe, 3));
html += metricRow('max DD', fmt(data.max_dd, 3));
const trades = data.n_trades == null ? 0 : data.n_trades;
html += metricRow('trades', escapeHtml(trades));
html += metricRow('style', escapeHtml(data.cognitive_style || DASH));
html += metricRow('tier', escapeHtml(data.model_tier || DASH));
html += metricRow('temp', fmt(data.temperature, 2));
const lookback = data.lookback_window == null ? DASH : data.lookback_window;
html += metricRow('lookback', escapeHtml(lookback));
const feats = (data.feature_access || []).join(', ');
html += metricRow('features', escapeHtml(feats || DASH));
if (data.system_prompt) {{
html += '<div style="' + PROMPT_STYLE + '">'
+ escapeHtml(data.system_prompt) + '</div>';
}}
if (data.ancestors && data.ancestors.length > 0) {{
html += '<h5 style="' + ANC_HEAD_STYLE + '">Discendenza</h5>';
data.ancestors.forEach(function(level, idx) {{
html += '<div style="margin-bottom:8px;">';
html += '<div style="font-size:10px;color:#64748b;margin-bottom:4px;">'
+ 'Gen N\\u2212' + (idx + 1) + '</div>';
level.forEach(function(ancestor) {{
const c = STYLE_COLORS[ancestor.cognitive_style] || DEFAULT_COLOR;
const aShort = String(ancestor.id || '').slice(0, 8);
html += '<div style="' + ANC_ROW_STYLE + c + ';">';
html += '<code style="color:' + c + ';font-size:10px;">'
+ escapeHtml(aShort) + '</code>';
const af = ancestor.fitness;
const fitTxt = (typeof af === 'number' && !isNaN(af))
? af.toFixed(2) : DASH;
html += '<span style="margin-left:auto;color:#94a3b8;font-size:10px;">'
+ 'fit ' + fitTxt + '</span>';
html += '</div>';
}});
html += '</div>';
}});
}} else {{
html += '<div style="' + NO_ANC_STYLE + '">'
+ 'Genoma di prima generazione (no ancestors)</div>';
}}
panelContent.innerHTML = html;
panel.style.display = 'block';
}}
if (fishState.length === 0) {{
ctx.fillStyle = 'rgba(255,255,255,0.7)';
ctx.font = '16px sans-serif';
ctx.textAlign = 'center';
ctx.fillText('Acquario vuoto: nessun genoma da mostrare.', W / 2, H / 2);
}} else {{
requestAnimationFrame(frame);
}}
}})();
</script>
"""
+119
View File
@@ -1,6 +1,7 @@
from __future__ import annotations
import json
import sqlite3
from pathlib import Path
from typing import Any
@@ -52,3 +53,121 @@ def genomes_df(
}
)
return pd.DataFrame(flat)
def _paper_conn(db_path: str | Path) -> sqlite3.Connection:
conn = sqlite3.connect(str(db_path))
conn.row_factory = sqlite3.Row
return conn
def paper_runs_df(db_path: str | Path) -> pd.DataFrame:
with _paper_conn(db_path) as conn:
rows = conn.execute(
"SELECT id, name, started_at, stopped_at, status, initial_capital, config_json "
"FROM paper_trading_runs ORDER BY started_at DESC"
).fetchall()
return pd.DataFrame([dict(r) for r in rows])
def paper_equity_df(db_path: str | Path, run_id: str) -> pd.DataFrame:
with _paper_conn(db_path) as conn:
rows = conn.execute(
"SELECT ts, equity, cash, positions_value FROM paper_trading_equity "
"WHERE paper_run_id=? ORDER BY ts ASC",
(run_id,),
).fetchall()
return pd.DataFrame([dict(r) for r in rows])
def paper_positions_df(db_path: str | Path, run_id: str) -> pd.DataFrame:
with _paper_conn(db_path) as conn:
rows = conn.execute(
"SELECT symbol, side, qty, entry_price, entry_ts "
"FROM paper_trading_positions WHERE paper_run_id=? ORDER BY symbol",
(run_id,),
).fetchall()
return pd.DataFrame([dict(r) for r in rows])
def paper_trades_df(db_path: str | Path, run_id: str, limit: int = 100) -> pd.DataFrame:
with _paper_conn(db_path) as conn:
rows = conn.execute(
"SELECT symbol, side, qty, entry_price, exit_price, entry_ts, exit_ts, pnl, fees "
"FROM paper_trading_trades WHERE paper_run_id=? ORDER BY exit_ts DESC LIMIT ?",
(run_id, limit),
).fetchall()
return pd.DataFrame([dict(r) for r in rows])
def paper_ticks_df(db_path: str | Path, run_id: str, limit: int = 50) -> pd.DataFrame:
with _paper_conn(db_path) as conn:
rows = conn.execute(
"SELECT ts, bar_ts, symbol, close_price, signal, action_taken "
"FROM paper_trading_ticks WHERE paper_run_id=? ORDER BY ts DESC LIMIT ?",
(run_id, limit),
).fetchall()
return pd.DataFrame([dict(r) for r in rows])
def paper_run_summary(db_path: str | Path, run_id: str) -> dict[str, Any]:
"""Aggrega metriche sintetiche per la pagina paper trading."""
with _paper_conn(db_path) as conn:
run = conn.execute(
"SELECT id, name, started_at, stopped_at, status, initial_capital, config_json "
"FROM paper_trading_runs WHERE id=?",
(run_id,),
).fetchone()
if run is None:
return {}
run = dict(run)
eq_row = conn.execute(
"SELECT equity, cash, positions_value, ts FROM paper_trading_equity "
"WHERE paper_run_id=? ORDER BY ts DESC LIMIT 1",
(run_id,),
).fetchone()
trades_agg = conn.execute(
"SELECT COUNT(*) AS n, COALESCE(SUM(pnl),0) AS sum_pnl, "
"COALESCE(SUM(fees),0) AS sum_fees FROM paper_trading_trades "
"WHERE paper_run_id=?",
(run_id,),
).fetchone()
tick_agg = conn.execute(
"SELECT COUNT(*) AS n, MAX(ts) AS last_ts FROM paper_trading_ticks "
"WHERE paper_run_id=?",
(run_id,),
).fetchone()
positions_n = conn.execute(
"SELECT COUNT(*) AS n FROM paper_trading_positions WHERE paper_run_id=?",
(run_id,),
).fetchone()["n"]
initial = float(run["initial_capital"])
current_equity = float(eq_row["equity"]) if eq_row is not None else initial
pnl_pct = (current_equity - initial) / initial * 100.0 if initial else 0.0
return {
"id": run["id"],
"name": run["name"],
"status": run["status"],
"started_at": run["started_at"],
"stopped_at": run["stopped_at"],
"initial_capital": initial,
"config": json.loads(run["config_json"]),
"current_equity": current_equity,
"current_cash": float(eq_row["cash"]) if eq_row is not None else initial,
"current_positions_value": float(eq_row["positions_value"]) if eq_row is not None else 0.0,
"last_equity_ts": eq_row["ts"] if eq_row is not None else None,
"pnl_abs": current_equity - initial,
"pnl_pct": pnl_pct,
"n_trades": int(trades_agg["n"]),
"trades_pnl": float(trades_agg["sum_pnl"]),
"trades_fees": float(trades_agg["sum_fees"]),
"n_ticks": int(tick_agg["n"]),
"last_tick_ts": tick_agg["last_ts"],
"n_open_positions": int(positions_n),
}
+219 -3
View File
@@ -24,6 +24,7 @@ import os
from pathlib import Path
from typing import Any
import pandas as pd # type: ignore[import-untyped]
import plotly.graph_objects as go # type: ignore[import-untyped]
from nicegui import app, ui
@@ -34,6 +35,12 @@ from multi_swarm.dashboard.data import (
get_repo,
get_run_overview,
list_runs_df,
paper_equity_df,
paper_positions_df,
paper_run_summary,
paper_runs_df,
paper_ticks_df,
paper_trades_df,
)
DB_PATH = os.environ.get("DB_PATH", "./runs.db")
@@ -370,6 +377,7 @@ def _build_header(active: str) -> None:
("/", "Overview"),
("/convergence", "Convergence"),
("/genomes", "Genomes"),
("/paper", "Paper"),
):
cls = "nav-link active" if active == path else "nav-link"
ui.link(label, path).classes(cls)
@@ -839,9 +847,10 @@ def genomes() -> None:
_render_detail(match.iloc[0].to_dict())
def on_row_selected(e: Any) -> None:
if not e.selection:
rows = (e.args or {}).get("rows") or []
if not rows:
return
full_id = e.selection[0].get("_full_id")
full_id = rows[0].get("_full_id")
if not full_id:
return
state["selected_gid"] = full_id
@@ -864,11 +873,218 @@ def genomes() -> None:
refresh()
def _paper_runs_options(only_running: bool = False) -> dict[str, str]:
runs = paper_runs_df(DB_PATH)
if runs.empty:
return {}
if only_running:
runs = runs[runs["status"] == "running"]
if runs.empty:
return {}
return {
row["id"]: f"{row['name']}{row['status']} ({row['started_at'][:16]})"
for _, row in runs.iterrows()
}
def _paper_equity_figure(eq_df: Any, initial_capital: float) -> go.Figure:
fig = go.Figure()
if eq_df is not None and not eq_df.empty:
ts = pd.to_datetime(eq_df["ts"])
fig.add_trace(
go.Scatter(
x=ts,
y=eq_df["equity"],
mode="lines",
line={"color": COLOR_PRIMARY, "width": 2},
name="equity",
)
)
fig.add_hline(
y=initial_capital,
line={"color": COLOR_TEXT_MUTED, "width": 1, "dash": "dash"},
annotation_text=f"initial ${initial_capital:.0f}",
annotation_position="bottom right",
annotation_font_color=COLOR_TEXT_MUTED,
)
fig.update_layout(
title=None,
paper_bgcolor=COLOR_SURFACE,
plot_bgcolor=COLOR_SURFACE,
font={"color": COLOR_TEXT, "family": "Inter"},
xaxis={"gridcolor": COLOR_SURFACE_2, "title": None},
yaxis={"gridcolor": COLOR_SURFACE_2, "title": "Equity ($)"},
margin={"l": 60, "r": 20, "t": 10, "b": 40},
height=320,
showlegend=False,
)
return fig
@ui.page("/paper")
def paper() -> None:
_apply_theme()
_build_header(active="/paper")
options = _paper_runs_options()
if not options:
ui.label("Nessuna paper-trading run nel database.").classes("text-h5")
return
state: dict[str, Any] = {"run_id": next(iter(options))}
with ui.row().classes("w-full items-center gap-4 q-mb-md"):
select = ui.select(options=options, value=state["run_id"], label="Paper run").classes(
"flex-grow"
)
status_badge = ui.badge("", color="primary").classes("text-body1 q-pa-sm")
ui.button("🔄 Refresh", on_click=lambda: refresh()).props("outline color=primary")
with ui.row().classes("w-full gap-4"):
with ui.card().classes("flex-grow metric-card accent-cyan"):
ui.label("Equity").classes("text-caption")
equity_lbl = ui.label("").classes("text-h4")
with ui.card().classes("flex-grow metric-card accent-purple"):
ui.label("P/L cumulato").classes("text-caption")
pnl_lbl = ui.label("").classes("text-h4")
with ui.card().classes("flex-grow metric-card accent-amber"):
ui.label("Trades chiusi").classes("text-caption")
trades_lbl = ui.label("").classes("text-h4")
with ui.card().classes("flex-grow metric-card accent-green"):
ui.label("Open / Tick").classes("text-caption")
ticks_lbl = ui.label("").classes("text-h4")
with ui.row().classes("w-full gap-4 q-mt-md"):
started_lbl = ui.label("Started: —")
last_tick_lbl = ui.label("Last tick: —")
cash_lbl = ui.label("Cash: —")
ui.separator()
ui.label("Equity curve").classes("text-subtitle1 q-mt-md")
equity_plot = ui.plotly(_paper_equity_figure(None, 0.0)).classes("w-full")
ui.separator()
ui.label("Open positions").classes("text-subtitle1 q-mt-md")
positions_table = ui.table(
columns=[
{"name": "symbol", "label": "symbol", "field": "symbol"},
{"name": "side", "label": "side", "field": "side"},
{"name": "qty", "label": "qty", "field": "qty"},
{"name": "entry_price", "label": "entry", "field": "entry_price"},
{"name": "entry_ts", "label": "entry ts", "field": "entry_ts"},
],
rows=[],
row_key="symbol",
).classes("w-full")
ui.separator()
ui.label("Ultimi 30 tick").classes("text-subtitle1 q-mt-md")
ticks_table = ui.table(
columns=[
{"name": "ts", "label": "ts", "field": "ts"},
{"name": "symbol", "label": "symbol", "field": "symbol"},
{"name": "bar_ts", "label": "bar", "field": "bar_ts"},
{"name": "close_price", "label": "close", "field": "close_price"},
{"name": "signal", "label": "signal", "field": "signal"},
{"name": "action_taken", "label": "action", "field": "action_taken"},
],
rows=[],
row_key="ts",
).classes("w-full")
ui.separator()
ui.label("Trades chiusi (ultimi 50)").classes("text-subtitle1 q-mt-md")
trades_table = ui.table(
columns=[
{"name": "exit_ts", "label": "exit ts", "field": "exit_ts"},
{"name": "symbol", "label": "symbol", "field": "symbol"},
{"name": "side", "label": "side", "field": "side"},
{"name": "qty", "label": "qty", "field": "qty"},
{"name": "entry_price", "label": "entry", "field": "entry_price"},
{"name": "exit_price", "label": "exit", "field": "exit_price"},
{"name": "pnl", "label": "pnl", "field": "pnl"},
{"name": "fees", "label": "fees", "field": "fees"},
],
rows=[],
row_key="exit_ts",
).classes("w-full")
def refresh() -> None:
run_id = select.value
if not run_id:
return
try:
summary = paper_run_summary(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)
except Exception as e: # noqa: BLE001
ui.notify(f"Errore: {e}", type="negative")
return
text, color = _STATUS_BADGE.get(summary["status"], (summary["status"], "primary"))
status_badge.text = text
status_badge.props(f"color={color}")
equity_lbl.text = f"${summary['current_equity']:.2f}"
pnl_lbl.text = f"{summary['pnl_pct']:+.2f}%"
trades_lbl.text = str(summary["n_trades"])
ticks_lbl.text = f"{summary['n_open_positions']} / {summary['n_ticks']}"
started_lbl.text = f"Started: {summary['started_at']}"
last_tick_lbl.text = f"Last tick: {summary['last_tick_ts'] or ''}"
cash_lbl.text = (
f"Cash: ${summary['current_cash']:.2f} | "
f"Pos value: ${summary['current_positions_value']:.2f}"
)
equity_plot.update_figure(_paper_equity_figure(eq, summary["initial_capital"]))
positions_table.rows = (
[
{col: (round(v, 6) if isinstance(v, float) else v) for col, v in row.items()}
for _, row in positions.iterrows()
]
if not positions.empty
else []
)
positions_table.update()
ticks_table.rows = (
[
{col: (round(v, 6) if isinstance(v, float) else v) for col, v in row.items()}
for _, row in ticks.iterrows()
]
if not ticks.empty
else []
)
ticks_table.update()
trades_table.rows = (
[
{col: (round(v, 6) if isinstance(v, float) else v) for col, v in row.items()}
for _, row in trades.iterrows()
]
if not trades.empty
else []
)
trades_table.update()
def on_select_change() -> None:
state["run_id"] = select.value
refresh()
select.on_value_change(on_select_change)
ui.timer(REFRESH_INTERVAL_S, refresh)
refresh()
def main() -> None:
app.on_startup(lambda: print(f"DB: {Path(DB_PATH).resolve()}"))
ui.run(
host="0.0.0.0",
port=8080,
port=int(os.environ.get("SWARM_DASHBOARD_PORT", "8080")),
title="Multi-Swarm Dashboard",
reload=False,
show=False,
@@ -1,84 +0,0 @@
from __future__ import annotations
from datetime import datetime
import streamlit as st
from multi_swarm.dashboard.data import (
evaluations_df,
generations_df,
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)
# --- Progress live ---
cfg = overview["config"]
pop_size = int(cfg.get("population_size", 0))
n_gens = int(cfg.get("n_generations", 0))
evals = evaluations_df(repo, selected)
gens = generations_df(repo, selected)
evals_done = len(evals)
evals_total = max(pop_size * n_gens, 1)
gens_done = int(gens["completed_at"].notna().sum()) if not gens.empty else 0
status_emoji = {"running": "🟢", "completed": "", "failed": ""}.get(overview["status"], "")
top_fit = float(evals["fitness"].max()) if not evals.empty else float("nan")
st.subheader(f"{status_emoji} Progresso run")
st.progress(
min(gens_done / max(n_gens, 1), 1.0),
text=f"Generations: {gens_done}/{n_gens}",
)
st.progress(
min(evals_done / evals_total, 1.0),
text=f"Evaluations: {evals_done}/{evals_total} ({100*evals_done/evals_total:.1f}%)",
)
pcol1, pcol2, pcol3 = st.columns(3)
pcol1.metric("Top fitness", f"{top_fit:.4f}" if evals_done else "")
pcol2.metric("Median fitness", f"{evals['fitness'].median():.4f}" if evals_done else "")
pcol3.metric("Cost so far", f"${overview['total_cost_usd']:.4f}")
ref_col1, ref_col2 = st.columns([1, 4])
if ref_col1.button("🔄 Refresh"):
st.rerun()
ref_col2.caption(f"Last update: {datetime.now().strftime('%H:%M:%S')}")
st.divider()
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("Statistiche evaluations")
col5, col6, col7, col8 = st.columns(4)
if not evals.empty:
parse_success = 100 * (evals["parse_error"].isna().sum() / len(evals))
col5.metric("Evaluations totali", len(evals))
col6.metric("Parse success %", f"{parse_success:.1f}%")
col7.metric("Top fitness", f"{evals['fitness'].max():.3f}")
col8.metric("Median fitness", f"{evals['fitness'].median():.3f}")
else:
col5.metric("Evaluations totali", 0)
st.subheader("Config")
st.json(overview["config"])
@@ -1,68 +0,0 @@
from __future__ import annotations
import plotly.graph_objects as go # type: ignore[import-untyped]
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)
@@ -1,72 +0,0 @@
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']}")
@@ -1,87 +0,0 @@
from __future__ import annotations
import streamlit as st
import streamlit.components.v1 as components
from multi_swarm.dashboard.aquarium import (
STYLE_COLORS,
build_aquarium_html,
build_fish_dataset,
build_lineage_index,
)
from multi_swarm.dashboard.data import (
evaluations_df,
genomes_df,
get_repo,
list_runs_df,
)
st.title("Aquarium 2D")
st.caption(
"Pesci colorati per stile cognitivo, dimensione proporzionale a fitness. "
"Click su un pesce per dettaglio + discendenza."
)
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.")
st.stop()
selected_run = st.selectbox("Run", runs["id"].tolist())
# Fetch ALL genomes of the run (no gen filter): needed to build the lineage
# index across generations. The active set is filtered afterwards.
all_genomes = genomes_df(repo, selected_run)
all_evals = evaluations_df(repo, selected_run)
if all_genomes.empty:
st.warning("Nessun genoma per questa run.")
st.stop()
available_gens = sorted(all_genomes["generation_idx"].unique().tolist())
selected_gen = st.selectbox(
"Generazione",
available_gens,
index=len(available_gens) - 1, # default ultima
)
active_genomes = all_genomes[all_genomes["generation_idx"] == selected_gen]
active_evals = (
all_evals[all_evals["genome_id"].isin(active_genomes["id"])]
if not all_evals.empty
else all_evals
)
if not active_evals.empty:
active_merged = active_genomes.merge(
active_evals,
left_on="id",
right_on="genome_id",
how="left",
suffixes=("", "_eval"),
)
else:
active_merged = active_genomes.copy()
active_merged["genome_id"] = active_merged["id"]
lineage = build_lineage_index(all_genomes, all_evals)
fish = build_fish_dataset(active_merged, lineage, max_lineage_levels=5)
if not fish:
st.warning("Nessun agente attivo in questa generazione.")
st.stop()
st.caption(f"{len(fish)} agenti in generazione {selected_gen}")
html_str = build_aquarium_html(fish, canvas_w=1000, canvas_h=600)
components.html(html_str, height=620, scrolling=False)
with st.expander("Legenda colori"):
legend_md = "\n".join(
f"- <span style='color:{color};font-weight:bold;'>&#9679;</span> "
f"`{style}`"
for style, color in STYLE_COLORS.items()
)
st.markdown(legend_md, unsafe_allow_html=True)
@@ -1,22 +0,0 @@
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.
- **Aquarium**: visualizzazione 2D dei genomi come pesci animati.
"""
)
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()}`")
+5
View File
@@ -0,0 +1,5 @@
"""Data loaders: OHLCV via Cerbero, train/test splits."""
from .cerbero_ohlcv import CerberoOHLCVLoader, OHLCVRequest
__all__ = ["CerberoOHLCVLoader", "OHLCVRequest"]
+20
View File
@@ -32,6 +32,26 @@ from ..agents.adversarial import AdversarialReport, Severity
from ..agents.falsification import FalsificationReport
def compute_combined_fitness(
fitness_train: float,
fitness_oos: float | None,
alpha: float = 0.5,
) -> float:
"""Combina fitness IS e OOS in uno scalare per selection multi-objective.
Formula::
combined = alpha * fitness_train + (1 - alpha) * fitness_oos
Se ``fitness_oos`` è ``None`` o NaN, ritorna ``fitness_train`` (fallback).
alpha=1.0 → solo IS (= comportamento default). alpha=0.0 → solo OOS.
alpha=0.5 → bilanciato.
"""
if fitness_oos is None or fitness_oos != fitness_oos: # noqa: PLR0124 (NaN check)
return fitness_train
return alpha * fitness_train + (1.0 - alpha) * fitness_oos
def compute_fitness(
falsification: FalsificationReport,
adversarial: AdversarialReport,
+31
View File
@@ -61,6 +61,12 @@ class RunConfig:
# dei top genomi sui restanti. None/0 = no WFA (eval full ohlcv).
wfa_train_split: float | None = None
wfa_top_k: int = 5 # quanti top genomi rivalutare OOS
# Multi-objective selection: se True, ogni genome viene valutato anche su
# test_ohlcv durante il loop e la fitness usata per tournament/elite è
# combined = alpha*IS + (1-alpha)*OOS. Richiede wfa_train_split attivo.
# 2x costo backtest engine.
eval_oos_during_loop: bool = False
fitness_combined_alpha: float = 0.5 # peso IS (1-alpha = OOS)
def run_phase1(
@@ -176,6 +182,31 @@ def run_phase1(
hard_kill_findings=cfg.fitness_hard_kill_findings,
adversarial_soft_penalty=cfg.fitness_adversarial_soft_penalty,
)
# Multi-objective: se attivo, eval OOS subito e combina via alpha.
if (
cfg.eval_oos_during_loop
and test_ohlcv is not None
and len(test_ohlcv) >= 100
and fit > 0
):
try:
fals_oos_inloop = falsification_agent.evaluate(
proposal.strategy, test_ohlcv
)
adv_oos_inloop = adversarial_agent.review(
proposal.strategy, test_ohlcv
)
fit_oos_inloop = compute_fitness(
fals_oos_inloop, adv_oos_inloop,
hard_kill_findings=cfg.fitness_hard_kill_findings,
adversarial_soft_penalty=cfg.fitness_adversarial_soft_penalty,
)
fit = (
cfg.fitness_combined_alpha * fit
+ (1.0 - cfg.fitness_combined_alpha) * fit_oos_inloop
)
except Exception: # noqa: BLE001
pass # fallback: usa solo IS
repo.save_evaluation(
run_id=run_id,
genome_id=genome.id,
+96
View File
@@ -0,0 +1,96 @@
"""PaperExecutor: applica un segnale di strategia a un Portfolio.
Il flusso per ogni tick:
bar OHLCV chiuso -> compile_strategy(strategy) -> Series[Side]
-> last_signal = series.iloc[-1]
-> match con posizione attuale -> open / close / hold
Niente delay 1-bar: in paper-trading il segnale viene calcolato sulla
barra appena chiusa e applicato al prezzo close della stessa. La latenza
reale tra tick e ordine va misurata separatamente (Phase 3 spec).
"""
from __future__ import annotations
import json
from dataclasses import dataclass
from datetime import datetime
from pathlib import Path
import pandas as pd # type: ignore[import-untyped]
from ..backtest.orders import Side, Trade
from ..protocol.compiler import compile_strategy
from ..protocol.parser import parse_strategy
from .portfolio import OpenPosition, Portfolio
@dataclass
class TickResult:
ts: datetime
symbol: str
bar_ts: datetime
close_price: float
signal: Side
action_taken: str # "open_long" | "open_short" | "close" | "reverse" | "hold"
trade: Trade | None = None
new_position: OpenPosition | None = None
class PaperExecutor:
def __init__(self, strategy_json_path: Path, symbol: str) -> None:
text = strategy_json_path.read_text()
# parse_strategy si aspetta JSON pulito, non fence; il file e' gia' JSON.
self._strategy = parse_strategy(text)
self._compiled = compile_strategy(self._strategy)
self.symbol = symbol
self.strategy_path = strategy_json_path
def execute_tick(
self,
portfolio: Portfolio,
ohlcv: pd.DataFrame,
now: datetime,
) -> TickResult:
"""Esegui un tick: calcola segnale su tutto ``ohlcv`` (per indicatori
con lookback), prendi l'ultimo, e applica al portfolio."""
if len(ohlcv) == 0:
raise ValueError("Empty OHLCV passed to execute_tick")
signals = self._compiled(ohlcv)
# ultimo bar chiuso
bar_ts = ohlcv.index[-1]
close_price = float(ohlcv["close"].iloc[-1])
signal = Side(signals.iloc[-1]) if signals.iloc[-1] is not None else Side.FLAT
current = portfolio.positions.get(self.symbol)
action = "hold"
trade: Trade | None = None
new_position: OpenPosition | None = None
if current is None and signal != Side.FLAT:
new_position = portfolio.open(self.symbol, signal, close_price, now)
action = f"open_{signal.value}"
elif current is not None and signal == Side.FLAT:
trade = portfolio.close(self.symbol, close_price, now)
action = "close"
elif current is not None and signal != current.side:
# reverse: chiudi e riapri opposto
trade = portfolio.close(self.symbol, close_price, now)
new_position = portfolio.open(self.symbol, signal, close_price, now)
action = "reverse"
return TickResult(
ts=now,
symbol=self.symbol,
bar_ts=bar_ts.to_pydatetime() if hasattr(bar_ts, "to_pydatetime") else bar_ts,
close_price=close_price,
signal=signal,
action_taken=action,
trade=trade,
new_position=new_position,
)
@property
def strategy_dict(self) -> dict:
return json.loads(self.strategy_path.read_text())
@@ -0,0 +1,114 @@
"""Persistenza paper-trading: usa lo stesso ``runs.db`` con tabelle dedicate
``paper_trading_*`` (vedi :mod:`multi_swarm.persistence.schema`).
"""
from __future__ import annotations
import json
import sqlite3
import uuid
from datetime import UTC, datetime
from pathlib import Path
from typing import Any
from .executor import TickResult
from .portfolio import Portfolio
class PaperRepository:
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
@staticmethod
def _now() -> str:
return datetime.now(UTC).isoformat()
def create_run(self, name: str, initial_capital: float, config: dict[str, Any]) -> str:
rid = uuid.uuid4().hex
with self._conn() as conn:
conn.execute(
"INSERT INTO paper_trading_runs "
"(id, name, started_at, status, initial_capital, config_json) "
"VALUES (?,?,?,?,?,?)",
(rid, name, self._now(), "running", initial_capital, json.dumps(config)),
)
return rid
def stop_run(self, run_id: str, status: str = "stopped") -> None:
with self._conn() as conn:
conn.execute(
"UPDATE paper_trading_runs SET stopped_at=?, status=? WHERE id=?",
(self._now(), status, run_id),
)
def save_tick(self, run_id: str, tick: TickResult) -> None:
with self._conn() as conn:
conn.execute(
"INSERT INTO paper_trading_ticks "
"(paper_run_id, symbol, ts, bar_ts, close_price, signal, action_taken) "
"VALUES (?,?,?,?,?,?,?)",
(
run_id,
tick.symbol,
tick.ts.isoformat(),
tick.bar_ts.isoformat() if hasattr(tick.bar_ts, "isoformat") else str(tick.bar_ts),
tick.close_price,
tick.signal.value,
tick.action_taken,
),
)
if tick.trade is not None:
t = tick.trade
conn.execute(
"INSERT INTO paper_trading_trades "
"(paper_run_id, symbol, side, qty, entry_price, exit_price, "
"entry_ts, exit_ts, pnl, fees) VALUES (?,?,?,?,?,?,?,?,?,?)",
(
run_id,
tick.symbol,
t.side.value,
t.size,
t.entry_price,
t.exit_price,
t.entry_ts.isoformat(),
t.exit_ts.isoformat(),
t.net_pnl,
t.fees,
),
)
def save_equity_snapshot(
self,
run_id: str,
ts: datetime,
equity: float,
cash: float,
positions_value: float,
) -> None:
with self._conn() as conn:
conn.execute(
"INSERT INTO paper_trading_equity "
"(paper_run_id, ts, equity, cash, positions_value) VALUES (?,?,?,?,?)",
(run_id, ts.isoformat(), equity, cash, positions_value),
)
def sync_open_positions(self, run_id: str, portfolio: Portfolio) -> None:
"""Sostituisce snapshot posizioni aperte. Idempotente: cancella e reinserisce."""
with self._conn() as conn:
conn.execute(
"DELETE FROM paper_trading_positions WHERE paper_run_id=?", (run_id,)
)
for sym, pos in portfolio.positions.items():
conn.execute(
"INSERT INTO paper_trading_positions "
"(paper_run_id, symbol, side, qty, entry_price, entry_ts) "
"VALUES (?,?,?,?,?,?)",
(run_id, sym, pos.side.value, pos.qty, pos.entry_price, pos.entry_ts.isoformat()),
)
+104
View File
@@ -0,0 +1,104 @@
"""Portfolio multi-asset per paper-trading.
Modello semplificato: capitale unico ``cash``, allocazione equal-weight
fra N posizioni (sleeve = 1/N del capitale iniziale per ogni simbolo).
Niente leva, niente liquidation, fees su entry+exit (bp del notional).
Una :class:`Position` rappresenta una posizione aperta su un singolo
simbolo (long/short, qty in unita' dell'asset, prezzo di entry). La
posizione viene chiusa con :meth:`Portfolio.close` che produce un
:class:`Trade` realized e accredita ``cash``.
Mark-to-market via :meth:`Portfolio.equity`.
"""
from __future__ import annotations
from dataclasses import dataclass, field
from datetime import datetime
from ..backtest.orders import Side, Trade
@dataclass(frozen=True)
class OpenPosition:
symbol: str
side: Side
qty: float
entry_price: float
entry_ts: datetime
@dataclass
class Portfolio:
initial_capital: float
fees_bp: float = 5.0
n_sleeves: int = 2 # numero strategie / asset previsti
cash: float = field(init=False)
positions: dict[str, OpenPosition] = field(default_factory=dict)
closed_trades: list[Trade] = field(default_factory=list)
def __post_init__(self) -> None:
self.cash = self.initial_capital
@property
def sleeve_capital(self) -> float:
return self.initial_capital / self.n_sleeves
def open(
self,
symbol: str,
side: Side,
price: float,
ts: datetime,
) -> OpenPosition:
if symbol in self.positions:
raise ValueError(f"Position already open on {symbol}")
if side == Side.FLAT:
raise ValueError("Cannot open a FLAT position")
# sleeve fisso: alloca 1/n_sleeves del capitale iniziale, qty = notional/price.
notional = self.sleeve_capital
qty = notional / price
fees = notional * (self.fees_bp / 10000.0)
self.cash -= fees
pos = OpenPosition(symbol=symbol, side=side, qty=qty, entry_price=price, entry_ts=ts)
self.positions[symbol] = pos
return pos
def close(
self,
symbol: str,
price: float,
ts: datetime,
) -> Trade:
if symbol not in self.positions:
raise ValueError(f"No open position on {symbol}")
pos = self.positions.pop(symbol)
trade = Trade(
entry_ts=pos.entry_ts,
exit_ts=ts,
side=pos.side,
size=pos.qty,
entry_price=pos.entry_price,
exit_price=price,
fees_bp=self.fees_bp,
)
# net_pnl include gia' i fees sull'intero round-trip; abbiamo gia'
# addebitato meta' fees all'open, ora addebitiamo il resto.
self.cash += trade.gross_pnl - (trade.fees / 2.0)
self.closed_trades.append(trade)
return trade
def equity(self, last_prices: dict[str, float]) -> tuple[float, float]:
"""Ritorna (equity_totale, positions_value) marcando posizioni aperte
al ``last_prices[symbol]``. Posizioni senza prezzo disponibile valgono
notional di entry (fallback conservativo)."""
positions_value = 0.0
for sym, pos in self.positions.items():
price = last_prices.get(sym, pos.entry_price)
unreal = pos.qty * (
price - pos.entry_price if pos.side == Side.LONG
else pos.entry_price - price
)
positions_value += pos.qty * pos.entry_price + unreal
return self.cash + positions_value, positions_value
+61
View File
@@ -77,7 +77,68 @@ 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);
"""
+142
View File
@@ -0,0 +1,142 @@
{
"rules": [
{
"condition": {
"op": "and",
"args": [
{
"op": "gt",
"args": [
{
"kind": "indicator",
"name": "rsi",
"params": [
14
]
},
{
"kind": "literal",
"value": 70.0
}
]
},
{
"op": "gt",
"args": [
{
"kind": "indicator",
"name": "atr",
"params": [
14
]
},
{
"kind": "indicator",
"name": "sma",
"params": [
14
]
}
]
},
{
"op": "gt",
"args": [
{
"kind": "feature",
"name": "hour"
},
{
"kind": "literal",
"value": 9
}
]
},
{
"op": "lt",
"args": [
{
"kind": "feature",
"name": "hour"
},
{
"kind": "literal",
"value": 17
}
]
}
]
},
"action": "entry-short"
},
{
"condition": {
"op": "and",
"args": [
{
"op": "lt",
"args": [
{
"kind": "indicator",
"name": "rsi",
"params": [
14
]
},
{
"kind": "literal",
"value": 30.0
}
]
},
{
"op": "lt",
"args": [
{
"kind": "indicator",
"name": "atr",
"params": [
14
]
},
{
"kind": "indicator",
"name": "sma",
"params": [
14
]
}
]
},
{
"op": "gt",
"args": [
{
"kind": "feature",
"name": "hour"
},
{
"kind": "literal",
"value": 9
}
]
},
{
"op": "lt",
"args": [
{
"kind": "feature",
"name": "hour"
},
{
"kind": "literal",
"value": 17
}
]
}
]
},
"action": "entry-long"
}
]
}
+162
View File
@@ -0,0 +1,162 @@
{
"rules": [
{
"condition": {
"op": "and",
"args": [
{
"op": "gt",
"args": [
{
"kind": "indicator",
"name": "atr",
"params": [
14
]
},
{
"kind": "literal",
"value": 0.02
}
]
},
{
"op": "gt",
"args": [
{
"kind": "indicator",
"name": "realized_vol",
"params": [
20
]
},
{
"kind": "literal",
"value": 0.03
}
]
},
{
"op": "gt",
"args": [
{
"kind": "indicator",
"name": "sma",
"params": [
50
]
},
{
"kind": "indicator",
"name": "sma",
"params": [
200
]
}
]
}
]
},
"action": "entry-long"
},
{
"condition": {
"op": "and",
"args": [
{
"op": "lt",
"args": [
{
"kind": "indicator",
"name": "atr",
"params": [
14
]
},
{
"kind": "literal",
"value": 0.01
}
]
},
{
"op": "lt",
"args": [
{
"kind": "indicator",
"name": "realized_vol",
"params": [
20
]
},
{
"kind": "literal",
"value": 0.02
}
]
},
{
"op": "lt",
"args": [
{
"kind": "indicator",
"name": "sma",
"params": [
50
]
},
{
"kind": "indicator",
"name": "sma",
"params": [
200
]
}
]
}
]
},
"action": "entry-short"
},
{
"condition": {
"op": "or",
"args": [
{
"op": "crossover",
"args": [
{
"kind": "indicator",
"name": "rsi",
"params": [
14
]
},
{
"kind": "literal",
"value": 70.0
}
]
},
{
"op": "crossunder",
"args": [
{
"kind": "indicator",
"name": "rsi",
"params": [
14
]
},
{
"kind": "literal",
"value": 30.0
}
]
}
]
},
"action": "exit"
}
]
}
-310
View File
@@ -1,310 +0,0 @@
import importlib
import pandas as pd
def test_streamlit_app_imports():
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")
def test_aquarium_helper_builds_html_with_click_handler():
from multi_swarm.dashboard.aquarium import build_aquarium_html
fish = [
{
"id": "abc123",
"fitness": 0.8,
"cognitive_style": "physicist",
"n_trades": 30,
"dsr": 0.7,
"sharpe": 1.2,
"max_dd": 0.1,
"system_prompt": "test",
"temperature": 0.9,
"lookback_window": 200,
"feature_access": ["close"],
"model_tier": "C",
"generation": 1,
"parent_ids": [],
"ancestors": [],
}
]
html = build_aquarium_html(fish, canvas_w=800, canvas_h=400)
assert "canvas" in html
assert "abc123" in html # fish id present in JSON payload
assert "addEventListener('click'" in html
assert "fish-info-panel" in html
assert "showFishInfo" in html
assert "Discendenza" in html
assert "requestAnimationFrame" in html
def test_aquarium_build_fish_dataset_legacy_path():
from multi_swarm.dashboard.aquarium import build_fish_dataset
df = pd.DataFrame(
[
{
"genome_id": "low",
"fitness": 0.1,
"cognitive_style": "physicist",
"n_trades": 1,
"dsr": 0.0,
},
{
"genome_id": "high",
"fitness": 0.9,
"cognitive_style": "biologist",
"n_trades": 10,
"dsr": 0.5,
},
]
)
out = build_fish_dataset(df)
ids = {f["id"] for f in out}
assert ids == {"low", "high"}
high = next(f for f in out if f["id"] == "high")
assert high["cognitive_style"] == "biologist"
assert high["ancestors"] == []
def test_aquarium_build_fish_dataset_drops_nan_fitness():
from multi_swarm.dashboard.aquarium import build_fish_dataset
df = pd.DataFrame(
[
{
"genome_id": "ok",
"fitness": 0.4,
"cognitive_style": "historian",
"n_trades": 2,
"dsr": 0.1,
},
{
"genome_id": "bad",
"fitness": float("nan"),
"cognitive_style": "ecologist",
"n_trades": 0,
"dsr": 0.0,
},
]
)
out = build_fish_dataset(df)
assert len(out) == 1
assert out[0]["id"] == "ok"
def test_aquarium_empty_input_returns_empty():
from multi_swarm.dashboard.aquarium import build_aquarium_html, build_fish_dataset
assert build_fish_dataset(pd.DataFrame()) == []
html = build_aquarium_html([], canvas_w=400, canvas_h=200)
assert "canvas" in html
assert "Acquario vuoto" in html
def test_build_lineage_index_returns_dict_keyed_by_id():
from multi_swarm.dashboard.aquarium import build_lineage_index
genomes = pd.DataFrame(
[
{
"id": "g1",
"generation_idx": 0,
"generation": 0,
"system_prompt": "x",
"feature_access": ["close"],
"temperature": 0.9,
"top_p": 0.95,
"model_tier": "C",
"lookback_window": 100,
"cognitive_style": "physicist",
"parent_ids": [],
},
{
"id": "g2",
"generation_idx": 1,
"generation": 1,
"system_prompt": "y",
"feature_access": ["close", "volume"],
"temperature": 1.0,
"top_p": 0.95,
"model_tier": "C",
"lookback_window": 200,
"cognitive_style": "biologist",
"parent_ids": ["g1"],
},
]
)
evals = pd.DataFrame(
[
{
"genome_id": "g1",
"fitness": 0.5,
"dsr": 0.6,
"sharpe": 1.2,
"max_dd": 0.1,
"n_trades": 30,
"parse_error": None,
"raw_text": "",
},
{
"genome_id": "g2",
"fitness": 0.7,
"dsr": 0.8,
"sharpe": 1.5,
"max_dd": 0.05,
"n_trades": 40,
"parse_error": None,
"raw_text": "",
},
]
)
idx = build_lineage_index(genomes, evals)
assert "g1" in idx and "g2" in idx
assert idx["g2"]["parent_ids"] == ["g1"]
assert idx["g2"]["fitness"] == 0.7
assert idx["g1"]["cognitive_style"] == "physicist"
assert idx["g2"]["feature_access"] == ["close", "volume"]
def test_trace_ancestors_walks_levels():
from multi_swarm.dashboard.aquarium import trace_ancestors
idx = {
"child": {
"id": "child",
"parent_ids": ["p1", "p2"],
"fitness": 0.8,
"generation": 2,
"cognitive_style": "physicist",
},
"p1": {
"id": "p1",
"parent_ids": ["gp1"],
"fitness": 0.5,
"generation": 1,
"cognitive_style": "biologist",
},
"p2": {
"id": "p2",
"parent_ids": [],
"fitness": 0.3,
"generation": 1,
"cognitive_style": "engineer",
},
"gp1": {
"id": "gp1",
"parent_ids": [],
"fitness": 0.2,
"generation": 0,
"cognitive_style": "historian",
},
}
levels = trace_ancestors("child", idx, max_levels=5)
assert len(levels) == 2
assert {a["id"] for a in levels[0]} == {"p1", "p2"}
assert {a["id"] for a in levels[1]} == {"gp1"}
def test_trace_ancestors_handles_cycles():
from multi_swarm.dashboard.aquarium import trace_ancestors
# Pathological cycle: a <-> b. Should terminate cleanly.
idx = {
"a": {
"id": "a",
"parent_ids": ["b"],
"fitness": 0.1,
"generation": 1,
"cognitive_style": "physicist",
},
"b": {
"id": "b",
"parent_ids": ["a"],
"fitness": 0.2,
"generation": 0,
"cognitive_style": "biologist",
},
}
levels = trace_ancestors("a", idx, max_levels=5)
# a -> b at level 0; b's only parent is a, already seen -> stop.
assert len(levels) == 1
assert levels[0][0]["id"] == "b"
def test_trace_ancestors_no_parents_returns_empty():
from multi_swarm.dashboard.aquarium import trace_ancestors
idx = {
"solo": {
"id": "solo",
"parent_ids": [],
"fitness": 0.4,
"generation": 0,
"cognitive_style": "engineer",
},
}
assert trace_ancestors("solo", idx) == []
def test_build_fish_dataset_attaches_ancestors():
from multi_swarm.dashboard.aquarium import build_fish_dataset, build_lineage_index
genomes = pd.DataFrame(
[
{
"id": "p",
"generation_idx": 0,
"generation": 0,
"system_prompt": "p",
"feature_access": ["close"],
"temperature": 0.8,
"top_p": 0.9,
"model_tier": "C",
"lookback_window": 100,
"cognitive_style": "physicist",
"parent_ids": [],
},
{
"id": "c",
"generation_idx": 1,
"generation": 1,
"system_prompt": "c",
"feature_access": ["close"],
"temperature": 0.8,
"top_p": 0.9,
"model_tier": "C",
"lookback_window": 120,
"cognitive_style": "biologist",
"parent_ids": ["p"],
},
]
)
evals = pd.DataFrame(
[
{"genome_id": "p", "fitness": 0.3, "dsr": 0.0, "sharpe": 0.0,
"max_dd": 0.0, "n_trades": 0},
{"genome_id": "c", "fitness": 0.6, "dsr": 0.0, "sharpe": 0.0,
"max_dd": 0.0, "n_trades": 0},
]
)
lineage = build_lineage_index(genomes, evals)
active = genomes[genomes["generation_idx"] == 1].merge(
evals, left_on="id", right_on="genome_id", how="left"
)
fish = build_fish_dataset(active, lineage)
assert len(fish) == 1
assert fish[0]["id"] == "c"
assert len(fish[0]["ancestors"]) == 1
assert fish[0]["ancestors"][0][0]["id"] == "p"
Generated
+209 -298
View File
@@ -111,22 +111,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490, upload-time = "2025-07-03T22:54:42.156Z" },
]
[[package]]
name = "altair"
version = "6.1.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "jinja2" },
{ name = "jsonschema" },
{ name = "narwhals" },
{ name = "packaging" },
{ name = "typing-extensions", marker = "python_full_version < '3.15'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/3a/1e/365a9144db3254f86f1b974660b9ede1e9a38c9dc0730e4a9b1192eec5d6/altair-6.1.0.tar.gz", hash = "sha256:dda699216cf85b040d968ae5a569ad45957616811e38760a85e5118269daca67", size = 765519, upload-time = "2026-04-21T13:08:46.44Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ce/63/5dacc8d8306c715088b897a479e551bc0779fd2f0f26c97fec5e36542b4e/altair-6.1.0-py3-none-any.whl", hash = "sha256:fdf5fd939512e5b2fc4441c82dfd2635e706defbd037db0ac429ef5ddce66c3b", size = 796996, upload-time = "2026-04-21T13:08:48.549Z" },
]
[[package]]
name = "annotated-doc"
version = "0.0.4"
@@ -204,6 +188,19 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" },
]
[[package]]
name = "beautifulsoup4"
version = "4.14.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "soupsieve" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/c3/b0/1c6a16426d389813b48d95e26898aff79abbde42ad353958ad95cc8c9b21/beautifulsoup4-4.14.3.tar.gz", hash = "sha256:6292b1c5186d356bba669ef9f7f051757099565ad9ada5dd630bd9de5fa7fb86", size = 627737, upload-time = "2025-11-30T15:08:26.084Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/1a/39/47f9197bdd44df24d67ac8893641e16f386c984a0619ef2ee4c51fbbc019/beautifulsoup4-4.14.3-py3-none-any.whl", hash = "sha256:0918bfe44902e6ad8d57732ba310582e98da931428d231a5ecb9e7c703a735bb", size = 107721, upload-time = "2025-11-30T15:08:24.087Z" },
]
[[package]]
name = "bidict"
version = "0.23.1"
@@ -213,24 +210,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/99/37/e8730c3587a65eb5645d4aba2d27aae48e8003614d6aaf15dda67f702f1f/bidict-0.23.1-py3-none-any.whl", hash = "sha256:5dae8d4d79b552a71cbabc7deb25dfe8ce710b17ff41711e13010ead2abfc3e5", size = 32764, upload-time = "2024-02-18T19:09:04.156Z" },
]
[[package]]
name = "blinker"
version = "1.9.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/21/28/9b3f50ce0e048515135495f198351908d99540d69bfdc8c1d15b73dc55ce/blinker-1.9.0.tar.gz", hash = "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf", size = 22460, upload-time = "2024-11-08T17:25:47.436Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc", size = 8458, upload-time = "2024-11-08T17:25:46.184Z" },
]
[[package]]
name = "cachetools"
version = "7.1.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/ff/e2/85f227594656000ff4d8adadae91a21f536d4a84c6c716a86bd6685874be/cachetools-7.1.1.tar.gz", hash = "sha256:27bdf856d68fd3c71c26c01b5edc312124ed427524d1ddb31aa2b7746fe20d4b", size = 40202, upload-time = "2026-05-03T20:00:29.391Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/bf/0f/f897abe4ea0a8c408ae65c8c83bffab4936ad65d6032d4fb4cd35bbdc3ee/cachetools-7.1.1-py3-none-any.whl", hash = "sha256:0335cd7a0952d2b22327441fb0628139e234c565559eeb91a8a4ac7551c5353d", size = 16775, upload-time = "2026-05-03T20:00:27.857Z" },
]
[[package]]
name = "certifi"
version = "2026.4.22"
@@ -240,6 +219,51 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/22/30/7cd8fdcdfbc5b869528b079bfb76dcdf6056b1a2097a662e5e8c04f42965/certifi-2026.4.22-py3-none-any.whl", hash = "sha256:3cb2210c8f88ba2318d29b0388d1023c8492ff72ecdde4ebdaddbb13a31b1c4a", size = 135707, upload-time = "2026-04-22T11:26:09.372Z" },
]
[[package]]
name = "cffi"
version = "2.0.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pycparser", marker = "implementation_name != 'PyPy'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" },
{ url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" },
{ url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" },
{ url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" },
{ url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" },
{ url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" },
{ url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" },
{ url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" },
{ url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" },
{ url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" },
{ url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" },
{ url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" },
{ url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" },
{ url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" },
{ url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" },
{ url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" },
{ url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" },
{ url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" },
{ url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" },
{ url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" },
{ url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" },
{ url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" },
{ url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" },
{ url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" },
{ url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" },
{ url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" },
{ url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" },
{ url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" },
{ url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" },
{ url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" },
{ url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" },
{ url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" },
{ url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" },
{ url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" },
]
[[package]]
name = "charset-normalizer"
version = "3.4.7"
@@ -318,6 +342,39 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
]
[[package]]
name = "curl-cffi"
version = "0.15.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "certifi" },
{ name = "cffi" },
{ name = "rich" },
]
sdist = { url = "https://files.pythonhosted.org/packages/48/5b/89fcfebd3e5e85134147ac99e9f2b2271165fd4d71984fc65da5f17819b7/curl_cffi-0.15.0.tar.gz", hash = "sha256:ea0c67652bf6893d34ee0f82c944f37e488f6147e9421bef1771cc6545b02ded", size = 196437, upload-time = "2026-04-03T11:12:31.525Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/5e/42/54ddd442c795f30ce5dd4e49f87ce77505958d3777cd96a91567a3975d2a/curl_cffi-0.15.0-cp310-abi3-macosx_10_9_x86_64.whl", hash = "sha256:bda66404010e9ed743b1b83c20c86f24fe21a9a6873e17479d6e67e29d8ded28", size = 2795267, upload-time = "2026-04-03T11:11:46.48Z" },
{ url = "https://files.pythonhosted.org/packages/83/2d/3915e238579b3c5a92cead5c79130c3b8d20caaba7616cc4d894650e1d6b/curl_cffi-0.15.0-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:a25620d9bf989c9c029a7d1642999c4c265abb0bad811deb2f77b0b5b2b12e5b", size = 2573544, upload-time = "2026-04-03T11:11:47.951Z" },
{ url = "https://files.pythonhosted.org/packages/2a/b3/9d2f1057749a1b07ba1989db3c1503ce8bed998310bae9aea2c43aa64f20/curl_cffi-0.15.0-cp310-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:582e570aa2586b96ed47cf4a17586b9a3c462cbe43f780487c3dc245c6ef1527", size = 10515369, upload-time = "2026-04-03T11:11:50.126Z" },
{ url = "https://files.pythonhosted.org/packages/b5/1d/6d10dded5ce3fd8157e558ebd97d09e551b77a62cdc1c31e93d0a633cee5/curl_cffi-0.15.0-cp310-abi3-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:838e48212447d9c81364b04707a5c861daf08f8320f9ecb3406a8919d1d5c3b3", size = 10160045, upload-time = "2026-04-03T11:11:52.664Z" },
{ url = "https://files.pythonhosted.org/packages/5c/12/c70b835487ace3b9ba1502631912e3440082b8ae3a162f60b59cb0b6444d/curl_cffi-0.15.0-cp310-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2b6c847d86283b07ae69bb72c82eb8a59242277142aa35b89850f89e792a02fc", size = 11090433, upload-time = "2026-04-03T11:11:55.049Z" },
{ url = "https://files.pythonhosted.org/packages/ea/0d/78edcc4f71934225db99df68197a107386d59080742fc7bf6bb4d007924f/curl_cffi-0.15.0-cp310-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:9e5e69eee735f659287e2c84444319d68a1fa68dd37abf228943a4074864283a", size = 10479178, upload-time = "2026-04-03T11:11:57.685Z" },
{ url = "https://files.pythonhosted.org/packages/5b/84/1e101c1acb1ea2f0b4992f5c3024f596d8e21db0d53540b9d583f673c4e7/curl_cffi-0.15.0-cp310-abi3-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:aa1323950224db24f4c510d010b3affa02196ca853fb424191fa917a513d3f4b", size = 10317051, upload-time = "2026-04-03T11:12:00.295Z" },
{ url = "https://files.pythonhosted.org/packages/28/42/8ef236b22a6c23d096c85a1dc507efe37bfdfc7a2f8a4b34efb590197369/curl_cffi-0.15.0-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:41f80170ba844009273b2660da1964ec31e99e5719d16b3422ada87177e32e13", size = 11299660, upload-time = "2026-04-03T11:12:02.791Z" },
{ url = "https://files.pythonhosted.org/packages/1d/01/56aeb055d962da87a1be0d74c6c644e251c7e88129b5471dc44ac724e678/curl_cffi-0.15.0-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:1977e1e12cfb5c11352cbb74acef1bed24eb7d226dab61ca57c168c21acd4d61", size = 11945049, upload-time = "2026-04-03T11:12:05.912Z" },
{ url = "https://files.pythonhosted.org/packages/d8/8c/2abf99a38d6340d66cf0557e0c750ef3f8883dfc5d450087e01c85861343/curl_cffi-0.15.0-cp310-abi3-win_amd64.whl", hash = "sha256:5a0c1896a0d5a5ac1eb89cd24b008d2b718dd1df6fd2f75451b59ca66e49e572", size = 1661649, upload-time = "2026-04-03T11:12:07.948Z" },
{ url = "https://files.pythonhosted.org/packages/3d/39/dfd54f2240d3a9b96d77bacc62b97813b35e2aa8ecf5cd5013c683f1ba96/curl_cffi-0.15.0-cp310-abi3-win_arm64.whl", hash = "sha256:a6d57f8389273a3a1f94370473c74897467bcc36af0a17336989780c507fa43d", size = 1410741, upload-time = "2026-04-03T11:12:10.073Z" },
{ url = "https://files.pythonhosted.org/packages/19/6a/c24df8a4fc22fa84070dcd94abeba43c15e08cc09e35869565c0bad196fd/curl_cffi-0.15.0-cp313-abi3-android_24_arm64_v8a.whl", hash = "sha256:4682dc38d4336e0eb0b185374db90a760efde63cbea994b4e63f3521d44c4c92", size = 7190427, upload-time = "2026-04-03T11:12:12.142Z" },
{ url = "https://files.pythonhosted.org/packages/11/56/132225cb3491d07cc6adcce5fe395e059bde87c68cff1ef87a31c88c7819/curl_cffi-0.15.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:967ad7355bd8e9586f8c2d02eaa99953747549e7ea4a9b25cd53353e6b67fe6d", size = 2795723, upload-time = "2026-04-03T11:12:13.668Z" },
{ url = "https://files.pythonhosted.org/packages/07/8f/f4f83cd303bef7e8f1749512e5dd157e7e5d08b0a36c8211f9640a2757bf/curl_cffi-0.15.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7e63539d0d839d0a8c5eacf86229bc68c57803547f35e0db7ee0986328b478c3", size = 2573739, upload-time = "2026-04-03T11:12:15.08Z" },
{ url = "https://files.pythonhosted.org/packages/e8/5c/643d65c7fc9acd742876aa55c2d7823c438cb7665810acd2e66c9976c4d9/curl_cffi-0.15.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:08c799b89740b9bc49c09fbc3d5907f13ac1f845ca52620507ef9466d4639dd5", size = 10521046, upload-time = "2026-04-03T11:12:17.034Z" },
{ url = "https://files.pythonhosted.org/packages/7f/0b/9b8037113c93f4c5323096163471fa7c35c7676c3f608eeaf1287cd99d58/curl_cffi-0.15.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7b7a92767a888ee90147e18964b396d8435ff42737030d6fb00824ffd6094805", size = 11096115, upload-time = "2026-04-03T11:12:19.694Z" },
{ url = "https://files.pythonhosted.org/packages/5f/96/fff2fcbd924ef4042e0d67379f751a8a4e3186a91e75e35a4cf218b306ee/curl_cffi-0.15.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:829cc357061ecb99cc2d406301f609a039e05665322f5c025ec67c38b0dc49ce", size = 11305346, upload-time = "2026-04-03T11:12:22.151Z" },
{ url = "https://files.pythonhosted.org/packages/53/1b/304b253a45ab28691c8c5e8cca1e6cbb9cf8e46dfceae4648dd536f75e73/curl_cffi-0.15.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:408d6f14e346841cd889c2e0962832bb235ba3b6749ebf609f347f747da5e60f", size = 11949834, upload-time = "2026-04-03T11:12:24.986Z" },
{ url = "https://files.pythonhosted.org/packages/5a/ff/4723d92f08259c707a974aba27a08d0a822b9555e35ca581bf18d055a364/curl_cffi-0.15.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b624c7ce087bfda967a013ed0a64702a525444e5b6e97d23534d567ccc6525aa", size = 1702771, upload-time = "2026-04-03T11:12:28.201Z" },
{ url = "https://files.pythonhosted.org/packages/59/8c/36bbe06d66fa2b765e4a07199f643a59a9cd1a754207a96335402a9520f4/curl_cffi-0.15.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0b6c0543b993996670e9e4b78e305a2d60809d5681903ffb5568e21a387434d3", size = 1466312, upload-time = "2026-04-03T11:12:30.054Z" },
]
[[package]]
name = "distro"
version = "1.9.0"
@@ -352,6 +409,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/5a/ff/2e4eca3ade2c22fe1dea7043b8ee9dabe47753349eb1b56a202de8af6349/fastapi-0.136.1-py3-none-any.whl", hash = "sha256:a6e9d7eeada96c93a4d69cb03836b44fa34e2854accb7244a1ece36cd4781c3f", size = 117683, upload-time = "2026-04-23T16:49:42.437Z" },
]
[[package]]
name = "frozendict"
version = "2.4.7"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/90/b2/2a3d1374b7780999d3184e171e25439a8358c47b481f68be883c14086b4c/frozendict-2.4.7.tar.gz", hash = "sha256:e478fb2a1391a56c8a6e10cc97c4a9002b410ecd1ac28c18d780661762e271bd", size = 317082, upload-time = "2025-11-11T22:40:14.251Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/38/74/f94141b38a51a553efef7f510fc213894161ae49b88bffd037f8d2a7cb2f/frozendict-2.4.7-py3-none-any.whl", hash = "sha256:972af65924ea25cf5b4d9326d549e69a9a4918d8a76a9d3a7cd174d98b237550", size = 16264, upload-time = "2025-11-11T22:40:12.836Z" },
]
[[package]]
name = "frozenlist"
version = "1.8.0"
@@ -425,30 +491,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/9a/9a/e35b4a917281c0b8419d4207f4334c8e8c5dbf4f3f5f9ada73958d937dcc/frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d", size = 13409, upload-time = "2025-10-06T05:38:16.721Z" },
]
[[package]]
name = "gitdb"
version = "4.0.12"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "smmap" },
]
sdist = { url = "https://files.pythonhosted.org/packages/72/94/63b0fc47eb32792c7ba1fe1b694daec9a63620db1e313033d18140c2320a/gitdb-4.0.12.tar.gz", hash = "sha256:5ef71f855d191a3326fcfbc0d5da835f26b13fbcba60c32c21091c349ffdb571", size = 394684, upload-time = "2025-01-02T07:20:46.413Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a0/61/5c78b91c3143ed5c14207f463aecfc8f9dbb5092fb2869baf37c273b2705/gitdb-4.0.12-py3-none-any.whl", hash = "sha256:67073e15955400952c6565cc3e707c554a4eea2e428946f7a4c162fab9bd9bcf", size = 62794, upload-time = "2025-01-02T07:20:43.624Z" },
]
[[package]]
name = "gitpython"
version = "3.1.50"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "gitdb" },
]
sdist = { url = "https://files.pythonhosted.org/packages/33/f6/354ae6491228b5eb40e10d89c4d13c651fe1cf7556e35ebdded50cff57ce/gitpython-3.1.50.tar.gz", hash = "sha256:80da2d12504d52e1f998772dc5baf6e553f8d2fcfe1fcc226c9d9a2ee3372dcc", size = 219798, upload-time = "2026-05-06T04:01:26.571Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/20/7a/1c6e3562dfd8950adbb11ffbc65d21e7c89d01a6e4f137fa981056de25c5/gitpython-3.1.50-py3-none-any.whl", hash = "sha256:d352abe2908d07355014abdd21ddf798c2a961469239afec4962e9da884858f9", size = 212507, upload-time = "2026-05-06T04:01:23.799Z" },
]
[[package]]
name = "greenlet"
version = "3.5.0"
@@ -458,7 +500,9 @@ 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" },
@@ -466,7 +510,9 @@ 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" },
@@ -474,7 +520,9 @@ 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" },
@@ -641,33 +689,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/dc/2e/a44c20c58aeed0355f2d326969a181696aeb551a25195f47563908a815be/jiter-0.14.0-cp314-cp314t-win_arm64.whl", hash = "sha256:5419d4aa2024961da9fe12a9cfe7484996735dca99e8e090b5c88595ef1951ff", size = 191338, upload-time = "2026-04-10T14:28:02.853Z" },
]
[[package]]
name = "jsonschema"
version = "4.26.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "attrs" },
{ name = "jsonschema-specifications" },
{ name = "referencing" },
{ name = "rpds-py" },
]
sdist = { url = "https://files.pythonhosted.org/packages/b3/fc/e067678238fa451312d4c62bf6e6cf5ec56375422aee02f9cb5f909b3047/jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326", size = 366583, upload-time = "2026-01-07T13:41:07.246Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce", size = 90630, upload-time = "2026-01-07T13:41:05.306Z" },
]
[[package]]
name = "jsonschema-specifications"
version = "2025.9.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "referencing" },
]
sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" },
]
[[package]]
name = "librt"
version = "0.10.0"
@@ -789,6 +810,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/d9/76/7ffc1d3005cf7749123bc47cb3ea343cd97b0ac2211bab40f57283577d0e/lxml_html_clean-0.4.4-py3-none-any.whl", hash = "sha256:ce2ef506614ecb85ee1c5fe0a2aa45b06a19514ec7949e9c8f34f06925cfabcb", size = 14565, upload-time = "2026-02-27T09:35:51.86Z" },
]
[[package]]
name = "markdown-it-py"
version = "4.2.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "mdurl" },
]
sdist = { url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454, upload-time = "2026-05-07T12:08:28.36Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687, upload-time = "2026-05-07T12:08:27.182Z" },
]
[[package]]
name = "markdown2"
version = "2.5.5"
@@ -850,6 +883,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" },
]
[[package]]
name = "mdurl"
version = "0.1.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" },
]
[[package]]
name = "multi-swarm"
version = "0.1.0"
@@ -868,8 +910,8 @@ dependencies = [
{ name = "requests" },
{ name = "scipy" },
{ name = "sqlmodel" },
{ name = "streamlit" },
{ name = "tenacity" },
{ name = "yfinance" },
]
[package.dev-dependencies]
@@ -898,8 +940,8 @@ requires-dist = [
{ name = "requests", specifier = ">=2.32" },
{ name = "scipy", specifier = ">=1.14" },
{ name = "sqlmodel", specifier = ">=0.0.22" },
{ name = "streamlit", specifier = ">=1.40" },
{ name = "tenacity", specifier = ">=9.0" },
{ name = "yfinance", specifier = ">=1.3.0" },
]
[package.metadata.requires-dev]
@@ -994,6 +1036,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/81/08/7036c080d7117f28a4af526d794aab6a84463126db031b007717c1a6676e/multidict-6.7.1-py3-none-any.whl", hash = "sha256:55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56", size = 12319, upload-time = "2026-01-26T02:46:44.004Z" },
]
[[package]]
name = "multitasking"
version = "0.0.13"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/be/c3/ac2cc9307fb15cc28ed6d4a9266b216c83ee7fe64299f0264047982bce88/multitasking-0.0.13.tar.gz", hash = "sha256:d896b5df877c9ca5eeddbf0e5994124694d6cb535aba698fb23344c7025155a1", size = 20585, upload-time = "2026-04-23T12:14:15.049Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d3/1c/24dbf69b247f287401c904a396233a43c89fd4fb9b7cd2e50e430e9cd57c/multitasking-0.0.13-py3-none-any.whl", hash = "sha256:ec9243af140c67bfe52dc98d7173c294512735a88e8425c458b250db99dc2b48", size = 16380, upload-time = "2026-04-23T12:14:13.776Z" },
]
[[package]]
name = "mypy"
version = "2.0.0"
@@ -1255,61 +1306,21 @@ wheels = [
]
[[package]]
name = "pillow"
version = "12.2.0"
name = "peewee"
version = "4.0.5"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/8c/21/c2bcdd5906101a30244eaffc1b6e6ce71a31bd0742a01eb89e660ebfac2d/pillow-12.2.0.tar.gz", hash = "sha256:a830b1a40919539d07806aa58e1b114df53ddd43213d9c8b75847eee6c0182b5", size = 46987819, upload-time = "2026-04-01T14:46:17.687Z" }
sdist = { url = "https://files.pythonhosted.org/packages/21/04/8b1f0388389c0595ccf69fec8723983e598edb0303f6227a08bc5d8cd013/peewee-4.0.5.tar.gz", hash = "sha256:b43a21493f19f205a016cb4a9e29c7eca3500576d29447a89732022d193450ba", size = 721062, upload-time = "2026-04-23T21:17:29.419Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/4a/01/53d10cf0dbad820a8db274d259a37ba50b88b24768ddccec07355382d5ad/pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:8297651f5b5679c19968abefd6bb84d95fe30ef712eb1b2d9b2d31ca61267f4c", size = 4100837, upload-time = "2026-04-01T14:43:41.506Z" },
{ url = "https://files.pythonhosted.org/packages/0f/98/f3a6657ecb698c937f6c76ee564882945f29b79bad496abcba0e84659ec5/pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:50d8520da2a6ce0af445fa6d648c4273c3eeefbc32d7ce049f22e8b5c3daecc2", size = 4176528, upload-time = "2026-04-01T14:43:43.773Z" },
{ url = "https://files.pythonhosted.org/packages/69/bc/8986948f05e3ea490b8442ea1c1d4d990b24a7e43d8a51b2c7d8b1dced36/pillow-12.2.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:766cef22385fa1091258ad7e6216792b156dc16d8d3fa607e7545b2b72061f1c", size = 3640401, upload-time = "2026-04-01T14:43:45.87Z" },
{ url = "https://files.pythonhosted.org/packages/34/46/6c717baadcd62bc8ed51d238d521ab651eaa74838291bda1f86fe1f864c9/pillow-12.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5d2fd0fa6b5d9d1de415060363433f28da8b1526c1c129020435e186794b3795", size = 5308094, upload-time = "2026-04-01T14:43:48.438Z" },
{ url = "https://files.pythonhosted.org/packages/71/43/905a14a8b17fdb1ccb58d282454490662d2cb89a6bfec26af6d3520da5ec/pillow-12.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:56b25336f502b6ed02e889f4ece894a72612fe885889a6e8c4c80239ff6e5f5f", size = 4695402, upload-time = "2026-04-01T14:43:51.292Z" },
{ url = "https://files.pythonhosted.org/packages/73/dd/42107efcb777b16fa0393317eac58f5b5cf30e8392e266e76e51cff28c3d/pillow-12.2.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f1c943e96e85df3d3478f7b691f229887e143f81fedab9b20205349ab04d73ed", size = 6280005, upload-time = "2026-04-01T14:43:54.242Z" },
{ url = "https://files.pythonhosted.org/packages/a8/68/b93e09e5e8549019e61acf49f65b1a8530765a7f812c77a7461bca7e4494/pillow-12.2.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:03f6fab9219220f041c74aeaa2939ff0062bd5c364ba9ce037197f4c6d498cd9", size = 8090669, upload-time = "2026-04-01T14:43:57.335Z" },
{ url = "https://files.pythonhosted.org/packages/4b/6e/3ccb54ce8ec4ddd1accd2d89004308b7b0b21c4ac3d20fa70af4760a4330/pillow-12.2.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5cdfebd752ec52bf5bb4e35d9c64b40826bc5b40a13df7c3cda20a2c03a0f5ed", size = 6395194, upload-time = "2026-04-01T14:43:59.864Z" },
{ url = "https://files.pythonhosted.org/packages/67/ee/21d4e8536afd1a328f01b359b4d3997b291ffd35a237c877b331c1c3b71c/pillow-12.2.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eedf4b74eda2b5a4b2b2fb4c006d6295df3bf29e459e198c90ea48e130dc75c3", size = 7082423, upload-time = "2026-04-01T14:44:02.74Z" },
{ url = "https://files.pythonhosted.org/packages/78/5f/e9f86ab0146464e8c133fe85df987ed9e77e08b29d8d35f9f9f4d6f917ba/pillow-12.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:00a2865911330191c0b818c59103b58a5e697cae67042366970a6b6f1b20b7f9", size = 6505667, upload-time = "2026-04-01T14:44:05.381Z" },
{ url = "https://files.pythonhosted.org/packages/ed/1e/409007f56a2fdce61584fd3acbc2bbc259857d555196cedcadc68c015c82/pillow-12.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1e1757442ed87f4912397c6d35a0db6a7b52592156014706f17658ff58bbf795", size = 7208580, upload-time = "2026-04-01T14:44:08.39Z" },
{ url = "https://files.pythonhosted.org/packages/23/c4/7349421080b12fb35414607b8871e9534546c128a11965fd4a7002ccfbee/pillow-12.2.0-cp313-cp313-win32.whl", hash = "sha256:144748b3af2d1b358d41286056d0003f47cb339b8c43a9ea42f5fea4d8c66b6e", size = 6375896, upload-time = "2026-04-01T14:44:11.197Z" },
{ url = "https://files.pythonhosted.org/packages/3f/82/8a3739a5e470b3c6cbb1d21d315800d8e16bff503d1f16b03a4ec3212786/pillow-12.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:390ede346628ccc626e5730107cde16c42d3836b89662a115a921f28440e6a3b", size = 7081266, upload-time = "2026-04-01T14:44:13.947Z" },
{ url = "https://files.pythonhosted.org/packages/c3/25/f968f618a062574294592f668218f8af564830ccebdd1fa6200f598e65c5/pillow-12.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:8023abc91fba39036dbce14a7d6535632f99c0b857807cbbbf21ecc9f4717f06", size = 2463508, upload-time = "2026-04-01T14:44:16.312Z" },
{ url = "https://files.pythonhosted.org/packages/4d/a4/b342930964e3cb4dce5038ae34b0eab4653334995336cd486c5a8c25a00c/pillow-12.2.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:042db20a421b9bafecc4b84a8b6e444686bd9d836c7fd24542db3e7df7baad9b", size = 5309927, upload-time = "2026-04-01T14:44:18.89Z" },
{ url = "https://files.pythonhosted.org/packages/9f/de/23198e0a65a9cf06123f5435a5d95cea62a635697f8f03d134d3f3a96151/pillow-12.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:dd025009355c926a84a612fecf58bb315a3f6814b17ead51a8e48d3823d9087f", size = 4698624, upload-time = "2026-04-01T14:44:21.115Z" },
{ url = "https://files.pythonhosted.org/packages/01/a6/1265e977f17d93ea37aa28aa81bad4fa597933879fac2520d24e021c8da3/pillow-12.2.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:88ddbc66737e277852913bd1e07c150cc7bb124539f94c4e2df5344494e0a612", size = 6321252, upload-time = "2026-04-01T14:44:23.663Z" },
{ url = "https://files.pythonhosted.org/packages/3c/83/5982eb4a285967baa70340320be9f88e57665a387e3a53a7f0db8231a0cd/pillow-12.2.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d362d1878f00c142b7e1a16e6e5e780f02be8195123f164edf7eddd911eefe7c", size = 8126550, upload-time = "2026-04-01T14:44:26.772Z" },
{ url = "https://files.pythonhosted.org/packages/4e/48/6ffc514adce69f6050d0753b1a18fd920fce8cac87620d5a31231b04bfc5/pillow-12.2.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c727a6d53cb0018aadd8018c2b938376af27914a68a492f59dfcaca650d5eea", size = 6433114, upload-time = "2026-04-01T14:44:29.615Z" },
{ url = "https://files.pythonhosted.org/packages/36/a3/f9a77144231fb8d40ee27107b4463e205fa4677e2ca2548e14da5cf18dce/pillow-12.2.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:efd8c21c98c5cc60653bcb311bef2ce0401642b7ce9d09e03a7da87c878289d4", size = 7115667, upload-time = "2026-04-01T14:44:32.773Z" },
{ url = "https://files.pythonhosted.org/packages/c1/fc/ac4ee3041e7d5a565e1c4fd72a113f03b6394cc72ab7089d27608f8aaccb/pillow-12.2.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9f08483a632889536b8139663db60f6724bfcb443c96f1b18855860d7d5c0fd4", size = 6538966, upload-time = "2026-04-01T14:44:35.252Z" },
{ url = "https://files.pythonhosted.org/packages/c0/a8/27fb307055087f3668f6d0a8ccb636e7431d56ed0750e07a60547b1e083e/pillow-12.2.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dac8d77255a37e81a2efcbd1fc05f1c15ee82200e6c240d7e127e25e365c39ea", size = 7238241, upload-time = "2026-04-01T14:44:37.875Z" },
{ url = "https://files.pythonhosted.org/packages/ad/4b/926ab182c07fccae9fcb120043464e1ff1564775ec8864f21a0ebce6ac25/pillow-12.2.0-cp313-cp313t-win32.whl", hash = "sha256:ee3120ae9dff32f121610bb08e4313be87e03efeadfc6c0d18f89127e24d0c24", size = 6379592, upload-time = "2026-04-01T14:44:40.336Z" },
{ url = "https://files.pythonhosted.org/packages/c2/c4/f9e476451a098181b30050cc4c9a3556b64c02cf6497ea421ac047e89e4b/pillow-12.2.0-cp313-cp313t-win_amd64.whl", hash = "sha256:325ca0528c6788d2a6c3d40e3568639398137346c3d6e66bb61db96b96511c98", size = 7085542, upload-time = "2026-04-01T14:44:43.251Z" },
{ url = "https://files.pythonhosted.org/packages/00/a4/285f12aeacbe2d6dc36c407dfbbe9e96d4a80b0fb710a337f6d2ad978c75/pillow-12.2.0-cp313-cp313t-win_arm64.whl", hash = "sha256:2e5a76d03a6c6dcef67edabda7a52494afa4035021a79c8558e14af25313d453", size = 2465765, upload-time = "2026-04-01T14:44:45.996Z" },
{ url = "https://files.pythonhosted.org/packages/bf/98/4595daa2365416a86cb0d495248a393dfc84e96d62ad080c8546256cb9c0/pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:3adc9215e8be0448ed6e814966ecf3d9952f0ea40eb14e89a102b87f450660d8", size = 4100848, upload-time = "2026-04-01T14:44:48.48Z" },
{ url = "https://files.pythonhosted.org/packages/0b/79/40184d464cf89f6663e18dfcf7ca21aae2491fff1a16127681bf1fa9b8cf/pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:6a9adfc6d24b10f89588096364cc726174118c62130c817c2837c60cf08a392b", size = 4176515, upload-time = "2026-04-01T14:44:51.353Z" },
{ url = "https://files.pythonhosted.org/packages/b0/63/703f86fd4c422a9cf722833670f4f71418fb116b2853ff7da722ea43f184/pillow-12.2.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:6a6e67ea2e6feda684ed370f9a1c52e7a243631c025ba42149a2cc5934dec295", size = 3640159, upload-time = "2026-04-01T14:44:53.588Z" },
{ url = "https://files.pythonhosted.org/packages/71/e0/fb22f797187d0be2270f83500aab851536101b254bfa1eae10795709d283/pillow-12.2.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2bb4a8d594eacdfc59d9e5ad972aa8afdd48d584ffd5f13a937a664c3e7db0ed", size = 5312185, upload-time = "2026-04-01T14:44:56.039Z" },
{ url = "https://files.pythonhosted.org/packages/ba/8c/1a9e46228571de18f8e28f16fabdfc20212a5d019f3e3303452b3f0a580d/pillow-12.2.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:80b2da48193b2f33ed0c32c38140f9d3186583ce7d516526d462645fd98660ae", size = 4695386, upload-time = "2026-04-01T14:44:58.663Z" },
{ url = "https://files.pythonhosted.org/packages/70/62/98f6b7f0c88b9addd0e87c217ded307b36be024d4ff8869a812b241d1345/pillow-12.2.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:22db17c68434de69d8ecfc2fe821569195c0c373b25cccb9cbdacf2c6e53c601", size = 6280384, upload-time = "2026-04-01T14:45:01.5Z" },
{ url = "https://files.pythonhosted.org/packages/5e/03/688747d2e91cfbe0e64f316cd2e8005698f76ada3130d0194664174fa5de/pillow-12.2.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7b14cc0106cd9aecda615dd6903840a058b4700fcb817687d0ee4fc8b6e389be", size = 8091599, upload-time = "2026-04-01T14:45:04.5Z" },
{ url = "https://files.pythonhosted.org/packages/f6/35/577e22b936fcdd66537329b33af0b4ccfefaeabd8aec04b266528cddb33c/pillow-12.2.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cbeb542b2ebc6fcdacabf8aca8c1a97c9b3ad3927d46b8723f9d4f033288a0f", size = 6396021, upload-time = "2026-04-01T14:45:07.117Z" },
{ url = "https://files.pythonhosted.org/packages/11/8d/d2532ad2a603ca2b93ad9f5135732124e57811d0168155852f37fbce2458/pillow-12.2.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4bfd07bc812fbd20395212969e41931001fd59eb55a60658b0e5710872e95286", size = 7083360, upload-time = "2026-04-01T14:45:09.763Z" },
{ url = "https://files.pythonhosted.org/packages/5e/26/d325f9f56c7e039034897e7380e9cc202b1e368bfd04d4cbe6a441f02885/pillow-12.2.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9aba9a17b623ef750a4d11b742cbafffeb48a869821252b30ee21b5e91392c50", size = 6507628, upload-time = "2026-04-01T14:45:12.378Z" },
{ url = "https://files.pythonhosted.org/packages/5f/f7/769d5632ffb0988f1c5e7660b3e731e30f7f8ec4318e94d0a5d674eb65a4/pillow-12.2.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:deede7c263feb25dba4e82ea23058a235dcc2fe1f6021025dc71f2b618e26104", size = 7209321, upload-time = "2026-04-01T14:45:15.122Z" },
{ url = "https://files.pythonhosted.org/packages/6a/7a/c253e3c645cd47f1aceea6a8bacdba9991bf45bb7dfe927f7c893e89c93c/pillow-12.2.0-cp314-cp314-win32.whl", hash = "sha256:632ff19b2778e43162304d50da0181ce24ac5bb8180122cbe1bf4673428328c7", size = 6479723, upload-time = "2026-04-01T14:45:17.797Z" },
{ url = "https://files.pythonhosted.org/packages/cd/8b/601e6566b957ca50e28725cb6c355c59c2c8609751efbecd980db44e0349/pillow-12.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:4e6c62e9d237e9b65fac06857d511e90d8461a32adcc1b9065ea0c0fa3a28150", size = 7217400, upload-time = "2026-04-01T14:45:20.529Z" },
{ url = "https://files.pythonhosted.org/packages/d6/94/220e46c73065c3e2951bb91c11a1fb636c8c9ad427ac3ce7d7f3359b9b2f/pillow-12.2.0-cp314-cp314-win_arm64.whl", hash = "sha256:b1c1fbd8a5a1af3412a0810d060a78b5136ec0836c8a4ef9aa11807f2a22f4e1", size = 2554835, upload-time = "2026-04-01T14:45:23.162Z" },
{ url = "https://files.pythonhosted.org/packages/b6/ab/1b426a3974cb0e7da5c29ccff4807871d48110933a57207b5a676cccc155/pillow-12.2.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:57850958fe9c751670e49b2cecf6294acc99e562531f4bd317fa5ddee2068463", size = 5314225, upload-time = "2026-04-01T14:45:25.637Z" },
{ url = "https://files.pythonhosted.org/packages/19/1e/dce46f371be2438eecfee2a1960ee2a243bbe5e961890146d2dee1ff0f12/pillow-12.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d5d38f1411c0ed9f97bcb49b7bd59b6b7c314e0e27420e34d99d844b9ce3b6f3", size = 4698541, upload-time = "2026-04-01T14:45:28.355Z" },
{ url = "https://files.pythonhosted.org/packages/55/c3/7fbecf70adb3a0c33b77a300dc52e424dc22ad8cdc06557a2e49523b703d/pillow-12.2.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5c0a9f29ca8e79f09de89293f82fc9b0270bb4af1d58bc98f540cc4aedf03166", size = 6322251, upload-time = "2026-04-01T14:45:30.924Z" },
{ url = "https://files.pythonhosted.org/packages/1c/3c/7fbc17cfb7e4fe0ef1642e0abc17fc6c94c9f7a16be41498e12e2ba60408/pillow-12.2.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1610dd6c61621ae1cf811bef44d77e149ce3f7b95afe66a4512f8c59f25d9ebe", size = 8127807, upload-time = "2026-04-01T14:45:33.908Z" },
{ url = "https://files.pythonhosted.org/packages/ff/c3/a8ae14d6defd2e448493ff512fae903b1e9bd40b72efb6ec55ce0048c8ce/pillow-12.2.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a34329707af4f73cf1782a36cd2289c0368880654a2c11f027bcee9052d35dd", size = 6433935, upload-time = "2026-04-01T14:45:36.623Z" },
{ url = "https://files.pythonhosted.org/packages/6e/32/2880fb3a074847ac159d8f902cb43278a61e85f681661e7419e6596803ed/pillow-12.2.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e9c4f5b3c546fa3458a29ab22646c1c6c787ea8f5ef51300e5a60300736905e", size = 7116720, upload-time = "2026-04-01T14:45:39.258Z" },
{ url = "https://files.pythonhosted.org/packages/46/87/495cc9c30e0129501643f24d320076f4cc54f718341df18cc70ec94c44e1/pillow-12.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fb043ee2f06b41473269765c2feae53fc2e2fbf96e5e22ca94fb5ad677856f06", size = 6540498, upload-time = "2026-04-01T14:45:41.879Z" },
{ url = "https://files.pythonhosted.org/packages/18/53/773f5edca692009d883a72211b60fdaf8871cbef075eaa9d577f0a2f989e/pillow-12.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f278f034eb75b4e8a13a54a876cc4a5ab39173d2cdd93a638e1b467fc545ac43", size = 7239413, upload-time = "2026-04-01T14:45:44.705Z" },
{ url = "https://files.pythonhosted.org/packages/c9/e4/4b64a97d71b2a83158134abbb2f5bd3f8a2ea691361282f010998f339ec7/pillow-12.2.0-cp314-cp314t-win32.whl", hash = "sha256:6bb77b2dcb06b20f9f4b4a8454caa581cd4dd0643a08bacf821216a16d9c8354", size = 6482084, upload-time = "2026-04-01T14:45:47.568Z" },
{ url = "https://files.pythonhosted.org/packages/ba/13/306d275efd3a3453f72114b7431c877d10b1154014c1ebbedd067770d629/pillow-12.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:6562ace0d3fb5f20ed7290f1f929cae41b25ae29528f2af1722966a0a02e2aa1", size = 7225152, upload-time = "2026-04-01T14:45:50.032Z" },
{ url = "https://files.pythonhosted.org/packages/ff/6e/cf826fae916b8658848d7b9f38d88da6396895c676e8086fc0988073aaf8/pillow-12.2.0-cp314-cp314t-win_arm64.whl", hash = "sha256:aa88ccfe4e32d362816319ed727a004423aab09c5cea43c01a4b435643fa34eb", size = 2556579, upload-time = "2026-04-01T14:45:52.529Z" },
{ url = "https://files.pythonhosted.org/packages/ef/d4/2cf83b5fb7fae8939ed596009465c6de0b8fafa70abd486ed9d192e457f7/peewee-4.0.5-py3-none-any.whl", hash = "sha256:05bf687660f04f925bcb7d744e5af826ca526322f2fad894bfa4f1d0f3bcaf50", size = 144479, upload-time = "2026-04-23T21:17:28.061Z" },
]
[[package]]
name = "platformdirs"
version = "4.9.6"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/9f/4a/0883b8e3802965322523f0b200ecf33d31f10991d0401162f4b23c698b42/platformdirs-4.9.6.tar.gz", hash = "sha256:3bfa75b0ad0db84096ae777218481852c0ebc6c727b3168c1b9e0118e458cf0a", size = 29400, upload-time = "2026-04-09T00:04:10.812Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/75/a6/a0a304dc33b49145b21f4808d763822111e67d1c3a32b524a1baf947b6e1/platformdirs-4.9.6-py3-none-any.whl", hash = "sha256:e61adb1d5e5cb3441b4b7710bea7e4c12250ca49439228cc1021c00dcfac0917", size = 21348, upload-time = "2026-04-09T00:04:09.463Z" },
]
[[package]]
@@ -1462,6 +1473,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/51/be/6f79d55816d5c22557cf27533543d5d70dfe692adfbee4b99f2760674f38/pyarrow-24.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:c91d00057f23b8d353039520dc3a6c09d8608164c692e9f59a175a42b2ae0c19", size = 28131282, upload-time = "2026-04-21T10:51:16.815Z" },
]
[[package]]
name = "pycparser"
version = "3.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" },
]
[[package]]
name = "pydantic"
version = "2.13.4"
@@ -1547,19 +1567,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/ae/8d/f1af3832f5e6eb13ba94ee809e72b8ecb5eef226d27ee0bef7d963d943c7/pydantic_settings-2.14.1-py3-none-any.whl", hash = "sha256:6e3c7edfd8277687cdc598f56e5cff0e9bfff0910a3749deaa8d4401c3a2b9de", size = 60964, upload-time = "2026-05-08T13:40:04.958Z" },
]
[[package]]
name = "pydeck"
version = "0.9.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "jinja2" },
{ name = "numpy" },
]
sdist = { url = "https://files.pythonhosted.org/packages/eb/df/4e9e7f20f8034a37c6571c93809f6d22388c39978c98d174d656c1a18fd2/pydeck-0.9.2.tar.gz", hash = "sha256:c10d9035e81ead6385264cac8d19402471f6866a15ca1f7df1400f52142bcf87", size = 5849672, upload-time = "2026-04-16T18:30:30.089Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/88/24/b30ee7d723100fd822de1bb4c0adea62f3419884a75a536f35f355d1e7c0/pydeck-0.9.2-py2.py3-none-any.whl", hash = "sha256:8213dfeacc5f6bfe6825f61c8ee34e3850e8a31fc43924379ec98edb34a75b25", size = 11305615, upload-time = "2026-04-16T18:30:28.133Z" },
]
[[package]]
name = "pygments"
version = "2.20.0"
@@ -1669,6 +1676,15 @@ asyncio-client = [
{ name = "aiohttp" },
]
[[package]]
name = "pytz"
version = "2026.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/ff/46/dd499ec9038423421951e4fad73051febaa13d2df82b4064f87af8b8c0c3/pytz-2026.2.tar.gz", hash = "sha256:0e60b47b29f21574376f218fe21abc009894a2321ea16c6754f3cad6eb7cdd6a", size = 320861, upload-time = "2026-05-04T01:35:29.667Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ec/dd/96da98f892250475bdf2328112d7468abdd4acc7b902b6af23f4ed958ea0/pytz-2026.2-py2.py3-none-any.whl", hash = "sha256:04156e608bee23d3792fd45c94ae47fae1036688e75032eea2e3bf0323d1f126", size = 510141, upload-time = "2026-05-04T01:35:27.408Z" },
]
[[package]]
name = "pyyaml"
version = "6.0.3"
@@ -1705,19 +1721,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" },
]
[[package]]
name = "referencing"
version = "0.37.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "attrs" },
{ name = "rpds-py" },
]
sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" },
]
[[package]]
name = "requests"
version = "2.33.1"
@@ -1748,69 +1751,16 @@ wheels = [
]
[[package]]
name = "rpds-py"
version = "0.30.0"
name = "rich"
version = "15.0.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/20/af/3f2f423103f1113b36230496629986e0ef7e199d2aa8392452b484b38ced/rpds_py-0.30.0.tar.gz", hash = "sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84", size = 69469, upload-time = "2025-11-30T20:24:38.837Z" }
dependencies = [
{ name = "markdown-it-py" },
{ name = "pygments" },
]
sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ed/dc/d61221eb88ff410de3c49143407f6f3147acf2538c86f2ab7ce65ae7d5f9/rpds_py-0.30.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f83424d738204d9770830d35290ff3273fbb02b41f919870479fab14b9d303b2", size = 374887, upload-time = "2025-11-30T20:22:41.812Z" },
{ url = "https://files.pythonhosted.org/packages/fd/32/55fb50ae104061dbc564ef15cc43c013dc4a9f4527a1f4d99baddf56fe5f/rpds_py-0.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7536cd91353c5273434b4e003cbda89034d67e7710eab8761fd918ec6c69cf8", size = 358904, upload-time = "2025-11-30T20:22:43.479Z" },
{ url = "https://files.pythonhosted.org/packages/58/70/faed8186300e3b9bdd138d0273109784eea2396c68458ed580f885dfe7ad/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2771c6c15973347f50fece41fc447c054b7ac2ae0502388ce3b6738cd366e3d4", size = 389945, upload-time = "2025-11-30T20:22:44.819Z" },
{ url = "https://files.pythonhosted.org/packages/bd/a8/073cac3ed2c6387df38f71296d002ab43496a96b92c823e76f46b8af0543/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0a59119fc6e3f460315fe9d08149f8102aa322299deaa5cab5b40092345c2136", size = 407783, upload-time = "2025-11-30T20:22:46.103Z" },
{ url = "https://files.pythonhosted.org/packages/77/57/5999eb8c58671f1c11eba084115e77a8899d6e694d2a18f69f0ba471ec8b/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:76fec018282b4ead0364022e3c54b60bf368b9d926877957a8624b58419169b7", size = 515021, upload-time = "2025-11-30T20:22:47.458Z" },
{ url = "https://files.pythonhosted.org/packages/e0/af/5ab4833eadc36c0a8ed2bc5c0de0493c04f6c06de223170bd0798ff98ced/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:692bef75a5525db97318e8cd061542b5a79812d711ea03dbc1f6f8dbb0c5f0d2", size = 414589, upload-time = "2025-11-30T20:22:48.872Z" },
{ url = "https://files.pythonhosted.org/packages/b7/de/f7192e12b21b9e9a68a6d0f249b4af3fdcdff8418be0767a627564afa1f1/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9027da1ce107104c50c81383cae773ef5c24d296dd11c99e2629dbd7967a20c6", size = 394025, upload-time = "2025-11-30T20:22:50.196Z" },
{ url = "https://files.pythonhosted.org/packages/91/c4/fc70cd0249496493500e7cc2de87504f5aa6509de1e88623431fec76d4b6/rpds_py-0.30.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:9cf69cdda1f5968a30a359aba2f7f9aa648a9ce4b580d6826437f2b291cfc86e", size = 408895, upload-time = "2025-11-30T20:22:51.87Z" },
{ url = "https://files.pythonhosted.org/packages/58/95/d9275b05ab96556fefff73a385813eb66032e4c99f411d0795372d9abcea/rpds_py-0.30.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a4796a717bf12b9da9d3ad002519a86063dcac8988b030e405704ef7d74d2d9d", size = 422799, upload-time = "2025-11-30T20:22:53.341Z" },
{ url = "https://files.pythonhosted.org/packages/06/c1/3088fc04b6624eb12a57eb814f0d4997a44b0d208d6cace713033ff1a6ba/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5d4c2aa7c50ad4728a094ebd5eb46c452e9cb7edbfdb18f9e1221f597a73e1e7", size = 572731, upload-time = "2025-11-30T20:22:54.778Z" },
{ url = "https://files.pythonhosted.org/packages/d8/42/c612a833183b39774e8ac8fecae81263a68b9583ee343db33ab571a7ce55/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ba81a9203d07805435eb06f536d95a266c21e5b2dfbf6517748ca40c98d19e31", size = 599027, upload-time = "2025-11-30T20:22:56.212Z" },
{ url = "https://files.pythonhosted.org/packages/5f/60/525a50f45b01d70005403ae0e25f43c0384369ad24ffe46e8d9068b50086/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:945dccface01af02675628334f7cf49c2af4c1c904748efc5cf7bbdf0b579f95", size = 563020, upload-time = "2025-11-30T20:22:58.2Z" },
{ url = "https://files.pythonhosted.org/packages/0b/5d/47c4655e9bcd5ca907148535c10e7d489044243cc9941c16ed7cd53be91d/rpds_py-0.30.0-cp313-cp313-win32.whl", hash = "sha256:b40fb160a2db369a194cb27943582b38f79fc4887291417685f3ad693c5a1d5d", size = 223139, upload-time = "2025-11-30T20:23:00.209Z" },
{ url = "https://files.pythonhosted.org/packages/f2/e1/485132437d20aa4d3e1d8b3fb5a5e65aa8139f1e097080c2a8443201742c/rpds_py-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:806f36b1b605e2d6a72716f321f20036b9489d29c51c91f4dd29a3e3afb73b15", size = 240224, upload-time = "2025-11-30T20:23:02.008Z" },
{ url = "https://files.pythonhosted.org/packages/24/95/ffd128ed1146a153d928617b0ef673960130be0009c77d8fbf0abe306713/rpds_py-0.30.0-cp313-cp313-win_arm64.whl", hash = "sha256:d96c2086587c7c30d44f31f42eae4eac89b60dabbac18c7669be3700f13c3ce1", size = 230645, upload-time = "2025-11-30T20:23:03.43Z" },
{ url = "https://files.pythonhosted.org/packages/ff/1b/b10de890a0def2a319a2626334a7f0ae388215eb60914dbac8a3bae54435/rpds_py-0.30.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:eb0b93f2e5c2189ee831ee43f156ed34e2a89a78a66b98cadad955972548be5a", size = 364443, upload-time = "2025-11-30T20:23:04.878Z" },
{ url = "https://files.pythonhosted.org/packages/0d/bf/27e39f5971dc4f305a4fb9c672ca06f290f7c4e261c568f3dea16a410d47/rpds_py-0.30.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:922e10f31f303c7c920da8981051ff6d8c1a56207dbdf330d9047f6d30b70e5e", size = 353375, upload-time = "2025-11-30T20:23:06.342Z" },
{ url = "https://files.pythonhosted.org/packages/40/58/442ada3bba6e8e6615fc00483135c14a7538d2ffac30e2d933ccf6852232/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdc62c8286ba9bf7f47befdcea13ea0e26bf294bda99758fd90535cbaf408000", size = 383850, upload-time = "2025-11-30T20:23:07.825Z" },
{ url = "https://files.pythonhosted.org/packages/14/14/f59b0127409a33c6ef6f5c1ebd5ad8e32d7861c9c7adfa9a624fc3889f6c/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:47f9a91efc418b54fb8190a6b4aa7813a23fb79c51f4bb84e418f5476c38b8db", size = 392812, upload-time = "2025-11-30T20:23:09.228Z" },
{ url = "https://files.pythonhosted.org/packages/b3/66/e0be3e162ac299b3a22527e8913767d869e6cc75c46bd844aa43fb81ab62/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f3587eb9b17f3789ad50824084fa6f81921bbf9a795826570bda82cb3ed91f2", size = 517841, upload-time = "2025-11-30T20:23:11.186Z" },
{ url = "https://files.pythonhosted.org/packages/3d/55/fa3b9cf31d0c963ecf1ba777f7cf4b2a2c976795ac430d24a1f43d25a6ba/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39c02563fc592411c2c61d26b6c5fe1e51eaa44a75aa2c8735ca88b0d9599daa", size = 408149, upload-time = "2025-11-30T20:23:12.864Z" },
{ url = "https://files.pythonhosted.org/packages/60/ca/780cf3b1a32b18c0f05c441958d3758f02544f1d613abf9488cd78876378/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51a1234d8febafdfd33a42d97da7a43f5dcb120c1060e352a3fbc0c6d36e2083", size = 383843, upload-time = "2025-11-30T20:23:14.638Z" },
{ url = "https://files.pythonhosted.org/packages/82/86/d5f2e04f2aa6247c613da0c1dd87fcd08fa17107e858193566048a1e2f0a/rpds_py-0.30.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:eb2c4071ab598733724c08221091e8d80e89064cd472819285a9ab0f24bcedb9", size = 396507, upload-time = "2025-11-30T20:23:16.105Z" },
{ url = "https://files.pythonhosted.org/packages/4b/9a/453255d2f769fe44e07ea9785c8347edaf867f7026872e76c1ad9f7bed92/rpds_py-0.30.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6bdfdb946967d816e6adf9a3d8201bfad269c67efe6cefd7093ef959683c8de0", size = 414949, upload-time = "2025-11-30T20:23:17.539Z" },
{ url = "https://files.pythonhosted.org/packages/a3/31/622a86cdc0c45d6df0e9ccb6becdba5074735e7033c20e401a6d9d0e2ca0/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c77afbd5f5250bf27bf516c7c4a016813eb2d3e116139aed0096940c5982da94", size = 565790, upload-time = "2025-11-30T20:23:19.029Z" },
{ url = "https://files.pythonhosted.org/packages/1c/5d/15bbf0fb4a3f58a3b1c67855ec1efcc4ceaef4e86644665fff03e1b66d8d/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:61046904275472a76c8c90c9ccee9013d70a6d0f73eecefd38c1ae7c39045a08", size = 590217, upload-time = "2025-11-30T20:23:20.885Z" },
{ url = "https://files.pythonhosted.org/packages/6d/61/21b8c41f68e60c8cc3b2e25644f0e3681926020f11d06ab0b78e3c6bbff1/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c5f36a861bc4b7da6516dbdf302c55313afa09b81931e8280361a4f6c9a2d27", size = 555806, upload-time = "2025-11-30T20:23:22.488Z" },
{ url = "https://files.pythonhosted.org/packages/f9/39/7e067bb06c31de48de3eb200f9fc7c58982a4d3db44b07e73963e10d3be9/rpds_py-0.30.0-cp313-cp313t-win32.whl", hash = "sha256:3d4a69de7a3e50ffc214ae16d79d8fbb0922972da0356dcf4d0fdca2878559c6", size = 211341, upload-time = "2025-11-30T20:23:24.449Z" },
{ url = "https://files.pythonhosted.org/packages/0a/4d/222ef0b46443cf4cf46764d9c630f3fe4abaa7245be9417e56e9f52b8f65/rpds_py-0.30.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f14fc5df50a716f7ece6a80b6c78bb35ea2ca47c499e422aa4463455dd96d56d", size = 225768, upload-time = "2025-11-30T20:23:25.908Z" },
{ url = "https://files.pythonhosted.org/packages/86/81/dad16382ebbd3d0e0328776d8fd7ca94220e4fa0798d1dc5e7da48cb3201/rpds_py-0.30.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:68f19c879420aa08f61203801423f6cd5ac5f0ac4ac82a2368a9fcd6a9a075e0", size = 362099, upload-time = "2025-11-30T20:23:27.316Z" },
{ url = "https://files.pythonhosted.org/packages/2b/60/19f7884db5d5603edf3c6bce35408f45ad3e97e10007df0e17dd57af18f8/rpds_py-0.30.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ec7c4490c672c1a0389d319b3a9cfcd098dcdc4783991553c332a15acf7249be", size = 353192, upload-time = "2025-11-30T20:23:29.151Z" },
{ url = "https://files.pythonhosted.org/packages/bf/c4/76eb0e1e72d1a9c4703c69607cec123c29028bff28ce41588792417098ac/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f251c812357a3fed308d684a5079ddfb9d933860fc6de89f2b7ab00da481e65f", size = 384080, upload-time = "2025-11-30T20:23:30.785Z" },
{ url = "https://files.pythonhosted.org/packages/72/87/87ea665e92f3298d1b26d78814721dc39ed8d2c74b86e83348d6b48a6f31/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac98b175585ecf4c0348fd7b29c3864bda53b805c773cbf7bfdaffc8070c976f", size = 394841, upload-time = "2025-11-30T20:23:32.209Z" },
{ url = "https://files.pythonhosted.org/packages/77/ad/7783a89ca0587c15dcbf139b4a8364a872a25f861bdb88ed99f9b0dec985/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3e62880792319dbeb7eb866547f2e35973289e7d5696c6e295476448f5b63c87", size = 516670, upload-time = "2025-11-30T20:23:33.742Z" },
{ url = "https://files.pythonhosted.org/packages/5b/3c/2882bdac942bd2172f3da574eab16f309ae10a3925644e969536553cb4ee/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e7fc54e0900ab35d041b0601431b0a0eb495f0851a0639b6ef90f7741b39a18", size = 408005, upload-time = "2025-11-30T20:23:35.253Z" },
{ url = "https://files.pythonhosted.org/packages/ce/81/9a91c0111ce1758c92516a3e44776920b579d9a7c09b2b06b642d4de3f0f/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47e77dc9822d3ad616c3d5759ea5631a75e5809d5a28707744ef79d7a1bcfcad", size = 382112, upload-time = "2025-11-30T20:23:36.842Z" },
{ url = "https://files.pythonhosted.org/packages/cf/8e/1da49d4a107027e5fbc64daeab96a0706361a2918da10cb41769244b805d/rpds_py-0.30.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:b4dc1a6ff022ff85ecafef7979a2c6eb423430e05f1165d6688234e62ba99a07", size = 399049, upload-time = "2025-11-30T20:23:38.343Z" },
{ url = "https://files.pythonhosted.org/packages/df/5a/7ee239b1aa48a127570ec03becbb29c9d5a9eb092febbd1699d567cae859/rpds_py-0.30.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4559c972db3a360808309e06a74628b95eaccbf961c335c8fe0d590cf587456f", size = 415661, upload-time = "2025-11-30T20:23:40.263Z" },
{ url = "https://files.pythonhosted.org/packages/70/ea/caa143cf6b772f823bc7929a45da1fa83569ee49b11d18d0ada7f5ee6fd6/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0ed177ed9bded28f8deb6ab40c183cd1192aa0de40c12f38be4d59cd33cb5c65", size = 565606, upload-time = "2025-11-30T20:23:42.186Z" },
{ url = "https://files.pythonhosted.org/packages/64/91/ac20ba2d69303f961ad8cf55bf7dbdb4763f627291ba3d0d7d67333cced9/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ad1fa8db769b76ea911cb4e10f049d80bf518c104f15b3edb2371cc65375c46f", size = 591126, upload-time = "2025-11-30T20:23:44.086Z" },
{ url = "https://files.pythonhosted.org/packages/21/20/7ff5f3c8b00c8a95f75985128c26ba44503fb35b8e0259d812766ea966c7/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:46e83c697b1f1c72b50e5ee5adb4353eef7406fb3f2043d64c33f20ad1c2fc53", size = 553371, upload-time = "2025-11-30T20:23:46.004Z" },
{ url = "https://files.pythonhosted.org/packages/72/c7/81dadd7b27c8ee391c132a6b192111ca58d866577ce2d9b0ca157552cce0/rpds_py-0.30.0-cp314-cp314-win32.whl", hash = "sha256:ee454b2a007d57363c2dfd5b6ca4a5d7e2c518938f8ed3b706e37e5d470801ed", size = 215298, upload-time = "2025-11-30T20:23:47.696Z" },
{ url = "https://files.pythonhosted.org/packages/3e/d2/1aaac33287e8cfb07aab2e6b8ac1deca62f6f65411344f1433c55e6f3eb8/rpds_py-0.30.0-cp314-cp314-win_amd64.whl", hash = "sha256:95f0802447ac2d10bcc69f6dc28fe95fdf17940367b21d34e34c737870758950", size = 228604, upload-time = "2025-11-30T20:23:49.501Z" },
{ url = "https://files.pythonhosted.org/packages/e8/95/ab005315818cc519ad074cb7784dae60d939163108bd2b394e60dc7b5461/rpds_py-0.30.0-cp314-cp314-win_arm64.whl", hash = "sha256:613aa4771c99f03346e54c3f038e4cc574ac09a3ddfb0e8878487335e96dead6", size = 222391, upload-time = "2025-11-30T20:23:50.96Z" },
{ url = "https://files.pythonhosted.org/packages/9e/68/154fe0194d83b973cdedcdcc88947a2752411165930182ae41d983dcefa6/rpds_py-0.30.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:7e6ecfcb62edfd632e56983964e6884851786443739dbfe3582947e87274f7cb", size = 364868, upload-time = "2025-11-30T20:23:52.494Z" },
{ url = "https://files.pythonhosted.org/packages/83/69/8bbc8b07ec854d92a8b75668c24d2abcb1719ebf890f5604c61c9369a16f/rpds_py-0.30.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a1d0bc22a7cdc173fedebb73ef81e07faef93692b8c1ad3733b67e31e1b6e1b8", size = 353747, upload-time = "2025-11-30T20:23:54.036Z" },
{ url = "https://files.pythonhosted.org/packages/ab/00/ba2e50183dbd9abcce9497fa5149c62b4ff3e22d338a30d690f9af970561/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d08f00679177226c4cb8c5265012eea897c8ca3b93f429e546600c971bcbae7", size = 383795, upload-time = "2025-11-30T20:23:55.556Z" },
{ url = "https://files.pythonhosted.org/packages/05/6f/86f0272b84926bcb0e4c972262f54223e8ecc556b3224d281e6598fc9268/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5965af57d5848192c13534f90f9dd16464f3c37aaf166cc1da1cae1fd5a34898", size = 393330, upload-time = "2025-11-30T20:23:57.033Z" },
{ url = "https://files.pythonhosted.org/packages/cb/e9/0e02bb2e6dc63d212641da45df2b0bf29699d01715913e0d0f017ee29438/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a4e86e34e9ab6b667c27f3211ca48f73dba7cd3d90f8d5b11be56e5dbc3fb4e", size = 518194, upload-time = "2025-11-30T20:23:58.637Z" },
{ url = "https://files.pythonhosted.org/packages/ee/ca/be7bca14cf21513bdf9c0606aba17d1f389ea2b6987035eb4f62bd923f25/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5d3e6b26f2c785d65cc25ef1e5267ccbe1b069c5c21b8cc724efee290554419", size = 408340, upload-time = "2025-11-30T20:24:00.2Z" },
{ url = "https://files.pythonhosted.org/packages/c2/c7/736e00ebf39ed81d75544c0da6ef7b0998f8201b369acf842f9a90dc8fce/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:626a7433c34566535b6e56a1b39a7b17ba961e97ce3b80ec62e6f1312c025551", size = 383765, upload-time = "2025-11-30T20:24:01.759Z" },
{ url = "https://files.pythonhosted.org/packages/4a/3f/da50dfde9956aaf365c4adc9533b100008ed31aea635f2b8d7b627e25b49/rpds_py-0.30.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:acd7eb3f4471577b9b5a41baf02a978e8bdeb08b4b355273994f8b87032000a8", size = 396834, upload-time = "2025-11-30T20:24:03.687Z" },
{ url = "https://files.pythonhosted.org/packages/4e/00/34bcc2565b6020eab2623349efbdec810676ad571995911f1abdae62a3a0/rpds_py-0.30.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fe5fa731a1fa8a0a56b0977413f8cacac1768dad38d16b3a296712709476fbd5", size = 415470, upload-time = "2025-11-30T20:24:05.232Z" },
{ url = "https://files.pythonhosted.org/packages/8c/28/882e72b5b3e6f718d5453bd4d0d9cf8df36fddeb4ddbbab17869d5868616/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:74a3243a411126362712ee1524dfc90c650a503502f135d54d1b352bd01f2404", size = 565630, upload-time = "2025-11-30T20:24:06.878Z" },
{ url = "https://files.pythonhosted.org/packages/3b/97/04a65539c17692de5b85c6e293520fd01317fd878ea1995f0367d4532fb1/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3e8eeb0544f2eb0d2581774be4c3410356eba189529a6b3e36bbbf9696175856", size = 591148, upload-time = "2025-11-30T20:24:08.445Z" },
{ url = "https://files.pythonhosted.org/packages/85/70/92482ccffb96f5441aab93e26c4d66489eb599efdcf96fad90c14bbfb976/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dbd936cde57abfee19ab3213cf9c26be06d60750e60a8e4dd85d1ab12c8b1f40", size = 556030, upload-time = "2025-11-30T20:24:10.956Z" },
{ url = "https://files.pythonhosted.org/packages/20/53/7c7e784abfa500a2b6b583b147ee4bb5a2b3747a9166bab52fec4b5b5e7d/rpds_py-0.30.0-cp314-cp314t-win32.whl", hash = "sha256:dc824125c72246d924f7f796b4f63c1e9dc810c7d9e2355864b3c3a73d59ade0", size = 211570, upload-time = "2025-11-30T20:24:12.735Z" },
{ url = "https://files.pythonhosted.org/packages/d0/02/fa464cdfbe6b26e0600b62c528b72d8608f5cc49f96b8d6e38c95d60c676/rpds_py-0.30.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27f4b0e92de5bfbc6f86e43959e6edd1425c33b5e69aab0984a72047f2bcf1e3", size = 226532, upload-time = "2025-11-30T20:24:14.634Z" },
{ url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" },
]
[[package]]
@@ -1910,15 +1860,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" },
]
[[package]]
name = "smmap"
version = "5.0.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/1f/ea/49c993d6dfdd7338c9b1000a0f36817ed7ec84577ae2e52f890d1a4ff909/smmap-5.0.3.tar.gz", hash = "sha256:4d9debb8b99007ae47165abc08670bd74cb74b5227dda7f643eccc4e9eb5642c", size = 22506, upload-time = "2026-03-09T03:43:26.1Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c1/d4/59e74daffcb57a07668852eeeb6035af9f32cbfd7a1d2511f17d2fe6a738/smmap-5.0.3-py3-none-any.whl", hash = "sha256:c106e05d5a61449cf6ba9a1e650227ecfb141590d2a98412103ff35d89fc7b2f", size = 24390, upload-time = "2026-03-09T03:43:24.361Z" },
]
[[package]]
name = "sniffio"
version = "1.3.1"
@@ -1928,6 +1869,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" },
]
[[package]]
name = "soupsieve"
version = "2.8.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/7b/ae/2d9c981590ed9999a0d91755b47fc74f74de286b0f5cee14c9269041e6c4/soupsieve-2.8.3.tar.gz", hash = "sha256:3267f1eeea4251fb42728b6dfb746edc9acaffc4a45b27e19450b676586e8349", size = 118627, upload-time = "2026-01-20T04:27:02.457Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/46/2c/1462b1d0a634697ae9e55b3cecdcb64788e8b7d63f54d923fcd0bb140aed/soupsieve-2.8.3-py3-none-any.whl", hash = "sha256:ed64f2ba4eebeab06cc4962affce381647455978ffc1e36bb79a545b91f45a95", size = 37016, upload-time = "2026-01-20T04:27:01.012Z" },
]
[[package]]
name = "sqlalchemy"
version = "2.0.49"
@@ -1993,41 +1943,6 @@ 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 = "streamlit"
version = "1.57.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "altair" },
{ name = "anyio" },
{ name = "blinker" },
{ name = "cachetools" },
{ name = "click" },
{ name = "gitpython" },
{ name = "httptools" },
{ name = "itsdangerous" },
{ name = "numpy" },
{ name = "packaging" },
{ name = "pandas" },
{ name = "pillow" },
{ name = "protobuf" },
{ name = "pyarrow" },
{ name = "pydeck" },
{ name = "python-multipart" },
{ name = "requests" },
{ name = "starlette" },
{ name = "tenacity" },
{ name = "toml" },
{ name = "typing-extensions" },
{ name = "uvicorn" },
{ name = "watchdog", marker = "sys_platform != 'darwin'" },
{ name = "websockets" },
]
sdist = { url = "https://files.pythonhosted.org/packages/5d/f8/b2daf7a5f8ae15527daf94406e771bb6075e958a01c3dde9eba79dc3c9a3/streamlit-1.57.0.tar.gz", hash = "sha256:0b028d305c1a1a757071b2c9504966787602842fc8af6e873795ca58d2b4d12f", size = 8678859, upload-time = "2026-04-28T22:13:32.238Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d8/1a/3ca2293d8552bacea3e67e9600d2d1df7df4a325059769ad83d91c279595/streamlit-1.57.0-py3-none-any.whl", hash = "sha256:0d1d41972aeade5637dbb0e7f0eefa5312272f85304923d240a1b1f0475249c8", size = 9194216, upload-time = "2026-04-28T22:13:29.624Z" },
]
[[package]]
name = "tenacity"
version = "9.1.4"
@@ -2049,15 +1964,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/60/45/c7b5c3168458db837e8ceab06dc77824e18202679d0463f0e8f002143a97/tinycss2-1.5.1-py3-none-any.whl", hash = "sha256:3415ba0f5839c062696996998176c4a3751d18b7edaaeeb658c9ce21ec150661", size = 28404, upload-time = "2025-11-23T10:29:08.676Z" },
]
[[package]]
name = "toml"
version = "0.10.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/be/ba/1f744cdc819428fc6b5084ec34d9b30660f6f9daaf70eead706e3203ec3c/toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f", size = 22253, upload-time = "2020-11-01T01:40:22.204Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/44/6f/7120676b6d73228c96e17f1f794d8ab046fc910d781c8d151120c3f1569e/toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", size = 16588, upload-time = "2020-11-01T01:40:20.672Z" },
]
[[package]]
name = "tqdm"
version = "4.67.3"
@@ -2171,24 +2077,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/e4/16/c1fd27e9549f3c4baf1dc9c20c456cd2f822dbf8de9f463824b0c0357e06/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6cde23eeda1a25c75b2e07d39970f3374105d5eafbaab2a4482be82f272d5a5e", size = 4296730, upload-time = "2025-10-16T22:17:00.744Z" },
]
[[package]]
name = "watchdog"
version = "6.0.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/db/7d/7f3d619e951c88ed75c6037b246ddcf2d322812ee8ea189be89511721d54/watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282", size = 131220, upload-time = "2024-11-01T14:07:13.037Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a9/c7/ca4bf3e518cb57a686b2feb4f55a1892fd9a3dd13f470fca14e00f80ea36/watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13", size = 79079, upload-time = "2024-11-01T14:06:59.472Z" },
{ url = "https://files.pythonhosted.org/packages/5c/51/d46dc9332f9a647593c947b4b88e2381c8dfc0942d15b8edc0310fa4abb1/watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379", size = 79078, upload-time = "2024-11-01T14:07:01.431Z" },
{ url = "https://files.pythonhosted.org/packages/d4/57/04edbf5e169cd318d5f07b4766fee38e825d64b6913ca157ca32d1a42267/watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e", size = 79076, upload-time = "2024-11-01T14:07:02.568Z" },
{ url = "https://files.pythonhosted.org/packages/ab/cc/da8422b300e13cb187d2203f20b9253e91058aaf7db65b74142013478e66/watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f", size = 79077, upload-time = "2024-11-01T14:07:03.893Z" },
{ url = "https://files.pythonhosted.org/packages/2c/3b/b8964e04ae1a025c44ba8e4291f86e97fac443bca31de8bd98d3263d2fcf/watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26", size = 79078, upload-time = "2024-11-01T14:07:05.189Z" },
{ url = "https://files.pythonhosted.org/packages/62/ae/a696eb424bedff7407801c257d4b1afda455fe40821a2be430e173660e81/watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c", size = 79077, upload-time = "2024-11-01T14:07:06.376Z" },
{ url = "https://files.pythonhosted.org/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2", size = 79078, upload-time = "2024-11-01T14:07:07.547Z" },
{ url = "https://files.pythonhosted.org/packages/07/f6/d0e5b343768e8bcb4cda79f0f2f55051bf26177ecd5651f84c07567461cf/watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a", size = 79065, upload-time = "2024-11-01T14:07:09.525Z" },
{ url = "https://files.pythonhosted.org/packages/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680", size = 79070, upload-time = "2024-11-01T14:07:10.686Z" },
{ url = "https://files.pythonhosted.org/packages/33/e8/e40370e6d74ddba47f002a32919d91310d6074130fe4e17dabcafc15cbf1/watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f", size = 79067, upload-time = "2024-11-01T14:07:11.845Z" },
]
[[package]]
name = "watchfiles"
version = "1.1.1"
@@ -2388,3 +2276,26 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/51/47/3fa2286c3cb162c71cdb34c4224d5745a1ceceb391b2bd9b19b668a8d724/yarl-1.23.0-cp314-cp314t-win_arm64.whl", hash = "sha256:44bb7bef4ea409384e3f8bc36c063d77ea1b8d4a5b2706956c0d6695f07dcc25", size = 86041, upload-time = "2026-03-01T22:07:49.026Z" },
{ url = "https://files.pythonhosted.org/packages/69/68/c8739671f5699c7dc470580a4f821ef37c32c4cb0b047ce223a7f115757f/yarl-1.23.0-py3-none-any.whl", hash = "sha256:a2df6afe50dea8ae15fa34c9f824a3ee958d785fd5d089063d960bae1daa0a3f", size = 48288, upload-time = "2026-03-01T22:07:51.388Z" },
]
[[package]]
name = "yfinance"
version = "1.3.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "beautifulsoup4" },
{ name = "curl-cffi" },
{ name = "frozendict" },
{ name = "multitasking" },
{ name = "numpy" },
{ name = "pandas" },
{ name = "peewee" },
{ name = "platformdirs" },
{ name = "protobuf" },
{ name = "pytz" },
{ name = "requests" },
{ name = "websockets" },
]
sdist = { url = "https://files.pythonhosted.org/packages/9b/fd/943a7d71ce98a40b9006daccba96a83837acadb8e55361f41c7a81873013/yfinance-1.3.0.tar.gz", hash = "sha256:42c4e64a889dab8eeaffd3a66d4ccf1baffd566910ca63fb6332283f8f9b8a40", size = 145297, upload-time = "2026-04-16T19:51:05.785Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/dc/bc/e46ed5dfb88c6f7af0f641ffb6227d32f484ea989a2987a52a9c35d17aa9/yfinance-1.3.0-py2.py3-none-any.whl", hash = "sha256:c89539f0cf6af026d570131189bd659a962e8fb942376ef8ff8913e77c9fca75", size = 133706, upload-time = "2026-04-16T19:51:04.298Z" },
]