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