The Quest Begins (The "Why")
Honestly, I was tired of staring at candlestick charts at 2 a.m., coffee gone cold, wishing I had a tiny robot that could buy the dip while I slept. I’d dabbled in manual trades on Binance, felt the rush of a winning trade, and then the gut‑punch of missing a sudden spike because I was busy debugging a completely unrelated script. Sound familiar?
That moment — when I realized I’d just watched a 5 % move happen while I was refreshing a Reddit thread — felt like Neo taking the red pill. I wanted out of the simulation of endless manual clicking and into a world where my code could watch the markets 24/7, execute a simple strategy, and let me focus on actually building stuff instead of chasing price ticks.
So I embarked on a quest: build a crypto trading bot that could run on a cheap VPS, use a popular exchange API, and implement a basic mean‑reversion strategy. If I could get that working, I’d have a solid foundation to experiment with more complex ideas later.
The Revelation (The Insight)
The biggest “aha!” wasn’t some exotic algorithm; it was realizing that the hard part isn’t the math — it’s the plumbing. Once you have reliable market data, a clean way to place orders, and solid error handling, the strategy itself becomes just a few lines of logic.
I settled on three core pieces:
-
Data fetch – using the
ccxtlibrary to pull OHLCV candles from Binance. - Signal generation – a simple mean‑reversion rule: if the price drops more than 2 % below its 20‑period SMA, we go long; if it rises 2 % above the SMA, we exit.
- Order execution – market orders with a fixed USD amount, plus basic safety checks (min‑order size, balance).
The magic was in separating concerns: fetch → decide → act. When each piece worked in isolation, wiring them together felt like casting a spell that actually worked.
Wielding the Power (Code & Examples)
The Struggle (Before)
My first attempt was a monolithic script that fetched data, calculated indicators, placed an order, then slept for a minute — all inside a while True loop with zero error handling. It looked something like this:
import time, ccxt
exchange = ccxt.binance({'enableRateLimit': True})
symbol = 'BTC/USDT'
amount_usd = 20
while True:
ohlcv = exchange.fetch_ohlcv(symbol, timeframe='1m', limit=21)
closes = [c[4] for c in ohlcv]
sma = sum(closes[-20:]) / 20
price = closes[-1]
if price < sma * 0.98: # 2% below SMA → buy
exchange.create_market_buy_order(symbol, amount_usd / price)
elif price > sma * 1.02: # 2% above SMA → sell (close)
# Oops! No position tracking → we might sell nothing or short!
exchange.create_market_sell_order(symbol, amount_usd / price)
time.sleep(60)
Traps I fell into:
- No position awareness – the bot would keep buying on every dip, even if it already held BTC, quickly exceeding my intended exposure.
- No error handling – a network hiccup or rate‑limit burst would crash the loop, leaving the bot silent until I noticed.
- Hard‑coded amount – using a fixed USD amount ignored the fact that the minimum order size on Binance changes with price; sometimes the bot tried to send 0.00001 BTC and got rejected.
The Victory (After)
I refactored the script into three clear functions, added a simple position tracker, and wrapped exchange calls in retry logic. Here’s the cleaned‑up version:
import time, ccxt, logging
from decimal import Decimal, ROUND_DOWN
logging.basicConfig(level=logging.INFO, format='%(asctime)s %(levelname)s %(message)s')
exchange = ccxt.binance({
'enableRateLimit': True,
'options': {'defaultType': 'future'} # adjust if you trade spot or futures
})
symbol = 'BTC/USDT'
timeframe = '1m'
lookback = 20 # SMA period
threshold = Decimal('0.02') # 2%
usd_per_trade = Decimal('20')
position = 0 # positive = long, negative = short, 0 = flat
def fetch_sma():
ohlcv = exchange.fetch_ohlcv(symbol, timeframe=timeframe, limit=lookback + 1)
closes = [Decimal(str(c[4])) for c in ohlcv]
sma = sum(closes[:-1]) / lookback # exclude the forming candle
return sma, closes[-1]
def amount_to_trade(price):
# Ensure we respect the exchange's step size and min notional
market = exchange.market(symbol)
step = Decimal(str(market['precision']['amount']))
min_notional = Decimal(str(market['limits']['cost']['min'])) if market['limits']['cost']['min'] else Decimal('0')
raw = (usd_per_trade / price).quantize(step, rounding=ROUND_DOWN)
if raw * price < min_notional:
raise ValueError(f'Order too small: {raw} {symbol.split("/")[0]} < min_notional')
return raw
def place_order(side, qty):
try:
if side == 'buy':
order = exchange.create_market_buy_order(symbol, float(qty))
else:
order = exchange.create_market_sell_order(symbol, float(qty))
logging.info(f'{side.upper()} order placed: {qty} {symbol.split("/")[0]} @ market')
return order
except Exception as e:
logging.error(f'Order failed: {e}')
return None
def main():
global position
while True:
try:
sma, price = fetch_sma()
signal = Decimal('0')
if price < sma * (Decimal('1') - threshold):
signal = Decimal('1') # go long
elif price > sma * (Decimal('1') + threshold):
signal = Decimal('-1') # go flat (exit long)
if signal == 1 and position <= 0: # enter long
qty = amount_to_trade(price)
if place_order('buy', qty):
position = float(qty)
elif signal == -1 and position > 0: # exit long
qty = amount_to_trade(price)
if place_order('sell', qty):
position = 0
else:
logging.debug(f'No action. SMA={sma:.2f}, price={price:.2f}, position={position}')
except ccxt.NetworkError as e:
logging.warning(f'Network issue: {e}')
except ccxt.ExchangeError as e:
logging.error(f'Exchange error: {e}')
except Exception as e:
logging.exception(f'Unexpected error: {e}')
time.sleep(60) # respect rate limits; adjust as needed
if __name__ == '__main__':
main()
What changed?
- Position tracking – we only open a trade when we’re flat and only close when we’re long. No accidental pyramiding.
-
Robust order sizing –
amount_to_traderespects the exchange’s step size and minimum notional, preventing those pesky “order too small” rejections. - Error handling – network glitches are logged and the loop continues; unexpected exceptions are caught with a traceback so I can debug without losing the bot entirely.
- Clean separation – fetching data, deciding, and executing are distinct blocks, making it trivial to swap in a different indicator or risk model later.
Running this on a $5/month VPS has given me a sleep‑friendly, semi‑automated trader that logs every action. I’ve already seen it catch a few quick reversions I would have missed while binge‑watching The Mandalorian.
Why This New Power Matters
Now that I’ve got the skeleton, the real fun begins. I can plug in a moving‑average crossover, add a simple stop‑loss, or even experiment with machine‑learning signals — all without rewriting the core loop. The bot has turned trading from a frantic, adrenaline‑fueled hobby into a systematic experiment where I control the variables and learn from the data.
More importantly, it’s reminded me that automation isn’t about replacing intuition; it’s about freeing up mental bandwidth to focus on the why behind a strategy, not the how of clicking buttons. If you’ve ever felt stuck in a loop of manual checks, give this a try. You might just feel like you’ve dodged a bullet — or, in my case, avoided missing a 3 % pump while I was making breakfast.
Your Turn
Take the skeleton above, swap the SMA for an RSI threshold, or add a trailing stop. Deploy it on a testnet first, watch the logs, and see how the bot behaves when the market gets choppy. What’s the first tweak you’ll make? Drop a comment or tweet your results — let’s keep the quest going together!
Happy coding, and may your trades be ever in your favor. 🚀
Top comments (0)