Jaspers Hub

biseroff / massive

pluginReviewed★ 0↓ 4

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.4 KB
1import { useData, usePublish, type PanelRef } from '@jaspers-ai/sdk'
2import './styles.css'
3
4interface Opt {
5 contract: string
6 type: 'call' | 'put'
7 strike: number
8 expiration: string
9 bid: number | null
10 ask: number | null
11 last: number | null
12 volume: number | null
13 open_interest: number | null
14 iv: number | null
15 delta: number | null
16 gamma: number | null
17 theta: number | null
18 vega: number | null
19 underlying_price: number | null
20 [field: string]: unknown
21}
22
23interface State {
24 underlying?: string
25 expiration?: string
26 strike_min?: number
27 strike_max?: number
28}
29
30const today = () => new Date().toISOString().slice(0, 10)
31const f = (v: number | null | undefined, d = 2) => (typeof v === 'number' ? v.toFixed(d) : '–')
32const COLS = ['Bid', 'Ask', 'Last', 'Vol', 'OI', 'IV', 'Δ', 'Γ', 'Θ', 'Vega']
33
34function cells(o: Opt | undefined) {
35 return [f(o?.bid), f(o?.ask), f(o?.last), f(o?.volume, 0), f(o?.open_interest, 0), o?.iv != null ? `${(o.iv * 100).toFixed(1)}%` : '–', f(o?.delta, 3), f(o?.gamma, 4), f(o?.theta, 3), f(o?.vega, 3)]
36}
37
38export function ChainView({ panel }: { panel: PanelRef }) {
39 const state = useData(`workspaces/${panel.workspaceId}/panels/${panel.id}/state`) as State | undefined
40 if (state === undefined) return <p className="mv-note">Loading…</p>
41 const underlying = (state.underlying ?? 'SPY').trim().toUpperCase()
42 if (!state.expiration) return <Nearest panel={panel} underlying={underlying} state={state} />
43 return <Chain panel={panel} underlying={underlying} expiration={state.expiration} state={state} />
44}
45
46/** Finds the nearest expiration from the contracts reference, then shows that chain. */
47function Nearest({ panel, underlying, state }: { panel: PanelRef; underlying: string; state: State }) {
48 const { data, error } = useData('massive/option-contracts', { underlying, expiration_from: today(), limit: 1 })
49 const expiration = ((data ?? []) as { expiration?: string }[])[0]?.expiration
50 if (expiration) return <Chain panel={panel} underlying={underlying} expiration={expiration} state={state} />
51 return <Status panel={panel} underlying={underlying} text={error ?? (data ? `No upcoming expirations for ${underlying}.` : 'Finding the nearest expiration…')} isError={Boolean(error)} />
52}
53
54function Status({ panel, underlying, text, isError }: { panel: PanelRef; underlying: string; text: string; isError: boolean }) {
55 usePublish(panel, { underlying, expiration: null, strikes: 0, underlyingPrice: null, error: isError ? text : null })
56 return <p className={`mv-note${isError ? ' mv-error' : ''}`}>{text}</p>
57}
58
59function Chain({ panel, underlying, expiration, state }: { panel: PanelRef; underlying: string; expiration: string; state: State }) {
60 const { data, error, loading, refetch } = useData('massive/option-chain', {
61 underlying,
62 expiration,
63 strike_min: state.strike_min,
64 strike_max: state.strike_max,
65 limit: 1000,
66 })
67 const rows = (data ?? []) as Opt[]
68 const spot = rows.find((r) => typeof r.underlying_price === 'number')?.underlying_price ?? null
69 const ranged = state.strike_min !== undefined || state.strike_max !== undefined
70 const byStrike = new Map<number, { call?: Opt; put?: Opt }>()
71 for (const r of rows) {
72 if (!ranged && spot !== null && Math.abs(r.strike - spot) > spot * 0.1) continue
73 const e = byStrike.get(r.strike) ?? {}
74 e[r.type === 'put' ? 'put' : 'call'] = r
75 byStrike.set(r.strike, e)
76 }
77 const strikes = [...byStrike.keys()].sort((a, b) => a - b)
78 usePublish(panel, { underlying, expiration, strikes: strikes.length, underlyingPrice: spot, error: error ?? null })
79
80 return (
81 <div className="mv-wrap">
82 <div className="mv-bar">
83 <span className="mv-title">
84 {underlying} {expiration} · spot {f(spot)}
85 {!ranged && spot !== null ? ' · strikes within 10%' : ''}
86 </span>
87 <button className="mv-button" onClick={() => refetch()} disabled={loading}>
88 {loading ? 'Loading…' : 'Refresh'}
89 </button>
90 </div>
91 {error ? <p className="mv-note mv-error">{error}</p> : null}
92 {!data && !error ? <p className="mv-note">Loading…</p> : null}
93 {data && !strikes.length ? <p className="mv-note">No contracts for that expiration and strike range.</p> : null}
94 {strikes.length ? (
95 <table className="mv-table">
96 <thead>
97 <tr>
98 <th colSpan={COLS.length} style={{ textAlign: 'center' }}>Calls</th>
99 <th className="mv-strike">Strike</th>
100 <th colSpan={COLS.length} style={{ textAlign: 'center' }}>Puts</th>
101 </tr>
102 <tr>
103 {COLS.map((c) => <th key={`c${c}`} className="mv-num">{c}</th>)}
104 <th className="mv-strike" />
105 {COLS.map((c) => <th key={`p${c}`} className="mv-num">{c}</th>)}
106 </tr>
107 </thead>
108 <tbody>
109 {strikes.map((k) => {
110 const { call, put } = byStrike.get(k)!
111 const callItm = spot !== null && k < spot
112 const putItm = spot !== null && k > spot
113 return (
114 <tr key={k}>
115 {cells(call).map((v, i) => <td key={`c${i}`} className={`mv-num${callItm ? ' mv-itm' : ''}`}>{v}</td>)}
116 <td className="mv-num mv-strike">{k.toFixed(2)}</td>
117 {cells(put).map((v, i) => <td key={`p${i}`} className={`mv-num${putItm ? ' mv-itm' : ''}`}>{v}</td>)}
118 </tr>
119 )
120 })}
121 </tbody>
122 </table>
123 ) : null}
124 </div>
125 )
126}