DEV Community

Timevolt
Timevolt

Posted on

Cryptocurrency trading bots: building your first automation

Quick context (why you're writing this)

I still remember the first time I stared at a candlestick chart at 2 a.m., coffee cold, wishing I could just set a rule and let the machine do the heavy lifting. I’d spent hours manually copying trades from Telegram signals into Binance, only to miss a move because I got distracted by a Slack notification. That “aha” moment – realizing the market never sleeps but I do – pushed me to throw together a tiny bot. If you’ve ever felt the same pull between wanting to stay involved and needing a life outside the screen, you’re in the right place.

The Insight

A trading bot isn’t magic; it’s just a disciplined way to enforce a strategy you already trust. The real win comes from removing emotional fatigue, not from predicting the future. If your edge is a simple moving‑average crossover, a bot will execute it exactly the same way every time, 24/7. The trade‑off? You still need to watch for edge cases: exchange rate limits, order‑filled status, and the ever‑present risk of slippage. Treat the bot as a tireless assistant, not a crystal ball.

How (with code)

Below is a minimal, working example that uses the ccxt library to talk to Binance and pandas for the indicator. I’ll show the core loop, point out two common pitfalls, and then give a cleaned‑up version.

Step 1 – Pull recent candles and compute the signal

import ccxt
import pandas as pd
import time

exchange = ccxt.binance({
    'enableRateLimit': True,   # lets ccxt respect Binance's limits
    'apiKey': 'YOUR_KEY',
    'secret': 'YOUR_SECRET',
})

def get_signal(symbol='BTC/USDT', timeframe='15m', limit=100):
    ohlcv = exchange.fetch_ohlcv(symbol, timeframe=timeframe, limit=limit)
    df = pd.DataFrame(ohlcv, columns=['timestamp','open','high','low','close','volume'])
    df['timestamp'] = pd.to_datetime(df['timestamp'], unit='ms')
    # Simple moving averages
    df['ma_fast'] = df['close'].rolling(9).mean()
    df['ma_slow'] = df['close'].rolling(21).mean()
    # Generate a signal: 1 = bullish crossover, -1 = bearish crossover
    df['signal'] = 0
    df.loc[df['ma_fast'] > df['ma_slow'], 'signal'] = 1
    df.loc[df['ma_fast'] < df['ma_slow'], 'signal'] = -1
    df['signal'] = df['signal'].diff()
    return df.iloc[-1]   # most recent row
Enter fullscreen mode Exit fullscreen mode

Mistake #1 – Ignoring the rate‑limit flag

If you omit 'enableRateLimit': True, ccxt will hammer the endpoint as fast as your loop can run. Binance will quickly return HTTP 429, your script will crash, and you’ll spend time debugging why “the exchange is mad at me.” Turning on the flag lets ccxt pause automatically when you’re close to the limit.

Step 2 – Place an order based on the signal

def trade(symbol='BTC/USDT', usd_amount=20):
    latest = get_signal(symbol)
    signal = latest['signal']
    price = latest['close']

    if signal == 2:   # bullish crossover (previous -1 -> now 1)
        print(f'[{time.strftime("%X")}] BUY signal @ {price}')
        order = exchange.create_market_buy_order(symbol, usd_amount / price)
    elif signal == -2: # bearish crossover (previous 1 -> now -1)
        print(f'[{time.strftime("%X")}] SELL signal @ {price}')
        # In a real bot you’d check your position size first
        order = exchange.create_market_sell_order(symbol, usd_amount / price)
    else:
        print(f'[{time.strftime("%X")}] No signal')
Enter fullscreen mode Exit fullscreen mode

Mistake #2 – Assuming the order fills instantly

Market orders on Binance are usually instant, but during high volatility they can partially fill or be rejected. The snippet above doesn’t check order['status'] or handle the case where you end up with less BTC than expected. A safer pattern is to poll the order until it’s closed or canceled, then adjust your position tracking accordingly.

Step 3 – The main loop (with a tiny safety net)

if __name__ == '__main__':
    while True:
        try:
            trade()
        except Exception as e:
            # Don't let a single hiccup kill the bot
            print(f'[{time.strftime("%X")}] Error: {e}')
        time.sleep(60)   # wait a minute before the next check
Enter fullscreen mode Exit fullscreen mode

That’s it – a functional skeleton you can run on a cheap VPS or even a Raspberry Pi. Replace the simple MA crossover with your own indicator, add proper position sizing, and you’ve got a repeatable process.

Why This Matters

Running a bot like this taught me three things that still shape how I approach automation:

  1. Consistency beats optimism – The bot will follow the rule even when I’m tempted to “just hold a little longer.”
  2. Observability is non‑negotiable – I now log every signal, order, and error to a file and forward critical alerts to Slack. Without that, a silent failure can wipe out gains.
  3. Start stupidly simple – The first version had no stop‑loss, no trailing‑exit, and a fixed USD amount. It was ugly, but it let me verify that the plumbing worked before I layered on complexity.

If you jump straight to a sophisticated AI‑driven model, you’ll spend weeks debugging data pipelines while missing the basics: can your code talk to the exchange, can it handle an error, and do you know what it actually did?

A challenge for you

Take the snippet above, run it against Binance’s testnet (or any exchange with a sandbox), and tweak one thing: add a basic stop‑loss that closes the position if the price moves 2% against you. Then ask yourself: Did the bot protect me during a sudden spike, or did it create extra trades that ate into profit? Share what you learned – the real value is in the conversation, not the code.


Happy hacking, and may your fills be ever in your favor.

Top comments (0)