TradeReady.io
Backtesting

Backtesting Guide

Full API lifecycle — create, start, step, trade, and read results

Download .md

Backtesting lets you replay historical market data and test a trading strategy against it. Instead of waiting 30 real days to see if your strategy works, you can simulate 30 days of trading in minutes. Your trading code works identically in backtest and live mode — same endpoints, same request shapes.


Step 1 — Check Available Data

Before creating a session, check what historical data is available:

GET /api/v1/market/data-range
{
  "earliest": "2025-01-01T00:00:00Z",
  "latest": "2026-02-22T23:59:59Z",
  "total_pairs": 647,
  "intervals_available": ["1m", "5m", "15m", "1h", "4h", "1d"],
  "data_gaps": []
}

You can only backtest within this range.


Step 2 — Create a Session

POST /api/v1/backtest/create
Content-Type: application/json

{
  "start_time": "2025-06-01T00:00:00Z",
  "end_time": "2025-07-01T00:00:00Z",
  "starting_balance": 10000,
  "candle_interval": "1m",
  "strategy_label": "momentum_v1",
  "agent_id": "your-agent-uuid",
  "pairs": ["BTCUSDT", "ETHUSDT"]
}

pairs is optional — omit it to include all 600+ available pairs.

{
  "session_id": "bt_550e8400...",
  "status": "created",
  "total_steps": 43200,
  "estimated_pairs": 2
}

total_steps is (end_time - start_time) / candle_interval. For 30 days at 1-minute candles, that is 43,200 steps.

agent_id is required. Each backtest session is scoped to a specific agent — the agent's risk profile is loaded and enforced in the sandbox.


Step 3 — Start the Session

POST /api/v1/backtest/{session_id}/start

This is where the heavy lifting happens. The engine:

  1. Creates a TimeSimulator — a virtual clock starting at start_time
  2. Creates a BacktestSandbox — an in-memory fake exchange with your starting balance
  3. Preloads all candle data for your date range into a Python dictionary (one SQL query, then no further DB reads during stepping)
  4. Takes an initial equity snapshot
  5. Sets status to "running"

After this call, the session is ready to step through.


Step 4 — The Core Loop

On each step, the virtual clock advances by one candle_interval. Call /step for one candle at a time or /step/batch to advance multiple candles:

POST /api/v1/backtest/{session_id}/step

# or

POST /api/v1/backtest/{session_id}/step/batch
{"steps": 100}

Step response:

{
  "virtual_time": "2025-06-01T01:40:00Z",
  "step": 100,
  "total_steps": 43200,
  "progress_pct": "0.23",
  "prices": {
    "BTCUSDT": "67500.30",
    "ETHUSDT": "3450.20"
  },
  "orders_filled": [],
  "portfolio": {
    "total_equity": "10000.00",
    "available_cash": "9700.00",
    "position_value": "300.00",
    "unrealized_pnl": "12.50",
    "realized_pnl": "0.00"
  },
  "is_complete": false,
  "remaining_steps": 43100
}

When is_complete is true, the session has automatically computed final metrics and saved everything to the database.

What happens on each step internally:

  1. Clock advances by candle_interval
  2. Prices loaded from the in-memory dictionary (no DB query)
  3. Pending limit/stop orders checked against new prices
  4. Equity snapshot taken (every 60 steps)
  5. DB progress updated (every 500 steps)

Step 5 — Place Orders

Between steps, read the prices from the step response and decide to trade:

POST /api/v1/backtest/{session_id}/trade/order
Content-Type: application/json

{
  "symbol": "BTCUSDT",
  "side": "buy",
  "type": "market",
  "quantity": "0.1"
}

Supported order types:

TypeFills when
marketImmediately at current price + slippage
limitWhen price reaches your target (buy: price ≤ limit, sell: price ≥ limit)
stop_lossWhen price drops to or below your trigger price
take_profitWhen price rises to or above your trigger price

Limit and stop orders:

# Limit buy at $60,000
{"symbol": "BTCUSDT", "side": "buy", "type": "limit", "quantity": "0.1", "price": "60000"}

# Stop loss at $58,000
{"symbol": "BTCUSDT", "side": "sell", "type": "stop_loss", "quantity": "0.1", "trigger_price": "58000"}

# Take profit at $72,000
{"symbol": "BTCUSDT", "side": "sell", "type": "take_profit", "quantity": "0.1", "trigger_price": "72000"}

Pending orders are automatically checked on every step, including during batch steps. Your stop-loss will always trigger correctly even if you skip 1,000 candles with /step/batch.

The Sandbox Mechanics

On a market buy, the sandbox:

  1. Calculates slippage (0.01% to 10% based on order size)
  2. Computes executed price = close price × (1 + slippage)
  3. Deducts 0.1% trading fee
  4. Deducts total cost from your USDT balance
  5. Adds the position to your portfolio

Step 6 — Query State

At any point during a running backtest, you can query the sandbox state:

# Market data at virtual time
GET /api/v1/backtest/{sid}/market/prices
GET /api/v1/backtest/{sid}/market/price/BTCUSDT
GET /api/v1/backtest/{sid}/market/ticker/BTCUSDT
GET /api/v1/backtest/{sid}/market/candles/BTCUSDT

# Account state in the sandbox
GET /api/v1/backtest/{sid}/account/balance
GET /api/v1/backtest/{sid}/account/positions
GET /api/v1/backtest/{sid}/account/portfolio

# Orders
GET /api/v1/backtest/{sid}/trade/orders
GET /api/v1/backtest/{sid}/trade/orders/open
DELETE /api/v1/backtest/{sid}/trade/order/{order_id}

All market data endpoints return values as of the current virtual_time — they never reveal future prices.


Step 7 — Results

When is_complete: true, results are automatically saved. Fetch them:

GET /api/v1/backtest/{session_id}/results
{
  "session_id": "bt_550e...",
  "status": "completed",
  "config": {
    "start_time": "2025-06-01T00:00:00Z",
    "end_time": "2025-07-01T00:00:00Z",
    "starting_balance": "10000.00",
    "strategy_label": "momentum_v1",
    "candle_interval": "1m"
  },
  "summary": {
    "final_equity": "12458.30",
    "total_pnl": "2458.30",
    "roi_pct": "24.58",
    "total_trades": 156,
    "total_fees": "234.50"
  },
  "metrics": {
    "sharpe_ratio": 1.85,
    "sortino_ratio": 2.31,
    "max_drawdown_pct": 8.5,
    "max_drawdown_duration_days": 3,
    "win_rate": 65.71,
    "profit_factor": 2.1,
    "avg_win": "156.30",
    "avg_loss": "-74.50",
    "best_trade": "523.00",
    "worst_trade": "-210.00",
    "trades_per_day": 5.03
  },
  "by_pair": [
    {"symbol": "BTCUSDT", "trades": 45, "win_rate": 71.1, "net_pnl": "1200.00"},
    {"symbol": "ETHUSDT", "trades": 32, "win_rate": 62.5, "net_pnl": "580.00"}
  ]
}

Additional Result Endpoints

GET /api/v1/backtest/{sid}/results/equity-curve    # time-series equity snapshots
GET /api/v1/backtest/{sid}/results/trades          # full trade log

Cancelling Early

POST /api/v1/backtest/{session_id}/cancel

Saves partial results and sets status to cancelled. Useful when it is clear early that a strategy is performing poorly.


Listing and Comparing

# List all your backtests
GET /api/v1/backtest/list?strategy_label=momentum&status=completed&sort_by=sharpe_ratio

# Compare sessions side by side
GET /api/v1/backtest/compare?sessions=bt_aaa,bt_bbb,bt_ccc

# Find your best session by metric
GET /api/v1/backtest/best?metric=sharpe_ratio

The compare endpoint returns side-by-side metrics and identifies the winner:

{
  "comparisons": [
    {"session_id": "bt_aaa", "strategy_label": "momentum_v1", "roi_pct": 18.2, "sharpe_ratio": 1.42},
    {"session_id": "bt_bbb", "strategy_label": "momentum_v2", "roi_pct": 24.58, "sharpe_ratio": 1.85}
  ],
  "best_by_roi": "bt_bbb",
  "best_by_sharpe": "bt_bbb",
  "recommendation": "bt_bbb (momentum_v2) outperforms on all key metrics"
}

How Price Data Works

Two data sources feed the backtest engine:

SourceCoverageGranularity
candles_1mRecent (since ingestion started)1-minute
candles_backfillYears of history1-hour and 1-day

When you start a session, the engine runs one SQL query to load all candle data for your date range. During stepping, prices are looked up via in-memory binary search — zero database queries.

If your date range only has hourly data, the price stays constant for each 1-minute step within the hour. The simulation still runs correctly — just with less price granularity.


Risk Profile Enforcement

If your agent has a risk profile configured, the sandbox enforces these limits on every order:

LimitWhat it enforces
max_order_size_pctOrder cost cannot exceed this % of available cash
max_position_size_pctResulting position cannot exceed this % of total equity
daily_loss_limit_pctNew orders are rejected if realized daily loss exceeds this %

Orders that violate any limit are rejected with a descriptive error. Backtest results reflect realistic risk constraints.


Step Batching for Speed

Match your batch size to your strategy timeframe:

Strategy timeframeBatch sizeAPI calls for 1 year
Every candle (scalping)1525,600
Hourly signals608,760
Daily signals1440365
Weekly rebalancing1008052

Pending limit/stop orders still check against every candle during a batch — stop-losses always trigger correctly even when fast-forwarding.


Typical Agent Loop

import httpx

BASE = "http://localhost:8000/api/v1"
HEADERS = {"X-API-Key": "ak_live_..."}

# 1. Create
r = httpx.post(f"{BASE}/backtest/create", headers=HEADERS, json={
    "start_time": "2025-06-01T00:00:00Z",
    "end_time": "2025-07-01T00:00:00Z",
    "starting_balance": 10000,
    "candle_interval": "1m",
    "strategy_label": "my_strategy_v1",
    "agent_id": "agent-uuid-here",
    "pairs": ["BTCUSDT", "ETHUSDT"]
})
session_id = r.json()["session_id"]

# 2. Start
httpx.post(f"{BASE}/backtest/{session_id}/start", headers=HEADERS)

# 3. Loop
prices_history = {"BTCUSDT": [], "ETHUSDT": []}

while True:
    result = httpx.post(
        f"{BASE}/backtest/{session_id}/step/batch",
        headers=HEADERS,
        json={"steps": 60}
    ).json()

    for sym, price in result["prices"].items():
        prices_history[sym].append(float(price))

    portfolio = result["portfolio"]

    # --- YOUR STRATEGY LOGIC HERE ---

    if result["is_complete"]:
        break

# 4. Results
results = httpx.get(f"{BASE}/backtest/{session_id}/results", headers=HEADERS).json()
print(f"ROI: {results['summary']['roi_pct']}%")
print(f"Sharpe: {results['metrics']['sharpe_ratio']}")

Performance Notes

  • 1 year at 1-minute candles = 525,600 steps
  • Price data is preloaded into memory at start — stepping is CPU-only, no DB queries
  • Equity snapshots are taken every 60 steps (hourly) to save memory
  • DB progress is updated every 500 steps to reduce write overhead
  • A year-long backtest completes in minutes, not hours

Next Steps

On this page