View as markdown
Getting started

Tutorials

Short, real walkthroughs. Each one ends with working code you can drop into a project, not pseudocode. Three takes on the same API.

What every tutorial assumes

One-time setup so each walkthrough can stay focused.

  • You have a Tickerbot API key. Get one from the dashboard; every plan starts with a 14-day free trial.
  • You set TICKERBOT_KEY in your environment. Tutorial code reads from there. Don't commit it.
  • You have Node 20+ (tutorials 1 and 2) or Python 3.10+ (tutorial 3) installed.

Momentum scanner in 20 lines

Build a live scanner that prints small-cap stocks gapping up on real volume, refreshed every minute. The whole thing is one /v2/scan call wrapped in a polling loop.

What you’ll build

A Node script that polls the API every 60 seconds and prints something like:

[2026-05-19T14:31:00.000Z] 4 matches
  OWLT   +14.2%  4.8× vol  $81M
  DJT    +11.8%  3.2× vol  $1840M
  RIVN    +9.2%  2.7× vol  $1390M
  SOUN    +7.4%  2.1× vol  $920M

1. Compose the query

The interesting work is picking a WHERE clause that captures the setup you care about:

  • gap_up_3pct: opened at least 3% above prior close
  • volume_unusual_2x: trading at >= 2× the 10-day average volume
  • market_cap < 2000000000: small-cap filter
  • NOT earnings_this_week: skip earnings reactions
gap_up_3pct AND volume_unusual_2x AND market_cap < 2000000000 AND NOT earnings_this_week

See the schema page for every flag and numeric column you can drop in.

2. The full script

// scanner.mjs — run with: TICKERBOT_KEY=tb_live_... node scanner.mjs
const QUERY = "gap_up_3pct AND volume_unusual_2x AND market_cap < 2000000000 AND NOT earnings_this_week";

async function scan() {
  const url = new URL("https://api.tickerbot.io/v2/scan");
  url.searchParams.set("q", QUERY);
  url.searchParams.set("order", "day_change_pct");
  url.searchParams.set("dir", "desc");
  url.searchParams.set("limit", "20");
  const r = await fetch(url, {
    headers: { Authorization: `Bearer ${process.env.TICKERBOT_KEY}` },
  });
  if (!r.ok) throw new Error(`scan failed: ${r.status}`);
  return r.json();
}

function render({ as_of, results }) {
  console.log(`[${as_of}] ${results.length} matches`);
  for (const row of results) {
    const pct = (row.day_change_pct * 100).toFixed(1);
    const rv  = row.relative_volume.toFixed(1);
    const mc  = (row.market_cap / 1e6).toFixed(0);
    console.log(`  ${row.ticker.padEnd(6)} +${pct}%  ${rv}× vol  $${mc}M`);
  }
}

async function tick() {
  try { render(await scan()); } catch (e) { console.error(e.message); }
}

await tick();
setInterval(tick, 60_000);

20 lines including imports and the loop. Edit the query to change the strategy; everything else stays the same.

Discord breakout bot

Get a Discord ping every time a new ticker breaks out on volume. A Tickerbot webhook fires on the match, your receiver verifies the signature, and forwards a tidy message to a Discord channel.

What you’ll build

A 30-line Node service that sits between Tickerbot and Discord. When a ticker enters the match set, you get a message like this in your channel:

🚀 New breakouts:
  NVDA  +7.1% on 1.8× volume
  AMD   +5.2% on 1.4× volume

The flow: Tickerbot evaluates the rule every minute → POSTs to your receiver when a new ticker matches → your receiver posts to Discord. No polling.

1. Create a Discord webhook

In Discord, right-click your channel, Edit Channel → Integrations → Webhooks → New Webhook. Name it, copy the URL. Save it as DISCORD_HOOK in your environment.

2. Create a Tickerbot webhook subscription

Below: any ticker that fires the breakout flag while sitting above its 200-day moving average. Replace the target URL with whatever public URL points at your receiver (use ngrok for local dev).

curl -X POST https://api.tickerbot.io/v2/webhooks \
  -H "Authorization: Bearer $TICKERBOT_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "discord-breakouts",
    "q": "breakout AND above_sma_200",
    "target_url": "https://your-receiver.example.com/hook"
  }'

The response includes a one-shot signing_secret. Save it as TB_SECRET; it's only shown once. Every delivery is HMAC-signed with this so you can verify it came from us.

3. Write the receiver

A tiny Express server with one route. Two jobs: verify the signature, then forward to Discord.

// receiver.mjs
import express from "express";
import crypto from "crypto";

const app = express();
const DISCORD_HOOK = process.env.DISCORD_HOOK;
const TB_SECRET    = process.env.TB_SECRET;

