View as markdown
Endpoints · Signals

Create a custom signal

POSThttps://api.tickerbot.io/v2/signals

Persists 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/scan q (live and asof), mixed freely with columns: q = my_squeeze AND market_cap > 1e9.
  • Signal — as the signal itself on GET /v2/signals/{name} (live and asof) and GET /v2/signals/{name}/{ticker}/history; it behaves like a built-in boolean signal (matches where the predicate is true, condition is ignored).
  • Ticker — inside a POST /v2/tickers/{ticker}/subscribe condition.
  • Subscribe — as the signal on POST /v2/signals/{name}/subscribe, or referenced from a scan/ticker subscribe q/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.

stringrequired

Slug — ^[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.

stringrequired

Boolean SQL predicate. May reference built-in columns and other custom signals you own. Must evaluate to true/false. Max 4000 chars.

string

Free-form notes. Max 500 chars.

201
{ as_of, signal: { name, kind: "expression", description, expr, created_at, updated_at } }.
400
compile_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.
409
already_exists (slug taken on this account) or name_collision (slug matches a built-in column).
  • Built-in signal infrastructure is unchanged — custom signals run alongside it. kind discriminates 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/condition rather 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/scan q, but not across the /v2/signals family (single-ticker customs work everywhere). Keep a custom single-ticker to use it on every endpoint.