The Quest Begins (The "Why")
Honestly, I was tired of staring at charts at 2 a.m., wondering if I should hit “buy” or “sell” while my coffee went cold. I’d tried manual trading a few times, made a profit here, a loss there, and always ended up feeling like I was playing a rigged slot machine. The moment that flipped the switch? I missed a 12 % swing on Bitcoin because I was busy debugging a unrelated script. I thought, “If only I could automate this… like a cheat code.”
That’s when I decided to build my first crypto trading bot. Not some black‑box hedge‑fund monster, but a simple, transparent script that could watch the market, execute a basic strategy, and free me up to actually enjoy life (or at least get a decent night’s sleep).
The Revelation (The Insight)
The big insight? You don’t need a PhD in quant finance to get started. A handful of ideas—price data, a simple indicator, and an order‑placement API—are enough to create something that works today. The real magic is in the loop: fetch data → compute signal → place order → wait → repeat.
I chose the popular ccxt library because it normalizes dozens of exchanges under one clean Python interface. With it, I could write the same code for Binance, Kraken, or Coinbase Pro and swap them out with a single line change.
The strategy I went with is a classic moving‑average crossover: when the short‑term MA crosses above the long‑term MA, we buy; when it crosses below, we sell. It’s not going to make you a whale overnight, but it’s a perfect sandbox to learn about order execution, error handling, and rate limits—those pesky “boss levels” that trips up many beginners.
Like when Neo dodges bullets, we need to dodge rate limits.
If you ignore them, the exchange will happily return HTTP 429 errors and your bot will grind to a halt—definitely not the hero moment you’re after.
Wielding the Power (Code & Examples)
Below is the before version—what I wrote the first night, full of optimism and a few hidden traps.
# before.py – naïve bot (don’t run this as‑is!)
import time
import ccxt
exchange = ccxt.binance({'enableRateLimit': True}) # we set it, but we don’t actually respect it
symbol = 'BTC/USDT'
short_window = 10
long_window = 30
def get_ohlcv():
return exchange.fetch_ohlcv(symbol, timeframe='1h', limit=long_window)
def compute_mas(ohlcv):
closes = [c[4] for c in ohlcv] # close price
short_ma = sum(closes[-short_window:]) / short_window
long_ma = sum(closes[-long_window:]) / long_window
return short_ma, long_ma
while True:
ohlcv = get_ohlcv()
short_ma, long_ma = compute_mas(ohlcv)
ticker = exchange.fetch_ticker(symbol)
price = ticker['last']
if short_ma > long_ma and not position:
print(f'🚀 BUY at {price}')
exchange.create_market_buy_order(symbol, 0.001) # tiny test size
position = True
elif short_ma < long_ma and position:
print(f'🔻 SELL at {price}')
exchange.create_market_sell_order(symbol, 0.001)
position = False
time.sleep(60) # wait a minute
What went wrong?
-
Rate‑limit ignorance – Even though we passed
enableRateLimit: True, we never checkedexchange.rateLimitor handled the occasional 429. After a few hundred calls, Binance would start returning errors, and our bot would crash. - Missing error handling – Network hiccups, exchange maintenance, or a malformed response would raise an uncaught exception and stop the loop.
- Hard‑coded order size – 0.001 BTC is fine for a testnet, but on mainnet it could be too big or too small depending on your balance.
The after version – a battle‑tested bot
# after.py – resilient bot with proper rate‑limit handling
import time
import ccxt
import logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s %(levelname)s %(message)s')
log = logging.getLogger(__name__)
exchange = ccxt.binance({
'enableRateLimit': True,
'options': {'defaultType': 'future'}, # adjust if you want spot/futures
})
symbol = 'BTC/USDT'
short_window = 12
long_window = 26
trade_amount = 0.001 # adjust to your risk tolerance
position = False # simple in‑memory flag; replace with persistence for production
def safe_fetch_ohlcv():
"""Fetch OHLCV while respecting rate limits and handling errors."""
try:
return exchange.fetch_ohlcv(symbol, timeframe='1h', limit=long_window)
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
log.warning(f'Transient error fetching OHLCV: {e}')
return None
def compute_mas(ohlcv):
closes = [c[4] for c in ohlcv]
short_ma = sum(closes[-short_window:]) / short_window
long_ma = sum(closes[-long_window:]) / long_window
return short_ma, long_ma
def place_order(side, amount):
try:
order = exchange.create_market_order(symbol, side, amount)
log.info(f'✅ {side.upper()} order placed: {order["id"]}')
return order
except (ccxt.InsufficientFunds, ccxt.InvalidOrder) as e:
log.error(f'Order failed: {e}')
except ccxt.NetworkError as e:
log.warning(f'Network issue on order: {e}')
except ccxt.ExchangeError as e:
log.error(f'Exchange error on order: {e}')
return None
def main_loop():
global position
while True:
ohlcv = safe_fetch_ohlcv()
if ohlcv is None:
time.sleep(exchange.rateLimit / 1000) # back off a bit
continue
short_ma, long_ma = compute_mas(ohlcv)
ticker = exchange.fetch_ticker(symbol)
price = ticker['last']
log.info(f'Price: {price:.2f} | Short MA: {short_ma:.2f} | Long MA: {long_ma:.2f} | Position: {"LONG" if position else "FLAT"}')
if short_ma > long_ma and not position:
log.info('🚀 Signal: BUY')
place_order('buy', trade_amount)
position = True
elif short_ma < long_ma and position:
log.info('🔻 Signal: SELL')
place_order('sell', trade_amount)
position = False
else:
log.info('⏳ No action this cycle.')
# Sleep respecting the exchange's rate limit (in ms)
time.sleep(exchange.rateLimit / 1000)
if __name__ == '__main__':
main_loop()
Why this feels like a win:
- The
safe_fetch_ohlcvwrapper catches network glitches and respects the exchange’s built‑in rate limit (exchange.rateLimit). - Logging replaces noisy
prints, giving you timestamps and severity levels—essential when you leave the bot running overnight. - Order placement is isolated, so a bad order doesn’t kill the whole loop.
- The core logic (MA crossover) stays unchanged, proving you can iterate on safety without reinventing the wheel.
Why This New Power Matters
Now you have a template that you can morph into anything:
- Swap the MA crossover for RSI, MACD, or even a machine‑learning signal.
- Persist
positionin a tiny SQLite DB or Redis so survivable restarts don’t lose state. - Add risk‑management layers: stop‑loss, take‑profit, position sizing based on account equity.
- Deploy it on a cheap VPS, a Raspberry Pi, or a serverless function (just remember to keep the loop alive).
The biggest shift? You’re no longer reacting to the market; you’re letting a disciplined script do the heavy lifting while you focus on strategy, learning, or actually sleeping. It’s like upgrading from a wooden sword to a lightsaber—same goal, far less fatigue.
Your Turn
Ready to forge your own bot? Grab the code above, plug in your API keys (testnet first!), and see what happens when the short MA kisses the long MA. Tweak one thing—maybe the timeframe, maybe the trade size—and observe how the bot’s behavior changes.
Challenge: After you get the basic loop running, add a simple stop‑loss that triggers if the price drops 2 % below your entry price. Share your results (or your struggles) in the comments—I’d love to hear how your quest went!
Happy hacking, and may your trades be ever in your favor. 🚀
Top comments (0)