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, list, upper } from './client' |
| 4 | |
| 5 | // Ticker news, newest first, with the per-ticker sentiment insights Massive attaches. |
| 6 | |
| 7 | export const news = defineSource({ |
| 8 | description: |
| 9 | 'News articles, newest first: published (ISO), title, publisher, author, url, summary, tickers, keywords, and sentiment insights (ticker: sentiment, reasoning). Pass one or more tickers, or none for all news.', |
| 10 | hosts: HOSTS, |
| 11 | input: z.object({ |
| 12 | tickers: z.array(z.string()).max(20).default([]).describe('Tickers whose news to read, like AAPL. Empty for all news.'), |
| 13 | from: z.string().optional().describe('Earliest publish time, YYYY-MM-DD or ISO date-time (published_utc.gte).'), |
| 14 | to: z.string().optional().describe('Latest publish time, YYYY-MM-DD or ISO date-time (published_utc.lte).'), |
| 15 | limit: z.number().int().min(1).max(1000).default(50).describe('Most articles to answer, across all tickers.'), |
| 16 | base: baseField, |
| 17 | }), |
| 18 | async run(args, ctx) { |
| 19 | const tickers = args.tickers.map(upper).filter(Boolean) |
| 20 | const per = Math.min(args.limit, 1000) |
| 21 | const read = (ticker?: string) => |
| 22 | list( |
| 23 | ctx, |
| 24 | args.base, |
| 25 | '/v2/reference/news', |
| 26 | { ticker, 'published_utc.gte': args.from, 'published_utc.lte': args.to, order: 'desc', sort: 'published_utc', limit: per }, |
| 27 | args.limit, |
| 28 | ) |
| 29 | const results = await Promise.all(tickers.length ? tickers.map((t) => read(t)) : [read()]) |
| 30 | const seen = new Set<string>() |
| 31 | const articles: any[] = [] |
| 32 | for (const r of results) |
| 33 | for (const a of r.rows) { |
| 34 | const key = String(a.id ?? a.article_url) |
| 35 | if (!seen.has(key)) { |
| 36 | seen.add(key) |
| 37 | articles.push(a) |
| 38 | } |
| 39 | } |
| 40 | articles.sort((a, b) => String(b.published_utc).localeCompare(String(a.published_utc))) |
| 41 | const rows = articles.slice(0, args.limit).map((a) => { |
| 42 | const insights = (Array.isArray(a.insights) ? a.insights : []) as any[] |
| 43 | const mine = insights.find((i) => tickers.includes(String(i.ticker))) ?? insights[0] |
| 44 | return { |
| 45 | id: a.id ?? null, |
| 46 | published: a.published_utc ?? null, |
| 47 | title: a.title ?? null, |
| 48 | publisher: a.publisher?.name ?? null, |
| 49 | author: a.author ?? null, |
| 50 | url: a.article_url ?? null, |
| 51 | summary: a.description ?? null, |
| 52 | image_url: a.image_url ?? null, |
| 53 | tickers: Array.isArray(a.tickers) ? a.tickers.join(', ') : null, |
| 54 | keywords: Array.isArray(a.keywords) ? a.keywords.join(', ') : null, |
| 55 | sentiment: mine?.sentiment ?? null, |
| 56 | sentiment_ticker: mine?.ticker ?? null, |
| 57 | sentiment_reasoning: mine?.sentiment_reasoning ?? null, |
| 58 | insights: insights.map((i) => `${i.ticker}: ${i.sentiment}`).join(', ') || null, |
| 59 | } |
| 60 | }) |
| 61 | return { rows, more: results.some((r) => r.more) } |
| 62 | }, |
| 63 | }) |