Build a Simple Trading Bot in Python
A trading bot in Python is less about genius strategy and more about plumbing: fetching data, generating signals, sizing positions, sending orders, handling errors. Get the plumbing right and you can swap strategies in and out. Get it wrong and even a perfect strategy will destroy your account.
Before You Start
Run this on a testnet or paper trading account first. A bug in a live trading loop can empty an account in seconds. We will use Binance testnet in this tutorial.
Install Dependencies
pip install ccxt pandas numpy python-dotenvWe use ccxt because it abstracts over 100 crypto exchanges with one API. If you want to use a specific exchange API directly, the logic stays the same - only the client changes.
Exchange Setup
import ccxt
import pandas as pd
import numpy as np
import time
from dotenv import load_dotenv
import os
load_dotenv()
exchange = ccxt.binance({
'apiKey': os.getenv('BINANCE_API_KEY'),
'secret': os.getenv('BINANCE_SECRET'),
'enableRateLimit': True,
'options': {'defaultType': 'spot'}
})
# CRITICAL: use testnet until confident
exchange.set_sandbox_mode(True)
SYMBOL = 'BTC/USDT'
TIMEFRAME = '1h'
API keys live in a .env file, never in code. The sandbox mode routes everything to a test environment with fake money. Always start there.
Fetch Historical Candles
def fetch_ohlcv(symbol=SYMBOL, timeframe=TIMEFRAME, limit=200):
bars = exchange.fetch_ohlcv(symbol, timeframe=timeframe, limit=limit)
df = pd.DataFrame(bars, columns=['ts', 'open', 'high', 'low', 'close', 'volume'])
df['ts'] = pd.to_datetime(df['ts'], unit='ms')
df.set_index('ts', inplace=True)
return df
The RSI Strategy
RSI (Relative Strength Index) measures the speed and magnitude of recent price changes. Below 30 is typically called oversold, above 70 overbought. The strategy here waits for RSI to cross back through 30 from below (a bounce signal) and exits on a cross down through 70.
This is not a secret alpha. It is a simple, well-known pattern - the point of this tutorial is the infrastructure, not the strategy. You would backtest and refine the signal before trusting it.
def rsi(series, period=14):
delta = series.diff()
gain = delta.clip(lower=0)
loss = -delta.clip(upper=0)
avg_gain = gain.ewm(alpha=1/period, adjust=False).mean()
avg_loss = loss.ewm(alpha=1/period, adjust=False).mean()
rs = avg_gain / avg_loss
return 100 - (100 / (1 + rs))
def signal(df):
df['rsi'] = rsi(df['close'])
df['rsi_prev'] = df['rsi'].shift(1)
# Buy when RSI crosses up through 30 (oversold bounce)
buy = (df['rsi_prev'] < 30) & (df['rsi'] >= 30)
# Sell when RSI crosses down through 70 (overbought exhaustion)
sell = (df['rsi_prev'] > 70) & (df['rsi'] <= 70)
df['signal'] = 0
df.loc[buy, 'signal'] = 1
df.loc[sell, 'signal'] = -1
return df
Risk Management
This is the most important part of the bot. We risk a fixed percentage of equity per trade, calculated from the stop distance. If price is $50,000, equity is $10,000, risk is 1% ($100), and stop is 2% ($1,000), position size is 0.1 BTC. If we get stopped out, we lose exactly $100 regardless of price.
RISK_PER_TRADE = 0.01 # 1% of equity per trade
STOP_PCT = 0.02 # 2% stop loss
def position_size(equity_usdt, price, stop_pct=STOP_PCT):
risk_amount = equity_usdt * RISK_PER_TRADE
stop_distance = price * stop_pct
qty = risk_amount / stop_distance
return qty
def get_equity():
balance = exchange.fetch_balance()
return balance['total'].get('USDT', 0)
The Main Loop
The loop ties it together: fetch data, check signal, manage position, sleep, repeat. The try/except catches exchange errors (timeouts, rate limits, rejected orders) and keeps the bot running. A bot that crashes on the first network blip is useless.
def run_bot():
position = None # None, 'long'
entry_price = None
while True:
try:
df = fetch_ohlcv()
df = signal(df)
latest = df.iloc[-1]
price = latest['close']
if position is None and latest['signal'] == 1:
equity = get_equity()
qty = position_size(equity, price)
order = exchange.create_market_buy_order(SYMBOL, qty)
position = 'long'
entry_price = price
print(f"BUY {qty} at {price}")
elif position == 'long':
stop_hit = price < entry_price * (1 - STOP_PCT)
signal_exit = latest['signal'] == -1
if stop_hit or signal_exit:
bal = exchange.fetch_balance()
qty = bal['BTC']['free']
if qty > 0:
exchange.create_market_sell_order(SYMBOL, qty)
print(f"SELL at {price}, stop_hit={stop_hit}")
position = None
entry_price = None
time.sleep(60 * 60) # wait an hour for next candle
except Exception as e:
print(f"Error: {e}")
time.sleep(60)
if __name__ == '__main__':
run_bot()
Things This Bot Does Not Do Yet
- Backtest the strategy on historical data before running.
- Log trades to a database for analysis.
- Send alerts on fills or errors (use Telegram or Discord webhooks).
- Handle partial fills or slippage.
- Use a real stop-loss order instead of a soft exit.
- Run as a systemd service or in Docker for reliability.
Each of these is a substantial piece of work. Build them one at a time. The naive bot above will teach you more in one week of paper trading than any theoretical tutorial.
Paper Trading First
Run this on Binance testnet for at least two weeks. Watch how it handles weekends, news spikes, low liquidity periods. Count every bug, every missed fill, every weird edge case. Only when you cannot think of anything else that can go wrong should you consider live capital - and even then, start with an amount you are prepared to lose entirely.
Risk Warning
Automated trading can result in rapid and total loss of capital. Crypto markets are unregulated and highly volatile. Code is illustrative only - always test thoroughly and never trade money you cannot afford to lose.