import { useState } from 'react'; import { useCompanies, useTrends, useRecommendations, usePositions, useModelPerformance, useModelFailures } from '../api/hooks'; import { useTradingMetrics, useTradingMetricsHistory } from '../api/tradingHooks'; import { TrendArrow, StatusBadge, ConfidenceBar, LoadingSpinner, DateRangeSelector, TickerFilter, Card } from '../components/ui'; import { BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer, CartesianGrid, LineChart, Line, } from 'recharts'; type DashboardId = 'gallery' | 'symbol-overview' | 'sentiment-heatmap' | 'prediction-accuracy' | 'paper-pnl' | 'model-quality'; const dashboards: Array<{ id: DashboardId; title: string; description: string }> = [ { id: 'symbol-overview', title: 'Symbol Overview', description: 'Company cards with trend direction, latest recommendation, position status' }, { id: 'sentiment-heatmap', title: 'Sentiment Heatmap', description: 'Sector × time matrix colored by aggregated sentiment' }, { id: 'prediction-accuracy', title: 'Prediction Accuracy', description: 'Predicted confidence vs realized price move' }, { id: 'paper-pnl', title: 'Paper Trading PnL', description: 'Equity curve, daily PnL bars, win rate metrics' }, { id: 'model-quality', title: 'Model Quality', description: 'Extraction success rate, latency distribution, retry rate' }, ]; export function DashboardsPage() { const [active, setActive] = useState('gallery'); const [hours, setHours] = useState(168); const [ticker, setTicker] = useState(''); return (

Dashboards

{/* Gallery / nav */} {active === 'gallery' ? (
{dashboards.map((d) => ( ))}
) : (
{active === 'symbol-overview' && } {active === 'sentiment-heatmap' && } {active === 'prediction-accuracy' && } {active === 'paper-pnl' && } {active === 'model-quality' && }
)}
); } // --------------------------------------------------------------------------- // Symbol Overview (unchanged — already uses PostgreSQL hooks) // --------------------------------------------------------------------------- function SymbolOverview({ ticker }: { ticker: string }) { const { data: companies, isLoading: cLoading } = useCompanies({ ticker: ticker || undefined }); const { data: trends } = useTrends({ ticker: ticker || undefined, window: '7d', limit: 100 }); const { data: recs } = useRecommendations({ ticker: ticker || undefined, limit: 100 }); const { data: positions } = usePositions(ticker || undefined); if (cLoading) return ; const trendMap = new Map((trends ?? []).map((t) => [t.entity_id, t])); const recMap = new Map((recs ?? []).map((r) => [r.ticker, r])); const posMap = new Map((positions ?? []).map((p) => [p.ticker, p])); return (
{(companies ?? []).map((c) => { const trend = trendMap.get(c.ticker); const rec = recMap.get(c.ticker); const pos = posMap.get(c.ticker); return (
{c.ticker} {trend && }
{c.legal_name}
{trend && (
Strength
)} {rec && (
)} {pos && (
Position: {pos.quantity} @ ${pos.avg_entry_price.toFixed(2)} {pos.unrealized_pnl != null && ( = 0 ? 'ml-2 text-green-400' : 'ml-2 text-red-400'}> {pos.unrealized_pnl >= 0 ? '+' : ''}{pos.unrealized_pnl.toFixed(2)} )}
)}
); })}
); } // --------------------------------------------------------------------------- // Sentiment Heatmap — powered by useTrends (PostgreSQL) // --------------------------------------------------------------------------- function SentimentHeatmap() { const { data: trends, isLoading } = useTrends({ limit: 500 }); const { data: companies } = useCompanies(); if (isLoading) return ; if (!trends?.length) return

No sentiment data available

; // Build sector lookup from companies const sectorMap = new Map((companies ?? []).map((c) => [c.ticker, c.sector ?? 'Unknown'])); // Aggregate trend_strength by sector const sectorAgg = new Map(); for (const t of trends) { const sector = sectorMap.get(t.entity_id) ?? 'Unknown'; const entry = sectorAgg.get(sector) ?? { total: 0, count: 0 }; entry.total += t.trend_strength; entry.count += 1; sectorAgg.set(sector, entry); } const sectorData = Array.from(sectorAgg.entries()) .map(([sector, { total, count }]) => ({ sector, avgStrength: Math.round((total / count) * 100) / 100, count, })) .sort((a, b) => b.avgStrength - a.avgStrength); // Also build per-ticker chart const tickerAgg = new Map(); for (const t of trends) { const entry = tickerAgg.get(t.entity_id) ?? { total: 0, count: 0 }; entry.total += t.trend_strength; entry.count += 1; tickerAgg.set(t.entity_id, entry); } const tickerData = Array.from(tickerAgg.entries()) .map(([ticker, { total, count }]) => ({ ticker, strength: Math.round((total / count) * 100) / 100, })) .sort((a, b) => b.strength - a.strength) .slice(0, 25); return (

Average Trend Strength by Sector

