Crypto Bots

How to Write a Python Script That Trades Crypto Automatically

A step-by-step guide to writing your first automated crypto trading script in Python using ccxt. Goes from zero to live trading with Binance API in under 100 lines of code.

A
AI Agents Hubยท2026-03-16ยท7 min readยท1,279 words

Builder of AI agents, crypto trading bots, and open-source automation tools. Sharing practical guides on how to build, deploy, and profit from AI and DeFi technology.

This is the tutorial I wish existed when I started. No theory, no fluff โ€” just working Python code that connects to Binance, reads prices, and executes trades automatically.

By the end of this guide you'll have a script that: connects to Binance via API, pulls real-time OHLCV data, calculates a simple buy/sell signal, and places orders automatically.

Prerequisites

  • Python 3.10+
  • A Binance account (testnet is fine to start)
  • Basic Python knowledge

Step 1: Install Dependencies

pip install ccxt python-dotenv pandas

Step 2: Set Up Binance API Keys

  1. Go to Binance Testnet โ€” you get free test funds
  2. Create API keys (Settings โ†’ API Management)
  3. Enable "Spot & Margin Trading" permission
  4. Disable withdrawals โ€” never enable this on a bot API key

Create a .env file:

BINANCE_API_KEY=your_api_key_here
BINANCE_API_SECRET=your_secret_here
BINANCE_TESTNET=true

Step 3: Connect to Binance

import ccxt
import os
from dotenv import load_dotenv

load_dotenv()

def get_exchange():
    """Initialize Binance connection."""
    exchange = ccxt.binance({
        'apiKey': os.getenv('BINANCE_API_KEY'),
        'secret': os.getenv('BINANCE_API_SECRET'),
        'enableRateLimit': True,  # Respect API rate limits
        'options': {'defaultType': 'spot'},
    })
    
    # Use testnet if configured
    if os.getenv('BINANCE_TESTNET') == 'true':
        exchange.set_sandbox_mode(True)
    
    return exchange

exchange = get_exchange()

# Verify connection
balance = exchange.fetch_balance()
usdt_balance = balance['USDT']['free']
print(f"Connected! USDT balance: ${usdt_balance:.2f}")

Step 4: Fetch Price Data

import pandas as pd

def get_ohlcv(symbol='BTC/USDT', timeframe='1h', limit=100):
    """Fetch OHLCV candlestick data."""
    ohlcv = exchange.fetch_ohlcv(symbol, timeframe, limit=limit)
    df = pd.DataFrame(ohlcv, columns=['timestamp', 'open', 'high', 'low', 'close', 'volume'])
    df['timestamp'] = pd.to_datetime(df['timestamp'], unit='ms')
    df.set_index('timestamp', inplace=True)
    return df

df = get_ohlcv('BTC/USDT', '1h', 100)
print(df.tail())

Output:

                          open      high       low     close       volume
timestamp                                                                   
2025-03-15 18:00:00  65234.50  65892.00  65100.20  65670.30  1234.56789
2025-03-15 19:00:00  65670.30  66120.00  65500.00  65980.50  1456.78900

Step 5: Calculate Your Signal

We'll use EMA crossover โ€” simple and effective:

def calculate_signals(df):
    """Calculate EMA crossover buy/sell signals."""
    df = df.copy()
    df['ema_fast'] = df['close'].ewm(span=9).mean()   # 9-period EMA
    df['ema_slow'] = df['close'].ewm(span=21).mean()  # 21-period EMA
    
    # Signal: 1 = buy, -1 = sell, 0 = hold
    df['signal'] = 0
    df.loc[df['ema_fast'] > df['ema_slow'], 'signal'] = 1
    df.loc[df['ema_fast'] < df['ema_slow'], 'signal'] = -1
    
    # Only trigger on crossovers (signal changes)
    df['crossover'] = df['signal'].diff()
    
    return df

df = calculate_signals(df)
latest = df.iloc[-1]
print(f"Current signal: {latest['signal']}")
print(f"EMA Fast: {latest['ema_fast']:.2f}, EMA Slow: {latest['ema_slow']:.2f}")

Step 6: Place Orders

def get_position(symbol='BTC/USDT'):
    """Check current position."""
    balance = exchange.fetch_balance()
    base = symbol.split('/')[0]  # 'BTC' from 'BTC/USDT'
    return balance[base]['free'] if base in balance else 0.0

