I deployed my first trading bot with real money on the line. $33. Not life-changing, not pocket change — just enough to make me care.
Every single trade closed in the red.
This is the story of what went wrong, what I fixed, and what I actually learned about building trading bots that most tutorials won't tell you.
The Bot
Quick context if you haven't seen my other post: I built an end-to-end crypto trading system in Python. Backtesting engine, 50 strategy implementations, live trading bot. The whole thing runs on ccxt, connects to a real exchange, and places real orders.
The live bot runs an EMA Crossover strategy on BTC/USDT. When the fast EMA (12-period) crosses above the slow EMA (26-period), it buys. When it crosses below, it sells. Simple, boring, well-understood.
I'd backtested this strategy across three years of historical data. The numbers looked solid — 491% return, Sharpe ratio of 1.30, 34 trades total. The max drawdown was -34%, which I thought I could stomach.
I was wrong about that last part.
Going Live
Before touching real money, I ran the bot in DRY_RUN mode for a few days. DRY_RUN does everything the live bot does — fetches prices, calculates signals, decides when to buy or sell — except it doesn't actually place orders. It just logs what it would have done.
class ExchangeClient:
def __init__(self, config):
self.dry_run = config.dry_run
self.exchange = ccxt.mexc({
'apiKey': config.api_key,
'secret': config.api_secret,
'enableRateLimit': True,
})
async def place_order(self, symbol, side, amount):
if self.dry_run:
logger.info(f"[DRY_RUN] {side} {amount} {symbol}")
return {"status": "dry_run", "side": side}
return await self.exchange.create_market_order(symbol, side, amount)
DRY_RUN looked fine. Signals were firing, logs were clean. So I flipped the switch.
I started with $1. Literally one dollar. The reasoning was simple: if something goes horribly wrong — a bug in the order logic, a miscalculated position size, some edge case I didn't think of — I lose a dollar. That's a vending machine coffee.
After a few days at $1 with no explosions, I moved to $5, then $10, then put in my full $33 budget.
The Day Everything Went Red
It didn't happen all at once. That would've been easier to deal with, honestly.
The first trade closed at a small loss. Fine. That happens. The second one, same thing. Then the third. By the fourth consecutive red trade, I started checking the bot logs every hour. By the sixth, I was reading the EMA calculation code line by line at 2 AM looking for bugs.
There were no bugs. The code was doing exactly what I told it to do.
Here's what the trade log looked like:
2026-01-15 08:00 BUY 0.0004 BTC at market → SELL at market -0.62%
2026-01-17 08:00 BUY 0.0004 BTC at market → SELL at market -0.57%
2026-01-19 08:00 BUY 0.0004 BTC at market → SELL at market -0.57%
2026-01-21 08:00 BUY 0.0004 BTC at market → SELL at market -0.43%
2026-01-23 08:00 BUY 0.0004 BTC at market → SELL at market -0.43%
Five trades, five losses. Small individually — half a percent each — but watching your own code systematically lose real money hits different than a red line on a backtest chart. There's a moment where you think: "Did I just automate losing money?"
The dollar amounts were tiny. We're talking about losing 20 cents per trade on a $33 account. But the feeling was not tiny. When you hand-trade and lose, you can blame your emotions, your timing, your gut. When your bot loses, there's nobody else. You wrote the rules. The machine followed them perfectly. And it still lost.
What Actually Went Wrong
The root cause had nothing to do with the code.
BTC was in a ranging market — bouncing around within a tight range for about two weeks, barely moving a few percent in either direction. The price wasn't trending. It was just chopping sideways in a narrow band.
EMA Crossover is a trend-following strategy. It needs trends to work. When the market goes sideways, the two moving averages keep crossing and re-crossing every few hours, generating buy signals right before the price dips and sell signals right before it recovers. Textbook whipsaw.
My backtest covered 37 months of data. Over that entire period, the strategy made money because BTC had several strong trends — the 2023 recovery, the 2024 rally, the post-ETF run. The ranging periods were there too, but they got averaged out in the final numbers.
I happened to start live trading during one of those ranging periods. Bad luck? Kind of. But also a design flaw: I didn't have anything in the system to detect "hey, the market isn't trending right now, maybe sit this one out."
What I Changed
The Circuit Breaker
The first thing I built was a circuit breaker — not based on consecutive losses, but on cumulative loss over rolling time windows. Financial circuit breakers work the same way: if total losses in a period exceed a threshold, trading stops.
class CircuitBreakerLevel(Enum):
NONE = "none"
DAILY = "daily"
WEEKLY = "weekly"
MONTHLY = "monthly"
class CircuitBreaker:
def __init__(self, initial_balance, daily_threshold=0.03,
weekly_threshold=0.07, monthly_threshold=0.15,
state_file=None):
self._initial_balance = initial_balance
self._daily_threshold = daily_threshold # -3% daily
self._weekly_threshold = weekly_threshold # -7% weekly
self._monthly_threshold = monthly_threshold # -15% monthly (kill switch)
self._trades = []
self._killed = False
def check_limits(self):
daily_pct = self.get_daily_pnl() / self._initial_balance
weekly_pct = self.get_weekly_pnl() / self._initial_balance
monthly_pct = self.get_monthly_pnl() / self._initial_balance
if monthly_pct <= -self._monthly_threshold:
self._killed = True # Kill switch — manual reset required
return CircuitBreakerLevel.MONTHLY
if weekly_pct <= -self._weekly_threshold:
return CircuitBreakerLevel.WEEKLY # Resumes next Monday
if daily_pct <= -self._daily_threshold:
return CircuitBreakerLevel.DAILY # Resumes next day
return CircuitBreakerLevel.NONE
Three levels: daily (-3%), weekly (-7%), and monthly (-15% kill switch). On a $33 account, the daily limit trips at about $1 of loss. That sounds tiny, but the point is this same logic scales — at $3,300 it's $99, same ratio. The state is persisted to a JSON file so it survives bot restarts, and the monthly kill switch requires a manual reset to resume trading.
Volatility Filter
The real fix was teaching the bot to recognize when the market isn't trending. I added an ATR (Average True Range) filter. When volatility drops below a threshold, the bot ignores crossover signals entirely.
def should_trade(self, candles) -> bool:
atr = self.calculate_atr(candles, period=14)
# candles[-1][4] is the close price in ccxt OHLCV format
# [timestamp, open, high, low, close, volume]
close = candles[-1][4]
atr_pct = atr / close * 100
if atr_pct < self.config.atr_threshold:
logger.info(f"ATR {atr_pct:.2f}% below threshold, skipping")
return False
return True
The logic: if ATR as a percentage of price is below the threshold, the market is too quiet for a trend-following strategy. Don't trade.
I'm still tuning the threshold value. Set it too low and the bot still trades in weak trends. Set it too high and it misses real opportunities.
Position State Persistence
This one I found by accident. The bot crashed during a test run (out of memory — I was logging too aggressively), and when it restarted, it had no idea it was holding a position. It would've opened a duplicate order.
import json
from pathlib import Path
class PositionManager:
def __init__(self, state_file="position_state.json"):
self.state_file = Path(state_file)
self.position = self._load_state()
def _load_state(self):
if self.state_file.exists():
return json.loads(self.state_file.read_text())
return {"side": None, "entry_price": 0, "amount": 0}
def update(self, side, entry_price, amount):
self.position = {
"side": side,
"entry_price": entry_price,
"amount": amount,
}
self.state_file.write_text(json.dumps(self.position, indent=2))
Simple JSON file. Survives crashes. Not elegant, but it works and I can inspect it manually when things look weird.
The 50-Strategy Reality Check
While I was debugging the live bot, I went back and looked at my backtest results more carefully.
I'd tested 50 different strategies — EMA Crossover, MACD, Bollinger Bands, RSI, Parabolic SAR, some multi-timeframe stuff, even a few machine learning attempts. Here's the thing nobody tells you about backtesting 50 strategies: most of them are garbage.
The top 5 looked like this:
| Strategy | Sharpe | Return | Max Drawdown | Trades |
|---|---|---|---|---|
| Multi-Timeframe | 1.50 | 546% | -31.8% | 2 |
| EMA Crossover | 1.30 | 491% | -34.0% | 34 |
| Parabolic SAR | 1.25 | 456% | -37.3% | 94 |
| Triple MA | 1.25 | 502% | -39.4% | 20 |
| MACD | 1.17 | 428% | -33.2% | 84 |
See that Multi-Timeframe strategy at the top? Sharpe of 1.50, 546% return. Looks amazing. But it only made 2 trades in 37 months. Two. That's not a strategy, that's a coincidence. Classic overfitting — the parameters happened to catch two lucky swings.
I kept EMA Crossover for live trading because 34 trades felt like enough data points to trust, and the Sharpe of 1.30 wasn't the flashiest number but it was built on actual volume.
The ML strategies? All six of them produced zero trades. Not losses — literally zero. The models couldn't find patterns strong enough to trigger entries on daily BTC data. My guess is I need to add volume profiles, funding rates, or on-chain metrics as features — daily OHLCV alone just doesn't give ML enough signal to work with. I'll revisit this eventually, but for now, classical technical analysis won.
The pip Install Mystery
Side story. When I first set up the project, I ran pip install -e ".[dev]" and ended up with a bunch of mystery files in my project root — things like =0.2.1, =0.26.0, just floating there. Empty files named after version numbers.
I still don't know what caused it. Something about the way my pyproject.toml was parsed, maybe a pip version mismatch. I deleted them, recreated the venv, reinstalled, and they didn't come back. If you know what causes this, genuinely, please leave a comment.
The Rate Limit Incident
Early on, before I added proper rate limiting, I was fetching OHLCV data in a tight loop during development. Didn't think much of it — I just wanted the candles fast.
The exchange API returned a 429 (Too Many Requests) and soft-banned me for about 30 minutes. Not a big deal in hindsight, but in the moment I wasn't sure if I'd gotten my API key permanently revoked.
That's why the exchange client has enableRateLimit: True in the ccxt config. The library handles request spacing automatically. Without it, you will get rate-limited if you're doing anything more than one request every few seconds.
What I'd Tell Past Me
Start with $1. I know I already said this, but it bears repeating because every single person I've talked to about this starts with "well, $50 isn't that much." It is. $1 for at least a week. You're testing your code with real money for the first time, and the goal is to catch bugs, not to generate returns.
Same goes for DRY_RUN. Don't just flip it on, check the logs once, and call it good. Run DRY_RUN for days. Compare the signals against what you would've done manually. Check that order sizes make sense. I found a rounding error in my position sizing during DRY_RUN that would've cost me about 8% per trade if I hadn't caught it.
The biggest mental shift was realizing that the backtest is not the strategy. A 491% return over 37 months means nothing if you start trading during the one month where the strategy bleeds. My EMA Crossover made money over three years but lost money in my first two weeks. Both of those things are true at the same time, and neither one is a lie.
Build a circuit breaker before you need one. I added mine after the losing streak — should've been there from day one.
And the hardest lesson: -34% drawdown feels very different when it's your money. In a backtest, -34% is a number in a table. In real life, it's watching your $33 become $21 and wondering if it's going to keep going. The math is the same. The experience is not.
Where I Am Now
The bot is running live — DRY_RUN=false, currently trading with 1.0 USDT. Circuit breaker is in place. ATR filter is catching most of the ranging-market whipsaws. I'm still on a small budget — $33 isn't going to make me rich, and I know that. The math says I'd need around $2,000 to generate $100/month at the backtested rate, and even that assumes the backtest translates 1:1 to live performance (it won't).
But the point was never to get rich from $33. The point was to build something real — a system that downloads data, tests strategies, picks winners, trades live, manages risk, and survives its own mistakes. That part works.
If You Want to Try It Yourself
You'll need Python 3.11+ and an exchange account with API access. I use MEXC — spot maker fees are zero and taker fees are just 0.1%, the minimum trade amount is low enough for small-budget testing, and the ccxt integration is solid. Any ccxt-compatible exchange will work with the codebase, though.
That's a referral link. Signing up is free and supports this project's development.
Setup:
# The code will be open-sourced on GitHub soon
git clone https://github.com/maymay5692/crypto-backtest-engine
cd crypto-backtest-engine
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -e ".[dev]"
Copy the environment template and add your API keys:
cp .env.example .env
EXCHANGE_API_KEY=your_key_here
EXCHANGE_SECRET_KEY=your_secret_here
DRY_RUN=true
TRADING_AMOUNT=1
Start with DRY_RUN=true and TRADING_AMOUNT=1. Seriously. Watch it run for a few days before you change either of those values.
Then run:
python -m src.live.main
The Honest Numbers
Since someone's going to ask: my $33 account has not made me money yet. After fees, slippage, and that initial losing streak, I'm slightly below where I started. The circuit breaker and ATR filter are catching bad trades now, but I've only been running the updated version for a few weeks.
I'll do a full 30-day performance report once I have enough data. No cherry-picking, no "if you started on the perfect day" projections. Just the actual P&L from a $33 account running one strategy on one pair.
If you want to follow along, the code will be open-sourced on GitHub soon. Bookmark this article — I'll update it with the repo link when it's ready.
Disclaimer: This is a technical walkthrough of my personal project, not financial advice. Crypto trading involves risk and you can lose money — I literally just told you about losing money. Only trade with funds you can afford to lose. Backtest results do not guarantee future performance. Also worth noting: automated trades are taxable events in most countries. Check your local rules before you start.
Top comments (0)