fix(B5): derive quick stats from chart bars

This commit is contained in:
Saravana Achu Mac 2026-05-04 17:13:32 -07:00
parent e28c34f773
commit 27b3d1a1dc
2 changed files with 55 additions and 9 deletions

View File

@ -53,9 +53,9 @@ const appContext: AppContextValue = {
handleSignOut: vi.fn(), handleSignOut: vi.fn(),
}; };
function renderTickerHeader() { function renderTickerHeader(contextValue: AppContextValue = appContext) {
return render( return render(
<AppContext.Provider value={appContext}> <AppContext.Provider value={contextValue}>
<MemoryRouter initialEntries={['/']}> <MemoryRouter initialEntries={['/']}>
<Routes> <Routes>
<Route path="/" element={<HomeView />} /> <Route path="/" element={<HomeView />} />
@ -125,4 +125,31 @@ describe('TickerHeader', () => {
expect(screen.getByText(/Jan 3, 2024, 03:30 PM ET · NASDAQ/)).toBeInTheDocument(); 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);
});
}); });

View File

@ -115,6 +115,13 @@ function calculateEma(values: number[], period: number): Array<number | undefine
return ema; return ema;
} }
function lastDefined(values: Array<number | undefined>) {
for (let i = values.length - 1; i >= 0; i -= 1) {
if (values[i] != null) return values[i];
}
return undefined;
}
function calculateMacd(closes: number[]) { function calculateMacd(closes: number[]) {
const fast = calculateEma(closes, 12); const fast = calculateEma(closes, 12);
const slow = calculateEma(closes, 26); const slow = calculateEma(closes, 26);
@ -242,9 +249,11 @@ export function TickerHeader({
function StockChart({ function StockChart({
symbol, symbol,
onLatestBarTimestamp, onLatestBarTimestamp,
onBarsChange,
}: { }: {
symbol: string; symbol: string;
onLatestBarTimestamp?: (timestamp: number | null) => void; onLatestBarTimestamp?: (timestamp: number | null) => void;
onBarsChange?: (bars: OHLCVBar[]) => void;
}) { }) {
const [period, setPeriod] = useState<Period>('1Y'); const [period, setPeriod] = useState<Period>('1Y');
const [bars, setBars] = useState<OHLCVBar[]>([]); const [bars, setBars] = useState<OHLCVBar[]>([]);
@ -262,17 +271,19 @@ function StockChart({
setError(null); setError(null);
setBars([]); setBars([]);
onLatestBarTimestamp?.(null); onLatestBarTimestamp?.(null);
onBarsChange?.([]);
fetchChartBars(symbol, period) fetchChartBars(symbol, period)
.then(data => { .then(data => {
if (!cancelled) { if (!cancelled) {
setBars(data); setBars(data);
onLatestBarTimestamp?.(data.at(-1)?.ts ?? null); onLatestBarTimestamp?.(data.at(-1)?.ts ?? null);
onBarsChange?.(data);
} }
}) })
.catch(err => { if (!cancelled) setError(err?.message ?? 'Failed to load chart'); }) .catch(err => { if (!cancelled) setError(err?.message ?? 'Failed to load chart'); })
.finally(() => { if (!cancelled) setLoading(false); }); .finally(() => { if (!cancelled) setLoading(false); });
return () => { cancelled = true; }; return () => { cancelled = true; };
}, [symbol, period, onLatestBarTimestamp]); }, [symbol, period, onLatestBarTimestamp, onBarsChange]);
const closes = bars.map(b => b.close); const closes = bars.map(b => b.close);
const rsi = calculateRsi(closes); const rsi = calculateRsi(closes);
@ -552,14 +563,21 @@ function StockChart({
} }
// ─── Quick stats cards ──────────────────────────────────────────────────────── // ─── Quick stats cards ────────────────────────────────────────────────────────
function QuickStats({ symbol }: { symbol: string }) { function QuickStats({ symbol, bars }: { symbol: string; bars: OHLCVBar[] }) {
const { botState } = useAppContext(); const { botState } = useAppContext();
const d = botState.symbols?.[symbol]; 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 = [ const stats = [
{ label: 'RSI (1H)', value: d?.indicators?.rsi_1h?.toFixed(1) ?? '—' }, { label: 'RSI (14)', value: rsi != null ? rsi.toFixed(1) : '—' },
{ label: 'EMA 50', value: d?.indicators?.ema50_4h?.toFixed(2) ?? '—' }, { label: 'EMA 50', value: ema50 != null ? ema50.toFixed(2) : '—' },
{ label: 'EMA 200', value: d?.indicators?.ema200_4h?.toFixed(2) ?? '—' }, { label: 'EMA 200', value: ema200 != null ? ema200.toFixed(2) : '—' },
{ label: 'Signal', value: d?.signal ?? '—' }, { label: 'Signal', value: d?.signal ?? '—' },
]; ];
@ -773,6 +791,7 @@ export function HomeView() {
const [profile, setProfile] = useState<ResearchProfile | null>(null); const [profile, setProfile] = useState<ResearchProfile | null>(null);
const [profileLoading, setProfileLoading] = useState(false); const [profileLoading, setProfileLoading] = useState(false);
const [latestBarTimestamp, setLatestBarTimestamp] = useState<number | null>(null); const [latestBarTimestamp, setLatestBarTimestamp] = useState<number | null>(null);
const [chartBars, setChartBars] = useState<OHLCVBar[]>([]);
useEffect(() => { useEffect(() => {
if (!activeSymbol) { if (!activeSymbol) {
@ -802,8 +821,8 @@ export function HomeView() {
return ( return (
<div> <div>
<TickerHeader symbol={activeSymbol} profile={profile} latestBarTimestamp={latestBarTimestamp} /> <TickerHeader symbol={activeSymbol} profile={profile} latestBarTimestamp={latestBarTimestamp} />
<StockChart symbol={activeSymbol} onLatestBarTimestamp={setLatestBarTimestamp} /> <StockChart symbol={activeSymbol} onLatestBarTimestamp={setLatestBarTimestamp} onBarsChange={setChartBars} />
<QuickStats symbol={activeSymbol} /> <QuickStats symbol={activeSymbol} bars={chartBars} />
<ResearchCards symbol={activeSymbol} profile={profile} profileLoading={profileLoading} /> <ResearchCards symbol={activeSymbol} profile={profile} profileLoading={profileLoading} />
</div> </div>
); );