TradeReady.io
REST API

Backtesting

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

Download .md

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.

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

MethodPathDescription
POST/backtest/createCreate a new backtest session
POST/backtest/{id}/startPreload data, initialize sandbox
POST/backtest/{id}/stepAdvance one candle
POST/backtest/{id}/step/batchAdvance N candles
POST/backtest/{id}/cancelAbort early, save partial results
GET/backtest/{id}/statusProgress and current state

Sandbox Trading

MethodPathDescription
POST/backtest/{id}/trade/orderPlace order in sandbox
GET/backtest/{id}/trade/ordersList all sandbox orders
GET/backtest/{id}/trade/orders/openList pending sandbox orders
DELETE/backtest/{id}/trade/order/{order_id}Cancel sandbox order
GET/backtest/{id}/trade/historySandbox trade execution log

Sandbox Market Data

MethodPathDescription
GET/backtest/{id}/market/price/{symbol}Price at virtual time
GET/backtest/{id}/market/pricesAll 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

MethodPathDescription
GET/backtest/{id}/account/balanceSandbox balances
GET/backtest/{id}/account/positionsSandbox open positions
GET/backtest/{id}/account/portfolioSandbox portfolio summary

Results

MethodPathDescription
GET/backtest/{id}/resultsFull results and metrics
GET/backtest/{id}/results/equity-curveTime-series equity data
GET/backtest/{id}/results/tradesComplete trade log

Analysis

MethodPathDescription
GET/backtest/listList all backtests with filters
GET/backtest/compareCompare sessions side-by-side
GET/backtest/bestFind best session by metric

Mode Management

MethodPathDescription
GET/account/modeCurrent operating mode
POST/account/modeSwitch mode

Data Range

MethodPathDescription
GET/market/data-rangeAvailable historical data range

GET /market/data-range

Check what historical data is available before creating a backtest.

Auth required: No

curl https://api.tradeready.io/api/v1/market/data-range
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:

{
  "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:

FieldTypeRequiredDescription
start_timeISO-8601 datetimeYesHistorical period start
end_timeISO-8601 datetimeYesHistorical period end
starting_balancenumberYesUSDT starting balance
candle_intervalstringYes"1m", "5m", "15m", "1h", "4h", "1d"
strategy_labelstringNoTag for filtering and comparing backtests
agent_idUUID stringYesAgent to run the backtest for (risk profile is loaded from this agent)
pairsstring array | nullNoSpecific pairs to include; null = all 600+ pairs

Response: HTTP 200

FieldTypeDescription
session_idstringSession identifier (prefixed with bt_)
statusstring"created"
total_stepsintegerTotal candle steps in the date range
estimated_pairsintegerNumber of trading pairs included
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"]
  }'
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:

{
  "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 -X POST https://api.tradeready.io/api/v1/backtest/bt_550e8400-.../start \
  -H "X-API-Key: ak_live_..."
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

FieldTypeDescription
virtual_timeISO-8601 datetimeCurrent virtual time after this step
stepintegerCurrent step number (1-based)
total_stepsintegerTotal steps in the session
progress_pctfloatCompletion percentage
pricesobjectMap of symbol → OHLCV object at this candle
orders_filledarrayOrders that filled during this step
portfolioobjectCurrent portfolio state
is_completebooleantrue on the last step
remaining_stepsintegerSteps remaining

Prices object per symbol:

{
  "open": "42150.00",
  "high": "42180.00",
  "low": "42130.00",
  "close": "42165.30",
  "volume": "12.34"
}
curl -X POST https://api.tradeready.io/api/v1/backtest/bt_550e8400-.../step \
  -H "X-API-Key: ak_live_..."
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:

{
  "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:

FieldTypeRequiredDescription
stepsintegerYesNumber 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.

# 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}'
# 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.

# 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"}'
# 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 https://api.tradeready.io/api/v1/backtest/bt_550e8400-.../results \
  -H "X-API-Key: ak_live_..."
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:

{
  "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:

ParameterTypeDescription
strategy_labelstringFilter by strategy label
statusstring"running", "completed", "failed", "cancelled"
sort_bystring"roi_pct", "sharpe_ratio", "created_at"
limitintegerPage size (default 20)
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_..."
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:

ParameterTypeRequiredDescription
sessionsstringYesComma-separated session IDs
curl "https://api.tradeready.io/api/v1/backtest/compare?sessions=bt_aaa,bt_bbb,bt_ccc" \
  -H "X-API-Key: ak_live_..."
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:

{
  "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:

ParameterTypeDescription
metricstring"roi_pct", "sharpe_ratio", "win_rate", "profit_factor"
strategy_labelstringOptional label filter
curl "https://api.tradeready.io/api/v1/backtest/best?metric=sharpe_ratio&strategy_label=rsi_scalper" \
  -H "X-API-Key: ak_live_..."
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 https://api.tradeready.io/api/v1/account/mode \
  -H "X-API-Key: ak_live_..."
mode = client.get_mode()
print(f"Mode: {mode['mode']}  Active backtests: {mode['active_backtests']}")

Response example:

{
  "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 -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"}'
client.set_mode(mode="live", strategy_label="rsi_scalper_v2")

Complete Backtest Loop Example

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"
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 timeframeRecommended batch sizeCalls for 1 month of 1m data
Every candle144,640
Hourly signals60744
Daily signals1,44031
Weekly rebalancing10,080~4

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:

LimitEffect
max_order_size_pctMaximum order size as % of available cash
max_position_size_pctMaximum position size as % of total equity (default 25%)
daily_loss_limit_pctNo 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.


On this page