<!-- Generated from TradeReady.io docs. Visit https://tradeready.io/docs for the full experience. -->

---
title: Backtesting
description: Replay historical market data and trade against it using the full sandbox trading API — identical to live mode.
---

The backtesting engine lets you replay historical Binance market data and execute trades against it at your own pace. Your trading code works identically in backtest and live mode — the sandbox accepts the same order types, enforces the same risk rules, and returns the same response shapes.

> **Info:**
> The critical invariant: the `DataReplayer` filters `WHERE bucket <= virtual_clock` — no look-ahead bias is possible. You can only see prices and candles that existed at or before the current virtual time.

---

## How It Works

1. **Create** a session with a date range, starting balance, candle interval, and list of pairs
2. **Start** — the engine bulk-preloads all candle data into memory (zero per-step DB queries)
3. **Step** — advance the virtual clock one candle (or batch N candles at once)
4. Each step response includes: current prices for all pairs, orders that filled, current portfolio state
5. **Trade** using the sandbox endpoints (same API as live trading)
6. **Auto-complete** when the last step is reached, or **cancel** early
7. **Results** — full metrics: Sharpe, Sortino, max drawdown, win rate, profit factor, per-pair breakdown

---

## Endpoint Summary

### Lifecycle

| Method | Path | Description |
|--------|------|-------------|
| `POST` | `/backtest/create` | Create a new backtest session |
| `POST` | `/backtest/{id}/start` | Preload data, initialize sandbox |
| `POST` | `/backtest/{id}/step` | Advance one candle |
| `POST` | `/backtest/{id}/step/batch` | Advance N candles |
| `POST` | `/backtest/{id}/cancel` | Abort early, save partial results |
| `GET` | `/backtest/{id}/status` | Progress and current state |

### Sandbox Trading

| Method | Path | Description |
|--------|------|-------------|
| `POST` | `/backtest/{id}/trade/order` | Place order in sandbox |
| `GET` | `/backtest/{id}/trade/orders` | List all sandbox orders |
| `GET` | `/backtest/{id}/trade/orders/open` | List pending sandbox orders |
| `DELETE` | `/backtest/{id}/trade/order/{order_id}` | Cancel sandbox order |
| `GET` | `/backtest/{id}/trade/history` | Sandbox trade execution log |

### Sandbox Market Data

| Method | Path | Description |
|--------|------|-------------|
| `GET` | `/backtest/{id}/market/price/{symbol}` | Price at virtual time |
| `GET` | `/backtest/{id}/market/prices` | All prices at virtual time |
| `GET` | `/backtest/{id}/market/ticker/{symbol}` | 24h stats at virtual time |
| `GET` | `/backtest/{id}/market/candles/{symbol}` | Candles before virtual time |

### Sandbox Account

| Method | Path | Description |
|--------|------|-------------|
| `GET` | `/backtest/{id}/account/balance` | Sandbox balances |
| `GET` | `/backtest/{id}/account/positions` | Sandbox open positions |
| `GET` | `/backtest/{id}/account/portfolio` | Sandbox portfolio summary |

### Results

| Method | Path | Description |
|--------|------|-------------|
| `GET` | `/backtest/{id}/results` | Full results and metrics |
| `GET` | `/backtest/{id}/results/equity-curve` | Time-series equity data |
| `GET` | `/backtest/{id}/results/trades` | Complete trade log |

### Analysis

| Method | Path | Description |
|--------|------|-------------|
| `GET` | `/backtest/list` | List all backtests with filters |
| `GET` | `/backtest/compare` | Compare sessions side-by-side |
| `GET` | `/backtest/best` | Find best session by metric |

### Mode Management

| Method | Path | Description |
|--------|------|-------------|
| `GET` | `/account/mode` | Current operating mode |
| `POST` | `/account/mode` | Switch mode |

### Data Range

| Method | Path | Description |
|--------|------|-------------|
| `GET` | `/market/data-range` | Available historical data range |

---

## GET /market/data-range

Check what historical data is available before creating a backtest.

**Auth required:** No

**curl:**

```bash
curl https://api.tradeready.io/api/v1/market/data-range
```
**Python SDK:**

```python
from agentexchange import AgentExchangeClient

with AgentExchangeClient(api_key="ak_live_...") as client:
    data_range = client.get_data_range()
    print(f"Data available from {data_range.earliest} to {data_range.latest}")
    print(f"Pairs: {data_range.total_pairs}")
```

**Response example:**

```json
{
  "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": []
}
```

---

## POST /backtest/create

Create a new backtest session. The session is created but not started — call `/start` next.

**Auth required:** Yes

**Request body:**

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `start_time` | ISO-8601 datetime | Yes | Historical period start |
| `end_time` | ISO-8601 datetime | Yes | Historical period end |
| `starting_balance` | number | Yes | USDT starting balance |
| `candle_interval` | string | Yes | `"1m"`, `"5m"`, `"15m"`, `"1h"`, `"4h"`, `"1d"` |
| `strategy_label` | string | No | Tag for filtering and comparing backtests |
| `agent_id` | UUID string | Yes | Agent to run the backtest for (risk profile is loaded from this agent) |
| `pairs` | string array \| null | No | Specific pairs to include; `null` = all 600+ pairs |

**Response: HTTP 200**

| Field | Type | Description |
|-------|------|-------------|
| `session_id` | string | Session identifier (prefixed with `bt_`) |
| `status` | string | `"created"` |
| `total_steps` | integer | Total candle steps in the date range |
| `estimated_pairs` | integer | Number of trading pairs included |

**curl:**

```bash
curl -X POST https://api.tradeready.io/api/v1/backtest/create \
  -H "Content-Type: application/json" \
  -H "X-API-Key: ak_live_..." \
  -d '{
    "start_time": "2025-01-01T00:00:00Z",
    "end_time": "2025-01-31T23:59:59Z",
    "starting_balance": 10000,
    "candle_interval": "1h",
    "strategy_label": "rsi_scalper_v2",
    "agent_id": "your-agent-uuid",
    "pairs": ["BTCUSDT", "ETHUSDT", "SOLUSDT"]
  }'
```
**Python SDK:**

```python
session = client.create_backtest(
    start_time="2025-01-01T00:00:00Z",
    end_time="2025-01-31T23:59:59Z",
    starting_balance=10000,
    candle_interval="1h",
    strategy_label="rsi_scalper_v2",
    agent_id="your-agent-uuid",
    pairs=["BTCUSDT", "ETHUSDT", "SOLUSDT"],
)
session_id = session["session_id"]
print(f"Session: {session_id}  Steps: {session['total_steps']}")
```

**Response example:**

```json
{
  "session_id": "bt_550e8400-e29b-41d4-a716-446655440000",
  "status": "created",
  "total_steps": 744,
  "estimated_pairs": 3
}
```

---

## POST /backtest/{id}/start

Initialize the sandbox and preload all candle data into memory. The virtual clock is set to `start_time`. Call this once before beginning the step loop.

**Auth required:** Yes

**curl:**

```bash
curl -X POST https://api.tradeready.io/api/v1/backtest/bt_550e8400-.../start \
  -H "X-API-Key: ak_live_..."
```
**Python SDK:**

```python
client.start_backtest(session_id=session_id)
```

---

## POST /backtest/{id}/step

Advance the virtual clock by one candle interval. Returns current prices, filled orders, and portfolio state.

**Auth required:** Yes

**Response: HTTP 200** — the core step response that drives your strategy loop

| Field | Type | Description |
|-------|------|-------------|
| `virtual_time` | ISO-8601 datetime | Current virtual time after this step |
| `step` | integer | Current step number (1-based) |
| `total_steps` | integer | Total steps in the session |
| `progress_pct` | float | Completion percentage |
| `prices` | object | Map of `symbol → OHLCV object` at this candle |
| `orders_filled` | array | Orders that filled during this step |
| `portfolio` | object | Current portfolio state |
| `is_complete` | boolean | `true` on the last step |
| `remaining_steps` | integer | Steps remaining |

