import { useMemo, useEffect, useState } from 'react'; import { useParams } from 'react-router-dom'; import { useTickerDetail } from '../hooks/useTickerDetail'; import { useFetchSymbolData } from '../hooks/useFetchSymbolData'; import { CandlestickChart } from '../components/charts/CandlestickChart'; import { ScoreCard } from '../components/ui/ScoreCard'; import { SkeletonCard } from '../components/ui/Skeleton'; import { SentimentPanel } from '../components/ticker/SentimentPanel'; import { FundamentalsPanel } from '../components/ticker/FundamentalsPanel'; import { IndicatorSelector } from '../components/ticker/IndicatorSelector'; import { RecommendationPanel } from '../components/ticker/RecommendationPanel'; import { Button } from '../components/ui/Button'; import { Callout } from '../components/ui/Callout'; import { Section } from '../components/ui/Section'; import { Tabs } from '../components/ui/Tabs'; import { formatPrice } from '../lib/format'; import type { TradeSetup } from '../lib/types'; const detailTabs = ['Analysis', 'Indicators', 'S/R Levels'] as const; type DetailTab = (typeof detailTabs)[number]; function SectionError({ message, onRetry }: { message: string; onRetry?: () => void }) { return ( {message} ); } function timeAgo(iso: string): string { const diff = Date.now() - new Date(iso).getTime(); const mins = Math.floor(diff / 60_000); if (mins < 1) return 'just now'; if (mins < 60) return `${mins}m ago`; const hrs = Math.floor(mins / 60); if (hrs < 24) return `${hrs}h ago`; const days = Math.floor(hrs / 24); return `${days}d ago`; } interface DataStatusItem { label: string; available: boolean; timestamp?: string | null; } function DataFreshnessBar({ items }: { items: DataStatusItem[] }) { return (
{items.map((item) => (
{item.label} {item.available && item.timestamp && ( {timeAgo(item.timestamp)} )} {!item.available && ( no data )}
))}
); } export default function TickerDetailPage() { const { symbol = '' } = useParams<{ symbol: string }>(); const { ohlcv, scores, srLevels, sentiment, fundamentals, trades } = useTickerDetail(symbol); const ingestion = useFetchSymbolData(); const [activeTab, setActiveTab] = useState('Analysis'); const dataStatus: DataStatusItem[] = useMemo(() => [ { label: 'OHLCV', available: !!ohlcv.data && ohlcv.data.length > 0, timestamp: ohlcv.data?.[ohlcv.data.length - 1]?.created_at, }, { label: 'Sentiment', available: !!sentiment.data && sentiment.data.count > 0, timestamp: sentiment.data?.scores?.[0]?.timestamp, }, { label: 'Fundamentals', available: !!fundamentals.data && fundamentals.data.fetched_at !== null, timestamp: fundamentals.data?.fetched_at, }, { label: 'S/R Levels', available: !!srLevels.data && srLevels.data.count > 0, timestamp: srLevels.data?.levels?.[0]?.created_at, }, { label: 'Scores', available: !!scores.data && scores.data.composite_score !== null, timestamp: scores.data?.computed_at, }, ], [ohlcv.data, sentiment.data, fundamentals.data, srLevels.data, scores.data]); // Log trades API errors but don't disrupt the page useEffect(() => { if (trades.error) { console.error('Failed to fetch trade setups:', trades.error); } }, [trades.error]); const setupsForSymbol: TradeSetup[] = useMemo(() => { if (trades.error || !trades.data) return []; return trades.data.filter((t) => t.symbol.toUpperCase() === symbol.toUpperCase()); }, [trades.data, trades.error, symbol]); const longSetup = useMemo( () => setupsForSymbol?.find((s) => s.direction === 'long'), [setupsForSymbol], ); const shortSetup = useMemo( () => setupsForSymbol?.find((s) => s.direction === 'short'), [setupsForSymbol], ); // Use the highest-confidence setup for chart overlay fallback. const tradeSetup: TradeSetup | undefined = useMemo(() => { const candidates = [longSetup, shortSetup].filter(Boolean) as TradeSetup[]; if (candidates.length === 0) return undefined; return candidates.sort((a, b) => (b.confidence_score ?? 0) - (a.confidence_score ?? 0))[0]; }, [longSetup, shortSetup]); // Sort visible S/R levels by strength for the table (only levels within chart zones) const sortedLevels = useMemo(() => { if (!srLevels.data?.visible_levels) return []; return [...srLevels.data.visible_levels].sort((a, b) => b.strength - a.strength); }, [srLevels.data]); return (
{/* Header */}

{symbol.toUpperCase()}

Ticker Detail

{/* Data freshness bar */} {/* Chart — always visible */}
{ohlcv.isLoading && } {ohlcv.isError && ( ohlcv.refetch()} /> )} {ohlcv.data && (
{srLevels.isError && (

S/R levels unavailable — chart shown without overlays

)}
)}
{/* Detail tabs */} {activeTab === 'Analysis' && (
{scores.isLoading && } {scores.isError && ( scores.refetch()} /> )} {scores.data && ( )}
{sentiment.isLoading && } {sentiment.isError && ( sentiment.refetch()} /> )} {sentiment.data && }
{fundamentals.isLoading && } {fundamentals.isError && ( fundamentals.refetch()} /> )} {fundamentals.data && }
)} {activeTab === 'Indicators' && (
)} {activeTab === 'S/R Levels' && (
{sortedLevels.length === 0 ? ( No S/R levels detected for this ticker yet. ) : (
{sortedLevels.map((level) => ( ))}
Type Price Level Strength Method
{level.type} {formatPrice(level.price_level)} {level.strength} {level.detection_method}
)}
)}
); }