fix(B5): derive quick stats from chart bars
This commit is contained in:
parent
e28c34f773
commit
27b3d1a1dc
@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@ -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>
|
||||
);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user