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(),
};
function renderTickerHeader() {
function renderTickerHeader(contextValue: AppContextValue = appContext) {
return render(
<AppContext.Provider value={appContext}>
<AppContext.Provider value={contextValue}>
<MemoryRouter initialEntries={['/']}>
<Routes>
<Route path="/" element={<HomeView />} />
@ -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);
});
});

View File

@ -115,6 +115,13 @@ function calculateEma(values: number[], period: number): Array<number | undefine
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[]) {
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<Period>('1Y');
const [bars, setBars] = useState<OHLCVBar[]>([]);
@ -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<ResearchProfile | null>(null);
const [profileLoading, setProfileLoading] = useState(false);
const [latestBarTimestamp, setLatestBarTimestamp] = useState<number | null>(null);
const [chartBars, setChartBars] = useState<OHLCVBar[]>([]);
useEffect(() => {
if (!activeSymbol) {
@ -802,8 +821,8 @@ export function HomeView() {
return (
<div>
<TickerHeader symbol={activeSymbol} profile={profile} latestBarTimestamp={latestBarTimestamp} />
<StockChart symbol={activeSymbol} onLatestBarTimestamp={setLatestBarTimestamp} />
<QuickStats symbol={activeSymbol} />
<StockChart symbol={activeSymbol} onLatestBarTimestamp={setLatestBarTimestamp} onBarsChange={setChartBars} />
<QuickStats symbol={activeSymbol} bars={chartBars} />
<ResearchCards symbol={activeSymbol} profile={profile} profileLoading={profileLoading} />
</div>
);