The deploy job hard-failed without CRYPTO_API_TOKEN, and /api/ws (which skips Basic auth) relied on that token. Drop the requirement: the app now works with no token at all, matching the backend guard that was always a no-op when unset. When a token is configured it still protects /api and /api/ws; when it is not, /api/ws is as open as the rest of the API, which the Traefik Basic auth still guards for everything else. Adds tests for the tokenless API and WebSocket.
218 lines
9.3 KiB
Markdown
218 lines
9.3 KiB
Markdown
# 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.
|
|
|
|
> **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.<BASE_DOMAIN>` |
|
|
| `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.
|
|
|
|
## 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<token>/getUpdates`.
|
|
|
|
## 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/stop` | Stop the bot |
|
|
| `POST` | `/api/bot/kill` | Kill switch: flatten + halt |
|
|
| `POST` | `/api/backtest` | Backtest a strategy on historical candles |
|
|
| `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
|
|
```
|