import { useEffect, useMemo, useState, type ReactElement } from 'react' import { useData, usePanelState, usePublish, type PanelRef } from '@jaspers-ai/sdk' import { fromDate, INDICATORS, indicatorId, indicatorLabel, mergeByDate, num, readSymbol, readTechnicalsState, shortDate, technicalsOutput, TIMEFRAMES, type Indicator, type Row, type Timeframe, } from './fmp' import { cleanError, Loading, NoSymbol, SymbolField, useInitialState } from './shared' import './styles.css' // Technical indicators for one company: one run per indicator in the panel's state, merged into // one table by bar. Each run is a child of its own, since hooks cannot be called in a loop whose // length the state decides; the children report their rows up and the table is made here. interface RunReport { rows: Row[] | undefined loading: boolean error: string | null } export function TechnicalsView({ panel }: { panel: PanelRef }): ReactElement { const initial = useInitialState(panel) if (initial === undefined) return return } function Technicals({ panel, initial }: { panel: PanelRef; initial: unknown }): ReactElement { const first = readTechnicalsState(initial) const [symbol, setSymbol] = usePanelState(panel, 'symbol', first.symbol) const [timeframe, setTimeframe] = usePanelState(panel, 'timeframe', first.timeframe) const [days] = usePanelState(panel, 'days', first.days) const [indicators] = usePanelState(panel, 'indicators', first.indicators) const state = readTechnicalsState({ symbol, timeframe, days, indicators }) return (
{state.indicators.map(indicatorLabel).join(', ') || 'no indicators'}
{state.symbol ? ( ) : ( )}
) } function Body({ panel, symbol, timeframe, days, indicators, }: { panel: PanelRef symbol: string timeframe: Timeframe days: number indicators: Indicator[] }): ReactElement { // The from date is fixed for as long as `days` holds, or a new date every render would re-run everything. const from = useMemo(() => fromDate(new Date(), days), [days]) const [reports, setReports] = useState>({}) const ids = indicators.map(indicatorId) const merged = mergeByDate(indicators.map((indicator) => ({ indicator, rows: reports[indicatorId(indicator)]?.rows }))) const loading = ids.some((id) => reports[id]?.loading ?? true) const errors = ids.flatMap((id) => (reports[id]?.error ? [`${id}: ${cleanError(reports[id]!.error!)}`] : [])) usePublish(panel, technicalsOutput({ symbol, timeframe, days, indicators }, merged)) const intraday = timeframe !== '1day' return ( <> {indicators.map((indicator) => ( setReports((previous) => ({ ...previous, [id]: report }))} /> ))} {errors.map((error) => (

{error}

))} {indicators.length === 0 ? (

No indicators in state.

) : ( {indicators.map((indicator) => ( ))} {merged.map((row) => ( {ids.map((id) => ( ))} ))}
{intraday ? 'Bar' : 'Date'} Close {indicatorLabel(indicator)}
{intraday ? row.date.slice(0, 16) : shortDate(row.date)} {num(row.close)} {num(row.values[id])}
)} {merged.length === 0 && !loading && errors.length === 0 && indicators.length > 0 &&

Nothing returned.

} {loading &&

loading…

} ) } /** One indicator's run. Renders nothing; the parent draws the table once every run has reported. */ function IndicatorRun({ id, args, onReport }: { id: string; args: Record; onReport: (id: string, report: RunReport) => void }): null { const run = useData('fmp/technical-indicators', args) useEffect(() => { onReport(id, { rows: run.data, loading: run.loading, error: run.error }) // The parent's callback is a new function every render; what matters is the run's result. // eslint-disable-next-line react-hooks/exhaustive-deps }, [id, run.data, run.loading, run.error]) return null }