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