Building an AI Trading Bot - Part 3: Understanding Your Bot's Code
In Part 2, you bought a $5 VPS, SSH’d in, cloned a repo, and ran a bot that printed live BTC prices with predictions every 60 seconds. You saw it work. Now let’s understand what it’s actually doing.
Open bot.py. All 70 lines of it. Don’t let code intimidate you. If you can read a TradingView chart, you can read this. Every line exists for a reason, and by the end of this post, you’ll be able to modify your bot without guessing what breaks.
We’re not doing “Python for beginners.” We’re doing “your trading bot, explained in trader language.” You know what a candlestick is. You know what a limit order does. That vocabulary is harder than anything in this file. The code is just the execution layer. Let’s walk through it.
The Imports: Where the Tools Come From
import time
import sys
import ccxt
Three lines at the top of your bot. Three different jobs.
import time gives your bot a clock. Without it, the bot fetches prices as fast as the CPU can loop - thousands of times per second. Binance rate-limits that instantly, and even if it didn’t, you don’t need 1,000 identical BTC prices per second. time lets you say “wait 60 seconds, then go again.” That’s the time.sleep(INTERVAL) call later in the file.
import sys handles system-level things - in this case, clean shutdown. When you press Ctrl+C, Python raises a KeyboardInterrupt. Without sys.exit(0), you’d get an ugly traceback dumped to your terminal. sys turns “user pressed stop” into a clean exit with no drama.
import ccxt is the engine. CCXT stands for CryptoCurrency eXchange Trading library. Think of it as a universal translator. Instead of learning Binance’s specific API format, Bybit’s format, Kraken’s format - you write one line of Python and CCXT handles the translation. When the bot calls exchange.fetch_ticker("BTC/USDT"), CCXT figures out which exchange you’re connected to, constructs the right API request, parses the response, and returns a clean Python dictionary. This library powers trading bots from hobbyist projects to institutional systems. It’s the standard.
If you’re wondering why we don’t import the model in Part 2: the model doesn’t exist yet. That’s Part 5. Right now, we’re building the scaffold it will sit on. The data pipeline - fetch price, run through prediction function, print result - stays identical whether the prediction comes from a placeholder or a trained neural network. That’s intentional architecture.
The Configuration Block: Your Bot’s Control Panel
# --- Configuration ---
SYMBOL = "BTC/USDT" # Trading pair
INTERVAL = 60 # Seconds between updates
CONFIDENCE = 65.0 # Simulated model confidence (Part 5 replaces this)
These three variables control everything the bot does. This is your dashboard. No config file, no JSON, no environment variables - just three lines you can read and change.
SYMBOL = "BTC/USDT" is the trading pair. The format is BASE/QUOTE, same convention as every exchange. BTC is what you’re watching. USDT is what it’s priced in. If you trade altcoins, change this to "ETH/USDT", "SOL/USDT", "DOGE/USDT" - anything Binance lists. The bot doesn’t care what symbol you give it. CCXT passes it straight through to the exchange API.
A word on exchange compatibility: the code uses ccxt.binance(). But CCXT supports 100+ exchanges. If you want to pull prices from Bybit instead, change the exchange creation line (we’ll get to it) to ccxt.bybit(). Same symbol format. Same method calls. This is the power of CCXT’s abstraction layer. Your bot doesn’t know or care which exchange it’s talking to.
INTERVAL = 60 is seconds between fetches. At 60 seconds, you’re pulling 1,440 data points per day. That’s plenty for a 1-hour or 4-hour timeframe model. If you’re day-trading 5-minute candles, drop it to 10 seconds. If you’re running a daily prediction model, set it to 3600 (one fetch per hour).
Be aware: Binance’s public API has rate limits. Even unauthenticated, you get 1,200 requests per minute for ticker data. At 10-second intervals, you’re at 6 requests per minute. You won’t hit the limit. But if you set INTERVAL = 1 and don’t have an API key, you’ll get temporarily blocked. Stay above 2 seconds for public endpoints.
CONFIDENCE = 65.0 is a placeholder. Right now, every prediction the bot prints says “confidence: 65%.” That number means nothing - it’s a hardcoded value the predict() function returns regardless of market conditions. In Part 5, this gets replaced with the model’s actual probability output. A real ML model doesn’t say BUY or SELL. It says “72% probability price goes up in the next hour.” That 72% is the confidence score. The CONFIDENCE variable exists now so the output format is stable from Part 2 through Part 10. When you swap the placeholder for a real model, everything downstream still works.
fetch_price(): The Data Pipeline
def fetch_price(exchange):
"""Fetch current BTC/USDT price from Binance."""
ticker = exchange.fetch_ticker(SYMBOL)
return ticker["last"]
Six lines, but this function does the one thing every trading bot must do: get the current price. Let’s trace it step by step.
The function takes an exchange object - that’s the CCXT exchange connection created in main(). You pass it in rather than creating it inside the function because you don’t want to reconnect to Binance every 60 seconds. Establish the connection once, reuse it. Network handshakes are expensive.
exchange.fetch_ticker(SYMBOL) calls Binance’s /api/v3/ticker/24hr endpoint. The response contains 15+ fields: last price, 24h high, 24h low, volume, bid, ask, percentage change. CCXT normalizes this into a dictionary you can access by key name.
ticker["last"] extracts only the last traded price - the most recent transaction on the order book, regardless of direction. For a prediction bot, this is what you want. You’re not placing orders yet, so bid/ask spread doesn’t matter. You need the price the market actually traded at.
A subtle detail: fetch_ticker() is a REST API call. It creates a new HTTP connection, sends a request, waits for the response, parses it. This takes 200-500ms depending on network latency. For a 60-second interval bot, that’s irrelevant. For a high-frequency system, you’d use WebSocket streams instead - persistent connections that push data to you in real time. We’ll upgrade to WebSocket in Part 9 when latency starts to matter for live trading. For now, REST is simpler and more than adequate.
If Binance is unavailable - server maintenance, DDoS protection, your VPS region blocked - CCXT raises a NetworkError or ExchangeError. We handle those in the main loop, not inside fetch_price(). This separation means the price fetcher focuses on one job: get the price and return it. Error handling lives where the decision about what to do lives - in the main loop.
predict(): Where the AI Will Live
def predict(price):
"""
Simulated AI prediction.
In Part 5, this will be replaced with a trained ML model
that outputs real probability scores.
"""
return {
"price": price,
"direction": "UP" if price % 2 == 0 else "DOWN",
"confidence": CONFIDENCE,
}
This is the most important function in the bot, and right now it’s completely fake. That’s by design.
The function receives the current price, packages it with a prediction, and returns a dictionary. The prediction logic is price % 2 - if the integer part of the price is even, predict UP. If odd, predict DOWN. This is mathematically random for our purposes. It has zero predictive power. That’s fine. The function exists to define the interface: inputs go in, a dictionary with direction and confidence comes out.
Why build a fake prediction function instead of waiting for Part 5? Two reasons.
First, the interface matters more than the implementation. The main loop expects predict() to return a dictionary with three keys: price, direction, confidence. As long as whatever replaces this function returns the same structure, the main loop works unchanged. This is called separation of concerns - the prediction logic and the display logic don’t depend on each other’s internals.
Second, it forces you to think about the model’s output format before writing any ML code. A real model doesn’t return a string like “UP” or “DOWN.” It returns a float between 0 and 1 - the probability. But downstream code that prints arrows and formats output expects direction labels. Where does the threshold conversion happen? Inside predict(). The function takes the model’s raw probability, compares it to a threshold (0.5 is the baseline for binary classification), and decides direction. The confidence score is the model’s probability, not a hardcoded number. This architecture means the complexity lives in one function, not scattered across the file.
In Part 5, predict() will look something like this:
def predict(price, features):
proba = model.predict_proba(features)[0][1] # probability of UP
return {
"price": price,
"direction": "UP" if proba >= 0.5 else "DOWN",
"confidence": round(proba * 100, 1),
}
Same output format. Same keys. The main loop won’t change.
main(): The Loop That Never Sleeps
def main():
print("=" * 50)
print(f" AI Trading Bot - {SYMBOL}")
print(" Build Your Own AI Trading Bot (Part 2)")
print("=" * 50)
print()
print(f"Fetching {SYMBOL} every {INTERVAL}s...")
print("Press Ctrl+C to stop.")
print()
The banner. "=" * 50 prints fifty equals signs - no magic, just Python multiplying a string. The f-strings (f"...{SYMBOL}") insert variable values into text. When SYMBOL is "BTC/USDT", the output reads AI Trading Bot - BTC/USDT. Change the variable at the top, the banner updates automatically.
exchange = ccxt.binance()
This creates the exchange connection object. Everything from fetch_ticker() calls to WebSocket streams runs through this object. CCXT handles authentication, rate limiting, and response parsing internally. Call ccxt.binance() for Binance, ccxt.bybit() for Bybit, ccxt.kraken() for Kraken. No API key is needed for public market data on any major exchange.
If you have a Binance account and want authenticated access (higher rate limits, account-specific endpoints), you’d add:
exchange = ccxt.binance({
'apiKey': 'YOUR_API_KEY',
'secret': 'YOUR_SECRET',
})
We won’t need this until Part 8 when the bot starts placing real trades. For now, public data is free and unlimited within generous rate limits.
try:
while True:
The outer try catches KeyboardInterrupt (Ctrl+C). The while True loop runs forever until explicitly stopped. This is the bot’s heartbeat.
try:
price = fetch_price(exchange)
prediction = predict(price)
arrow = "\u25b2" if prediction["direction"] == "UP" else "\u25bc"
print(f" {arrow} {SYMBOL}: ${price:,.2f} | "
f"confidence: {prediction['confidence']:.0f}% | "
f"prediction: {prediction['direction']}")
The inner try handles exchange errors. Call fetch_price() to get the latest price. Pass it to predict() for the (simulated) AI prediction. Decide the arrow character based on direction. Print a formatted line.
The formatting in the print statement deserves a close look:
$price:,.2fformats the price with commas and 2 decimal places.64110500becomes$64,110.50.prediction['confidence']:.0fformats the confidence as a whole number.65.0becomes65.- The f-string spans two lines for readability. Python concatenates adjacent string literals inside parentheses.
except ccxt.NetworkError:
print(" \u26a0 Network error - retrying in 30s...")
time.sleep(30)
continue
except ccxt.ExchangeError as e:
print(f" \u26a0 Exchange error: {e}")
time.sleep(30)
continue
Two error handlers, both essential for a bot running unattended.
NetworkError means the VPS couldn’t reach Binance at all - DNS failure, connection timeout, routing issue. Common causes: VPS data center has a transient network problem, Binance is under DDoS and unreachable, your VPS provider’s upstream is flapping. The bot waits 30 seconds and retries. If the issue is temporary, it recovers without human intervention. If it persists for hours, you have a bigger problem than the bot’s error handling.
ExchangeError means Binance responded with an error - rate limiting, maintenance, malformed request. The bot prints the error message and retries after 30 seconds. This distinction matters: NetworkError is connectivity, ExchangeError is the exchange telling you something specific. A production bot would also handle ccxt.RateLimitExceeded separately with exponential backoff, but for a single trading pair at 60-second intervals, you won’t hit rate limits on public endpoints.
Both handlers use continue to jump back to the top of the while True loop. This skips the time.sleep(INTERVAL) at the bottom. The 30-second wait inside the handler means the bot doesn’t sit idle for 60 seconds before retrying a known failure.
time.sleep(INTERVAL)
except KeyboardInterrupt:
print()
print("Bot stopped. See you in Part 3!")
sys.exit(0)
After a successful fetch and print, time.sleep(INTERVAL) pauses for the configured number of seconds. When INTERVAL is 60, the bot prints one line per minute and uses effectively zero CPU while sleeping. time.sleep() is a blocking call - it tells the operating system “wake this process up in 60 seconds.” The CPU is free to handle other tasks in the meantime.
The outermost KeyboardInterrupt handler catches Ctrl+C. sys.exit(0) terminates the process cleanly. Exit code 0 means “stopped by user, no error.” If you were monitoring this bot with a process supervisor, exit code 0 would prevent it from auto-restarting (which is correct - you stopped it intentionally).
if __name__ == "__main__":
main()
This is a Python convention that deserves explanation because it confuses every beginner. __name__ is a special variable Python sets automatically. When you run a file directly (python bot.py), __name__ equals "__main__". When the same file is imported by another script (import bot), __name__ equals "bot".
This guard means “only run main() if this file is being executed directly, not imported.” It prevents the bot from auto-starting when something else imports the file - useful later when we write backtest.py that imports fetch_price() and predict() from bot.py without starting the loop.
What You Can Tweak Right Now
You understand the code. Now change it and see what happens. This is how traders learn systems - observation, adjustment, observation.
Change the trading pair. Set SYMBOL = "ETH/USDT" and restart the bot. It pulls Ethereum prices instead of Bitcoin. No code changes required beyond one variable. Try "SOL/USDT", "LINK/USDT", "BNB/USDT". The bot doesn’t care what you trade as long as Binance lists it.
Change the interval. Set INTERVAL = 10 and watch the bot print every 10 seconds. Set it to 300 and get one update every 5 minutes. If you’re running multiple copies of the bot (one per trading pair), consider staggering intervals so they don’t all hit the API simultaneously.
Change the exchange. Replace ccxt.binance() with ccxt.bybit(). No other changes needed. The symbol format is the same. The method names are the same. Try Kraken if you prefer it. The bot is exchange-agnostic because CCXT abstracts the differences.
Change the confidence. Set CONFIDENCE = 82.5. Watch the output change. This is cosmetic right now, but when Part 5 replaces the placeholder, seeing a real probability next to each prediction will feel familiar because you’ve been looking at this format since Part 2.
Break something on purpose. Delete the import ccxt line. Run the bot. Read the error message. Then put it back. Delete the time.sleep(INTERVAL) line. Watch the bot spam your terminal. Then put it back. The fastest way to learn what each line does is to remove it and observe what breaks.
The Architecture Pattern
Step back from the code and notice the structure. The bot follows a pattern used by virtually every trading system:
while True:
data = fetch(exchange)
signal = model.predict(data)
display(signal)
wait(interval)
Three stages. Four lines of logic. This is not simplified for the tutorial - this is how real systems work. They just have more complex implementations of each stage.
fetch() might pull from 15 exchanges simultaneously, normalize timestamps, fill gaps in data. model.predict() might run a transformer model on the last 100 candles with 40 features. display() might write to a database, send a Telegram alert, fire an API call to place an order. But the loop structure is identical.
Once you internalize this pattern, you can read any trading bot’s code and immediately identify where data comes in, where decisions happen, and where output goes. Everything else is implementation detail.
What’s Coming in Part 4
Your bot fetches one number - the last price. That’s not enough for a real AI model. Models need features: moving averages, volume profiles, volatility measures, order book depth, recent price patterns. One data point becomes a feature vector - an array of numbers the model can learn from.
In Part 4, we’ll transform the raw price feed into a proper feature pipeline. You’ll learn what features matter for price prediction, how to build them from OHLCV data, and why the quality of your features matters more than which ML algorithm you pick.
Missed the earlier parts? Start here:
- Part 1: How One Trade Works - the mental model behind everything you’re building.*
- Part 2: Setup Your VPS and Run an AI Bot in 15 Minutes - your bot is already running on a $5 server.*
Continue to:
- Part 4: How Your AI Model Reads the Market - build a feature pipeline from OHLCV data.*
The code we analyzed: github.com/hovinng/ai-trading-bot