**Prices object per symbol:**

```json
{
  "open": "42150.00",
  "high": "42180.00",
  "low": "42130.00",
  "close": "42165.30",
  "volume": "12.34"
}
```

**curl:**

```bash
curl -X POST https://api.tradeready.io/api/v1/backtest/bt_550e8400-.../step \
  -H "X-API-Key: ak_live_..."
```
**Python SDK:**

```python
step = client.step_backtest(session_id=session_id)
print(f"Step {step['step']}/{step['total_steps']}  ({step['progress_pct']:.1f}%)")
print(f"BTC close: {step['prices']['BTCUSDT']['close']}")
print(f"Portfolio equity: {step['portfolio']['total_equity']}")

for fill in step['orders_filled']:
    print(f"Order filled: {fill['side']} {fill['quantity']} {fill['symbol']} @ {fill['executed_price']}")
```

**Full step response example:**

```json
{
  "virtual_time": "2025-01-01T01:00:00Z",
  "step": 1,
  "total_steps": 744,
  "progress_pct": 0.13,
  "prices": {
    "BTCUSDT": {
      "open": "42150.00",
      "high": "42220.00",
      "low": "42090.00",
      "close": "42180.00",
      "volume": "234.56"
    },
    "ETHUSDT": {
      "open": "2280.00",
      "high": "2295.00",
      "low": "2270.00",
      "close": "2288.50",
      "volume": "1823.4"
    }
  },
  "orders_filled": [],
  "portfolio": {
    "total_equity": "10000.00",
    "available_cash": "10000.00",
    "positions": [],
    "unrealized_pnl": "0.00"
  },
  "is_complete": false,
  "remaining_steps": 743
}
```

---

## POST /backtest/{id}/step/batch

Advance multiple candles in one call. Limit and stop-loss orders are still checked against every candle in the batch — they will fill correctly even when batching.

**Auth required:** Yes

**Request body:**

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `steps` | integer | Yes | Number of candles to advance |

The response has the same shape as a single step. `orders_filled` includes all fills from the entire batch. `prices` shows the final candle in the batch.

**curl:**

```bash
# Skip 24 hourly candles (advance one day)
curl -X POST https://api.tradeready.io/api/v1/backtest/bt_550e8400-.../step/batch \
  -H "X-API-Key: ak_live_..." \
  -H "Content-Type: application/json" \
  -d '{"steps": 24}'
```
**Python SDK:**

```python
# Check every hour (24 candles per day if using 1h interval)
step = client.step_backtest_batch(session_id=session_id, steps=24)
print(f"Advanced to {step['virtual_time']}")
```

---

## Sandbox Trading Endpoints

These endpoints are identical to the live trading API but scoped to the backtest session. Substitute `{id}` with your `session_id`.

### POST /backtest/{id}/trade/order

Place a market, limit, stop-loss, or take-profit order in the sandbox.

**curl:**

```bash
# Market buy in sandbox
curl -X POST https://api.tradeready.io/api/v1/backtest/bt_550e8400-.../trade/order \
  -H "X-API-Key: ak_live_..." \
  -H "Content-Type: application/json" \
  -d '{"symbol": "BTCUSDT", "side": "buy", "type": "market", "quantity": "0.1"}'

# Limit buy
curl -X POST https://api.tradeready.io/api/v1/backtest/bt_550e8400-.../trade/order \
  -H "X-API-Key: ak_live_..." \
  -H "Content-Type: application/json" \
  -d '{"symbol": "BTCUSDT", "side": "buy", "type": "limit", "quantity": "0.1", "price": "41000.00"}'
```
**Python SDK:**

```python
# Market buy
order = client.backtest_trade_order(
    session_id=session_id,
    symbol="BTCUSDT",
    side="buy",
    order_type="market",
    quantity="0.1",
)

# Limit with stop-loss
client.backtest_trade_order(session_id, "BTCUSDT", "buy", "limit", "0.1", price="41000.00")
client.backtest_trade_order(session_id, "BTCUSDT", "sell", "stop_loss", "0.1", trigger_price="40000.00")
```

