AlgoPro UniversityCourse home
Part 8 Β· Lesson

The architecture of a live bot

Live trading is a different sport from backtesting: real-time data, persistent state, partial fills, and a broker that is the only source of truth. Here is the shape of a bot that survives it.

In Part 1 you learned that every trading system is the same five boxes β€” data feed β†’ signal β†’ risk/sizing β†’ execution β†’ portfolio state. A backtest is that machine running over a frozen file of history, in a single thread, with perfect knowledge of every fill. A live bot is the same five boxes, but now the world is moving underneath it: prices tick in real time, orders take time to fill (or half-fill, or fail), the network drops, the process crashes at 3am, and your idea of "what I hold" can silently drift from reality.

This part builds one coherent bot across five lessons β€” config, exchange client, order manager, live loop, deployment. By the end you will have a real, runnable skeleton. But before a line of code, you have to internalise how live differs from backtest, because almost every live-trading disaster is a backtest assumption that quietly stopped being true.

Five ways live betrays your backtest

  1. Data arrives in real time, out of order, and with gaps. A backtest hands you a clean, complete candle. Live, the current candle is *still forming*, feeds hiccup, and a websocket can silently go stale. You must act only on closed bars and treat every read as possibly missing.
  2. State must persist across restarts. A backtest holds everything in memory and never dies. A live bot *will* be killed β€” deploys, crashes, reboots. When it comes back up it must remember what it was doing, and re-derive the rest from the exchange.
  3. Orders are asynchronous and partial. You do not "buy at the close". You *submit* an order; later it fills β€” fully, partially, or not at all. Between submit and fill, price moves and your bot must not panic-send a second order.
  4. Everything fails. The API times out, returns a rate-limit error, or goes down for maintenance. In a backtest a bug throws once and stops. Live, an unhandled exception at the wrong moment can leave you with an open position and no bot watching it.
  5. The broker is the single source of truth. Your local variable saying position = 0 is a *hopeful cache*. The exchange knows what you actually hold. When they disagree β€” and they will β€” the exchange wins, every time.

The live event loop

A live bot is not a straight line from data to order β€” it is a loop with a heartbeat. On each tick it re-establishes reality, decides, acts, and records, wrapped in error handling so a single failure delays the bot rather than killing it. This is the machine we are going to build:

flat/longsizefillloopfailretryWait for barSync from exchangeFetch OHLCVSignalRisk / sizeExecute orderPersist stateError β†’ backoff
The live event loop. Every cycle starts by syncing reality from the exchange (the source of truth) and ends by persisting state. Errors route to backoff, not to death.

Notice what comes *first*: not the signal, but sync from the exchange. Before deciding anything, the bot asks "what do I actually hold, and what happened to my open orders while I was asleep?" Only then does it fetch data and think. This ordering is the whole difference between a toy and a bot you can leave running.

The project skeleton

A live bot is small but it should not be one giant file. We separate the pieces that change for different reasons: configuration, the exchange connection, order/position bookkeeping, the strategy, and the loop that ties them together. Here is the layout we will fill in over the next four lessons:

bashProject layout β€” one module per responsibility
livebot/
β”œβ”€β”€ config.py          # settings loaded from environment variables
β”œβ”€β”€ exchange.py        # thin wrapper around the ccxt client (Lesson 2)
β”œβ”€β”€ order_manager.py   # orders, positions, reconciliation (Lesson 3)
β”œβ”€β”€ strategy.py        # the signal β€” pure, testable, no I/O
β”œβ”€β”€ bot.py             # the live loop wiring it all together (Lesson 4)
β”œβ”€β”€ state.json         # persisted state (created at runtime)
β”œβ”€β”€ requirements.txt   # ccxt, python-dotenv, ...
└── .env               # SECRETS β€” never committed (in .gitignore)

The rule behind this split: the strategy knows nothing about the exchange, and the exchange wrapper knows nothing about the strategy. The loop in bot.py is the only place they meet. That separation is what let you backtest strategy.py in Part 5 and now run the identical logic live β€” the signal code does not change, only what feeds it and what acts on it.

Configuration first

Every knob that might differ between paper and live, or between your laptop and a VPS, lives in one place and is read from the environment, never hard-coded. This is also where our first safety switch lives β€” a DRY_RUN flag and a LIVE gate, so the default state of the bot is "do not send real orders".

pythonconfig.py β€” one source of settings, read from the environment
import os
from dataclasses import dataclass
from dotenv import load_dotenv

load_dotenv()  class="c"># read a local .env file into os.environ (dev convenience)


def _flag(name: str, default: str = class="s">"false") -> bool:
    return os.getenv(name, default).strip().lower() in (class="s">"class="n">1", class="s">"true", class="s">"yes", class="s">"on")


@dataclass(frozen=True)
class Config:
    exchange_id: str = os.getenv(class="s">"EXCHANGE_ID", class="s">"binance")
    symbol: str = os.getenv(class="s">"SYMBOL", class="s">"BTC/USDT")
    timeframe: str = os.getenv(class="s">"TIMEFRAME", class="s">"1h")

    class="c"># Credentials come ONLY from the environment β€” never written in code.
    api_key: str = os.getenv(class="s">"API_KEY", class="s">"")
    api_secret: str = os.getenv(class="s">"API_SECRET", class="s">"")

    class="c"># Safety switches. Defaults are deliberately the SAFE choice.
    testnet: bool = _flag(class="s">"TESTNET", class="s">"true")    class="c"># paper/sandbox by default
    dry_run: bool = _flag(class="s">"DRY_RUN", class="s">"true")    class="c"># log orders, don't send them
    live: bool = _flag(class="s">"LIVE", class="s">"false")         class="c"># master arm switch

    class="c"># Risk knobs
    risk_fraction: float = float(os.getenv(class="s">"RISK_FRACTION", class="s">"class="n">0.02"))
    max_position: float = float(os.getenv(class="s">"MAX_POSITION", class="s">"class="n">0.05"))

    def sending_real_orders(self) -> bool:
        class="s">""class="s">"Only true when explicitly armed AND not in dry-run."class="s">""
        return self.live and not self.dry_run


CONFIG = Config()

if __name__ == class="s">"__main__":
    print(class="s">"Exchange:", CONFIG.exchange_id, class="s">"| symbol:", CONFIG.symbol)
    print(class="s">"Testnet:", CONFIG.testnet, class="s">"| dry_run:", CONFIG.dry_run,
          class="s">"| live:", CONFIG.live)
    print(class="s">"Sending real orders?", CONFIG.sending_real_orders())

That is the architecture: a loop that treats the exchange as truth, a clean module split, and configuration whose defaults refuse to risk money by accident. In the next lesson we make the exchange wrapper real β€” connecting to a live API with ccxt, loading keys safely, and placing our first orders on a testnet.