diff --git a/deploy/README.md b/deploy/README.md index c87dd7f..a5caadf 100644 --- a/deploy/README.md +++ b/deploy/README.md @@ -15,6 +15,43 @@ its **PostgreSQL datastore** — inside a single Kubernetes namespace. The app loop and the CronJobs run the **same image** (built from the repo `Dockerfile`), just with different commands. +## Local development with Docker Compose + +For a single-machine stack (same components, no Kubernetes) use the root +`docker-compose.yml`. It builds the app from the `Dockerfile`, brings up +PostgreSQL with a persistent volume, and runs the loop with its embedded +dashboard. Config comes from a local `.env` (optional — without `ALPACA_*` keys +the loop runs in dry mode; the dashboard and DB still work). `DATABASE_URL` is +overridden to the in-network `postgres` service, so the stack is self-contained. + +```bash +cp .env.example .env # optional: add ALPACA_* keys for live data + +docker compose up -d --build # postgres + app loop + dashboard (:8010) +docker compose logs -f app # watch the loop +open http://localhost:8010 # dashboard + +docker compose --profile monitoring up -d # also start Grafana (:3000) +``` + +The scheduled jobs sit under the `jobs` profile so they don't auto-start; run +them on demand (or from host cron / a scheduler on the ET times in `.env`): + +```bash +docker compose run --rm nightly-tune +docker compose run --rm eod-replay +docker compose run --rm journal +``` + +Postgres credentials default to `momentum`/`momentum`/`momentum`; override with +`POSTGRES_USER` / `POSTGRES_PASSWORD` / `POSTGRES_DB` in `.env`. Named volumes +`pg-data`, `app-data`, and `grafana-data` persist across restarts (the jobs share +`app-data` with the app so `nightly_tune.py`'s `learned_params.json` reaches the +loop). The Compose Grafana uses its own datasource override at +`deploy/compose/grafana/datasources/momentum.yaml` (pointed at the `postgres` +service); the legacy `grafana/docker-compose.yml` is unrelated and left for +Grafana-only use against an external DB. + ## Build & publish the image Pushes to the working branch / `main` / `v*` tags trigger diff --git a/deploy/compose/grafana/datasources/momentum.yaml b/deploy/compose/grafana/datasources/momentum.yaml new file mode 100644 index 0000000..a072133 --- /dev/null +++ b/deploy/compose/grafana/datasources/momentum.yaml @@ -0,0 +1,19 @@ +# Grafana datasource for the docker-compose stack — points at the in-network +# `postgres` service (not the LAN IP used by grafana/provisioning/datasources). +# Credentials are interpolated from the PG_* env vars set on the grafana service. +apiVersion: 1 +datasources: + - name: Momentum + uid: momentum_pg + type: postgres + access: proxy + url: postgres:5432 + user: ${PG_USER} + jsonData: + database: ${PG_DATABASE} + sslmode: disable + postgresVersion: 1600 + secureJsonData: + password: ${PG_PASSWORD} + isDefault: true + editable: true diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..6fe7000 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,137 @@ +# Full local stack for the Momentum System — mirrors the Helm package (deploy/). +# +# docker compose up -d # postgres + app loop + dashboard +# docker compose --profile monitoring up -d # also start Grafana +# docker compose run --rm nightly-tune # run a scheduled job on demand +# +# Config/secrets come from a local .env (copy from .env.example). DATABASE_URL is +# overridden below to point at the in-network postgres service regardless of what +# .env says, so the stack is self-contained. Without ALPACA_* keys the loop runs +# in dry mode (dashboard + DB still work). + +services: + postgres: + image: postgres:16 + environment: + POSTGRES_USER: ${POSTGRES_USER:-momentum} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-momentum} + POSTGRES_DB: ${POSTGRES_DB:-momentum} + # Keep data in a subdir so the volume root doesn't clash with PGDATA init. + PGDATA: /var/lib/postgresql/data/pgdata + volumes: + - pg-data:/var/lib/postgresql/data + ports: + # Exposed for psql/Grafana-on-host debugging; remove if not needed. + - "5432:5432" + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-momentum} -d ${POSTGRES_DB:-momentum}"] + interval: 10s + timeout: 5s + retries: 5 + restart: unless-stopped + + # Trading loop WITH the embedded dashboard (the dashboard's approve/reject/exit + # endpoints need the in-process TradingExecutionService — see deploy/README.md). + app: + build: . + command: ["python", "run_live_paper.py"] + env_file: + - path: .env + required: false + environment: &app-env + # Built from the postgres service; overrides any DATABASE_URL in .env. + DATABASE_URL: postgresql://${POSTGRES_USER:-momentum}:${POSTGRES_PASSWORD:-momentum}@postgres:5432/${POSTGRES_DB:-momentum} + DASHBOARD_HOST: "0.0.0.0" + DASHBOARD_PORT: "8010" + # Schema auto-creates from storage/pg_schema.sql on first connect. + PG_SKIP_SCHEMA: "0" + ports: + - "8010:8010" + volumes: + # DuckDB research file, logs, learned_params.json, schwab tokens. Shared + # with the job services so nightly-tune's output reaches the loop. + - app-data:/app/data + depends_on: + postgres: + condition: service_healthy + restart: unless-stopped + + # ---- Scheduled jobs (the *.sh crontab equivalents). On-demand only: they sit + # under the "jobs" profile so `up` doesn't start them. Run with: + # docker compose run --rm journal | eod-replay | nightly-tune + # (Use host cron / a scheduler to invoke these on the ET schedule in .env.) + journal: + build: . + profiles: ["jobs"] + command: ["python", "momentum_cli.py", "journal"] + env_file: + - path: .env + required: false + environment: *app-env + volumes: + - app-data:/app/data + depends_on: + postgres: + condition: service_healthy + restart: "no" + + eod-replay: + build: . + profiles: ["jobs"] + command: ["python", "eod_replay.py"] + env_file: + - path: .env + required: false + environment: *app-env + volumes: + - app-data:/app/data + depends_on: + postgres: + condition: service_healthy + restart: "no" + + nightly-tune: + build: . + profiles: ["jobs"] + command: ["python", "nightly_tune.py"] + env_file: + - path: .env + required: false + environment: *app-env + volumes: + - app-data:/app/data + depends_on: + postgres: + condition: service_healthy + restart: "no" + + # ---- Grafana (optional): `docker compose --profile monitoring up -d` + grafana: + image: grafana/grafana:11.2.0 + profiles: ["monitoring"] + environment: + GF_AUTH_ANONYMOUS_ENABLED: "true" + GF_AUTH_ANONYMOUS_ORG_ROLE: "Admin" + GF_AUTH_DISABLE_LOGIN_FORM: "true" + GF_DASHBOARDS_MIN_REFRESH_INTERVAL: "2s" + # Interpolated into the provisioned datasource (see datasource override). + PG_USER: ${POSTGRES_USER:-momentum} + PG_PASSWORD: ${POSTGRES_PASSWORD:-momentum} + PG_DATABASE: ${POSTGRES_DB:-momentum} + ports: + - "3000:3000" + volumes: + # Compose-specific datasource points at the in-network postgres service; + # the dashboard + provider are reused from the existing grafana/ dir. + - ./deploy/compose/grafana/datasources:/etc/grafana/provisioning/datasources:ro + - ./grafana/provisioning/dashboards:/etc/grafana/provisioning/dashboards:ro + - grafana-data:/var/lib/grafana + depends_on: + postgres: + condition: service_healthy + restart: unless-stopped + +volumes: + pg-data: + app-data: + grafana-data: