← tickerbot.io
View as markdown
Concepts

As-of queries

An as-of query rewinds the table to a past date: the state as it actually stood then, not as it looks now. One parameter, asof, and one grain control, interval.

What asof answers

Pass a past date, get the state as it stood then.

Pass asof and you get the row (or the match set) as it stood at that instant: one ticker’s whole row, who matched this signal, who matched this scan, one row per ticker on Series, or the last closed bar. A YYYY-MM-DD date returns the state at that day’s close; a timestamp returns it at that intraday moment. Timestamps are the strict ISO subset YYYY-MM-DDTHH:MM[:SS]Z — UTC Z only, numeric offsets are a 400.

State is stored at three grains — 1m, 1h, 1d — and they do not all carry the same signals, so a past date carries a second question: at what grain? interval answers it; _meta.interval reports what was actually served.

Which company did the symbol mean?

A ticker is not a permanent name. asof resolves the entity, not just the date.

Symbols get reassigned. SHLD was Sears Holdings until 2018, then a VictoryShares ETF, and today it is the Global X Defense Tech ETF — three unrelated companies, one symbol. Without asof a symbol always means the company holding it now. With it, the symbol resolves to whoever held it at that instant, and you get that company’s identity and its prices:

# Sears Holdings — the company that traded under SHLD in 2010
curl "https://api.tickerbot.io/v2/tickers/SHLD?asof=2010-06-30" \
  -H "Authorization: Bearer $TICKERBOT_API_KEY"

# the Global X ETF that holds the symbol today
curl "https://api.tickerbot.io/v2/tickers/SHLD" \
  -H "Authorization: Bearer $TICKERBOT_API_KEY"

The ticker record carries the ownership history directly: tenure_start and tenure_end are the dates the company on the record has held the symbol, symbol_reused is true when an earlier unrelated company held it, predecessors[] lists those earlier holders with their own cik and composite_figi, and prior_symbols[] lists the same company’s earlier tickers — so META reports FB. This is what makes a backtest over a past universe safe: the symbol you screen on resolves to the company that actually traded it.

Choosing the grain: interval

1m / 1h / 1d, or auto. Same name and same values as Series and Bars.

interval takes 1m, 1h, 1d, or auto (the default) — the same values as Series and Bars, one vocabulary for “which grain” across the API.

# Pin the grain. Every signal comes from the 1h tier, or you get a 400.
curl -X POST "https://api.tickerbot.io/v2/scan" \
  -H "Authorization: Bearer YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"q": "above_vwap", "asof": "2026-06-28T17:45:00Z", "interval": "1h"}'

Why pin it. Not every signal exists at every grain — rsi_14, fundamentals and valuation ratios are 1d-only — and a cross-section has to resolve to one grain, so a single 1d-only signal decides the grain for every signal in the request. With interval set that is a 400 interval_unavailable naming the signal and the grains that do carry it, instead of the daily close quietly standing in for everything else.

auto picks the finest grain whose signals and ticker coverage both work out, resolving upward on a miss; _meta.interval_reason says which grain ran and why, and _meta.coverage_gap lists any tickers with no row there. The grain can legitimately differ between two requests — a different projection, universe, or instant changes what is servable — so pin the interval if you are storing numbers or comparing across dates.

_meta.resolution is a deprecated alias of _meta.interval, still carrying the old minute/hourly/daily names — read interval.

The three shapes

One parameter, three shapes: a ticker read merges per signal, while a signal’s match set and a market-wide scan are cross-sections that resolve to a single grain.

One ticker — Ticker state (as-of). This is the one shape that merges per signal: each signal comes from the finest grain that carries it and has a row at or before your instant, so price can be the 17:45 value while rsi_14 is that morning’s daily figure. The row is therefore a blend, which is what _meta.blended, _meta.sources (per grain, with the signals each contributed) and _meta.column_intervals (per signal) tell you. _meta.interval is the finest grain present, not the age of the whole row.

curl "https://api.tickerbot.io/v2/tickers/AAPL?asof=2026-06-28" \
  -H "Authorization: Bearer YOUR_KEY"

One signal — Ticker matches (as-of):

