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,
httpxfor market data. - Frontend: Vue 3 + Vite + Tailwind dashboard with a live WebSocket feed.
- Brokers:
paper(simulated fills, default) andbinance(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)
# 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)
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_defaultnetwork and is published atcrypto.${BASE_DOMAIN}(defaultcrypto.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/wsWebSocket) 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_USERSis set (falling back to the sharedSCA_FRONTEND_AUTH_USERS). Generate a value withhtpasswd -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 |
1m |
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.
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, 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 for the full
template and the StrategyContext API. Two examples ship in the repo:
sma_cross and 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
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