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.9 KB
1import type { ReactElement } from 'react'
2import { useData, usePublish, type PanelRef } from '@jaspers-ai/sdk'
3import { display as d } from './format'
4import { lineValue, type Frequency, type LineKind, type Statement } from './server/financials'
5import { Bar, Empty, Loading, Section, SymbolField, usePanelStateValue, useStateWriter } from './shared'
6import { list, record, text } from './values'
7import { financialsOutput, readFinancialsState, type FinancialsState } from './views'
8import './styles.css'
9
10// One company's statement: line items down, periods across, latest first, the lines that matter in
11// bold. The toggles write `statement` and `frequency`; a balance sheet has no trailing twelve months,
12// so choosing one while TTM is on moves to annual.
13
14const STATEMENTS: [Statement, string][] = [
15 ['income', 'Income'],
16 ['balance', 'Balance sheet'],
17 ['cashflow', 'Cash flow'],
18]
19
20const FREQUENCIES: [Frequency, string][] = [
21 ['annual', 'Annual'],
22 ['quarterly', 'Quarterly'],
23 ['trailing', 'TTM'],
24]
25
26export function FinancialsView({ panel }: { panel: PanelRef }): ReactElement {
27 const state = usePanelStateValue(panel)
28 const write = useStateWriter(panel)
29 if (state === undefined) return <Loading />
30 const read = readFinancialsState(state)
31 return (
32 <div className="yf-root">
33 <Bar>
34 <SymbolField symbol={read.symbol} onSymbol={(symbol) => write([['symbol', symbol]])} />
35 <Toggle
36 label="Statement"
37 options={STATEMENTS}
38 value={read.statement}
39 onChange={(statement) => write(statement === 'balance' && read.frequency === 'trailing' ? [['frequency', 'annual'], ['statement', statement]] : [['statement', statement]])}
40 />
41 <Toggle
42 label="Period"
43 options={FREQUENCIES}
44 value={read.frequency}
45 disabled={read.statement === 'balance' ? ['trailing'] : []}
46 onChange={(frequency) => write([['frequency', frequency]])}
47 />
48 </Bar>
49 <div className="yf-body">
50 {read.symbol ? (
51 <Statement panel={panel} state={read} />
52 ) : (
53 <Empty panel={panel} output={financialsOutput(read, { data: [], meta: {}, error: null, loading: false })}>
54 Enter a symbol: AAPL, MSFT, 7203.T.
55 </Empty>
56 )}
57 </div>
58 </div>
59 )
60}
61
62function Toggle<T extends string>({
63 label,
64 options,
65 value,
66 disabled = [],
67 onChange,
68}: {
69 label: string
70 options: [T, string][]
71 value: T
72 disabled?: T[]
73 onChange: (value: T) => void
74}): ReactElement {
75 return (
76 <div className="yf-toggle" role="group" aria-label={label}>
77 {options.map(([key, name]) => (
78 <button
79 key={key}
80 type="button"
81 className={key === value ? 'yf-tab yf-tab-on' : 'yf-tab'}
82 aria-pressed={key === value}
83 disabled={disabled.includes(key)}
84 title={disabled.includes(key) ? 'A balance sheet is a point in time: there is no trailing twelve months.' : undefined}
85 onClick={() => onChange(key)}
86 >
87 {name}
88 </button>
89 ))}
90 </div>
91 )
92}
93
94function Statement({ panel, state }: { panel: PanelRef; state: FinancialsState }): ReactElement {
95 const run = useData('yfinance/financials', { symbol: state.symbol, statement: state.statement, frequency: state.frequency })
96 usePublish(panel, financialsOutput(state, run))
97 const periods = list(run.meta['periods']).map(text).filter((p): p is string => p !== null)
98 const rows = run.data ?? []
99 const currency = text(run.meta['currency'])
100 const name = STATEMENTS.find(([key]) => key === state.statement)![1]
101 const every = FREQUENCIES.find(([key]) => key === state.frequency)![1]
102 return (
103 <Section
104 title={`${state.symbol} ${name.toLowerCase()}`}
105 run={run}
106 aside={[every, currency && `in ${currency}`].filter(Boolean).join(' · ')}
107 empty={rows.length === 0}
108 >
109 <table className="yf-table yf-statement">
110 <thead>
111 <tr>
112 <th>{state.frequency === 'trailing' ? 'Twelve months to' : 'Period ended'}</th>
113 {periods.map((p) => (
114 <th key={p} className="yf-num">
115 {p}
116 </th>
117 ))}
118 </tr>
119 </thead>
120 <tbody>
121 {rows.map((row) => {
122 const values = record(row['values']) ?? {}
123 const kind = (text(row['kind']) ?? 'money') as LineKind
124 return (
125 <tr key={text(row['key']) ?? ''} className={row['highlight'] === true ? 'yf-strong' : undefined}>
126 <td>{text(row['label'])}</td>
127 {periods.map((p) => {
128 const value = typeof values[p] === 'number' ? (values[p] as number) : null
129 return (
130 <td key={p} className={`yf-num${value !== null && value < 0 ? ' yf-negative' : ''}`}>
131 {lineValue(kind, value, d)}
132 </td>
133 )
134 })}
135 </tr>
136 )
137 })}
138 </tbody>
139 </table>
140 </Section>
141 )
142}