A paper trading engine for a class
My school does a stock market simulation, so I built something to trade it properly instead of guessing. It runs on the server, watches 28 large-cap stocks during market hours, and places paper trades based on indicators. It mirrors its fills to a real brokerage paper account too, so I can check its numbers against something I didn't write.
It is deliberately not an AI stock picker. The strategy is ordinary quantitative stuff — moving averages, RSI, MACD, momentum, volume and volatility, combined into a weighted score. There's an optional language-model sentiment scorer sitting behind the same interface as the default one, but it's off by default and it never makes the trading decision.
Interfaces first
I didn't know what the class platform would look like — whether it had an API, whether automated trading was even allowed. So everything talks through abstract interfaces: market data, news, strategy, order execution. Swapping in an adapter for the class platform later doesn't require touching the engine.
- Strategies — two of them, a trend-following one and a contrarian mean-reversion one, picked by config.
- Backtesting — runs a strategy over historical data, compares two strategies head to head, and does parameter grid search.
- Risk management — position sizing and caps on how often it can trade the same stock.
- Dashboard — a read-only web view of the portfolio and recent trades.
142 tests. They caught plenty. They did not catch any of the following.
Six things only the live system showed me
The big one. Every buy signal was being suppressed and I couldn't see why. The volume check compared the size of the single most recent trade — maybe 100 shares — against a full day's volume, which is millions. So every stock looked like its volume had collapsed ~100%, every cycle, all day. That dragged every score down just enough that nothing ever crossed the buy threshold. The bot spent its first live trading day thinking hard and never buying anything.
The one that cost actual money. The limit on how many times it could trade the same stock per day was counted in memory. Every time I redeployed a fix, that counter reset to zero. INTC got bought six times in one day against a limit of three, paying slippage on every extra one. It now counts real fills from the database instead.
And a credential leak. There was no .dockerignore, so the build copied my .env file with real API keys straight into the image layer. I caught it because the container started crash-looping — the file permissions I'd set on the host made it unreadable inside the container. Fixed the ignore file and pruned the images that had it baked in.