Create a custom signal
https://api.tickerbot.io/ v2/ signalsPersists a named boolean predicate you can reference anywhere a built-in signal goes. A custom signal is always boolean — the expression must evaluate to true/false (a non-boolean root is rejected at compile).
Once saved, the name resolves in these contexts, expanded to its SQL before the query runs:
- Scan — inside
/v2/scanq(live andasof), mixed freely with columns:q = my_squeeze AND market_cap > 1e9. - Signal — as the signal itself on
GET /v2/signals/{name}(live andasof) andGET /v2/signals/{name}/{ticker}/history; it behaves like a built-in boolean signal (matches where the predicate is true,conditionis ignored). - Ticker — inside a
POST /v2/tickers/{ticker}/subscribecondition. - Subscribe — as the signal on
POST /v2/signals/{name}/subscribe, or referenced from a scan/ticker subscribeq/condition. - Composition — inside another custom signal's
expr; nested references are inlined, and recursion is detected.
It does not resolve on GET /v2/signals/{name}/{ticker}/events — occurrence spans are precomputed for built-in flags only, so that endpoint returns 400 not_supported_for_custom_signal and points you at /history or /scan?asof=.
Resolution is an exact match against your own custom signal names. A name can never collide with a built-in column (create returns 409 name_collision), so the two never ambiguate; any identifier that isn't one of your custom signals is left as raw SQL.
Freeze on subscribe: the subscribe endpoints expand the custom signal into the stored webhook query at creation time, so a subscription is a snapshot. Editing or deleting the custom signal later does not change existing subscriptions — re-subscribe to apply a new definition. Ad-hoc reads (scan/signal GETs) always reflect the current definition.
The expression is compiled at create time against the live column whitelist; references to other custom signals you own are inlined first. Names are scoped per-user: (user_id, name) is the key.
On every plan, including Free. Custom signals are not a paid feature and are not capped — authoring one and *referencing* one (in a scan q, a ticker condition, as a signal name, or via history/stats/subscribe) both work on any plan. Defining a signal costs nothing at rest: it compiles into your scan SQL and is paid for by your existing rate limit. An account may hold up to 1,000 custom signals, an anti-abuse ceiling rather than a plan limit.
Body parameters
stringrequiredSlug — ^[a-z][a-z0-9_]{0,63}$. Must not collide with any built-in column name; columns and scaffold are reserved. This is the signal's API handle: it's what you reference in q and in the CRUD path.
stringrequiredBoolean SQL predicate. May reference built-in columns and other custom signals you own. Must evaluate to true/false. Max 4000 chars.
stringFree-form notes. Max 500 chars.
Status codes
201{ as_of, signal: { name, kind: "expression", description, expr, created_at, updated_at } }.400compile_failed (with errors array) when the expression doesn't parse / references unknown columns. bad_request for shape failures. too_many_custom_signals at the 1,000-per-account ceiling — an anti-abuse guard, not a plan limit.409already_exists (slug taken on this account) or name_collision (slug matches a built-in column).Notes
- Built-in signal infrastructure is unchanged — custom signals run alongside it.
kinddiscriminates the two on the catalog endpoint. - Custom signals are boolean by construction (the expression must evaluate to true/false). To vary a numeric threshold per query, keep the numeric column raw in
q/conditionrather than wrapping it in a custom signal. - Ad-hoc reads (scan/signal GETs) expand the current definition on every request; subscribe endpoints freeze the expansion at creation time (see the freeze note in the description).
- Cross-ticker references (e.g.
spy.close) inside a custom are supported in/v2/scanq, but not across the/v2/signalsfamily (single-ticker customs work everywhere). Keep a custom single-ticker to use it on every endpoint.