# Expected move and close odds

> SPX's expected move left to the close (twice the ATM straddle) and the 0DTE chain's own odds for the close, as READOUTS shows them; with day, the same at a minute of a finished session against what the close did.

Source: https://firmtape.com/docs/developers/live/expected-move

SPX's expected move left to the close (twice the ATM straddle) and the 0DTE chain's own odds for the close, as READOUTS shows them; with `day`, the same at a minute of a finished session against what the close did.

`GET /api/v1/expected_move`

Plan LIVE · Key scope live · Limit 120 a minute per key; a 304 is free · Version 1.0.0 · Since 2026-10-03

**This endpoint serves:** [LIVE: The expected move](https://firmtape.com/docs/live#expected-move), [LIVE: READOUTS drawer](https://firmtape.com/docs/live#readouts), [LIVE: Levels API](https://firmtape.com/docs/live#levels-api). **Plan:** [LIVE](https://firmtape.com/features/live). LIVE: today's session every second, alerts, ASK, the levels API and the platform integrations, and 5 LAB credits a month. Every finished session is free in REPLAY.

## Parameters

| Name | What it does | Default | Error |
| --- | --- | --- | --- |
| `sym` query, SPX | SPX only. | SPX | spx_only |
| `day` query, string | A finished session, `YYYY-MM-DD`; leave it out for today. | today | bad_day, not_finished |
| `time` query, string | With `day`: the ET minute, `HH:MM`, 09:31 to 15:59. | 10:00 | bad_time |
| `errors` query, 200 | `200`: an error answers 200 with the same body and the real status in `X-FirmTape-Status`, for clients that cannot read a status. |  |  |
| `key` query, string | Legacy: the key in the address, for platforms that cannot send headers. Prefer the header. |  |  |

## Response

The expected move and the odds; with `day`, the `outcome` too.

| Field | Meaning | Unit |
| --- | --- | --- |
| `sym` ("SPX") | `SPX`. |  |
| `day` (string) | The ET trading day. |  |
| `session` (string) | `open`, `closed` or `stale`. |  |
| `as_of` (string) | The reading's time, ISO 8601 in New York time. |  |
| `as_of_unix` (integer) | The same, Unix seconds. |  |
| `age_s` (integer) | Seconds since the reading. | seconds |
| `price` (number \| null) | SPX at the reading. | index points |
| `minutes_left` (integer) | Minutes to 16:00 ET; 0 when closed. | minutes |
| `left_to_close` (object \| null) | Twice the ATM straddle from here to 16:00; null when closed or unpriced. |  |
| `left_to_close.points` (number \| null) | The expected move left. | index points |
| `left_to_close.low` (number \| null) | Price minus it. | index points |
| `left_to_close.high` (number \| null) | Price plus it. | index points |
| `left_to_close.source` (string) | Where the straddle came from. |  |
| `left_to_close.note` (string) | What it measured over the archive. |  |
| `close_odds` (object \| null) | The 0DTE chain's odds for the close, with 30 or more minutes left; null otherwise. |  |
| `close_odds.frame_t` (string \| null) | The ladder frame's ET minute. |  |
| `close_odds.range_80` (array) | The 10th and 90th percentile of the close. | index points |
| `close_odds.middle_half` (array) | The 25th and 75th percentile. | index points |
| `close_odds.median` (number \| null) | The median close. | index points |
| `close_odds.beyond_pct` (object) | Odds, percent, that the close ends beyond each level, away from spot. |  |
| `close_odds.beyond_pct.flip` (number \| null) | Beyond the flip. | percent |
| `close_odds.beyond_pct.call_wall` (number \| null) | Beyond the call wall. | percent |
| `close_odds.beyond_pct.put_wall` (number \| null) | Beyond the put wall. | percent |
| `close_odds.note` (string) | How the odds are made and how they held. |  |
| `close_odds_note` (string) | Why `close_odds` is absent. |  |
| `levels` (object) | The volume book's levels at the reading. |  |
| `levels.flip` (number \| null) | Flip. | index points |
| `levels.call_wall` (number \| null) | Call wall. | index points |
| `levels.put_wall` (number \| null) | Put wall. | index points |
| `levels.book` (string) | `volume`. |  |
| `levels_at` (string) | A finished day: the levels minute. |  |
| `last_second` (object) | A finished day: the levels at the last second. |  |
| `last_second.flip` (number \| null) | Flip. | index points |
| `last_second.call_wall` (number \| null) | Call wall. | index points |
| `last_second.put_wall` (number \| null) | Put wall. | index points |
| `outcome` (object) | With `day`: what the close did. |  |
| `outcome.close` (number \| null) | The close. | index points |
| `outcome.close_as_of` (string) | Its time. |  |
| `outcome.inside_left_to_close` (boolean \| null) | Whether the close landed inside the expected move. |  |
| `outcome.inside_range_80` (boolean \| null) | Inside the 80% range. |  |
| `outcome.inside_middle_half` (boolean \| null) | Inside the middle half. |  |
| `outcome.vs_median` (number \| null) | Close minus the median. | index points |

## Errors

| Status | Code | What to do |
| --- | --- | --- |
| 400 | `spx_only`, `bad_day`, `not_finished`, `bad_time` | A parameter is wrong. Send `sym=SPX` or leave it out. |
| 401 | `no_key`, `bad_key` | No key, or an unknown or revoked one. Send `Authorization: Bearer <key>` with a key of the product's scope. |
| 403 | `live_required` | No LIVE plan. Start or renew LIVE; `pricing` names the page. |
| 404 | `no_data` | No reading for that day. Ask again after `next_update_s`, or pick another day. |
| 429 | `rate_limited`, `too_many_places` | Over a limit. Wait `Retry-After` seconds; poll with `If-None-Match`, a 304 is free. |

## Caching

Answers carry an `ETag`: send it back as `If-None-Match` and an unchanged reading answers 304 with no body, not counted against the limit.

## Request

```sh
curl \
  -H "Authorization: Bearer $FIRMTAPE_LIVE_KEY" \
  "https://firmtape.com/api/v1/expected_move"
```

```python
import os, requests

H = {"Authorization": f"Bearer {os.environ['FIRMTAPE_LIVE_KEY']}"}
r = requests.get("https://firmtape.com/api/v1/expected_move", headers=H)
print(r.status_code, r.text[:500])
```

```js
const r = await fetch("https://firmtape.com/api/v1/expected_move", {
  method: "GET",
  headers: { Authorization: `Bearer ${process.env.FIRMTAPE_LIVE_KEY}` },
});
console.log(r.status, await r.text());
```

## Example response

```json
{
  "sym": "SPX",
  "day": "2026-09-30",
  "session": "open",
  "as_of": "2026-09-30T14:00:04-04:00",
  "as_of_unix": 1790791204,
  "age_s": 1,
  "price": 6712.14,
  "minutes_left": 120,
  "left_to_close": {
    "points": 24.6,
    "low": 6687.54,
    "high": 6736.74,
    "source": "the session's ATM straddle",
    "note": "twice the ATM straddle from here to 16:00; spot plus or minus it held 91% of closes over 1,108 sessions"
  },
  "close_odds": {
    "frame_t": "14:00",
    "range_80": [
      6694.1,
      6729.8
    ],
    "middle_half": [
      6703.2,
      6720.9
    ],
    "median": 6712.3,
    "beyond_pct": {
      "flip": 18.2,
      "call_wall": 3.1,
      "put_wall": 4.4
    },
    "note": "the 0DTE chain's own odds for the close"
  },
  "levels": {
    "flip": 6692,
    "call_wall": 6742,
    "put_wall": 6662,
    "book": "volume"
  }
}
```

## Changes

- 2026-10-03: Added.
