Crypto Trading Simulator
A production-grade, real-time cryptocurrency trading platform built with Python and FastAPI — designed to demonstrate backend engineering depth including async systems, WebSocket streaming, caching architecture, and cloud deployment.
🔗 Live Demo: https://crypto.jaayysoni.com
Trading Terminal — Successful Order Execution
- Overview
- Live Deployment
- Key Engineering Highlights
- Features
- System Architecture
- API Reference
- Tech Stack
- Project Structure
- Getting Started
- Docker Setup
- CI/CD Pipeline
- Design Decisions
- Known Limitations & Future Improvements
- License
The Crypto Trading Simulator is a real-time, risk-free cryptocurrency trading platform that mirrors the backend complexity of production trading systems — without involving real money.
It was built as a hands-on demonstration of:
- Real-time systems engineering using WebSockets and live market data APIs
- Async backend design with FastAPI for non-blocking, high-throughput request handling
- Performance optimization using Redis caching to reduce redundant API calls
- Cloud deployment on AWS EC2 with Nginx, HTTPS, and automated CI/CD
The platform streams live prices for the top 90 cryptocurrencies, supports portfolio management using FIFO (First In, First Out) cost basis tracking, and provides a complete trading terminal with buy/sell order execution.
| Property | Value |
|---|---|
| URL | https://crypto.jaayysoni.com |
| Cloud Provider | AWS EC2 — Amazon Linux 2023 (Mumbai Region) |
| Reverse Proxy | Nginx |
| SSL | Let's Encrypt (HTTPS enforced) |
| CI/CD | GitHub Actions (auto-deploy on push) |
| Realtime | ~90 concurrent WebSocket connections |
These are the backend decisions most relevant to engineering and technical evaluation:
All API endpoints involving live price lookups use async def, enabling non-blocking I/O. This is critical for a trading platform where price data is fetched from Redis and WebSocket streams under high concurrency.
@app.get("/dashboard/prices")
async def get_dashboard_prices(db: Session = Depends(get_db)) -> List[Dict]:
data = await get_crypto_prices() # Non-blocking Redis fetch
...The app connects to Binance's WebSocket API and maintains ~90 simultaneous streams — one per cryptocurrency — to receive real-time ticker updates. These updates are pushed into Redis cache for instant access by all HTTP endpoints.
asyncio.create_task(start_crypto_ws()) # Started on app startupLive price data from WebSocket streams is stored in Redis. When an API endpoint needs a current price (e.g., portfolio valuation or order execution), it reads from Redis instead of making a fresh API call — keeping latency low and external API usage minimal.
price_data = await get_symbol_price(symbol) # Read from Redis, not Binance APIPortfolio holdings are computed using a First In, First Out (FIFO) algorithm. Each buy creates a cost lot; each sell depletes the earliest lots. This gives accurate average cost basis and profit/loss calculations.
portfolio_lots = defaultdict(deque)
# BUY → append lot; SELL → consume from leftBalance deduction and transaction insertion are handled in the same database transaction. Available quantity for sells is computed from the aggregate difference of all BUY and SELL records, preventing overselling.
available_qty = bought_qty - sold_qty
if available_qty < quantity:
return JSONResponse(status_code=400, content={"error": "Not enough holdings"})The app uses FastAPI lifecycle events to establish Redis, run DB migrations, and launch the WebSocket background task on startup — and cleanly closes Redis on shutdown.
- Live price and 24h change for the top 90 cryptocurrencies
- Fetched from Redis-backed WebSocket stream, updated in real time
- Clickable cards redirect to the Trading Terminal
- Current holdings computed via FIFO across all historical transactions
- Live P/L (profit/loss) per asset using real-time prices from Redis
- Total portfolio value displayed dynamically
- Complete timestamped record of every buy and sell
- Sorted newest-first, with status, price, and quantity per trade
- Default starting balance: $100,000 virtual USD
- Users can add or remove funds at any time
- Balance updates are persisted to SQLite
- Interactive price chart per cryptocurrency
- Buy and sell order forms with live price feedback
- Order validation: checks balance (buy) and available holdings (sell) before execution
┌─────────────────────────────────────────────────────────┐
│ Browser │
│ HTML / CSS / JS (Static Frontend) │
└────────────────────┬────────────────────────────────────┘
│ HTTP / REST API
┌────────────────────▼────────────────────────────────────┐
│ FastAPI Backend │
│ │
│ /api/dashboard/prices → Redis Cache Read │
│ /api/portfolio/holdings → SQLite + Redis │
│ /api/order/buy → Redis + SQLite Write │
│ /api/order/sell → Redis + SQLite Write │
│ /api/transactions → SQLite Read │
│ /api/balance → SQLite Read/Write │
└──────────┬──────────────────────┬───────────────────────┘
│ │
┌──────────▼──────┐ ┌──────────▼──────────────────────┐
│ SQLite (app.db) │ │ Redis Cache │
│ │ │ (live prices from WS stream) │
│ - Balance │ └──────────┬──────────────────────┘
│ - Transactions │ │ subscribe / write
│ - Crypto list │ ┌──────────▼──────────────────────┐
└──────────────────┘ │ Binance WebSocket API │
│ (~90 concurrent streams) │
└─────────────────────────────────┘
Data Flow Summary:
- On startup, a background async task opens ~90 WebSocket connections to Binance
- Each ticker update writes the latest price into Redis
- API endpoints read from Redis for live prices (no repeated external calls)
- Buy/sell orders validate against Redis prices and write to SQLite
- Portfolio and transaction history are computed from SQLite records
All endpoints are prefixed with /api. Interactive docs available at /docs when running locally.
| Method | Endpoint | Description |
|---|---|---|
GET |
/api/balance |
Get current virtual balance |
POST |
/api/balance/update |
Add or remove virtual funds |
POST /api/balance/update payload:
{
"action": "add",
"amount": 5000.0
}action accepts "add" or "remove". Returns updated balance or error.
| Method | Endpoint | Description |
|---|---|---|
GET |
/api/dashboard/prices |
Live prices + 24h change for all tracked cryptos |
Sample response:
[
{ "name": "Bitcoin", "symbol": "BTCUSDT", "price": 62450.00, "change": "+1.24%" },
{ "name": "Ethereum", "symbol": "ETHUSDT", "price": 3120.50, "change": "-0.47%" }
]| Method | Endpoint | Description |
|---|---|---|
GET |
/api/portfolio/holdings |
FIFO-computed holdings with live P/L |
Sample response:
{
"holdings": [
{
"symbol": "BTC",
"quantity": 0.05,
"avg_price": 60000.00,
"live_price": 62450.00,
"profit_loss": 122.50
}
]
}| Method | Endpoint | Description |
|---|---|---|
POST |
/api/order/buy |
Execute a simulated buy order |
POST |
/api/order/sell |
Execute a simulated sell order |
Request payload (both):
{
"symbol": "BTCUSDT",
"quantity": 0.01
}Buy response:
{
"message": "Order executed",
"side": "BUY",
"symbol": "BTCUSDT",
"quantity": 0.01,
"price": 62450.00,
"spent": 624.50,
"balance": 99375.50
}Sell error (insufficient holdings):
{
"error": "Not enough holdings",
"available": 0.005
}| Method | Endpoint | Description |
|---|---|---|
GET |
/api/transactions |
Full history of all buy/sell transactions |
Sample response:
[
{
"transaction_type": "buy",
"crypto_symbol": "BTC",
"quantity": 0.01,
"price": 62450.00,
"timestamp": "2025-04-01T10:32:00",
"status": "completed"
}
]| Layer | Technology | Why |
|---|---|---|
| Backend | Python 3.13.2 + FastAPI | Async-first, fast, auto-docs via OpenAPI |
| Real-Time | WebSockets (Binance API) | Live price streaming at scale |
| Caching | Redis | Low-latency price reads, reduced API calls |
| Database | SQLite | Lightweight persistent storage for trades and balance |
| Frontend | HTML, CSS, Vanilla JS | Simple, no build tooling needed |
| Hosting | AWS EC2 (Amazon Linux 2023) | Full control, production-like environment |
| Proxy | Nginx | SSL termination, static file serving |
| SSL | Let's Encrypt | Free, auto-renewing HTTPS |
| CI/CD | GitHub Actions | Auto-deploy on push to main |
| Containers | Docker + Docker Compose | Portable local dev environment |
Crypto-Trading-Simulator/
│
├── app/
│ ├── main.py # FastAPI app, all route definitions
│ ├── config.py # App-wide configuration
│ ├── constants/
│ │ └── coin.py # List of tracked cryptocurrencies
│ ├── database/
│ │ ├── db.py # SQLAlchemy engine and Base
│ │ ├── init_db.py # Table creation
│ │ └── session.py # DB session dependency
│ ├── models/
│ │ ├── balance.py # Balance ORM model
│ │ ├── crypto.py # Crypto ORM model
│ │ └── transaction.py # Transaction ORM model
│ ├── schemas/
│ │ ├── portfolio_schemas.py # Pydantic schemas for portfolio
│ │ └── transaction_schema.py # Pydantic schemas for transactions
│ ├── services/
│ │ ├── crypto_ws.py # Binance WebSocket manager
│ │ ├── price_service.py # Price fetch logic
│ │ ├── balance_transaction.py # Balance update service
│ │ └── transaction_services.py # Trade execution logic
│ ├── tasks/
│ │ └── scheduler.py # Background task scheduler
│ └── utils/
│ ├── cache.py # Redis read helpers (get_symbol_price, etc.)
│ └── redis_client.py # Redis connection lifecycle
│
├── static/ # Frontend assets
│ ├── dashboard.html / .js / .css
│ ├── portfolio.html / .js / .css
│ ├── tradingterminal.html / .js / .css
│ └── transaction.html / .js / .css
│
├── deploy/
│ ├── fastapi_nginx.conf # Nginx reverse proxy config
│ └── setup_nginx.sh # EC2 setup script
│
├── docs/
│ ├── api_docs.md
│ └── setup_guide.md
│
├── insert_crypto.py # Seed script for initial crypto data
├── app.db # SQLite database
├── Dockerfile
├── docker-compose.yml
├── requirements.txt
└── README.md
- Python 3.13.2
- Redis running locally (
redis-server) - Git
# 1. Clone the repository
git clone https://github.com/jaayysoni/Crypto-Trading-Simulator.git
cd Crypto-Trading-Simulator
# 2. Create a virtual environment
python -m venv venv
source venv/bin/activate # Mac/Linux
# venv\Scripts\activate # Windows
# 3. Install dependencies
pip install -r requirements.txt
# 4. Seed the crypto list (first time only)
python insert_crypto.py
# 5. Start the server
uvicorn app.main:app --reloadVisit http://localhost:8000 in your browser.
Redis must be running before starting the server. The app will fail to start if Redis is unreachable (it retries 3 times with 2s delay before raising
RuntimeError).
Run the full stack (FastAPI + Redis) with one command:
docker-compose up --buildVisit http://localhost:8000. Redis is configured automatically via Docker Compose networking.
The project uses GitHub Actions for continuous deployment:
- On every push to
main, the workflow SSHs into the EC2 instance - Pulls the latest code
- Restarts the FastAPI service via
systemd - Nginx serves the updated app with zero downtime
Pipeline config is in .github/workflows/.
Why FastAPI over Flask/Django?
FastAPI's native async/await support is essential for managing ~90 concurrent WebSocket connections and non-blocking Redis reads. Flask would require external async wrappers; Django is too heavyweight for this use case.
Why SQLite over PostgreSQL? This is a single-user simulator. SQLite removes infrastructure overhead while still providing ACID guarantees for trade and balance operations. Swapping to PostgreSQL for multi-user support would be a straightforward change to the SQLAlchemy connection string.
Why Redis for price caching? Calling Binance's REST API on every price request would hit rate limits almost immediately under real load. Redis acts as a write-through cache: the WebSocket streams update it continuously, and all endpoints read from it in sub-millisecond time.
Why FIFO for portfolio tracking?
FIFO is the most commonly used cost basis method and maps directly to how real brokerages calculate capital gains. It was implemented using collections.deque for O(1) pop from the front.
| Limitation | Planned Fix |
|---|---|
| Single-user (no auth) | Add JWT-based authentication and per-user portfolios |
| SQLite | Migrate to PostgreSQL for multi-user concurrency |
| No order types (limit, stop-loss) | Implement limit order queue with background task |
| Frontend is vanilla JS | Migrate to React for component-based UI |
| No test coverage | Add pytest unit tests for services and API endpoints |
CORS set to * |
Restrict to known origins in production |
This project is licensed under the MIT License.
Jay Soni — GitHub | Open to backend, full-stack, and platform engineering roles.
Built to demonstrate real-time systems design, async Python, and production cloud deployment skills.



