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.

5.8 KB
1import { defineSource } from '@jaspers-ai/sdk'
2import { z } from 'zod'
3import { HOSTS, baseField, get, iso, list, upper } from './client'
4import { snapRow } from './stocks-snap'
5
6// Indices, forex, and crypto: snapshots, conversion, last quote and last trade. Their bars and
7// previous close are the aggregates and previous-close sources with I:, C:, and X: tickers.
8
9const n = (v: unknown): number | null => (typeof v === 'number' && Number.isFinite(v) ? v : null)
10const prefixed = (t: string, p: string) => {
11 const u = upper(t)
12 return u.includes(':') ? u : `${p}:${u}`
13}
14
15const indexSnapshot = defineSource({
16 description: 'Index snapshots: ticker, name, value, change, change %, session open/high/low/close, previous close, market status, updated.',
17 hosts: HOSTS,
18 input: z.object({
19 tickers: z.array(z.string()).min(1).max(250).default(['I:SPX', 'I:NDX', 'I:DJI', 'I:VIX']).describe('Index tickers, like I:SPX.'),
20 base: baseField,
21 }),
22 async run(args, ctx) {
23 const tickers = args.tickers.map((t) => prefixed(t, 'I'))
24 const res = await list(ctx, args.base, '/v3/snapshot/indices', { 'ticker.any_of': tickers.join(','), limit: 250 }, 250)
25 return res.rows.map((r) => {
26 const s = r.session ?? {}
27 return {
28 ticker: r.ticker ?? null,
29 name: r.name ?? null,
30 value: n(r.value),
31 change: n(s.change),
32 changePercent: n(s.change_percent),
33 open: n(s.open),
34 high: n(s.high),
35 low: n(s.low),
36 close: n(s.close),
37 previousClose: n(s.previous_close),
38 market_status: r.market_status ?? null,
39 updated: iso(r.last_updated),
40 error: r.error ? `${r.error}: ${r.message ?? ''}`.trim() : null,
41 }
42 })
43 },
44})
45
46const fxInput = (example: string) =>
47 z.object({
48 tickers: z.array(z.string()).max(250).default([]).describe(`Tickers like ${example}. Empty for all.`),
49 limit: z.number().int().min(1).max(5000).default(500).describe('Most rows to answer.'),
50 base: baseField,
51 })
52
53async function globalSnapshot(ctx: any, base: string, market: 'forex' | 'crypto', raw: string[], limit: number) {
54 const tickers = raw.map((t) => prefixed(t, market === 'forex' ? 'C' : 'X'))
55 const path = `/v2/snapshot/locale/global/markets/${market}/tickers`
56 if (tickers.length === 1) {
57 const body = await get(ctx, base, `${path}/${tickers[0]}`)
58 return body?.ticker ? [snapRow(body.ticker)] : []
59 }
60 const body = await get(ctx, base, path, { tickers: tickers.length ? tickers.join(',') : undefined })
61 const rows = ((body?.tickers ?? []) as any[]).map(snapRow)
62 if (!tickers.length) rows.sort((a, b) => Number(b.volume ?? 0) - Number(a.volume ?? 0))
63 return rows.slice(0, limit)
64}
65
66const forexSnapshot = defineSource({
67 description: 'Forex pair snapshots: ticker, last (mid of bid/ask), bid, ask, change, change %, day OHLC, previous close, updated.',
68 hosts: HOSTS,
69 input: fxInput('C:EURUSD'),
70 async run(args, ctx) {
71 return globalSnapshot(ctx, args.base, 'forex', args.tickers, args.limit)
72 },
73})
74
75const cryptoSnapshot = defineSource({
76 description: 'Crypto pair snapshots: ticker, last trade price, change, change %, day OHLCV, previous close, updated.',
77 hosts: HOSTS,
78 input: fxInput('X:BTCUSD'),
79 async run(args, ctx) {
80 return globalSnapshot(ctx, args.base, 'crypto', args.tickers, args.limit)
81 },
82})
83
84const pair = {
85 from: z.string().length(3).describe('The base currency, like EUR or BTC.'),
86 to: z.string().length(3).describe('The quote currency, like USD.'),
87}
88
89const forexConversion = defineSource({
90 description: 'Converts an amount between two currencies at the latest rate: from, to, amount, converted, bid, ask, time.',
91 hosts: HOSTS,
92 input: z.object({
93 ...pair,
94 amount: z.number().positive().default(1).describe('The amount of the base currency.'),
95 precision: z.number().int().min(0).max(4).default(4).describe('Decimal places of the answer.'),
96 base: baseField,
97 }),
98 async run(args, ctx) {
99 const b = await get(ctx, args.base, `/v1/conversion/${upper(args.from)}/${upper(args.to)}`, {
100 amount: args.amount,
101 precision: args.precision,
102 })
103 const last = b?.last ?? {}
104 return [
105 {
106 from: b?.from ?? upper(args.from),
107 to: b?.to ?? upper(args.to),
108 amount: n(b?.initialAmount) ?? args.amount,
109 converted: n(b?.converted),
110 bid: n(last.bid),
111 ask: n(last.ask),
112 exchange: last.exchange ?? null,
113 time: iso(last.timestamp),
114 },
115 ]
116 },
117})
118
119const forexLastQuote = defineSource({
120 description: 'The last quote for a currency pair: bid, ask, exchange, time.',
121 hosts: HOSTS,
122 input: z.object({ ...pair, base: baseField }),
123 async run(args, ctx) {
124 const b = await get(ctx, args.base, `/v1/last_quote/currencies/${upper(args.from)}/${upper(args.to)}`)
125 const last = b?.last ?? {}
126 return [{ symbol: b?.symbol ?? `${upper(args.from)}/${upper(args.to)}`, bid: n(last.bid), ask: n(last.ask), exchange: last.exchange ?? null, time: iso(last.timestamp) }]
127 },
128})
129
130const cryptoLastTrade = defineSource({
131 description: 'The last trade for a crypto pair: price, size, exchange, conditions, time.',
132 hosts: HOSTS,
133 input: z.object({ ...pair, base: baseField }),
134 async run(args, ctx) {
135 const b = await get(ctx, args.base, `/v1/last/crypto/${upper(args.from)}/${upper(args.to)}`)
136 const last = b?.last ?? {}
137 return [
138 {
139 symbol: b?.symbol ?? `${upper(args.from)}-${upper(args.to)}`,
140 price: n(last.price),
141 size: n(last.size),
142 exchange: last.exchange ?? null,
143 conditions: Array.isArray(last.conditions) ? last.conditions.join(', ') : null,
144 time: iso(last.timestamp),
145 },
146 ]
147 },
148})
149
150export const globalSources = {
151 'index-snapshot': indexSnapshot,
152 'forex-snapshot': forexSnapshot,
153 'crypto-snapshot': cryptoSnapshot,
154 'forex-conversion': forexConversion,
155 'forex-last-quote': forexLastQuote,
156 'crypto-last-trade': cryptoLastTrade,
157}