import { defineSource } from '@jaspers-ai/sdk' import { z } from 'zod' import { DATA_HOST, alpaca, paged } from './client' import { mid, num, parseOcc, str } from './rows' import { barRow, history, list, monthAgo } from './stocks' const contracts = z .array(z.string().min(1)) .min(1) .max(100) .describe('OCC option symbols, like ["AAPL250117C00190000"].') const feed = z .enum(['indicative', 'opra']) .default('indicative') .describe('Options feed: indicative (free, delayed/derived) or opra (real-time, needs a subscription).') const start = z.string().optional().describe('Start, RFC 3339 or YYYY-MM-DD.') const end = z.string().optional().describe('End, RFC 3339 or YYYY-MM-DD. Leave out for now.') type Fields = Record interface OptionSnapshot { latestTrade?: Fields latestQuote?: Fields impliedVolatility?: number greeks?: Fields dailyBar?: Fields prevDailyBar?: Fields } /** One contract's snapshot as a flat row, with the OCC symbol parsed. */ function snapshotRow(symbol: string, s: OptionSnapshot | null) { const bid = num(s?.latestQuote?.bp) const ask = num(s?.latestQuote?.ap) return { symbol, ...parseOcc(symbol), bid, bid_size: num(s?.latestQuote?.bs), ask, ask_size: num(s?.latestQuote?.as), mid: mid(bid, ask), quote_time: str(s?.latestQuote?.t), last: num(s?.latestTrade?.p), last_time: str(s?.latestTrade?.t), iv: num(s?.impliedVolatility), delta: num(s?.greeks?.delta), gamma: num(s?.greeks?.gamma), theta: num(s?.greeks?.theta), vega: num(s?.greeks?.vega), rho: num(s?.greeks?.rho), day_volume: num(s?.dailyBar?.v), day_close: num(s?.dailyBar?.c), prev_close: num(s?.prevDailyBar?.c), } } export const optionBars = defineSource({ description: 'Historical OHLCV bars for option contracts from Alpaca. Rows: symbol, t, o, h, l, c, v, n, vw.', hosts: [DATA_HOST], input: z.object({ symbols: contracts, timeframe: z.string().default('1Day').describe('Bar size: 1Min, 5Min, 15Min, 1Hour, 1Day, 1Week, 1Month.'), start: start.describe('Start, RFC 3339 or YYYY-MM-DD. Defaults to 30 days ago.'), end, limit: z.number().int().min(1).max(10000).default(1000).describe('Most bars in all.'), }), async run(args, ctx) { const query = { symbols: list(args.symbols), timeframe: args.timeframe, start: args.start ?? monthAgo(), end: args.end } return history(ctx, '/v1beta1/options/bars', 'bars', query, args.limit, barRow) }, }) export const optionTrades = defineSource({ description: 'Historical trades for option contracts from Alpaca. Rows: symbol, t, p (price), s (size), x (exchange), c (condition).', hosts: [DATA_HOST], input: z.object({ symbols: contracts, start, end, limit: z.number().int().min(1).max(10000).default(1000).describe('Most trades in all.'), }), async run(args, ctx) { const query = { symbols: list(args.symbols), start: args.start, end: args.end } return history(ctx, '/v1beta1/options/trades', 'trades', query, args.limit, (t) => ({ t: str(t.t), p: num(t.p), s: num(t.s), x: str(t.x), c: str(t.c), })) }, }) export const optionLatestQuote = defineSource({ description: 'Latest quote for option contracts from Alpaca. One row per contract: symbol, underlying, expiration, type, strike, bid, bid_size, ask, ask_size, mid, time.', hosts: [DATA_HOST], input: z.object({ symbols: contracts, feed }), async run(args, ctx) { const answer = (await alpaca(ctx, { host: DATA_HOST, path: '/v1beta1/options/quotes/latest', query: { symbols: list(args.symbols), feed: args.feed }, })) as { quotes?: Record } | null return Object.entries(answer?.quotes ?? {}).map(([symbol, q]) => { const bid = num(q.bp) const ask = num(q.ap) return { symbol, ...parseOcc(symbol), bid, bid_size: num(q.bs), ask, ask_size: num(q.as), mid: mid(bid, ask), time: str(q.t), } }) }, }) export const optionLatestTrade = defineSource({ description: 'Latest trade for option contracts from Alpaca. One row per contract: symbol, underlying, expiration, type, strike, price, size, exchange, time.', hosts: [DATA_HOST], input: z.object({ symbols: contracts, feed }), async run(args, ctx) { const answer = (await alpaca(ctx, { host: DATA_HOST, path: '/v1beta1/options/trades/latest', query: { symbols: list(args.symbols), feed: args.feed }, })) as { trades?: Record } | null return Object.entries(answer?.trades ?? {}).map(([symbol, t]) => ({ symbol, ...parseOcc(symbol), price: num(t.p), size: num(t.s), exchange: str(t.x), time: str(t.t), })) }, }) export const optionSnapshots = defineSource({ description: 'Snapshots of option contracts from Alpaca, one row per contract: symbol, underlying, expiration, type, strike, bid, ask, mid, last, iv, delta, gamma, theta, vega, rho.', hosts: [DATA_HOST], input: z.object({ symbols: contracts, feed }), async run(args, ctx) { const { data } = await paged(ctx, '/v1beta1/options/snapshots', { symbols: list(args.symbols), feed: args.feed }, 'snapshots', 1000, 1000) return Object.entries(data).map(([symbol, s]) => snapshotRow(symbol, s as OptionSnapshot | null)) }, }) export const optionChain = defineSource({ description: 'The option chain of one underlying from Alpaca: one row per contract with symbol, underlying, expiration, type, strike, bid, ask, mid, last, iv, delta, gamma, theta, vega, rho. Sorted by expiration, strike, type. Narrow it with type, strikes, and expirations.', hosts: [DATA_HOST], input: z.object({ underlying: z.string().min(1).describe('The underlying stock symbol, like AAPL.'), feed, type: z.enum(['call', 'put']).optional().describe('Only calls or only puts.'), strike_price_gte: z.number().optional().describe('Lowest strike.'), strike_price_lte: z.number().optional().describe('Highest strike.'), expiration_date: z.string().optional().describe('Exactly this expiration, YYYY-MM-DD.'), expiration_date_gte: z.string().optional().describe('Expirations on or after, YYYY-MM-DD.'), expiration_date_lte: z.string().optional().describe('Expirations on or before, YYYY-MM-DD.'), limit: z.number().int().min(1).max(5000).default(500).describe('Most contracts.'), }), async run(args, ctx) { const { underlying, limit, ...filters } = args const path = `/v1beta1/options/snapshots/${encodeURIComponent(underlying.trim().toUpperCase())}` const { data, truncated } = await paged(ctx, path, filters, 'snapshots', limit, 1000) const rows = Object.entries(data).map(([symbol, s]) => snapshotRow(symbol, s as OptionSnapshot | null)) rows.sort( (a, b) => (a.expiration ?? '').localeCompare(b.expiration ?? '') || (a.strike ?? 0) - (b.strike ?? 0) || (a.type ?? '').localeCompare(b.type ?? ''), ) return { rows: rows.slice(0, limit), truncated } }, })