From 55512ca5a8a26460a30dadb11d92d012d5f18861 Mon Sep 17 00:00:00 2001 From: Celes Renata Date: Thu, 16 Apr 2026 00:58:18 +0000 Subject: [PATCH] fix: rewrite dashboards to use PostgreSQL API instead of empty Trino lakehouse MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Trino/Iceberg lakehouse has zero tables, so all Trino-backed dashboards showed 'No data available'. Rewrote all four to use existing PostgreSQL-backed API endpoints: - Sentiment Heatmap: useTrends + useCompanies → sector and ticker trend strength bar charts (30k trend_windows in DB) - Prediction Accuracy: useRecommendations → confidence distribution and action distribution charts (30k recommendations in DB) - Paper PnL: useTradingMetrics + useTradingMetricsHistory → equity curve, daily returns, win/loss stats from trading engine - Model Quality: useModelPerformance + useModelFailures → success rate, latency, retries, and failure table from ops API Removed unused Trino query function and ScatterChart imports. --- frontend/src/pages/Dashboards.tsx | 467 ++++++++++++++++++++---------- 1 file changed, 306 insertions(+), 161 deletions(-) diff --git a/frontend/src/pages/Dashboards.tsx b/frontend/src/pages/Dashboards.tsx index 98e3f6f..3d600e0 100644 --- a/frontend/src/pages/Dashboards.tsx +++ b/frontend/src/pages/Dashboards.tsx @@ -1,11 +1,11 @@ import { useState } from 'react'; -import { useCompanies, useTrends, useRecommendations, usePositions } from '../api/hooks'; -import { useQuery } from '@tanstack/react-query'; -import { apiPost } from '../api/client'; +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 { - LineChart, Line, BarChart, Bar, ScatterChart, Scatter, + BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer, CartesianGrid, Legend, + LineChart, Line, } from 'recharts'; type DashboardId = 'gallery' | 'symbol-overview' | 'sentiment-heatmap' | 'prediction-accuracy' | 'paper-pnl' | 'model-quality'; @@ -51,9 +51,9 @@ export function DashboardsPage() { ← Back to gallery {active === 'symbol-overview' && } - {active === 'sentiment-heatmap' && } - {active === 'prediction-accuracy' && } - {active === 'paper-pnl' && } + {active === 'sentiment-heatmap' && } + {active === 'prediction-accuracy' && } + {active === 'paper-pnl' && } {active === 'model-quality' && } )} @@ -62,7 +62,7 @@ export function DashboardsPage() { } // --------------------------------------------------------------------------- -// Symbol Overview +// Symbol Overview (unchanged — already uses PostgreSQL hooks) // --------------------------------------------------------------------------- function SymbolOverview({ ticker }: { ticker: string }) { @@ -121,195 +121,340 @@ function SymbolOverview({ ticker }: { ticker: string }) { } // --------------------------------------------------------------------------- -// Trino-backed dashboards +// Sentiment Heatmap — powered by useTrends (PostgreSQL) // --------------------------------------------------------------------------- -function useTrinoQuery(sql: string, enabled = true) { - return useQuery({ - queryKey: ['trino-dashboard', sql], - queryFn: () => apiPost<{ columns: Array<{ name: string }>; rows: unknown[][] }>('query', '/api/analytics/query', { sql, limit: 5000 }), - enabled, - staleTime: 60_000, - }); -} - -function SentimentHeatmap({ hours }: { hours: number }) { - const days = Math.ceil(hours / 24); - const { data, isLoading } = useTrinoQuery( - `SELECT entity_id AS ticker, window, trend_direction, trend_strength, confidence, generated_at FROM trend_windows WHERE entity_type = 'company' AND generated_at >= current_timestamp - interval '${days}' day ORDER BY generated_at DESC LIMIT 500` - ); +function SentimentHeatmap() { + const { data: trends, isLoading } = useTrends({ limit: 500 }); + const { data: companies } = useCompanies(); if (isLoading) return ; - if (!data?.rows.length) return

No sentiment data available

; + if (!trends?.length) return

No sentiment data available

