tickerbot.io
View as markdown

Trading algorithm

Tickerbot is the stock market API designed for agents. A trading algorithm is a few queries on it, in sequence: define the condition, validate it as of past dates, subscribe it, trade on the webhook.

How it works

A trading algorithm is a condition and what you do when it fires. Write the condition in SQL, validate it against the past, subscribe it as a webhook. Your handler decides, your broker executes.

1. Define the entry and exit condition

A SQL WHERE clause over 452+ columns: the whole market evaluated in one call.

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" }'

One q carries both sides: a ticker starting to match is your entry, a held ticker no longer matching is your exit. There is no separate exit condition to author.

2. Validate it historically

The same q with asof reruns it against the market as it was, delisted tickers included, unlimited depth.

const KEY = process.env.TICKERBOT_API_KEY
const Q = 'rsi_14 < 30 AND above_sma_200 AND market_cap > 1e9'
const dates = ['2024-01-05', '2024-02-05', '2024-03-05', '2024-04-05', '2024-05-06']

const positions = new Map()   // ticker → entry price

for (const date of dates) {
  // who matches, knowing only what this date knew — rows carry the price
  const { results } = await fetch('https://api.tickerbot.io/v2/scan', {
    method: 'POST',
    headers: { Authorization: `Bearer ${KEY}`, 'Content-Type': 'application/json' },
    body: JSON.stringify({ q: Q, asof: date }),
  }).then((r) => r.json())
  const matching = new Map(results.map((r) => [r.ticker, r.price]))

  // EXITS — held positions no longer matching. An exited ticker is absent
  // from the scan, so one series call prices the exits.
  const exiting = [...positions.keys()].filter((t) => !matching.has(t))
  if (exiting.length) {
    const { series } = await fetch(
      `https://api.tickerbot.io/v2/series?tickers=${exiting.join(',')}` +
        `&columns=close&interval=1d&from=${date}&to=${date}`,
      { headers: { Authorization: `Bearer ${KEY}` } },
    ).then((r) => r.json())
    for (const t of exiting) {
      const close = (series[t] ?? [])[0]?.close
      if (close) console.log(`${date}  exit  ${t}  ${((close / positions.get(t) - 1) * 100).toFixed(1)}%`)
      positions.delete(t)
    }
  }

  // ENTRIES — matching tickers not yet held, priced by their own scan row
  for (const [t, price] of matching) {
    if (!positions.has(t) && price) positions.set(t, price)
  }
}

// Real output for this q (condensed):
// 2024-02-05  exit  AAPL  3.6%     2024-03-05  exit  DUOL  18.4%
// 2024-02-05  exit  MANH  25.0%    2024-03-05  exit  AFL   5.9%
// 2024-02-05  exit  KFRC  8.7%     2024-04-05  exit  RAMP  8.4%
// 2024-02-05  exit  LOPE  5.1%     2024-05-06  exit  PFGC  -1.1%
// 2024-02-05  exit  WINA  -2.1%    2024-05-06  exit  NXT   -6.3%

This walk validates the exact rule the subscription trades: enter when a ticker matches (its scan row carries the entry price), exit when it no longer does. An exited ticker is absent from the scan, so one series call prices the exits. The full treatment (equity curve, position sizing) is the backtester guide.

3. Subscribe it

Subscribe the same query as a webhook: a ticker entering your condition is your cue to decide; your handler routes any order to your broker.

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_SVrlI0OowiE",
  "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",
  …
}

4. Write the algorithm

The delivery's matches are tickers that just entered the condition; current_matches is everything matching now, so entries and exits come from one delivery. Payload lists cap at 100 rows; confirm any single position with GET /v2/tickers/{ticker}.

import express from 'express'
const app = express()
app.use(express.json())

const positions = new Map()                     // ticker → qty: your book
const buy  = async (ticker, qty) => { /* your broker's order API */ }
const sell = async (ticker, qty) => { /* your broker's order API */ }

app.post('/hook', async (req, res) => {
  const { matches, current_matches } = req.body
  const matching = new Set(current_matches.map((r) => r.ticker))

  // ENTRIES — new matches you don't already hold
  for (const row of matches) {
    if (!positions.has(row.ticker)) {
      await buy(row.ticker, 100)
      positions.set(row.ticker, 100)
    }
  }

  // EXITS — held positions whose entry condition no longer holds
  for (const [ticker, qty] of positions) {
    if (!matching.has(ticker)) {
      await sell(ticker, qty)
      positions.delete(ticker)
    }
  }

  res.sendStatus(200)
})

app.listen(3000)   // the server behind your target_url

Condition-exit is one exit style, thesis invalidation. Stops, targets, and time exits are your strategy logic, layered on the same book.

The calls behind it

CallWhat it does here
POST /v2/scanThe condition, evaluated across the whole market: live or ?asof=
GET /v2/seriesThe validation’s pricing side: forward closes on one aligned grid
POST /v2/scan/subscribeThe same condition as a push: fires on match-set changes