Massive Market Data (Polygon.io): Market data plugin wrapping the Polygon.io/Massive REST API: bars, snapshots, options, news, financials, indicators, and four chart/watchlist views.
| 1 | import { defineSource } from '@jaspers-ai/sdk' |
| 2 | import { z } from 'zod' |
| 3 | import { HOSTS, baseField, get, iso, list, upper, type Row } from './client' |
| 4 | |
| 5 | // Snapshots: stock tickers, gainers and losers, the universal snapshot, last trade and last quote. |
| 6 | |
| 7 | const n = (v: unknown): number | null => (typeof v === 'number' && Number.isFinite(v) ? v : null) |
| 8 | /** The first number that is there and not zero (Massive zeroes the day before the open). */ |
| 9 | const firstNonZero = (...vs: unknown[]): number | null => { |
| 10 | for (const v of vs) if (typeof v === 'number' && v !== 0 && Number.isFinite(v)) return v |
| 11 | return null |
| 12 | } |
| 13 | |
| 14 | /** One v2 snapshot (stocks, forex, crypto) as a flat row. */ |
| 15 | export function snapRow(s: any): Row { |
| 16 | const day = s?.day ?? {} |
| 17 | const prev = s?.prevDay ?? {} |
| 18 | const lt = s?.lastTrade ?? {} |
| 19 | const lq = s?.lastQuote ?? {} |
| 20 | const min = s?.min ?? {} |
| 21 | const bid = n(lq.p ?? lq.b) |
| 22 | const ask = n(lq.P ?? lq.a) |
| 23 | const mid = bid !== null && ask !== null && bid > 0 && ask > 0 ? (bid + ask) / 2 : null |
| 24 | return { |
| 25 | ticker: s?.ticker ?? null, |
| 26 | last: firstNonZero(lt.p, min.c, mid, day.c, prev.c), |
| 27 | change: n(s?.todaysChange), |
| 28 | changePercent: n(s?.todaysChangePerc), |
| 29 | open: n(day.o), |
| 30 | high: n(day.h), |
| 31 | low: n(day.l), |
| 32 | close: n(day.c), |
| 33 | volume: n(day.v), |
| 34 | vwap: n(day.vw), |
| 35 | prevClose: n(prev.c), |
| 36 | bid, |
| 37 | ask, |
| 38 | updated: iso(s?.updated), |
| 39 | } |
| 40 | } |
| 41 | |
| 42 | function universalRow(r: any): Row { |
| 43 | const s = r.session ?? {} |
| 44 | const lq = r.last_quote ?? {} |
| 45 | const g = r.greeks ?? {} |
| 46 | return { |
| 47 | ticker: r.ticker ?? null, |
| 48 | type: r.type ?? null, |
| 49 | name: r.name ?? null, |
| 50 | market_status: r.market_status ?? null, |
| 51 | price: firstNonZero(r.value, r.last_trade?.price, s.price, s.close, s.previous_close), |
| 52 | change: n(s.change), |
| 53 | changePercent: n(s.change_percent), |
| 54 | open: n(s.open), |
| 55 | high: n(s.high), |
| 56 | low: n(s.low), |
| 57 | close: n(s.close), |
| 58 | previousClose: n(s.previous_close), |
| 59 | volume: n(s.volume), |
| 60 | bid: n(lq.bid), |
| 61 | ask: n(lq.ask), |
| 62 | implied_volatility: n(r.implied_volatility), |
| 63 | open_interest: n(r.open_interest), |
| 64 | delta: n(g.delta), |
| 65 | gamma: n(g.gamma), |
| 66 | theta: n(g.theta), |
| 67 | vega: n(g.vega), |
| 68 | underlying: r.underlying_asset?.ticker ?? null, |
| 69 | updated: iso(r.last_updated ?? s.last_updated), |
| 70 | error: r.error ? `${r.error}: ${r.message ?? ''}`.trim() : null, |
| 71 | } |
| 72 | } |
| 73 | |
| 74 | const snapshot = defineSource({ |
| 75 | description: |
| 76 | '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).', |
| 77 | hosts: HOSTS, |
| 78 | input: z.object({ |
| 79 | tickers: z.array(z.string()).max(250).default([]).describe('Stock tickers like AAPL. Empty for all.'), |
| 80 | include_otc: z.boolean().default(false).describe('Include OTC securities when listing all.'), |
| 81 | limit: z.number().int().min(1).max(15000).default(500).describe('Most rows to answer.'), |
| 82 | base: baseField, |
| 83 | }), |
| 84 | async run(args, ctx) { |
| 85 | const tickers = args.tickers.map(upper).filter(Boolean) |
| 86 | const path = '/v2/snapshot/locale/us/markets/stocks/tickers' |
| 87 | if (tickers.length === 1) { |
| 88 | const body = await get(ctx, args.base, `${path}/${tickers[0]}`) |
| 89 | return body?.ticker ? [snapRow(body.ticker)] : [] |
| 90 | } |
| 91 | const body = await get(ctx, args.base, path, { |
| 92 | tickers: tickers.length ? tickers.join(',') : undefined, |
| 93 | include_otc: args.include_otc, |
| 94 | }) |
| 95 | const rows = ((body?.tickers ?? []) as any[]).map(snapRow) |
| 96 | if (tickers.length) { |
| 97 | const order = new Map(tickers.map((t, i) => [t, i])) |
| 98 | rows.sort((a, b) => (order.get(String(a.ticker)) ?? 1e9) - (order.get(String(b.ticker)) ?? 1e9)) |
| 99 | } else rows.sort((a, b) => Number(b.volume ?? 0) - Number(a.volume ?? 0)) |
| 100 | return rows.slice(0, args.limit) |
| 101 | }, |
| 102 | }) |
| 103 | |
| 104 | const gainersLosers = defineSource({ |
| 105 | description: "The day's top 20 US stock gainers or losers by percent change, flattened like snapshot.", |
| 106 | hosts: HOSTS, |
| 107 | input: z.object({ |
| 108 | direction: z.enum(['gainers', 'losers']).default('gainers').describe('gainers or losers.'), |
| 109 | include_otc: z.boolean().default(false).describe('Include OTC securities.'), |
| 110 | base: baseField, |
| 111 | }), |
| 112 | async run(args, ctx) { |
| 113 | const body = await get(ctx, args.base, `/v2/snapshot/locale/us/markets/stocks/${args.direction}`, { include_otc: args.include_otc }) |
| 114 | return ((body?.tickers ?? []) as any[]).map(snapRow) |
| 115 | }, |
| 116 | }) |
| 117 | |
| 118 | const universalSnapshot = defineSource({ |
| 119 | description: |
| 120 | '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.', |
| 121 | hosts: HOSTS, |
| 122 | input: z.object({ |
| 123 | tickers: z.array(z.string()).min(1).max(250).describe('Tickers of any asset class, like AAPL, I:SPX, X:BTCUSD.'), |
| 124 | base: baseField, |
| 125 | }), |
| 126 | async run(args, ctx) { |
| 127 | const tickers = args.tickers.map((t) => t.trim()).filter(Boolean) |
| 128 | const res = await list(ctx, args.base, '/v3/snapshot', { 'ticker.any_of': tickers.join(','), limit: 250 }, 250) |
| 129 | return res.rows.map(universalRow) |
| 130 | }, |
| 131 | }) |
| 132 | |
| 133 | const lastTrade = defineSource({ |
| 134 | description: 'The most recent trade for a stock or option ticker: price, size, exchange, conditions, time.', |
| 135 | hosts: HOSTS, |
| 136 | input: z.object({ ticker: z.string().min(1).describe('A stock or option ticker, like AAPL.'), base: baseField }), |
| 137 | async run(args, ctx) { |
| 138 | const t = upper(args.ticker) |
| 139 | const r = (await get(ctx, args.base, `/v2/last/trade/${t}`))?.results ?? {} |
| 140 | return [ |
| 141 | { |
| 142 | ticker: r.T ?? t, |
| 143 | time: iso(r.t), |
| 144 | price: n(r.p), |
| 145 | size: n(r.s), |
| 146 | exchange: r.x ?? null, |
| 147 | conditions: Array.isArray(r.c) ? r.c.join(', ') : null, |
| 148 | tape: r.z ?? null, |
| 149 | }, |
| 150 | ] |
| 151 | }, |
| 152 | }) |
| 153 | |
| 154 | const lastQuote = defineSource({ |
| 155 | description: 'The most recent NBBO quote for a stock: bid, bid size, ask, ask size, exchanges, time.', |
| 156 | hosts: HOSTS, |
| 157 | input: z.object({ ticker: z.string().min(1).describe('A stock ticker, like AAPL.'), base: baseField }), |
| 158 | async run(args, ctx) { |
| 159 | const t = upper(args.ticker) |
| 160 | const r = (await get(ctx, args.base, `/v2/last/nbbo/${t}`))?.results ?? {} |
| 161 | return [ |
| 162 | { |
| 163 | ticker: r.T ?? t, |
| 164 | time: iso(r.t), |
| 165 | bid: n(r.p), |
| 166 | bid_size: n(r.s), |
| 167 | bid_exchange: r.x ?? null, |
| 168 | ask: n(r.P), |
| 169 | ask_size: n(r.S), |
| 170 | ask_exchange: r.X ?? null, |
| 171 | }, |
| 172 | ] |
| 173 | }, |
| 174 | }) |
| 175 | |
| 176 | export const snapSources = { |
| 177 | snapshot, |
| 178 | 'gainers-losers': gainersLosers, |
| 179 | 'universal-snapshot': universalSnapshot, |
| 180 | 'last-trade': lastTrade, |
| 181 | 'last-quote': lastQuote, |
| 182 | } |