diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..a9d3c8c --- /dev/null +++ b/.dockerignore @@ -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/ diff --git a/.env.example b/.env.example index cfe2c1b..d136a30 100644 --- a/.env.example +++ b/.env.example @@ -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 Streamlit dashboard (Traefik fa il TLS davanti) +SWARM_DASHBOARD_PORT=8501 + +# 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 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..472842b --- /dev/null +++ b/Dockerfile @@ -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. diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..70cac31 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,110 @@ +# 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: + - streamlit + - run + - /app/src/multi_swarm/dashboard/streamlit_app.py + - --server.address=0.0.0.0 + - --server.port=${SWARM_DASHBOARD_PORT:-8501} + - --server.headless=true + - --browser.gatherUsageStats=false + command: [] + healthcheck: + test: + - "CMD" + - "python" + - "-c" + - "import os, urllib.request; urllib.request.urlopen(f'http://localhost:{os.environ.get(\"SWARM_DASHBOARD_PORT\",\"8501\")}/_stcore/health', 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:-8501}" + - com.centurylinklabs.watchtower.enable=true