; - const chartData = data.rows.map((r) => ({ - ticker: String(r[0]), - window: String(r[1]), - strength: Number(r[3]) || 0, - direction: String(r[2]), - })); + // Build sector lookup from companies + const sectorMap = new Map((companies ?? []).map((c) => [c.ticker, c.sector ?? 'Unknown'])); - return ( - -

Sentiment by Symbol

- - - - - - - - - -
- ); -} + // 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); + } -function PredictionAccuracy({ hours }: { hours: number }) { - const days = Math.ceil(hours / 24); - const { data, isLoading } = useTrinoQuery( - `SELECT predicted_confidence, realized_move_pct, ticker, prediction_date FROM prediction_vs_outcome WHERE prediction_date >= current_date - interval '${days}' day ORDER BY prediction_date DESC LIMIT 1000` - ); + 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); - if (isLoading) return ; - if (!data?.rows.length) return

No prediction data available

; + // 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 chartData = data.rows.map((r) => ({ - confidence: Number(r[0]) || 0, - realized: Number(r[1]) || 0, - ticker: String(r[2]), - })); - - return ( - -

Predicted Confidence vs Realized Move

- - - - - - - - - -
- ); -} - -function PaperPnl({ hours }: { hours: number }) { - const days = Math.ceil(hours / 24); - const { data, isLoading } = useTrinoQuery( - `SELECT dt, daily_pnl, cumulative_pnl, win_count, loss_count FROM pnl_daily WHERE dt >= current_date - interval '${days}' day ORDER BY dt ASC LIMIT 365` - ); - - if (isLoading) return ; - if (!data?.rows.length) return

No PnL data available

; - - const chartData = data.rows.map((r) => ({ - date: String(r[0]), - daily: Number(r[1]) || 0, - cumulative: Number(r[2]) || 0, - wins: Number(r[3]) || 0, - losses: Number(r[4]) || 0, - })); - - const totalWins = chartData.reduce((s, d) => s + d.wins, 0); - const totalLosses = chartData.reduce((s, d) => s + d.losses, 0); - const winRate = totalWins + totalLosses > 0 ? ((totalWins / (totalWins + totalLosses)) * 100).toFixed(1) : '—'; + 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
-
{totalWins}
+
{winCount}
Wins
-
{totalLosses}
+
{lossCount}
Losses
+ +
= 0 ? 'text-green-400' : 'text-red-400'}`}> + ${(metrics?.unrealized_pnl ?? 0).toFixed(2)} +
+
Unrealized PnL
+
- -

Equity Curve

- - - - - - - - - -
+ {/* 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
+
+
+ )} - -

Daily PnL

- - - - - - - - - -
+ {/* 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 days = Math.ceil(hours / 24); - const { data, isLoading } = useTrinoQuery( - `SELECT date_trunc('hour', recorded_at) AS hour, count(*) AS total, count(*) filter (where success = true) AS successes, avg(total_duration_ms) AS avg_latency, avg(retry_count) AS avg_retries FROM model_performance_metrics WHERE recorded_at >= current_timestamp - interval '${days}' day GROUP BY 1 ORDER BY 1 ASC LIMIT 500` - ); + const { data: perf, isLoading: perfLoading } = useModelPerformance(hours); + const { data: failures, isLoading: failLoading } = useModelFailures(hours); - if (isLoading) return ; - if (!data?.rows.length) return

No model metrics available

; + if (perfLoading || failLoading) return ; - const chartData = data.rows.map((r) => ({ - hour: String(r[0]).slice(11, 16), - total: Number(r[1]) || 0, - successes: Number(r[2]) || 0, - rate: Number(r[1]) > 0 ? ((Number(r[2]) / Number(r[1])) * 100) : 0, - latency: Math.round(Number(r[3]) || 0), - retries: Number(r[4]) || 0, - })); + 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 (
- -

Success Rate Over Time

- - - - - - - - - -
+ {/* 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
+
+
- -

Latency & Retries

- - - - - - - - - - - -
+ {/* Recent failures table */} + {failureList.length > 0 && ( + +

Recent Failures ({failureList.length})

+
+ + + + + + + + + + + {failureList.slice(0, 20).map((f, i) => ( + + + + + + + ))} + +
TimeModelErrorRetries
{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 ?? '—')}
+
+
+ )}
); }