Overview
Most trading-bot tutorials stop at "loop through symbols and call the broker." Real bots need an event bus, multiple broker abstractions, paper-trading parity, a kill switch you can hit fast, and a database schema that survives your first compliance audit. This bot covers all of that — Canadian-market focused (Questrade), with US stocks via Alpaca and crypto via CCXT, all running through one async event loop.
Stack
- Language: Python 3.10+ (tested on 3.10, 3.11, 3.12)
- Async:
asyncio,aiohttp,httpx - Data: pandas, numpy
- Database: SQLAlchemy 2.0 async — SQLite/aiosqlite (dev), PostgreSQL (prod)
- Config: Pydantic Settings (
BaseSettingsfrom.env) - CLI: Click + Rich
- Brokers: alpaca-py, ccxt (crypto)
- Dashboard: FastAPI + uvicorn (optional)
- Testing: pytest, pytest-asyncio, pytest-cov
- Linting: ruff, black, mypy
- Deployment: Docker
Event-Driven Architecture
Components communicate exclusively through an event bus (core/events.py):
QUOTE_UPDATE— market data updates from a brokerSIGNAL_GENERATED— a strategy firesORDER_SUBMITTED/ORDER_FILLED— order lifecyclePOSITION_CHANGED— portfolio updateKILL_SWITCH_TRIGGERED— emergency stop
Strategies don't know about brokers and brokers don't know about strategies. Everything routes through events.
Broker Abstraction
Every broker implements BaseBroker:
async def connect(self) -> None: ...
async def disconnect(self) -> None: ...
async def get_quote(self, symbol: str) -> Quote: ...
async def submit_order(self, order: Order) -> str: ...
async def cancel_order(self, order_id: str) -> bool: ...
async def get_positions(self) -> list[Position]: ...
async def get_account_balance(self) -> Decimal: ...Switching from paper to live is one config change.
Strategy Interface
Strategies implement BaseStrategy with initialize(), generate_signals(), and validate_parameters(). The bot auto-discovers strategies via STRATEGY_NAME and STRATEGY_CLASS exports — adding a new one is a single file.
Active Strategies
| Strategy | Type | Signal Logic |
|---|---|---|
sma_crossover | Trend | Fast/slow SMA crossover |
rsi_mean_reversion | Mean reversion | Oversold (<30) buy, overbought (>70) sell |
grid_trading | Range | Buy/sell at grid intervals |
vwap | Volume | Buy below VWAP, sell above |
bollinger_breakout | Volatility | Buy on upper-BB breakout with volume confirm |
macd_crossover | Momentum | MACD/signal-line cross with trend filter |
stochastic_oscillator | Fast oscillator | %K/%D cross in oversold/overbought |
crypto_momentum | Momentum | Crypto-specific momentum with volume confirmation |
Database Models
AuditLog, Order, Trade, Position, MarketData, StrategyState, FintracReport. Schema is created directly via init_db() — no migrations layer; the schema is the code.
Risk & Compliance
- Kill switch: any subscriber can publish
KILL_SWITCH_TRIGGERED. Brokers cancel open orders and halt new submissions immediately. - FINTRAC reporting: any single transaction at or above CA$10,000 is auto-flagged into a
FintracReportrow. - Position limits:
MAX_POSITION_SIZE_PCT(default 5%),MAX_DAILY_LOSS(default $1,000).
Key CLI Commands
trading-bot run --mode paper --strategies sma_crossover
trading-bot backtest -s sma_crossover --start 2024-01-01 --end 2024-12-31 -sym AAPL
trading-bot verify-broker
trading-bot statusLessons Learned
float(None)crashes: optional price fields must be guarded — broker APIs return null on illiquid symbols and a singlefloat(quote.bid)deep in a loop will down the bot.- Questrade 401 retry guard: a naive retry-on-401 will infinite-loop when the refresh token is genuinely dead. Cap retries and surface the error.
- Admin keys belong in request bodies, not query strings: query strings end up in access logs.
- Dashboard auth is critical: a FastAPI dashboard with no auth is a remote shell to your broker. Don't ship without it.
Testing
206 tests (pytest), 43% coverage. CI matrix runs Python 3.10, 3.11, and 3.12 through ruff → black → mypy → pytest with coverage.