import { defineSource } from '@jaspers-ai/sdk' import { z } from 'zod' import { HOSTS, baseField, get, iso, list, upper, type Row } from './client' // Snapshots: stock tickers, gainers and losers, the universal snapshot, last trade and last quote. const n = (v: unknown): number | null => (typeof v === 'number' && Number.isFinite(v) ? v : null) /** The first number that is there and not zero (Massive zeroes the day before the open). */ const firstNonZero = (...vs: unknown[]): number | null => { for (const v of vs) if (typeof v === 'number' && v !== 0 && Number.isFinite(v)) return v return null } /** One v2 snapshot (stocks, forex, crypto) as a flat row. */ export function snapRow(s: any): Row { const day = s?.day ?? {} const prev = s?.prevDay ?? {} const lt = s?.lastTrade ?? {} const lq = s?.lastQuote ?? {} const min = s?.min ?? {} const bid = n(lq.p ?? lq.b) const ask = n(lq.P ?? lq.a) const mid = bid !== null && ask !== null && bid > 0 && ask > 0 ? (bid + ask) / 2 : null return { ticker: s?.ticker ?? null, last: firstNonZero(lt.p, min.c, mid, day.c, prev.c), change: n(s?.todaysChange), changePercent: n(s?.todaysChangePerc), open: n(day.o), high: n(day.h), low: n(day.l), close: n(day.c), volume: n(day.v), vwap: n(day.vw), prevClose: n(prev.c), bid, ask, updated: iso(s?.updated), } } function universalRow(r: any): Row { const s = r.session ?? {} const lq = r.last_quote ?? {} const g = r.greeks ?? {} return { ticker: r.ticker ?? null, type: r.type ?? null, name: r.name ?? null, market_status: r.market_status ?? null, price: firstNonZero(r.value, r.last_trade?.price, s.price, s.close, s.previous_close), change: n(s.change), changePercent: n(s.change_percent), open: n(s.open), high: n(s.high), low: n(s.low), close: n(s.close), previousClose: n(s.previous_close), volume: n(s.volume), bid: n(lq.bid), ask: n(lq.ask), implied_volatility: n(r.implied_volatility), open_interest: n(r.open_interest), delta: n(g.delta), gamma: n(g.gamma), theta: n(g.theta), vega: n(g.vega), underlying: r.underlying_asset?.ticker ?? null, updated: iso(r.last_updated ?? s.last_updated), error: r.error ? `${r.error}: ${r.message ?? ''}`.trim() : null, } } const snapshot = defineSource({ description: 'Stock snapshots: ticker, last price, change, change %, day open/high/low/close/volume/vwap, previous close, bid, ask, updated. Pass tickers, or none for every US stock (largest volume first).', hosts: HOSTS, input: z.object({ tickers: z.array(z.string()).max(250).default([]).describe('Stock tickers like AAPL. Empty for all.'), include_otc: z.boolean().default(false).describe('Include OTC securities when listing all.'), limit: z.number().int().min(1).max(15000).default(500).describe('Most rows to answer.'), base: baseField, }), async run(args, ctx) { const tickers = args.tickers.map(upper).filter(Boolean) const path = '/v2/snapshot/locale/us/markets/stocks/tickers' if (tickers.length === 1) { const body = await get(ctx, args.base, `${path}/${tickers[0]}`) return body?.ticker ? [snapRow(body.ticker)] : [] } const body = await get(ctx, args.base, path, { tickers: tickers.length ? tickers.join(',') : undefined, include_otc: args.include_otc, }) const rows = ((body?.tickers ?? []) as any[]).map(snapRow) if (tickers.length) { const order = new Map(tickers.map((t, i) => [t, i])) rows.sort((a, b) => (order.get(String(a.ticker)) ?? 1e9) - (order.get(String(b.ticker)) ?? 1e9)) } else rows.sort((a, b) => Number(b.volume ?? 0) - Number(a.volume ?? 0)) return rows.slice(0, args.limit) }, }) const gainersLosers = defineSource({ description: "The day's top 20 US stock gainers or losers by percent change, flattened like snapshot.", hosts: HOSTS, input: z.object({ direction: z.enum(['gainers', 'losers']).default('gainers').describe('gainers or losers.'), include_otc: z.boolean().default(false).describe('Include OTC securities.'), base: baseField, }), async run(args, ctx) { const body = await get(ctx, args.base, `/v2/snapshot/locale/us/markets/stocks/${args.direction}`, { include_otc: args.include_otc }) return ((body?.tickers ?? []) as any[]).map(snapRow) }, }) const universalSnapshot = defineSource({ description: 'Snapshots across asset classes in one call (stocks, options O:, indices I:, forex C:, crypto X:): ticker, type, name, price, change, change %, session OHLC, volume, bid, ask, greeks for options.', hosts: HOSTS, input: z.object({ tickers: z.array(z.string()).min(1).max(250).describe('Tickers of any asset class, like AAPL, I:SPX, X:BTCUSD.'), base: baseField, }), async run(args, ctx) { const tickers = args.tickers.map((t) => t.trim()).filter(Boolean) const res = await list(ctx, args.base, '/v3/snapshot', { 'ticker.any_of': tickers.join(','), limit: 250 }, 250) return res.rows.map(universalRow) }, }) const lastTrade = defineSource({ description: 'The most recent trade for a stock or option ticker: price, size, exchange, conditions, time.', hosts: HOSTS, input: z.object({ ticker: z.string().min(1).describe('A stock or option ticker, like AAPL.'), base: baseField }), async run(args, ctx) { const t = upper(args.ticker) const r = (await get(ctx, args.base, `/v2/last/trade/${t}`))?.results ?? {} return [ { ticker: r.T ?? t, time: iso(r.t), price: n(r.p), size: n(r.s), exchange: r.x ?? null, conditions: Array.isArray(r.c) ? r.c.join(', ') : null, tape: r.z ?? null, }, ] }, }) const lastQuote = defineSource({ description: 'The most recent NBBO quote for a stock: bid, bid size, ask, ask size, exchanges, time.', hosts: HOSTS, input: z.object({ ticker: z.string().min(1).describe('A stock ticker, like AAPL.'), base: baseField }), async run(args, ctx) { const t = upper(args.ticker) const r = (await get(ctx, args.base, `/v2/last/nbbo/${t}`))?.results ?? {} return [ { ticker: r.T ?? t, time: iso(r.t), bid: n(r.p), bid_size: n(r.s), bid_exchange: r.x ?? null, ask: n(r.P), ask_size: n(r.S), ask_exchange: r.X ?? null, }, ] }, }) export const snapSources = { snapshot, 'gainers-losers': gainersLosers, 'universal-snapshot': universalSnapshot, 'last-trade': lastTrade, 'last-quote': lastQuote, }