---

## GET /backtest/{id}/results

Get the full results after a session is completed or cancelled.

**Auth required:** Yes

**curl:**

```bash
curl https://api.tradeready.io/api/v1/backtest/bt_550e8400-.../results \
  -H "X-API-Key: ak_live_..."
```
**Python SDK:**

```python
results = client.get_backtest_results(session_id=session_id)
print(f"Final equity: ${results['summary']['final_equity']}")
print(f"ROI: {results['summary']['roi_pct']}%")
print(f"Sharpe: {results['metrics']['sharpe_ratio']}")
print(f"Max drawdown: {results['metrics']['max_drawdown_pct']}%")
print(f"Win rate: {results['metrics']['win_rate']}%")
```

**Response example:**

```json
{
  "session_id": "bt_550e8400-...",
  "status": "completed",
  "config": {
    "start_time": "2025-01-01T00:00:00Z",
    "end_time": "2025-01-31T23:59:59Z",
    "starting_balance": "10000.00",
    "strategy_label": "rsi_scalper_v2",
    "candle_interval": "1h"
  },
  "summary": {
    "final_equity": "12458.30",
    "total_pnl": "2458.30",
    "roi_pct": "24.58",
    "total_trades": 156,
    "total_fees": "234.50",
    "duration_simulated_days": 31,
    "duration_real_seconds": 750
  },
  "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",
    "avg_trade_duration_minutes": 340,
    "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"},
    {"symbol": "SOLUSDT", "trades": 79, "win_rate": 63.3, "net_pnl": "678.30"}
  ]
}
```

---

## GET /backtest/list

List backtests with optional filters. Returns summary data for each session.

**Auth required:** Yes

**Query parameters:**

| Parameter | Type | Description |
|-----------|------|-------------|
| `strategy_label` | string | Filter by strategy label |
| `status` | string | `"running"`, `"completed"`, `"failed"`, `"cancelled"` |
| `sort_by` | string | `"roi_pct"`, `"sharpe_ratio"`, `"created_at"` |
| `limit` | integer | Page size (default 20) |

**curl:**

```bash
curl "https://api.tradeready.io/api/v1/backtest/list?strategy_label=rsi_scalper&sort_by=sharpe_ratio&status=completed&limit=10" \
  -H "X-API-Key: ak_live_..."
```
**Python SDK:**

```python
backtests = client.list_backtests(
    strategy_label="rsi_scalper",
    sort_by="sharpe_ratio",
    status="completed",
    limit=10,
)
for bt in backtests["backtests"]:
    print(f"{bt['session_id']}: ROI={bt['roi_pct']}%  Sharpe={bt['sharpe_ratio']}")
```

---

## GET /backtest/compare

Compare multiple backtest sessions side-by-side and get a recommendation.

**Auth required:** Yes

**Query parameters:**

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `sessions` | string | Yes | Comma-separated session IDs |

**curl:**

```bash
curl "https://api.tradeready.io/api/v1/backtest/compare?sessions=bt_aaa,bt_bbb,bt_ccc" \
  -H "X-API-Key: ak_live_..."
```
**Python SDK:**

```python
comparison = client.compare_backtests(
    sessions=["bt_aaa", "bt_bbb", "bt_ccc"],
)
print(f"Best by ROI: {comparison['best_by_roi']}")
print(f"Best by Sharpe: {comparison['best_by_sharpe']}")
print(comparison["recommendation"])
```

**Response example:**

```json
{
  "comparisons": [
    {
      "session_id": "bt_aaa...",
      "strategy_label": "rsi_scalper_v1",
      "roi_pct": 18.20,
      "sharpe_ratio": 1.42,
      "max_drawdown_pct": 12.3,
      "win_rate": 58.33
    },
    {
      "session_id": "bt_bbb...",
      "strategy_label": "rsi_scalper_v2",
      "roi_pct": 24.58,
      "sharpe_ratio": 1.85,
      "max_drawdown_pct": 8.5,
      "win_rate": 65.71
    }
  ],
  "best_by_roi": "bt_bbb...",
  "best_by_sharpe": "bt_bbb...",
  "best_by_drawdown": "bt_bbb...",
  "recommendation": "bt_bbb (rsi_scalper_v2) outperforms on all key metrics"
}
```

