Running a Backtest
Backtesting lets you test a strategy against historical market data before risking real capital. This guide walks you through running a backtest, checking the results, and iterating on parameters to improve performance.Prerequisites
- A created strategy (or a strategy JSON you want to test). If you have not created one yet, follow the Creating a Strategy guide.
- An authentication token. See the Authentication guide.
Step 1: Start a backtest
The backtesting endpoint takes your strategy configuration, a date range, and risk parameters. It runs synchronously and returns results in the response. This example backtests an ETH strategy using the MACD bullish cross + SMA filter, looking back 12 months of 1-hour data:The
strategy_json field must be a stringified JSON object, not a nested object. Use JSON.stringify() in JavaScript or json.dumps() in Python.Reuse a saved strategy by ID
If you already created a strategy and have itsstrategy_id, you can backtest it directly instead of re-supplying the full strategy_json. Pass strategy_id and the run parameters (date range, account, and risk settings); the API loads the strategy’s definition for you and defaults asset and execution_config from the saved strategy when you omit them.
Provide either
strategy_id or strategy_json, not both. Sending both returns a 400; an unknown or inaccessible strategy_id returns a 404. To override the saved strategy’s market, include asset (and execution_config) explicitly alongside strategy_id.Step 2: Choose your date range
The backtesting API supports four modes for specifying the time window:| Mode | Parameters | Example |
|---|---|---|
| Recent history | lookback_months | Last 12 months from now |
| From a start date to now | start_date | June 1, 2025 to present |
| Explicit range | start_date + end_date | Jan 1 to Jun 30, 2025 |
| Lookback from end date | end_date + lookback_months | 6 months back from Dec 31, 2025 |
lookback_months with:
Data availability is the effective floor on the window. The requested range
is clipped to what the upstream market-data provider (CoinAPI) actually has for
the asset and interval. If the provider only holds, say, the last few months for
a symbol, then
lookback_months: 12 and an equivalent start_date resolve to
the same backtest — identical trade count and metrics — because both are
bounded by the available history, not by the parameter you sent. Treat the date
parameters as an upper bound on the window, not a guarantee of its length, and
check the returned date range / bar count to see what was actually used.Step 3: Review the results
A successful backtest returns performance metrics and a trade count:| Metric | What it means | Good values |
|---|---|---|
sharpe_ratio | Risk-adjusted return (annualized). Higher is better. | > 1.0 |
sortino_ratio | Like Sharpe but only penalizes downside volatility. | > 1.0 |
calmar_ratio | Annual return divided by max drawdown. | > 0.5 |
irr_annualized | Annualized internal rate of return. | Positive |
max_drawdown | Largest peak-to-trough equity decline (as a decimal). | < 0.20 |
max_drawdown_duration | Longest drawdown in number of bars. | Depends on timeframe |
win_rate | Fraction of profitable trades. | > 0.45 |
trade_count | Total trades executed. | Varies (too few = insufficient data) |
Step 4: Iterate on parameters
If the results are not satisfactory, adjust your strategy and re-run. Here are common levers to pull:Adjust signal parameters
Try different windows or thresholds for your signals:Adjust risk parameters
Change how much the strategy risks per trade or how long it holds positions. These go in theexecution_config:
atr_volatility_factor (e.g., 3.0) gives each trade more room to breathe with a wider stop-loss. A lower value (e.g., 1.5) tightens the stop. See the Risk Management guide for details.
Change the timeframe
Switch from1h to 4h or 1d bars. Longer timeframes produce fewer but potentially stronger signals:
Compare multiple strategies at once
Use the bulk backtest endpoint to test several strategies in a single request. Market data is fetched once per unique(asset, timeframe) pair and shared across all strategies:
Retrieving a saved backtest
Backtests run through the AI Copilot are persisted with abacktest_id. You can retrieve them later:
The standalone backtesting endpoint (
POST /api/v1/backtesting/backtest) returns results directly but does not create a persistent backtest_runs record. Only backtests initiated through the AI Copilot are persisted and retrievable by ID.Concurrency and limits
The synchronous backtest endpoint runs one backtest at a time per worker. This keeps results deterministic, but it means you cannot speed things up by firing many single-backtest requests in parallel — they serialize behind the running one.| Behavior | What happens |
|---|---|
| One request at a time | Runs immediately and returns results in the response. |
| A second request while one is running | Waits briefly for the first to finish. If it completes in time, your request then runs normally. |
| Too many in parallel / long-running run ahead of you | Your request is rejected with 503 Service Unavailable and a Retry-After header instead of hanging until the gateway times out (504). |
- Run sequentially — wait for each response before sending the next request.
- Honor
Retry-After— on a503, wait the indicated number of seconds and retry (an exponential backoff also works). Official SDKs do this automatically. - Use the bulk endpoint to evaluate many strategies in a single request — market data is fetched once and shared, which is both faster and avoids contention.
503 here is transient and means “busy, try again” — it is not a problem with your strategy or parameters.
Common errors
| Error | Cause | Fix |
|---|---|---|
| Missing required fields | Risk parameters like max_risk_per_trade were omitted | Include all required fields (see Step 1) |
| Invalid strategy format | strategy_json is malformed or missing entry/exit | Verify the JSON structure has entry with 1 TRIGGER + 1 FILTER |
| Invalid date format | Date not in YYYY-MM-DD | Use ISO format: "2025-01-01" |
| Market data unavailable | CoinAPI has no data for the requested range | Try a different date range or asset |
503 engine busy | Another backtest was already running (you sent requests in parallel) | Wait for the Retry-After delay and retry, run sequentially, or use the bulk endpoint. See Concurrency and limits. |
Next steps
- Tune risk parameters to control stop-loss, position sizing, and cooldowns
- Use the AI Copilot to get backtest suggestions automatically
- Understand signals to explore more signal combinations