def buy(symbol, usdt_amount):
    """Market buy with USDT amount."""
    ticker = exchange.fetch_ticker(symbol)
    price = ticker['last']
    amount = usdt_amount / price
    
    # Round to exchange's precision
    amount = exchange.amount_to_precision(symbol, amount)
    
    print(f"Placing BUY order: {amount} {symbol.split('/')[0]} @ ~${price:,.2f}")
    
    order = exchange.create_market_buy_order(symbol, float(amount))
    print(f"Order filled: {order['id']}")
    return order

def sell(symbol, amount):
    """Market sell all holdings."""
    amount = exchange.amount_to_precision(symbol, amount)
    print(f"Placing SELL order: {amount} {symbol.split('/')[0]}")
    
    order = exchange.create_market_sell_order(symbol, float(amount))
    print(f"Order filled: {order['id']}")
    return order

Step 7: The Main Trading Loop

import time

def run_bot(
    symbol='BTC/USDT',
    timeframe='1h',
    trade_size_usdt=100,  # $100 per trade
    sleep_seconds=60,     # Check every minute
):
    """Main bot loop."""
    print(f"๐Ÿค– Starting bot: {symbol} on {timeframe}")
    print(f"Trade size: ${trade_size_usdt} USDT")
    print("-" * 40)
    
    last_signal = 0
    
    while True:
        try:
            # 1. Get fresh data
            df = get_ohlcv(symbol, timeframe, limit=50)
            df = calculate_signals(df)
            
            current_signal = df['signal'].iloc[-1]
            current_price = df['close'].iloc[-1]
            timestamp = df.index[-1]
            
            # 2. Check for position
            btc_held = get_position(symbol)
            
            print(f"[{timestamp}] Price: ${current_price:,.2f} | Signal: {'LONG' if current_signal == 1 else 'SHORT'} | BTC held: {btc_held:.6f}")
            
            # 3. Act on new signals
            if current_signal != last_signal:
                if current_signal == 1 and btc_held < 0.0001:
                    # New BUY signal โ€” we have no position
                    balance = exchange.fetch_balance()
                    available_usdt = balance['USDT']['free']
                    size = min(trade_size_usdt, available_usdt * 0.95)
                    
                    if size > 10:  # Minimum $10 order
                        buy(symbol, size)
                    
                elif current_signal == -1 and btc_held > 0.0001:
                    # New SELL signal โ€” we have a position
                    sell(symbol, btc_held)
                
                last_signal = current_signal
            
        except ccxt.NetworkError as e:
            print(f"Network error: {e} โ€” retrying in 60s")
        except ccxt.ExchangeError as e:
            print(f"Exchange error: {e}")
        except KeyboardInterrupt:
            print("\nBot stopped by user.")
            break
        
        time.sleep(sleep_seconds)

# Start the bot
run_bot(symbol='BTC/USDT', timeframe='1h', trade_size_usdt=100)

Step 8: Add Stop-Loss Protection

This is critical. Without stops, one bad trade can wipe your account.

def run_bot_with_stops(
    symbol='BTC/USDT',
    timeframe='1h', 
    trade_size_usdt=100,
    stop_loss_pct=3.0,   # Exit if price drops 3% from entry
    take_profit_pct=6.0, # Exit if price rises 6% from entry
):
    """Bot with stop-loss and take-profit."""
    entry_price = None
    
    while True:
        df = get_ohlcv(symbol, timeframe, limit=50)
        df = calculate_signals(df)
        
        current_price = df['close'].iloc[-1]
        current_signal = df['signal'].iloc[-1]
        btc_held = get_position(symbol)
        in_position = btc_held > 0.0001
        
        # Stop-loss / take-profit check
        if in_position and entry_price:
            pnl_pct = (current_price - entry_price) / entry_price * 100
            
            if pnl_pct <= -stop_loss_pct:
                print(f"๐Ÿ›‘ STOP LOSS triggered at {pnl_pct:.1f}%")
                sell(symbol, btc_held)
                entry_price = None
                
            elif pnl_pct >= take_profit_pct:
                print(f"โœ… TAKE PROFIT triggered at +{pnl_pct:.1f}%")
                sell(symbol, btc_held)
                entry_price = None
        
        # Normal signal logic
        elif not in_position and current_signal == 1:
            balance = exchange.fetch_balance()
            size = min(trade_size_usdt, balance['USDT']['free'] * 0.95)
            if size > 10:
                buy(symbol, size)
                entry_price = current_price
        
        time.sleep(60)