---

## GET /backtest/best

Find your best backtest by a single metric.

**Auth required:** Yes

**Query parameters:**

| Parameter | Type | Description |
|-----------|------|-------------|
| `metric` | string | `"roi_pct"`, `"sharpe_ratio"`, `"win_rate"`, `"profit_factor"` |
| `strategy_label` | string | Optional label filter |

**curl:**

```bash
curl "https://api.tradeready.io/api/v1/backtest/best?metric=sharpe_ratio&strategy_label=rsi_scalper" \
  -H "X-API-Key: ak_live_..."
```
**Python SDK:**

```python
best = client.get_best_backtest(metric="sharpe_ratio", strategy_label="rsi_scalper")
print(f"Best: {best['session_id']}  Sharpe={best['sharpe_ratio']}")
```

---

## Mode Management

### GET /account/mode

Check your current operating mode and active backtest count.

**curl:**

```bash
curl https://api.tradeready.io/api/v1/account/mode \
  -H "X-API-Key: ak_live_..."
```
**Python SDK:**

```python
mode = client.get_mode()
print(f"Mode: {mode['mode']}  Active backtests: {mode['active_backtests']}")
```

**Response example:**

```json
{
  "mode": "live",
  "live_session": {
    "started_at": "2026-02-20T00:00:00Z",
    "current_equity": "12458.30",
    "strategy_label": "rsi_scalper_v2"
  },
  "active_backtests": 1,
  "total_backtests_completed": 14
}
```

### POST /account/mode

Switch your primary operating mode. This does not stop active backtests — you can run backtests while trading live.

**curl:**

```bash
curl -X POST https://api.tradeready.io/api/v1/account/mode \
  -H "X-API-Key: ak_live_..." \
  -H "Content-Type: application/json" \
  -d '{"mode": "live", "strategy_label": "rsi_scalper_v2"}'
```
**Python SDK:**

```python
client.set_mode(mode="live", strategy_label="rsi_scalper_v2")
```

---

## Complete Backtest Loop Example

**curl (pseudocode):**

```bash
API_KEY="ak_live_..."
AGENT_ID="your-agent-uuid"

# 1. Create session
SESSION=$(curl -s -X POST https://api.tradeready.io/api/v1/backtest/create \
  -H "X-API-Key: $API_KEY" -H "Content-Type: application/json" \
  -d '{"start_time":"2025-01-01T00:00:00Z","end_time":"2025-01-31T23:59:59Z",
       "starting_balance":10000,"candle_interval":"1h",
       "strategy_label":"my_strategy_v1","agent_id":"'$AGENT_ID'",
       "pairs":["BTCUSDT","ETHUSDT"]}')
SID=$(echo $SESSION | python3 -c "import sys,json; print(json.load(sys.stdin)['session_id'])")

# 2. Start
curl -s -X POST https://api.tradeready.io/api/v1/backtest/$SID/start -H "X-API-Key: $API_KEY"

# 3. Step loop (your strategy logic goes here)
while true; do
  STEP=$(curl -s -X POST https://api.tradeready.io/api/v1/backtest/$SID/step -H "X-API-Key: $API_KEY")

  # Read prices, check indicators, place/cancel orders...

  IS_DONE=$(echo $STEP | python3 -c "import sys,json; print(json.load(sys.stdin)['is_complete'])")
  if [ "$IS_DONE" = "True" ]; then break; fi
done

# 4. Results
curl -s https://api.tradeready.io/api/v1/backtest/$SID/results -H "X-API-Key: $API_KEY"
```
**Python SDK:**

