Jaspers Hub

jaspers / yfinance

pluginOfficialReviewed★ 0↓ 11

yfinance: Free Yahoo Finance market data (quotes, history, fundamentals, options, news, movers) via a bundled MCP server, with five views.

5.6 KB
1import type { ReactElement } from 'react'
2import { useBridge, useData, usePublish, type PanelRef } from '@jaspers-ai/sdk'
3import { display as d } from './format'
4import type { MoverList } from './server/quotes'
5import { Bar, Loading, Section, Signed, useEvery, usePanelStateValue, useStateWriter } from './shared'
6import { num, text } from './values'
7import { MARKET_SCREENS, MARKET_TABS, marketsOutput, readMarketList, REFRESH_MS, tone, yahooPage } from './views'
8import './styles.css'
9
10// The market at a glance: Yahoo's summary strip (index futures, commodities, rates, currencies,
11// crypto) as tiles, then one of Yahoo's lists of stocks. Both are asked again every minute. A tab or
12// the screens menu writes `list`.
13
14const LIMIT = 25
15
16export function MarketsView({ panel }: { panel: PanelRef }): ReactElement {
17 const state = usePanelStateValue(panel)
18 const write = useStateWriter(panel)
19 if (state === undefined) return <Loading />
20 const list = readMarketList(state)
21 const screen = MARKET_SCREENS.some((s) => s.list === list)
22 return (
23 <div className="yf-root">
24 <Bar>
25 <nav className="yf-tabs" aria-label="Lists">
26 {MARKET_TABS.map((tab) => (
27 <button
28 key={tab.list}
29 type="button"
30 className={tab.list === list ? 'yf-tab yf-tab-on' : 'yf-tab'}
31 aria-pressed={tab.list === list}
32 onClick={() => write([['list', tab.list]])}
33 >
34 {tab.label}
35 </button>
36 ))}
37 </nav>
38 <select
39 className={screen ? 'yf-select yf-tab-on' : 'yf-select'}
40 aria-label="Screens"
41 value={screen ? list : ''}
42 onChange={(event) => event.target.value && write([['list', event.target.value]])}
43 >
44 <option value="">Screens…</option>
45 {MARKET_SCREENS.map((s) => (
46 <option key={s.list} value={s.list}>
47 {s.label}
48 </option>
49 ))}
50 </select>
51 </Bar>
52 <div className="yf-body">
53 <Markets panel={panel} list={list} />
54 </div>
55 </div>
56 )
57}
58
59function Markets({ panel, list }: { panel: PanelRef; list: MoverList }): ReactElement {
60 const bridge = useBridge()
61 const summary = useData('yfinance/market-summary', {})
62 const movers = useData('yfinance/movers', { list, limit: LIMIT })
63 useEvery(summary.refetch, REFRESH_MS)
64 useEvery(movers.refetch, REFRESH_MS)
65 const tiles = summary.data ?? []
66 const rows = movers.data ?? []
67 const title = text(movers.meta['title'])
68 const total = num(movers.meta['total'])
69 usePublish(panel, marketsOutput(list, tiles, { title, total, rows }))
70 const open = (symbol: string): void => void bridge.openLink(yahooPage(symbol))
71 return (
72 <>
73 <Section title="Markets" run={summary}>
74 <div className="yf-tiles">
75 {tiles.map((t) => {
76 const symbol = text(t['symbol']) ?? ''
77 const hint = num(t['priceHint'])
78 return (
79 <button key={symbol} type="button" className={`yf-tile yf-tile-${tone(num(t['changePercent'])) || 'flat'}`} title={symbol} onClick={() => open(symbol)}>
80 <span className="yf-tile-name">{text(t['name']) ?? symbol}</span>
81 <span className="yf-tile-price yf-num">{d.price(num(t['price']), hint)}</span>
82 <span className="yf-tile-move">
83 <Signed value={num(t['change'])} hint={hint} /> <Signed value={num(t['changePercent'])} percent />
84 </span>
85 </button>
86 )
87 })}
88 </div>
89 </Section>
90 <Section
91 title={title ?? MARKET_TABS.find((t) => t.list === list)?.label ?? list}
92 run={movers}
93 aside={rows.length ? `${rows.length}${total !== null ? ` of ${d.fixed(total, 0)}` : ''}` : undefined}
94 empty={rows.length === 0}
95 >
96 {text(movers.meta['description']) && <p className="yf-line yf-muted">{text(movers.meta['description'])}</p>}
97 <table className="yf-table">
98 <thead>
99 <tr>
100 <th>Symbol</th>
101 <th>Name</th>
102 <th className="yf-num">Price</th>
103 <th className="yf-num">Change</th>
104 <th className="yf-num">%</th>
105 <th className="yf-num">Volume</th>
106 <th className="yf-num">Avg volume</th>
107 <th className="yf-num">Market cap</th>
108 <th className="yf-num">P/E</th>
109 </tr>
110 </thead>
111 <tbody>
112 {rows.map((q) => {
113 const symbol = text(q['symbol']) ?? ''
114 const hint = num(q['priceHint'])
115 return (
116 <tr key={symbol}>
117 <td>
118 <button type="button" className="yf-link yf-strong" onClick={() => open(symbol)}>
119 {symbol}
120 </button>
121 </td>
122 <td className="yf-name-cell">{text(q['name'])}</td>
123 <td className="yf-num">{d.price(num(q['price']), hint)}</td>
124 <td className="yf-num">
125 <Signed value={num(q['change'])} hint={hint} />
126 </td>
127 <td className="yf-num">
128 <Signed value={num(q['changePercent'])} percent />
129 </td>
130 <td className="yf-num">{d.big(num(q['volume']))}</td>
131 <td className="yf-num">{d.big(num(q['averageVolume']))}</td>
132 <td className="yf-num">{d.big(num(q['marketCap']))}</td>
133 <td className="yf-num">{d.fixed(num(q['pe']))}</td>
134 </tr>
135 )
136 })}
137 </tbody>
138 </table>
139 </Section>
140 </>
141 )
142}