# Intraday Backtester API

Run custom intraday option backtests with one-minute precision. Define a strategy with entry and exit times, delta or strike-width leg targets, stops, and profit targets, then backtest it against intraday options data going back to October 2020. The API powers the [Intraday Custom Backtester](https://orats.com/intraday-backtester) in the ORATS dashboard.

This is an asynchronous job API: submit a backtest, poll its status until it completes (a two week single-symbol run typically finishes in under a minute), then fetch the results. Authentication uses the token query parameter on every request.

Available strategies: 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, and DOUBLE_CALENDAR.

Product overview and pricing for this and the other Tools APIs live on the [Tools APIs page](https://orats.com/tools-apis).

Base URL: `https://api.orats.io/backtests/intraday`

---

## Submit a Backtest

`POST https://api.orats.io/backtests/intraday`

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 rejected submission returns a message field instead of a backtestId. Jobs with invalid inputs can remain queued rather than reaching an error status, so give up on jobs that stay queued well past their expected runtime.

### Required attributes

- `email` (string): Your email address, used to list your backtests on the status endpoint.
- `inputs` (object): The backtest definition. Its fields are documented below.
- `inputs.symbol` (string): The underlying to backtest. Ex: SPY
- `inputs.startDate` (string): The first trade date, formatted by YYYY-MM-DD. Must be 2020-10-01 or later. Ex: 2026-06-01
- `inputs.entryTime` (string): Entry time of day, formatted by HHMM (in EST) within market hours 0930 to 1600. Ex: 0934
- `inputs.strategy` (string): The strategy, from the list above. Ex: PUT_CREDIT_SPREAD
- `inputs.legs` (array): 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), and a DTE band (dteTarget, dteMin, dteMax) where DTE 1 is same-day (0DTE), 2 is the next trading day, and the maximum target is 30.

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).

### Optional attributes

- `inputs.endDate` (string): The last trade date, formatted by YYYY-MM-DD. Omit for the most recent data.
- `inputs.exitTime` (string): Exit time of day, formatted by HHMM (in EST). Null holds to expiration.
- `inputs.expiryTod` (string): Filters AM or PM settled expirations: both, am, or pm. Defaults to both.
- `inputs.stopLossPct` (number): Stop loss as a percent of entry cost. Send null explicitly to disable; when the field is omitted the engine applies a 200 percent stop.
- `inputs.profitTargetPct` (number): Profit target as a percent of entry cost. Null for no target.
- `inputs.tradePct` (number): Slippage as a fraction of the bid-ask spread paid on each side, 0 to 1. Ex: 0.66
- `inputs.commissionPerContract` (number): Commission in dollars per contract per leg, applied on entry and exit. Ex: 0.65
- `inputs.maxHoldDays` (number): Maximum trading days to hold before a forced exit. Null for none.
- `inputs.badPriceFilter` (object): Skips entries with wide markets: maxWidthPctOfStrike and maxWidthPctOfMid as fractions. Defaults to 0.05 and 0.15.
- `inputs.signals` (array): Signal-driven entries and exits: one object per trade with entry and exit timestamps formatted by YYYYMMDDHHMM. Null to trade on entryTime every day.

**cURL**

```bash
curl -L "https://api.orats.io/backtests/intraday?token=my-token" \
  -H "Content-Type: application/json" \
  -d '{
    "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,
      "tradePct": 0.66,
      "commissionPerContract": 0.65,
      "maxHoldDays": null,
      "badPriceFilter": {"maxWidthPctOfStrike": 0.05, "maxWidthPctOfMid": 0.15},
      "signals": null
    }
  }'
```

**Response**

```json
{
  "backtestId": "aapg5gmoqs1r"
}
```

---

## Backtest Status

`GET https://api.orats.io/backtests/intraday/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. Status values are queued, running, completed, and error. While queued the object holds the submission fields; on completion it adds progress, annReturn, annSharpe, maxDrawDown, and a condensed legs summary. Poll every 10 seconds or so until status is completed.

An unknown id returns a message of Status not found with a 200 status.

### Optional attributes

- `id` (string): The backtestId to look up. Returns a single object. Ex: aapg5gmoqs1r
- `email` (string): Returns every backtest submitted with this email as an array.

**cURL**

```bash
curl -L "https://api.orats.io/backtests/intraday/status?token=my-token&id=aapg5gmoqs1r"
```

**Response**

```json
{
  "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,
  "legs": [
    {
      "dteTarget": 1,
      "deltaTarget": 0.3
    },
    {
      "legRelation": {
        "strikeWidth": {
          "refLeg": 1,
          "target": 3
        },
        "dteDiff": {
          "refLeg": 1,
          "target": 0
        }
      }
    }
  ]
}
```

---

## Backtest Results

`GET https://api.orats.io/backtests/intraday`

Retrieves the results of a completed backtest: summary statistics, monthly returns by year, daily returns, and the per-leg trade log.

The trades array has one row per leg; 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. Symbol and strategy are not repeated here; read them from the status or inputs endpoints.

### Required attributes

- `id` (string): The backtestId to retrieve. Ex: aapg5gmoqs1r

**cURL**

```bash
curl -L "https://api.orats.io/backtests/intraday?token=my-token&id=aapg5gmoqs1r"
```

**Response**

```json
{
  "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
  },
  "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": [
    {
      "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
    },
    {
      "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
    },
    {
      "date": "2026-06-02",
      "return": 0.000652,
      "marginReturn": 0.198059
    }
  ]
}
```

---

## Backtest Inputs

`GET https://api.orats.io/backtests/intraday/input`

Retrieves the original inputs a backtest was submitted with, exactly as they were sent. Useful for re-running or editing a backtest.

### Required attributes

- `id` (string): The backtestId to retrieve. Ex: aapg5gmoqs1r

**cURL**

```bash
curl -L "https://api.orats.io/backtests/intraday/input?token=my-token&id=aapg5gmoqs1r"
```

**Response**

```json
{
  "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

`POST https://api.orats.io/backtests/intraday/notes`

Saves a note on a backtest. The note is returned on the status endpoint as the notes field and survives re-runs. Sending an empty string clears the note.

### Required attributes

- `id` (string): The backtestId to annotate. Ex: aapg5gmoqs1r
- `notes` (string): The note text, up to 10,000 characters.

**cURL**

```bash
curl -L "https://api.orats.io/backtests/intraday/notes?token=my-token" \
  -H "Content-Type: application/json" \
  -d '{"id": "aapg5gmoqs1r", "notes": "SPY 0DTE put credit spread test"}'
```

**Response**

```json
{
  "message": "Notes updated successfully",
  "id": "aapg5gmoqs1r"
}
```

---

## Delete a Backtest

`DELETE https://api.orats.io/backtests/intraday`

Deletes a backtest and its results. Deletion is idempotent: deleting an already-deleted id still returns success. After deletion the status endpoint returns a message of Status not found for the id.

### Required attributes

- `id` (string): The backtestId to delete. Ex: aapg5gmoqs1r

**cURL**

```bash
curl -L -X DELETE "https://api.orats.io/backtests/intraday?token=my-token&id=aapg5gmoqs1r"
```

**Response**

```json
{
  "message": "Backtest deleted successfully",
  "id": "aapg5gmoqs1r"
}
```
