Adds a "Strategies (multi)" panel: add/remove strategy rows (strategy, symbol, timeframe, per-strategy params and weight), start all, stop all and kill all, plus a per-strategy status table (allocation, equity, P&L, halted). Polls /api/bot/status alongside the existing status. Includes component tests.
233 lines
10 KiB
Markdown
233 lines
10 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.
|
||
- **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.<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.
|
||
|
||
## 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<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/start-many` | Start several strategies with per-strategy allocations |
|
||
| `GET` | `/api/bot/status` | Per-strategy status for a multi-strategy run |
|
||
| `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
|
||
```
|