Common Mistakes to Avoid

1. Trading with too much capital before testing Start with $20-50, not your life savings. Test on testnet first.

2. Not handling API errors Exchanges go down, network drops. Always wrap API calls in try/except.

3. Ignoring exchange fees Binance charges 0.1% per trade. A strategy that makes 0.05% per trade is actually losing money after fees.

4. Over-optimizing on historical data If your backtest shows 500% returns, it's almost certainly overfitted. Aim for consistent, modest returns.

5. Running on a laptop Your laptop sleeps, crashes, and loses connection. Deploy to a VPS (DigitalOcean, AWS EC2, etc.).

Running 24/7 on a VPS

# On Ubuntu VPS
pip install ccxt python-dotenv pandas

# Run with nohup (survives logout)
nohup python bot.py > bot.log 2>&1 &

# Or use screen
screen -S crypto-bot
python bot.py
# Ctrl+A, D to detach

# Check logs
tail -f bot.log

Full Script (Under 100 Lines)

Here's the complete, production-ready script:

import ccxt, os, time, pandas as pd
from dotenv import load_dotenv

load_dotenv()

exchange = ccxt.binance({
    'apiKey': os.getenv('BINANCE_API_KEY'),
    'secret': os.getenv('BINANCE_API_SECRET'),
    'enableRateLimit': True,
})
if os.getenv('BINANCE_TESTNET') == 'true':
    exchange.set_sandbox_mode(True)

def ohlcv(symbol, tf, limit=50):
    data = exchange.fetch_ohlcv(symbol, tf, limit=limit)
    df = pd.DataFrame(data, columns=['ts','o','h','l','c','v'])
    df['ema9'] = df['c'].ewm(span=9).mean()
    df['ema21'] = df['c'].ewm(span=21).mean()
    df['sig'] = (df['ema9'] > df['ema21']).astype(int) * 2 - 1
    return df

def held(symbol):
    b = exchange.fetch_balance()
    base = symbol.split('/')[0]
    return b.get(base, {}).get('free', 0)

def buy(symbol, usd):
    price = exchange.fetch_ticker(symbol)['last']
    amt = exchange.amount_to_precision(symbol, usd / price)
    return exchange.create_market_buy_order(symbol, float(amt))

def sell(symbol, amt):
    amt = exchange.amount_to_precision(symbol, amt)
    return exchange.create_market_sell_order(symbol, float(amt))

def run(symbol='BTC/USDT', tf='1h', size=100, sl=3.0, tp=6.0):
    prev_sig, entry = 0, None
    while True:
        try:
            df = ohlcv(symbol, tf)
            sig = df['sig'].iloc[-1]
            price = df['c'].iloc[-1]
            btc = held(symbol)
            in_pos = btc > 0.0001
            
            if in_pos and entry:
                pnl = (price - entry) / entry * 100
                if pnl <= -sl or pnl >= tp:
                    print(f"Exit at {pnl:+.1f}%")
                    sell(symbol, btc); entry = None
            elif not in_pos and sig == 1 and prev_sig != 1:
                bal = exchange.fetch_balance()['USDT']['free']
                s = min(size, bal * 0.95)
                if s > 10: buy(symbol, s); entry = price
            
            prev_sig = sig
            print(f"{symbol} ${price:,.0f} | {'LONG' if sig==1 else 'SHORT'} | {'IN' if in_pos else 'OUT'}")
        except Exception as e:
            print(f"Error: {e}")
        time.sleep(60)

if __name__ == '__main__':
    run()

That's it. Under 70 lines of actual trading logic. Start there, build up as you learn. The best bots are built incrementally โ€” add features only when you understand why you need them.

Next Steps

Once this is running:

  1. Add Telegram notifications (see our Telegram bot guide)
  2. Add multiple pairs (loop through ['BTC/USDT', 'ETH/USDT', 'SOL/USDT'])
  3. Backtest the strategy before going live
  4. Read about funding rate arbitrage for a market-neutral strategy

Related Articles