Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4d79e66d68 | |||
| 4b9cded966 | |||
| 14f130aa5a | |||
| b86dbdc9ee | |||
| a66f97fb0e | |||
| 073200313c | |||
| 03f723f7fc | |||
| 8e5efde219 | |||
| 45f273f591 | |||
| 9d1ef8adcf | |||
| 67ae6ff74e | |||
| 1a1dfb7a73 |
@@ -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/
|
||||
@@ -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
|
||||
|
||||
@@ -14,6 +14,7 @@ venv/
|
||||
.DS_Store
|
||||
*.swp
|
||||
*.swo
|
||||
.claude/
|
||||
|
||||
# Env / secrets
|
||||
.env
|
||||
|
||||
+52
@@ -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.
|
||||
@@ -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.
|
||||
|
||||
@@ -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: **+10–23%** 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
@@ -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]
|
||||
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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",
|
||||
]
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
"""Cerbero data-provider HTTP client."""
|
||||
|
||||
from .client import CerberoClient
|
||||
|
||||
__all__ = ["CerberoClient"]
|
||||
|
||||
@@ -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;
|
||||
">×</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>
|
||||
"""
|
||||
@@ -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),
|
||||
}
|
||||
|
||||
@@ -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;'>●</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()}`")
|
||||
@@ -0,0 +1,5 @@
|
||||
"""Data loaders: OHLCV via Cerbero, train/test splits."""
|
||||
|
||||
from .cerbero_ohlcv import CerberoOHLCVLoader, OHLCVRequest
|
||||
|
||||
__all__ = ["CerberoOHLCVLoader", "OHLCVRequest"]
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()),
|
||||
)
|
||||
@@ -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
|
||||
@@ -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);
|
||||
"""
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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"
|
||||
@@ -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" },
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user