High-performance paper trading engine with realistic market simulation, built in Rust.
- Realistic order execution — square-root market impact model, bid-ask spread simulation, liquidity constraints
- In-memory hot path — all trade execution happens in-memory via DashMap for sub-millisecond latency
- Async persistence — background worker flushes trades to Postgres every 5 seconds
- Real-time prices — WebSocket connection to EODHD for live US equities and crypto prices
- Batch orders — execute up to 100 orders in a single request
- API key auth — programmatic bot registration with API key authentication
- Firebase Auth — JWT-based authentication for frontend integrations
- Portfolio management — create portfolios, track positions, view trade history
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ HTTP API │────▶│ In-Memory │────▶│ Postgres │
│ (Axum) │ │ EntryStore │ │ (async) │
└──────────────┘ │ PriceStore │ └──────────────┘
└──────────────┘
▲
┌─────┴──────┐
│ EODHD WS │
│ (prices) │
└────────────┘
- EntryStore — DashMap holding all portfolio state (balances, positions, trade counts)
- PriceStore — DashMap of latest prices from EODHD WebSocket feed
- TradeFlusher — Background Tokio task that persists trades to Postgres every 5s
- Firestore — Portfolio metadata, user profiles, API key storage
- Postgres — Durable trade history, entry balances, positions
- Rust 1.75+
- PostgreSQL
- Firebase project (for auth)
- Google Cloud Firestore
- EODHD API key (for real-time prices)
-
Clone the repo and copy the example env file:
cp .env.example .env
-
Fill in your
.envvalues (see.env.examplefor all options). -
Run database migrations (the app auto-creates tables on startup via
ensure_tables). -
Start the server:
cargo run
The server starts on http://localhost:8080 by default.
POST /api/auth/register
Content-Type: application/json
{
"email": "bot@example.com",
"name": "My Bot",
"display_name": "TradingBot"
}
→ 201 { "uid": "bot_...", "api_key": "st_...", "message": "..." }
POST /api/portfolios
x-api-key: st_your_api_key
{ "starting_balance": 100000 } // optional, defaults to $100k
→ 201 { "portfolio_id": "...", "balance": 100000 }
GET /api/portfolios
x-api-key: st_your_api_key
→ 200 { "portfolios": [...] }
GET /api/portfolios/{id}/portfolio
x-api-key: st_your_api_key
→ 200 {
"cash_balance": 98500.25,
"positions": [{ "ticker": "AAPL", "shares": 10, ... }],
"total_value": 100150.75,
"total_return_pct": 0.15
}
POST /api/portfolios/{id}/trades
x-api-key: st_your_api_key
{ "ticker": "AAPL", "side": "buy", "shares": 10 }
→ 201 { "fill_count": 1, "fills": [...], ... }
POST /api/portfolios/{id}/batch-trades
x-api-key: st_your_api_key
{
"orders": [
{ "ticker": "AAPL", "side": "buy", "shares": 10 },
{ "ticker": "MSFT", "side": "buy", "shares": 5 }
]
}
→ 201 { "fill_count": 2, "rejection_count": 0, "fills": [...] }
GET /api/portfolios/{id}/trades?limit=50&offset=0
x-api-key: st_your_api_key
→ 200 { "trades": [...], "total": 42 }
GET /health → 200 "ok"
| Variable | Required | Default | Description |
|---|---|---|---|
DATABASE_URL |
Yes | — | PostgreSQL connection string |
EODHD_API_KEY |
Yes | — | EODHD API key for real-time prices |
FIREBASE_PROJECT_ID |
Yes | — | Firebase project ID |
FIREBASE_SERVICE_ACCOUNT_JSON |
No | — | Service account JSON for Firestore |
ADMIN_EMAIL |
Yes | — | Admin email for system operations |
PORT |
No | 8080 |
HTTP server port |
CORS_ORIGINS |
No | http://localhost:3000 |
Comma-separated allowed origins |
DEFAULT_STARTING_BALANCE |
No | 100000 |
Default portfolio starting balance |
API_KEY_PREFIX |
No | st_ |
Prefix for generated API keys |
simtrade uses a realistic slippage model that simulates real market conditions:
- Bid-ask spread: Simulated spread based on typical market conditions
- Square-root market impact: Price impact proportional to √(order_size / daily_volume)
- Liquidity constraints: Orders are rejected if they exceed available liquidity
- Buy orders fill above mid price, sell orders fill below — just like real markets
MIT