From 27b3d1a1dc05509c6093396a38aed9fbf13b264e Mon Sep 17 00:00:00 2001 From: Saravana Achu Mac Date: Mon, 4 May 2026 17:13:32 -0700 Subject: [PATCH] fix(B5): derive quick stats from chart bars --- web/src/views/HomeView.dom.test.tsx | 31 +++++++++++++++++++++++++-- web/src/views/HomeView.tsx | 33 +++++++++++++++++++++++------ 2 files changed, 55 insertions(+), 9 deletions(-) diff --git a/web/src/views/HomeView.dom.test.tsx b/web/src/views/HomeView.dom.test.tsx index 0f24f3b..d72e12f 100644 --- a/web/src/views/HomeView.dom.test.tsx +++ b/web/src/views/HomeView.dom.test.tsx @@ -53,9 +53,9 @@ const appContext: AppContextValue = { handleSignOut: vi.fn(), }; -function renderTickerHeader() { +function renderTickerHeader(contextValue: AppContextValue = appContext) { return render( - + } /> @@ -125,4 +125,31 @@ describe('TickerHeader', () => { expect(screen.getByText(/Jan 3, 2024, 03:30 PM ET · NASDAQ/)).toBeInTheDocument(); }); }); + + it('derives quick stats from chart bars for symbols missing bot indicators', async () => { + fetchResearchProfileMock.mockResolvedValue({ companyName: 'Microsoft Corporation' }); + fetchChartBarsMock.mockResolvedValueOnce( + Array.from({ length: 220 }, (_, index) => ({ + ts: Date.UTC(2024, 0, 1 + index), + open: 100, + high: 100, + low: 100, + close: 100, + volume: 1000, + })), + ); + const searchedContext: AppContextValue = { + ...appContext, + activeSymbol: 'MSFT', + botState: { + ...appContext.botState, + symbols: {}, + } as any, + }; + + renderTickerHeader(searchedContext); + + await waitFor(() => expect(screen.getByText('50.0')).toBeInTheDocument()); + expect(screen.getAllByText('100.00').length).toBeGreaterThanOrEqual(2); + }); }); diff --git a/web/src/views/HomeView.tsx b/web/src/views/HomeView.tsx index 4616a7d..081bafc 100644 --- a/web/src/views/HomeView.tsx +++ b/web/src/views/HomeView.tsx @@ -115,6 +115,13 @@ function calculateEma(values: number[], period: number): Array) { + for (let i = values.length - 1; i >= 0; i -= 1) { + if (values[i] != null) return values[i]; + } + return undefined; +} + function calculateMacd(closes: number[]) { const fast = calculateEma(closes, 12); const slow = calculateEma(closes, 26); @@ -242,9 +249,11 @@ export function TickerHeader({ function StockChart({ symbol, onLatestBarTimestamp, + onBarsChange, }: { symbol: string; onLatestBarTimestamp?: (timestamp: number | null) => void; + onBarsChange?: (bars: OHLCVBar[]) => void; }) { const [period, setPeriod] = useState('1Y'); const [bars, setBars] = useState([]); @@ -262,17 +271,19 @@ function StockChart({ setError(null); setBars([]); onLatestBarTimestamp?.(null); + onBarsChange?.([]); fetchChartBars(symbol, period) .then(data => { if (!cancelled) { setBars(data); onLatestBarTimestamp?.(data.at(-1)?.ts ?? null); + onBarsChange?.(data); } }) .catch(err => { if (!cancelled) setError(err?.message ?? 'Failed to load chart'); }) .finally(() => { if (!cancelled) setLoading(false); }); return () => { cancelled = true; }; - }, [symbol, period, onLatestBarTimestamp]); + }, [symbol, period, onLatestBarTimestamp, onBarsChange]); const closes = bars.map(b => b.close); const rsi = calculateRsi(closes); @@ -552,14 +563,21 @@ function StockChart({ } // ─── Quick stats cards ──────────────────────────────────────────────────────── -function QuickStats({ symbol }: { symbol: string }) { +function QuickStats({ symbol, bars }: { symbol: string; bars: OHLCVBar[] }) { const { botState } = useAppContext(); const d = botState.symbols?.[symbol]; + const closes = bars.map(bar => bar.close); + const fallbackRsi = lastDefined(calculateRsi(closes)); + const fallbackEma50 = lastDefined(calculateEma(closes, 50)); + const fallbackEma200 = lastDefined(calculateEma(closes, 200)); + const rsi = d?.indicators?.rsi_1h ?? fallbackRsi; + const ema50 = d?.indicators?.ema50_4h ?? fallbackEma50; + const ema200 = d?.indicators?.ema200_4h ?? fallbackEma200; const stats = [ - { label: 'RSI (1H)', value: d?.indicators?.rsi_1h?.toFixed(1) ?? '—' }, - { label: 'EMA 50', value: d?.indicators?.ema50_4h?.toFixed(2) ?? '—' }, - { label: 'EMA 200', value: d?.indicators?.ema200_4h?.toFixed(2) ?? '—' }, + { label: 'RSI (14)', value: rsi != null ? rsi.toFixed(1) : '—' }, + { label: 'EMA 50', value: ema50 != null ? ema50.toFixed(2) : '—' }, + { label: 'EMA 200', value: ema200 != null ? ema200.toFixed(2) : '—' }, { label: 'Signal', value: d?.signal ?? '—' }, ]; @@ -773,6 +791,7 @@ export function HomeView() { const [profile, setProfile] = useState(null); const [profileLoading, setProfileLoading] = useState(false); const [latestBarTimestamp, setLatestBarTimestamp] = useState(null); + const [chartBars, setChartBars] = useState([]); useEffect(() => { if (!activeSymbol) { @@ -802,8 +821,8 @@ export function HomeView() { return (
- - + +
);