feat: add Reports page and detail view to frontend
ci/woodpecker/push/test Pipeline was successful
ci/woodpecker/push/build-2 Pipeline was successful
ci/woodpecker/push/build-1 Pipeline was successful
ci/woodpecker/push/build-3 Pipeline was successful
ci/woodpecker/push/finalize Pipeline was successful
Build and Push / lint-and-test (push) Has been cancelled
Build and Push / build-services (map[cmd:python -m services.adapters.broker_adapter name:broker-adapter]) (push) Has been cancelled
Build and Push / build-services (map[cmd:python -m services.aggregation.worker name:aggregation]) (push) Has been cancelled
Build and Push / build-services (map[cmd:python -m services.extractor.worker name:extractor]) (push) Has been cancelled
Build and Push / build-services (map[cmd:python -m services.ingestion.worker name:ingestion]) (push) Has been cancelled
Build and Push / build-services (map[cmd:python -m services.lake_publisher.worker name:lake-publisher]) (push) Has been cancelled
Build and Push / build-services (map[cmd:python -m services.parser.worker name:parser]) (push) Has been cancelled
Build and Push / build-services (map[cmd:python -m services.recommendation.worker name:recommendation]) (push) Has been cancelled
Build and Push / build-services (map[cmd:python -m services.scheduler.app name:scheduler]) (push) Has been cancelled
Build and Push / build-services (map[cmd:uvicorn services.api.app:app --host 0.0.0.0 --port 8000 name:query-api]) (push) Has been cancelled
Build and Push / build-services (map[cmd:uvicorn services.risk.app:app --host 0.0.0.0 --port 8000 name:risk]) (push) Has been cancelled
Build and Push / build-services (map[cmd:uvicorn services.symbol_registry.app:app --host 0.0.0.0 --port 8000 name:symbol-registry]) (push) Has been cancelled
Build and Push / build-services (map[cmd:uvicorn services.trading.app:app --host 0.0.0.0 --port 8000 name:trading-engine]) (push) Has been cancelled
Build and Push / build-dashboard (push) Has been cancelled
Build and Push / build-superset (push) Has been cancelled
Build and Push / integration-test (push) Has been cancelled
Build and Push / beta-gate (push) Has been cancelled

- Reports list page with type filter (daily/weekly)
- Report detail page with all sections: P&L, recommendation accuracy,
  position performance table, risk metrics, model quality windows