Trend Strength by Ticker (Top 25)

); } // --------------------------------------------------------------------------- // Prediction Accuracy — powered by useRecommendations (PostgreSQL) // --------------------------------------------------------------------------- function PredictionAccuracy() { const { data: recs, isLoading } = useRecommendations({ limit: 500 }); if (isLoading) return ; if (!recs?.length) return

No recommendation data available

; // Confidence distribution buckets const buckets = [ { label: '<0.40', min: 0, max: 0.4 }, { label: '0.40–0.55', min: 0.4, max: 0.55 }, { label: '0.55–0.75', min: 0.55, max: 0.75 }, { label: '0.75–1.00', min: 0.75, max: 1.01 }, ]; const confidenceData = buckets.map((b) => ({ bucket: b.label, count: recs.filter((r) => r.confidence >= b.min && r.confidence < b.max).length, })); // Action distribution const actionCounts = new Map(); for (const r of recs) { actionCounts.set(r.action, (actionCounts.get(r.action) ?? 0) + 1); } const actionData = Array.from(actionCounts.entries()) .map(([action, count]) => ({ action, count })) .sort((a, b) => b.count - a.count); return (

Confidence Distribution

Action Distribution

Note: Showing confidence and action distributions from recommendations. Realized price move comparison will be available once outcome tracking is implemented.

); } // --------------------------------------------------------------------------- // Paper PnL — powered by trading engine API (PostgreSQL) // --------------------------------------------------------------------------- function PaperPnl() { const { data: metrics, isLoading: metricsLoading } = useTradingMetrics(); const { data: snapshots, isLoading: historyLoading } = useTradingMetricsHistory(); if (metricsLoading || historyLoading) return ; const hasMetrics = !!metrics; const hasSnapshots = snapshots && snapshots.length > 0; if (!hasMetrics && !hasSnapshots) { return (

Paper trading metrics will appear after the first trading day.

); } const equityCurve = (snapshots ?? []) .map((s) => ({ date: s.snapshot_date, value: s.portfolio_value, dailyReturn: s.daily_return != null ? Math.round(s.daily_return * 10000) / 100 : 0, })) .sort((a, b) => a.date.localeCompare(b.date)); const winRate = metrics ? (metrics.win_rate * 100).toFixed(1) : '—'; const winCount = metrics?.win_count ?? 0; const lossCount = metrics?.loss_count ?? 0; return (
{/* Stat cards */}
{winRate}%
Win Rate
{winCount}
Wins
{lossCount}
Losses
= 0 ? 'text-green-400' : 'text-red-400'}`}> ${(metrics?.unrealized_pnl ?? 0).toFixed(2)}
Unrealized PnL
{/* More metrics */} {hasMetrics && (
${metrics.total_portfolio_value.toFixed(0)}
Portfolio Value
{metrics.profit_factor.toFixed(2)}
Profit Factor
{metrics.sharpe_ratio.toFixed(2)}
Sharpe Ratio
{(metrics.max_drawdown * 100).toFixed(1)}%
Max Drawdown
)} {/* Equity curve */} {hasSnapshots && (

Equity Curve

)} {/* Daily returns */} {hasSnapshots && equityCurve.some((d) => d.dailyReturn !== 0) && (

Daily Returns (%)

)}
); } // --------------------------------------------------------------------------- // Model Quality — powered by ops API (PostgreSQL) // --------------------------------------------------------------------------- function ModelQuality({ hours }: { hours: number }) { const { data: perf, isLoading: perfLoading } = useModelPerformance(hours); const { data: failures, isLoading: failLoading } = useModelFailures(hours); if (perfLoading || failLoading) return ; const perfData = perf as Record | undefined; const failureList = (failures ?? []) as Array>; const totalCalls = Number(perfData?.total_calls ?? 0); const successCalls = Number(perfData?.successful_calls ?? 0); const successRate = totalCalls > 0 ? ((successCalls / totalCalls) * 100).toFixed(1) : '—'; const avgLatency = Number(perfData?.avg_latency_ms ?? 0); const avgRetries = Number(perfData?.avg_retries ?? 0); const hasData = totalCalls > 0 || failureList.length > 0; if (!hasData) return

No model metrics available

; return (
{/* Stat cards */}
{totalCalls}
Total Calls
= 90 ? 'text-green-400' : Number(successRate) >= 70 ? 'text-yellow-400' : 'text-red-400'}`}> {successRate}%
Success Rate
{Math.round(avgLatency)}ms
Avg Latency
{avgRetries.toFixed(2)}
Avg Retries
{/* Recent failures table */} {failureList.length > 0 && (

Recent Failures ({failureList.length})

{failureList.slice(0, 20).map((f, i) => ( ))}
Time Model Error Retries
{String(f.recorded_at ?? f.created_at ?? '').slice(0, 19)} {String(f.model_name ?? f.model ?? '—')} {String(f.error_message ?? f.error ?? '—')} {String(f.retry_count ?? '—')}
)}
); }