import { useMemo, useEffect } from 'react'; import { useParams } from 'react-router-dom'; import { useMutation, useQueryClient } from '@tanstack/react-query'; import { useTickerDetail } from '../hooks/useTickerDetail'; 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 { useToast } from '../components/ui/Toast'; import { fetchData } from '../api/ingestion'; import { formatPrice } from '../lib/format'; import type { TradeSetup } from '../lib/types'; function SectionError({ message, onRetry }: { message: string; onRetry?: () => void }) { return (

{message}

{onRetry && ( )}
); } 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 queryClient = useQueryClient(); const { addToast } = useToast(); const ingestion = useMutation({ mutationFn: () => fetchData(symbol), onSuccess: (result: any) => { // Show per-source status breakdown const sources = result?.sources; if (sources) { const parts: string[] = []; for (const [name, info] of Object.entries(sources) as [string, any][]) { const label = name.charAt(0).toUpperCase() + name.slice(1); if (info.status === 'ok') { parts.push(`${label} ✓`); } else if (info.status === 'skipped') { parts.push(`${label}: skipped (${info.message})`); } else { parts.push(`${label} ✗: ${info.message}`); } } const hasError = Object.values(sources).some((s: any) => s.status === 'error'); const hasSkip = Object.values(sources).some((s: any) => s.status === 'skipped'); const toastType = hasError ? 'error' : hasSkip ? 'info' : 'success'; addToast(toastType, parts.join(' · ')); } else { addToast('success', `Data fetched for ${symbol.toUpperCase()}`); } queryClient.invalidateQueries({ queryKey: ['ohlcv', symbol] }); queryClient.invalidateQueries({ queryKey: ['sentiment', symbol] }); queryClient.invalidateQueries({ queryKey: ['fundamentals', symbol] }); queryClient.invalidateQueries({ queryKey: ['sr-levels', symbol] }); queryClient.invalidateQueries({ queryKey: ['scores', symbol] }); }, onError: (err: Error) => { addToast('error', err.message || 'Failed to fetch data'); }, }); 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]); // Pick the latest trade setup for the current symbol const tradeSetup: TradeSetup | undefined = useMemo(() => { if (trades.error || !trades.data) return undefined; const matching = trades.data.filter( (t) => t.symbol.toUpperCase() === symbol.toUpperCase(), ); if (matching.length === 0) return undefined; return matching.reduce((latest, t) => new Date(t.detected_at) > new Date(latest.detected_at) ? t : latest, ); }, [trades.data, trades.error, symbol]); // 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 Section */}

Price Chart

{ohlcv.isLoading && } {ohlcv.isError && ( ohlcv.refetch()} /> )} {ohlcv.data && (
{srLevels.isError && (

S/R levels unavailable — chart shown without overlays

)}
)}
{/* Trade Setup Summary Card */} {tradeSetup && (

Trade Setup

Direction {tradeSetup.direction.toUpperCase()}
Entry {formatPrice(tradeSetup.entry_price)}
Stop {formatPrice(tradeSetup.stop_loss)}
Target {formatPrice(tradeSetup.target)}
R:R {tradeSetup.rr_ratio.toFixed(2)}
)} {/* Scores + Side Panels */}

Scores

{scores.isLoading && } {scores.isError && ( scores.refetch()} /> )} {scores.data && ( )}

Sentiment

{sentiment.isLoading && } {sentiment.isError && ( sentiment.refetch()} /> )} {sentiment.data && }

Fundamentals

{fundamentals.isLoading && } {fundamentals.isError && ( fundamentals.refetch()} /> )} {fundamentals.data && }
{/* Indicators */}

Technical Indicators

{/* S/R Levels Table — sorted by strength */} {sortedLevels.length > 0 && (

Support & Resistance Levels sorted by strength

{sortedLevels.map((level) => ( ))}
Type Price Level Strength Method
{level.type} {formatPrice(level.price_level)} {level.strength} {level.detection_method}
)}
); }