StructureX

Home › Guides › Trading bots & code

Paper trading a bot: test it live, with no real money at stake

A backtest tells you how a rule behaved on the past. Paper trading shows how your bot behaves right now — real prices, real delays, fake money. It's the step that catches the bugs a backtest can't.

Beginner guide · 11-minute read · Updated 2026-10-21

What paper trading a bot means

Paper trading means trading with pretend money. For a bot, it means the bot runs exactly as it would live — fetching prices, deciding, sending orders — but the orders land in a practice account.

It's sometimes called forward testing: testing on prices that hadn't happened yet when you wrote the rule. That alone makes it more honest than any backtest.

Example code for learning — test it on paper first; it is not a ready-made trading system.

What paper trading catches that backtests miss

A backtest lives in a perfect world. Paper trading shows you the real one:

3 ways to paper trade a bot

WayWhat it isGood for
A simulator in your codeYour bot "fills" its own orders at live prices, with costs addedLearning, any market, no sign-up
A broker paper accountA practice account at a real broker, with its own API keysTesting the real order code end to end
An exchange testnetA practice copy of a crypto exchange, with test coinsCrypto bots

Some examples, as of October 2026 — check each provider's docs, as details change:

Whichever you use: paper keys only. Live keys should not exist yet. See API key security for trading bots.

Build a tiny paper broker

The simplest start is a simulator. This one fills orders at the price you give it, then makes it a little worse (slippage) and adds a fee. Every fill goes into a CSV log.

import csv
class PaperBroker:
    """Fills orders at a given price plus slippage and fees. Pretend money only."""
    def __init__(self, cash, fee_per_unit=0.01, slippage=0.02, log_path="paper_log.csv"):
        self.cash, self.fee, self.slip = cash, fee_per_unit, slippage
        self.units, self.log_path = 0, log_path
        with open(log_path, "w", newline="") as f:
            csv.writer(f).writerow(["time", "side", "qty", "wanted", "filled", "fee"])
    def market_order(self, time, side, qty, price):
        filled = price + self.slip if side == "buy" else price - self.slip
        fee = self.fee * qty
        if side == "buy":
            self.cash -= filled * qty + fee
            self.units += qty
        else:
            self.cash += filled * qty - fee
            self.units -= qty
        with open(self.log_path, "a", newline="") as f:
            csv.writer(f).writerow([time, side, qty, round(price, 4), round(filled, 4), fee])
        return filled
    def value(self, price):
        return self.cash + self.units * price

In plain words:

Feed it "live" prices

To test without any account, we feed made-up prices one candle at a time, as if they were arriving live.

import numpy as np
import pandas as pd
# Made-up prices standing in for a live feed. Not a real market.
rng = np.random.default_rng(seed=3)
prices = pd.Series(100 + rng.normal(0, 0.5, 500).cumsum(),
                   index=pd.date_range("2026-10-01", periods=500, freq="h"))
broker = PaperBroker(cash=10_000)
fast, slow = prices.rolling(10).mean(), prices.rolling(30).mean()
for t in range(31, len(prices)):
    up_now, up_before = fast.iloc[t - 1] > slow.iloc[t - 1], fast.iloc[t - 2] > slow.iloc[t - 2]
    price = prices.iloc[t]                   # the next price after the signal candle closed
    if up_now and not up_before and broker.units == 0:
        broker.market_order(prices.index[t], "buy", 50, price)
    elif not up_now and up_before and broker.units > 0:
        broker.market_order(prices.index[t], "sell", broker.units, price)
print("Account value:", round(broker.value(prices.iloc[-1]), 2))

The rule is a simple average crossover, just to make orders happen. The signal uses candles that have already closed (t − 1 and t − 2), and the order fills at the next price. That's how it works live: you see the close, then you act.

The account value at the end is meaningless — the prices are random, so any gain or loss is luck. Results on synthetic data mean nothing about real markets. What we want is the log.

Read the log

log = pd.read_csv("paper_log.csv")
log["slip_cost"] = (log["filled"] - log["wanted"]).abs() * log["qty"]
print(log.head(4))
print("Fills:", len(log), " Slippage paid:", round(log["slip_cost"].sum(), 2),
      " Fees paid:", round(log["fee"].sum(), 2))

This is the most useful table in bot testing. It shows, fill by fill, the gap between what the backtest assumed and what the bot got. On a broker paper account, the "filled" column comes from the broker instead of your guess.

Keep this log next to your normal trading journal. Each order is a row.

The 6 things to check before going live

Run the bot on paper until it has made a good number of trades — at least several weeks, and ideally 50 to 100 trades. Then check:

  1. Every order is explained. For each one in the log, you can point to the candle and the rule that caused it. One mystery order = not ready.
  2. Behaviour matches the backtest. Same kinds of trades, at the same kinds of places. Results will differ; behaviour shouldn't.
  3. Costs are counted. Slippage plus fees, in units of risk (R — the amount lost if a stop is hit). If costs eat most of the average trade, the rule may not survive live.
  4. The guards fired. The daily loss limit, the trade cap and the kill switch each worked at least once — even if you had to trigger them on purpose. See trading bot risk controls.
  5. It survived a restart. Stop it mid-trade, start it again. Does it know it has a position? Does it place a duplicate order?
  6. It handled an error. Pull the network cable or block the connection for a minute. It should wait, retry safely, or stop and tell you.

Pass all 6, then go live small — the smallest size your broker allows. Paper and live still feel different, and fills can differ too. See paper trading vs live trading.

Don't edit during the test. Every change to the rule restarts the clock. Write ideas down, finish the test, then change one thing.

Common mistakes

Questions

How long should I paper trade a bot?

Long enough for a good number of trades — often several weeks and ideally 50 to 100 trades — and until every order in the log is explained.

Is paper trading the same as backtesting?

No. A backtest replays past prices. Paper trading runs the bot in real time on new prices, so it shows delays, fills and errors a backtest can't.

Does Alpaca have paper trading for bots?

Yes. Alpaca offers a paper trading account with separate API keys and a separate paper address, so bot code can be tested without real money.

Why are my paper trading results different from my backtest?

Usually fills, costs and timing. Live prices move between the signal and the order. Check the log for slippage, late signals and skipped trades.

Want the next step? We'll walk you through it.

Four free lessons by email, one every few days — each with a 10-minute exercise on a real chart.

Send Me the Free Lessons 4 short emails · No spam · Unsubscribe anytime

Or see the whole method, step by step:

See What’s Inside the Course Try everything free first. Decide when you're ready. · Open the free chart tool (works in the TradingView phone app)

One chart idea a day, in plain English. Follow along:

Follow on XFollow on TradingView