# Crypto Trader A small, hackable crypto trading bot with a web dashboard. Describe a strategy, drop in one Python file, run it, and watch positions and P&L live. - **Backend:** FastAPI, SQLAlchemy, SQLite (Postgres optional), pure-Python indicators, `httpx` for market data. - **Frontend:** Vue 3 + Vite + Tailwind dashboard with a live WebSocket feed. - **Brokers:** `paper` (simulated fills, default) and `binance` (live spot). - **Market data:** Binance public REST (no API key needed). - **Backtesting:** replay historical candles through the same strategy interface, with return/drawdown/win-rate/Sharpe metrics — so what you test is what runs. - **Parameter sweeps:** grid-search a strategy's params and rank the results. - **Trailing stops:** strategies can trail a stop with the price (never loosening), enforced identically in backtest, paper and live. > **Paper trading is the default.** Live mode places real orders and is opt-in. > This is experimental software. Never trade more than you can afford to lose. ## How it works ``` Binance REST ──▶ MarketData ──▶ TradingEngine ──▶ Broker (paper | binance) │ Strategy.on_candle(ctx) │ REST /api + WebSocket /api/ws ──▶ Vue dashboard ``` The engine runs one strategy at a time. Each tick it fetches candles, updates the mark price, enforces stop-loss/take-profit, calls the strategy, executes the resulting intents, and persists orders, equity snapshots and events. ## Quickstart (local) ```bash # 1. Config cp .env.example .env # 2. Backend python3 -m venv .venv .venv/bin/pip install -r backend/requirements-dev.txt cd backend && ../.venv/bin/python -m uvicorn app.main:app --reload --port 8000 # 3. Frontend (new terminal) cd frontend && npm install && npm run dev ``` Open http://localhost:5173. API docs are at http://localhost:8000/docs. ## Quickstart (Docker, local) ```bash cp .env.example .env docker compose -f docker-compose.local.yml up --build # dashboard: http://localhost:8080 API docs: http://localhost:8000/docs ``` The default `docker-compose.yml` is the **deployment** stack (Traefik + `docker_default`); use `docker-compose.local.yml` for a self-contained local run. ## Deployment Merging to `main` triggers `.gitea/workflows/deploy.yml`, which tests, builds both images, and rolls the stack out on the shared Traefik host: - The **frontend** joins the external `docker_default` network and is published at `crypto.${BASE_DOMAIN}` (default `crypto.matitos.org`) via Traefik, with a Let's Encrypt cert (`certresolver=myresolvercd`). - The **backend** stays on the internal network only; the frontend nginx proxies `/api` (including the `/api/ws` WebSocket) to it, so API keys are never directly exposed. - The WebUI is protected by **fail-closed Basic auth**: the deploy aborts unless `CRYPTO_FRONTEND_AUTH_USERS` is set (falling back to the shared `SCA_FRONTEND_AUTH_USERS`). Generate a value with `htpasswd -nbB user 'password'`. - A new release is rolled back automatically if healthchecks fail. Repo/org variables and secrets used by the deploy: | Name | Kind | Purpose | | --- | --- | --- | | `CRYPTO_FRONTEND_AUTH_USERS` | var/secret | htpasswd string for the WebUI (required) | | `BASE_DOMAIN` | secret | Base domain (default `matitos.org`) | | `CRYPTO_MODE` | var | `paper` (default) or `live` | | `CRYPTO_EXCHANGE` / `CRYPTO_SYMBOL` / `CRYPTO_TIMEFRAME` | var | Trading defaults | | `CRYPTO_STARTING_BALANCE` / `CRYPTO_POLL_INTERVAL` | var | Paper account + tick rate | | `CRYPTO_EXCHANGE_API_KEY` / `CRYPTO_EXCHANGE_API_SECRET` | secret | Required only for `live` | | `CRYPTO_EXCHANGE_SANDBOX` | var | Use the Binance testnet | | `CRYPTO_MAX_DAILY_LOSS` / `CRYPTO_MAX_DRAWDOWN_PCT` / `CRYPTO_MAX_POSITION_PCT` / `CRYPTO_MAX_TRADES_PER_DAY` | var | Risk limits | | `CRYPTO_TELEGRAM_BOT_TOKEN` | secret | Telegram alerts | | `CRYPTO_TELEGRAM_CHAT_ID` | var/secret | Telegram chat id | | `CRYPTO_NOTIFY_WEBHOOK_URL` | secret | Generic webhook alerts | | `CRYPTO_CORS_ORIGINS` | var | Defaults to `https://crypto.` | | `CRYPTO_DATA_DIR` / `PERSISTENT_MOUNT` | var | Host path for the SQLite DB | | `DOCKERHUB_USERNAME` / `DOCKERHUB_TOKEN` | secret | Optional; for authenticated pulls | ## Configuration All settings are environment variables (see `.env.example`): | Variable | Default | Purpose | | --- | --- | --- | | `MODE` | `paper` | `paper` or `live` | | `EXCHANGE` | `binance` | Market data / execution venue | | `SYMBOL` | `BTC/USDT` | Default trading pair | | `TIMEFRAME` | `5m` | Candle interval | | `POLL_INTERVAL` | `5` | Seconds between ticks | | `STARTING_BALANCE` | `10000` | Paper account size | | `FEE_BPS` / `SLIPPAGE_BPS` | `10` / `5` | Paper fill model | | `DATABASE_URL` | `sqlite:///./data/trader.db` | Postgres: `postgresql+psycopg://...` | | `EXCHANGE_API_KEY` / `EXCHANGE_API_SECRET` | — | Required only for live | | `EXCHANGE_SANDBOX` | `false` | Use Binance testnet | | `MAX_DAILY_LOSS` | `0` | Halt+flatten after this daily loss (quote ccy); 0 disables | | `MAX_DRAWDOWN_PCT` | `0` | Halt+flatten after this % drop from peak; 0 disables | | `MAX_POSITION_PCT` | `1.0` | Cap a single position at this fraction of equity | | `MAX_TRADES_PER_DAY` | `0` | Max new entries per UTC day; 0 disables | | `TELEGRAM_BOT_TOKEN` / `TELEGRAM_CHAT_ID` | — | Telegram alerts (both required) | | `NOTIFY_WEBHOOK_URL` | — | Generic webhook alerts | | `API_TOKEN` / `API_TOKEN_HEADER` | — | Bearer token required on `/api` when set | ## Exchanges Market data works out of the box on **Binance**, **Kraken** and **Coinbase** (public REST, no API key). Set `EXCHANGE` to `binance`, `kraken` or `coinbase`. Live execution (`MODE=live`) is implemented for **Binance** (spot, optional testnet via `EXCHANGE_SANDBOX`) and **Coinbase** (Advanced Trade; set `EXCHANGE_API_SECRET` to the CDP EC private key in PEM). Kraken is market-data-only for now — `MODE=live` on Kraken raises a clear error. ## API token (defense-in-depth) Set `API_TOKEN` to require a bearer token on all `/api` routes (health stays open). The frontend nginx injects it server-side, so the browser never sees it and the dashboard keeps working unchanged. This sits *behind* the Traefik Basic auth, not instead of it. `/api/ws` is special: browsers do not reliably replay Basic-auth credentials on a WebSocket upgrade (which left the live feed stuck on "reconnecting"), so Traefik serves that one path on a separate higher-priority router **without** Basic auth. If `API_TOKEN` is set, the WebSocket still requires it (nginx injects it on the handshake). If `API_TOKEN` is unset, `/api/ws` is as open as the rest of `/api`, which is fine for a trusted/private deployment but is the reason to set a token if the host is exposed. ## Going live Setting `MODE=live` alone is **not** enough. `GET /api/live-check` (and the dashboard banner) validate a preflight before any live order can be placed; live trading is blocked unless *all* of these hold: - at least one risk limit is active (`MAX_DAILY_LOSS`, `MAX_DRAWDOWN_PCT` or `MAX_TRADES_PER_DAY`); - `MAX_POSITION_PCT < 1.0` (a single position can never be the whole account); - `LIVE_CONFIRMATION` is exactly `I UNDERSTAND THE RISK`; - the starting balance is at least `MIN_LIVE_BALANCE`; - exchange API credentials are set. A blocked live start returns `400` with the reasons, and the dashboard shows a banner listing them. Practise the kill switch in paper mode first. ## Multi-strategy The **Strategies (multi)** panel runs several strategies at once, each with its own capital slice. Set a **weight** per strategy; weights are normalised across the run (`[1, 3]` gives 25% / 75%) and each strategy starts with `starting_balance × allocation` as its own cash, so one strategy's losses cannot drain another's. Risk limits apply per strategy, and **Kill all** flattens and halts every one. ## Risk controls The engine enforces limits on every tick and **halts + flattens** when one trips: - **`MAX_DAILY_LOSS`** — absolute loss since the start of the UTC day. - **`MAX_DRAWDOWN_PCT`** — percentage fall from peak equity. - **`MAX_POSITION_PCT`** — position size cap as a fraction of equity. - **`MAX_TRADES_PER_DAY`** — new-entry budget per UTC day. A halt is sticky: the position is closed, the bot stops, a red banner appears in the dashboard, and a notification is sent. Restarting the bot resets the limits. The dashboard also has a manual **Kill switch** button (`POST /api/bot/kill`) that flattens and halts immediately. ## Notifications Set `TELEGRAM_BOT_TOKEN` + `TELEGRAM_CHAT_ID` and/or `NOTIFY_WEBHOOK_URL` to receive alerts on trades, stop-outs, errors and halts. Notifications are best-effort: a failing channel never affects trading. For Telegram, create a bot with [@BotFather](https://t.me/BotFather), message it once, then read the chat id from `https://api.telegram.org/bot/getUpdates`. Additional controls: - `NOTIFY_LEVELS` (default `trade,error`) — which event levels to send; add `warning` or `info`. - `NOTIFY_LIFECYCLE` (default `true`) — explicit start/stop messages; the kill switch always notifies. - `SUMMARY_INTERVAL_MINUTES` (default `0`) — periodic portfolio summary (equity, per-strategy P&L and allocation). ## Adding a strategy Drop a file in `backend/app/engine/strategies/` — it is auto-discovered and appears in the UI immediately. See **[AGENTS.md](AGENTS.md)** for the full template and the `StrategyContext` API. Bundled strategies: `sma_cross`, `ema_cross`, `macd_trend`, `bollinger_reversion`, `donchian_breakout`, `momentum`, `rsi_reversion`. ## API | Method | Path | Description | | --- | --- | --- | | `GET` | `/api/status` | Bot status, equity, P&L | | `GET` | `/api/strategies` | Available strategies + parameter specs | | `POST` | `/api/bot/start` | Start a strategy | | `POST` | `/api/bot/start-many` | Start several strategies with per-strategy allocations | | `GET` | `/api/bot/status` | Per-strategy status for a multi-strategy run | | `GET` | `/api/live-check` | Go-live preflight (blockers for live trading) | | `POST` | `/api/bot/stop` | Stop the bot | | `POST` | `/api/bot/kill` | Kill switch: flatten + halt | | `POST` | `/api/backtest` | Backtest a strategy on historical candles | | `POST` | `/api/sweep` | Grid-sweep strategy params, ranked | | `GET` | `/api/positions` | Positions (with unrealized P&L when open) | | `GET` | `/api/orders` | Order history | | `GET` | `/api/events` | Activity log | | `GET` | `/api/equity` | Equity curve points | | `POST` | `/api/orders` | Manual market order | | `POST` | `/api/positions/{id}/close` | Close the open position | | `GET`/`POST` | `/api/presets` | List / save strategy presets | | `DELETE` | `/api/presets/{id}` | Delete a preset | | `WS` | `/api/ws` | Live status + event stream | ## Tests & lint ```bash make test # backend pytest + frontend vitest make lint # ruff make build # frontend production build ``` ## Project layout ``` backend/ app/ engine/ trading loop, indicators, strategies/ broker/ paper + binance execution market/ binance + fake data feeds api/routes.py REST + WebSocket models.py positions, orders, equity, events store.py persistence tests/ frontend/ src/ Vue dashboard ```