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.

2.9 KB
1import { useData, usePublish, type PanelRef } from '@jaspers-ai/sdk'
2import './styles.css'
3
4interface Snap {
5 ticker: string
6 last: number | null
7 change: number | null
8 changePercent: number | null
9 volume: number | null
10 updated: string | null
11 [field: string]: unknown
12}
13
14const fmt = (v: number | null | undefined, d = 2) => (typeof v === 'number' ? v.toFixed(d) : '–')
15const vol = (v: number | null | undefined) =>
16 typeof v !== 'number' ? '–' : v >= 1e9 ? `${(v / 1e9).toFixed(2)}B` : v >= 1e6 ? `${(v / 1e6).toFixed(2)}M` : v >= 1e3 ? `${(v / 1e3).toFixed(1)}K` : String(v)
17const sign = (v: number | null | undefined) => (typeof v !== 'number' || v === 0 ? '' : v > 0 ? 'mv-up' : 'mv-down')
18
19export function WatchlistView({ panel }: { panel: PanelRef }) {
20 const state = useData(`workspaces/${panel.workspaceId}/panels/${panel.id}/state`) as { tickers?: string[] } | undefined
21 if (state === undefined) return <p className="mv-note">Loading…</p>
22 const tickers = (state.tickers ?? []).map((t) => t.trim().toUpperCase()).filter(Boolean)
23 if (!tickers.length) return <Empty panel={panel} />
24 return <Table panel={panel} tickers={tickers} />
25}
26
27function Empty({ panel }: { panel: PanelRef }) {
28 usePublish(panel, { tickers: [], count: 0, error: null })
29 return <p className="mv-note">No tickers yet. Ask for some.</p>
30}
31
32function Table({ panel, tickers }: { panel: PanelRef; tickers: string[] }) {
33 const { data, error, loading, refetch } = useData('massive/snapshot', { tickers })
34 const rows = (data ?? []) as Snap[]
35 usePublish(panel, { tickers: rows.map((r) => r.ticker), count: rows.length, error: error ?? null })
36
37 return (
38 <div className="mv-wrap">
39 <div className="mv-bar">
40 <span className="mv-title">Watchlist</span>
41 <button className="mv-button" onClick={() => refetch()} disabled={loading}>
42 {loading ? 'Refreshing…' : 'Refresh'}
43 </button>
44 </div>
45 {error ? <p className="mv-note mv-error">{error}</p> : null}
46 {!data && !error ? <p className="mv-note">Loading…</p> : null}
47 {data ? (
48 <table className="mv-table">
49 <thead>
50 <tr>
51 <th>Ticker</th>
52 <th className="mv-num">Last</th>
53 <th className="mv-num">Change</th>
54 <th className="mv-num">Change %</th>
55 <th className="mv-num">Volume</th>
56 </tr>
57 </thead>
58 <tbody>
59 {rows.map((r) => (
60 <tr key={r.ticker}>
61 <td>{r.ticker}</td>
62 <td className="mv-num">{fmt(r.last)}</td>
63 <td className={`mv-num ${sign(r.change)}`}>{fmt(r.change)}</td>
64 <td className={`mv-num ${sign(r.changePercent)}`}>{typeof r.changePercent === 'number' ? `${fmt(r.changePercent)}%` : '–'}</td>
65 <td className="mv-num">{vol(r.volume)}</td>
66 </tr>
67 ))}
68 </tbody>
69 </table>
70 ) : null}
71 </div>
72 )
73}