fix(B4): show latest bar timestamp in ticker header
This commit is contained in:
parent
bed7f83f3c
commit
311d79c4c4
@ -110,4 +110,19 @@ describe('TickerHeader', () => {
|
||||
await waitFor(() => expect(screen.getAllByText('Apple Inc.').length).toBeGreaterThan(0));
|
||||
expect(fetchResearchProfileMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('shows the latest chart bar timestamp instead of the render time', async () => {
|
||||
fetchResearchProfileMock.mockResolvedValue({ companyName: 'Apple Inc.' });
|
||||
fetchChartBarsMock.mockResolvedValueOnce([
|
||||
{ ts: Date.UTC(2024, 0, 3, 19, 30), open: 210, high: 213, low: 209, close: 212, volume: 1000 },
|
||||
{ ts: Date.UTC(2024, 0, 3, 20, 30), open: 212, high: 214, low: 211, close: 213, volume: 1200 },
|
||||
]);
|
||||
|
||||
renderTickerHeader();
|
||||
|
||||
expect(screen.getByText(/Latest bar pending ET/)).toBeInTheDocument();
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/Jan 3, 2024, 03:30 PM ET · NASDAQ/)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -55,6 +55,19 @@ function formatPriceLabel(ts: number, period: Period) {
|
||||
return d.toLocaleDateString([], { month: 'short', day: 'numeric' });
|
||||
}
|
||||
|
||||
function formatAsOfTimestamp(ts: number | null) {
|
||||
if (ts == null) return 'Latest bar pending';
|
||||
|
||||
return new Date(ts).toLocaleString('en-US', {
|
||||
timeZone: 'America/New_York',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
});
|
||||
}
|
||||
|
||||
function average(values: number[]) {
|
||||
return values.reduce((sum, value) => sum + value, 0) / values.length;
|
||||
}
|
||||
@ -150,7 +163,15 @@ function normalizeResearchProfile(profile: any): ResearchProfile | null {
|
||||
}
|
||||
|
||||
// ─── Ticker header ────────────────────────────────────────────────────────────
|
||||
export function TickerHeader({ symbol, profile }: { symbol: string; profile?: ResearchProfile | null }) {
|
||||
export function TickerHeader({
|
||||
symbol,
|
||||
profile,
|
||||
latestBarTimestamp,
|
||||
}: {
|
||||
symbol: string;
|
||||
profile?: ResearchProfile | null;
|
||||
latestBarTimestamp?: number | null;
|
||||
}) {
|
||||
const navigate = useNavigate();
|
||||
const { botState } = useAppContext();
|
||||
const data = botState.symbols?.[symbol];
|
||||
@ -211,17 +232,20 @@ export function TickerHeader({ symbol, profile }: { symbol: string; profile?: Re
|
||||
</div>
|
||||
|
||||
<div style={{ fontSize: 11, color: '#9CA3AF', marginTop: 3 }}>
|
||||
{new Date().toLocaleString('en-US', {
|
||||
month: 'short', day: 'numeric', year: 'numeric',
|
||||
hour: '2-digit', minute: '2-digit',
|
||||
})} ET · NASDAQ
|
||||
{formatAsOfTimestamp(latestBarTimestamp ?? null)} ET · NASDAQ
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Stock chart ──────────────────────────────────────────────────────────────
|
||||
function StockChart({ symbol }: { symbol: string }) {
|
||||
function StockChart({
|
||||
symbol,
|
||||
onLatestBarTimestamp,
|
||||
}: {
|
||||
symbol: string;
|
||||
onLatestBarTimestamp?: (timestamp: number | null) => void;
|
||||
}) {
|
||||
const [period, setPeriod] = useState<Period>('1Y');
|
||||
const [bars, setBars] = useState<OHLCVBar[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
@ -237,12 +261,18 @@ function StockChart({ symbol }: { symbol: string }) {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
setBars([]);
|
||||
onLatestBarTimestamp?.(null);
|
||||
fetchChartBars(symbol, period)
|
||||
.then(data => { if (!cancelled) setBars(data); })
|
||||
.then(data => {
|
||||
if (!cancelled) {
|
||||
setBars(data);
|
||||
onLatestBarTimestamp?.(data.at(-1)?.ts ?? null);
|
||||
}
|
||||
})
|
||||
.catch(err => { if (!cancelled) setError(err?.message ?? 'Failed to load chart'); })
|
||||
.finally(() => { if (!cancelled) setLoading(false); });
|
||||
return () => { cancelled = true; };
|
||||
}, [symbol, period]);
|
||||
}, [symbol, period, onLatestBarTimestamp]);
|
||||
|
||||
const closes = bars.map(b => b.close);
|
||||
const rsi = calculateRsi(closes);
|
||||
@ -742,6 +772,7 @@ export function HomeView() {
|
||||
const { activeSymbol, setActiveSymbol } = useAppContext();
|
||||
const [profile, setProfile] = useState<ResearchProfile | null>(null);
|
||||
const [profileLoading, setProfileLoading] = useState(false);
|
||||
const [latestBarTimestamp, setLatestBarTimestamp] = useState<number | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!activeSymbol) {
|
||||
@ -770,8 +801,8 @@ export function HomeView() {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<TickerHeader symbol={activeSymbol} profile={profile} />
|
||||
<StockChart symbol={activeSymbol} />
|
||||
<TickerHeader symbol={activeSymbol} profile={profile} latestBarTimestamp={latestBarTimestamp} />
|
||||
<StockChart symbol={activeSymbol} onLatestBarTimestamp={setLatestBarTimestamp} />
|
||||
<QuickStats symbol={activeSymbol} />
|
||||
<ResearchCards symbol={activeSymbol} profile={profile} profileLoading={profileLoading} />
|
||||
</div>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user