- Executive summary card, validation warnings display
- Nav item under Trading group
- Routes: /reports and /reports/:id
This commit is contained in:
Celes Renata
2026-05-02 00:27:34 +00:00
parent 3a8c6f6c80
commit 7e2343ec2c
4 changed files with 371 additions and 0 deletions
+2
View File
@@ -19,6 +19,7 @@ import {
Globe, Globe,
BarChart3, BarChart3,
Bot, Bot,
ClipboardList,
} from 'lucide-react'; } from 'lucide-react';
interface NavItem { interface NavItem {
@@ -40,6 +41,7 @@ const navItems: NavItem[] = [
{ to: '/positions', label: 'Positions', icon: <Wallet size={18} />, group: 'Trading' }, { to: '/positions', label: 'Positions', icon: <Wallet size={18} />, group: 'Trading' },
{ to: '/trading', label: 'Trading Controls', icon: <ShieldCheck size={18} />, group: 'Trading' }, { to: '/trading', label: 'Trading Controls', icon: <ShieldCheck size={18} />, group: 'Trading' },
{ to: '/trading/engine', label: 'Trading Engine', icon: <BarChart3 size={18} />, group: 'Trading' }, { to: '/trading/engine', label: 'Trading Engine', icon: <BarChart3 size={18} />, group: 'Trading' },
{ to: '/reports', label: 'Reports', icon: <ClipboardList size={18} />, group: 'Trading' },
{ to: '/ops/pipeline', label: 'Pipeline', icon: <Activity size={18} />, group: 'Ops' }, { to: '/ops/pipeline', label: 'Pipeline', icon: <Activity size={18} />, group: 'Ops' },
{ to: '/ops/ingestion', label: 'Ingestion', icon: <Download size={18} />, group: 'Ops' }, { to: '/ops/ingestion', label: 'Ingestion', icon: <Download size={18} />, group: 'Ops' },
{ to: '/ops/model', label: 'Model Perf', icon: <Cpu size={18} />, group: 'Ops' }, { to: '/ops/model', label: 'Model Perf', icon: <Cpu size={18} />, group: 'Ops' },
+275
View File
@@ -0,0 +1,275 @@
import { useParams, Link } from '@tanstack/react-router';
import { useReport } from '../api/hooks';
import { LoadingSpinner, StatusBadge, Card } from '../components/ui';
import { ArrowLeft } from 'lucide-react';
interface PLSection {
realized_pnl: number;
unrealized_pnl: number;
daily_return: number;
cumulative_return: number;
win_count: number;
loss_count: number;
win_rate: number;
profit_factor: number;
sharpe_ratio: number;
summary: string;
validation_warnings?: { field_name: string; computed_value: number; snapshot_value: number; pct_difference: number }[];
}
interface PositionDetail {
ticker: string;
entry_price: number;
current_or_exit_price: number;
pnl: number;
pnl_pct: number;
hold_duration_hours: number;
status: string;
}
interface RiskMetrics {
current_risk_tier: string;
portfolio_heat: number;
max_drawdown: number;
current_drawdown_pct: number;
reserve_pool_balance: number;
circuit_breaker_event_count: number;
summary: string;
}
interface ModelWindow {
lookback: string;
win_rate: number | null;
directional_accuracy: number | null;
information_coefficient: number | null;
calibration_error: number | null;
brier_score: number | null;
}
interface ReportData {
pnl: PLSection;
recommendation_accuracy: {
total_evaluated: number;
act_count: number;
skip_count: number;
acted_win_rate: number;
avg_confidence_acted: number;
avg_confidence_skipped: number;
summary: string;
validation_warnings?: { field_name: string; pct_difference: number }[];
};
position_performance: {
positions: PositionDetail[];
summary: string;
};
risk_metrics: RiskMetrics;
model_quality: {
windows: ModelWindow[];
summary: string;
validation_warnings?: { field_name: string; pct_difference: number }[];
};
executive_summary: string;
validation_status: string;
}
function MetricCard({ label, value, sub }: { label: string; value: string; sub?: string }) {
return (
<div className="rounded-lg bg-surface-800 border border-surface-700 p-3">
<div className="text-xs text-gray-400 mb-1">{label}</div>
<div className="text-lg font-semibold text-gray-100">{value}</div>
{sub && <div className="text-xs text-gray-500 mt-0.5">{sub}</div>}
</div>
);
}
function pct(v: number) {
return `${(v * 100).toFixed(2)}%`;
}
function dollar(v: number) {
return v >= 0 ? `$${v.toFixed(2)}` : `-$${Math.abs(v).toFixed(2)}`;
}
export function ReportDetailPage() {
const { id } = useParams({ from: '/reports/$id' });
const { data, isLoading } = useReport(id);
if (isLoading) return <LoadingSpinner />;
if (!data) return <div className="text-gray-400">Report not found</div>;
const report = data.report_data as unknown as ReportData;
return (
<div className="space-y-6">
{/* Header */}
<div className="flex items-center gap-3">
<Link to="/reports" className="text-gray-400 hover:text-gray-200">
<ArrowLeft size={20} />
</Link>
<div>
<h1 className="text-xl font-semibold text-gray-100">
{data.report_type === 'daily' ? 'Daily' : 'Weekly'} Report
</h1>
<p className="text-sm text-gray-400">
{data.period_start === data.period_end
? data.period_start
: `${data.period_start}${data.period_end}`}
{' · '}
<StatusBadge status={data.validation_status} />
</p>
</div>
</div>
{/* Executive Summary */}
{report.executive_summary && (
<Card>
<h2 className="text-sm font-medium text-gray-300 mb-2">Executive Summary</h2>
<p className="text-sm text-gray-200 whitespace-pre-wrap leading-relaxed">
{report.executive_summary}
</p>
</Card>
)}
{/* P&L Section */}
<Card>
<h2 className="text-sm font-medium text-gray-300 mb-3">P&L</h2>
<div className="grid grid-cols-2 md:grid-cols-4 gap-3 mb-3">
<MetricCard label="Realized P&L" value={dollar(report.pnl.realized_pnl)} />
<MetricCard label="Unrealized P&L" value={dollar(report.pnl.unrealized_pnl)} />
<MetricCard label="Daily Return" value={pct(report.pnl.daily_return)} />
<MetricCard label="Cumulative Return" value={pct(report.pnl.cumulative_return)} />
<MetricCard label="Win Rate" value={pct(report.pnl.win_rate)} sub={`${report.pnl.win_count}W / ${report.pnl.loss_count}L`} />
<MetricCard label="Profit Factor" value={report.pnl.profit_factor.toFixed(2)} />
<MetricCard label="Sharpe Ratio" value={report.pnl.sharpe_ratio.toFixed(2)} />
</div>
{report.pnl.summary && (
<p className="text-xs text-gray-400 mt-2">{report.pnl.summary}</p>
)}
</Card>
{/* Recommendation Accuracy */}
<Card>
<h2 className="text-sm font-medium text-gray-300 mb-3">Recommendation Accuracy</h2>
<div className="grid grid-cols-2 md:grid-cols-4 gap-3 mb-3">
<MetricCard label="Total Evaluated" value={String(report.recommendation_accuracy.total_evaluated)} />
<MetricCard label="Acted" value={String(report.recommendation_accuracy.act_count)} />
<MetricCard label="Skipped" value={String(report.recommendation_accuracy.skip_count)} />
<MetricCard label="Acted Win Rate" value={pct(report.recommendation_accuracy.acted_win_rate)} />
<MetricCard label="Avg Confidence (Acted)" value={report.recommendation_accuracy.avg_confidence_acted.toFixed(3)} />
<MetricCard label="Avg Confidence (Skipped)" value={report.recommendation_accuracy.avg_confidence_skipped.toFixed(3)} />
</div>
{report.recommendation_accuracy.validation_warnings && report.recommendation_accuracy.validation_warnings.length > 0 && (
<div className="mt-2 rounded bg-yellow-900/20 border border-yellow-700/30 p-2">
<span className="text-xs text-yellow-400"> Validation warnings:</span>
{report.recommendation_accuracy.validation_warnings.map((w, i) => (
<span key={i} className="text-xs text-yellow-300 ml-2">{w.field_name} ({w.pct_difference.toFixed(1)}% off)</span>
))}
</div>
)}
{report.recommendation_accuracy.summary && (
<p className="text-xs text-gray-400 mt-2">{report.recommendation_accuracy.summary}</p>
)}
</Card>
{/* Position Performance */}
<Card>
<h2 className="text-sm font-medium text-gray-300 mb-3">
Positions ({report.position_performance.positions.length})
</h2>
{report.position_performance.positions.length > 0 ? (
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="text-left text-xs text-gray-400 border-b border-surface-700">
<th className="pb-2 pr-4">Ticker</th>
<th className="pb-2 pr-4">Status</th>
<th className="pb-2 pr-4">Entry</th>
<th className="pb-2 pr-4">Current/Exit</th>
<th className="pb-2 pr-4">P&L</th>
<th className="pb-2 pr-4">P&L %</th>
<th className="pb-2">Hold (hrs)</th>
</tr>
</thead>
<tbody>
{report.position_performance.positions.map((p, i) => (
<tr key={i} className="border-b border-surface-800 text-gray-200">
<td className="py-1.5 pr-4 font-mono font-semibold text-brand-300">{p.ticker}</td>
<td className="py-1.5 pr-4"><StatusBadge status={p.status} /></td>
<td className="py-1.5 pr-4">${p.entry_price.toFixed(2)}</td>
<td className="py-1.5 pr-4">${p.current_or_exit_price.toFixed(2)}</td>
<td className={`py-1.5 pr-4 font-mono ${p.pnl >= 0 ? 'text-green-400' : 'text-red-400'}`}>
{dollar(p.pnl)}
</td>
<td className={`py-1.5 pr-4 ${p.pnl_pct >= 0 ? 'text-green-400' : 'text-red-400'}`}>
{p.pnl_pct.toFixed(2)}%
</td>
<td className="py-1.5 text-gray-400">{p.hold_duration_hours.toFixed(1)}</td>
</tr>
))}
</tbody>
</table>
</div>
) : (
<p className="text-sm text-gray-500">No positions during this period.</p>
)}
{report.position_performance.summary && (
<p className="text-xs text-gray-400 mt-3">{report.position_performance.summary}</p>
)}
</Card>
{/* Risk Metrics */}
<Card>
<h2 className="text-sm font-medium text-gray-300 mb-3">Risk Metrics</h2>
<div className="grid grid-cols-2 md:grid-cols-3 gap-3">
<MetricCard label="Risk Tier" value={report.risk_metrics.current_risk_tier} />
<MetricCard label="Portfolio Heat" value={pct(report.risk_metrics.portfolio_heat)} />
<MetricCard label="Max Drawdown" value={pct(report.risk_metrics.max_drawdown)} />
<MetricCard label="Current Drawdown" value={pct(report.risk_metrics.current_drawdown_pct)} />
<MetricCard label="Reserve Pool" value={dollar(report.risk_metrics.reserve_pool_balance)} />
<MetricCard label="Circuit Breaker Events" value={String(report.risk_metrics.circuit_breaker_event_count)} />
</div>
{report.risk_metrics.summary && (
<p className="text-xs text-gray-400 mt-3">{report.risk_metrics.summary}</p>
)}
</Card>
{/* Model Quality */}
<Card>
<h2 className="text-sm font-medium text-gray-300 mb-3">Model Quality</h2>
{report.model_quality.windows.length > 0 ? (
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="text-left text-xs text-gray-400 border-b border-surface-700">
<th className="pb-2 pr-4">Window</th>
<th className="pb-2 pr-4">Win Rate</th>
<th className="pb-2 pr-4">Dir. Accuracy</th>
<th className="pb-2 pr-4">IC</th>
<th className="pb-2 pr-4">ECE</th>
<th className="pb-2">Brier</th>
</tr>
</thead>
<tbody>
{report.model_quality.windows.map((w, i) => (
<tr key={i} className="border-b border-surface-800 text-gray-200">
<td className="py-1.5 pr-4 font-medium">{w.lookback}</td>
<td className="py-1.5 pr-4">{w.win_rate != null ? pct(w.win_rate) : '—'}</td>
<td className="py-1.5 pr-4">{w.directional_accuracy != null ? pct(w.directional_accuracy) : '—'}</td>
<td className="py-1.5 pr-4">{w.information_coefficient != null ? w.information_coefficient.toFixed(4) : '—'}</td>
<td className="py-1.5 pr-4">{w.calibration_error != null ? w.calibration_error.toFixed(4) : '—'}</td>
<td className="py-1.5">{w.brier_score != null ? w.brier_score.toFixed(4) : '—'}</td>
</tr>
))}
</tbody>
</table>
</div>
) : (
<p className="text-sm text-gray-500">No model quality data available.</p>
)}
{report.model_quality.summary && (
<p className="text-xs text-gray-400 mt-3">{report.model_quality.summary}</p>
)}
</Card>
</div>
);
}
+79
View File
@@ -0,0 +1,79 @@
import { useState } from 'react';
import { useNavigate } from '@tanstack/react-router';
import { useReports } from '../api/hooks';
import { DataTable, type Column } from '../components/DataTable';
import { StatusBadge, LoadingSpinner } from '../components/ui';
import type { ReportListItem } from '../api/hooks';
export function ReportsPage() {
const navigate = useNavigate();
const [reportType, setReportType] = useState('');
const { data, isLoading } = useReports({
report_type: reportType || undefined,
limit: 50,
});
const columns: Column<ReportListItem>[] = [
{
key: 'report_type',
header: 'Type',
render: (r) => (
<span className="inline-flex items-center rounded px-2 py-0.5 text-xs font-medium bg-surface-700 text-brand-300 capitalize">
{r.report_type}
</span>
),
},
{
key: 'period_start',
header: 'Period',
render: (r) =>
r.period_start === r.period_end
? r.period_start
: `${r.period_start}${r.period_end}`,
},
{
key: 'validation_status',
header: 'Validation',
render: (r) => <StatusBadge status={r.validation_status} />,
},
{
key: 'generated_at',
header: 'Generated',
render: (r) => (
<span className="text-xs text-gray-400">
{new Date(r.generated_at).toLocaleString()}
</span>
),
},
];
if (isLoading) return <LoadingSpinner />;
return (
<div>
<div className="mb-4 flex items-center justify-between">
<h1 className="text-xl font-semibold text-gray-100">
Trading Reports
</h1>
<select
value={reportType}
onChange={(e) => setReportType(e.target.value)}
className="rounded border border-surface-600 bg-surface-800 px-3 py-1.5 text-sm text-gray-200 focus:border-brand-500 focus:outline-none"
aria-label="Filter by report type"
>
<option value="">All Types</option>
<option value="daily">Daily</option>
<option value="weekly">Weekly</option>
</select>
</div>
<DataTable<ReportListItem>
data={data ?? []}
columns={columns}
keyField="id"
onRowClick={(row) =>
navigate({ to: '/reports/$id', params: { id: row.id } })
}
/>
</div>
);
}
+15
View File
@@ -30,6 +30,8 @@ import { HomePage } from './pages/Home';
import { GlobalEventsPage } from './pages/GlobalEvents'; import { GlobalEventsPage } from './pages/GlobalEvents';
import { GlobalEventDetailPage } from './pages/GlobalEventDetail'; import { GlobalEventDetailPage } from './pages/GlobalEventDetail';
import { AgentsPage } from './pages/Agents'; import { AgentsPage } from './pages/Agents';
import { ReportsPage } from './pages/Reports';
import { ReportDetailPage } from './pages/ReportDetail';
// Root route wraps everything in the app shell layout // Root route wraps everything in the app shell layout
const rootRoute = createRootRoute({ const rootRoute = createRootRoute({
@@ -167,6 +169,17 @@ const agentsRoute = createRoute({
component: AgentsPage, component: AgentsPage,
}); });
const reportsRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/reports',
component: ReportsPage,
});
const reportDetailRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/reports/$id',
component: ReportDetailPage,
});
const routeTree = rootRoute.addChildren([ const routeTree = rootRoute.addChildren([
indexRoute, indexRoute,
companiesRoute, companiesRoute,
@@ -192,6 +205,8 @@ const routeTree = rootRoute.addChildren([
globalEventsRoute, globalEventsRoute,
globalEventDetailRoute, globalEventDetailRoute,
agentsRoute, agentsRoute,
reportsRoute,
reportDetailRoute,
]); ]);
export const router = createRouter({ routeTree }); export const router = createRouter({ routeTree });