# 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

- You have a Tickerbot API key. Get one from https://tickerbot.io/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

```
[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

```
gap_up_3pct AND volume_unusual_2x AND market_cap < 2000000000 AND NOT earnings_this_week
```

See the schema page (https://tickerbot.io/api/schema) for every flag and numeric column.

### 2. The full script

```javascript
// 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);
```

---

## 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

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

### 1. Create a Discord webhook

In Discord: Edit Channel → Integrations → Webhooks → New Webhook. Copy the URL. Save it as `DISCORD_HOOK`.

### 2. Create a Tickerbot webhook subscription

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

### 3. Write the receiver

```javascript
// 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"));
```

See https://tickerbot.io/api/endpoints/webhooks/dedup for why you won't be hammered with repeated events on the same ticker.

---

## 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

```
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%
```

### 1. Pick the strategy

```
rsi_oversold AND above_sma_200 AND volume_unusual_2x AND market_cap > 5000000000
```

### 2. Walk every trading day, collect hits

```python
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:
        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")
```

### 3. Check forward returns

```python
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)
    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

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