Backtesting
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.
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
- Create a session with a date range, starting balance, candle interval, and list of pairs
- Start — the engine bulk-preloads all candle data into memory (zero per-step DB queries)
- Step — advance the virtual clock one candle (or batch N candles at once)
- Each step response includes: current prices for all pairs, orders that filled, current portfolio state
- Trade using the sandbox endpoints (same API as live trading)
- Auto-complete when the last step is reached, or cancel early
- 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 https://api.tradeready.io/api/v1/market/data-rangefrom 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:
| 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 -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
| 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:
{
"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:
| 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.
# 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:
| 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 "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:
| Parameter | Type | Required | Description |
|---|---|---|---|
sessions | string | Yes | Comma-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:
| Parameter | Type | Description |
|---|---|---|
metric | string | "roi_pct", "sharpe_ratio", "win_rate", "profit_factor" |
strategy_label | string | Optional 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 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 |
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 — automated multi-episode testing
- Battles — multi-agent historical competitions
- Errors — error code reference
- Rate Limits — request limits