curl "https://api.tickerbot.io/v2/signals/at_52w_high?asof=2026-06-28" \
  -H "Authorization: Bearer YOUR_KEY"

Many tickers — Market-wide scan (as-of):

curl -X POST https://api.tickerbot.io/v2/scan \
  -H "Authorization: Bearer YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"q":"above_sma_200","asof":"2026-06-28"}'

Both are cross-sections: every row comes from ONE grain — comparing tickers captured at different instants is not a cross-section. That is why a 1d-only signal in the projection sets the grain for the whole request, and why interval exists.

Seven identity signals — name, sector, industry, asset_type, exchange, asset_class, ticker_category — are not historized: every as-of read serves them from the ticker’s current row, as _meta.frozen_fields discloses. Group an as-of scan by sector and you group by today’s sector.

Membership in an as-of set is point-in-time (survivorship-safe): a ticker appears if it was listed and not yet delisted at that instant, so a since-delisted name can legitimately show up in a historical match set — which is what keeps a backtest over these results honest. Live reads of the same endpoints return currently-active tickers only.

A bare YYYY-MM-DD means that day’s close; add a time (2026-06-28T17:45:00Z) for an intraday moment. A date has none to resolve, so a date plus interval=1m/1h is a 400.

Bars and Series take asof too — the last closed row on a grid

On a grid endpoint, point-in-time means the most recent grid row at or before the instant, on the grain you chose.

Bars accept asof and return the most recent bar whose period had closed at or before the moment you asked about. At 10:00 ET on a Wednesday the day’s close has not happened, so interval=1d&asof=that instant returns Tuesday’s bar. A bare date still means “that day’s close,” matching the rest of this page.

Series works the same way, for up to 50 tickers at once: asof turns the range read into a point read — one row per ticker, the last row at or before the instant on the requested interval’s grid, with the symbol resolved to whoever held it then (the SHLD rule above). At interval=1q the anchor is the date the quarter was reported, so you get the latest quarter that was public knowledge at the instant — a restatement filed later doesn’t leak back.

On bars, asof returns a single bar unless you pass limit, which gets you the last limit closed bars; on series it is always one row per ticker. On both, combining it with before/cursor or from/to is a 400: those page back from the live tail or bound a window, asof reads one point in time.

Unlimited depth

As-of reaches all the way back — no window.

asof is unlimited-depth. No endpoint bounds how far back you can look.

Point-in-time is a different promise from Series, which is also all-time: series answers “how did this evolve”; an as-of query rewinds the whole market to a past cross-section.

Staleness: “the state AT that instant”

asof answers what was true then — not the last thing known.

The row served must be within the tier’s staleness window (14 days for daily) of the instant you asked for. If nothing that fresh exists — a delisted ticker, an instant before it started trading — the ticker 404s with a last_available pointer rather than handing back a stale row. When you want the last known state instead of the state at a moment, read /v2/series?ticker={t}&limit=1.

Coverage: a null can mean “not yet backfilled”

Most signals reconstruct exactly; a few vendor-sourced ones are forward-only.

Price, technical indicators, and financial-statement signals reconstruct to the exact date. A few vendor-sourced signals — forward_pe, short_interest, ownership percentages, analyst estimates — are populated forward-only, so an older snapshot may return null while backfill is in progress. That null means “not yet backfilled,” not “no value existed.”

News and events don't take asof

Archives with a real timestamp filter by time directly — there's nothing to reconstruct.

asof exists because scan, signals, tickers and bars reconstruct the row as of a past date. News and events don’t need it: each row already carries the real moment it happened (time_published, the event ts), so “as of a past date” is a WHERE filter on that column (or since/until) — no asof parameter, no staleness window. Logs filter by their timestamp; reconstructed state takes asof.

The two meet on All events: join=state evaluates ticker-state signals as of each event’s timestamp — point-in-time state per event row, at 1d (reported as _meta.state_interval; the old state_resolution key remains as a deprecated alias).

The sandbox is the same endpoint

No key, same answer.

The no-key /v2/sandbox/* as-of routes run the same handlers as the keyed ones — same interval, _meta, staleness rule, as_of stamp, and the same unlimited depth. The only difference is the anonymous per-IP budget (see Errors & rate limits).