View as markdown

Backtester

Tickerbot is the stock market, in SQL. A backtest is one WHERE clause on it, with ?asof=.

The hard part of a backtester was never the loop; it's the data honesty. ?asof= reruns any query against the market as it was — delisted names still in it — so the loop is the only part you write.

1. Pick candidates without look-ahead

An as-of scan evaluates your entry condition with only what was knowable at that moment.

curl -X POST "https://api.tickerbot.io/v2/scan" \
  -H "Authorization: Bearer YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "q": "rsi_14 < 30 AND above_sma_200 AND market_cap > 1e9",
        "asof": "2024-01-05" }'
Response
{
  "as_of": "2024-01-05",
  "count": 5,
  "results": [
    { "ticker": "AAPL", "name": "Apple Inc.",
      "price": 181.18, "market_cap": 2864917265460, … },
    { "ticker": "MANH", "name": "Manhattan Associates Inc",
      "price": 199.37, "market_cap": 12422744700, … },
    { "ticker": "KFRC", … }, { "ticker": "LOPE", … }, { "ticker": "WINA", … }
  ]
}

Delisted names keep their rows and bar history — ask about SIVB or LEH and you get records, not a 404 — so match sets are not conditioned on survival.

2. Price the forward window

The trade's outcome is the price path after the pick: /v2/series returns the whole candidate set's closes on one aligned daily grid, entry date to exit date.

curl "https://api.tickerbot.io/v2/series?tickers=AAPL,MANH,KFRC,LOPE,WINA&columns=close&interval=1d&from=2024-01-05&to=2024-02-05" \
  -H "Authorization: Bearer YOUR_KEY"
Response
{
  "interval": "1d",
  "tickers": ["AAPL", "MANH", "KFRC", "LOPE", "WINA"],
  "columns": ["close"],
  "count": 105,
  "series": {
    "AAPL": [ { "t": "2024-01-05", "close": 181.18 }, …, { "t": "2024-02-05", "close": 187.68 } ],
    "MANH": [ { "t": "2024-01-05", "close": 199.37 }, …, { "t": "2024-02-05", "close": 249.15 } ],
    …
  }
}

3. The loop

Both calls in a walk over rebalance dates: candidates as of each date, equal-weight forward return from the closes, compounded into an equity curve. Complete — every variable is defined.

const KEY = process.env.TICKERBOT_API_KEY
const Q = 'rsi_14 < 30 AND above_sma_200 AND market_cap > 1e9'

// Monthly rebalance dates, 2024 → today
const dates = []
for (let d = new Date('2024-01-05'); d < new Date(); d.setMonth(d.getMonth() + 1)) {
  dates.push(d.toISOString().slice(0, 10))
}

let equity = 1
for (let i = 0; i < dates.length - 1; i++) {
  // candidates, knowing only what dates[i] knew
  const scan = await fetch('https://api.tickerbot.io/v2/scan', {
    method: 'POST',
    headers: { Authorization: `Bearer ${KEY}`, 'Content-Type': 'application/json' },
    body: JSON.stringify({ q: Q, asof: dates[i], order: 'market_cap', dir: 'desc' }),
  }).then((r) => r.json())
  const tickers = scan.results.map((r) => r.ticker).slice(0, 10) // largest 10, equal-weight
  if (tickers.length === 0) continue

  // entry and exit closes from the aligned grid
  const series = await fetch(
    `https://api.tickerbot.io/v2/series?tickers=${tickers.join(',')}` +
      `&columns=close&interval=1d&from=${dates[i]}&to=${dates[i + 1]}`,
    { headers: { Authorization: `Bearer ${KEY}` } },
  ).then((r) => r.json())

  // equal-weight period return, compounded
  const rets = tickers.map((t) => {
    const rows = series.series[t] ?? []
    return rows.length > 1 ? rows[rows.length - 1].close / rows[0].close - 1 : 0
  })
  const period = rets.reduce((a, b) => a + b, 0) / rets.length
  equity *= 1 + period
  console.log(dates[i], tickers.length + ' names', (period * 100).toFixed(1) + '%', equity.toFixed(3))
}

// 2024-01-05  5 names  +8.1%  1.081   ← real output for this q's first period

4. Take it live

The exact q the backtest validated becomes the live subscription — no rewrite between research and production.

curl -X POST "https://api.tickerbot.io/v2/scan/subscribe" \
  -H "Authorization: Bearer YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "q": "rsi_14 < 30 AND above_sma_200 AND market_cap > 1e9",
        "target_url": "https://your-bot.example.com/hook" }'
Response
{
  "id": "wh_I5VwAnNkgYA",
  "q": "rsi_14 < 30 AND above_sma_200 AND market_cap > 1e9",
  "channel": "webhook",
  "target_url": "https://your-bot.example.com/hook",
  "signing_secret": "whsec_…",
  "status": "active",
  …
}

The calls behind it

All build guides