Selected work · architecture notes

How ctrade is built

ctrade lets a user pick a trading strategy, set risk limits, and run a bot against a simulated wallet on live market data. It is a personal portfolio project, not a commercial product: paper trading only, no exchange keys, no real orders, no real money, no revenue. The interesting part is everything underneath.

What a user sees

A strategy library with a 30-day backtest for each strategy (profit against the market, trades, win rate, drawdown), a control form where stake, pairs and the strategy's own tunables are set, and a bots page that shows balance, open trades and a log tail for every running bot, refreshed every 15 seconds. Telegram alerts for entries, exits and errors. A plan page with subscription tiers.

Strategies page with seven strategies and their 30-day backtests

Backend

Django 4 with Django REST Framework and JWT authentication, in the cookiecutter-django layout: apps for users, strategies, controls, bots, exchanges, subscriptions and telegrams. PostgreSQL holds the data, Redis is the cache and the Celery broker, and Celery worker plus beat run the background work: starting and stopping bots, a reconcile task every minute that syncs bot state with the containers, log cleanup, demo limits, and the nightly removal of exited containers and old bot folders.

Bots as containers

Every bot is a freqtrade container in dry-run mode, started through the Docker socket on an isolated bot network with CPU and memory caps. The bot's config and parameter files are generated from the user's control and bind-mounted in. Each bot exposes freqtrade's REST API on that network only, with credentials generated per bot; a small wrapper builds a cached snapshot (balance, profit, open and closed trades, log tail) that the UI polls, and owner-only actions pause and resume entries, close a trade, or apply control changes in place without restarting the bot.

Market data

A separate candle-cache service sits between the bots and the exchanges (Kraken and Binance.US). It serves candles to every bot from one cache so ten bots do not mean ten times the exchange traffic, and it is the source for the USD conversions when a paper bot starts. It has its own test suite and its own container.

Strategies and backtests

A strategy's tunables (integer, decimal, categorical and boolean parameters) are extracted from the strategy code into a JSON schema stored on the strategy and rendered as form fields on the control page, grouped by hyperopt space. A management command replays every strategy on the last 30 days of Binance.US data so the library shows profit, trades, win rate and drawdown, and the list is sorted best backtest first.

Money in paper mode

Money is counted in US dollars. Each user has a starting balance that is applied to every bot they start; a control's stake per trade is in USD too, converted to the stake currency at the current price when the bot starts. Coins are validated against the exchange's market list so the pair list can never disagree with the stake currency, and the form shows the arithmetic of stake times trades against the balance before anything runs.

Deployment

One VPS, sized for a demo. Docker Compose runs Django (gunicorn), the Celery worker and beat, PostgreSQL, Redis, the candle cache and Traefik with Let's Encrypt. Static files are served by WhiteNoise; email goes out over SMTP. Bot containers live on their own network next to Django, the worker and the candle cache; PostgreSQL and Redis stay out of their reach. Demo limits stop a bot after 24 hours, cap bots at two per user and eight per host, and stop bots of accounts idle for a week.

Scope and what would change at scale

The container-per-bot design on a single host is deliberate for this project: it gives real process isolation, real resource caps and a real freqtrade instance per user with almost no orchestration code, and it is enough for a handful of concurrent users. It is not a design for thousands of them. Docker on one machine tops out at a few dozen bots, the reconcile loop polls rather than subscribes, and the candle cache is a single process. The path from here is known, not built: a bot scheduler on Kubernetes (one Job per bot, namespaces per tenant, quotas instead of per-container caps), bot state pushed over a queue instead of polled, the candle cache split into a fetcher and a shared store, and PostgreSQL moved off the app host. I would rather show a small system whose limits I can name than a large diagram I have not run.

Testing

Three suites run in CI as separate jobs: the Django unit and view tests in an isolated compose project, the candle-cache tests in a throwaway container, and Playwright end-to-end tests against the local stack, where bots start as real containers. A seed command produces deterministic data for the end-to-end run.

How it was built

With Claude Code as a pair programmer, from the first migration to the production compose file. The working rule was simple: scope each task tightly (an endpoint plus its tests, a migration, a refactor across one package), read every diff before it lands, and let the tests be the gate on anything that touches money math or order simulation. The design documents for the bot API, strategy parameters and the production plan live in the repository next to the code.

Source is private. This is a learning and portfolio project; it does not make money and is not meant to. Happy to walk through any part of it in a conversation.