cryptobot
An orchestrated crypto-trading framework on Binance, built as a sandbox for strategy ideas and architecture experiments. Not a commercial bot, not an attempt to print money — a research project about how to structure a system that trades, with real backtesting numbers behind every strategy.
The brief
Most crypto bots are one-file scripts — fetch candle, check indicator, place order. They work for a weekend and then fall apart the moment you want to try a second strategy, run a backtest, or survive a process restart without losing the current position.
I wanted to see what a slightly-more-serious version looked like: an execution framework decoupled from the strategies that feed it, a backtesting harness driven by real historical data, persistent state so a crash didn’t cost me an open position, and enough instrumentation that I could quantify whether any of the strategies I was trying actually did anything. The trading results were a secondary outcome — the architecture was the point.
Architecture
The core is an Orchestrator that holds the runtime state and a reference to a pluggable Strategy. The orchestrator knows nothing about specific indicators — it only knows when to ask the strategy for a decision (on a closed candle, to consider entry) and when to ask if a trade should exit (on every price tick, against stop-loss / take-profit / trailing rules).
That separation keeps strategy code free of order-book mechanics, wallet state, and persistence concerns. A strategy is a subclass of SuperStrategy with four entry points: check_entry, open_position, exit_limits, and check_exit. Everything else — order placement, fee accounting, wallet updates, history tracking, Telegram notifications — lives in the orchestrator and the state layer.
The same orchestrator runs against a live Binance exchange or a BacktestExchange implementation that replays historical candles from Redis. Paper-trading mode sits in the middle. One code path, three execution modes.
The strategy layer
Sixteen strategy files, all inheriting from the same interface:
Most of them are small variations on the same idea: combine an entry signal (MACD cross, RSI oversold, SAR flip, spike detection) with a different exit discipline (fixed TP/SL, trailing stop, Parabolic SAR, Fractal-based). The point wasn’t to find the one true signal — it was to make swapping one in or out trivial, and to quantify how each variation actually behaved under the same historical window.
Backtesting
Historical OHLCV candles are pre-loaded into Redis per contract. The BacktestExchange implementation replays them one at a time, triggering the same orchestrator callbacks a live WebSocket would: candle close to check entry, price ticks to check exit. The wallet, the persistence layer, and the state machine don’t know the difference.
At the end of each run the bot dumps a stats blob per strategy: total trades, profitable vs losing, win/loss ratio, average winner, average looser, max win, max loss, total fees, fees ratio, net profit, absolute net profit, average holding time. Thirteen measures per run. That’s what made it possible to compare strategies objectively instead of cherry-picking chart screenshots.
Actual backtest numbers
Two strategies, same $1,000 starting balance, same historical window. The numbers are straight out of the repo’s README.md:
- Total trades
- 2,042
- Win rate
- 29.8%
- Win / loss ratio
- 0.42
- Net profit (after fees)
- $164.89
- Fees ratio
- 4.82%
- Avg holding time
- ~70 hours
- Total trades
- 246
- Win rate
- 52.4%
- Win / loss ratio
- 1.10
- Net profit (after fees)
- $450.05
- Fees ratio
- 0.89%
- Avg holding time
- ~1,089 hours
Honest read on the numbers
The 45% net on the MACD-swing variant looks great until you read the fine print: 246 trades across four months is not a statistically meaningful sample, a single bull-market window is not a regime test, and the holding-time average sits in months — meaning most of that PnL is just being long crypto during an up-period. The baseline RSI strategy does something more honest: 2,042 trades, 4.8% fees ratio, and a 16% net that is almost entirely funded by the average winner being 3× the average loser, not by a higher win rate.
That’s the actual lesson I took from the project. The edge these strategies “found” was thinner than the fee structure in every case once you controlled for regime. None of this is news — it’s what anyone who has seriously backtested retail strategies concludes — but doing it yourself on your own framework with your own numbers is a different kind of knowing.
State and crash-recovery
Running a multi-pair bot live means the process will die — network hiccup, exchange timeout, Binance rate-limit, server reboot. The wallet state, the open positions, and the stats history all live in a PersistLayer so that on restart the orchestrator can hydrate: load open positions, reattach them to the in-memory state, reload the wallet’s running stats, and keep going without double-entering or forgetting a position it was already watching.
Max-coin exposure is enforced by the state (MAX_COINS) before a strategy is even asked for an entry signal — you can’t open a twenty-first position, no matter how bullish the indicator looks. Telegram notifications fire on fills, exits, wallet rollups, and uncaught exceptions via knockknock.
Looking back
Four years on, the architectural decisions still look right. Orchestrator / Strategy / State / Persistence is the separation I’d reach for again. Redis-as-backtest-store is overkill for the data volume (a directory of parquet files would be simpler), and some of the strategy files duplicate more than they should — a cleaner base class with composable exit rules would have cut the file count in half.
What I’d do differently now: treat fees and slippage as first-class in the backtester, test across regimes (not a single rising window), and resist the temptation to add the seventeenth strategy when the framework is already telling you the edge isn’t there.