DEV Community

Timevolt
Timevolt

Posted on

How I Built My First Crypto Trading Bot — A Neo's Journey into the Matrix

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:

  1. Data fetch – using the ccxt library to pull OHLCV candles from Binance.
  2. 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.
  3. 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)
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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 sizingamount_to_trade respects 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)