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, MassiveError, baseField, daysAgo, get, type Params } from './client' |
| 4 | |
| 5 | // Calls a few endpoints at once and says which the user's plan allows: OK, not authorized, rate-limited. |
| 6 | |
| 7 | interface Check { |
| 8 | endpoint: string |
| 9 | source: string |
| 10 | path: string |
| 11 | params?: Params |
| 12 | } |
| 13 | |
| 14 | const BASIC: Check[] = [ |
| 15 | { endpoint: 'AAPL day bars', source: 'aggregates', path: `/v2/aggs/ticker/AAPL/range/1/day/${daysAgo(10)}/${daysAgo(0)}`, params: { limit: 5 } }, |
| 16 | { endpoint: 'AAPL previous close', source: 'previous-close', path: '/v2/aggs/ticker/AAPL/prev' }, |
| 17 | { endpoint: 'AAPL stock snapshot', source: 'snapshot', path: '/v2/snapshot/locale/us/markets/stocks/tickers/AAPL' }, |
| 18 | { endpoint: 'SPY option chain', source: 'option-chain', path: '/v3/snapshot/options/SPY', params: { limit: 1 } }, |
| 19 | { endpoint: 'AAPL news', source: 'news', path: '/v2/reference/news', params: { ticker: 'AAPL', limit: 1 } }, |
| 20 | { endpoint: 'Market status', source: 'market-status', path: '/v1/marketstatus/now' }, |
| 21 | ] |
| 22 | |
| 23 | const EXTENDED: Check[] = [ |
| 24 | { endpoint: 'AAPL trades', source: 'trades', path: '/v3/trades/AAPL', params: { limit: 1 } }, |
| 25 | { endpoint: 'AAPL quotes', source: 'quotes', path: '/v3/quotes/AAPL', params: { limit: 1 } }, |
| 26 | { endpoint: 'AAPL last trade', source: 'last-trade', path: '/v2/last/trade/AAPL' }, |
| 27 | { endpoint: 'AAPL financials', source: 'financials', path: '/vX/reference/financials', params: { ticker: 'AAPL', limit: 1 } }, |
| 28 | { endpoint: 'AAPL ticker details', source: 'ticker-details', path: '/v3/reference/tickers/AAPL' }, |
| 29 | { endpoint: 'AAPL SMA', source: 'indicator', path: '/v1/indicators/sma/AAPL', params: { timespan: 'day', window: 50, limit: 1 } }, |
| 30 | { endpoint: 'SPY option contracts', source: 'option-contracts', path: '/v3/reference/options/contracts', params: { underlying_ticker: 'SPY', limit: 1 } }, |
| 31 | { endpoint: 'Gainers', source: 'gainers-losers', path: '/v2/snapshot/locale/us/markets/stocks/gainers' }, |
| 32 | { endpoint: 'SPX index snapshot', source: 'index-snapshot', path: '/v3/snapshot/indices', params: { 'ticker.any_of': 'I:SPX' } }, |
| 33 | { endpoint: 'EUR/USD forex snapshot', source: 'forex-snapshot', path: '/v2/snapshot/locale/global/markets/forex/tickers/C:EURUSD' }, |
| 34 | { endpoint: 'BTC/USD crypto snapshot', source: 'crypto-snapshot', path: '/v2/snapshot/locale/global/markets/crypto/tickers/X:BTCUSD' }, |
| 35 | ] |
| 36 | |
| 37 | /** What came back, in a few words: a row count, delayed data, or a per-ticker refusal inside a 200. */ |
| 38 | function readBody(body: any): { status: string; detail: string } { |
| 39 | const results = body?.results |
| 40 | const first = Array.isArray(results) ? results[0] : undefined |
| 41 | if (first?.error) return { status: first.error === 'NOT_AUTHORIZED' ? 'not authorized' : 'error', detail: String(first.message ?? first.error) } |
| 42 | const count = Array.isArray(results) ? results.length : Array.isArray(body?.tickers) ? body.tickers.length : Array.isArray(body) ? body.length : 1 |
| 43 | const delayed = body?.status === 'DELAYED' ? ', delayed data' : '' |
| 44 | return { status: 'OK', detail: `${count} result${count === 1 ? '' : 's'}${delayed}` } |
| 45 | } |
| 46 | |
| 47 | function classify(error: unknown): { status: string; http: number | null; detail: string } { |
| 48 | const message = error instanceof Error ? error.message : String(error) |
| 49 | if (error instanceof MassiveError) { |
| 50 | const s = error.status |
| 51 | const status = s === 403 ? 'not authorized' : s === 429 ? 'rate-limited' : s === 408 ? 'timeout' : s === 401 ? 'bad key' : 'error' |
| 52 | return { status, http: s, detail: message } |
| 53 | } |
| 54 | return { status: 'error', http: null, detail: message } |
| 55 | } |
| 56 | |
| 57 | export const selfCheck = defineSource({ |
| 58 | description: |
| 59 | "Checks which Massive endpoints the user's plan allows, by calling each once with a small request: one row per endpoint with status (OK, not authorized, rate-limited, timeout, error), the HTTP status, and Massive's message. extended adds more endpoints; the free plan allows 5 calls a minute, so later ones may show rate-limited.", |
| 60 | hosts: HOSTS, |
| 61 | input: z.object({ |
| 62 | extended: z.boolean().default(false).describe('Also check trades, quotes, financials, indices, forex, crypto, and gainers.'), |
| 63 | base: baseField, |
| 64 | }), |
| 65 | async run(args, ctx) { |
| 66 | const checks = args.extended ? [...BASIC, ...EXTENDED] : BASIC |
| 67 | const settled = await Promise.allSettled(checks.map((c) => get(ctx, args.base, c.path, c.params ?? {}))) |
| 68 | return settled.map((s, i) => { |
| 69 | const c = checks[i]! |
| 70 | const outcome = s.status === 'fulfilled' ? { ...readBody(s.value), http: 200 } : classify(s.reason) |
| 71 | return { endpoint: c.endpoint, source: `massive/${c.source}`, status: outcome.status, http: outcome.http, detail: outcome.detail, path: c.path } |
| 72 | }) |
| 73 | }, |
| 74 | }) |