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 period4. 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
{
"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
| Call | What it does here |
|---|---|
POST /v2/scan + asof | The candidate set at any past moment: the backtest’s selection side |
GET /v2/series | The pricing side: entry and exit closes on one aligned grid |
POST /v2/scan/subscribe | The validated condition, running live: same q |