# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Project Overview

Swym is an automated paper trading system built on [barter-rs](https://github.com/barter-rs/barter-rs). It connects to live cryptocurrency exchange WebSocket feeds, runs paper trading sessions, and persists results to PostgreSQL.

## Repository Structure

Rust workspace monorepo with one shared library crate, five binary service crates, and a React dashboard:

- `crates/swym-dal` — Shared data access layer (models, repositories, migrations)
- `services/api` — Axum HTTP API server (port 3000)
- `services/ingest-binance` — Binance historical spot trade ingestion daemon
- `services/market-worker` — Live market data ingestion daemon
- `services/paper-executor` — Paper trading execution daemon
- `services/paper-cli` — Standalone CLI for one-off paper trading sessions
- `dashboard/` — React 19 + TypeScript + Vite frontend
- `config/` — JSON config files for each service (per-env subdirectories)
- `assets/systemd/` — Systemd unit files for deployment

The barter-rs dependencies are resolved from a local path (`../../barter-rs/barter-rs/`), so that sibling directory must be present.

## Build & Run Commands

### Rust (all services)
```bash
cargo build                          # debug build, all workspace crates
cargo build --release                # release build
cargo build -p swym-api              # single service
cargo check                          # type-check without building
cargo clippy                         # lint

# Run services (config file is a required positional argument)
cargo run -p swym-api -- config/dev/api.json
cargo run -p swym-ingest-binance -- config/dev/ingest_binance.json
cargo run -p market-worker -- config/dev/market_worker.json
cargo run -p paper-executor -- config/dev/paper_executor.json

# Standalone paper trading CLI (no database)
cargo run -p paper-cli
cargo run -p paper-cli -- --duration 30 --pairs BTCUSDT,ETHUSDT --output json
```

### Dashboard

Node.js is managed by [fnm](https://github.com/Schniz/fnm) (Fast Node Manager). The shell integration
activates the correct version automatically in interactive shells, but non-interactive shells (such as
those used by Claude Code's Bash tool) do not source `~/.bashrc`, so `node` and `pnpm` may not be on
`PATH`. To run dashboard commands from a non-interactive context, invoke fnm explicitly:

```bash
eval "$(fnm env --use-on-cd)"   # activate fnm in the current shell session
cd dashboard
pnpm dev       # dev server with HMR, proxies /api/* to localhost:3000
pnpm build     # tsc + vite build
pnpm lint      # eslint
pnpm preview   # preview production build
pnpm tsc --noEmit   # type-check without emitting
```

The pinned version is declared in `dashboard/.node-version` (or `.nvmrc`). `fnm list` shows available
versions; the default is currently v24.13.1.

### Database
Database migrations run automatically on service startup via sqlx. No manual migration step is needed. The `DATABASE_URL` env var must be set for sqlx compile-time query checking:
```bash
DB_HOST=benjy.hanzalova.internal
CA_HOST=ca.internal
CL_HOST=$(hostname --fqdn)

DB_USER=swym
DB_PORT=5432
DB_NAME=swym
DB_MODE=verify-full
CA_CERT=/etc/pki/ca-trust/source/anchors/${CA_HOST}-rsa.pem
CL_CERT=/etc/pki/tls/misc/${CL_HOST}-rsa.pem
CL_KEY=/etc/pki/tls/private/${CL_HOST}-rsa.pem
export DATABASE_URL=postgres://${DB_USER}@${DB_HOST}:${DB_PORT}/${DB_NAME}?sslmode=${DB_MODE}&sslrootcert=${CA_CERT}&sslcert=${CL_CERT}&sslkey=${CL_KEY}
```

### Deployment

Prerequisites: `yq`, `jq`, `rsync`, `ssh` access to all target hosts as `grenade`.

Target hosts and webroots are declared in `assets/manifest.yml`. Config files live in `config/<env>/`.

```bash
# Deploy all services and dashboard to the dev environment
./script/deploy.sh dev

# Drop and recreate the public schema, then deploy (dev only)
# Rebuilds schema from scratch so migrations run clean; destroys all data
./script/deploy.sh dev drop
```

The script:
1. Validates SSH connectivity to all hosts in the manifest
2. Builds release binaries (`cargo build --release`)
3. Groups services by host, stages binaries/configs/systemd units into a local temp dir, rsyncs to `/tmp/swym-deploy` on each host, then remotely stops services, installs to `/usr/local/bin/` and `/etc/swym/`, reloads systemd, and starts services
4. Builds the dashboard (`pnpm install && pnpm build`) and rsyncs `dist/` to the configured webroot on the dashboard host, installs the nginx site config, sets SELinux context (`httpd_sys_content_t`), and reloads nginx

**Config resolution:** `resolve_config <env> <file>` checks `config/overlays/<env>/<file>` first, then falls back to `config/<env>/<file>`.

**TLS / certificates:** `script/cert-deploy.sh` provisions and deploys TLS certificates. It creates a local key, CSR, and cert signed by the internal CA under `~/.config/tls/internal/`, installs them to the system trust store and `/etc/pki/tls/`, and rsyncs the app cert chain and key to the nginx host.

## Architecture

```
Dashboard (React 19)
    │ HTTP
swym-api (Axum :3000)
    │ Postgres
    ├── market-worker     — polls active subscriptions → spawns barter streams → batch-inserts events
    ├── paper-executor    — polls queued runs (FOR UPDATE SKIP LOCKED) → executes barter engine → writes TradingSummary
    └── ingest-binance    — polls ingest_configs → fetches Binance S3 CSVs → batch-inserts into market_trades

s3://data.binance.vision  →  ingest-binance  →  market_trades

swym-dal (shared DAL crate)
PostgreSQL
```

### Key Design Patterns

**Database-driven coordination:** Services share state exclusively through PostgreSQL. `market-worker` polls `market_subscriptions` for active subscriptions. `paper-executor` claims runs with `FOR UPDATE SKIP LOCKED` to support multiple concurrent executor instances safely.

**Auto-migration:** All services call `swym_dal::run_migrations()` on startup, so schema is always up-to-date.

**Compile-time SQL:** sqlx `query!` / `query_as!` macros validate SQL at compile time. `DATABASE_URL` must point at a running database during compilation.

**Buffered ingestion:** `market-worker` accumulates events in a `HashMap<InstrumentId, Vec<Event>>` buffer and flushes on a configurable interval (`flush_interval_ms`) or batch size threshold.

**Strategy configuration:** Paper runs store a `strategy_config` JSON column. `paper-executor` deserializes this to select and configure the strategy (e.g., `SimpleSpread`, `SwyStrategy`).

### Services Detail

**`swym-api`** — REST API. Handlers in `services/api/src/handlers/`. Exchange clients in `services/api/src/exchange_client/` (Binance, Coinbase, Kraken REST APIs for instrument discovery). Shared state via `AppState` in `state.rs`.

**`market-worker`** — `subscription_manager.rs` manages barter stream lifecycle. `ingest.rs` handles buffering and batch DB insertion. Polls for subscription changes every `poll_interval_secs`.

**`paper-executor`** — `worker.rs` is the main poll loop. `runner.rs` executes a single run. `strategy/` contains strategy implementations. Resets stale `running` runs back to `queued` on startup.

**`swym-ingest-binance`** — `worker.rs` is the main poll loop. `fetcher.rs` downloads zipped CSVs from `s3://data.binance.vision` via `object_store` (anonymous access). `parser.rs` extracts and streams CSV rows from the zip, detecting ms vs μs timestamps and mapping `is_buyer_maker` to trade side. Uses monthly files for bulk backfill and daily files for the recent 45-day window. Enforces per-instrument retention by purging old trades after ingestion.

**`swym-dal`** — All database models in `src/models/`, repository traits + sqlx implementations in `src/repo/`. Migrations in `src/migrations/`.

### Data Flow for Market Ingestion
1. API creates a `market_subscription` row (exchange + instrument + sub_kind)
2. `market-worker` discovers it on next poll cycle
3. Spawns a barter stream for that subscription
4. Receives `MarketEvent`s (trades, L1 orderbook snapshots)
5. Buffers events and batch-inserts into `market_trades` / `market_order_book_l1`

### Data Flow for Historical Trade Ingestion
1. Insert a row into `ingest_configs` specifying instrument, source (`binance_s3`), and `retention_days`
2. `ingest-binance` discovers it on next poll cycle via `list_active`
3. Determines date range: `last_ingested_date + 1` through yesterday; uses monthly files for complete months > 45 days old, daily files for the recent window
4. Fetches each file from `s3://data.binance.vision/data/spot/{monthly,daily}/trades/{SYMBOL}/`
5. Streams CSV rows from the zip, batch-inserts into `market_trades` with `ON CONFLICT DO NOTHING`
6. Advances `last_ingested_date` cursor after each file; purges rows older than `retention_days`

### Data Flow for Paper Trading
1. API inserts a `paper_run` row with status `queued` and `strategy_config` JSON
2. `paper-executor` polls, claims the run, sets status to `running`
3. Executes barter paper trading engine against live feeds for the configured duration
4. Writes `TradingSummary` result JSON and sets status to `complete` or `failed`

## Configuration

Each service takes a JSON config file as its sole CLI argument. Config files are in `config/<env>/`:
- `config/dev/api.json` — database URL, host, port
- `config/dev/ingest_binance.json` — database URL, poll_interval_secs, batch_size, max_concurrent
- `config/dev/market_worker.json` — database URL, batch_size, flush_interval_ms, poll_interval_secs
- `config/dev/paper_executor.json` — database URL, poll_interval_secs, max_concurrent_runs, stale_timeout_secs

Per-environment overrides go in `config/overlays/<env>/` and take precedence over the base config.
