Agent API (MCP)
Run backtests from your own AI agent, such as Claude Code or Codex. Your agent writes the strategy; QuantWright runs it and sends back summary figures.
What it is
The Agent API gives an AI agent that you run yourself a set of tools over MCP, the Model Context Protocol. The agent writes a strategy as one backtesting.py Strategy class. QuantWright runs it in its sandbox on the price history it holds, and returns the result as summary figures. The agent does not need to find or download any price data.
It is part of your subscription, at no extra price, and works on your own account. QuantWright calls no AI model for these tools, so they use none of your AI allowance: the model is the agent you connect. They have daily limits of their own, listed under Limits.
QuantWright is research software. It never places orders, every result is hypothetical, and nothing the API returns is investment advice. Every result carries the same hypothetical-performance line the app shows.
Before you connect
You need an active subscription, and you need to have accepted the current Terms: sign in to QuantWright once, and accept them if it asks. Without both, create_study, run_backtest, run_held_out_test and compare_trades are refused. get_result still answers, so you can read what you already have.
You also need to have confirmed, once, in the API keys pane, that your AI assistant has training turned off, or runs under zero data retention, with a provider in the United States or the European Union (below). Until you have, every tool except describe_data is refused, get_result included, whoever made the key; describe_data still answers, and says what is missing.
Your assistant’s data settings
The results QuantWright returns go to the AI assistant you connect, and through it to that assistant’s provider. The Terms ask you to use the assistant with training on your conversations turned off, or under zero data retention, with a provider hosted in the United States or the European Union, and not to use the results to train AI models. The API keys pane asks you to confirm this once before it shows your keys, and the API refuses your assistant until you have.
Claude Code and the Claude apps
On a Free, Pro or Max account, which includes Claude Code signed in with one, Anthropic trains its models on your conversations when its model-improvement setting is on. To turn it off, open Settings, then Privacy, and switch off Help Improve our AI models; the same page is claude.ai/settings/data-privacy-controls. Anthropic’s documentation says that on Team and Enterprise plans, on the API and on third-party platforms it does not train its models on the code or prompts sent to Claude Code under its commercial terms, and that zero data retention is available to qualified Claude for Enterprise accounts, set up for the organisation with Anthropic.
ChatGPT and Codex
On a personal ChatGPT plan, open Settings, then Data controls, and switch off Improve the model for everyone. OpenAI says this also covers Codex tasks on a personal plan, and that Codex has a separate setting of its own, in Codex’s settings, for training on your environments: check it as well. If you run Codex on an OpenAI API key instead, OpenAI’s documentation says data sent to its API is not used to train its models unless you opt in.
Any other assistant
Check its provider’s privacy pages for the same two things: whether it trains on your conversations, and where it is hosted. Providers rename these settings. If you do not see the name above, look in your account’s privacy or data settings.
Create an API key
The API takes a bearer key that belongs to your account. Create it while you are signed in to QuantWright: open API keys in the app’s side menu. Add a key, give it a name you will recognise, and copy it when it is shown.
Treat the key like a password. Anyone who has it can run backtests as you, up to your daily limits. Revoke it on the same page if it leaks. Create it for your own account, not for an organisation: a key that belongs to an organisation names no person, and the API refuses it.
Connect your agent
The address is https://quantwright.com/api/mcp. The agent sends your key with each request in an Authorization header: the word Bearer, a space, then the key. The commands below show PASTE_YOUR_KEY_HERE where the key goes. Replace it with your key before you run a command: a command run with the placeholder still in it saves the placeholder as the key, and the server then refuses every request. The API keys pane in the app has a field for the key you just created and fills it into these commands for you.
Claude Code
claude mcp add --transport http --scope user quantwright https://quantwright.com/api/mcp --header "Authorization: Bearer PASTE_YOUR_KEY_HERE"
The server is added at user scope, so it is there in every folder you open Claude Code in; without --scope user Claude Code adds it for the current folder only. claude mcp list shows whether it connected.
Changing your key, or added it before? Claude Code will not add a name that already exists, so run claude mcp remove quantwright --scope user first, then the command again with your key. That also fixes a registration made with the placeholder. If you ran an older version of this command, which had no --scope, that entry belongs only to the folder where you ran it and is used in that folder instead of the new one: in that folder, also run claude mcp remove quantwright --scope local.
Codex
export QUANTWRIGHT_API_KEY="PASTE_YOUR_KEY_HERE" codex mcp add quantwright --url https://quantwright.com/api/mcp --bearer-token-env-var QUANTWRIGHT_API_KEY
Codex reads the key from that environment variable each time it starts, so the variable has to be set where you launch Codex. The export lasts only in the terminal where you ran it: to have it in new terminals, add the export line to your shell profile (~/.zshrc on a Mac, ~/.bashrc on most Linux systems) and open a new terminal. If you ran it with the placeholder, set the variable again with your key: the Codex entry holds only the variable’s name, and running the second line again replaces it. The same server written by hand in Codex’s config.toml:
[mcp_servers.quantwright] url = "https://quantwright.com/api/mcp" bearer_token_env_var = "QUANTWRIGHT_API_KEY"
Other clients
Any other MCP client that can call a remote server over HTTP with an Authorization header takes the same address and key. Keep the key out of anything you share, and out of any file that is committed to a repository.
Ask for it by name
An agent chooses its own tools. Asked only to test an idea, it may search the web or look for a price file instead of calling QuantWright. Name QuantWright in the request:
A request that names itUse QuantWright to test buying QQQ at the close the day after it falls more than 3%, and selling after five trading days. Keep 2025 onward as a hold-out, and run the held-out test once at the end.
The agent then works in this order: describe_data, create_study, run_backtest and get_result as often as it needs, and run_held_out_test once, on the run you would keep.
The tools
describe_dataWhat can be backtested: the markets and bar sizes served, the rules every run is held to, and the rules for writing the strategy code. It also says first whether this account can run: account.can_run is true, or false with a reason (terms_not_accepted, payment_required or ai_data_not_confirmed, as under Errors) and what to do, so a job is not planned for an account that cannot run it. For an account that can, it also says what is left of today’s counts, as numbers (studies, runs through the API and seconds of engine time; the runs left on one symbol are in each run’s result), and lists the account’s 10 most recent studies, newest first, each with its study_id, its name and when it was made, so a study can be used again. Call it first. It changes nothing.
create_studyOpens a study, the named set of runs that your backtests belong to. Make one per question. A study can have a hold-out, set here and nowhere else. The API runs only in studies made this way: a chat made in the app is refused.
name(required): What the study tests, in a few words.hold_out: The days kept back from every run except run_held_out_test, as { from, to }: the first day, and optionally the last (YYYY-MM-DD). With no to, everything from from on. Set once, here; it cannot be changed later.
run_backtestBacktests one Strategy class on one symbol and one bar size, in a study.
study_id(required): The id create_study returned.code(required): One backtesting.py Strategy class, up to 50,000 characters. describe_data returns the rules for writing it.symbol(required): One ticker, such as QQQ, BTCUSD or EURUSD. A run reads one symbol: pairs and spreads between symbols are not offered.timeframe(required): The bar size, one of 1m, 5m, 15m, 30m, 1h, 4h, 1d.window: The dates to test, as { from, to } (YYYY-MM-DD; to may be "latest"), or the last days, months or years as { last: { n, unit } }. Left out, a run tests the standard window: the newest bars of that market and bar size, counted back from the newest bar and cut at the start of the hold-out. That is a default, not all the data; dates reach further back.costs: Commission and slippage. See Costs below. Left out, the market’s default costs are charged.account_usd: The starting account in US dollars, from $100 to $100,000,000. Left out, $100,000.stated_rules: Rules the trades are checked against, at most one of each kind: no_overnight; flat_by { time, tz }; session { from, to, tz }; max_trades_per_day { n }, a whole number from 1 to 10; risk_per_trade_pct { pct }, above 0 and at most 10, written as a percent. Times are "HH:MM" on a 24-hour clock in an IANA time zone, New York when none is given. The result reports each rule that was not held, as a count.label: A short text shown with the run on QuantWright.
get_resultReads a run by its run_id, or lists a study's runs by study_id. It ranks nothing: it names the run with the highest simulated net return and says how many runs had a return figure to compare, and that is a figure of the simulation, not a recommendation. It changes nothing. A run comes with its trades, a page at a time: see Trades under What a result contains.
run_id: A run_id that run_backtest or run_held_out_test returned.study_id: A study’s id, to list its runs: the newest 20, or with version one of them.version: With study_id: one run of the study, by its number, written with or without a leading v.before_version: With study_id: the 20 runs before this version. A list that was cut gives the call to make in next.trades_page: With a run: the page of its trade list, from 1; a page holds 100 trades. Left out, the first.
run_held_out_testRuns a finished run's exact code once over its study's hold-out. A hold-out that holds fewer trades than a result needs gives no figures and spends the test all the same: a development run's hold_out_estimate says how many trades it is expected to hold. See Held-out tests below.
run_id(required): A run that ran (status ran), in a study that has a hold-out.
compare_tradesChecks a trader's own trades against a run's rule: for each trade, in the trader's order, whether the rule would have taken it and, if not, why. It runs the run's code again over the trades' dates. See Checking your own trades below.
run_id: A run_id that run_backtest or run_held_out_test returned: the run whose rule the trades are checked against.study_id: With version, the run by its study instead.version: With study_id: one run of the study, by its number, written with or without a leading v.trades(required): The trader’s own trades, in their order, 1 to 50. Each is { side, entry, exit? }: side is long or short, and entry and exit are a day (YYYY-MM-DD) or a time (YYYY-MM-DD HH:MM) exactly as the trader wrote them. Leave exit out when they gave none.time_zone: The clock the trader’s times are on: UTC+2, New York, Chicago, London or a zone name such as Europe/Prague. Left out, the clock the run’s trade list is on: ET for stocks and ETFs, UTC for everything else.
run_backtest and run_held_out_test wait up to 90 seconds for the run. If it has not finished by then, the answer has status running and a run_id. Call get_result with that run_id after the poll_after_seconds the answer gives, until the run ends. The same call made again within 6 minutes returns the first call’s run, starts nothing and counts nothing, and the answer says so: replayed_from is the first call’s run_id, the first field of the answer (after the disclaimer on a result), and the first sentence of the text, after the disclaimer, says the same. The same call means the same tool, study, code, symbol, bar size, costs, account, window and stated rules (their order does not matter). A different label does not make a call different: the label is only shown with the run.
Every run_id and study_id belongs to your account. A study made through the API is not shown in the QuantWright app, and a result carries no link to it.
Markets and bar sizes
The API serves the markets below, and only these, whatever the app serves: CME futures are not available through it. describe_data returns the same list.
- US stocks and ETFs on daily bars, split-adjusted, as total returns (cash dividends credited on each ex-date): about 7.9 years per test by default, and all daily history with Max.
- US stocks at 1 minute to 4 hours, from IEX exchange prints only: a small slice of US volume, so thin on anything but large names.
- Spot crypto pairs (BTCUSD, ETHUSD, SOLUSD…) at 1 minute to daily, aggregated across exchanges.
- Spot FX pairs (EURUSD, USDJPY…) at 1 minute to daily, without volume.
- Spot gold and silver (XAUUSD, XAGUSD) at 1 minute to daily, without volume: Sunday 18:00 to Friday 17:00 New York, closed 17:00–18:00 each day.
Bar sizes: 1m, 5m, 15m, 30m, 1h, 4h, 1d. A run uses one symbol and one bar size. How far back a window reaches is on the Market data page, and each result’s window field says which window was asked for, which one ran, and why they differ. Its longest says how far back dates reach on this market and bar size (from and most_bars are there when they have a value); on daily stocks and crypto the data starts at each symbol’s own first bar, and a window whose from is earlier than any bar finds it: window.start_later.first_bar is that day. Bars from before a window’s first bar are run first, so the indicators have history when it opens: a rule that looks back can signal on the window’s first bar, trades are counted from it, and none is placed on the earlier bars. window.warmup says how many bars ran before it and the dates they span, and market.bars counts the window’s own bars alone. A script of your own that starts at the window’s first bar starts without that history, so its first trades can differ from the run’s.
Costs
costs takes one of two shapes. The first is two fractions of the trade’s value, from 0 to 0.05 (5%): commission, charged once per round trip on the value at entry, and slippage, charged on each fill, entry and exit. The second is a list of 1 to 6 costs, each in its own unit and basis:
side: commission, slippage or spread.amount: a number, 0 or more, in the unit given.unit:percent,bps,usd_per_share,usd_per_ounce,pipsorusd_per_order.basis:per_side(on each fill) orround_trip(once, for the entry and exit together).
usd_per_share is charged on stocks and ETFs, usd_per_ounce on spot gold and silver, and pips on currency pairs, as a spread or a slippage. usd_per_order is a flat fee, so it is a commission; percent and bps are charged on any market.
"costs": [
{ "side": "commission", "amount": 0.005, "unit": "usd_per_share", "basis": "per_side" },
{ "side": "slippage", "amount": 1, "unit": "bps", "basis": "per_side" }
]A cost the engine cannot charge is refused, not dropped. A per-share cost on a currency pair, pips on a stock, an amount above what the engine charges in its unit, or the same cost given twice: the call returns invalid_arguments, names the cost and says why, and nothing runs. If the engine that runs the strategy cannot charge a flat fee per order, the run is refused with the reason cost_not_charged and gives no result, because its figures would leave the fee out.
With a list, a result’s settings.costs_charged is that list: every cost the run charged, each in its own unit and basis. Its note says where a cost changed basis when it was charged: a commission in percent or bps is charged once per round trip, on the value at entry (a per-side figure is doubled), and a cost stated per round trip is charged half on each fill. commission and slippage then appear only for a side the list left out, as the fraction of the trade’s value the market’s default charged.
A cost left out is the market’s default, listed under Default trading costs. Each result’s settings says what was charged and whether you set it (caller) or it is the default.
What a result contains
A finished run comes back as structured fields, in the order to read them:
disclaimer: the hypothetical-performance line.headline: the market, bar size and dates, the number of simulated trades, the net return, the deepest drawdown, and whether the run beat buy-and-hold over the same dates.grade: the evidence grade and score,trialsand the deflated Sharpe. The grade is marked down for the distinct settings this account has tested on the same market in the last 30 days (a setting is its code with its numbers, on a development window; blank lines, spacing and comments are not part of it: the same call made again adds none, whenever it is made, and so do other costs or another account size, a run on another development window adds one, and a held-out test run adds none; a held-out test that counts is marked down for held-out attempts alone), andtrialsis that count. With one trial nothing is deflated, anddeflated_sharpeis the Probabilistic Sharpe. Under the trade floorgrade,scoreanddeflated_sharpeare null, andtrialsandgrade.searchare still there.grade.searchshowstrialspart by part:runshas the distinct settings in this study, the distinct settings in all of this account’s studies over the last 30 days, the markets in this study and the draws of this code, and the largest of them is the one that counts, since one run is in several (counted);evaluations_without_a_runanddiscarded_candidatesare what the app’s /sweep, /sensitivity, walk-forward and /iterate tried besides, and they add;distinct_settingsays what makes a setting distinct. It is absent on a held-out test that counts. When the score sits on the grade-C ceiling, a line oflinessays so and what lifts it. Reading results explains the score.linesandlook_ahead: whether a look-ahead check found the code reading data from the future (clean,possibleorleak; null when no check was made, and on a run of fewer than 10 trades, which the check cannot compare, a line says so), whether this run is the held-out test, and any stated rule that was not held. A held-out test names the run whose code it ran inheld_out.tested_run.warnings: at most 2, the most severe first.noticessays when more were cut.metrics:trades,net_return,max_drawdown,annualized_return,win_rate,sharpe,profit_factor,exposureandtrades_per_year. Returns and drawdowns are fractions, not percentages, rounded to 0.1 percentage point. The other ratios are rounded to 2 significant figures.basis: what each ratio is measured on, once: the drawdown, the Sharpe, the profit factor, the net return, the exposure (the bars from each trade’s entry bar through its exit bar, both counted) and the deflated Sharpe. A figure that is withheld has none.buy_and_hold.beat: whether the run beat holding the symbol over the same dates.market,windowandsettings: what was tested, the window asked for and the one that ran (with the warm-up bars before it), and the costs charged, the account and the window the run used.stated_rule_breaks,held_out,hold_out_estimate,notices,runs_left_todayandnext: the rules not held, whether this run is the one held-out test that counts, on a development run the trades its study’s hold-out is expected to hold, everything the result leaves out, the runs left today on the run’s market, and the call to make next where one applies.- Trades:
trade_listhas the run’s trades in the order they were made, 100 to a page. Each trade is itsentryandexit, itsside(long or short), itsoutcome:winorlossafter costs, orflatfor a return of exactly zero, and itsexit_type: which exit ended it, as a word (stop,target,trailing stop,time limit,end of data, orexit rulewhen the strategy’s own code closed the position). A close the code made is a time limit when the code has one constant named for a hold limit (such asmax_hold_days) and the trade was held that many bars or longer, as the app’s trade list reads it, and an exit rule otherwise;exit_typeis null where the API has no word for it. On daily barsentryandexitare days; on shorter bars they are “YYYY-MM-DD HH:MM” on the clocktrade_list.clocknames (ET for stocks and ETFs, UTC for crypto and FX), as the app’s own trade list shows them. A trade still open when the data ended has the exit “open at the end”.totalis the number of trades,pageandpagessay where this page is, andnextinside it is the call for the next page:get_resultwithrun_idandtrades_page(orstudy_id,versionandtrades_page). A run of fewer than 10 trades lists its trades with nooutcomeand noexit_type(null, andoutcome_withheldsaystrade_floor), as it gives no win rate. A trade has no price, level, size or return. A run made before trade lists were kept has none, and says so intrade_list.message; one made before exit types were kept has the list withoutexit_type. A held-out test lists its trades the same way.
The text part of the answer says only what the structured part says. Every figure is a simulation on historical data: it says what a rule did, not what it will do.
Checking your own trades
compare_trades answers “would my rule have taken my trades?”. Give it a run and up to 50 of your own trades, each as side, entry and, if you have one, exit, in your order. It runs the run’s code again over your dates, with the run’s costs, account and rules, and answers each trade: rule says taken or not taken, reason says why in a code, and answer and why say it in words. summary counts them in one line. Each trade is written with its dates in full, year included. On bars shorter than a day every time of the answer is on one clock, which clock names: your time_zone as you stated it, or the clock the run’s trade list is on. An answer on those bars is about a bar (“no entry signal on that bar”), not a day. The answer carries runs_left_today, as a run’s result does: the runs left today on the run’s symbol, this one counted.
taken: the rule entered on your day (within a bar on shorter bars).answersays when its exit was on another day than yours.in_trade: the rule was already in a trade when you entered.flat: the rule was out of the market and entered nothing then.no_bar: there is no bar at that day or time in the bars tested.side_never: the rule’s code never opens a trade on your side.exit_unknown: an earlier trade of the rule has no exit date, so what it was doing then is not known.outside_bars: the trade is outside the bars tested.no_time: the run is on bars shorter than a day and the trade has no time of day to match on.
An answer holds dates, sides and words, never a price, a level or a return. It is a run: it counts in the runs a day, in the runs a day on the run’s symbol and in the engine time (see Limits), and it is refused, as the run tools are, to an account that has not subscribed, accepted the Terms or confirmed its assistant’s data settings. A trade that would let the comparison read a bar of the study’s hold-out is refused (holdout): the held-out days are tested once, with run_held_out_test.
What a result leaves out
The licence for the market data covers results worked out from it, not the data itself. So a result never contains prices or bars, any one trade’s price, level, size or return, equity or drawdown curves, or the return of holding the symbol: buy_and_hold.return is always null, and buy_and_hold.beat says whether the run beat it. A run that fails is reported by its error type and the line in your code, not by the engine’s own message. See why prices are not shown.
A figure that is withheld is null, with a key beside it named <name>_withheld that gives the reason, and notices lists it too. A withheld figure is not a value: when you compare runs, a run whose figure is withheld has none, and a comparison, range or “all” covers only the runs that give it.
| Reason | What it means |
|---|---|
trade_floor | A run of fewer than 10 trades gives no return figure, nor any figure worked out from its trades’ returns, nor the grade, nor the buy-and-hold verdict. The number of trades is still given, and so is the trial count with its parts (grade.search), which reads no return. Use a longer window, or a bar size that gives the rule more trades. |
market_data_licence | The licence lets a customer see derived results only, and on very few trades a figure would be one trade’s own. buy_and_hold.return is withheld under it too. |
trial_count_incomplete | The grade is withheld: the count of earlier runs it would be marked down for may be incomplete. The figures are still given. |
no_benchmark | The run had no buy-and-hold figure over its dates to compare with. |
not_on_api | The API does not give that figure. |
Held-out tests
A hold-out is a period you keep back from every run. You set it once, with hold_out in create_study, and it cannot be changed. A run with no window ends the day before it. A window of dates that reaches into it is cut at that day, and the result’s window.end_at_hold_out says which. A window wholly inside it is refused (holdout), and so is a window of the last days, months or years, which counts back from the newest bar: give dates before the hold-out, or no window.
run_held_out_test takes a run that ran, and runs that run’s exact code once over the whole hold-out, with the same symbol, bar size, costs, account and rules. The study must have a hold-out.
Only one held-out test counts. A test counts only if no earlier run of yours on the same market has already tested any of the hold-out days. That is counted over your whole account, not per study, so a run in another study that covered those days has used them up. A held-out test that does not count still runs, and the result says so: held_out.counts is false, and where the cause is an earlier run, held_out.covered_by names it: its version and the dates it ran in window, and for a run made through the API its run_id and study_id (a run made in the app is named by its dates alone). The earliest are named, and covered_by_total says how many there were. Run the test once, at the end, on the version you would keep.
A hold-out that holds fewer than 10 trades gives a result with no figures (see trade_floor above), and the test cannot be run again to count: an earlier run on those days has used them up. So each development run in a study with a hold-out carries hold_out_estimate: expected_trades, this run’s trades a year over the hold-out’s years (to its last day, or to today), and the trade_floor a result needs. When it is under the floor, a line of lines says so before the test is spent: a longer hold-out is set when a study is created, and cannot be changed later.
It is one look at days the strategy was not developed on. It does not show that the strategy will work later.
Limits
- Studies a day
50
- Runs a day
150 through the API.
- Runs a day, app and API
300 together. It is one count: a backtest in the app and a run through the API each add one. The app stops at 200, the Backtests number on Pricing and Limits and billing. A run through the API is refused once the count has passed 300, or after its own 150, so the API can still start runs after the app has stopped.
- Engine time a day
30 minutes, each run’s look-ahead check included.
- Runs a day on one symbol
40 a symbol, counted when a run starts, whatever it comes to. The bar size does not matter: QQQ on 1d and on 1h share one count, and QQQ and AAPL have one each. A call refused having run nothing and read no price counts toward no daily limit, this one or the others. That is
market_not_served, a limit, and these:holdout,window,account,params_override,api_single_symbol,no_bars,few_bars(a window inside the hold-out, a window of the last days with a hold-out, an account outside its range, a constant given as an argument, a second symbol, and a window that holds no bars or too few). A call refused after the market’s data was asked for or the code was read, such asvendor_unavailable,not_servedorno_exit, counts, and so does a rejection or an error of the code. The same call made again within 6 minutes counts nothing. A refusal that saysretryablegives up its place, so the call made again is a new run and counts again. A run’s result says how many are left today on its symbol, inruns_left_today.- Runs at once
1 per account. The API as a whole also has a limit on runs at once, shared by all accounts.
compare_trades counts as a run in each count above, the runs at once included. The daily counts reset at 00:00 UTC. A limit that is reached gives a refusal that names it and says whether the same call may work later; nothing runs. None of these uses your AI allowance (see Limits and billing).
Errors and what to do
Every answer has a status: ran or running for a run, study for a study’s list of runs, created for a new study, or one of refused, rejected, error, invalid_arguments and not_found. A refusal also carries a reason and a message. On a run, retryable is true when the same call may work if it is made again, for example after a short wait.
| Code | What it means | What to do | retryable |
|---|---|---|---|
HTTP 401 | The request carries no key, or a key the API does not accept. | Send the key as Authorization: Bearer <your key>. If it was revoked, create a new one. A key made for an organisation is refused: create it for your own account. | not given |
payment_required | Your subscription is not active. Only create_study, run_backtest, run_held_out_test and compare_trades check this. | Open QuantWright and subscribe, then make the call again. | not given |
terms_not_accepted | The Terms have been updated and you have not accepted them. The same four tools check this. | Sign in to quantwright.com, accept them, then make the call again. | not given |
ai_data_not_confirmed | You have not confirmed, in the API keys pane, that your AI assistant has training turned off, or runs under zero data retention, with a provider in the United States or the European Union. create_study, run_backtest, run_held_out_test, compare_trades and get_result check this; describe_data does not. | Sign in to quantwright.com, open API keys, confirm it, then make the call again. | not given |
invalid_arguments | An argument is missing, is not valid, or is one the tool does not take. The message names it. | Fix the call. Nothing ran and nothing was counted. | not given |
not_found | There is no study or run of this account by that id. | Use an id the API gave you. Ids from another account, or from a chat in the app, are not found. | not given |
| Code | What it means | What to do | retryable |
|---|---|---|---|
app_study | The study is a chat made in the app. The API runs only in studies made with create_study. | Make a study with create_study. | not given |
no_holdout | run_held_out_test was asked of a study that has no hold-out. | A hold-out is set only by create_study, once. Make a new study with one. | not given |
not_a_result | run_held_out_test and compare_trades take a run that ran, and this one did not. | Pass a run with status ran. | not given |
held_out_run | The run is itself a held-out test. | Pass the run that was tested, not its held-out test. | not given |
holdout | The window lies inside the study’s hold-out, or, in compare_trades, a trade would let the comparison read a bar of it: it reads the bars up to a day or more after the last entry. | Give dates before the hold-out, or no window; in compare_trades, trades that enter earlier. The held-out days are tested once, with run_held_out_test. | false |
window | A window of the last days, months or years was given in a study that has a hold-out. The two cannot both hold. | Give dates before the hold-out, or no window. | false |
| Code | What it means | What to do | retryable |
|---|---|---|---|
busy | Another run of this account is still running. | Make the call again when that run has ended. | true |
api_busy | The API as a whole is running as many runs at once as it takes. | Make the call again in a minute. | true |
api_daily_runs | This account has started its 150 runs of the day. | Wait for 00:00 UTC. | false |
daily_runs | The day’s count of backtests in the app and runs through the API together has passed 300. | Wait for 00:00 UTC. | false |
engine_time | This account has used its 30 minutes of engine time for the day. | Wait for 00:00 UTC. | false |
disclosure_budget | This account has started its 40 runs of the day on that symbol; each symbol has its own count, whatever the bar size. When the count itself could not be checked, retryable is true. | Test another symbol, or wait for 00:00 UTC. | false |
api_daily_studies | create_study: this account has made its 50 studies of the day. | Wait for 00:00 UTC, or put the next question in a study you already have: describe_data lists your 10 most recent. | not given |
limits_unavailable | The limits could not be checked, so nothing started. | Try again in a minute. | true |
| Code | What it means | What to do | retryable |
|---|---|---|---|
market_not_served | The symbol is not a market this API serves. A future (a CME contract such as GC or CL, or a continuous one written as TradingView writes it, such as MGC1!) is never served, and the message says so and names the nearest spot or ETF market that is. | Run the market it names, or one that describe_data lists. | false |
not_served | That symbol and bar size is not offered. | describe_data lists the markets and bar sizes served. | false |
no_bars | No usable bars came back for that symbol, bar size and window. | Try a longer window or another bar size. | false |
few_bars | The window holds fewer than 50 bars, the fewest a run takes. The bars before it that warm the indicators up do not count. | Give a window that holds more bars. | false |
vendor_unavailable | The market data source did not answer. | Try again in a few minutes. | true |
vendor_rejected | The market data source refused the request for those bars. The same call is refused again. | Try another window or bar size. | false |
api_single_symbol | The code reads more than one symbol, or a second feed. | A run reads one symbol. Test each symbol in its own run. | false |
not_compared | compare_trades could not run the run’s code again over the trades’ dates: the market data did not answer, the code does not run on the bars around them, or too few bars cover them. It comes as status error. | Try again in a minute, or give dates inside the run’s window. | not given |
| Code | What it means | What to do | retryable |
|---|---|---|---|
rejected | The code was rejected before anything ran. The message says which problem: it is not a strategy class the engine can run; it reads helper columns the feature library does not return; it writes a date of the run’s window into its logic; it did not compile (the error type and line); or the sandbox refused something it does. | Fix the code. describe_data returns the rules for writing it. | not given |
error | The strategy ran and raised an error. The message gives its type and the line in your code, not the engine’s own message. | Fix the code at that line. | not given |
no_exit | The code opens positions and never closes one. A strategy that never sells, such as buy-and-hold, is one trade, under the 10-trade floor, so its return would be withheld anyway. | Give the code an exit. buy_and_hold.beat on any run already says whether it beat holding the symbol. | false |
data_hours | The code reads hours of the day that the bars do not have, so it could take no trades. | Use the hours the bars have, or choose a market or bar size that has them. | false |
event_list | The code needs a list of event dates it was not given. | Write the rule without it. | false |
values_open | A value the code needs is not a number. | Set every constant in the code to a number. | false |
params_override | Constants are set in the code. They are not set as arguments. | Change the constant in the code. | false |
sandbox | The engine could not run the code inside its sandbox, so no result is given. | Try again in a few minutes. | true |
account | account_usd is outside $100 to $100,000,000, or is not a number. | Give an account in that range, or leave it out. | false |
cost_not_charged | The engine that ran this did not charge the flat fee per order in costs, so no result is given: its figures would leave the fee out. | Write to Support. A run without that fee is a different test. | false |
unclassified | The run came back with no result and no cause that could be named. | Try again in a few minutes. If it repeats, write to Support. | true |
lost | A run had no outcome 6 minutes after it started: the host running it stopped. It comes as status error, with retryable true. | Run it again. | true |
Not answered here? Write to us through the Support page.