Jaspers Hub

biseroff / massive

pluginReviewed★ 0↓ 3

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.

4.6 KB
1import { defineSource } from '@jaspers-ai/sdk'
2import { z } from 'zod'
3import { 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
7interface Check {
8 endpoint: string
9 source: string
10 path: string
11 params?: Params
12}
13
14const 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
23const 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. */
38function 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
47function 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
57export 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})