app.post("/hook", express.raw({ type: "application/json" }), async (req, res) => {
  // 1) verify HMAC
  const header = req.get("Tickerbot-Signature") || "";
  const parts  = Object.fromEntries(header.split(",").map(p => p.split("=")));
  const signed = crypto.createHmac("sha256", TB_SECRET)
    .update(`${parts.t}.${req.body.toString()}`)
    .digest("hex");
  if (signed !== parts.v1) return res.status(401).end();

  // 2) handle the event
  const event = JSON.parse(req.body.toString());
  if (event.event === "webhook.ping")  return res.status(200).end();
  if (event.event !== "webhook.fired") return res.status(200).end();

  const lines = event.tickers.map(t => {
    const pct = (t.day_change_pct * 100).toFixed(1);
    const rv  = t.relative_volume.toFixed(1);
    return `  **${t.ticker}**  +${pct}% on ${rv}× volume`;
  });

  await fetch(DISCORD_HOOK, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ content: `🚀 New breakouts:\n${lines.join("\n")}` }),
  });

  res.status(200).end();
});

app.listen(3000, () => console.log("listening on :3000"));

The HMAC verification protects you from spoofed webhooks. See state-change deduplication for why you won’t get hammered with repeated events on the same ticker.

4. Expose it and test

For local dev, run ngrok to get a public URL, then copy that URL into the target_url of the webhook from step 2 (use PATCH /v2/webhooks/{id} if you need to update it). Tickerbot sends a one-shot webhook.ping on creation to verify reachability; once your receiver returns 200, the subscription flips to active and starts firing real events on matches.

Backtest a strategy over 2024

Run the same SQL WHERE clause across every trading day of 2024, collect the hits, then check each ticker’s 5-day forward return. The backtest is two API calls in a loop.

What you’ll build

A Python script that prints something like:

452 signal hits across 2024
5-day forward win rate: 58.4%
Median 5-day forward return: +1.2%
Best:  NVDA on 2024-02-22 → +18.4%
Worst: SMCI on 2024-08-08 → -22.1%

The trick: /v2/scan?asof=YYYY-MM-DD evaluates your same query against the historical state of the universe on that date. No CSV downloads, no point-in-time bookkeeping; the API handles it.

1. Pick the strategy

Same q as a live scan. Below: stocks coming out of oversold while sitting above their 200-day average and carrying real volume.

rsi_oversold AND above_sma_200 AND volume_unusual_2x AND market_cap > 5000000000

2. Walk every trading day, collect hits

import os, requests
from datetime import date, timedelta

KEY     = os.environ["TICKERBOT_KEY"]
HEADERS = { "Authorization": f"Bearer {KEY}" }
QUERY   = "rsi_oversold AND above_sma_200 AND volume_unusual_2x AND market_cap > 5000000000"

def scan_asof(d: date) -> list[dict]:
    r = requests.get(
        "https://api.tickerbot.io/v2/scan",
        params={ "q": QUERY, "asof": d.isoformat(), "limit": 100 },
        headers=HEADERS,
    )
    r.raise_for_status()
    return r.json()["results"]

hits = []
day, end = date(2024, 1, 2), date(2024, 12, 31)
while day <= end:
    if day.weekday() < 5:           # skip weekends
        for row in scan_asof(day):
            hits.append({ "date": day, "ticker": row["ticker"], "entry": row["price"] })
    day += timedelta(days=1)

print(f"{len(hits)} signal hits across 2024")

~252 calls total — well under a paid plan's minute quota.

3. Check forward returns

def price_on_or_after(ticker: str, target: date) -> float | None:
    r = requests.get(
        f"https://api.tickerbot.io/v2/signals/price/{ticker}/history/1d",
        params={ "from": target.isoformat(), "limit": 1 },
        headers=HEADERS,
    )
    r.raise_for_status()
    bars = r.json()["bars"]
    return bars[0]["v"] if bars else None

returns = []
for h in hits:
    exit_target = h["date"] + timedelta(days=7)   # ~5 trading days
    exit_price  = price_on_or_after(h["ticker"], exit_target)
    if exit_price is None: continue
    returns.append((h["ticker"], h["date"], (exit_price - h["entry"]) / h["entry"]))

4. Summarize

wins = sum(1 for _, _, r in returns if r > 0)
median = sorted(r for _, _, r in returns)[len(returns) // 2]
best  = max(returns, key=lambda x: x[2])
worst = min(returns, key=lambda x: x[2])

print(f"5-day forward win rate: {wins / len(returns):.1%}")
print(f"Median 5-day forward return: {median:+.1%}")
print(f"Best:  {best[0]} on {best[1]} → {best[2]:+.1%}")
print(f"Worst: {worst[0]} on {worst[1]} → {worst[2]:+.1%}")

Plan history depth

To run a backtest over 2024 you need at least one full year of historical scan depth. Hobby covers 1 year, Pro covers 5, Scale and Enterprise cover all-time. If you go further back than your plan allows, the API returns history_window_exceeded with earliest_allowed_asof so your code can clamp.