View as markdown
Concepts

SQL

Compose signals into a SQL WHERE clause and pass it as q: gap_up AND small_cap AND NOT earnings_this_week. Signals are columns of various types — pre-thresholded flags you name directly (gap_up), numerics and strings you compare (rsi_14 < 30, sector = 'Technology') — built-in and custom alike. The same grammar goes everywhere a query goes: q on /scan, /events, and /news, and the q / event_q fields on /webhooks. What changes per surface is only the row it filters. The one exception is custom-signal expr, which compiles against a stricter subset — see the limitations list below.

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 schema columns of any type — pre-thresholded flags 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 columns 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_percent < 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 day_change_pct > xlk.day_change_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 columns. 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 columns 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 works for these fixed macro and sector ETFs:

contextwhat it is
diaDow Jones Industrial Average ETF.
iwmRussell 2000 ETF — small-cap proxy.
qqqNASDAQ 100 ETF — large-cap tech proxy.
spyS&P 500 ETF — broad-market proxy.
xlbMaterials sector ETF.
xlcCommunication Services sector ETF.
xleEnergy sector ETF.
xlfFinancials sector ETF.
xliIndustrials sector ETF.
xlkTechnology sector ETF.
xlpConsumer Staples sector ETF.
xlreReal Estate sector ETF.
xluUtilities sector ETF.
xlvHealthcare sector ETF.
xlyConsumer Discretionary sector ETF.

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 column, so it's a single bound like ?condition=>70 (operators > >= = != < <=). Boolean flags 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.

  • 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`.