← tickerbot.io
View as markdown
Concepts

SQL

One SQL WHERE clause over signals. The same clause is q on scan, events and news, and the condition on a webhook.

One grammar, three projections

Same WHERE clause; the identifiers come from the surface you're querying.

  • The ticker row — Scan and webhook q. Identifiers are signals of any type — booleans you name directly, numerics, strings, and dates you compare — plus the qualified context tickers below.
  • The event row — All events and the webhook event_q content filter. Exactly four identifiers: ticker, ts, kind, and payload, with jsonb operators for payload fields (payload->>'firm' = 'Goldman Sachs'). With join=stateticker-state signals become legal too, evaluated as of each event's timestamp.
  • The article row — News. Article columns (time_published, source, sentiment fields) plus the tk alias for per-ticker rollups.

Sending the query

POST a JSON body. No encoding needed.

Send the query in a JSON body via POST /v2/scan — the same pattern holds on /v2/events and /v2/news. GET with q as a URL parameter also works, but then the query must be URL-encoded: spaces become %20, > becomes %3E, single quotes become %27. Most HTTP clients encode automatically.

# JSON body: the query travels as written, no URL encoding
curl -X POST "https://api.tickerbot.io/v2/scan" \
  -H "Authorization: Bearer YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"q": "above_sma_200 AND market_cap > 1e10"}'

Examples

Bullish breakouts on volume

Stocks at or above their 20-day high on at least 2× their normal volume.

curl -X POST "https://api.tickerbot.io/v2/scan" \
  -H "Authorization: Bearer YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "q": "at_20d_high AND high_volume_alert"
  }'

Small-cap gappers, avoiding earnings noise

Small caps gapping up, with no earnings announcement this week.

curl -X POST "https://api.tickerbot.io/v2/scan" \
  -H "Authorization: Bearer YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "q": "gap_up AND small_cap AND NOT earnings_this_week"
  }'

Oversold tech with healthy fundamentals

Oversold technology names that are still profitable.

curl -X POST "https://api.tickerbot.io/v2/scan" \
  -H "Authorization: Bearer YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "q": "rsi_oversold AND sector = 'Technology' AND pe_ratio BETWEEN 5 AND 30"
  }'

New 52-week highs with momentum

Stocks making fresh 52-week highs with strong upward momentum.

curl -X POST "https://api.tickerbot.io/v2/scan" \
  -H "Authorization: Bearer YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "q": "at_52w_high AND momentum_strong_up"
  }'

Intraday range breaks

Names that broke above the prior 15-minute high on heavy volume.

curl -X POST "https://api.tickerbot.io/v2/scan" \
  -H "Authorization: Bearer YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "q": "range_break_15m_up AND high_volume_alert"
  }'

Mega-cap consolidation

Mega-cap stocks coiling for a move: tight Bollinger bands, low volatility.

curl -X POST "https://api.tickerbot.io/v2/scan" \
  -H "Authorization: Bearer YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "q": "market_cap > 200000000000 AND atr_pct < 1.5"
  }'

Sector rotation candidates

Healthcare and energy names crossing above their 50-day average.

curl -X POST "https://api.tickerbot.io/v2/scan" \
  -H "Authorization: Bearer YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "q": "sector IN ('Healthcare', 'Energy') AND above_sma_50 AND NOT in_downtrend"
  }'

Macro-conditioned setup (qualified context tickers)

RSI-oversold names, but only when the S&P 500 is holding above its 50-day average.

curl -X POST "https://api.tickerbot.io/v2/scan" \
  -H "Authorization: Bearer YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "q": "rsi_oversold AND spy.price > spy.sma_50"
  }'

Sector-relative breakout

Tech breakouts that are also outpacing the tech sector ETF on the same day.

curl -X POST "https://api.tickerbot.io/v2/scan" \
  -H "Authorization: Bearer YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "q": "at_20d_high AND sector = 'Technology' AND change_1d_pct > xlk.change_1d_pct"
  }'

