tickerbot.io
View as markdown

Backtester

Tickerbot is the stock market API designed for agents. A backtest is a few queries on it, in sequence: the same scan as of each past date, the prices that came next, then the same scan subscribed.

How it works

Run the same query as of each past date. ?asof= returns the market as it stood that day, delisted names included. The loop is all 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" }'

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"

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.

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 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" }'
Show responseHide response
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

CallWhat it does here
POST /v2/scan + asofThe candidate set at any past moment: the backtest’s selection side
GET /v2/seriesThe pricing side: entry and exit closes on one aligned grid
POST /v2/scan/subscribeThe validated condition, running live: same q