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));
|
await waitFor(() => expect(screen.getAllByText('Apple Inc.').length).toBeGreaterThan(0));
|
||||||
expect(fetchResearchProfileMock).toHaveBeenCalledTimes(1);
|
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' });
|
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[]) {
|
function average(values: number[]) {
|
||||||
return values.reduce((sum, value) => sum + value, 0) / values.length;
|
return values.reduce((sum, value) => sum + value, 0) / values.length;
|
||||||
}
|
}
|
||||||
@ -150,7 +163,15 @@ function normalizeResearchProfile(profile: any): ResearchProfile | null {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ─── Ticker header ────────────────────────────────────────────────────────────
|
// ─── 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 navigate = useNavigate();
|
||||||
const { botState } = useAppContext();
|
const { botState } = useAppContext();
|
||||||
const data = botState.symbols?.[symbol];
|
const data = botState.symbols?.[symbol];
|
||||||
@ -211,17 +232,20 @@ export function TickerHeader({ symbol, profile }: { symbol: string; profile?: Re
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ fontSize: 11, color: '#9CA3AF', marginTop: 3 }}>
|
<div style={{ fontSize: 11, color: '#9CA3AF', marginTop: 3 }}>
|
||||||
{new Date().toLocaleString('en-US', {
|
{formatAsOfTimestamp(latestBarTimestamp ?? null)} ET · NASDAQ
|
||||||
month: 'short', day: 'numeric', year: 'numeric',
|
|
||||||
hour: '2-digit', minute: '2-digit',
|
|
||||||
})} ET · NASDAQ
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Stock chart ──────────────────────────────────────────────────────────────
|
// ─── 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 [period, setPeriod] = useState<Period>('1Y');
|
||||||
const [bars, setBars] = useState<OHLCVBar[]>([]);
|
const [bars, setBars] = useState<OHLCVBar[]>([]);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
@ -237,12 +261,18 @@ function StockChart({ symbol }: { symbol: string }) {
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
setBars([]);
|
setBars([]);
|
||||||
|
onLatestBarTimestamp?.(null);
|
||||||
fetchChartBars(symbol, period)
|
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'); })
|
.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]);
|
}, [symbol, period, onLatestBarTimestamp]);
|
||||||
|
|
||||||
const closes = bars.map(b => b.close);
|
const closes = bars.map(b => b.close);
|
||||||
const rsi = calculateRsi(closes);
|
const rsi = calculateRsi(closes);
|
||||||
@ -742,6 +772,7 @@ export function HomeView() {
|
|||||||
const { activeSymbol, setActiveSymbol } = useAppContext();
|
const { activeSymbol, setActiveSymbol } = useAppContext();
|
||||||
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);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!activeSymbol) {
|
if (!activeSymbol) {
|
||||||
@ -770,8 +801,8 @@ export function HomeView() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<TickerHeader symbol={activeSymbol} profile={profile} />
|
<TickerHeader symbol={activeSymbol} profile={profile} latestBarTimestamp={latestBarTimestamp} />
|
||||||
<StockChart symbol={activeSymbol} />
|
<StockChart symbol={activeSymbol} onLatestBarTimestamp={setLatestBarTimestamp} />
|
||||||
<QuickStats symbol={activeSymbol} />
|
<QuickStats symbol={activeSymbol} />
|
||||||
<ResearchCards symbol={activeSymbol} profile={profile} profileLoading={profileLoading} />
|
<ResearchCards symbol={activeSymbol} profile={profile} profileLoading={profileLoading} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user