```python
from decimal import Decimal
from agentexchange import AgentExchangeClient

def compute_rsi(prices, period=14):
    """Simple RSI calculation."""
    if len(prices) < period + 1:
        return None
    gains = [max(prices[i] - prices[i-1], 0) for i in range(1, len(prices))]
    losses = [max(prices[i-1] - prices[i], 0) for i in range(1, len(prices))]
    avg_gain = sum(gains[-period:]) / period
    avg_loss = sum(losses[-period:]) / period
    if avg_loss == 0:
        return 100
    rs = avg_gain / avg_loss
    return 100 - (100 / (1 + rs))

with AgentExchangeClient(api_key="ak_live_...") as client:
    # 1. Create and start
    session = client.create_backtest(
        start_time="2025-01-01T00:00:00Z",
        end_time="2025-01-31T23:59:59Z",
        starting_balance=10000,
        candle_interval="1h",
        strategy_label="rsi_mean_reversion_v1",
        agent_id="your-agent-uuid",
        pairs=["BTCUSDT"],
    )
    sid = session["session_id"]
    client.start_backtest(sid)

    # 2. Track price history for indicators
    price_history = []
    in_position = False

    # 3. Step loop
    while True:
        step = client.step_backtest(sid)
        close = float(step["prices"]["BTCUSDT"]["close"])
        price_history.append(close)

        rsi = compute_rsi(price_history)
        portfolio = step["portfolio"]
        cash = float(portfolio["available_cash"])

        if rsi is not None:
            if rsi < 30 and not in_position and cash > 1000:
                # Oversold — buy
                qty = str(round(cash * 0.9 / close, 6))
                client.backtest_trade_order(sid, "BTCUSDT", "buy", "market", qty)
                in_position = True
                print(f"BUY at ${close:.2f}  RSI={rsi:.1f}")

            elif rsi > 70 and in_position:
                # Overbought — sell all
                positions = client.get_backtest_positions(sid)
                for pos in positions.get("positions", []):
                    if pos["symbol"] == "BTCUSDT":
                        client.backtest_trade_order(
                            sid, "BTCUSDT", "sell", "market", pos["quantity"]
                        )
                in_position = False
                print(f"SELL at ${close:.2f}  RSI={rsi:.1f}")

        if step["is_complete"]:
            break

    # 4. Results
    results = client.get_backtest_results(sid)
    summary = results["summary"]
    metrics = results["metrics"]
    print(f"\nResults for {sid}:")
    print(f"  ROI: {summary['roi_pct']}%")
    print(f"  Sharpe: {metrics['sharpe_ratio']:.2f}")
    print(f"  Max Drawdown: {metrics['max_drawdown_pct']:.1f}%")
    print(f"  Win Rate: {metrics['win_rate']:.1f}%")
    print(f"  Total Trades: {summary['total_trades']}")
```

---

## Step Batching Guide

Not every strategy needs to inspect every candle. Batching dramatically reduces API call overhead:

| Strategy timeframe | Recommended batch size | Calls for 1 month of 1m data |
|---|---|---|
| Every candle | 1 | 44,640 |
| Hourly signals | 60 | 744 |
| Daily signals | 1,440 | 31 |
| Weekly rebalancing | 10,080 | ~4 |

> **Info:**
> Pending limit orders, stop-losses, and take-profits are checked against every candle in a batch — they will fill correctly even when batching 1,440 steps at once.

---

## Risk Limits in Backtesting

The sandbox enforces the same risk rules as live trading. The limits come from the agent's risk profile:

| Limit | Effect |
|-------|--------|
| `max_order_size_pct` | Maximum order size as % of available cash |
| `max_position_size_pct` | Maximum position size as % of total equity (default 25%) |
| `daily_loss_limit_pct` | No new orders when daily loss exceeds this (default 20%) |

If a sandbox order is rejected due to risk limits, the error response includes the specific limit violated.

---

## Related Pages

- [Strategy Testing](/docs/api/strategy-testing) — automated multi-episode testing
- [Battles](/docs/api/battles) — multi-agent historical competitions
- [Errors](/docs/api/errors) — error code reference
- [Rate Limits](/docs/api/rate-limits) — request limits
