Intraday Backtester API
Run custom intraday option backtests on one-minute data. Define a strategy with entry and exit times, delta or strike-width leg targets, stops, and profit targets, then run it against intraday options data back to October 2020. The Intraday Custom Backtester in the ORATS dashboard runs on this same service.
Backtests run as jobs: submit one, poll its status until it completes, then fetch the results. A two-week run on a single symbol usually finishes in under a minute; a six-year single-symbol run takes around eight minutes. Authenticate with the token query parameter on every request.
Eighteen strategies are available: PUT_CREDIT_SPREAD, CALL_DEBIT_SPREAD, LONG_CALL, SHORT_PUT, CALL_CREDIT_SPREAD, PUT_DEBIT_SPREAD, SHORT_CALL, LONG_PUT, IRON_CONDOR, IRON_BUTTERFLY, LONG_STRADDLE, SHORT_STRADDLE, SHORT_STRANGLE, LONG_CALL_CALENDAR, LONG_PUT_CALENDAR, DOUBLE_CALENDAR, LONG_CALL_BUTTERFLY, and LONG_PUT_BUTTERFLY. The leg count must match the strategy: one leg for the single-leg strategies, two for spreads, straddles, strangles, and calendars, three for the butterflies, and four for IRON_CONDOR, IRON_BUTTERFLY, and DOUBLE_CALENDAR.
Each endpoint is rate limited to 10 requests per minute per token, counted separately per endpoint, so polling Backtest Status at that rate never blocks fetching results. Beyond the limit, requests return a 429 with a message saying when the minute window resets.
Validation and not-found responses use HTTP status codes: 400 for a bad request, 403 for another account's email or a status update without the engine credential, and 404 for an id that does not exist, is deleted, or belongs to another account, each with a message field in the body.
Overview and pricing for this and the other Tools APIs are in the Tools APIs section of the Data API page.
https://api.orats.io/backtests/intradaySubmit a Backtest
Submits a backtest job. A successful submission returns a backtestId to use with the other endpoints.
Validation happens asynchronously: a response containing a backtestId means the job was accepted, not that the inputs are valid. A submission rejected before a job is created (missing inputs, a body that is not valid JSON, a symbol outside the allowed characters, or an email that is not the token's account) returns a 400 or 403 with a message field instead of a backtestId. Inputs the engine cannot run fail within seconds with status error and the reason in the status object's error field: an unknown strategy, the wrong number of legs for the strategy, a deltaTarget without deltaMin and deltaMax, outside -1 to 1, or outside its own deltaMin to deltaMax band, inverted delta or DTE bands, a DTE of 0, a dteDiff that can never match, a leg with neither a delta band nor a legRelation, a missing optionType, an entryTime outside 0934 to 1559 or an exitTime outside 0934 to 1600 or not later than the entryTime, a stopLossPct or profitTargetPct that is not greater than zero, a negative commissionPerContract, a date window with no trading day, an endDate before the startDate or a startDate in the future, maxHoldDays below 1, timeoutMinutes below 1 or above 55, a negative maxConcurrentTrades, a group number no leg carries, a group exitTime not later than the entryTime, an unknown field at any level, a value of the wrong type (the error names the field and the expected type, for example field "stopLossPct" must be a number; got "none", with dates in YYYY-MM-DD form and legs indexed from 1), or a symbol with no intraday option data. The symbol is trimmed and upper-cased on submission and may contain only letters, digits and underscore (share classes use the underscore form, BRK_B); anything else is rejected before a job is created. Inputs that are valid but match no contracts complete normally with an all-zero summary; the results' warnings and diagnostics blocks say why (for example how many entry days had no leg match), so a summary of zeros means the legs matched nothing, not that the strategy broke even.
Required attributes
- Name
inputs- Type
- object
- Description
The backtest definition. Its fields are documented below.
- Name
inputs.symbol- Type
- string
- Description
The underlying to backtest. Ex: SPY
- Name
inputs.startDate- Type
- string
- Description
The first trade date, formatted by YYYY-MM-DD. Must be 2020-10-01 or later. Ex: 2026-06-01
- Name
inputs.entryTime- Type
- string
- Description
Entry time of day, formatted by HHMM (in EST), between 0934 and 1559 (1600 and later cannot open a position and are rejected). Ex: 0934
- Name
inputs.strategy- Type
- string
- Description
The strategy, from the list above. Ex: PUT_CREDIT_SPREAD
The strategy name fixes the leg count and selects the margin model used for margin-based metrics (AnnMarginReturn, marginPerTradeAvg, BestReturnOnRisk); the legs themselves define what is traded. Label the legs with the strategy they actually form: the same two put legs labeled PUT_DEBIT_SPREAD and PUT_CREDIT_SPREAD produce identical trades but different margin figures.
- Name
inputs.legs- Type
- array
- Description
One object per leg. The first leg sets its own strike and expiration: optionType (put or call), ratio (negative for short legs), a delta band (deltaTarget, deltaMin, deltaMax, always positive; deltaTarget requires both deltaMin and deltaMax and must lie inside them; error messages for put legs report these positive values), and a DTE band (dteTarget, dteMin, dteMax) where DTE 1 is same-day (0DTE) and 2 is the next trading day. A DTE of 0 is rejected.
Later legs may instead be defined relative to an earlier leg with legRelation: strikeWidth (refLeg, target in strike points, optional min and max; positive widths are further out of the money) and dteDiff (refLeg, target in days, 0 for the same expiration; positive values select a nearer expiration than the reference leg, negative a later one). A target without min and max must match a listed strike exactly (SPX strikes are 5 points apart); min and max without a target select the narrowest width in the band. A leg that carries both a delta band and a legRelation is selected by delta.
A leg may also carry a group number (1-based, contiguous, all legs or none) to enable per-group exits; see groups below.
Optional attributes
- Name
email- Type
- string
- Description
Every job is bound to the account that owns the API token, and the status, results, input, notes and delete endpoints answer only for that account's jobs. Leave email out and the job is bound to your account; if you send it, it must be the email on your ORATS account (case-insensitive) or the submission is rejected with a message that the email does not match the token's account.
- Name
inputs.endDate- Type
- string
- Description
The last trade date, formatted by YYYY-MM-DD. Omit it to run through the last completed trading session (Eastern time). Today or a future date is clamped to the last completed session; the results' effectiveConfig.endDate shows the date that ran (no warning is added). A startDate after the last completed session fails the job.
- Name
inputs.exitTime- Type
- string
- Description
Exit time of day, formatted by HHMM (in EST), between 0934 and 1600 and later than the entryTime (an exitTime at or before the entryTime fails the job). Null holds to expiration.
- Name
inputs.expiryTod- Type
- string
- Description
A settlement-time filter on the expirations the legs may use: both (the default when omitted), weekly or pm for PM-settled expirations, monthly or am for AM-settled expirations. Values are case-insensitive and anything else fails the job at config load. AM settlement exists only on cash-settled index options such as SPX, so on equities and ETFs monthly/am matches nothing and weekly/pm includes every expiration, the monthlies included. The results' effectiveConfig.expiryTodFilter shows what was applied (pm, am, or null for both) and each trade row carries the expiryTod of the expiration it used.
- Name
inputs.stopLossPct- Type
- number
- Description
Stop loss as a percent of entry cost. Null, or omitting the field, runs with no stop; the engine applies no default. A value of zero or less fails the job. Ex: 200
- Name
inputs.profitTargetPct- Type
- number
- Description
Profit target as a percent of entry cost. Null for no target; a value of zero or less fails the job.
- Name
inputs.tradePct- Type
- number
- Description
Accepted for compatibility but not used by the engine, and any value sent adds a line to the results' warnings saying so; leave it out. Fills pay a fixed fraction of the bid-ask spread that depends on the leg count: 0.75 for one leg, 0.66 for two, 0.56 for three, and 0.53 for four or more.
- Name
inputs.commissionPerContract- Type
- number
- Description
Commission in dollars per contract per leg, applied on entry and exit. Defaults to 0 when omitted; negative values are rejected. Ex: 0.65
- Name
inputs.maxHoldDays- Type
- number
- Description
Maximum trading days to hold before a forced exit. Null for none.
- Name
inputs.maxConcurrentTrades- Type
- number
- Description
The most trades that may be open at once when entries overlap across days (multi-day holds via a null exitTime or maxHoldDays). 0 means unlimited. Defaults to 1 for daily entries and unlimited for signal-driven entries. With more than one open trade, daily returns are capital-weighted across the open positions and each dailyReturns row reports openTrades, deployedMargin, and deployedNotional.
- Name
inputs.groups- Type
- array
- Description
Per-group exit overrides for legs tagged with a group number: one object per group with group and any of stopLossPct, profitTargetPct, exitTime, and maxHoldDays. A field left null falls back to the top-level value. A group exitTime must be later than the entryTime (an earlier or equal one fails the job). All legs still enter together; each group exits on its own triggers, measured against that group's entry credit, and the trade log records the group and its exit reason on every leg row. Margin is held for the whole trade until the last group closes.
- Name
inputs.timeoutMinutes- Type
- number
- Description
Wall-clock cap for the whole run in minutes; a job that exceeds it ends with status timed_out. Defaults to 35 and must be between 1 and 55: the compute job itself is stopped after 60 minutes, so a longer cap fails the job at load (status error naming the bound). A job whose compute container is killed or fails anyway is marked error with the failure reason rather than staying on running. A timed_out job publishes results for the trade days it processed before the cap, marked by a warning and by diagnostics.timedOut with diagnostics.coveredThrough naming the last date covered; its statistics, PctOfTimeInMarket included, cover that prefix only, while effectiveConfig.endDate still shows the requested end date.
- Name
inputs.badPriceFilter- Type
- object
- Description
Skips entries with wide markets: maxWidthPctOfStrike and maxWidthPctOfMid as fractions. Defaults to 0.05 and 0.15.
- Name
inputs.signals- Type
- array
- Description
Signal-driven entries and exits: one object per trade with entry and exit timestamps formatted by YYYYMMDDHHMM. Null to trade on entryTime every day. Signals are sorted by entry time; signals that would exceed maxConcurrentTrades are skipped and counted in the summary's SignalsSkippedByCap. Signals are clamped to the date window: an entry outside startDate to endDate is dropped and counted in diagnostics.signalsOutsideWindow, and a position still open at endDate is closed there with the exit reason End of Backtest. An entry on a weekend or holiday is dropped and counted in signalsNonTradingDay; an exit on a weekend or holiday rolls to the next trading day and fills at that day's first usable bar, counted in signalExitsNonTradingDay. Exact duplicate entry-and-exit pairs trade once and are counted in signalsDuplicate. Each entry must be strictly before its exit, entry times fall between 0934 and 1559 and exit times between 0934 and 1600, or the job fails with the invalid signal named.
{
"email": "you@example.com",
"inputs": {
"symbol": "SPY",
"startDate": "2026-06-01",
"endDate": "2026-06-12",
"entryTime": "0934",
"exitTime": null,
"strategy": "PUT_CREDIT_SPREAD",
"legs": [
{
"optionType": "put",
"ratio": -1,
"deltaTarget": 0.3,
"deltaMin": 0.2,
"deltaMax": 0.5,
"dteTarget": 1,
"dteMin": 1,
"dteMax": 1
},
{
"optionType": "put",
"ratio": 1,
"legRelation": {
"strikeWidth": {
"refLeg": 1,
"target": 3
},
"dteDiff": {
"refLeg": 1,
"target": 0
}
}
}
],
"expiryTod": "both",
"stopLossPct": null,
"profitTargetPct": null,
"commissionPerContract": 0.65,
"maxHoldDays": null,
"badPriceFilter": {
"maxWidthPctOfStrike": 0.05,
"maxWidthPctOfMid": 0.15
},
"signals": null
}
}
Response
{
"backtestId": "aapg5gmoqs1r"
}
Backtest Status
Retrieves the status of a backtest by id, or every backtest for an email address.
By id the response is a single object; by email it is an array of the same objects. Both answer only for jobs on the account that owns the token: another account's id returns a 404 with Status not found, and email must be your own account's address, in any letter case (another address returns a 403 with a message that the email does not match the token's account). With neither id nor email the response lists your own account. Status values are queued, running, completed, error, and timed_out. While queued the object holds the submission fields; on completion it adds progress, annReturn, annSharpe, maxDrawDown, annMarginReturn, totStratPL, pctOfTimeInMarket, and a condensed legs summary; on error it adds an error field with the reason. A job still queued two hours after submission is marked error with a message to resubmit. Poll every 10 seconds or so until status is completed.
An unknown or deleted id returns a 404 with a message of Status not found; a missing parameter is a 400.
Optional attributes
- Name
id- Type
- string
- Description
The backtestId to look up. Returns a single object. Ex: aapg5gmoqs1r
- Name
email- Type
- string
- Description
Returns every backtest on this account as an array. It must be the email on your ORATS account, in lowercase.
curl -L "https://api.orats.io/backtests/intraday/status?token=my-token&id=aapg5gmoqs1r"
Response
{
"email": "you@example.com",
"execution_time": 5.86,
"id": "aapg5gmoqs1r",
"progress": 100,
"status": "completed",
"strategy": "PUT_CREDIT_SPREAD",
"submitted_at": "2026-07-16T08:04Z",
"symbol": "SPY",
"updated_at": "2026-07-16T08:05:34.794Z",
"annReturn": -31.23,
"annSharpe": -8.23,
"maxDrawDown": 1.42,
"annMarginReturn": -9935.18,
"totStratPL": -766.84,
"pctOfTimeInMarket": 100,
"legs": [
{
"dteTarget": 1,
"deltaTarget": 0.3
},
{
"legRelation": {
"strikeWidth": {
"refLeg": 1,
"target": 3
},
"dteDiff": {
"refLeg": 1,
"target": 0
}
}
}
]
}
Backtest Results
Retrieves the results of a completed backtest: summary statistics, monthly returns by year, daily returns, the per-leg trade log, and three supporting blocks: diagnostics (counts of entry days with no matching leg, no chain snapshot, or blocked by maxConcurrentTrades, trade days skipped for missing data, trades dropped for lack of a usable expiration bar, signals skipped by the cap, outside the window, on non-trading days, rolled to the next session, or duplicated, plus timedOut and coveredThrough for runs that reached the timeout, with the dates each day counter includes under skippedDates), warnings (human-readable notes such as why no trades were executed), and effectiveConfig (the resolved configuration the engine actually ran, including the applied expiryTod filter, stop and target, maxConcurrentTrades, and the timeout).
The trades array has one row per leg; tradeId groups the legs of one trade, each row carries that leg's profit (sum a trade's legs for the whole-trade profit), and margin is written on leg 1 only. The exitQuoteTime field is populated for non-expiration exits. Grouped legs also carry their group number, and each leg's exit fields show its own group's exit. Each dailyReturns row carries the day's return, marginReturn, profit, deployedMargin, deployedNotional, and openTrades. Symbol and strategy are not repeated here; read them from the status or inputs endpoints.
Units in summary: percent-scale fields (AnnReturn, AnnMarginReturn, MaxDrawDown, StratWinRate, TotStratPLPct, PctOfTimeInMarket, and the other Pct and Rtn fields) are 0 to 100; PL fields are dollars; AnnSharpe, AnnSortino, ProfitFactor (null when there are no losing trades), riskRewardAvg, and BestReturnOnRisk are decimal ratios, so a BestReturnOnRisk of 0.5 means 50 percent.
Required attributes
- Name
id- Type
- string
- Description
The backtestId to retrieve. Ex: aapg5gmoqs1r
curl -L "https://api.orats.io/backtests/intraday?token=my-token&id=aapg5gmoqs1r"
Response
{
"summary": {
"AnnReturn": -31.23,
"AnnMarginReturn": -9935.18,
"AnnReturn1Yr": -25.87,
"AnnReturn5Yrs": -25.87,
"AnnSharpe": -8.23,
"AnnSortino": -5.43,
"AnnVolatility": 3.14,
"MaxDrawDown": 1.42,
"DrawDownDays": 8,
"BestMonthRtn": -1.03,
"WorstMonthRtn": -1.03,
"BestYearRtn": -1.03,
"WorstYearRtn": -1.03,
"BestTradePL": 90.06,
"WorstTradePL": -240.62,
"BestTradePct": 98.58,
"WorstTradePct": -447.16,
"PLPerTradeAvg": -76.68,
"PLPerDayAvg": -63.9,
"PLPerTradeAvgPct": -0.10267,
"PLPerDayAvgPct": -0.08556,
"StratWinRate": 50.0,
"DaysInTradeAvg": 1.0,
"TotStratTrades": 10,
"TotStratPL": -766.84,
"TotStratPLPct": -109.52,
"CreditReceivedPerTradeAvg": 70.02,
"marginPerTradeAvg": 229.98,
"mostMarginUsed": 249.32,
"leastMarginUsed": 208.64,
"bestTradePctMargin": 43.17,
"worstTradePctMargin": -100.59,
"marginToStockPct": 0.31,
"riskRewardAvg": 0.3098,
"PctOfTimeInMarket": 100.0,
"BestReturnOnRisk": 0.43165,
"ProfitFactor": 0.31,
"MaxConcurrentTrades": 1,
"SignalsSkippedByCap": 0
},
"monthly": [
{
"year": 2026,
"Jan": null,
"Feb": null,
"Mar": null,
"Apr": null,
"May": null,
"Jun": -1.03,
"Jul": null,
"Aug": null,
"Sep": null,
"Oct": null,
"Nov": null,
"Dec": null,
"annReturn": -1.03,
"annSharpe": -8.23,
"annSortino": -5.43,
"annVolatility": 3.14
}
],
"trades": [
{
"tradeId": 1,
"date": "2026-06-01",
"entryTime": "0934",
"ticker": "SPY",
"leg": 1,
"ratio": -1,
"optionType": "put",
"strike": 755.0,
"expiry": "2026-06-01",
"expiryTod": "pm",
"dte": 1,
"tradeOptPx": 1.33,
"delta": -0.3497,
"entryStockPx": 755.67,
"exitDate": "2026-06-01",
"exitTime": "1600",
"exitQuoteTime": null,
"exitStockPx": 758.54,
"exitOptionPx": 0.0,
"profit": 132.69,
"exitReason": "Expiration",
"margin": 221.32
},
{
"tradeId": 1,
"date": "2026-06-01",
"entryTime": "0934",
"ticker": "SPY",
"leg": 2,
"ratio": 1,
"optionType": "put",
"strike": 752.0,
"expiry": "2026-06-01",
"expiryTod": "pm",
"dte": 1,
"tradeOptPx": 0.55,
"delta": -0.0391,
"entryStockPx": 755.67,
"exitDate": "2026-06-01",
"exitTime": "1600",
"exitQuoteTime": null,
"exitStockPx": 758.54,
"exitOptionPx": 0.0,
"profit": -55.31,
"exitReason": "Expiration"
}
],
"dailyReturns": [
{
"date": "2026-06-01",
"return": 0.001024,
"marginReturn": 0.349629,
"profit": 77.38,
"deployedMargin": 221.32,
"deployedNotional": 75567,
"openTrades": 1
},
{
"date": "2026-06-02",
"return": 0.000652,
"marginReturn": 0.198059,
"profit": 49.21,
"deployedMargin": 248.46,
"deployedNotional": 75475,
"openTrades": 1
}
],
"diagnostics": {
"signalsSkippedByCap": 0,
"tradeDaysSkippedNoData": 0,
"entryDaysNoChain": 0,
"entryDaysNoMatch": 0
},
"warnings": [],
"effectiveConfig": {
"symbol": "SPY",
"startDate": "2026-06-01",
"endDate": "2026-06-12",
"entryTime": "0934",
"exitTime": null,
"strategy": "PUT_CREDIT_SPREAD",
"expiryTod": "both",
"expiryTodFilter": null,
"stopLossPct": null,
"profitTargetPct": null,
"maxHoldDays": null,
"commissionPerContract": 0.65,
"timeoutMinutes": 35,
"maxConcurrentTrades": 1,
"signalCount": 0,
"legs": [
{
"optionType": "put",
"ratio": -1,
"deltaTarget": -0.3,
"deltaMin": -0.5,
"deltaMax": -0.2,
"dteTarget": 1,
"dteMin": 1,
"dteMax": 1,
"legRelation": null,
"group": null
},
{
"optionType": "put",
"ratio": 1,
"deltaTarget": null,
"deltaMin": null,
"deltaMax": null,
"dteTarget": null,
"dteMin": null,
"dteMax": null,
"legRelation": {
"strikeWidth": {"refLeg": 1, "target": 3, "min": null, "max": null},
"dteDiff": {"refLeg": 1, "target": 0, "min": null, "max": null}
},
"group": null
}
],
"groups": null
}
}
Backtest Inputs
Retrieves the original inputs a backtest was submitted with, exactly as they were sent. Useful for re-running or editing a backtest. An unknown or deleted id returns a 404 with Input not found; a missing id is a 400.
Required attributes
- Name
id- Type
- string
- Description
The backtestId to retrieve. Ex: aapg5gmoqs1r
curl -L "https://api.orats.io/backtests/intraday/input?token=my-token&id=aapg5gmoqs1r"
Response
{
"symbol": "SPY",
"startDate": "2026-06-01",
"endDate": "2026-06-12",
"entryTime": "0934",
"exitTime": null,
"strategy": "PUT_CREDIT_SPREAD",
"legs": [
{
"optionType": "put",
"ratio": -1,
"deltaTarget": 0.3,
"deltaMin": 0.2,
"deltaMax": 0.5,
"dteTarget": 1,
"dteMin": 1,
"dteMax": 1
},
{
"optionType": "put",
"ratio": 1,
"legRelation": {
"strikeWidth": {
"refLeg": 1,
"target": 3
},
"dteDiff": {
"refLeg": 1,
"target": 0
}
}
}
],
"expiryTod": "both",
"stopLossPct": null,
"profitTargetPct": null,
"tradePct": 0.66,
"commissionPerContract": 0.65,
"maxHoldDays": null,
"badPriceFilter": {
"maxWidthPctOfStrike": 0.05,
"maxWidthPctOfMid": 0.15
},
"signals": null
}
Save a Note
Saves a note on a backtest. The note is returned on the status endpoint as the notes field and is kept across re-runs. Sending an empty string clears the note.
Required attributes
- Name
id- Type
- string
- Description
The backtestId to annotate. Ex: aapg5gmoqs1r
- Name
notes- Type
- string
- Description
The note text, up to 10,000 characters.
{
"id": "aapg5gmoqs1r",
"notes": "SPY 0DTE put credit spread test"
}
Response
{
"message": "Notes updated successfully",
"id": "aapg5gmoqs1r"
}
Delete a Backtest
Deletes a backtest, its results and its stored input. Only the account that submitted the job (or an id it has access to) can delete it; an unknown id or another account's id returns a 404 with a message of Backtest not found, while deleting an already-deleted id still returns success. After deletion the status, results, input and notes endpoints all answer not found for the id, and a run still finishing in the engine does not restore it.
Required attributes
- Name
id- Type
- string
- Description
The backtestId to delete. Ex: aapg5gmoqs1r
curl -L -X DELETE "https://api.orats.io/backtests/intraday?token=my-token&id=aapg5gmoqs1r"
Response
{
"message": "Backtest deleted successfully",
"id": "aapg5gmoqs1r"
}