import { defineSource } from '@jaspers-ai/sdk' import { z } from 'zod' import { HOSTS, baseField, get, iso, list, upper, type Row } from './client' // Options: contracts reference, the chain snapshot for an underlying, one contract's snapshot. // Option bars, trades, and quotes are the aggregates, trades, and quotes sources with O: tickers. const contractType = z.enum(['call', 'put']).optional().describe('call or put. Leave out for both.') const n = (v: unknown): number | null => (typeof v === 'number' && Number.isFinite(v) ? v : null) /** One option snapshot as a flat row. */ export function optionRow(r: any): Row { const d = r?.details ?? {} const q = r?.last_quote ?? {} const t = r?.last_trade ?? {} const g = r?.greeks ?? {} const day = r?.day ?? {} const u = r?.underlying_asset ?? {} return { contract: d.ticker ?? r?.ticker ?? null, type: d.contract_type ?? null, strike: n(d.strike_price), expiration: d.expiration_date ?? null, bid: n(q.bid), ask: n(q.ask), mid: n(q.midpoint), last: n(t.price) ?? n(day.close), volume: n(day.volume), open_interest: n(r?.open_interest), iv: n(r?.implied_volatility), delta: n(g.delta), gamma: n(g.gamma), theta: n(g.theta), vega: n(g.vega), break_even: n(r?.break_even_price), change: n(day.change), change_percent: n(day.change_percent), underlying: u.ticker ?? null, underlying_price: n(u.price), updated: iso(q.last_updated ?? t.sip_timestamp ?? day.last_updated), } } const optionContracts = defineSource({ description: 'Option contracts for an underlying: ticker, type, strike, expiration, exercise style, shares per contract. Nearest expiration first.', hosts: HOSTS, input: z.object({ underlying: z.string().min(1).describe('The underlying ticker, like SPY.'), expiration_from: z.string().optional().describe('Earliest expiration, YYYY-MM-DD.'), expiration_to: z.string().optional().describe('Latest expiration, YYYY-MM-DD.'), contract_type: contractType, strike_min: z.number().optional().describe('Lowest strike.'), strike_max: z.number().optional().describe('Highest strike.'), as_of: z.string().optional().describe('Contracts as they were on this date, YYYY-MM-DD. Leave out for today.'), expired: z.boolean().default(false).describe('Include expired contracts.'), limit: z.number().int().min(1).max(10000).default(1000).describe('Most contracts to answer.'), base: baseField, }), async run(args, ctx) { const res = await list( ctx, args.base, '/v3/reference/options/contracts', { underlying_ticker: upper(args.underlying), 'expiration_date.gte': args.expiration_from, 'expiration_date.lte': args.expiration_to, contract_type: args.contract_type, 'strike_price.gte': args.strike_min, 'strike_price.lte': args.strike_max, as_of: args.as_of, expired: args.expired, sort: 'expiration_date', order: 'asc', limit: Math.min(args.limit, 1000), }, args.limit, ) const rows = res.rows.map((r) => ({ contract: r.ticker, underlying: r.underlying_ticker ?? null, type: r.contract_type ?? null, strike: r.strike_price ?? null, expiration: r.expiration_date ?? null, exercise_style: r.exercise_style ?? null, shares_per_contract: r.shares_per_contract ?? null, primary_exchange: r.primary_exchange ?? null, })) return { rows, more: res.more } }, }) const optionChain = defineSource({ description: 'The option chain snapshot for an underlying: contract, type, strike, expiration, bid, ask, mid, last, volume, open interest, IV, delta, gamma, theta, vega, break-even, underlying price. Sorted by expiration then strike.', hosts: HOSTS, input: z.object({ underlying: z.string().min(1).describe('The underlying ticker, like SPY.'), expiration: z.string().optional().describe('One expiration, YYYY-MM-DD.'), expiration_from: z.string().optional().describe('Earliest expiration, YYYY-MM-DD.'), expiration_to: z.string().optional().describe('Latest expiration, YYYY-MM-DD.'), contract_type: contractType, strike_min: z.number().optional().describe('Lowest strike.'), strike_max: z.number().optional().describe('Highest strike.'), limit: z.number().int().min(1).max(5000).default(500).describe('Most contracts to answer.'), base: baseField, }), async run(args, ctx) { const res = await list( ctx, args.base, `/v3/snapshot/options/${upper(args.underlying)}`, { expiration_date: args.expiration, 'expiration_date.gte': args.expiration_from, 'expiration_date.lte': args.expiration_to, contract_type: args.contract_type, 'strike_price.gte': args.strike_min, 'strike_price.lte': args.strike_max, sort: 'expiration_date', order: 'asc', limit: Math.min(args.limit, 250), }, args.limit, 20, ) const rows = res.rows.map(optionRow) rows.sort( (a, b) => String(a.expiration).localeCompare(String(b.expiration)) || Number(a.strike) - Number(b.strike) || String(a.type).localeCompare(String(b.type)), ) return { rows, more: res.more, pages: res.pages } }, }) const optionContractSnapshot = defineSource({ description: 'The snapshot of one option contract, with the same fields as option-chain. One row.', hosts: HOSTS, input: z.object({ underlying: z.string().min(1).describe('The underlying ticker, like SPY.'), contract: z.string().min(1).describe('The option ticker, like O:SPY251219C00600000.'), base: baseField, }), async run(args, ctx) { const c = upper(args.contract) const contract = c.startsWith('O:') ? c : `O:${c}` const body = await get(ctx, args.base, `/v3/snapshot/options/${upper(args.underlying)}/${contract}`) return body?.results ? [optionRow(body.results)] : [] }, }) export const optionSources = { 'option-contracts': optionContracts, 'option-chain': optionChain, 'option-contract-snapshot': optionContractSnapshot, }