Reference
Every method and route.
Every public method of the optionspace Python package (version 0.9.0) and every route of the REST API: arguments, defaults and what comes back. Generated from the code. In Python, help(osp.Client.smile) (or client.smile? in Jupyter) shows the same description where you work.
Contents
- Python: connect:
Client - Python: account and background jobs:
entitlements()·prices()·job() - Python: discover what exists:
symbols()·snapshots()·expirations() - Python: data lookups:
smile()·chain()·atm_intraday()·term_structure()·tenor_history()·rates()·iv_rv()·expiry_history()·contract_history()·event_outcomes()·event_runup()·volume_summary()·contract_volume()·surface() - Python: research calls:
expiry_metrics()·exposure_distribution()·clean_atm_history()·earnings_history()·earnings_backtest() - Python: saved keys:
login()·whoami()·logout()·LoginStatus - Python: working with results:
curve_frame()·FrameCache()·cache.get()·cache.put() - Python: charts:
smile_chart()·compare_chart()·atm_chart()·term_chart()·history_chart()·term_comparison_chart()·skew_comparison_chart()·exposure_distribution_chart()·clean_atm_chart()·earnings_comparison_chart()·earnings_runup_chart()·earnings_backtest_chart()·backtest_path_chart()·save() - Python: session movies:
DayMovie()·movie.interactive()·movie.write_html()·movie.encode() - Python: errors: every error and when it is raised
- Response fields: shared rules · common fields · fit quality · quote availability
- REST: account and discovery:
/entitlements·/symbols·/snapshots·/expirations - REST: data lookups:
/smile·/term-structure·/chain·/atm-timeseries·/tenor-history·/rates·/iv-rv·/expiry-history·/contract-history·/event-outcomes·/event-runup·/volume-summary·/contract-volume·/surface - REST: research routes:
/expiry-metrics·/exposure-distribution·/clean-atm-history·/earnings-history·/earnings-backtest· estimate-only answer - REST: background jobs:
/jobs/{job_id}·DELETE /jobs/{job_id}· accepted (202) answer
How to read this page
- Dates are
YYYY-MM-DD; times areHH:MM, US Eastern.EODis the last snapshot at or before the official close that your plan covers. - Volatilities are decimals (
0.20= 20%). Prices are USD per share. - "required" means the argument has no default. In Python, arguments after
*are passed by name:client.smile("SPY", "2025-04-10", expiration="2025-04-17"). - Credits: calls count against your plan's daily credit allowance.
client.prices()gives what each call costs now. See credits.
Python: connect
Every data method is called on a Client. Create one per script or notebook.
osp.Client()
A connection to the OptionSpace data API, using your saved key.
osp.Client(
api_key=None,
*,
base_url=None,
profile=None,
timeout=60.0,
job_timeout=1800.0,
)
| Argument | Type | Default | Meaning |
|---|---|---|---|
api_key |
str or None | None |
Your sk_os_... key. None uses OPTIONSPACE_API_KEY, then the key saved by optionspace login. It never prompts: with no key found it raises CredentialError saying what to run. |
base_url |
str or None | None |
The server to send requests to, for a key passed here or set in the environment (or set OPTIONSPACE_BASE_URL). A saved key works only with the server it was saved for. |
profile |
str or None | None |
Which saved login to use. None uses OPTIONSPACE_PROFILE, else "default". Ignored when a key is passed here or set in the environment. |
timeout |
float | 60.0 |
Seconds to wait for each request. |
job_timeout |
float | 1800.0 |
Total seconds to wait for a research call that became a background job. After that, JobTimeout gives you the job_id to collect it later with client.job(). |
Returns: A client. client.credential_source says where the key came from ("argument", "env", "keyring" or "file"), client.profile names the saved login used, and client.base_url is the only server the key is sent to.
Requests: None: creating a client makes no API request.
Python: account and background jobs
Your account, the price list and background jobs. These answer even when today's allowance is used up.
client.entitlements()
Your plan, what your key can access, your daily limits and today's usage, with the price list.
client.entitlements() -> dict
Returns: A dictionary: tier, status, entitlements (your grants), key_id, limits (credits_per_day, rows_per_day, concurrent), usage (credits_today, credits_reserved: credits held by research calls still running, rows_today, ...) and prices. Always answers, even when today's allowance is used up.
REST: GET /entitlements
client.prices()
What each call costs, in credits.
client.prices() -> dict
Returns: A dictionary: lookups (the price of each lookup route), research (how research routes are charged), version and effective (the date the list took effect).
Requests: One account check (it reads the price list from entitlements()).
client.job()
Collect a research call that became a background job.
client.job(job_id, *, wait=True, timeout=None)
| Argument | Type | Default | Meaning |
|---|---|---|---|
job_id |
str | required | The id from JobTimeout.job_id, or from a REST 202 response. |
wait |
bool | True |
True waits until the job finishes and returns its answer. False returns the job's state now. |
timeout |
float or None | None |
Seconds to wait. None uses the client's job_timeout. |
Returns: With wait=True, the same result the research call would have returned. With wait=False, a dictionary whose status is queued, running, done, failed or canceled. Results are kept for one hour after the job finishes.
REST: GET /jobs/{job_id}
Python: discover what exists
Check what exists before a larger pull: symbols, the snapshot times your plan covers, and the listed expirations.
client.symbols()
Every symbol in the product, with the sessions it covers.
client.symbols() -> DataFrame
Returns: A DataFrame with symbol, start_date, end_date and n_days. When your access is limited to a sample window, .attrs["sample_window"] gives it.
REST: GET /symbols
DataFrame columns: symbol, start_date, end_date, n_days. .attrs keys: symbol, root, data_date, snapshot_time, session_close, granularity, returned_rows, notes, sample_window. Each means the same as the REST field of the same name (GET /symbols).
client.snapshots()
The snapshot times your key can retrieve for one session.
client.snapshots(symbol, data_date) -> DataFrame
| Argument | Type | Default | Meaning |
|---|---|---|---|
symbol |
str | required | The product symbol, such as "SPY" or "SPX". client.symbols() lists them. |
data_date |
date or str | required | The trading session: "YYYY-MM-DD" text or a datetime.date. |
Returns: A DataFrame with symbol, data_date, snapshot_time, after_close (a time after an early close) and is_eod (the time "EOD" resolves to). .attrs holds eod, session_close and granularity.
REST: GET /snapshots
DataFrame columns: symbol, data_date, snapshot_time, after_close, is_eod. .attrs keys: symbol, root, data_date, snapshot_time, session_close, granularity, returned_rows, notes, eod. Each means the same as the REST field of the same name (GET /snapshots); the ones that differ:
Columns that differ from REST (2)
| Column or attr | Type and unit | Can be empty | Meaning | Example |
|---|---|---|---|---|
after_close |
true or false | True when the time falls after an early close. | false |
|
is_eod |
true or false | True on the time EOD resolves to. |
false |
client.expirations()
The expirations listed at one snapshot.
client.expirations(
symbol,
data_date,
*,
snapshot="EOD",
root=None,
) -> DataFrame
| Argument | Type | Default | Meaning |
|---|---|---|---|
symbol |
str | required | The product symbol, such as "SPY" or "SPX". client.symbols() lists them. |
data_date |
date or str | required | The trading session: "YYYY-MM-DD" text or a datetime.date. |
snapshot |
str | "EOD" |
"EOD" (the last snapshot at or before the official close that your plan covers) or a time "HH:MM" in US Eastern time. The time used comes back as snapshot_time. |
root |
str or None | None |
The listed root, for a symbol that lists under several (SPX lists as SPX and SPXW). None lets the server choose, and the result names the root it served. See SPX and SPXW. |
Returns: A DataFrame with one row per root and expiration: root, expiration, dte (calendar days to expiry). An expiry whose fit did not pass our quality checks at that snapshot is not listed; .attrs["withheld_for_quality"] names those. When every expiry was withheld, NotFoundError with reason "withheld_for_quality".
REST: GET /expirations
DataFrame columns: symbol, root, data_date, snapshot_time, expiration, dte. .attrs keys: symbol, root, data_date, snapshot_time, session_close, granularity, returned_rows, notes, withheld_for_quality. Each means the same as the REST field of the same name (GET /expirations).
Python: data lookups
Each lookup is one request. A chain() over several snapshots makes several requests and says so before it starts.
client.smile()
The fitted smile for one expiry at one snapshot.
client.smile(
symbol,
data_date,
*,
expiration,
snapshot="EOD",
root=None,
) -> DataFrame
| Argument | Type | Default | Meaning |
|---|---|---|---|
symbol |
str | required | The product symbol, such as "SPY" or "SPX". client.symbols() lists them. |
data_date |
date or str | required | The trading session: "YYYY-MM-DD" text or a datetime.date. |
expiration |
date or str | required | The option expiration date: "YYYY-MM-DD" or a datetime.date. client.expirations() lists the ones that exist. |
snapshot |
str | "EOD" |
"EOD" (the last snapshot at or before the official close that your plan covers) or a time "HH:MM" in US Eastern time. The time used comes back as snapshot_time. |
root |
str or None | None |
The listed root, for a symbol that lists under several (SPX lists as SPX and SPXW). None lets the server choose, and the result names the root it served. See SPX and SPXW. |
Returns: A DataFrame with one row per strike and side: fitted_vol (decimal), z, the greeks, is_wing_extrapolated, and on past sessions the quotes (bid, ask, mid, iv_bid, iv_ask, iv_mid). Quote columns are absent until the quotes are released, at the next session's 09:30 ET open. Expiry values such as forward, atm_vol, rate, q, T_vol, rmse and the quality block are in .attrs; the fitted curve is .attrs["fitted_curve"] (see curve_frame()).
REST: GET /smile
DataFrame columns: symbol, root, data_date, snapshot_time, expiration, strike, put_call, fitted_vol, z, delta, gamma, vega, theta, theta_1d, theta_next_open, rho, vanna, volga, is_otm, is_wing_extrapolated, iv_bid, iv_ask, iv_mid, bid, ask, mid, volume, open_interest, quote_state. .attrs keys: symbol, root, data_date, snapshot_time, session_close, expiration, dte, forward, spot, s_implied, rate, q, T, T_vol, T_borrow, atm_vol, rmse, n_options, n_fitted, gated, granularity, notes, z_lo, z_hi, fitted_curve. Each means the same as the REST field of the same name (GET /smile); the ones that differ:
Columns that differ from REST (3)
| Column or attr | Type and unit | Can be empty | Meaning | Example |
|---|---|---|---|---|
strike |
number, USD | Strike price (the REST strikes list, one row per strike and side). |
520.0 |
|
put_call |
text | Call or put: which of the REST calls and puts lists the row came from. Values: call, put. |
"call" |
|
fitted_vol |
number, decimal vol | Fitted implied volatility (the REST fitted list). |
0.2144 |
client.chain()
Every fitted option for a session at one snapshot, across all expiries unless filtered.
client.chain(
symbol,
data_date,
snapshot="EOD",
*,
snapshots=None,
root=None,
expiration=None,
strike_min=None,
strike_max=None,
put_call=None,
columns=None,
check_budget=True,
) -> DataFrame
| Argument | Type | Default | Meaning |
|---|---|---|---|
symbol |
str | required | The product symbol, such as "SPY" or "SPX". client.symbols() lists them. |
data_date |
date or str | required | The trading session: "YYYY-MM-DD" text or a datetime.date. |
snapshot |
str | "EOD" |
One snapshot: "EOD" or "HH:MM" US Eastern time. Pass either snapshot or snapshots, not both. |
snapshots |
str or list of str or None | None |
Several snapshots: a list of "HH:MM" times, or "all" for every snapshot your key can retrieve. Each one is a separate request. |
root |
str or None | None |
Only this listed root. None returns every root the symbol lists; each row names its root. |
expiration |
date or str or None | None |
Only this expiration. None returns every expiration. |
strike_min |
float or None | None |
Only strikes at or above this price (USD). |
strike_max |
float or None | None |
Only strikes at or below this price (USD). |
put_call |
str or None | None |
"call" or "put" only. None returns both. |
columns |
str or list of str or None | None |
Return only these columns: a list, or one comma-separated text. Identity columns are always included. Quote columns exist for past sessions only. Allowed: data_date, snapshot_time, symbol, expiration, strike, put_call, company_name, sector, security_type, dte, spot, forward, q, r, T_vol, z, fitted_vol, fitted_price, delta, gamma, vega, theta, rho, vanna, volga, is_wing_extrapolated, bid, ask, bid_size, ask_size, iv_bid, iv_ask, iv_mid, theta_1d, theta_next_open. On request (not in the default columns), for past sessions only: volume, open_interest. volume is the contracts traded so far that session up to the snapshot minute; the official end-of-day figure is in /contract-volume. Filtering by expiration keeps a large chain fast. |
check_budget |
bool | True |
Before a pull over several snapshots, check that its credits and projected rows fit what is left today, and raise QuotaExceeded (nothing more fetched) if they do not. False skips the check. |
Returns: A DataFrame of option rows: identity columns (symbol the product, root the listed root, data_date, snapshot_time, expiration, strike, put_call), then the fitted values and greeks, and quotes on past sessions. .attrs["budget"] says what a multi-snapshot pull cost. One snapshot over the per-request row limit raises ResponseTooLarge (never a cut-off frame): narrow it with the filters.
Requests: One per snapshot.
REST: GET /chain
DataFrame columns: symbol, root, data_date, snapshot_time, expiration, strike, put_call, company_name, sector, security_type, dte, spot, forward, q, r, T_vol, z, fitted_vol, fitted_price, delta, gamma, vega, theta, rho, vanna, volga, is_wing_extrapolated, bid, ask, bid_size, ask_size, iv_bid, iv_ask, iv_mid, theta_1d, theta_next_open, quote_state. .attrs keys: symbol, root, data_date, session_close, granularity, columns, snapshot_time, snapshots, returned_rows, notes, budget. Each means the same as the REST field of the same name (GET /chain); the ones that differ:
Columns that differ from REST (4)
| Column or attr | Type and unit | Can be empty | Meaning | Example |
|---|---|---|---|---|
symbol |
text | The product symbol you asked for (in REST rows, symbol is the root). |
"SPX" |
|
root |
text | The listed root of the option (the REST row's symbol). |
"SPXW" |
|
.attrs["snapshots"] |
list of text, HH:MM, US Eastern | The snapshots fetched, in order. | ||
.attrs["budget"] |
object | What a multi-request call cost: the requests and credits used, and (for chain()) the rows fetched and projected. |
client.atm_intraday()
At-the-money volatility through one session for one expiry.
client.atm_intraday(
symbol,
data_date,
*,
expiration,
root=None,
) -> DataFrame
| Argument | Type | Default | Meaning |
|---|---|---|---|
symbol |
str | required | The product symbol, such as "SPY" or "SPX". client.symbols() lists them. |
data_date |
date or str | required | The trading session: "YYYY-MM-DD" text or a datetime.date. |
expiration |
date or str | required | The option expiration date: "YYYY-MM-DD" or a datetime.date. client.expirations() lists the ones that exist. |
root |
str or None | None |
The listed root. None serves the root that lists the expiry, or SPXW on the SPX monthlies where both do. Roots are never mixed; .attrs["root"] names the one served. |
Returns: A DataFrame with one row per snapshot your plan covers: snapshot_time, sigma_atm (decimal), forward, n_obs (options in the fit). Minutes where this expiry's fit did not pass our quality checks are left out and listed in .attrs["withheld_snapshots"].
REST: GET /atm-timeseries
DataFrame columns: symbol, root, data_date, snapshot_time, expiration, sigma_atm, forward, n_obs. .attrs keys: symbol, root, data_date, snapshot_time, session_close, granularity, returned_rows, notes, expiration, withheld_snapshots. Each means the same as the REST field of the same name (GET /atm-timeseries); the ones that differ:
Columns that differ from REST (1)
| Column or attr | Type and unit | Can be empty | Meaning | Example |
|---|---|---|---|---|
expiration |
text, YYYY-MM-DD | The expiry. | "2025-04-17" |
client.term_structure()
At-the-money volatility for every listed expiry at one snapshot.
client.term_structure(
symbol,
data_date,
*,
snapshot="EOD",
root=None,
max_expiries=60,
) -> DataFrame
| Argument | Type | Default | Meaning |
|---|---|---|---|
symbol |
str | required | The product symbol, such as "SPY" or "SPX". client.symbols() lists them. |
data_date |
date or str | required | The trading session: "YYYY-MM-DD" text or a datetime.date. |
snapshot |
str | "EOD" |
"EOD" (the last snapshot at or before the official close that your plan covers) or a time "HH:MM" in US Eastern time. The time used comes back as snapshot_time. |
root |
str or None | None |
The listed root. Needed when the symbol lists under several roots that day: a term structure uses one root. |
max_expiries |
int | 60 |
The most expiries to fetch, from 1 to 60. With more listed, it raises InvalidRequestError rather than leave any out. |
Returns: A DataFrame with one row per expiry, shortest first: symbol, root, data_date, snapshot_time, expiration, dte, atm_vol, forward, z_lo, z_hi. .attrs["budget"] gives the requests and credits used.
Requests: One for the expirations lookup plus one per expiry (one smile each). Checked against what is left today before the smiles are fetched.
REST: GET /term-structure
DataFrame columns: symbol, root, data_date, snapshot_time, expiration, dte, atm_vol, forward, z_lo, z_hi. .attrs keys: symbol, root, data_date, snapshot_time, session_close, granularity, returned_rows, budget. Each means the same as the REST field of the same name (GET /smile, GET /expirations); the ones that differ:
Columns that differ from REST (2)
| Column or attr | Type and unit | Can be empty | Meaning | Example |
|---|---|---|---|---|
atm_vol |
number, decimal vol | The expiry's at-the-money volatility (the atm_vol of its smile). |
0.1423 |
|
.attrs["budget"] |
object | What a multi-request call cost: the requests and credits used, and (for chain()) the rows fetched and projected. |
client.tenor_history()
Constant-maturity volatility measures through time for one tenor.
client.tenor_history(
symbol,
tenor_days,
start,
end,
*,
snapshot=None,
root=None,
fields=None,
sampling=None,
) -> DataFrame
| Argument | Type | Default | Meaning |
|---|---|---|---|
symbol |
str | required | The product symbol, such as "SPY" or "SPX". client.symbols() lists them. |
tenor_days |
int | required | The constant maturity in calendar days: one of 30, 60, 90, 180, 360. |
start |
date or str | required | The first session of the range: "YYYY-MM-DD" or a datetime.date. |
end |
date or str | required | The last session of the range: "YYYY-MM-DD" or a datetime.date. |
snapshot |
str or None | None |
None: every snapshot your plan covers. "EOD": each session's last snapshot at or before its close. "HH:MM": that time each session. |
root |
str or None | None |
The listed root, for a symbol that lists under several (SPX lists as SPX and SPXW). None lets the server choose, and the result names the root it served. See SPX and SPXW. |
fields |
str or list of str or None | None |
Return only these measures: a list, or one comma-separated text. None returns all. Allowed: atf_iv, skew_25d, curvature_25d, straddle_pct, strangle_25d_pct, rr_25d_pct, atf_iv_clean, atf_iv_clean_se, straddle_pct_clean, event_dates. |
sampling |
str or None | None |
How often each session is sampled: "eod", "30m", "5m" or "1m". None: your plan's frequency. Your plan must cover it (EntitlementError otherwise). Give either snapshot or sampling, not both. |
Returns: A DataFrame with one row per session and snapshot: identity columns, tenor_days and the measures (each one is described under GET /tenor-history). A range too wide for your plan's frequency raises InvalidRequestError with estimated_rows: narrow the dates or pass snapshot="EOD".
Requests: One call covers at most a set number of periods (one snapshot of one session; client.prices() gives the limit); a longer range is fetched in several calls, checked against what is left today before the first one.
REST: GET /tenor-history
DataFrame columns: symbol, root, data_date, snapshot_time, tenor_days, atf_iv, skew_25d, curvature_25d, straddle_pct, strangle_25d_pct, rr_25d_pct, atf_iv_clean, atf_iv_clean_se, straddle_pct_clean, event_dates. .attrs keys: symbol, root, data_date, snapshot_time, session_close, granularity, returned_rows, notes, start, end, tenor_days, snapshot_mode, fields, calls, budget. Each means the same as the REST field of the same name (GET /tenor-history); the ones that differ:
Columns that differ from REST (3)
| Column or attr | Type and unit | Can be empty | Meaning | Example |
|---|---|---|---|---|
symbol |
text | The product symbol. | "SPY" |
|
.attrs["calls"] |
integer | How many requests the range took: one, or several when it covered more periods than one call may. | 1 |
|
.attrs["budget"] |
object | What a multi-request call cost: the requests and credits used, and (for chain()) the rows fetched and projected. |
client.rates()
The risk-free rate implied by SPX options, per session and listed expiry.
client.rates(start, end, *, expiration=None) -> DataFrame
| Argument | Type | Default | Meaning |
|---|---|---|---|
start |
date or str | required | The first session of the range: "YYYY-MM-DD" or a datetime.date. |
end |
date or str | required | The last session of the range: "YYYY-MM-DD" or a datetime.date. |
expiration |
date or str or None | None |
Only this listed expiry. None returns every expiry. |
Returns: A DataFrame with data_date, expiration, rate (continuously compounded, annualized, decimal) and rate_se (its standard error).
Requests: One call covers at most a set number of periods (one snapshot of one session; client.prices() gives the limit); a longer range is fetched in several calls, checked against what is left today before the first one.
REST: GET /rates
DataFrame columns: symbol, data_date, expiration, rate, rate_se. .attrs keys: symbol, root, data_date, snapshot_time, session_close, granularity, returned_rows, notes, start, end, calls, budget. Each means the same as the REST field of the same name (GET /rates); the ones that differ:
Columns that differ from REST (3)
| Column or attr | Type and unit | Can be empty | Meaning | Example |
|---|---|---|---|---|
symbol |
text | Always SPX. |
"SPX" |
|
.attrs["calls"] |
integer | How many requests the range took: one, or several when it covered more periods than one call may. | 1 |
|
.attrs["budget"] |
object | What a multi-request call cost: the requests and credits used, and (for chain()) the rows fetched and projected. |
client.iv_rv()
At-the-money implied volatility, realized volatility and the gap between them at 1 to 30 days, through time.
client.iv_rv(
symbol,
tenor_days,
start,
end,
*,
snapshot=None,
root=None,
fields=None,
sampling=None,
) -> DataFrame
| Argument | Type | Default | Meaning |
|---|---|---|---|
symbol |
str | required | The product symbol, such as "SPY" or "SPX". client.symbols() lists them. |
tenor_days |
int | required | The horizon in calendar days: one of 1, 2, 3, 5, 7, 10, 14, 20, 30. |
start |
date or str | required | The first session of the range: "YYYY-MM-DD" or a datetime.date. |
end |
date or str | required | The last session of the range: "YYYY-MM-DD" or a datetime.date. |
snapshot |
str or None | None |
None: every snapshot your plan covers. "EOD": each session's last snapshot at or before its close. "HH:MM": that time each session. |
root |
str or None | None |
The listed root, for a symbol that lists under several (SPX lists as SPX and SPXW). None lets the server choose, and the result names the root it served. See SPX and SPXW. |
fields |
str or list of str or None | None |
Return only these measures: a list, or one comma-separated text. None returns all. |
sampling |
str or None | None |
How often each session is sampled: "eod", "30m", "5m" or "1m". None: your plan's frequency. Your plan must cover it (EntitlementError otherwise). Give either snapshot or sampling, not both. |
Returns: A DataFrame with one row per session and snapshot: identity columns, tenor_days and the measures (each one is described under GET /iv-rv).
Requests: One call covers at most a set number of periods (one snapshot of one session; client.prices() gives the limit); a longer range is fetched in several calls, checked against what is left today before the first one.
REST: GET /iv-rv
client.expiry_history()
One expiry's values across sessions, at one snapshot each session.
client.expiry_history(
symbol,
expiration,
start,
end,
*,
snapshot="EOD",
root=None,
) -> DataFrame
| Argument | Type | Default | Meaning |
|---|---|---|---|
symbol |
str | required | The product symbol, such as "SPY" or "SPX". client.symbols() lists them. |
expiration |
date or str | required | The option expiration date: "YYYY-MM-DD" or a datetime.date. client.expirations() lists the ones that exist. |
start |
date or str | required | The first session of the range: "YYYY-MM-DD" or a datetime.date. |
end |
date or str | required | The last session of the range: "YYYY-MM-DD" or a datetime.date. |
snapshot |
str | "EOD" |
"EOD" (each session's last snapshot at or before its close that your plan covers) or a time "HH:MM" in US Eastern time, the same each session. |
root |
str or None | None |
The listed root. None: for an SPX monthly listed under both SPX and SPXW, SPXW is served (.attrs["notes"] says so). |
Returns: A DataFrame with one row per session, oldest first: the values surface() gives for an expiry, from sigma_atm to r_squared (each one is described under GET /expiry-history). .attrs["withheld_sessions"] lists sessions with no row because the expiry's fit did not pass our quality checks.
REST: GET /expiry-history
client.contract_history()
One option across sessions: fitted volatility and price, greeks and, on past sessions, its quotes, at one snapshot each session.
client.contract_history(
symbol,
expiration,
strike,
put_call,
start,
end,
*,
snapshot="EOD",
root=None,
columns=None,
) -> DataFrame
| Argument | Type | Default | Meaning |
|---|---|---|---|
symbol |
str | required | The product symbol, such as "SPY" or "SPX". client.symbols() lists them. |
expiration |
date or str | required | The option expiration date: "YYYY-MM-DD" or a datetime.date. client.expirations() lists the ones that exist. |
strike |
float | required | The strike price (USD). |
put_call |
str | required | "call" or "put". |
start |
date or str | required | The first session of the range: "YYYY-MM-DD" or a datetime.date. |
end |
date or str | required | The last session of the range: "YYYY-MM-DD" or a datetime.date. |
snapshot |
str | "EOD" |
"EOD" (each session's last snapshot at or before its close that your plan covers) or a time "HH:MM" in US Eastern time, the same each session. |
root |
str or None | None |
The listed root. None: for an SPX monthly listed under both SPX and SPXW, SPXW is served. |
columns |
str or list of str or None | None |
Return only these columns, as for chain(): a list, or one comma-separated text. "volume" and "open_interest" are added only when named, for a range that ends on a past session. |
Returns: A DataFrame with one row per session, oldest first: identity columns, then chain()'s columns for this option, and quote_state where the quotes are included. .attrs["withheld_sessions"] lists sessions with no row because the expiry's fit did not pass our quality checks.
REST: GET /contract-history
client.event_outcomes()
For each earnings, Fed, CPI or jobs-report day, the move the options priced, the move that followed, and 30-day at-the-money volatility before and after.
client.event_outcomes(
symbol,
start,
end,
*,
event_type=None,
root=None,
) -> DataFrame
| Argument | Type | Default | Meaning |
|---|---|---|---|
symbol |
str | required | The product symbol, such as "SPY" or "SPX". client.symbols() lists them. |
start |
date or str | required | The first session of the range: "YYYY-MM-DD" or a datetime.date. |
end |
date or str | required | The last session of the range: "YYYY-MM-DD" or a datetime.date. |
event_type |
str or None | None |
One of earnings, fomc, cpi, nfp. None: every type. |
root |
str or None | None |
The listed root, for a symbol that lists under several (SPX lists as SPX and SPXW). None lets the server choose, and the result names the root it served. See SPX and SPXW. |
Returns: A DataFrame with one row per event, oldest first (each column is described under GET /event-outcomes). Moves are decimals of the share price.
REST: GET /event-outcomes
client.event_runup()
The move priced for each event on the sessions before it.
client.event_runup(
symbol,
start,
end,
*,
event_type=None,
root=None,
) -> DataFrame
| Argument | Type | Default | Meaning |
|---|---|---|---|
symbol |
str | required | The product symbol, such as "SPY" or "SPX". client.symbols() lists them. |
start |
date or str | required | The first session of the range: "YYYY-MM-DD" or a datetime.date. |
end |
date or str | required | The last session of the range: "YYYY-MM-DD" or a datetime.date. |
event_type |
str or None | None |
One of earnings, fomc, cpi, nfp. None: every type. |
root |
str or None | None |
The listed root, for a symbol that lists under several (SPX lists as SPX and SPXW). None lets the server choose, and the result names the root it served. See SPX and SPXW. |
Returns: A DataFrame with one row per event and session before it (each column is described under GET /event-runup).
REST: GET /event-runup
client.volume_summary()
Daily volume and open interest for calls, puts and both, with the put/call ratios. Past sessions only.
client.volume_summary(symbol, start, end, *, root=None) -> DataFrame
| Argument | Type | Default | Meaning |
|---|---|---|---|
symbol |
str | required | The product symbol, such as "SPY" or "SPX". client.symbols() lists them. |
start |
date or str | required | The first session of the range: "YYYY-MM-DD" or a datetime.date. |
end |
date or str | required | The last session of the range: "YYYY-MM-DD" or a datetime.date. |
root |
str or None | None |
The listed root, for a symbol that lists under several (SPX lists as SPX and SPXW). None lets the server choose, and the result names the root it served. See SPX and SPXW. |
Returns: A DataFrame with one row per session (each column is described under GET /volume-summary). A session not yet available raises EntitlementError with reason "not_yet_historical".
REST: GET /volume-summary
client.contract_volume()
One option's official daily volume and open interest. Past sessions only.
client.contract_volume(
symbol,
expiration,
strike,
put_call,
start,
end,
*,
root=None,
) -> DataFrame
| Argument | Type | Default | Meaning |
|---|---|---|---|
symbol |
str | required | The product symbol, such as "SPY" or "SPX". client.symbols() lists them. |
expiration |
date or str | required | The option expiration date: "YYYY-MM-DD" or a datetime.date. client.expirations() lists the ones that exist. |
strike |
float | required | The strike price (USD). |
put_call |
str | required | "call" or "put". |
start |
date or str | required | The first session of the range: "YYYY-MM-DD" or a datetime.date. |
end |
date or str | required | The last session of the range: "YYYY-MM-DD" or a datetime.date. |
root |
str or None | None |
The listed root. None: for an SPX monthly listed under both SPX and SPXW, SPXW is served. |
Returns: A DataFrame with one row per session the option was listed, oldest first: open_interest (at the end of the previous session) and volume (the official end-of-day figure), in contracts.
REST: GET /contract-volume
client.surface()
A legacy end-of-day summary: one row per listed expiry. For new work use term_structure() or smile().
client.surface(
symbol,
data_date,
snapshot="EOD",
*,
root=None,
) -> DataFrame
| Argument | Type | Default | Meaning |
|---|---|---|---|
symbol |
str | required | The product symbol, such as "SPY" or "SPX". client.symbols() lists them. |
data_date |
date or str | required | The trading session: "YYYY-MM-DD" text or a datetime.date. |
snapshot |
str | "EOD" |
"EOD" (the last snapshot at or before the official close that your plan covers) or a time "HH:MM" in US Eastern time. The time used comes back as snapshot_time. |
root |
str or None | None |
The listed root, for a symbol that lists under several (SPX lists as SPX and SPXW). None lets the server choose, and the result names the root it served. See SPX and SPXW. |
Returns: A DataFrame with one row per listed expiry: expiration, dte, sigma_atm, forward, rate, q, spot, the fit statistics, quality and its root. .attrs holds the resolved snapshot_time and session_close.
REST: GET /surface
DataFrame columns: snapshot_time, expiration, dte, sigma_atm, forward, rate, q, spot, s_implied, s_basis, T, T_vol, T_borrow, n_obs, rmse, r_squared, root, quality. .attrs keys: symbol, root, data_date, snapshot_time, session_close, granularity, returned_rows, date, spot. Each means the same as the REST field of the same name (GET /surface).
Python: research calls
Each research call answers a whole question in one request and returns a dictionary with its values, the observations it used, coverage and a status. Pass estimate=True first to see the most a call can cost, or max_credits=N to have it refused, unrun, if it could cost more. A long call becomes a background job that the client waits for. They work on completed sessions only, and except for expiry_metrics() they need a symbol that lists under one root.
client.expiry_metrics()
At-the-money volatility and 25-delta skew for one listed expiry at one snapshot, optionally beside the previous session.
client.expiry_metrics(
symbol,
data_date,
*,
expiration,
snapshot="EOD",
root=None,
compare_previous=False,
max_credits=None,
estimate=False,
) -> dict
| Argument | Type | Default | Meaning |
|---|---|---|---|
symbol |
str | required | The product symbol, such as "SPY" or "SPX". client.symbols() lists them. |
data_date |
date or str | required | The trading session: "YYYY-MM-DD" text or a datetime.date. |
expiration |
date or str | required | The option expiration date: "YYYY-MM-DD" or a datetime.date. client.expirations() lists the ones that exist. |
snapshot |
str | "EOD" |
"EOD" (the last snapshot at or before the official close that your plan covers) or a time "HH:MM" in US Eastern time. The time used comes back as snapshot_time. |
root |
str or None | None |
The listed root, for a symbol that lists under several (SPX lists as SPX and SPXW). None lets the server choose, and the result names the root it served. See SPX and SPXW. |
compare_previous |
bool | False |
Also return the previous trading session, and the change in skew. |
max_credits |
int or None | None |
Refuse the call, unrun, if it could cost more than this many credits (raises MaxCreditsExceeded). None sets no limit. At least 1. |
estimate |
bool | False |
True returns the cost estimate (credits_estimate, credits_cap) without running the call. |
Returns: A dictionary with one entry per session: sigma_atm, put_25d_iv, call_25d_iv, put_25d_skew, call_25d_skew, skew_25d and status; skew_change when the previous session is compared; credits (what the call cost).
REST: GET /expiry-metrics
client.exposure_distribution()
Where today's fitted volatility for a delta-selected option sits against the same selection on past sessions: same clock time, same days to expiry.
client.exposure_distribution(
symbol,
data_date,
*,
snapshot,
put_call,
delta=0.15,
dte=1,
delta_tolerance=0.01,
lookback_years=1,
root=None,
max_credits=None,
estimate=False,
) -> dict
| Argument | Type | Default | Meaning |
|---|---|---|---|
symbol |
str | required | The product symbol, such as "SPY" or "SPX". client.symbols() lists them. |
data_date |
date or str | required | The trading session: "YYYY-MM-DD" text or a datetime.date. |
snapshot |
str | required | An explicit "HH:MM" US Eastern time. "EOD" is refused, so every session is compared at the same clock time. |
put_call |
str | required | "call" or "put". |
delta |
float | 0.15 |
The absolute delta to select (0.15 = a 15-delta option). Greater than 0 and less than 1. |
dte |
int | 1 |
Days to expiry, in calendar days, matched exactly. From 0 to 365. |
delta_tolerance |
float | 0.01 |
How far a stored option's delta may be from delta and still be selected. From 0 to 0.05. |
lookback_years |
float | 1 |
How many years of past sessions to compare against. From 0.1 to 1. |
root |
str or None | None |
The listed root, for a symbol that lists under several (SPX lists as SPX and SPXW). None lets the server choose, and the result names the root it served. See SPX and SPXW. |
max_credits |
int or None | None |
Refuse the call, unrun, if it could cost more than this many credits (raises MaxCreditsExceeded). None sets no limit. At least 1. |
estimate |
bool | False |
True returns the cost estimate (credits_estimate, credits_cap) without running the call. |
Returns: A dictionary: current (the option selected on data_date), history (one selected option per past session, with expiration, strike, delta, fitted_vol), statistics (n, median, percentile from 0 to 100, robust_z_score), coverage, status and credits.
REST: GET /exposure-distribution
client.clean_atm_history()
Constant-maturity at-the-money volatility through time, with scheduled events removed (clean) and without (dirty).
client.clean_atm_history(
symbol,
start,
end,
*,
tenor_days=30,
granularity="eod",
max_credits=None,
estimate=False,
) -> dict
| Argument | Type | Default | Meaning |
|---|---|---|---|
symbol |
str | required | The product symbol, such as "SPY" or "SPX". client.symbols() lists them. |
start |
date or str | required | The first session: "YYYY-MM-DD" or a date. At most three years before end. |
end |
date or str | required | The last session, before today: "YYYY-MM-DD" or a date. |
tenor_days |
int | 30 |
The constant maturity in calendar days. From 7 to 180. |
granularity |
str | "eod" |
"eod" (one value per session) or "5m" (every five minutes; needs five-minute or finer access). |
max_credits |
int or None | None |
Refuse the call, unrun, if it could cost more than this many credits (raises MaxCreditsExceeded). None sets no limit. At least 1. |
estimate |
bool | False |
True returns the cost estimate (credits_estimate, credits_cap) without running the call. |
Returns: A dictionary: rows (data_date, snapshot_time, dirty_iv, clean_iv, clean_iv_se, status, event_dates, expirations), earnings (report dates in the window), notes and credits. For a stock only the next scheduled earnings report is removed; for an index or ETF, scheduled macro releases. Where an event cannot be removed, clean_iv repeats the unadjusted value and status says why.
REST: GET /clean-atm-history
client.earnings_history()
Past earnings reports: the move the options priced, the move that followed, and how the priced move built up beforehand.
client.earnings_history(
symbol,
as_of,
*,
count=8,
runup_sessions=30,
granularity="eod",
max_credits=None,
estimate=False,
) -> dict
| Argument | Type | Default | Meaning |
|---|---|---|---|
symbol |
str | required | The product symbol, such as "SPY" or "SPX". client.symbols() lists them. |
as_of |
date or str | required | Use the reports strictly before this date ("YYYY-MM-DD", before today). |
count |
int | 8 |
How many past reports. From 1 to 12. |
runup_sessions |
int | 30 |
How many sessions before each report the run-up path covers. From 5 to 60. |
granularity |
str | "eod" |
"eod" or "5m" for the run-up path (five-minute needs five-minute or finer access). |
max_credits |
int or None | None |
Refuse the call, unrun, if it could cost more than this many credits (raises MaxCreditsExceeded). None sets no limit. At least 1. |
estimate |
bool | False |
True returns the cost estimate (credits_estimate, credits_cap) without running the call. |
Returns: A dictionary with events, each with earnings_date, timing (AMC after the close or BMO before the open), implied_move (the average size of the move priced at the last close before the report, about 0.80 times the event's volatility; a decimal, not annualized) and implied_move_se, actual_return (signed), actual_move (its size), return_overnight, return_intraday, status and samples (the run-up path, by sessions_to_event); plus notes and credits.
REST: GET /earnings-history
client.earnings_backtest()
A bounded earnings put-spread simulation: for each recent report, sell a put spread before the report and close it after, with fills, fees, P&L and risk.
client.earnings_backtest(
symbol,
as_of,
*,
count=8,
entry_offset=-5,
exit_offset=1,
fill_model="quote_side",
fee_per_contract=0.65,
granularity="eod",
max_credits=None,
estimate=False,
) -> dict
| Argument | Type | Default | Meaning |
|---|---|---|---|
symbol |
str | required | The product symbol, such as "SPY" or "SPX". client.symbols() lists them. |
as_of |
date or str | required | Use the reports strictly before this date ("YYYY-MM-DD", before today). |
count |
int | 8 |
How many past reports to trade, one spread each. From 1 to 12. |
entry_offset |
int | -5 |
When to open: sessions relative to the last close before the report (-5 = five sessions earlier). From -20 to 0. |
exit_offset |
int | 1 |
When to close: sessions after the report. From 1 to 5. |
fill_model |
str | "quote_side" |
"quote_side" (sell at the bid, buy at the ask), "midpoint", or "theoretical" (the fitted price). Quote-based fills need an as_of whose quotes are released. |
fee_per_contract |
float | 0.65 |
Fee in USD per contract per fill. From 0 to 20. |
granularity |
str | "eod" |
"eod" or "1m" for the position path (one-minute needs one-minute access). |
max_credits |
int or None | None |
Refuse the call, unrun, if it could cost more than this many credits (raises MaxCreditsExceeded). None sets no limit. At least 1. |
estimate |
bool | False |
True returns the cost estimate (credits_estimate, credits_cap) without running the call. |
Returns: A dictionary: assumptions (the trade rules), events (each with status, entry_date, exit_date, expiration, legs, fills, gross_pnl, fees, net_pnl and path: P&L, delta, gamma, vega and theta at each observation), summary and credits. P&L is in USD for one spread.
REST: GET /earnings-backtest
Python: saved keys
The same steps as optionspace login, whoami and logout in the terminal, from Python.
osp.login()
Check a key with the server and save it on this computer. The same as optionspace login in the terminal.
osp.login(
api_key=None,
*,
profile=None,
store="keyring",
base_url="https://optionspace.ai",
stdin=False,
) -> LoginStatus
| Argument | Type | Default | Meaning |
|---|---|---|---|
api_key |
str or None | None |
The key to save. None asks for it without showing what you type. |
profile |
str or None | None |
The name to save it under (letters, digits, - and _). None uses OPTIONSPACE_PROFILE, else "default". |
store |
str | "keyring" |
"keyring" (your operating system's credential store) or "file" (a private file in your user folder). |
base_url |
str | "https://optionspace.ai" |
The server the key is checked against. The saved key works only with this server. |
stdin |
bool | False |
Read the key from one line of standard input instead of asking; for scripts. |
Returns: A LoginStatus. A rejected key, or a server that cannot be reached, saves nothing and raises CredentialError.
Requests: One account check.
osp.whoami()
Which key this computer would use, where it came from, and what the server says about it. The same as optionspace whoami.
osp.whoami(*, profile=None, api_key=None, base_url=None) -> dict
| Argument | Type | Default | Meaning |
|---|---|---|---|
profile |
str or None | None |
Which saved login to check. None uses the one Client() would use. |
api_key |
str or None | None |
Check this key instead of a saved one. |
base_url |
str or None | None |
The server to check against, for a key passed here or set in the environment. |
Returns: A dictionary: source, profile, base_url, key_id, org_name, tier, status, entitlements, limits, usage and notes. Never the key itself. A rejected key raises AuthenticationError.
Requests: One account check.
osp.logout()
Remove the saved key from this computer. The key stays valid on the server: revoke it on your account page.
osp.logout(profile=None, *, all_profiles=False) -> dict
| Argument | Type | Default | Meaning |
|---|---|---|---|
profile |
str or None | None |
Which saved login to remove. None uses OPTIONSPACE_PROFILE, else "default". |
all_profiles |
bool | False |
Remove every saved login. |
Returns: A dictionary: removed (what was deleted for each profile), env_key_still_set (whether OPTIONSPACE_API_KEY is still set) and a note.
Requests: No API request.
osp.LoginStatus
What login() did. Safe to print and to keep in a notebook: it never contains the key.
| Field | Type | Default | Meaning |
|---|---|---|---|
profile |
str | required | The profile the key was saved under. |
store |
str | required | Where it was saved: "keyring" or "file". |
base_url |
str | required | The server it was checked against. |
key_id |
str or None | required | The public key identifier (osk_...). |
org_id |
str or None | required | Your account's identifier. |
tier |
str or None | required | Your plan. |
entitlements |
dict | required | What the key can access. |
limits |
dict | required | Your daily limits. |
usage |
dict | required | Today's usage. |
removed_copies |
tuple | () |
Older saved copies of this profile that were removed. |
warnings |
tuple | () |
Anything to act on, such as an environment key that would take priority. |
Python: working with results
Helpers for frames you have already fetched. None of them makes an API request.
osp.curve_frame()
The fitted smile curve from a smile() result, as a DataFrame.
osp.curve_frame(smile) -> DataFrame
| Argument | Type | Default | Meaning |
|---|---|---|---|
smile |
DataFrame | required | A frame from client.smile(). |
Returns: A DataFrame with the identity columns plus z, strike, sigma (decimal) and is_wing_extrapolated (the point lies outside the fitted range). Empty for older curve types that carry no curve.
Requests: No API request.
osp.FrameCache()
A folder cache of the frames the client returns, so a notebook can rerun without new requests. Each entry is stored under its full query and checked when read: a frame that does not match raises CacheMismatch instead of being returned. Entries are Python pickle files: only open a cache folder you created.
osp.FrameCache(directory, *, base_url="https://optionspace.ai")
cache = osp.FrameCache("study_cache", base_url=client.base_url)
query = dict(symbol="SPY", data_date="2025-04-10",
expiration="2025-04-17", snapshot_time="15:57")
sm = cache.get("smile", **query)
if sm is None:
sm = client.smile("SPY", "2025-04-10", expiration="2025-04-17", snapshot="15:57")
cache.put(sm, "smile", **query)
| Argument | Type | Default | Meaning |
|---|---|---|---|
directory |
str or Path | required | The folder to keep the cache in. Created when needed. |
base_url |
str | "https://optionspace.ai" |
The server the data came from; data from different servers is kept apart. Pass client.base_url. |
Returns: A cache. Its methods follow.
Requests: No API request.
cache.get()
The cached frame for exactly this query, or None.
cache.get(route, **identity) -> DataFrame or None
| Argument | Type | Default | Meaning |
|---|---|---|---|
route |
str | required | The method the frame came from, such as "smile". |
**identity |
str | The query: symbol and data_date (both required), and root, expiration and snapshot_time where they apply. |
Returns: The DataFrame, or None when nothing is stored. An entry that does not match its query or has changed on disk raises CacheMismatch.
cache.put()
Store a frame under its query.
cache.put(frame, route, **identity) -> Path
| Argument | Type | Default | Meaning |
|---|---|---|---|
frame |
DataFrame | required | The DataFrame to store. Its own identity must match the query. |
route |
str | required | The method the frame came from, such as "smile". |
**identity |
str | The query, as for get(). |
Returns: The path of the stored file.
Python: charts
from optionspace import charts (install with pip install "optionspace[charts]"). Every chart takes results you already fetched and returns an editable Plotly figure: drawing makes no API request. Charts show volatility in vol points (20 = 20%) and never fill in missing observations. fig.show() displays one; charts.save() writes a file.
charts.smile_chart()
A smile card from one client.smile() result: observed mids, bid/ask bars on every quoted strike, and the fitted curve (solid inside the fitted range, dotted outside).
charts.smile_chart(
sm,
theme="light",
*,
title=None,
xlim=None,
width=1100,
mark=True,
) -> Figure
| Argument | Type | Default | Meaning |
|---|---|---|---|
sm |
DataFrame | required | A frame from client.smile(). |
theme |
str or Theme | "light" |
"light", "dark" or "institutional" (reduced branding), or a charts.Theme. |
title |
str or None | None |
A title for the card. None uses the default title. |
xlim |
tuple or None | None |
(low, high) strike range for the x-axis. None shows the quoted strikes. |
width |
int | 1100 |
Figure width in pixels. |
mark |
bool | True |
Draw the OptionSpace logo mark on the card. |
Returns: A Plotly figure.
charts.compare_chart()
Smiles side by side, one panel each, on shared axes.
charts.compare_chart(
smiles,
theme="light",
*,
labels=None,
title=None,
xlim=None,
width=1100,
mark=True,
) -> Figure
| Argument | Type | Default | Meaning |
|---|---|---|---|
smiles |
list of DataFrame | required | A list of client.smile() frames (two to four work best). |
theme |
str or Theme | "light" |
"light", "dark" or "institutional" (reduced branding), or a charts.Theme. |
labels |
list of str or None | None |
A label per panel. None labels each panel by what differs (time, date or expiry). |
title |
str or None | None |
A title for the card. None uses the default title. |
xlim |
tuple or None | None |
(low, high) strike range for every panel. None covers the quoted strikes. |
width |
int | 1100 |
Figure width in pixels. |
mark |
bool | True |
Draw the OptionSpace logo mark on the card. |
Returns: A Plotly figure.
charts.atm_chart()
The at-the-money volatility path through a session.
charts.atm_chart(
atm,
theme="light",
*,
highlight=(),
title=None,
width=1100,
mark=True,
cadence=None,
) -> Figure
| Argument | Type | Default | Meaning |
|---|---|---|---|
atm |
DataFrame | required | A frame from client.atm_intraday(). |
theme |
str or Theme | "light" |
"light", "dark" or "institutional" (reduced branding), or a charts.Theme. |
highlight |
list of str | () |
Snapshot times ("HH:MM") to mark on the path. |
title |
str or None | None |
A title for the card. None uses the default title. |
width |
int | 1100 |
Figure width in pixels. |
mark |
bool | True |
Draw the OptionSpace logo mark on the card. |
cadence |
int or None | None |
Minutes between snapshots, used to show gaps. None uses your plan's frequency, else the spacing in the data (stated on the chart). |
Returns: A Plotly figure.
charts.term_chart()
At-the-money volatility and the forward across expiries.
charts.term_chart(
term,
theme="light",
*,
title=None,
width=1100,
mark=True,
) -> Figure
| Argument | Type | Default | Meaning |
|---|---|---|---|
term |
DataFrame | required | A frame from charts.term_frame() or client.term_structure(). |
theme |
str or Theme | "light" |
"light", "dark" or "institutional" (reduced branding), or a charts.Theme. |
title |
str or None | None |
A title for the card. None uses the default title. |
width |
int | 1100 |
Figure width in pixels. |
mark |
bool | True |
Draw the OptionSpace logo mark on the card. |
Returns: A Plotly figure.
charts.history_chart()
One to four constant-maturity histories on one chart. Gaps between sessions break the line; missing values are never filled.
charts.history_chart(
histories,
metric="atf_iv",
theme="light",
*,
width=1100,
)
| Argument | Type | Default | Meaning |
|---|---|---|---|
histories |
list of DataFrame | required | One to four client.tenor_history() frames at the same tenor. |
metric |
str | "atf_iv" |
"atf_iv" (at-the-forward volatility) or "skew_25d" (25-delta skew). |
theme |
str or Theme | "light" |
"light", "dark" or "institutional" (reduced branding), or a charts.Theme. |
width |
int | 1100 |
Figure width in pixels. |
Returns: A Plotly figure.
charts.term_comparison_chart()
Term structures from different dates on one chart.
charts.term_comparison_chart(terms, theme="light", *, width=1100)
| Argument | Type | Default | Meaning |
|---|---|---|---|
terms |
list of DataFrame | required | One to four client.term_structure() frames for the same symbol and root. |
theme |
str or Theme | "light" |
"light", "dark" or "institutional" (reduced branding), or a charts.Theme. |
width |
int | 1100 |
Figure width in pixels. |
Returns: A Plotly figure.
charts.skew_comparison_chart()
25-delta skew by symbol, one bar per session.
charts.skew_comparison_chart(
rows,
tenor_days=None,
theme="light",
*,
expiration=None,
width=1100,
)
| Argument | Type | Default | Meaning |
|---|---|---|---|
rows |
list of dict | required | A list of dictionaries, each with symbol, data_date, snapshot_time and skew_25d (decimal), for example client.tenor_history(..., fields=["skew_25d"]).to_dict("records"). |
tenor_days |
int or None | None |
The tenor the rows come from, for the label. |
theme |
str or Theme | "light" |
"light", "dark" or "institutional" (reduced branding), or a charts.Theme. |
expiration |
str or None | None |
The listed expiry the rows come from, for the label (instead of tenor_days). |
width |
int | 1100 |
Figure width in pixels. |
Returns: A Plotly figure.
charts.exposure_distribution_chart()
The past distribution of fitted volatility, with today's value marked.
charts.exposure_distribution_chart(result, theme="light", *, width=1100)
| Argument | Type | Default | Meaning |
|---|---|---|---|
result |
dict | required | The dictionary from client.exposure_distribution(). |
theme |
str or Theme | "light" |
"light", "dark" or "institutional" (reduced branding), or a charts.Theme. |
width |
int | 1100 |
Figure width in pixels. |
Returns: A Plotly figure.
charts.clean_atm_chart()
At-the-money volatility with and without scheduled events, with the earnings dates marked.
charts.clean_atm_chart(result, theme="light", *, width=1100)
| Argument | Type | Default | Meaning |
|---|---|---|---|
result |
dict | required | The dictionary from client.clean_atm_history(). |
theme |
str or Theme | "light" |
"light", "dark" or "institutional" (reduced branding), or a charts.Theme. |
width |
int | 1100 |
Figure width in pixels. |
Returns: A Plotly figure.
charts.earnings_comparison_chart()
The move each report priced beside the move that followed.
charts.earnings_comparison_chart(result, theme="light", *, width=1100)
| Argument | Type | Default | Meaning |
|---|---|---|---|
result |
dict | required | The dictionary from client.earnings_history(). |
theme |
str or Theme | "light" |
"light", "dark" or "institutional" (reduced branding), or a charts.Theme. |
width |
int | 1100 |
Figure width in pixels. |
Returns: A Plotly figure.
charts.earnings_runup_chart()
How the priced earnings move built up in the sessions before each report.
charts.earnings_runup_chart(result, theme="light", *, width=1100)
| Argument | Type | Default | Meaning |
|---|---|---|---|
result |
dict | required | The dictionary from client.earnings_history(). |
theme |
str or Theme | "light" |
"light", "dark" or "institutional" (reduced branding), or a charts.Theme. |
width |
int | 1100 |
Figure width in pixels. |
Returns: A Plotly figure.
charts.earnings_backtest_chart()
P&L per event from an earnings put-spread simulation.
charts.earnings_backtest_chart(result, theme="light", *, width=1100)
| Argument | Type | Default | Meaning |
|---|---|---|---|
result |
dict | required | The dictionary from client.earnings_backtest(). |
theme |
str or Theme | "light" |
"light", "dark" or "institutional" (reduced branding), or a charts.Theme. |
width |
int | 1100 |
Figure width in pixels. |
Returns: A Plotly figure.
charts.backtest_path_chart()
One simulated trade from entry to exit: P&L and delta at each observation.
charts.backtest_path_chart(
result,
theme="light",
*,
earnings_date=None,
width=1100,
)
| Argument | Type | Default | Meaning |
|---|---|---|---|
result |
dict | required | The dictionary from client.earnings_backtest(). |
theme |
str or Theme | "light" |
"light", "dark" or "institutional" (reduced branding), or a charts.Theme. |
earnings_date |
str or None | None |
Which report's trade to draw ("YYYY-MM-DD"). None draws the last completed trade. |
width |
int | 1100 |
Figure width in pixels. |
Returns: A Plotly figure.
charts.save()
Write a figure to a file.
charts.save(fig, path, *, scale=2) -> Path
| Argument | Type | Default | Meaning |
|---|---|---|---|
fig |
Figure | required | A figure from any chart function. |
path |
str or Path | required | The file to write. The extension picks the format: .png, .svg or .pdf (need the chart export extras and Chrome), .html (an interactive page that loads Plotly from the internet) or .json (the figure itself). |
scale |
float | 2 |
Image scale for .png, .svg and .pdf. 2 gives 2200-pixel-wide cards. |
Returns: The path written.
Python: session movies
from optionspace.movies import DayMovie. A replay of one expiry's smile through a session, built from smiles you fetched (one request each, so count the snapshots first with client.snapshots()). Building and saving the movie makes no further request.
DayMovie()
A replay of one expiry's smile through a session.
DayMovie(
smiles,
*,
atm=None,
theme="light",
stride=1,
xlim="band",
title=None,
fps=12,
trail=20,
width=1100,
cadence=None,
source_snapshots=None,
include_last=False,
coverage_evidence=None,
)
from optionspace.movies import DayMovie
times = client.snapshots("SPY", "2025-04-10")["snapshot_time"][::30]
smiles = [client.smile("SPY", "2025-04-10", expiration="2025-04-17", snapshot=t)
for t in times]
movie = DayMovie(smiles)
movie.write_html("spy-day.html")
| Argument | Type | Default | Meaning |
|---|---|---|---|
smiles |
list of DataFrame | required | client.smile() frames for one expiry through one session, one per snapshot. |
atm |
DataFrame or None | None |
A client.atm_intraday() frame, drawn in a second panel. None leaves it out. |
theme |
str or Theme | "light" |
"light", "dark" or "institutional" (reduced branding), or a charts.Theme. |
stride |
int | 1 |
Play every Nth snapshot (counts snapshots, not minutes). |
xlim |
str, tuple or None | "band" |
"band": the strikes the fits are supported by quotes on, padded 5%. ("z", low, high): strikes whose standardized moneyness z lies between low and high. (low, high): explicit strikes. None: every quoted strike. |
title |
str or None | None |
A title for the card. None uses the default title. |
fps |
int | 12 |
Frames per second in a video. |
trail |
int | 20 |
How many earlier frames fade behind the current one. |
width |
int | 1100 |
Figure width in pixels. |
cadence |
int or None | None |
Minutes between snapshots, for reporting gaps. None uses your plan's frequency, else the spacing in the data. |
source_snapshots |
list of str or None | None |
Every snapshot time the session holds, when you fetched only some of them. Gaps are judged on this list, so a deliberate skip is not reported as missing data. |
include_last |
bool | False |
Add the session's last snapshot when the stride would skip it, so the movie ends at the close. |
coverage_evidence |
str or None | None |
A note on where source_snapshots came from, kept in the metadata. |
Returns: A movie. Its methods follow.
Requests: No API request (the smiles you pass were already fetched).
movie.interactive()
The movie as one interactive figure, with a timeline slider, play and pause, and three speeds.
movie.interactive() -> Figure
Returns: A Plotly figure.
movie.write_html()
Save the interactive movie as a web page.
movie.write_html(path) -> Path
| Argument | Type | Default | Meaning |
|---|---|---|---|
path |
str or Path | required | The .html file to write. The page loads Plotly from the internet. |
Returns: The path written.
movie.encode()
Turn rendered frames into video files (needs FFmpeg).
movie.encode(
frames_dir,
out_stem,
*,
formats=("webm", "mp4"),
fps=None,
crf=30,
) -> dict
| Argument | Type | Default | Meaning |
|---|---|---|---|
frames_dir |
str or Path | required | The folder render_frames() wrote. |
out_stem |
str or Path | required | The output path without an extension; each format adds its own. |
formats |
list of str | ("webm", "mp4") |
Which videos to write: "webm", "mp4" or both. |
fps |
int or None | None |
Frames per second. None uses the movie's fps. |
crf |
int | 30 |
Video quality: lower is sharper and larger. |
Returns: A dictionary of the files written: the videos, a poster image (the last frame) and a metadata file.
Python: errors
Every error is importable as osp.<Name> and catchable as osp.OptionSpaceError. The HTTP status is the REST API's answer that the Python error stands for; "none" means the package raises it itself.
| Error | HTTP status | When | Attributes |
|---|---|---|---|
OptionSpaceError |
any | The base class of every error below: except osp.OptionSpaceError catches them all. |
|
InvalidRequestError |
400, 422 | The request is not valid: an unknown root, column or field, a snapshot that is not "EOD" or "HH:MM", a value out of range, or a date range too wide for your plan's frequency. A 422 means a parameter is missing or has the wrong type. The message names each problem. |
detail: The server's hints, such as roots, estimated_rows or errors.status: The HTTP status, 400 or 422. |
AuthenticationError |
401 | The key is missing, invalid, revoked or expired. The server does not say which. | |
EntitlementError |
403 | The key is valid but your plan does not include this data: a date outside your window, a snapshot outside your frequency, a route your plan does not include, or quotes asked for before they are released. Volume and open interest for a session not out yet give reason "not_yet_historical". |
reason: The server's reason code, when it sends one, such as not_yet_historical.detail: The server's other hints, such as first_unavailable (the first date not out yet). |
NotFoundError |
404 | No data for that symbol, date, snapshot or expiration. When the fit exists but did not pass our quality checks, reason is "withheld_for_quality"; a route or event type not switched on yet gives "not_enabled". |
reason: The server's reason code, when it sends one: withheld_for_quality or not_enabled.detail: The server's other hints, such as withheld_for_quality (the expiries withheld) or withheld_snapshots. |
MaxCreditsExceeded |
412 | A research call could cost more than the max_credits you passed. It was not run. |
credits_estimate: The server's estimate of the call's cost.credits_cap: The most the call could cost.max_credits: The limit you passed. |
ResponseTooLarge |
413 | One request would return more rows than the per-request limit. Narrow it (an expiration, strike range or side for chain(); fewer dates for tenor_history() and rates()). A response is never cut off. |
cap: The per-request row limit. |
QuotaExceeded |
429 | A limit was reached (today's credits, rows, calls at a time, or the research queue), or the client refused to start a multi-request pull that would not fit what is left today. | reason: Which limit, such as daily_credits or projected_credits.retry_after_seconds: When to try again, if the server said.usage: Today's usage.limits: Your limits. |
ServerError |
5xx | A server error that persisted after the client's retries. | |
JobTimeout |
none | A research call became a background job and was still running when job_timeout ran out. The job keeps going: collect it with client.job(job_id) within an hour of it finishing. |
job_id: The job to collect.poll_url: The job's REST address.status: Its last state: queued or running. |
ContractError |
none | A response did not have the shape this package version expects. Upgrade the package, or report it to [email protected]. | |
CredentialError |
none | No usable key: none saved or set, the credential store is locked, the key file is unsafe, or a saved key belongs to another server. The message says what to do. | |
CacheMismatch |
none | A FrameCache entry does not match its query, or has changed on disk. Raised instead of returning the wrong data. |
Constants: osp.__version__: The installed package version.
Response fields: shared rules
Every route's own fields are listed under the route below, with each field's type, unit, when it can be empty and an example. Field names are the same in REST responses, Python results and MCP results. These rules apply to all of them.
- Volatilities are decimals (
0.20= 20%). Prices are in USD per share: multiply by the contract multiplier (usually 100) for one contract's value. Percent-of-spot fields say so in their unit (3.0= 3%). - Times are US Eastern (
HH:MM); dates areYYYY-MM-DD; job times are UTC. - Every response starts with the same identity fields:
symbol,root,data_date,snapshot_time,session_close,granularity,source,units,returned_rowsandnotes. A field that does not apply to the answer is empty (for exampledata_dateon a date-range answer). - Empty is never zero. A missing value is
null(or absent, where the field's row says so), never0. Astatusfield says why a value is missing.
Snapshot times and EOD
Intraday snapshots run from 09:32 to 15:57 ET each full session. The 30m frequency covers 09:32, 10:02 and every 30 minutes to 15:32, plus 15:57; 5m covers every five minutes from 09:32 plus 15:57; eod covers 15:57 only. EOD means the last snapshot at or before the official close within your frequency. On an early close (13:00) that is 13:00 for 1m, 12:57 for 5m and 12:32 for 30m. A minute missing from the data is never filled in; /snapshots lists what exists.
Quote availability
Quote fields (bid, ask, mid, bid_size, ask_size, iv_bid, iv_ask, iv_mid, quote_state) for a session are served from the next session's 09:30 ET open. Wednesday's quotes become available Thursday at 09:30; Friday's on Monday at 09:30 (Tuesday after a Monday holiday). Until then the quote fields are absent from the response rather than null, and asking /chain for one explicitly is refused with 403. Fitted values are not affected.
quote_state says how to read a quote: two_sided (bid and ask volatilities both exist), ask_only (only the ask bounds the volatility), no_vol (the price is past the early-exercise boundary, so no volatility exists), no_quote (neither side quoted). The raw prices are passed through unchanged.
Fit quality
Each fitted expiry carries a quality block: on /smile it is a field of the response, on /chain a list with one entry per root and expiration, and on /surface a field of each expiry. status is checked, or not_yet_checked (today's live fits, and any fit not checked yet: checks run after the close). Its other fields are listed under /smile. The codes list gives reasons the expiry's fit may be less reliable:
| Code | What it means |
|---|---|
term_structure_outlier |
This expiry's at-the-money level is out of line with the expiries around it, or its total variance is lower than a shorter expiry's. Check for an event (earnings, dividends) before treating the difference as a signal. |
forward_jump |
The expiry's forward or borrow jumped at this snapshot and reverted shortly after. It may be a brief market move or a data glitch; for a stable series, use the neighbouring snapshots. |
vol_jump |
The expiry's volatility level, skew or a wing jumped at this snapshot and reverted shortly after. As above, check the neighbouring snapshots. |
call_put_disagree |
Calls and puts at similar strikes imply different volatilities beyond what their spreads explain. This often points to a dividend, borrow or forward that does not match the market; use the forward, borrow and in-the-money values for this expiry with caution. |
one_quote_dominates |
A single quote carried most of the weight in this fit, so the smile mostly reflects that one strike. Confirm against other strikes or neighbouring snapshots. |
borrow_at_limit |
The implied borrow (dividend and borrow cost) could not be pinned down from the quotes and was set at a bound. The smile and prices are usable; treat the borrow and forward for this expiry with caution. |
smile_arbitrage |
Part of the fitted smile implies a small negative probability, so a butterfly priced off the fit would have negative value. Avoid this expiry for density or butterfly calculations at the affected strikes. |
quotes_not_convex |
The market quotes themselves contain a butterfly that could be bought for a credit after paying every spread, which usually means a stale or wrong quote. The fit stays smooth rather than follow it, so it may not match every quote; check the affected strikes. |
spot_moved |
The stock price implied by the option quotes differs from the recorded stock price by more than 1%. Exercise values, Greeks hedged against the stock, and the borrow and dividend series may be affected; the smile itself still fits its quotes. Stocks and ETFs only. |
stale_spot |
The recorded stock price at this snapshot looks out of date, because several short expiries agree on the same gap from it. The forwards and smiles are fine, but the implied borrow for short expiries will look unusually large; skip this snapshot for borrow series. Set on every expiry of the snapshot. Stocks and ETFs only. |
wide_quotes |
Too few call and put pairs had spreads tight enough to pin down the forward precisely. The forward, and with it the borrow, is less certain than usual for this expiry. Stocks and ETFs only. |
Common fields
Fields that many answers carry, described once here. Each route says which of them it carries; "the identity fields" are symbol, root, data_date, snapshot_time, session_close, granularity, returned_rows, notes. The credits block comes with research answers; the quality block with /smile, /chain (one per root and expiration) and /surface (inside each expiry).
| Field | Type and unit | Can be empty | Meaning | Example |
|---|---|---|---|---|
symbol |
text | On answers not about one symbol (/symbols). |
The product symbol you asked for. | "SPY" |
root |
text | When the answer mixes roots or has none (a discovery answer, or SPX without root=); then each row or expiry names its own. |
The listed root the data belongs to. One product can list under several roots (SPX lists as SPX and SPXW). | "SPXW" |
data_date |
text, YYYY-MM-DD | On answers that cover a date range (the range is in start and end). |
The trading session. | "2025-04-10" |
snapshot_time |
text, HH:MM, US Eastern | On answers that are not tied to one snapshot (a range, or a whole session). | The time of the quote sample. A request for EOD returns the time it resolved to. | "15:57" |
gated |
true or false | True when the quote fields (bid, ask, mid, iv_*, quote_state) are included. False on the current session, before the quotes are released. |
true |
|
session_close |
text, HH:MM, US Eastern | On answers that are not tied to one session. | The official close of that session. | "16:00" |
granularity |
text | How often your plan's snapshots are taken (on research answers: the frequency the call used). Values: 1m, 5m, 30m, eod. |
"30m" |
|
returned_rows |
integer | How many rows (or options, points, events) the answer holds. | 386 |
|
notes |
list of text | An empty list when there is nothing to say. | Specific limitations of this answer, as sentences, such as a snapshot after the official close or a field not yet filled in. Read them before using the numbers. | |
credits |
object | What a research call cost. | ||
credits.charged |
integer | Credits charged for this call. | 3 |
|
credits.estimate |
integer, credits | The server's estimate of the call's cost, made from its parameters before it ran. | 4 |
|
credits.cap |
integer, credits | The most the call can cost. | 4 |
|
credits.work_seconds |
number, seconds | Server time spent working on the call, not counting time waiting in a queue. | 2.41 |
|
credits.setting |
text | Which estimate rule applied: the observation frequency of the call, or default for a route with one rule. Values: default, eod, 5m, 1m. |
"eod" |
|
credits.price_version |
text | The price list the charge used. | "2026-10-beta-3" |
|
quality |
object | The fit-quality block for the expiry: see fit quality. | ||
quality.status |
text | Whether the expiry's fit has been checked yet: checks run after the close. Values: checked, not_yet_checked. |
"checked" |
|
quality.share_inside |
number, fraction from 0 to 1 | When no quote could be checked, or the fit is not checked yet. | Share of checked quotes where the fitted price lies within the market (between the bid minus half a cent and the ask plus half a cent). | 0.97 |
quality.miss_spreads |
number, half-spreads | When every checked quote is inside, or the fit is not checked yet. | For the checked quotes whose fitted price lies outside that range: how far outside, in half-spreads, as the median (1.0 = one half-spread beyond the bid or ask). | 0.6 |
quality.miss_usd |
number, USD per share | When every checked quote is inside, or the fit is not checked yet. | The same distance in dollars, median over the same quotes. | 0.03 |
quality.n_checked |
integer | When the fit is not checked yet. | How many quotes were checked: out-of-the-money quotes with a positive bid and an ask above it, inside the fitted strike range. | 64 |
quality.codes |
list of text | An empty list when there are none; null when the fit is not checked yet. | Reasons the expiry's fit may be less reliable; see fit quality. Values: term_structure_outlier, forward_jump, vol_jump, call_put_disagree, one_quote_dominates, borrow_at_limit, smile_arbitrage, quotes_not_convex, spot_moved, stale_spot, wide_quotes. |
REST: account and discovery
GET /entitlements
Your plan, access, limits, today's usage and the price list. Always answers, even when today's allowance is used up.
GET https://optionspace.ai/api/v1/data/entitlements
No parameters.
Returns: tier, status, entitlements, key_id, limits, usage, prices.
Python: client.entitlements()
Response fields (34)
| Field | Type and unit | Can be empty | Meaning | Example |
|---|---|---|---|---|
org_id |
text | Your account's identifier. | "org_8f2c91" |
|
org_name |
text | Your account's name. | "Acme Research" |
|
tier |
text | Your plan's name, such as free, basic, value or pro. |
"value" |
|
status |
text | Your account's status. Keys work while it is active or past_due. Values: active, past_due, and others when access has ended. |
"active" |
|
entitlements |
object | Your grants, such as granularity (your snapshot frequency) and a sample_window on a sample plan. Keys depend on the plan. |
||
key_id |
text | The public identifier of the key you used. It is not the secret. | "osk_06ce5c253284" |
|
meter |
text | What your daily allowance is counted in. Values: credits (normally), requests. |
"credits" |
|
limits |
object | Your limits. | ||
limits.credits_per_day |
integer, credits | Daily credit allowance, shared by every key on the account. It resets at midnight UTC. | 2500 |
|
limits.requests_per_day |
integer | Daily request count limit. Enforced only when meter is requests. |
2500 |
|
limits.rows_per_day |
integer, rows | Daily row limit across all calls. | 1000000 |
|
limits.concurrent |
integer | How many calls may run at a time. | 2 |
|
limits.research_running |
integer | How many research calls may run at a time. | 1 |
|
limits.research_queued |
integer | How many more research calls may wait behind the running one. | 2 |
|
usage |
object | Today's usage (UTC day). | ||
usage.credits_today |
integer, credits | Credits used today. | 214 |
|
usage.credits_reserved |
integer, credits | Credits held by research calls still running. The unused part is released when they finish. | 4 |
|
usage.requests_today |
integer | Calls made today. | 198 |
|
usage.rows_today |
integer, rows | Rows returned today. | 40512 |
|
usage.concurrent_now |
integer | Calls running right now. | 0 |
|
usage.research_jobs_now |
integer | Research calls running or waiting right now. | 0 |
|
prices |
object | The price list in force. | ||
prices.version |
text | The price list's version. | "2026-10-beta-3" |
|
prices.effective |
text, YYYY-MM-DD | The date the price list took effect. | "2026-10-21" |
|
prices.lookups |
object | The price of each lookup route, in credits, keyed by route name. | ||
prices.periods_per_call |
object | How much one call of a time-series lookup (/tenor-history, /iv-rv, /rates) may cover. A period is one snapshot of one session (and one root, where a symbol lists under several). |
||
prices.periods_per_call.rule |
text | The rule, in words. | "a period is one snapshot of one session (and one root); a call covers at most max periods, and a longer range is the same data in more calls" |
|
prices.periods_per_call.max |
object | The most periods one call may cover, keyed by route name. A longer range returns 400 too_many_periods with suggested_end. |
"{\"tenor-history\": 10000}" |
|
prices.periods_per_call.per_session |
object | Periods in one full session at each sampling: eod 1, 30m 14, 5m 78, 1m 386 (fewer on an early close). snapshot=EOD or HH:MM counts 1 a session. |
"{\"eod\": 1, \"30m\": 14, \"5m\": 78, \"1m\": 386}" |
|
prices.research |
object | How research routes are charged. | ||
prices.research.rule |
text | The charging rule, in words. | ||
prices.research.cap_multiple |
integer | The cap as a multiple of the estimate. At 1 a call never costs more than its estimate. |
1 |
|
prices.research.estimate_seconds |
object | The numbers each research route's estimate is made from, keyed by route and setting (such as default or 1m). Each entry has base_seconds and per, the seconds added for each unit of the request (for example sessions: each session in the date range). Estimated seconds = base_seconds plus each unit count times its per value, times the number of symbols; the estimate in credits is that rounded up, at least 1. Use estimate=true on a call to get its estimate directly. |
"{\"default\": {\"base_seconds\": 0.646, \"per\": {\"sessions\": 0.0185}}}" |
|
prices.research.fixed_credits |
object | Research settings priced per call instead of by measured time, keyed by route and setting. A call at such a setting costs that price when it succeeds, however long it runs. |
GET /symbols
Every symbol with the sessions it covers.
GET https://optionspace.ai/api/v1/data/symbols
No parameters.
Returns: symbols (each with its first and last session and number of sessions); sample_window when your access is limited to one.
Python: client.symbols()
Also carries the common fields: the identity fields.
Response fields (6)
| Field | Type and unit | Can be empty | Meaning | Example |
|---|---|---|---|---|
symbols |
list of objects | Every symbol with its coverage. Coverage is for the product as a whole; your plan's dates may be narrower. | ||
symbols[].symbol |
text | On answers not about one symbol (/symbols). |
The product symbol you asked for. | "SPY" |
symbols[].start_date |
text, YYYY-MM-DD | The first session with data. | "2017-01-03" |
|
symbols[].end_date |
text, YYYY-MM-DD | The last session with data. | "2026-10-19" |
|
symbols[].n_days |
integer | How many sessions have data. | 2461 |
|
sample_window |
object | When your plan is not limited to a window. | The date window your plan can read, when it is limited to one. |
GET /snapshots
The snapshot times your key can retrieve for one session.
GET https://optionspace.ai/api/v1/data/snapshots?symbol=SPY&data_date=2025-04-10
| Parameter | Type | Default | Meaning and allowed values |
|---|---|---|---|
symbol |
text | required | The product symbol, such as SPY or SPX. /symbols lists them. |
data_date |
date | required | The trading session, YYYY-MM-DD. |
Returns: snapshots, after_close (times after an early close), eod (what EOD resolves to), session_close, granularity.
Python: client.snapshots()
Also carries the common fields: the identity fields.
Response fields (3)
| Field | Type and unit | Can be empty | Meaning | Example |
|---|---|---|---|---|
snapshots |
list of text, list of HH:MM, US Eastern | The snapshot times your key can retrieve for the session, in order. A minute missing from the data is never filled in. | "09:32" |
|
after_close |
list of text, HH:MM, US Eastern | An empty list on a full session. | Times in snapshots that fall after an early close. |
|
eod |
text, HH:MM, US Eastern | When no snapshot at or before the close is available to you. | The time EOD resolves to for your key on this session. |
"15:57" |
GET /expirations
The expirations listed at one snapshot.
GET https://optionspace.ai/api/v1/data/expirations?symbol=SPY&data_date=2025-04-10
| Parameter | Type | Default | Meaning and allowed values |
|---|---|---|---|
symbol |
text | required | The product symbol, such as SPY or SPX. /symbols lists them. |
data_date |
date | required | The trading session, YYYY-MM-DD. |
snapshot |
text | none | EOD or HH:MM US Eastern time. Leaving it out means EOD: the last snapshot at or before the official close that your plan covers. The time used comes back as snapshot_time. |
root |
text | none | The listed root, for a symbol that lists under several. Leave it out to let the server choose; see SPX and SPXW. |
Returns: expirations: one entry per root and expiration, with dte.
Python: client.expirations()
Also carries the common fields: the identity fields.
Response fields (7)
| Field | Type and unit | Can be empty | Meaning | Example |
|---|---|---|---|---|
expirations |
list of objects | One entry per listed root and expiration at the snapshot. | ||
expirations[].root |
text | When the answer mixes roots or has none (a discovery answer, or SPX without root=); then each row or expiry names its own. |
The listed root the data belongs to. One product can list under several roots (SPX lists as SPX and SPXW). | "SPXW" |
expirations[].expiration |
text, YYYY-MM-DD | The contract's expiration date. | "2025-04-17" |
|
expirations[].dte |
integer, calendar days | Calendar days from data_date to expiration. |
7 |
|
withheld_for_quality |
list of objects | An empty list when none was withheld. | Expirations fitted at this snapshot whose fit did not pass our quality checks, so they are not in expirations. Each names its root and expiration. |
|
withheld_for_quality[].root |
text | When the answer mixes roots or has none (a discovery answer, or SPX without root=); then each row or expiry names its own. |
The listed root the data belongs to. One product can list under several roots (SPX lists as SPX and SPXW). | "SPXW" |
withheld_for_quality[].expiration |
text, YYYY-MM-DD | The contract's expiration date. | "2025-04-17" |
REST: data lookups
GET /smile
The fitted smile for one expiry at one snapshot.
GET https://optionspace.ai/api/v1/data/smile?symbol=SPY&data_date=2025-04-10&expiration=2025-04-17
| Parameter | Type | Default | Meaning and allowed values |
|---|---|---|---|
symbol |
text | required | The product symbol, such as SPY or SPX. /symbols lists them. |
data_date |
date | required | The trading session, YYYY-MM-DD. |
expiration |
date | required | The option expiration date, YYYY-MM-DD. /expirations lists them. |
snapshot |
text | none | EOD or HH:MM US Eastern time. Leaving it out means EOD: the last snapshot at or before the official close that your plan covers. The time used comes back as snapshot_time. |
root |
text | none | The listed root, for a symbol that lists under several. Leave it out to let the server choose; see SPX and SPXW. |
Returns: Expiry values (forward, atm_vol, rate, q, T_vol, rmse, quality, fitted_curve, ...) and one entry per strike and side with fitted_vol, z, the greeks and, on past sessions, the quotes.
Python: client.smile()
Also carries the common fields: the identity fields, gated, the quality block.
Response fields (45)
| Field | Type and unit | Can be empty | Meaning | Example |
|---|---|---|---|---|
expiration |
text, YYYY-MM-DD | The contract's expiration date. | "2025-04-17" |
|
dte |
integer, calendar days | Calendar days from data_date to expiration. |
7 |
|
forward |
number, USD | The forward price implied by the expiry's options. For VIX, use forward: each expiry's forward sits at its own futures level. |
525.31 |
|
spot |
number, USD | The underlying price at the snapshot. For VIX this is the VIX index, not a futures price. | 524.58 |
|
s_implied |
number, USD | When it could not be determined for the expiry. | The spot price implied by that expiry's options. | 524.61 |
rate |
number, decimal (0.041 = 4.1%) | The risk-free rate for that expiry, continuously compounded and annualized. | 0.0412 |
|
q |
number, decimal per year (0.012 = 1.2%) | The continuous dividend or borrow yield implied for that expiry. On VIX it restates the spot-to-futures gap and is not a borrow cost. | 0.0118 |
|
T |
number, years | Time to expiry used for discounting. | 0.0192 |
|
T_vol |
number, years | Time to expiry used for volatility. It counts trading hours more than nights and weekends. | 0.0168 |
|
T_borrow |
number, years | Time to expiry used for the dividend and borrow term. | 0.0192 |
|
atm_vol |
number, decimal vol | The expiry's at-the-money volatility. | 0.1423 |
|
rmse |
number, decimal vol (0.001 = 0.1 vol point) | Typical gap between the quotes' implied volatility and the fitted curve at the same strikes. Smaller means the curve fits the quotes more closely. | 0.001 |
|
n_options |
integer | How many options (strikes times sides) the answer holds. | 236 |
|
n_fitted |
integer | How many of those options lie inside the fitted range. | 198 |
|
callsputs |
object | The calls, as parallel lists: entry i of every list is the same strike. puts has the same lists for the puts. |
||
calls.strikesputs.strikes |
list of number, USD | Strike prices, lowest first. | ||
calls.fittedputs.fitted |
list of number, decimal vol | Fitted implied volatility at each strike. | ||
calls.deltaputs.delta |
list of number | Change in option value per $1 move in the underlying. | 0.48 |
|
calls.gammaputs.gamma |
list of number | Change in delta per $1 move in the underlying. | 0.0215 |
|
calls.vegaputs.vega |
list of number, USD per share | Change in option value per 1 volatility point (1%). | 0.29 |
|
calls.thetaputs.theta |
list of number, USD per share per day | On v3.1 fits. | Change in option value per calendar day. Fits made before methodology v3.1 only: v3.1 fits carry theta_1d and theta_next_open instead. |
-0.31 |
calls.theta_1dputs.theta_1d |
list of number, USD per share | On fits made before methodology v3.1. | Change in option value if held, unexercised, to the same time on the next trading day, with the underlying price and volatility unchanged. An amount, not a rate: before a weekend or holiday it covers the days the market is closed. It includes carry, so in-the-money puts can show a positive value, mostly on Thursdays. Never larger than the time value: an option worth exercising is not assumed held, and on the day before an ex-dividend date the underlying opens lower by the dividend. | -0.44 |
calls.theta_next_openputs.theta_next_open |
list of number, USD per share | On fits made before methodology v3.1. | The same change, until 09:30 ET on the next trading day. It shrinks through the trading day. | -0.24 |
calls.rhoputs.rho |
list of number, USD per share | Change in option value per 1% change in rates. | 0.05 |
|
calls.vannaputs.vanna |
list of number | Change in delta per 1 volatility point. | 0.004 |
|
calls.volgaputs.volga |
list of number | Change in vega per 1 volatility point. | 0.012 |
|
calls.zputs.z |
list of number | Standardized moneyness: the log of strike over forward, divided by ATM volatility times the square root of T_vol. 0 is at the forward; negative is below it. |
-0.42 |
|
calls.is_otmputs.is_otm |
list of true or false | Null when the forward is unknown. | True when the option is out of the money against the forward (a call above it, a put below it). | |
calls.volumeputs.volume |
list of integer, contracts | Contracts traded so far that session at each strike, through the snapshot minute. Empty for now: per-strike volume is on /chain with columns=volume,open_interest. |
||
calls.open_interestputs.open_interest |
list of integer, contracts | Open contracts at each strike at the end of the previous session. Empty for now: per-strike open interest is on /chain with columns=volume,open_interest. |
||
calls.iv_bidputs.iv_bid |
list of number, decimal vol | Absent until the session's quotes are released; null where no volatility matches the price. Read it with quote_state. |
Implied volatility of the bid price. | 0.2121 |
calls.iv_askputs.iv_ask |
list of number, decimal vol | Absent until the session's quotes are released; null where no volatility matches the price. | Implied volatility of the ask price. | 0.2168 |
calls.bidputs.bid |
list of number, USD per share | Absent until the session's quotes are released (the next session's 09:30 ET open); null where that side was not quoted. | Quoted bid price. | 6.8 |
calls.askputs.ask |
list of number, USD per share | Absent until the session's quotes are released; null where that side was not quoted. | Quoted ask price. | 6.9 |
calls.iv_midputs.iv_mid |
list of number, decimal vol | Absent until the session's quotes are released; null without a usable mid. | Implied volatility of the mid price. | 0.2144 |
calls.midputs.mid |
list of number, USD per share | Absent until the session's quotes are released; null without a two-sided quote. | Midpoint of bid and ask. | 6.85 |
calls.is_wing_extrapolatedputs.is_wing_extrapolated |
list of true or false | True when the strike lies outside the fitted range, so the value extends the wing rather than sitting among the quotes used. Values: true, false (1, 0 on /chain rows). |
false |
|
calls.quote_stateputs.quote_state |
list of text | Absent until the session's quotes are released. | How to read the quote: two_sided (bid and ask volatilities both exist), ask_only (only the ask bounds the volatility), no_vol (the price is past the early-exercise boundary, so no volatility exists), no_quote (neither side quoted). |
"two_sided" |
z_lo |
number | For older curve types with no evaluated curve. | The lower end of the fitted range, in z. |
-2.9 |
z_hi |
number | For older curve types with no evaluated curve. | The upper end of the fitted range, in z. |
2.4 |
fitted_curve |
object | Null for older curve types, which have only the per-strike values. | The fitted smile evaluated on a grid, for drawing a smooth curve. curve_frame() turns it into a DataFrame. |
|
fitted_curve.z |
list of number | Grid points in standardized moneyness. | ||
fitted_curve.strike |
list of number, USD | The strike at each grid point. | ||
fitted_curve.sigma |
list of number, decimal vol | The fitted volatility at each grid point. | ||
fitted_curve.is_wing_extrapolated |
list of true or false | True where the grid point lies outside the fitted range (z_lo to z_hi). |
GET /term-structure
At-the-money volatility, forward and fitted strike range for every listed expiry at one snapshot, in one request. The same values /smile gives for each expiry.
GET https://optionspace.ai/api/v1/data/term-structure?symbol=SPY&data_date=2025-04-10
| Parameter | Type | Default | Meaning and allowed values |
|---|---|---|---|
symbol |
text | required | The product symbol, such as SPY or SPX. /symbols lists them. |
data_date |
date | required | The trading session, YYYY-MM-DD. |
snapshot |
text | none | EOD or HH:MM US Eastern time. Leaving it out means EOD: the last snapshot at or before the official close that your plan covers. The time used comes back as snapshot_time. |
root |
text | none | The listed root. Needed when the symbol lists under several roots that day: a term structure uses one root. |
Returns: expiries: one entry per listed expiry, shortest first, with expiration, dte, atm_vol, forward, z_lo and z_hi. If any listed expiry has no fit, the request fails and names it in missing, rather than leave it out.
Python: client.term_structure()
Also carries the common fields: the identity fields.
Response fields (14)
| Field | Type and unit | Can be empty | Meaning | Example |
|---|---|---|---|---|
expiries |
list of objects | One entry per listed expiry, shortest first. | ||
expiries[].symbol |
text | On answers not about one symbol (/symbols). |
The product symbol you asked for. | "SPY" |
expiries[].root |
text | When the answer mixes roots or has none (a discovery answer, or SPX without root=); then each row or expiry names its own. |
The listed root the data belongs to. One product can list under several roots (SPX lists as SPX and SPXW). | "SPXW" |
expiries[].data_date |
text, YYYY-MM-DD | On answers that cover a date range (the range is in start and end). |
The trading session. | "2025-04-10" |
expiries[].snapshot_time |
text, HH:MM, US Eastern | On answers that are not tied to one snapshot (a range, or a whole session). | The time of the quote sample. A request for EOD returns the time it resolved to. | "15:57" |
expiries[].expiration |
text, YYYY-MM-DD | The contract's expiration date. | "2025-04-17" |
|
expiries[].dte |
integer, calendar days | Calendar days from data_date to expiration. |
7 |
|
expiries[].atm_vol |
number, decimal vol | The expiry's at-the-money volatility (the atm_vol of its smile). |
0.1423 |
|
expiries[].forward |
number, USD | The forward price implied by the expiry's options. For VIX, use forward: each expiry's forward sits at its own futures level. |
525.31 |
|
expiries[].z_lo |
number | For older curve types with no evaluated curve. | The lower end of the expiry's fitted range, in z (the z_lo of its smile). |
-2.9 |
expiries[].z_hi |
number | For older curve types with no evaluated curve. | The upper end of the expiry's fitted range, in z (the z_hi of its smile). |
2.4 |
withheld_for_quality |
list of objects | An empty list when none was withheld. | Expirations fitted at this snapshot whose fit did not pass our quality checks, so they are not in expiries. Each names its root and expiration. |
|
withheld_for_quality[].root |
text | When the answer mixes roots or has none (a discovery answer, or SPX without root=); then each row or expiry names its own. |
The listed root the data belongs to. One product can list under several roots (SPX lists as SPX and SPXW). | "SPXW" |
withheld_for_quality[].expiration |
text, YYYY-MM-DD | The contract's expiration date. | "2025-04-17" |
GET /chain
Every fitted option for a session at one snapshot, across all expiries unless filtered. One snapshot per request.
GET https://optionspace.ai/api/v1/data/chain?symbol=SPY&data_date=2025-04-10
| Parameter | Type | Default | Meaning and allowed values |
|---|---|---|---|
symbol |
text | required | The product symbol, such as SPY or SPX. /symbols lists them. |
data_date |
date | required | The trading session, YYYY-MM-DD. |
snapshot |
text | none | One snapshot: EOD or HH:MM US Eastern time. Leaving it out means EOD. |
root |
text | none | Only this listed root. Left out, every root the symbol lists is returned and each row names its root. |
expiration |
date | none | Only this expiration. |
strike_min |
number | none | Only strikes at or above this price (USD). |
strike_max |
number | none | Only strikes at or below this price (USD). |
put_call |
text | none | call or put only. |
columns |
list | none | Only these columns, comma-separated or repeated. Identity columns are always included; quote columns exist for past sessions only. Allowed: data_date, snapshot_time, symbol, expiration, strike, put_call, company_name, sector, security_type, dte, spot, forward, q, r, T_vol, z, fitted_vol, fitted_price, delta, gamma, vega, theta, rho, vanna, volga, is_wing_extrapolated, bid, ask, bid_size, ask_size, iv_bid, iv_ask, iv_mid, theta_1d, theta_next_open. On request (not in the default columns), for past sessions only: volume, open_interest. volume is the contracts traded so far that session up to the snapshot minute; the official end-of-day figure is in /contract-volume. Filtering by expiration keeps a large chain fast. |
format |
text | json |
json, or csv for a CSV file download. |
Returns: rows: identity columns, fitted values and greeks, and quotes on past sessions; quality per root and expiration. More rows than the per-request limit returns 413, never a cut-off response.
Python: client.chain()
Also carries the common fields: symbol, data_date, snapshot_time, session_close, granularity, returned_rows, notes, the quality block.
Response fields (44)
| Field | Type and unit | Can be empty | Meaning | Example |
|---|---|---|---|---|
root |
text | When you did not pass root; each row's symbol then names its root. |
The root you asked for. | "SPXW" |
columns |
list of text, list of field names | The columns each row carries, in order. | ||
rows |
list of objects | One entry per option (expiration, strike and side). | ||
rows[].data_date |
text, YYYY-MM-DD | On answers that cover a date range (the range is in start and end). |
The trading session. | "2025-04-10" |
rows[].snapshot_time |
text, HH:MM, US Eastern | On answers that are not tied to one snapshot (a range, or a whole session). | The time of the quote sample. A request for EOD returns the time it resolved to. | "15:57" |
rows[].symbol |
text | The listed root of the option (for SPX, SPX or SPXW). |
"SPXW" |
|
rows[].expiration |
text, YYYY-MM-DD | The contract's expiration date. | "2025-04-17" |
|
rows[].strike |
number, USD | Strike price. | 520.0 |
|
rows[].put_call |
text | Call or put. Values: call, put. |
"call" |
|
rows[].company_name |
text | When no name is on file. | The underlying's name. | "SPDR S&P 500 ETF Trust" |
rows[].sector |
text | When no label is on file. | The underlying's sector label. | "Technology" |
rows[].security_type |
text | When no label is on file. | What kind of underlying it is, such as stock, ETF or index. | "ETF" |
rows[].dte |
integer, calendar days | Calendar days from data_date to expiration. |
7 |
|
rows[].spot |
number, USD | The underlying price at the snapshot. For VIX this is the VIX index, not a futures price. | 524.58 |
|
rows[].forward |
number, USD | The forward price implied by the expiry's options. For VIX, use forward: each expiry's forward sits at its own futures level. |
525.31 |
|
rows[].q |
number, decimal per year (0.012 = 1.2%) | The continuous dividend or borrow yield implied for that expiry. On VIX it restates the spot-to-futures gap and is not a borrow cost. | 0.0118 |
|
rows[].r |
number, decimal (0.041 = 4.1%) | The risk-free rate for that expiry, continuously compounded and annualized (the same value as rate on other routes). |
0.0412 |
|
rows[].T_vol |
number, years | Time to expiry used for volatility. It counts trading hours more than nights and weekends. | 0.0168 |
|
rows[].z |
number | Standardized moneyness: the log of strike over forward, divided by ATM volatility times the square root of T_vol. 0 is at the forward; negative is below it. |
-0.42 |
|
rows[].fitted_vol |
number, decimal vol (0.20 = 20%) | The fitted implied volatility at the strike. | 0.2144 |
|
rows[].fitted_price |
number, USD per share (multiply by the contract multiplier, usually 100, for one contract) | The option price at the fitted volatility. | 6.85 |
|
rows[].delta |
number | Change in option value per $1 move in the underlying. | 0.48 |
|
rows[].gamma |
number | Change in delta per $1 move in the underlying. | 0.0215 |
|
rows[].vega |
number, USD per share | Change in option value per 1 volatility point (1%). | 0.29 |
|
rows[].theta |
number, USD per share per day | On v3.1 fits. | Change in option value per calendar day. Fits made before methodology v3.1 only: v3.1 fits carry theta_1d and theta_next_open instead. |
-0.31 |
rows[].rho |
number, USD per share | Change in option value per 1% change in rates. | 0.05 |
|
rows[].vanna |
number | Change in delta per 1 volatility point. | 0.004 |
|
rows[].volga |
number | Change in vega per 1 volatility point. | 0.012 |
|
rows[].is_wing_extrapolated |
integer | True when the strike lies outside the fitted range, so the value extends the wing rather than sitting among the quotes used. Values: true, false (1, 0 on /chain rows). |
false |
|
rows[].bid |
number, USD per share | Absent until the session's quotes are released (the next session's 09:30 ET open); null where that side was not quoted. | Quoted bid price. | 6.8 |
rows[].ask |
number, USD per share | Absent until the session's quotes are released; null where that side was not quoted. | Quoted ask price. | 6.9 |
rows[].bid_size |
integer, contracts | Absent until the session's quotes are released; null where not quoted. | Contracts quoted at the bid. | 120 |
rows[].ask_size |
integer, contracts | Absent until the session's quotes are released; null where not quoted. | Contracts quoted at the ask. | 85 |
rows[].iv_bid |
number, decimal vol | Absent until the session's quotes are released; null where no volatility matches the price. Read it with quote_state. |
Implied volatility of the bid price. | 0.2121 |
rows[].iv_ask |
number, decimal vol | Absent until the session's quotes are released; null where no volatility matches the price. | Implied volatility of the ask price. | 0.2168 |
rows[].iv_mid |
number, decimal vol | Absent until the session's quotes are released; null without a usable mid. | Implied volatility of the mid price. | 0.2144 |
rows[].theta_1d |
number, USD per share | On fits made before methodology v3.1. | Change in option value if held, unexercised, to the same time on the next trading day, with the underlying price and volatility unchanged. An amount, not a rate: before a weekend or holiday it covers the days the market is closed. It includes carry, so in-the-money puts can show a positive value, mostly on Thursdays. Never larger than the time value: an option worth exercising is not assumed held, and on the day before an ex-dividend date the underlying opens lower by the dividend. | -0.44 |
rows[].theta_next_open |
number, USD per share | On fits made before methodology v3.1. | The same change, until 09:30 ET on the next trading day. It shrinks through the trading day. | -0.24 |
rows[].quote_state |
text | Absent until the session's quotes are released. | How to read the quote: two_sided (bid and ask volatilities both exist), ask_only (only the ask bounds the volatility), no_vol (the price is past the early-exercise boundary, so no volatility exists), no_quote (neither side quoted). |
"two_sided" |
rows[].volume |
integer, contracts | When the day's volume has not been loaded; 0 means no trades. |
On /chain: contracts traded so far that session, through the snapshot minute. On /contract-volume: the official end-of-day volume for the session. |
63 |
rows[].open_interest |
integer, contracts | When the day's open-interest figure has not been loaded; 0 means none. |
Open contracts at the end of the previous session (the figure published before this session opened). | 5236 |
quality |
list of objects | The fit-quality block for each root and expiration in the answer; see fit quality. | ||
quality[].root |
text | The root the block belongs to. | "SPXW" |
|
quality[].expiration |
text, YYYY-MM-DD | The expiration the block belongs to. | "2025-04-17" |
GET /atm-timeseries
At-the-money volatility through one session for one expiry.
GET https://optionspace.ai/api/v1/data/atm-timeseries?symbol=SPY&data_date=2025-04-10&expiration=2025-04-17
| Parameter | Type | Default | Meaning and allowed values |
|---|---|---|---|
symbol |
text | required | The product symbol, such as SPY or SPX. /symbols lists them. |
data_date |
date | required | The trading session, YYYY-MM-DD. |
expiration |
date | required | The option expiration date, YYYY-MM-DD. /expirations lists them. |
root |
text | none | The listed root. Left out, the root that lists the expiry is served, or SPXW on the SPX monthlies where both do. |
Returns: points: one per snapshot, with snapshot_time, sigma_atm, forward, n_obs; and root, the root served.
Python: client.atm_intraday()
Also carries the common fields: symbol, data_date, snapshot_time, session_close, granularity, returned_rows, notes.
Response fields (9)
| Field | Type and unit | Can be empty | Meaning | Example |
|---|---|---|---|---|
root |
text | The root served. Roots are never mixed. | "SPY" |
|
expiration |
text, YYYY-MM-DD | The contract's expiration date. | "2025-04-17" |
|
points |
list of objects | One entry per snapshot your plan covers, in time order. | ||
points[].snapshot_time |
text, HH:MM, US Eastern | The snapshot time. | "10:02" |
|
points[].root |
text | The root of the point (always the root served). | "SPY" |
|
points[].sigma_atm |
number, decimal vol | When unavailable at that snapshot. | At-the-money volatility at that snapshot. | 0.1431 |
points[].forward |
number, USD | When unavailable at that snapshot. | The expiry's forward at that snapshot. | 525.1 |
points[].n_obs |
integer | When unavailable at that snapshot. | How many options went into the fit at that snapshot. | 180 |
withheld_snapshots |
list of text, list of HH:MM, US Eastern | An empty list when none was withheld. | Snapshot times your plan covers at which this expiry's fit did not pass our quality checks; they are left out of points. |
GET /tenor-history
Constant-maturity volatility measures through time for one tenor.
GET https://optionspace.ai/api/v1/data/tenor-history?symbol=SPY&tenor_days=30&start=2025-04-01&end=2025-04-10
| Parameter | Type | Default | Meaning and allowed values |
|---|---|---|---|
symbol |
text | required | The product symbol, such as SPY or SPX. /symbols lists them. |
tenor_days |
integer | required | The constant maturity in calendar days: one of 30, 60, 90, 180, 360. |
start |
date | required | The first session of the range, YYYY-MM-DD. |
end |
date | required | The last session of the range, YYYY-MM-DD. |
snapshot |
text | none | Left out: every snapshot your plan covers. EOD: each session's last snapshot at or before its close. HH:MM: that time each session. |
root |
text | none | The listed root, for a symbol that lists under several. Leave it out to let the server choose; see SPX and SPXW. |
fields |
list | none | Only these measures, comma-separated or repeated. Allowed: atf_iv, skew_25d, curvature_25d, straddle_pct, strangle_25d_pct, rr_25d_pct, atf_iv_clean, atf_iv_clean_se, straddle_pct_clean, event_dates. |
sampling |
text | none | Left out: your plan's frequency. eod, 30m, 5m or 1m: every snapshot at that frequency, up to your plan's. Coarser sampling covers a longer range in one call. Give snapshot or sampling, not both. |
Returns: rows: one per session and snapshot with the measures. One call covers at most 10,000 periods (a period is one snapshot of one session): about 40 years at end of day, 2.8 years at 30m, 6 months at 5m, 5 weeks at 1m. A longer range returns 400 too_many_periods with suggested_end, the last session that fits; ask for the rest in further calls.
Python: client.tenor_history()
Also carries the common fields: the identity fields.
Response fields (20)
| Field | Type and unit | Can be empty | Meaning | Example |
|---|---|---|---|---|
start |
text, YYYY-MM-DD | The first session of the range you asked for. | "2025-01-02" |
|
end |
text, YYYY-MM-DD | The last session of the range you asked for. | "2025-03-31" |
|
tenor_days |
integer, calendar days | The constant maturity, in calendar days. | 30 |
|
snapshot_mode |
text | Which snapshots the rows use: every snapshot your plan covers, each session's end of day, or one time each session. Values: bucket (every snapshot your plan covers), EOD, or an HH:MM time. |
"EOD" |
|
fields |
list of text | The measures each row carries. Values: atf_iv, skew_25d, curvature_25d, straddle_pct, strangle_25d_pct, rr_25d_pct, atf_iv_clean, atf_iv_clean_se, straddle_pct_clean, event_dates. |
||
rows |
list of objects | One entry per session and snapshot. | ||
rows[].data_date |
text, YYYY-MM-DD | On answers that cover a date range (the range is in start and end). |
The trading session. | "2025-04-10" |
rows[].snapshot_time |
text, HH:MM, US Eastern | On answers that are not tied to one snapshot (a range, or a whole session). | The time of the quote sample. A request for EOD returns the time it resolved to. | "15:57" |
rows[].root |
text | The root the row was read from. | "SPY" |
|
rows[].tenor_days |
integer, calendar days | The constant maturity, in calendar days. | 30 |
|
rows[].atf_iv |
number, decimal vol | When no listed expiries span that maturity on that date (expected, not missing data). | At-the-forward implied volatility at that maturity. | 0.1912 |
rows[].skew_25d |
number, decimal vol | When no listed expiries span that maturity, or the value is unavailable. | 25-delta put volatility minus 25-delta call volatility. | 0.0451 |
rows[].curvature_25d |
number, decimal vol | When no listed expiries span that maturity. | Average of the 25-delta put and call volatilities minus ATM volatility. | 0.0083 |
rows[].straddle_pct |
number, percent of spot (3.0 = 3%) | When no listed expiries span that maturity. | The at-the-money straddle's price as a percent of spot. | 4.38 |
rows[].strangle_25d_pct |
number, percent of spot | When no listed expiries span that maturity. | The 25-delta strangle's price as a percent of spot. | 1.12 |
rows[].rr_25d_pct |
number, percent of spot | When no listed expiries span that maturity. | The 25-delta risk reversal's price as a percent of spot. | -0.41 |
rows[].atf_iv_clean |
number, decimal vol | Where atf_iv is empty, and on history not yet filled in (then notes says so). |
ATM volatility with scheduled events removed. For a stock, the next scheduled earnings report when it falls within 90 days; for an index or ETF, Fed decisions within 30 days and CPI and payrolls releases within 15 days. Equal to atf_iv when nothing was removed. |
0.1874 |
rows[].atf_iv_clean_se |
number, decimal vol | When the removal cannot be vouched for; the unadjusted value is then repeated in atf_iv_clean. |
One-standard-error band of atf_iv_clean. 0 when no event was in range. |
0.0021 |
rows[].straddle_pct_clean |
number, percent of spot | Where atf_iv_clean is empty. |
The straddle priced at the clean volatility. | 4.29 |
rows[].event_dates |
list of text, list of YYYY-MM-DD | An empty list when nothing was removed. | The scheduled events removed from the clean value. | "2025-05-28" |
GET /rates
The risk-free rate implied by SPX options, per session and listed expiry.
GET https://optionspace.ai/api/v1/data/rates?start=2025-04-01&end=2025-04-10
| Parameter | Type | Default | Meaning and allowed values |
|---|---|---|---|
start |
date | required | The first session of the range, YYYY-MM-DD. |
end |
date | required | The last session of the range, YYYY-MM-DD. |
expiration |
date | none | Only this listed expiry. |
Returns: rows: data_date, expiration, rate, rate_se. One call covers at most 10,000 sessions.
Python: client.rates()
Also carries the common fields: root, data_date, snapshot_time, session_close, granularity, returned_rows, notes.
Response fields (8)
| Field | Type and unit | Can be empty | Meaning | Example |
|---|---|---|---|---|
symbol |
text | Always SPX: rates are implied from SPX options. |
"SPX" |
|
start |
text, YYYY-MM-DD | The first session of the range you asked for. | "2025-01-02" |
|
end |
text, YYYY-MM-DD | The last session of the range you asked for. | "2025-03-31" |
|
rows |
list of objects | One entry per session and listed expiry. | ||
rows[].data_date |
text, YYYY-MM-DD | On answers that cover a date range (the range is in start and end). |
The trading session. | "2025-04-10" |
rows[].expiration |
text, YYYY-MM-DD | The contract's expiration date. | "2025-04-17" |
|
rows[].rate |
number, decimal (0.041 = 4.1%) | The risk-free rate for that expiry, continuously compounded and annualized. | 0.0412 |
|
rows[].rate_se |
number, decimal | Standard error of rate: how precisely the rate is pinned down. |
0.0003 |
GET /iv-rv
At-the-money implied volatility, realized volatility and the gap between them, at one short tenor, through time.
GET https://optionspace.ai/api/v1/data/iv-rv?symbol=SPY&tenor_days=30&start=2025-04-01&end=2025-04-10
| Parameter | Type | Default | Meaning and allowed values |
|---|---|---|---|
symbol |
text | required | The product symbol, such as SPY or SPX. /symbols lists them. |
tenor_days |
integer | required | The tenor in calendar days: one of 1, 2, 3, 5, 7, 10, 14, 20, 30. The implied side looks that many calendar days ahead; the realized side looks back over that many trading sessions. |
start |
date | required | The first session of the range, YYYY-MM-DD. |
end |
date | required | The last session of the range, YYYY-MM-DD. |
snapshot |
text | none | Left out: every snapshot your plan covers. EOD: each session's last snapshot at or before its close. HH:MM: that time each session. |
root |
text | none | The listed root, for a symbol that lists under several. Leave it out to let the server choose; see SPX and SPXW. |
fields |
list | none | Only these measures, comma-separated or repeated. Allowed: atf_iv, rv, vrp, atf_iv_clean, atf_iv_clean_se, event_dates, rv_clean, vrp_clean. |
sampling |
text | none | Left out: your plan's frequency. eod, 30m, 5m or 1m: every snapshot at that frequency, up to your plan's. Coarser sampling covers a longer range in one call. Give snapshot or sampling, not both. |
Returns: rows: one per session and snapshot with the measures. Short tenors are often empty for symbols without daily expiries (notes says so). One call covers at most 10,000 periods (a period is one snapshot of one session): about 40 years at end of day, 2.8 years at 30m, 6 months at 5m, 5 weeks at 1m. A longer range returns 400 too_many_periods with suggested_end, the last session that fits; ask for the rest in further calls.
Python: client.iv_rv()
Also carries the common fields: the identity fields.
Response fields (18)
| Field | Type and unit | Can be empty | Meaning | Example |
|---|---|---|---|---|
start |
text, YYYY-MM-DD | The first session of the range you asked for. | "2025-01-02" |
|
end |
text, YYYY-MM-DD | The last session of the range you asked for. | "2025-03-31" |
|
tenor_days |
integer, calendar days | The constant maturity, in calendar days. | 30 |
|
snapshot_mode |
text | Which snapshots the rows use: every snapshot your plan covers, each session's end of day, or one time each session. Values: bucket (every snapshot your plan covers), EOD, or an HH:MM time. |
"EOD" |
|
fields |
list of text | The measures each row carries. Values: atf_iv, rv, vrp, atf_iv_clean, atf_iv_clean_se, event_dates, rv_clean, vrp_clean. |
||
rows |
list of objects | One entry per session and snapshot. | ||
rows[].data_date |
text, YYYY-MM-DD | On answers that cover a date range (the range is in start and end). |
The trading session. | "2025-04-10" |
rows[].snapshot_time |
text, HH:MM, US Eastern | On answers that are not tied to one snapshot (a range, or a whole session). | The time of the quote sample. A request for EOD returns the time it resolved to. | "15:57" |
rows[].root |
text | The root the row was read from. | "SPY" |
|
rows[].tenor_days |
integer, calendar days | The constant maturity, in calendar days. | 30 |
|
rows[].atf_iv |
number, decimal vol | When no listed expiries span that maturity on that date (expected, not missing data). | At-the-forward implied volatility at that maturity. | 0.1912 |
rows[].rv |
number, decimal vol | Early in a symbol's history (fewer sessions than the tenor), or when the window reaches a session with no stored prices. | Realized volatility of the underlying over the last tenor_days trading sessions up to that snapshot, overnight moves included, annualized. (The implied side's tenor is calendar days; this one is sessions.) |
0.088 |
rows[].vrp |
number, decimal vol | When atf_iv or rv is empty. |
atf_iv minus rv: positive when options price more movement than the underlying has had. |
0.0729 |
rows[].atf_iv_clean |
number, decimal vol | Where atf_iv is empty, and on history not yet filled in (then notes says so). |
ATM volatility with scheduled events removed. For a stock, the next scheduled earnings report when it falls within 90 days; for an index or ETF, Fed decisions within 30 days and CPI and payrolls releases within 15 days. Equal to atf_iv when nothing was removed. |
0.1874 |
rows[].atf_iv_clean_se |
number, decimal vol | When the removal cannot be vouched for; the unadjusted value is then repeated in atf_iv_clean. |
One-standard-error band of atf_iv_clean. 0 when no event was in range. |
0.0021 |
rows[].event_dates |
list of text, list of YYYY-MM-DD | An empty list when nothing was removed. | The scheduled events removed from the clean value. | "2025-05-28" |
rows[].rv_clean |
number, decimal vol | When rv is empty, or the event calendar is incomplete for a session in the window (a report missing). |
rv with scheduled events taken out: the overnight move into the session after an earnings report (before the open or after the close), a CPI or jobs report, and 2:00 to 3:00 pm on a Fed day. The window counts that much less time. |
0.081 |
rows[].vrp_clean |
number, decimal vol | When atf_iv_clean or rv_clean is empty. Check atf_iv_clean_se on the same row: when it is empty, the implied side could not be fully cleaned. |
atf_iv_clean minus rv_clean: the gap with scheduled events taken out of both sides. |
0.0612 |
GET /expiry-history
One expiry's values across sessions, at one snapshot each session: the per-expiry values /surface gives, through time.
GET https://optionspace.ai/api/v1/data/expiry-history?symbol=SPY&expiration=2025-04-17&start=2025-04-01&end=2025-04-10
| Parameter | Type | Default | Meaning and allowed values |
|---|---|---|---|
symbol |
text | required | The product symbol, such as SPY or SPX. /symbols lists them. |
expiration |
date | required | The option expiration date, YYYY-MM-DD. /expirations lists them. |
start |
date | required | The first session of the range, YYYY-MM-DD. |
end |
date | required | The last session of the range, YYYY-MM-DD. |
snapshot |
text | none | EOD (the default: each session's last snapshot at or before its close that your plan covers) or HH:MM, the same each session. |
root |
text | none | The listed root. Left out, SPXW is served for an SPX monthly listed under both. |
Returns: rows: one per session, oldest first. withheld_sessions lists sessions with no row because the expiry's fit did not pass our quality checks.
Python: client.expiry_history()
Also carries the common fields: the identity fields.
Response fields (25)
| Field | Type and unit | Can be empty | Meaning | Example |
|---|---|---|---|---|
start |
text, YYYY-MM-DD | The first session of the range you asked for. | "2025-01-02" |
|
end |
text, YYYY-MM-DD | The last session of the range you asked for. | "2025-03-31" |
|
expiration |
text, YYYY-MM-DD | The contract's expiration date. | "2025-04-17" |
|
snapshot_mode |
text | Which snapshot each row uses: each session's end of day, or one time each session. Values: EOD, or an HH:MM time. |
"EOD" |
|
fields |
list of text | The values each row carries, after its identity fields. | ||
rows |
list of objects | One entry per session, oldest first. | ||
rows[].data_date |
text, YYYY-MM-DD | On answers that cover a date range (the range is in start and end). |
The trading session. | "2025-04-10" |
rows[].snapshot_time |
text, HH:MM, US Eastern | On answers that are not tied to one snapshot (a range, or a whole session). | The time of the quote sample. A request for EOD returns the time it resolved to. | "15:57" |
rows[].root |
text | When the answer mixes roots or has none (a discovery answer, or SPX without root=); then each row or expiry names its own. |
The listed root the data belongs to. One product can list under several roots (SPX lists as SPX and SPXW). | "SPXW" |
rows[].expiration |
text, YYYY-MM-DD | The contract's expiration date. | "2025-04-17" |
|
rows[].dte |
integer, calendar days | Calendar days from data_date to expiration. |
7 |
|
rows[].sigma_atm |
number, decimal vol | When unavailable for that snapshot. | At-the-money volatility of the expiry. | 0.1423 |
rows[].forward |
number, USD | The forward price implied by the expiry's options. For VIX, use forward: each expiry's forward sits at its own futures level. |
525.31 |
|
rows[].spot |
number, USD | The underlying price at the snapshot. For VIX this is the VIX index, not a futures price. | 524.58 |
|
rows[].s_implied |
number, USD | When it could not be determined for the expiry. | The spot price implied by that expiry's options. | 524.61 |
rows[].s_basis |
number, USD | When s_implied is empty. |
s_implied minus spot: how far the options' implied spot sits from the quoted spot. |
0.03 |
rows[].rate |
number, decimal (0.041 = 4.1%) | The risk-free rate for that expiry, continuously compounded and annualized. | 0.0412 |
|
rows[].q |
number, decimal per year (0.012 = 1.2%) | The continuous dividend or borrow yield implied for that expiry. On VIX it restates the spot-to-futures gap and is not a borrow cost. | 0.0118 |
|
rows[].T |
number, years | Time to expiry used for discounting. | 0.0192 |
|
rows[].T_vol |
number, years | Time to expiry used for volatility. It counts trading hours more than nights and weekends. | 0.0168 |
|
rows[].T_borrow |
number, years | Time to expiry used for the dividend and borrow term. | 0.0192 |
|
rows[].n_obs |
integer | How many options went into the expiry's fit. | 182 |
|
rows[].rmse |
number, decimal vol (0.001 = 0.1 vol point) | Typical gap between the quotes' implied volatility and the fitted curve at the same strikes. Smaller means the curve fits the quotes more closely. | 0.001 |
|
rows[].r_squared |
number | When not available. | How much of the variation in the quotes the fit explains (R squared). Closer to 1 is a tighter fit. | 0.998 |
withheld_sessions |
list of text, list of YYYY-MM-DD | An empty list when none was withheld. | Sessions in the range with no row because the expiry's fit did not pass our quality checks. |
GET /contract-history
One option across sessions: /chain's columns for that option, at one snapshot each session.
GET https://optionspace.ai/api/v1/data/contract-history?symbol=SPY&expiration=2025-04-17&strike=520&put_call=put&start=2025-04-01&end=2025-04-10
| Parameter | Type | Default | Meaning and allowed values |
|---|---|---|---|
symbol |
text | required | The product symbol, such as SPY or SPX. /symbols lists them. |
expiration |
date | required | The option expiration date, YYYY-MM-DD. /expirations lists them. |
strike |
number | required | The strike price (USD). |
put_call |
text | required | call or put. |
start |
date | required | The first session of the range, YYYY-MM-DD. |
end |
date | required | The last session of the range, YYYY-MM-DD. |
snapshot |
text | none | EOD (the default: each session's last snapshot at or before its close that your plan covers) or HH:MM, the same each session. |
root |
text | none | The listed root. Left out, SPXW is served for an SPX monthly listed under both. |
columns |
list | none | Only these columns, comma-separated or repeated, as for /chain. volume and open_interest are added only when named, for a range ending on a past session (403 otherwise). |
Returns: rows: one per session, oldest first, with quote_state where the quotes are included; columns names them. withheld_sessions lists sessions with no row because the expiry's fit did not pass our quality checks.
Python: client.contract_history()
Also carries the common fields: the identity fields.
Response fields (26)
| Field | Type and unit | Can be empty | Meaning | Example |
|---|---|---|---|---|
start |
text, YYYY-MM-DD | The first session of the range you asked for. | "2025-01-02" |
|
end |
text, YYYY-MM-DD | The last session of the range you asked for. | "2025-03-31" |
|
expiration |
text, YYYY-MM-DD | The contract's expiration date. | "2025-04-17" |
|
strike |
number, USD | Strike price. | 520.0 |
|
put_call |
text | Call or put. Values: call, put. |
"call" |
|
snapshot_mode |
text | Which snapshot each row uses: each session's end of day, or one time each session. Values: EOD, or an HH:MM time. |
"EOD" |
|
columns |
list of text, list of field names | The columns each row carries, in order. | ||
rows |
list of objects | One entry per session, oldest first. | ||
rows[].data_date |
text, YYYY-MM-DD | On answers that cover a date range (the range is in start and end). |
The trading session. | "2025-04-10" |
rows[].snapshot_time |
text, HH:MM, US Eastern | On answers that are not tied to one snapshot (a range, or a whole session). | The time of the quote sample. A request for EOD returns the time it resolved to. | "15:57" |
rows[].symbol |
text | On answers not about one symbol (/symbols). |
The product symbol you asked for. | "SPY" |
rows[].expiration |
text, YYYY-MM-DD | The contract's expiration date. | "2025-04-17" |
|
rows[].strike |
number, USD | Strike price. | 520.0 |
|
rows[].put_call |
text | Call or put. Values: call, put. |
"call" |
|
rows[].fitted_vol |
number, decimal vol (0.20 = 20%) | The fitted implied volatility at the strike. | 0.2144 |
|
rows[].fitted_price |
number, USD per share (multiply by the contract multiplier, usually 100, for one contract) | The option price at the fitted volatility. | 6.85 |
|
rows[].delta |
number | Change in option value per $1 move in the underlying. | 0.48 |
|
rows[].bid |
number, USD per share | Absent until the session's quotes are released (the next session's 09:30 ET open); null where that side was not quoted. | Quoted bid price. | 6.8 |
rows[].ask |
number, USD per share | Absent until the session's quotes are released; null where that side was not quoted. | Quoted ask price. | 6.9 |
rows[].iv_bid |
number, decimal vol | Absent until the session's quotes are released; null where no volatility matches the price. Read it with quote_state. |
Implied volatility of the bid price. | 0.2121 |
rows[].iv_ask |
number, decimal vol | Absent until the session's quotes are released; null where no volatility matches the price. | Implied volatility of the ask price. | 0.2168 |
rows[].iv_mid |
number, decimal vol | Absent until the session's quotes are released; null without a usable mid. | Implied volatility of the mid price. | 0.2144 |
rows[].volume |
integer, contracts | When the day's volume has not been loaded; 0 means no trades. |
On /chain: contracts traded so far that session, through the snapshot minute. On /contract-volume: the official end-of-day volume for the session. |
63 |
rows[].open_interest |
integer, contracts | When the day's open-interest figure has not been loaded; 0 means none. |
Open contracts at the end of the previous session (the figure published before this session opened). | 5236 |
rows[].quote_state |
text | Absent until the session's quotes are released. | How to read the quote: two_sided (bid and ask volatilities both exist), ask_only (only the ask bounds the volatility), no_vol (the price is past the early-exercise boundary, so no volatility exists), no_quote (neither side quoted). |
"two_sided" |
withheld_sessions |
list of text, list of YYYY-MM-DD | An empty list when none was withheld. | Sessions in the range with no row because the expiry's fit did not pass our quality checks. |
GET /event-outcomes
For each earnings, Fed, CPI or jobs-report day: the implied move the options priced, the move that happened, and 30-day ATM volatility before and after.
GET https://optionspace.ai/api/v1/data/event-outcomes?symbol=NVDA&start=2025-04-01&end=2025-04-10
| Parameter | Type | Default | Meaning and allowed values |
|---|---|---|---|
symbol |
text | required | The product symbol, such as SPY or SPX. /symbols lists them. |
start |
date | required | The first event date of the range, YYYY-MM-DD. |
end |
date | required | The last event date of the range, YYYY-MM-DD. |
event_type |
text | none | One of earnings, fomc, cpi, nfp. Left out: every type. |
root |
text | none | The listed root, for a symbol that lists under several. Leave it out to let the server choose; see SPX and SPXW. |
Returns: rows: one per event, oldest first. Moves are decimals of the share price (0.05 = 5%).
Python: client.event_outcomes()
Also carries the common fields: the identity fields.
Response fields (18)
| Field | Type and unit | Can be empty | Meaning | Example |
|---|---|---|---|---|
start |
text, YYYY-MM-DD | The first session of the range you asked for. | "2025-01-02" |
|
end |
text, YYYY-MM-DD | The last session of the range you asked for. | "2025-03-31" |
|
event_type |
text | When every type was asked for. | The event type asked for. | "earnings" |
rows |
list of objects | One entry per event, oldest first. | ||
rows[].root |
text | The ticker the event is stored under: an older ticker before a rename (FB for Meta before 2022-06-09). | "NVDA" |
|
rows[].event_type |
text | The kind of event. Values: earnings (earnings reports, single stocks), fomc (Fed rate decisions), cpi (consumer price index releases), nfp (the monthly jobs report). |
"earnings" |
|
rows[].event_date |
text, YYYY-MM-DD | The date of the announcement. | "2026-08-26" |
|
rows[].timing |
text | When the announcement came. Values: BMO (before the open), AMC (after the close), INTRADAY (during the session; Fed decisions). |
"AMC" |
|
rows[].implied_move |
number, decimal (0.05 = 5%) | When the options priced no measurable extra move for the event, or no figure exists for the session before it; notes says so. Common for CPI and jobs-report days. |
The implied move: the average size of the move the options priced for the event, up or down, as a fraction of the price. It is about 0.80 times the event's volatility (one standard deviation of the move); see the API guide's "Implied move and event volatility". | 0.0543 |
rows[].implied_move_se |
number, decimal | When implied_move is empty. |
One standard error of implied_move. |
0.0002 |
rows[].actual_return |
number, decimal; negative = down | Across a stock split, or when a price is missing. | The move that followed, with its sign: from the last close before the announcement to the first close after it. It equals the overnight and intraday parts compounded. | -0.0162 |
rows[].actual_move |
number, decimal (0.086 = 8.6%) | Across a stock split, or when a price is missing. | The size of actual_return, without its sign. |
0.086 |
rows[].return_overnight |
number, decimal; negative = down | Across a stock split, or when a price is missing. | The signed change from the last close before the announcement to the price at the next open (09:32 ET). | 0.0671 |
rows[].return_intraday |
number, decimal; negative = down | Across a stock split, or when a price is missing. | The signed change from that open to the close of the same session. | 0.0177 |
rows[].atm_vol_30d_before |
number, decimal vol | When unavailable that session. | 30-day at-the-money implied volatility at the last close before the announcement. | 0.4711 |
rows[].atm_vol_30d_after |
number, decimal vol | When unavailable that session. | The same at the first close after it. | 0.3928 |
rows[].vol_crush |
number, decimal | When either volatility is empty. | How far that volatility fell, as a fraction of the value before: 0.17 = it fell by 17%; negative = it rose. | 0.1662 |
rows[].actual_minus_implied |
number, decimal of the price | When either move is empty. | actual_move minus implied_move: positive when the stock moved more than the options priced. A difference of moves, not a profit in dollars. |
0.0317 |
GET /event-runup
For each event in the range, the implied move priced for it on each session before it.
GET https://optionspace.ai/api/v1/data/event-runup?symbol=NVDA&start=2025-04-01&end=2025-04-10
| Parameter | Type | Default | Meaning and allowed values |
|---|---|---|---|
symbol |
text | required | The product symbol, such as SPY or SPX. /symbols lists them. |
start |
date | required | The first event date of the range, YYYY-MM-DD. |
end |
date | required | The last event date of the range, YYYY-MM-DD. |
event_type |
text | none | One of earnings, fomc, cpi, nfp. Left out: every type. |
root |
text | none | The listed root, for a symbol that lists under several. Leave it out to let the server choose; see SPX and SPXW. |
Returns: rows: one per event and session before it, oldest event first. An upcoming event appears with the sessions up to the latest one available.
Python: client.event_runup()
Also carries the common fields: the identity fields.
Response fields (12)
| Field | Type and unit | Can be empty | Meaning | Example |
|---|---|---|---|---|
start |
text, YYYY-MM-DD | The first session of the range you asked for. | "2025-01-02" |
|
end |
text, YYYY-MM-DD | The last session of the range you asked for. | "2025-03-31" |
|
event_type |
text | When every type was asked for. | The event type asked for. | "earnings" |
rows |
list of objects | One entry per event and session before it, oldest event first. | ||
rows[].root |
text | The ticker the event is stored under: an older ticker before a rename. | "NVDA" |
|
rows[].event_type |
text | The kind of event. Values: earnings (earnings reports, single stocks), fomc (Fed rate decisions), cpi (consumer price index releases), nfp (the monthly jobs report). |
"earnings" |
|
rows[].event_date |
text, YYYY-MM-DD | The date of the announcement. | "2026-08-26" |
|
rows[].data_date |
text, YYYY-MM-DD | The session the implied move was read at. | "2026-10-19" |
|
rows[].sessions_to_event |
number, sessions | Trading sessions before the announcement: 0 is the last close before it (the event day itself for an announcement after the close; the session before for one before the open or during the session), -1 the session before that. Exchange holidays are not sessions. | -21 |
|
rows[].implied_move |
number, decimal (0.05 = 5%) | When the options priced no measurable extra move for the event, or no figure exists for the session before it; notes says so. Common for CPI and jobs-report days. |
The implied move: the average size of the move the options priced for the event, up or down, as a fraction of the price. It is about 0.80 times the event's volatility (one standard deviation of the move); see the API guide's "Implied move and event volatility". | 0.0543 |
rows[].implied_move_se |
number, decimal | When implied_move is empty. |
One standard error of implied_move. |
0.0002 |
rows[].atm_vol_30d |
number, decimal vol | When unavailable that session. | 30-day at-the-money implied volatility at that session's close. | 0.4191 |
GET /volume-summary
Per session: official volume and open interest for calls, puts and both, and the put/call ratios. Past sessions only.
GET https://optionspace.ai/api/v1/data/volume-summary?symbol=SPY&start=2025-04-01&end=2025-04-10
| Parameter | Type | Default | Meaning and allowed values |
|---|---|---|---|
symbol |
text | required | The product symbol, such as SPY or SPX. /symbols lists them. |
start |
date | required | The first session of the range, YYYY-MM-DD. |
end |
date | required | The last session of the range, YYYY-MM-DD. |
root |
text | none | The listed root, for a symbol that lists under several. Leave it out to let the server choose; see SPX and SPXW. |
Returns: rows: one per session. A session's figures are available from the next session's 09:30 ET open; a range reaching a session not yet available returns 403 with reason not_yet_historical.
Python: client.volume_summary()
Also carries the common fields: the identity fields.
Response fields (13)
| Field | Type and unit | Can be empty | Meaning | Example |
|---|---|---|---|---|
start |
text, YYYY-MM-DD | The first session of the range you asked for. | "2025-01-02" |
|
end |
text, YYYY-MM-DD | The last session of the range you asked for. | "2025-03-31" |
|
rows |
list of objects | One entry per session. | ||
rows[].data_date |
text, YYYY-MM-DD | On answers that cover a date range (the range is in start and end). |
The trading session. | "2025-04-10" |
rows[].root |
text | The root the row was read from; SPX returns SPX and SPXW rows separately. |
"SPY" |
|
rows[].total_volume |
integer, contracts | When the day's official figures have not been loaded. | Official end-of-day volume, calls and puts together. | 16617125 |
rows[].call_volume |
integer, contracts | As total_volume. |
Official end-of-day call volume. | 6838428 |
rows[].put_volume |
integer, contracts | As total_volume. |
Official end-of-day put volume. | 9778697 |
rows[].total_open_interest |
number, contracts | When the day's open-interest figures have not been loaded. | Open interest, calls and puts together, at the end of the previous session. | 19645860 |
rows[].call_open_interest |
number, contracts | As total_open_interest. |
Call open interest at the end of the previous session. | 5973153 |
rows[].put_open_interest |
number, contracts | As total_open_interest. |
Put open interest at the end of the previous session. | 13672707 |
rows[].put_call_volume_ratio |
number, ratio | When call volume is 0 or empty. | put_volume divided by call_volume. |
1.43 |
rows[].put_call_open_interest_ratio |
number, ratio | When call open interest is 0 or empty. | put_open_interest divided by call_open_interest. |
2.29 |
GET /contract-volume
One option's official daily volume and open interest. Past sessions only.
GET https://optionspace.ai/api/v1/data/contract-volume?symbol=SPY&expiration=2025-04-17&strike=520&put_call=put&start=2025-04-01&end=2025-04-10
| Parameter | Type | Default | Meaning and allowed values |
|---|---|---|---|
symbol |
text | required | The product symbol, such as SPY or SPX. /symbols lists them. |
expiration |
date | required | The option's expiration date, YYYY-MM-DD. |
strike |
number | required | The strike price (USD). |
put_call |
text | required | call or put. |
start |
date | required | The first session of the range, YYYY-MM-DD. |
end |
date | required | The last session of the range, YYYY-MM-DD. |
root |
text | none | The listed root. Left out, SPXW is served for an SPX monthly listed under both. |
Returns: rows: one per session the option was listed. A range reaching a session not yet available returns 403 with reason not_yet_historical.
Python: client.contract_volume()
Also carries the common fields: the identity fields.
Response fields (13)
| Field | Type and unit | Can be empty | Meaning | Example |
|---|---|---|---|---|
start |
text, YYYY-MM-DD | The first session of the range you asked for. | "2025-01-02" |
|
end |
text, YYYY-MM-DD | The last session of the range you asked for. | "2025-03-31" |
|
expiration |
text, YYYY-MM-DD | The contract's expiration date. | "2025-04-17" |
|
strike |
number, USD | Strike price. | 520.0 |
|
put_call |
text | Call or put. Values: call, put. |
"call" |
|
rows |
list of objects | One entry per session the option was listed, oldest first. | ||
rows[].data_date |
text, YYYY-MM-DD | On answers that cover a date range (the range is in start and end). |
The trading session. | "2025-04-10" |
rows[].root |
text | When the answer mixes roots or has none (a discovery answer, or SPX without root=); then each row or expiry names its own. |
The listed root the data belongs to. One product can list under several roots (SPX lists as SPX and SPXW). | "SPXW" |
rows[].expiration |
text, YYYY-MM-DD | The contract's expiration date. | "2025-04-17" |
|
rows[].strike |
number, USD | Strike price. | 520.0 |
|
rows[].put_call |
text | Call or put. Values: call, put. |
"call" |
|
rows[].open_interest |
integer, contracts | When the day's open-interest figure has not been loaded; 0 means none. |
Open contracts at the end of the previous session (the figure published before this session opened). | 5236 |
rows[].volume |
number, contracts | When the day's volume has not been loaded; 0 means no trades. |
On /chain: contracts traded so far that session, through the snapshot minute. On /contract-volume: the official end-of-day volume for the session. |
63 |
GET /surface
A legacy end-of-day summary, one entry per listed expiry. For new work use /expirations and /smile.
GET https://optionspace.ai/api/v1/data/surface?symbol=SPY&data_date=2025-04-10
| Parameter | Type | Default | Meaning and allowed values |
|---|---|---|---|
symbol |
text | required | The product symbol, such as SPY or SPX. /symbols lists them. |
data_date |
date | required | The trading session, YYYY-MM-DD. |
snapshot |
text | EOD |
EOD or HH:MM US Eastern time. Leaving it out means EOD: the last snapshot at or before the official close that your plan covers. The time used comes back as snapshot_time. |
root |
text | none | Only this root. Left out, each expiry comes from the root that lists it, SPXW where both do. |
Returns: expiries: expiration, dte, sigma_atm, forward, rate, q, the fit statistics, quality and root; and spot.
Python: client.surface()
Also carries the common fields: the identity fields, the expiries[].quality block.
Response fields (20)
| Field | Type and unit | Can be empty | Meaning | Example |
|---|---|---|---|---|
date |
text, YYYY-MM-DD | The session (the same as data_date). |
"2025-04-10" |
|
spot |
number, USD | The underlying price at the snapshot. | 524.58 |
|
expiries |
list of objects | One entry per listed expiry, shortest first. | ||
expiries[].snapshot_time |
text, HH:MM, US Eastern | On answers that are not tied to one snapshot (a range, or a whole session). | The time of the quote sample. A request for EOD returns the time it resolved to. | "15:57" |
expiries[].expiration |
text, YYYY-MM-DD | The contract's expiration date. | "2025-04-17" |
|
expiries[].dte |
integer, calendar days | Calendar days from data_date to expiration. |
7 |
|
expiries[].sigma_atm |
number, decimal vol | When unavailable for that snapshot. | At-the-money volatility of the expiry. | 0.1423 |
expiries[].forward |
number, USD | The forward price implied by the expiry's options. For VIX, use forward: each expiry's forward sits at its own futures level. |
525.31 |
|
expiries[].rate |
number, decimal (0.041 = 4.1%) | The risk-free rate for that expiry, continuously compounded and annualized. | 0.0412 |
|
expiries[].q |
number, decimal per year (0.012 = 1.2%) | The continuous dividend or borrow yield implied for that expiry. On VIX it restates the spot-to-futures gap and is not a borrow cost. | 0.0118 |
|
expiries[].spot |
number, USD | The underlying price at the snapshot. For VIX this is the VIX index, not a futures price. | 524.58 |
|
expiries[].s_implied |
number, USD | When it could not be determined for the expiry. | The spot price implied by that expiry's options. | 524.61 |
expiries[].s_basis |
number, USD | When s_implied is empty. |
s_implied minus spot: how far the options' implied spot sits from the quoted spot. |
0.03 |
expiries[].T |
number, years | Time to expiry used for discounting. | 0.0192 |
|
expiries[].T_vol |
number, years | Time to expiry used for volatility. It counts trading hours more than nights and weekends. | 0.0168 |
|
expiries[].T_borrow |
number, years | Time to expiry used for the dividend and borrow term. | 0.0192 |
|
expiries[].n_obs |
integer | How many options went into the expiry's fit. | 182 |
|
expiries[].rmse |
number, decimal vol (0.001 = 0.1 vol point) | Typical gap between the quotes' implied volatility and the fitted curve at the same strikes. Smaller means the curve fits the quotes more closely. | 0.001 |
|
expiries[].r_squared |
number | When not available. | How much of the variation in the quotes the fit explains (R squared). Closer to 1 is a tighter fit. | 0.998 |
expiries[].root |
text | The root the expiry was taken from. | "SPXW" |
REST: research routes
estimate=true returns the most a call can cost without running it, and max_credits=N refuses a call that could cost more (412, not run). A call still working after about 20 seconds answers 202 with a job_id: see background jobs. Completed sessions only; except for /expiry-metrics the symbol must list under one root.
GET /expiry-metrics
At-the-money volatility and 25-delta skew for one listed expiry, optionally beside the previous session.
GET https://optionspace.ai/api/v1/data/expiry-metrics?symbol=SPY&data_date=2025-04-10&expiration=2025-04-17
| Parameter | Type | Default | Meaning and allowed values |
|---|---|---|---|
symbol |
text | required | The product symbol, such as SPY or SPX. /symbols lists them. |
data_date |
date | required | The trading session, YYYY-MM-DD. |
expiration |
date | required | The option expiration date, YYYY-MM-DD. /expirations lists them. |
snapshot |
text | EOD |
EOD or HH:MM US Eastern time. Leaving it out means EOD: the last snapshot at or before the official close that your plan covers. The time used comes back as snapshot_time. |
root |
text | none | The listed root, for a symbol that lists under several. Leave it out to let the server choose; see SPX and SPXW. |
compare_previous |
true or false | false |
true also returns the previous trading session and the change in skew. |
estimate |
true or false | false |
true returns credits_estimate and credits_cap without running the call. |
max_credits |
integer | none | Refuse the call with 412, unrun, if it could cost more than this. At least 1. |
Returns: Per session: sigma_atm, put_25d_iv, call_25d_iv, put_25d_skew, call_25d_skew, skew_25d, status; skew_change when compared; credits.
Python: client.expiry_metrics()
Also carries the common fields: the identity fields, the credits block.
Response fields (17)
| Field | Type and unit | Can be empty | Meaning | Example |
|---|---|---|---|---|
expiration |
text, YYYY-MM-DD | The contract's expiration date. | "2025-04-17" |
|
rows |
list of objects | One entry per session: the requested session, then the previous one when compared. | ||
rows[].symbol |
text | On answers not about one symbol (/symbols). |
The product symbol you asked for. | "SPY" |
rows[].root |
text | When the answer mixes roots or has none (a discovery answer, or SPX without root=); then each row or expiry names its own. |
The listed root the data belongs to. One product can list under several roots (SPX lists as SPX and SPXW). | "SPXW" |
rows[].data_date |
text, YYYY-MM-DD | On answers that cover a date range (the range is in start and end). |
The trading session. | "2025-04-10" |
rows[].snapshot_time |
text, HH:MM, US Eastern | On answers that are not tied to one snapshot (a range, or a whole session). | The time of the quote sample. A request for EOD returns the time it resolved to. | "15:57" |
rows[].expiration |
text, YYYY-MM-DD | The contract's expiration date. | "2025-04-17" |
|
rows[].session_close |
text, HH:MM, US Eastern | The official close of that session. | "16:00" |
|
rows[].sigma_atm |
number, decimal vol | When status is not ok. |
At-the-money volatility of the expiry. | 0.1423 |
rows[].put_25d_iv |
number, decimal vol | When status is not ok. |
Volatility of the 25-delta put. | 0.1611 |
rows[].call_25d_iv |
number, decimal vol | When status is not ok. |
Volatility of the 25-delta call. | 0.1259 |
rows[].put_25d_skew |
number, decimal vol | When status is not ok. |
25-delta put volatility minus at-the-money volatility. | 0.0188 |
rows[].call_25d_skew |
number, decimal vol | When status is not ok. |
25-delta call volatility minus at-the-money volatility. | -0.0164 |
rows[].skew_25d |
number, decimal vol | When status is not ok. |
25-delta put volatility minus 25-delta call volatility. | 0.0352 |
rows[].status |
text | Whether the session's values are available. Values: ok, metric_unavailable (the expiry was found but the skew could not be read), unavailable (no fitted expiry at that snapshot). |
"ok" |
|
skew_change |
number, decimal vol | Without compare_previous, or when either session's skew is unavailable. |
skew_25d on the requested session minus the previous session. |
0.0041 |
requested_snapshot |
text | The snapshot you asked for, before it was resolved. | "EOD" |
GET /exposure-distribution
Where the fitted volatility of a delta-selected option sits against the same selection on past sessions, at the same clock time and days to expiry.
GET https://optionspace.ai/api/v1/data/exposure-distribution?symbol=SPY&data_date=2025-04-10&snapshot=09:45&put_call=put
| Parameter | Type | Default | Meaning and allowed values |
|---|---|---|---|
symbol |
text | required | The product symbol, such as SPY or SPX. /symbols lists them. |
data_date |
date | required | The trading session, YYYY-MM-DD. |
snapshot |
text | required | An explicit HH:MM US Eastern time. EOD is refused. |
put_call |
text | required | call or put. |
delta |
number | 0.15 |
The absolute delta to select (0.15 = a 15-delta option). Greater than 0 and less than 1. |
dte |
integer | 1 |
Days to expiry, in calendar days, matched exactly. From 0 to 365. |
delta_tolerance |
number | 0.01 |
How far a stored option's delta may be from delta and still be selected. From 0 to 0.05. |
lookback_years |
number | 1 |
How many years of past sessions to compare against. From 0.1 to 1. |
root |
text | none | The listed root, for a symbol that lists under several. Leave it out to let the server choose; see SPX and SPXW. |
estimate |
true or false | false |
true returns credits_estimate and credits_cap without running the call. |
max_credits |
integer | none | Refuse the call with 412, unrun, if it could cost more than this. At least 1. |
Returns: current, history, statistics (n, median, percentile, robust_z_score), coverage, status, credits.
Python: client.exposure_distribution()
Also carries the common fields: the identity fields, the credits block.
Response fields (54)
| Field | Type and unit | Can be empty | Meaning | Example |
|---|---|---|---|---|
status |
text | Whether a statistic is given, or why not. Values: no_current_observation, insufficient_history, zero_dispersion, ok. |
"ok" |
|
metric |
text | The value being compared. Values: fitted_vol. |
"fitted_vol" |
|
current |
object | When no option matches on that session (status says so). |
The option selected on data_date. |
|
current.symbol |
text | On answers not about one symbol (/symbols). |
The product symbol you asked for. | "SPY" |
current.root |
text | When the answer mixes roots or has none (a discovery answer, or SPX without root=); then each row or expiry names its own. |
The listed root the data belongs to. One product can list under several roots (SPX lists as SPX and SPXW). | "SPXW" |
current.data_date |
text, YYYY-MM-DD | On answers that cover a date range (the range is in start and end). |
The trading session. | "2025-04-10" |
current.snapshot_time |
text, HH:MM, US Eastern | On answers that are not tied to one snapshot (a range, or a whole session). | The time of the quote sample. A request for EOD returns the time it resolved to. | "15:57" |
current.expiration |
text, YYYY-MM-DD | The contract's expiration date. | "2025-04-17" |
|
current.dte |
integer, calendar days | Calendar days from data_date to expiration. |
7 |
|
current.put_call |
text | Call or put. Values: call, put. |
"call" |
|
current.strike |
number, USD | Strike price. | 520.0 |
|
current.delta |
number | Change in option value per $1 move in the underlying. | 0.48 |
|
current.fitted_vol |
number, decimal vol (0.20 = 20%) | The fitted implied volatility at the strike. | 0.2144 |
|
current.candidate_count |
integer | How many stored options on that session matched the delta band before one was selected. | 3 |
|
history |
list of objects | One selected option per past session, oldest first. | ||
history[].symbol |
text | On answers not about one symbol (/symbols). |
The product symbol you asked for. | "SPY" |
history[].root |
text | When the answer mixes roots or has none (a discovery answer, or SPX without root=); then each row or expiry names its own. |
The listed root the data belongs to. One product can list under several roots (SPX lists as SPX and SPXW). | "SPXW" |
history[].data_date |
text, YYYY-MM-DD | On answers that cover a date range (the range is in start and end). |
The trading session. | "2025-04-10" |
history[].snapshot_time |
text, HH:MM, US Eastern | On answers that are not tied to one snapshot (a range, or a whole session). | The time of the quote sample. A request for EOD returns the time it resolved to. | "15:57" |
history[].expiration |
text, YYYY-MM-DD | The contract's expiration date. | "2025-04-17" |
|
history[].dte |
integer, calendar days | Calendar days from data_date to expiration. |
7 |
|
history[].put_call |
text | Call or put. Values: call, put. |
"call" |
|
history[].strike |
number, USD | Strike price. | 520.0 |
|
history[].delta |
number | Change in option value per $1 move in the underlying. | 0.48 |
|
history[].fitted_vol |
number, decimal vol (0.20 = 20%) | The fitted implied volatility at the strike. | 0.2144 |
|
history[].candidate_count |
integer | How many stored options on that session matched the delta band before one was selected. | 2 |
|
statistics |
object | Where current sits against history. |
||
statistics.n |
integer | How many past sessions are in history. |
247 |
|
statistics.median |
number, decimal vol | When status is not ok. |
Median fitted volatility over history. |
0.2011 |
statistics.scaled_mad |
number, decimal vol | When status is not ok. |
The spread of history: its median absolute deviation from the median, scaled to read like a standard deviation. |
0.021 |
statistics.robust_z_score |
number | When status is not ok. |
How far current is from the median, in units of scaled_mad. Not bounded, and not a normal-tail probability. |
1.35 |
statistics.percentile |
number | When status is not ok. |
Share of history at or below current, from 0 to 100. |
84.2 |
statistics.percentile_rule |
text | The percentile's definition, in words. | ||
statistics.z_score_rule |
text | The z-score's definition, in words. | ||
statistics.minimum_observations |
integer | The fewest past sessions needed for a statistic. | 30 |
|
coverage |
object | How complete history is. |
||
coverage.expected_sessions |
integer | Trading sessions in the lookback where the snapshot time existed. | 251 |
|
coverage.matched_sessions |
integer | Sessions with a selected option. | 247 |
|
coverage.missing_sessions |
list of text, YYYY-MM-DD | An empty list when every session matched. | Sessions with no selected option. | |
selection |
object | The selection you asked for, as applied. | ||
selection.delta_convention |
text | Which delta the selection uses. | "stored option spot delta" |
|
selection.target_abs_delta |
number | The absolute delta you asked for. | 0.15 |
|
selection.delta_tolerance |
number | The tolerance you asked for. | 0.01 |
|
selection.dte |
integer, calendar days | The days to expiry you asked for. | 1 |
|
selection.dte_unit |
text | The unit of dte. Values: calendar days. |
||
selection.snapshot_time |
text, HH:MM, US Eastern | The clock time compared on every session. | "09:45" |
|
selection.put_call |
text | The side you asked for. Values: call, put. |
"put" |
|
selection.rule |
text | How one option is picked when several match, in words. | ||
selection.filters |
text | Which stored options are eligible, in words. | ||
lookback |
object | The window of past sessions. | ||
lookback.start_inclusive |
text, YYYY-MM-DD | The first day of the window. | "2024-04-10" |
|
lookback.end_exclusive |
text, YYYY-MM-DD | The day after the window (your data_date, which is not part of history). |
"2025-04-10" |
|
lookback.years |
number, years | The lookback you asked for. | 1.0 |
|
lookback.sampling |
text | How sessions are sampled, in words. |
GET /clean-atm-history
Constant-maturity at-the-money volatility with scheduled events removed (clean_iv) and without (dirty_iv).
GET https://optionspace.ai/api/v1/data/clean-atm-history?symbol=NVDA&start=2025-04-01&end=2025-04-10
| Parameter | Type | Default | Meaning and allowed values |
|---|---|---|---|
symbol |
text | required | The product symbol, such as SPY or SPX. /symbols lists them. |
start |
date | required | The first session, YYYY-MM-DD. At most three years before end. |
end |
date | required | The last session, YYYY-MM-DD, before today. |
tenor_days |
integer | 30 |
The constant maturity in calendar days. From 7 to 180. |
granularity |
text | eod |
eod (one value per session), or every 30m, 5m or 1m (minute) through each session. Your plan must cover that frequency or a finer one. At 1m, at most one year per call. |
estimate |
true or false | false |
true returns credits_estimate and credits_cap without running the call. |
max_credits |
integer | none | Refuse the call with 412, unrun, if it could cost more than this. At least 1. |
Returns: rows (data_date, snapshot_time, dirty_iv, clean_iv, clean_iv_se, status, event_dates, expirations), earnings, notes, credits.
Python: client.clean_atm_history()
Also carries the common fields: symbol, root, notes, returned_rows, the credits block.
Response fields (17)
| Field | Type and unit | Can be empty | Meaning | Example |
|---|---|---|---|---|
start |
text, YYYY-MM-DD | The first session of the range you asked for. | "2025-01-02" |
|
end |
text, YYYY-MM-DD | The last session of the range you asked for. | "2025-03-31" |
|
tenor_days |
integer, calendar days | The constant maturity, in calendar days. | 30 |
|
snapshot |
text | Which observations the rows use. Values: EOD (one per session), 5m (every five minutes). |
"EOD" |
|
granularity |
text | The observation frequency you asked for. Values: eod, 5m. |
"eod" |
|
rows |
list of objects | One entry per observation, in time order. | ||
rows[].data_date |
text, YYYY-MM-DD | On answers that cover a date range (the range is in start and end). |
The trading session. | "2025-04-10" |
rows[].snapshot_time |
text, HH:MM, US Eastern | On answers that are not tied to one snapshot (a range, or a whole session). | The time of the quote sample. A request for EOD returns the time it resolved to. | "15:57" |
rows[].dirty_iv |
number, decimal vol | When status is tenor_not_bracketed or observation_unavailable. |
Constant-maturity ATM volatility with nothing removed. | 0.5412 |
rows[].clean_iv |
number, decimal vol | When dirty_iv is empty. |
The same volatility with scheduled events removed. Equals dirty_iv when nothing was removed or removal was not possible. |
0.4987 |
rows[].clean_iv_se |
number, decimal vol | When the removal cannot be vouched for (status says why). |
One-standard-error band of clean_iv. |
0.0062 |
rows[].status |
text | What happened to the scheduled events for this observation. Values: no_material_event, adjusted, event_size_unresolved, adjustment_unavailable, event_estimate_unavailable, tenor_not_bracketed, observation_unavailable. |
"adjusted" |
|
rows[].event_dates |
list of text, list of YYYY-MM-DD | An empty list when nothing was removed; absent when there is no value. | The scheduled events removed. | |
rows[].expirations |
list of text, list of YYYY-MM-DD | Absent when status is tenor_not_bracketed or observation_unavailable. |
The listed expiries the tenor was read from. | |
earnings |
list of objects | The earnings reports in the window. | ||
earnings[].date |
text, YYYY-MM-DD | The report date. | "2025-02-26" |
|
earnings[].timing |
text | When the company reported: after the close or before the open. Values: AMC (after market close), BMO (before market open). |
"AMC" |
GET /earnings-history
Past earnings: the move the options priced, the move that followed, and the run-up before each report.
GET https://optionspace.ai/api/v1/data/earnings-history?symbol=NVDA&as_of=2026-09-16
| Parameter | Type | Default | Meaning and allowed values |
|---|---|---|---|
symbol |
text | required | The product symbol, such as SPY or SPX. /symbols lists them. |
as_of |
date | required | Use the reports strictly before this date, YYYY-MM-DD, before today. |
count |
integer | 8 |
How many past reports. From 1 to 12. |
runup_sessions |
integer | 30 |
How many sessions before each report the run-up path covers. From 5 to 60. |
granularity |
text | eod |
eod, 30m, 5m or 1m for the run-up path. Your plan must cover that frequency or a finer one. |
estimate |
true or false | false |
true returns credits_estimate and credits_cap without running the call. |
max_credits |
integer | none | Refuse the call with 412, unrun, if it could cost more than this. At least 1. |
Returns: events (earnings_date, timing, pre_event_session, post_event_session, event_vol, realized_return, realized_abs_move, status, samples), notes, credits.
Python: client.earnings_history()
Also carries the common fields: symbol, root, notes, returned_rows, the credits block.
Response fields (27)
| Field | Type and unit | Can be empty | Meaning | Example |
|---|---|---|---|---|
as_of |
text, YYYY-MM-DD | The as-of date you asked for: reports strictly before it are used. | "2026-09-16" |
|
granularity |
text | The run-up observation frequency you asked for. Values: eod, 5m. |
"eod" |
|
events |
list of objects | One entry per report, oldest first. | ||
events[].earnings_date |
text, YYYY-MM-DD | The earnings report date. | "2025-02-26" |
|
events[].timing |
text | When the company reported: after the close or before the open. Values: AMC (after market close), BMO (before market open). |
"AMC" |
|
events[].pre_event_session |
text, YYYY-MM-DD | The last session closing before the report. | "2025-02-26" |
|
events[].post_event_session |
text, YYYY-MM-DD | When that session has not happened yet. | The first session after the report. | "2025-02-27" |
events[].implied_move |
number, decimal (0.05 = 5%) | When the options priced no measurable extra move for the event, or no figure exists for the session before it; notes says so. Common for CPI and jobs-report days. |
The implied move: the average size of the move the options priced for the event, up or down, as a fraction of the price. It is about 0.80 times the event's volatility (one standard deviation of the move); see the API guide's "Implied move and event volatility". | 0.0543 |
events[].implied_move_se |
number, decimal | When implied_move is empty. |
One standard error of implied_move. |
0.0002 |
events[].estimate_date |
text, YYYY-MM-DD | When implied_move is empty. |
The session implied_move was read on: the last close before the report. |
"2025-02-26" |
events[].estimate_snapshot_time |
text | When implied_move is empty. |
EOD: the report-level estimate is the end-of-day one at every granularity. |
"EOD" |
events[].actual_return |
number, decimal; negative = down | Across a stock split, or when a price is missing. | The move that followed, with its sign: from the last close before the announcement to the first close after it. It equals the overnight and intraday parts compounded. | -0.0162 |
events[].actual_move |
number, decimal (0.086 = 8.6%) | Across a stock split, or when a price is missing. | The size of actual_return, without its sign. |
0.086 |
events[].return_overnight |
number, decimal; negative = down | Across a stock split, or when a price is missing. | The signed change from the last close before the announcement to the price at the next open (09:32 ET). | 0.0671 |
events[].return_intraday |
number, decimal; negative = down | Across a stock split, or when a price is missing. | The signed change from that open to the close of the same session. | 0.0177 |
events[].status |
text | Whether the event is complete. Values: ok, incomplete (the priced or realized move is missing), event_timing_unavailable (the report's timing is unknown; the event has no values). |
"ok" |
|
events[].samples |
list of objects | The run-up: the priced move at each observation before the report. | ||
events[].samples[].data_date |
text, YYYY-MM-DD | The session of the observation. | "2025-02-12" |
|
events[].samples[].snapshot_time |
text, HH:MM, US Eastern, or EOD | The time of the observation: EOD on end-of-day run-ups. |
"EOD" |
|
events[].samples[].sessions_to_event |
integer, sessions | Trading sessions before the announcement: 0 is the last close before it (the event day itself for an announcement after the close; the session before for one before the open or during the session), -1 the session before that. Exchange holidays are not sessions. | -21 |
|
events[].samples[].implied_move |
number, decimal (0.05 = 5%) | When the options priced no measurable extra move for the event, or no figure exists for the session before it; notes says so. Common for CPI and jobs-report days. |
The implied move: the average size of the move the options priced for the event, up or down, as a fraction of the price. It is about 0.80 times the event's volatility (one standard deviation of the move); see the API guide's "Implied move and event volatility". | 0.0543 |
events[].samples[].implied_move_se |
number, decimal | When implied_move is empty. |
One standard error of implied_move. |
0.0002 |
events[].samples[].status |
text | Whether this observation has a value. Values: ok, no_positive_event_estimate, event_estimate_unavailable. |
"ok" |
|
events[].samples[].session_offset |
number | On five-minute run-ups: the observation's position in sessions, with the fraction of the session added (for example -2.5 is midway through the session two before). | -2.5 |
|
requested_events |
integer | How many reports you asked for. | 8 |
|
returned_events |
integer | How many reports were found and returned. | 8 |
|
runup_sessions |
integer, sessions | The run-up length you asked for. | 30 |
GET /earnings-backtest
A bounded earnings put-spread simulation with fills, fees, P&L and risk.
GET https://optionspace.ai/api/v1/data/earnings-backtest?symbol=NVDA&as_of=2026-09-16
| Parameter | Type | Default | Meaning and allowed values |
|---|---|---|---|
symbol |
text | required | The product symbol, such as SPY or SPX. /symbols lists them. |
as_of |
date | required | Use the reports strictly before this date, YYYY-MM-DD, before today. |
count |
integer | 8 |
How many past reports to trade, one spread each. From 1 to 12. |
entry_offset |
integer | -5 |
When to open: sessions relative to the last close before the report. From -20 to 0. |
exit_offset |
integer | 1 |
When to close: sessions after the report. From 1 to 5. |
fill_model |
text | quote_side |
quote_side (sell at the bid, buy at the ask), midpoint or theoretical (the fitted price). |
fee_per_contract |
number | 0.65 |
USD per contract per fill. From 0 to 20. |
granularity |
text | eod |
eod or 1m for the position path (needs one-minute access). |
estimate |
true or false | false |
true returns credits_estimate and credits_cap without running the call. |
max_credits |
integer | none | Refuse the call with 412, unrun, if it could cost more than this. At least 1. |
Returns: assumptions, events (status, entry_date, exit_date, expiration, legs, fills, gross_pnl, fees, net_pnl, path), summary, credits.
Python: client.earnings_backtest()
Also carries the common fields: symbol, root, notes, returned_rows, the credits block.
Response fields (66)
| Field | Type and unit | Can be empty | Meaning | Example |
|---|---|---|---|---|
as_of |
text, YYYY-MM-DD | The as-of date you asked for: reports strictly before it are used. | "2026-09-16" |
|
granularity |
text | The position-path frequency you asked for. Values: eod, 1m. |
"eod" |
|
strategy |
text | The simulated strategy. Values: earnings_put_credit_spread. |
"earnings_put_credit_spread" |
|
assumptions |
object | The trade rules the simulation used. | ||
assumptions.fill_model |
text | The fill model you asked for. Values: quote_side, midpoint, theoretical. |
"quote_side" |
|
assumptions.fee_per_contract |
number, USD per contract per fill | The fee you asked for. | 0.65 |
|
assumptions.multiplier |
integer | Shares per contract used for P&L. | 100 |
|
assumptions.entry_sessions |
integer | Your entry_offset. |
-5 |
|
assumptions.exit_sessions |
integer | Your exit_offset. |
1 |
|
assumptions.expiry |
text | Which expiry is traded, in words. | ||
assumptions.selection |
text | How the two strikes are picked, in words. | ||
assumptions.marking |
text | How the position is valued along the way, in words. | ||
events |
list of objects | One entry per report, oldest first. | ||
events[].earnings_date |
text, YYYY-MM-DD | The earnings report date. | "2025-02-26" |
|
events[].timing |
text | When the company reported: after the close or before the open. Values: AMC (after market close), BMO (before market open). |
"AMC" |
|
events[].status |
text | Whether the trade was simulated. Values: complete, unavailable (see reason). |
"complete" |
|
events[].legs |
list of objects | An empty list when the event was not traded. | The two options: the short put first, then the long put. | |
events[].legs[].quantity |
integer | Contracts: -1 short, +1 long. | -1 |
|
events[].legs[].target_delta |
number | The absolute delta the leg aimed for. | 0.25 |
|
events[].legs[].strike |
number, USD | The leg's strike. | 120.0 |
|
events[].legs[].delta |
number | The selected option's delta at entry. | -0.24 |
|
events[].legs[].fitted_price |
number, USD per share | The selected option's fitted price at entry. | 3.1 |
|
events[].legs[].fitted_vol |
number, decimal vol | The selected option's fitted volatility at entry. | 0.62 |
|
events[].path |
list of objects | An empty list when the event was not traded. | The position at each observation from entry to exit. | |
events[].path[].data_date |
text, YYYY-MM-DD | The session. | "2025-02-20" |
|
events[].path[].snapshot_time |
text, HH:MM, US Eastern | The time of the observation. | "15:57" |
|
events[].path[].pnl |
number, USD | When a leg's price is unavailable (status says so). |
Profit or loss so far, for one spread. | -11.3 |
events[].path[].delta |
number, share-equivalent | When a leg's value is unavailable. | The position's delta. | 15.0 |
events[].path[].gamma |
number, shares per $1 of spot | When a leg's value is unavailable. | The position's gamma. | -0.4 |
events[].path[].vega |
number, USD per 1 volatility point | When a leg's value is unavailable. | The position's vega. | -8.2 |
events[].path[].theta |
number, USD per calendar day | When a leg's value is unavailable. | The position's theta. | 4.1 |
events[].path[].status |
text | Whether the observation is complete. Values: complete, mark_unavailable, risk_incomplete, closed (the exit). |
"complete" |
|
events[].fills |
list of objects | An empty list when the event was not traded. | Every fill: two at entry, two at exit. | |
events[].fills[].id |
text | The fill's identifier: report date, phase and leg number. | "2025-02-26-entry-0" |
|
events[].fills[].trade_id |
text | The trade the fill belongs to (the report date). | "2025-02-26" |
|
events[].fills[].symbol |
text | The product symbol. | "NVDA" |
|
events[].fills[].root |
text | The listed root. | "NVDA" |
|
events[].fills[].leg_kind |
text | What was traded. Values: option. |
"option" |
|
events[].fills[].expiration |
text, YYYY-MM-DD | The option's expiry. | "2025-03-21" |
|
events[].fills[].strike |
number, USD | The option's strike. | 120.0 |
|
events[].fills[].put_call |
text | Always put in this strategy. Values: put. |
"put" |
|
events[].fills[].qty |
integer | The leg's position: -1 short, +1 long. | -1 |
|
events[].fills[].executed_qty |
integer | Contracts traded in this fill, signed: negative sells, positive buys. | -1 |
|
events[].fills[].fill_price |
number, USD per share | The fill price under the fill model. | 3.05 |
|
events[].fills[].side |
text | Opening or closing. Values: open, close. |
"open" |
|
events[].fills[].fill_ts |
text, YYYY-MM-DDTHH:MM:SS, US Eastern | When the fill is assumed to happen. | "2025-02-19T15:57:00" |
|
events[].fills[].session_date |
text, YYYY-MM-DD | The fill's session. | "2025-02-19" |
|
events[].fills[].fee |
number, USD | The fee charged on the fill. | 0.65 |
|
events[].fills[].snapshot_time |
text, HH:MM, US Eastern | The snapshot whose prices were used. | "15:57" |
|
events[].entry_date |
text, YYYY-MM-DD | Absent when the sessions could not be placed. | The session the spread was opened. | "2025-02-19" |
events[].exit_date |
text, YYYY-MM-DD | Absent when the sessions could not be placed. | The session the spread was closed. | "2025-02-27" |
events[].expiration |
text, YYYY-MM-DD | Absent when the event was not traded. | The expiry traded. | "2025-03-21" |
events[].gross_pnl |
number, USD | Absent when the event was not traded. | Profit or loss before fees, for one spread. | -20.0 |
events[].fees |
number, USD | Absent when the event was not traded. | Fees on the four fills. | 2.6 |
events[].net_pnl |
number, USD | Absent when the event was not traded. | gross_pnl minus fees. |
-22.6 |
events[].initial_credit |
number, USD | Absent when the event was not traded. | Cash received when the spread was opened, after the entry fees. | 108.7 |
events[].spread_width |
number, USD (strike difference times 100) | Absent when the event was not traded. | The distance between the strikes, for one spread. | 1000.0 |
events[].reason |
text | Absent when status is complete. |
Why the event was not traded. Values: unknown_report_timing, session_unavailable, split_spanning_trade, no_monthly_expiry_after_exit, no_valid_stored_delta_put_spread, ambiguous_contract_rows, entry_or_exit_fill_unavailable. |
|
summary |
object | Totals over the traded events. | ||
summary.requested_events |
integer | How many reports you asked for. | 8 |
|
summary.events |
integer | How many reports were found. | 8 |
|
summary.traded_events |
integer | How many were traded. | 7 |
|
summary.excluded_events |
integer | How many were not traded. | 1 |
|
summary.net_pnl |
number, USD | Net profit or loss over the traded events. | -67.8 |
|
summary.gross_pnl |
number, USD | Profit or loss before fees. | -60.0 |
|
summary.fees |
number, USD | All fees. | 7.8 |
Estimate-only answer
What any research route returns when called with estimate=true: nothing is run.
Fields (12)
| Field | Type and unit | Can be empty | Meaning | Example |
|---|---|---|---|---|
route |
text | The route estimated. | "clean-atm-history" |
|
setting |
text | Which estimate rule applied. Values: default, eod, 5m, 1m. |
"eod" |
|
credits_estimate |
integer, credits | The estimated cost. | 4 |
|
credits_cap |
integer, credits | The most a run can cost. | 4 |
|
estimate_seconds |
number, seconds | The estimated server time. | 3.2 |
|
features |
object | The sizes the estimate was made from, such as years of history or events. |
||
symbols |
integer | How many symbols the call covers. | 1 |
|
price_version |
text | The price list used. | "2026-10-beta-3" |
|
fixed_price |
true or false | True when this setting has a fixed price per call: the estimate and the cap are that price, and a successful run is charged it. | false |
|
estimate_only |
true or false | Always true: nothing was run. | true |
|
credits_charged |
integer | Credits charged for the estimate itself. | 0 |
|
note |
text | The charging rule, in words. |
REST: background jobs
GET /jobs/{job_id}
The state of a background research job. Only your account can read its jobs.
GET https://optionspace.ai/api/v1/data/jobs/JOB_ID
| Parameter | Type | Default | Meaning and allowed values |
|---|---|---|---|
job_id |
text | required | The job_id from a 202 response. |
Returns: status: queued, running, done (with result: the body the call would have returned), failed (with error) or canceled. Kept for one hour after the job finishes.
Python: client.job()
Response fields (20)
| Field | Type and unit | Can be empty | Meaning | Example |
|---|---|---|---|---|
job_id |
text | The job's identifier. | "Jq3xR1c8vT" |
|
status |
text | The job's state. Values: queued, running, done, failed, canceled. |
"done" |
|
route |
text | The research route that started the job. | "/clean-atm-history" |
|
setting |
text | The estimate rule the job's cost uses. Values: default, eod, 5m, 1m. |
"eod" |
|
created_at |
text, ISO 8601 time, UTC | When the job was created. | "2026-10-20T14:00:00Z" |
|
started_at |
text, ISO 8601 time, UTC | Until it starts. | When the job started running. | "2026-10-20T14:00:01Z" |
ended_at |
text, ISO 8601 time, UTC | Until it finishes. | When the job finished. | "2026-10-20T14:00:31Z" |
queue_seconds |
number, seconds | Until it starts. | Time spent waiting before it started. | 1.0 |
work_seconds |
number, seconds | Until it finishes. | Time spent working. | 29.6 |
credits |
object | The job's cost. | ||
credits.estimate |
integer, credits | The server's estimate of the call's cost, made from its parameters before it ran. | 4 |
|
credits.cap |
integer, credits | The most the call can cost. | 4 |
|
credits.charged |
integer | Until it finishes. | Credits charged. | 30 |
poll_url |
text | Where to collect the job. | "/api/v1/data/jobs/Jq3xR1c8vT" |
|
result |
object | Absent unless status is done. |
When status is done: exactly the body the research call would have returned (see that route's fields). |
|
result_kept_seconds |
integer, seconds | Absent unless status is done. |
How long the result is kept after the job finishes. | 3600 |
error |
object | Absent otherwise. | When status is failed or canceled: the status and message the call would have returned. |
|
error.status_code |
integer | The HTTP status the call would have returned (499 for a cancelled job). | 503 |
|
error.detail |
text | The message. | "the data store is unavailable" |
|
poll_after_seconds |
integer, seconds | Absent once the job has finished. | How long to wait before polling again. | 2 |
DELETE /jobs/{job_id}
Cancel a job that has not started. A running job cannot be stopped (409).
DELETE https://optionspace.ai/api/v1/data/jobs/JOB_ID
| Parameter | Type | Default | Meaning and allowed values |
|---|---|---|---|
job_id |
text | required | The job_id from a 202 response. |
Returns: The job's state, canceled.
Also carries the common fields: the credits block.
Response fields (14)
| Field | Type and unit | Can be empty | Meaning | Example |
|---|---|---|---|---|
job_id |
text | The job's identifier. | "Jq3xR1c8vT" |
|
status |
text | Always canceled after a successful cancel. Values: canceled. |
"canceled" |
|
route |
text | The research route that started the job. | "/clean-atm-history" |
|
setting |
text | Which estimate rule applied: the observation frequency of the call, or default for a route with one rule. Values: default, eod, 5m, 1m. |
"eod" |
|
created_at |
text, ISO 8601 time, UTC | When the job was created. | "2026-10-20T14:00:00Z" |
|
started_at |
null | Always. | Only a job that has not started can be cancelled. | |
ended_at |
null | Always. | When the job ended; not set at the time of the cancel. | |
queue_seconds |
number, seconds | Until it starts. | Time spent waiting before it started. | 1.0 |
work_seconds |
null | Always. | The job never ran. | |
credits.charged |
null | Always. | Empty for a cancelled job. | |
poll_url |
text | Where to collect the job. | "/api/v1/data/jobs/Jq3xR1c8vT" |
|
error |
object | Says the job was cancelled. | ||
error.status_code |
integer | Always 499 for a cancelled job. | 499 |
|
error.detail |
text | The message. | "cancelled by the caller" |
Accepted (202) answer
What a research route returns when it is still working after about 20 seconds. Collect the answer from poll_url.
Fields (11)
| Field | Type and unit | Can be empty | Meaning | Example |
|---|---|---|---|---|
job_id |
text | The job's identifier. | "Jq3xR1c8vT" |
|
status |
text | The job's state when the call answered. Values: queued, running. |
"queued" |
|
route |
text | The research route. | "/clean-atm-history" |
|
poll_url |
text | Where to collect the answer. | "/api/v1/data/jobs/Jq3xR1c8vT" |
|
poll_after_seconds |
integer, seconds | How long to wait before the first poll. | 2 |
|
result_kept_seconds |
integer, seconds | How long the answer is kept after the job finishes. | 3600 |
|
credits |
object | The job's cost so far. | ||
credits.estimate |
integer, credits | The server's estimate of the call's cost, made from its parameters before it ran. | 4 |
|
credits.cap |
integer, credits | The most the call can cost. | 4 |
|
credits.charged |
null | Always. | Empty here; the finished job reports it (GET /jobs/{job_id}). |
|
message |
text | What to do next, in words. |