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.

4.3 KB
1import { useEffect, useState, type ReactElement } from 'react'
2import { useBridge, useData, usePublish, type PanelRef } from '@jaspers-ai/sdk'
3import { display as d } from './format'
4import { Bar, Empty, Loading, Section, Signed, SymbolField, useEvery, usePanelStateValue, useStateWriter } from './shared'
5import { num, text } from './values'
6import { addSymbol, readWatchlist, REFRESH_MS, removeSymbol, watchlistOutput, WATCHLIST_MAX, yahooPage } from './views'
7import './styles.css'
8
9// A list of symbols with their quotes, asked again every minute. The field adds a symbol and the
10// cross removes one; both write the whole list to state, which is also how the orchestrator sets it.
11
12export function WatchlistView({ panel }: { panel: PanelRef }): ReactElement {
13 const state = usePanelStateValue(panel)
14 const write = useStateWriter(panel)
15 if (state === undefined) return <Loading />
16 const symbols = readWatchlist(state)
17 const set = (next: string[]): void => write([['symbols', next]])
18 return (
19 <div className="yf-root">
20 <Bar>
21 <SymbolField symbol="" placeholder="Add symbol" clear onSymbol={(symbol) => set(addSymbol(symbols, symbol))} />
22 <span className="yf-muted">
23 Watchlist · {symbols.length} of {WATCHLIST_MAX}
24 </span>
25 </Bar>
26 <div className="yf-body">
27 {symbols.length ? (
28 <Quotes panel={panel} symbols={symbols} onRemove={(symbol) => set(removeSymbol(symbols, symbol))} />
29 ) : (
30 <Empty panel={panel} output={watchlistOutput([], [])}>
31 Add a symbol: AAPL, ^GSPC, BTC-USD, EURUSD=X.
32 </Empty>
33 )}
34 </div>
35 </div>
36 )
37}
38
39function Quotes({ panel, symbols, onRemove }: { panel: PanelRef; symbols: string[]; onRemove: (symbol: string) => void }): ReactElement {
40 const bridge = useBridge()
41 const run = useData('yfinance/quote', { symbols })
42 useEvery(run.refetch, REFRESH_MS)
43 const [updated, setUpdated] = useState<string | null>(null)
44 useEffect(() => {
45 if (run.data) setUpdated(new Date().toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit', second: '2-digit' }))
46 }, [run.data])
47 const quotes = run.data ?? []
48 usePublish(panel, watchlistOutput(symbols, quotes))
49 const bySymbol = new Map(quotes.map((q) => [text(q['symbol']) ?? '', q]))
50 return (
51 <Section title="Quotes" run={run} aside={updated ? `updated ${updated}, every minute` : undefined}>
52 <table className="yf-table yf-quotes">
53 <thead>
54 <tr>
55 <th>Symbol</th>
56 <th>Name</th>
57 <th className="yf-num">Last</th>
58 <th className="yf-num">Change</th>
59 <th className="yf-num">%</th>
60 <th className="yf-num">Volume</th>
61 <th className="yf-num">Market cap</th>
62 <th className="yf-num">Day range</th>
63 <th aria-label="Remove" />
64 </tr>
65 </thead>
66 <tbody>
67 {symbols.map((symbol) => {
68 const q = bySymbol.get(symbol)
69 const hint = num(q?.['priceHint'])
70 const low = num(q?.['dayLow'])
71 const high = num(q?.['dayHigh'])
72 return (
73 <tr key={symbol}>
74 <td>
75 <button type="button" className="yf-link yf-strong" onClick={() => void bridge.openLink(yahooPage(symbol))}>
76 {symbol}
77 </button>
78 </td>
79 <td className="yf-name-cell">{q ? text(q['name']) : <span className="yf-muted">{run.loading ? '…' : 'not found'}</span>}</td>
80 <td className="yf-num">{q ? d.price(num(q['price']), hint) : ''}</td>
81 <td className="yf-num">{q && <Signed value={num(q['change'])} hint={hint} />}</td>
82 <td className="yf-num">{q && <Signed value={num(q['changePercent'])} percent />}</td>
83 <td className="yf-num">{q ? d.big(num(q['volume'])) : ''}</td>
84 <td className="yf-num">{q ? d.big(num(q['marketCap'])) : ''}</td>
85 <td className="yf-num yf-muted">{low !== null && high !== null ? `${d.price(low, hint)}–${d.price(high, hint)}` : ''}</td>
86 <td>
87 <button type="button" className="yf-x" aria-label={`Remove ${symbol}`} title={`Remove ${symbol}`} onClick={() => onRemove(symbol)}>
88 ×
89 </button>
90 </td>
91 </tr>
92 )
93 })}
94 </tbody>
95 </table>
96 </Section>
97 )
98}