Context tickers (qualified syntax)

Reference the market itself inside a query about individual stocks.

A scan or webhook q normally filters each ticker on its own signals. Sometimes the condition you care about lives on a different symbol: “oversold names, but only while the S&P 500 holds its 50-day.” Writing spy.price > spy.sma_50 pulls those signals from SPY into the same query — no second call, no client-side join, and every clause evaluates against the same instant (including under asof). The qualifier is any symbol the catalog returns, spelled as it appears there — an ETF, an equity, a rate, a crypto or FX pair:

examplewhat it is
spyAn ETF — S&P 500 proxy. Any ETF works the same way (qqq, iwm, xlk, ...).
aaplAny equity in the catalog — a bellwether, a peer, a competitor.
r:sofrA rate row (R: prefix) — SOFR, fed funds, Treasury yields and spreads.
x:btcusdA crypto or FX row (X: prefix).
  • A qualifier that is not in the catalog is a 400 unknown_context_ticker naming it — never a raw SQL error, never a silent zero-row result.
  • Symbols with a dot or hyphen in them (BRK.B) cannot be qualifiers: the dot is the separator. Everything else in the catalog can, including prefixed ids (r:sofr, x:btcusd).
  • vix and dxy are index series no vendor we hold carries; they answer 400 context_ticker_unavailable.
  • There is no cap on refs per query beyond the 4,000-character limit on q; each ref is one indexed lookup evaluated once per query, whatever the symbol.
  • Refs inside a custom signal's expr are not rewritten. Keep custom signals single-ticker and put the context ref in q.

Where SQL doesn’t apply

A few reads ask a narrower question than a WHERE clause.

  • Signal matches take condition, not q: the question is one comparator on one named signal, so it's a single bound like ?condition=>70 (operators > >= = != < <=). Booleans need no condition at all. The same condition shape appears on ticker- and signal-trigger webhooks.
  • Series and bars take structured params (columns, interval, from/to) — you're selecting a grid, not filtering rows.
  • Tickers reads are keyed by symbol; there's nothing to filter.

What we don’t accept

Valid SQL features we explicitly reject. Avoids surprises when an otherwise-correct WHERE clause returns a 400.

The one surface with a stricter subset is custom-signal expr, which compiles against the list below.

  • No subqueries or joins. A query filters one row: the wide ticker row on /v2/scan, the (ticker, ts, kind, payload) event row on /v2/events, the article row on /v2/news. The one sanctioned join is `join=state` on /v2/events — a parameter, not SQL you write.
  • No statement composition. `UNION` / `EXCEPT` / `INTERSECT`, `WITH`, `SELECT`, any DDL/DML keyword, semicolons, and SQL comments (`--`, `/* */`) are rejected.
  • Functions are whitelisted: aggregates (`count`, `avg`, `sum`, `min`, `max`, `string_agg`, ...) in `select`/`having`, plus scalar helpers (`coalesce`, `round`, `abs`, `lower`, `upper`, `length`, `date_trunc`, `extract`, `now`, ...). Anything else — including window functions (`OVER (...)`, `row_number`, `rank`, `lag`) — is a 400 naming the function.
  • Everything else you'd expect works: `LIKE`/`ILIKE`, `IN`, `BETWEEN`, `CASE`, computed expressions (`price / sma_50 > 1.05`), and casts to whitelisted types (`::numeric`, `::date`, `::timestamptz`, ...).
  • No double-quoted identifiers. Write columns bare (`price`, not `"price"`); single quotes are for string literals.
  • Custom-signal `expr` is stricter still: comparisons, `AND`/`OR`/`NOT`, `IN`, `BETWEEN`, `IS [NOT] NULL`, arithmetic, and only `abs`/`coalesce`/`round`/`least`/`greatest`. No `LIKE`/`ILIKE`, no `CASE`, no `::` casts. An expression that works in a scan `q` can still be rejected with `compile_failed`.