feat(web-dashboard): trading, risk, ML, performance, backtesting, config dashboards

Phase 5: Trading dashboard with candlestick chart (TradingView lightweight-charts v5),
order book, positions table, order form, and orders table with cancel support.

Phase 6: Risk dashboard with radial gauges (VaR, position utilization, drawdown),
drawdown chart, WebSocket-fed alerts panel, and emergency stop controls.

Phase 7: ML dashboard with model cards (DQN/PPO/TFT/Mamba2), ensemble voting
panel, regime state indicator, and training job progress tracking.

Phase 8: Performance dashboard with metric cards and P&L charts, backtesting
dashboard with strategy form and trade list, config dashboard with tabbed
category editor and audit log.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
jgrusewski
2026-02-22 00:14:05 +01:00
parent 4f184bf9a5
commit 6c118d1f53
19 changed files with 1530 additions and 72 deletions

View File

@@ -0,0 +1,83 @@
import { useEffect, useRef } from 'react';
import { createChart, CandlestickSeries, type IChartApi, type CandlestickData, type Time } from 'lightweight-charts';
interface Props {
symbol: string;
data?: CandlestickData<Time>[];
}
export function CandlestickChart({ symbol, data = [] }: Props) {
const containerRef = useRef<HTMLDivElement>(null);
const chartRef = useRef<IChartApi | null>(null);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const seriesRef = useRef<any>(null);
// Initialize chart
useEffect(() => {
if (!containerRef.current) return;
const chart = createChart(containerRef.current, {
layout: {
background: { color: '#1e293b' },
textColor: '#94a3b8',
},
grid: {
vertLines: { color: '#334155' },
horzLines: { color: '#334155' },
},
crosshair: {
mode: 0, // Normal
},
timeScale: {
borderColor: '#334155',
timeVisible: true,
},
rightPriceScale: {
borderColor: '#334155',
},
});
const series = chart.addSeries(CandlestickSeries, {
upColor: '#22c55e',
downColor: '#ef4444',
borderUpColor: '#22c55e',
borderDownColor: '#ef4444',
wickUpColor: '#22c55e',
wickDownColor: '#ef4444',
});
chartRef.current = chart;
seriesRef.current = series;
// Handle resize
const observer = new ResizeObserver((entries) => {
const { width, height } = entries[0].contentRect;
chart.applyOptions({ width, height });
});
observer.observe(containerRef.current);
return () => {
observer.disconnect();
chart.remove();
chartRef.current = null;
seriesRef.current = null;
};
}, []);
// Update data
useEffect(() => {
if (seriesRef.current && data.length > 0) {
seriesRef.current.setData(data);
}
}, [data]);
return (
<div className="flex flex-col h-full">
<div className="flex items-center justify-between px-3 py-1.5 border-b border-[var(--color-border)]">
<span className="text-sm font-medium">{symbol}</span>
<span className="text-xs text-[var(--color-text-secondary)]">1m</span>
</div>
<div ref={containerRef} className="flex-1 min-h-0" />
</div>
);
}

View File

@@ -0,0 +1,30 @@
interface Props {
label: string;
value: string | number;
change?: number;
prefix?: string;
suffix?: string;
}
export function MetricCard({ label, value, change, prefix = '', suffix = '' }: Props) {
return (
<div className="p-3">
<div className="text-xs text-[var(--color-text-secondary)] mb-1">{label}</div>
<div className="text-lg font-bold">
{prefix}
{typeof value === 'number' ? value.toLocaleString() : value}
{suffix}
</div>
{change !== undefined && (
<div
className={`text-xs mt-0.5 ${
change >= 0 ? 'text-[var(--color-green)]' : 'text-[var(--color-red)]'
}`}
>
{change >= 0 ? '+' : ''}
{change.toFixed(2)}%
</div>
)}
</div>
);
}

View File

@@ -0,0 +1,61 @@
import { AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts';
interface PnLPoint {
date: string;
pnl: number;
}
interface Props {
data?: PnLPoint[];
title?: string;
}
export function PnLChart({ data = [], title = 'P&L' }: Props) {
return (
<div className="flex flex-col h-full">
<div className="px-3 py-1.5 border-b border-[var(--color-border)]">
<span className="text-sm font-medium">{title}</span>
</div>
<div className="flex-1 min-h-0 p-2">
{data.length > 0 ? (
<ResponsiveContainer width="100%" height="100%">
<AreaChart data={data}>
<CartesianGrid strokeDasharray="3 3" stroke="#334155" />
<XAxis dataKey="date" tick={{ fontSize: 10, fill: '#94a3b8' }} />
<YAxis
tick={{ fontSize: 10, fill: '#94a3b8' }}
tickFormatter={(v: number) => `$${v.toLocaleString()}`}
/>
<Tooltip
contentStyle={{
backgroundColor: '#1e293b',
border: '1px solid #334155',
borderRadius: 4,
fontSize: 12,
}}
formatter={(value: number | string | undefined) => [`$${Number(value ?? 0).toLocaleString()}`, 'P&L']}
/>
<defs>
<linearGradient id="pnlGradient" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#3b82f6" stopOpacity={0.3} />
<stop offset="95%" stopColor="#3b82f6" stopOpacity={0} />
</linearGradient>
</defs>
<Area
type="monotone"
dataKey="pnl"
stroke="#3b82f6"
fill="url(#pnlGradient)"
strokeWidth={2}
/>
</AreaChart>
</ResponsiveContainer>
) : (
<div className="flex items-center justify-center h-full text-sm text-[var(--color-text-secondary)]">
No P&L data
</div>
)}
</div>
</div>
);
}

View File

@@ -0,0 +1,103 @@
import type { MlPrediction } from '../../lib/types';
interface Props {
predictions?: MlPrediction[];
}
const MODELS = ['DQN', 'PPO', 'TFT', 'Mamba2'];
export function EnsemblePanel({ predictions = [] }: Props) {
// Count votes
const votes = { buy: 0, sell: 0, hold: 0 };
for (const p of predictions) {
if (p.signal in votes) {
votes[p.signal]++;
}
}
const totalVotes = votes.buy + votes.sell + votes.hold;
const consensus =
totalVotes === 0
? 'hold'
: votes.buy > votes.sell && votes.buy > votes.hold
? 'buy'
: votes.sell > votes.buy && votes.sell > votes.hold
? 'sell'
: 'hold';
const consensusColor = {
buy: 'text-[var(--color-green)]',
sell: 'text-[var(--color-red)]',
hold: 'text-[var(--color-yellow)]',
}[consensus];
return (
<div>
<div className="px-3 py-1.5 border-b border-[var(--color-border)]">
<span className="text-sm font-medium">Ensemble Voting</span>
</div>
<div className="p-3 space-y-3">
{/* Consensus */}
<div className="text-center py-2">
<div className="text-xs text-[var(--color-text-secondary)]">Consensus Signal</div>
<div className={`text-2xl font-bold ${consensusColor}`}>
{consensus.toUpperCase()}
</div>
<div className="text-xs text-[var(--color-text-secondary)]">
{votes.buy}B / {votes.sell}S / {votes.hold}H
</div>
</div>
{/* Vote bar */}
{totalVotes > 0 && (
<div className="flex h-3 rounded-full overflow-hidden">
{votes.buy > 0 && (
<div
className="bg-[var(--color-green)]"
style={{ width: `${(votes.buy / totalVotes) * 100}%` }}
/>
)}
{votes.hold > 0 && (
<div
className="bg-[var(--color-yellow)]"
style={{ width: `${(votes.hold / totalVotes) * 100}%` }}
/>
)}
{votes.sell > 0 && (
<div
className="bg-[var(--color-red)]"
style={{ width: `${(votes.sell / totalVotes) * 100}%` }}
/>
)}
</div>
)}
{/* Per-model status */}
<div className="space-y-1">
{MODELS.map((model) => {
const pred = predictions.find((p) => p.model === model);
return (
<div key={model} className="flex justify-between text-xs">
<span className="text-[var(--color-text-secondary)]">{model}</span>
<span
className={
pred
? {
buy: 'text-[var(--color-green)]',
sell: 'text-[var(--color-red)]',
hold: 'text-[var(--color-yellow)]',
}[pred.signal]
: 'text-[var(--color-text-secondary)]'
}
>
{pred ? pred.signal.toUpperCase() : 'N/A'}
</span>
</div>
);
})}
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,59 @@
interface Props {
model: string;
signal?: 'buy' | 'sell' | 'hold';
confidence?: number;
predictedReturn?: number;
}
export function ModelCard({ model, signal, confidence, predictedReturn }: Props) {
const signalColor = {
buy: 'text-[var(--color-green)]',
sell: 'text-[var(--color-red)]',
hold: 'text-[var(--color-yellow)]',
}[signal ?? 'hold'];
const signalBg = {
buy: 'bg-green-500/10',
sell: 'bg-red-500/10',
hold: 'bg-yellow-500/10',
}[signal ?? 'hold'];
return (
<div className="p-3 space-y-2">
<div className="flex items-center justify-between">
<span className="text-sm font-medium">{model}</span>
<span className={`text-xs px-2 py-0.5 rounded ${signalColor} ${signalBg}`}>
{signal?.toUpperCase() ?? 'N/A'}
</span>
</div>
<div className="space-y-1">
<div className="flex justify-between text-xs">
<span className="text-[var(--color-text-secondary)]">Confidence</span>
<span>{confidence !== undefined ? `${(confidence * 100).toFixed(1)}%` : '--'}</span>
</div>
<div className="w-full bg-[var(--color-bg-primary)] rounded-full h-1.5">
<div
className="h-1.5 rounded-full bg-[var(--color-accent)]"
style={{ width: `${(confidence ?? 0) * 100}%` }}
/>
</div>
</div>
<div className="flex justify-between text-xs">
<span className="text-[var(--color-text-secondary)]">Predicted Return</span>
<span
className={
(predictedReturn ?? 0) >= 0
? 'text-[var(--color-green)]'
: 'text-[var(--color-red)]'
}
>
{predictedReturn !== undefined
? `${predictedReturn >= 0 ? '+' : ''}${(predictedReturn * 100).toFixed(2)}%`
: '--'}
</span>
</div>
</div>
);
}

View File

@@ -0,0 +1,83 @@
import type { TrainingJob } from '../../lib/types';
interface Props {
jobs?: TrainingJob[];
loading?: boolean;
}
export function TrainingProgress({ jobs = [], loading }: Props) {
return (
<div>
<div className="flex items-center justify-between px-3 py-1.5 border-b border-[var(--color-border)]">
<span className="text-sm font-medium">Training Jobs</span>
<span className="text-xs text-[var(--color-text-secondary)]">
{jobs.filter((j) => j.status === 'running').length} running
</span>
</div>
<div className="divide-y divide-[var(--color-border)]">
{loading && (
<div className="p-4 text-sm text-[var(--color-text-secondary)] text-center">
Loading...
</div>
)}
{!loading && jobs.length === 0 && (
<div className="p-4 text-sm text-[var(--color-text-secondary)] text-center">
No training jobs
</div>
)}
{jobs.map((job) => (
<div key={job.job_id} className="px-3 py-2 space-y-1">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<span className="text-xs font-medium">{job.model_type}</span>
<span className="text-xs font-mono text-[var(--color-text-secondary)]">
{job.job_id.slice(0, 8)}
</span>
</div>
<StatusBadge status={job.status} />
</div>
{/* Progress bar */}
<div className="w-full bg-[var(--color-bg-primary)] rounded-full h-1.5">
<div
className={`h-1.5 rounded-full transition-all ${
job.status === 'failed'
? 'bg-[var(--color-red)]'
: job.status === 'completed'
? 'bg-[var(--color-green)]'
: 'bg-[var(--color-accent)]'
}`}
style={{ width: `${job.progress * 100}%` }}
/>
</div>
<div className="flex justify-between text-xs text-[var(--color-text-secondary)]">
<span>
Epoch {job.epoch}/{job.total_epochs}
</span>
<span>Loss: {job.loss.toFixed(4)}</span>
<span>{(job.progress * 100).toFixed(0)}%</span>
</div>
</div>
))}
</div>
</div>
);
}
function StatusBadge({ status }: { status: TrainingJob['status'] }) {
const styles = {
queued: 'text-[var(--color-text-secondary)] bg-slate-500/10',
running: 'text-[var(--color-accent)] bg-blue-500/10',
completed: 'text-[var(--color-green)] bg-green-500/10',
failed: 'text-[var(--color-red)] bg-red-500/10',
stopped: 'text-[var(--color-yellow)] bg-yellow-500/10',
}[status];
return (
<span className={`text-xs px-2 py-0.5 rounded ${styles}`}>
{status}
</span>
);
}

View File

@@ -0,0 +1,54 @@
import { AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts';
interface DrawdownPoint {
time: string;
drawdown: number;
}
interface Props {
data?: DrawdownPoint[];
}
export function DrawdownChart({ data = [] }: Props) {
return (
<div className="flex flex-col h-full">
<div className="px-3 py-1.5 border-b border-[var(--color-border)]">
<span className="text-sm font-medium">Portfolio Drawdown</span>
</div>
<div className="flex-1 min-h-0 p-2">
{data.length > 0 ? (
<ResponsiveContainer width="100%" height="100%">
<AreaChart data={data}>
<CartesianGrid strokeDasharray="3 3" stroke="#334155" />
<XAxis dataKey="time" tick={{ fontSize: 10, fill: '#94a3b8' }} />
<YAxis
tick={{ fontSize: 10, fill: '#94a3b8' }}
tickFormatter={(v: number) => `${v.toFixed(1)}%`}
domain={['dataMin', 0]}
/>
<Tooltip
contentStyle={{
backgroundColor: '#1e293b',
border: '1px solid #334155',
borderRadius: 4,
fontSize: 12,
}}
/>
<Area
type="monotone"
dataKey="drawdown"
stroke="#ef4444"
fill="#ef444433"
strokeWidth={2}
/>
</AreaChart>
</ResponsiveContainer>
) : (
<div className="flex items-center justify-center h-full text-sm text-[var(--color-text-secondary)]">
No drawdown data
</div>
)}
</div>
</div>
);
}

View File

@@ -0,0 +1,91 @@
import { useState } from 'react';
import { useApiPost } from '../../hooks/useApi';
export function EmergencyControls() {
const [confirmText, setConfirmText] = useState('');
const [showConfirm, setShowConfirm] = useState(false);
const emergencyStop = useApiPost('/risk/emergency-stop');
function handleEmergencyStop() {
if (confirmText === 'STOP') {
emergencyStop.mutate({});
setShowConfirm(false);
setConfirmText('');
emergencyStop.reset();
}
}
return (
<div>
<div className="px-3 py-1.5 border-b border-[var(--color-border)]">
<span className="text-sm font-medium">Emergency Controls</span>
</div>
<div className="p-3 space-y-3">
{!showConfirm ? (
<button
onClick={() => setShowConfirm(true)}
className="w-full py-3 text-sm font-bold rounded bg-[var(--color-red)] text-white hover:bg-red-600 transition-colors"
>
EMERGENCY STOP
</button>
) : (
<div className="space-y-2">
<p className="text-xs text-[var(--color-red)]">
Type STOP to confirm emergency shutdown of all positions.
</p>
<input
type="text"
value={confirmText}
onChange={(e) => setConfirmText(e.target.value)}
placeholder="Type STOP"
className="w-full px-2 py-1.5 text-sm rounded border border-[var(--color-red)] bg-[var(--color-bg-primary)] text-[var(--color-text-primary)] outline-none"
autoFocus
/>
<div className="grid grid-cols-2 gap-2">
<button
onClick={() => {
setShowConfirm(false);
setConfirmText('');
}}
className="py-1.5 text-xs rounded border border-[var(--color-border)] text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)]"
>
Cancel
</button>
<button
onClick={handleEmergencyStop}
disabled={confirmText !== 'STOP' || emergencyStop.isPending}
className="py-1.5 text-xs rounded bg-[var(--color-red)] text-white disabled:opacity-50"
>
{emergencyStop.isPending ? 'Stopping...' : 'Confirm STOP'}
</button>
</div>
</div>
)}
{emergencyStop.isSuccess && (
<p className="text-xs text-[var(--color-green)]">
Emergency stop executed successfully.
</p>
)}
{emergencyStop.isError && (
<p className="text-xs text-[var(--color-red)]">
{emergencyStop.error.message}
</p>
)}
<div className="pt-2 border-t border-[var(--color-border)]">
<div className="text-xs text-[var(--color-text-secondary)] space-y-1">
<p>Emergency stop will:</p>
<ul className="list-disc list-inside space-y-0.5">
<li>Cancel all pending orders</li>
<li>Close all open positions at market</li>
<li>Disable new order submission</li>
</ul>
</div>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,52 @@
import { RadialBarChart, RadialBar, PolarAngleAxis } from 'recharts';
interface Props {
label: string;
value: number;
max?: number;
thresholds?: { warn: number; danger: number };
}
export function RiskGauge({
label,
value,
max = 100,
thresholds = { warn: 60, danger: 80 },
}: Props) {
const pct = Math.min((value / max) * 100, 100);
const color =
pct >= thresholds.danger
? 'var(--color-red)'
: pct >= thresholds.warn
? 'var(--color-yellow)'
: 'var(--color-green)';
const data = [{ value: pct, fill: color }];
return (
<div className="flex flex-col items-center justify-center h-full p-2">
<RadialBarChart
width={140}
height={140}
cx={70}
cy={70}
innerRadius={45}
outerRadius={65}
barSize={12}
data={data}
startAngle={180}
endAngle={0}
>
<PolarAngleAxis type="number" domain={[0, 100]} tick={false} />
<RadialBar dataKey="value" cornerRadius={6} background={{ fill: '#334155' }} />
</RadialBarChart>
<div className="text-center -mt-8">
<div className="text-lg font-bold" style={{ color }}>
{pct.toFixed(1)}%
</div>
<div className="text-xs text-[var(--color-text-secondary)]">{label}</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,97 @@
interface OrderBookLevel {
price: number;
size: number;
}
interface Props {
bids?: OrderBookLevel[];
asks?: OrderBookLevel[];
spread?: number;
}
export function OrderBook({ bids = [], asks = [], spread }: Props) {
const maxSize = Math.max(
...bids.map((b) => b.size),
...asks.map((a) => a.size),
1,
);
return (
<div className="flex flex-col h-full">
<div className="flex items-center justify-between px-3 py-1.5 border-b border-[var(--color-border)]">
<span className="text-sm font-medium">Order Book</span>
{spread !== undefined && (
<span className="text-xs text-[var(--color-text-secondary)]">
Spread: {spread.toFixed(2)}
</span>
)}
</div>
<div className="flex-1 overflow-auto">
{/* Header */}
<div className="grid grid-cols-3 px-3 py-1 text-xs text-[var(--color-text-secondary)] border-b border-[var(--color-border)]">
<span>Price</span>
<span className="text-right">Size</span>
<span className="text-right">Total</span>
</div>
{/* Asks (reversed so lowest ask is at bottom) */}
<div className="flex flex-col-reverse">
{asks.slice(0, 10).map((level, i) => {
const pct = (level.size / maxSize) * 100;
return (
<div
key={`ask-${level.price}`}
className="relative grid grid-cols-3 px-3 py-0.5 text-xs"
>
<div
className="absolute inset-0 bg-red-500/10"
style={{ width: `${pct}%`, right: 0, left: 'auto' }}
/>
<span className="relative text-[var(--color-red)]">
{level.price.toFixed(2)}
</span>
<span className="relative text-right">{level.size}</span>
<span className="relative text-right text-[var(--color-text-secondary)]">
{asks
.slice(0, i + 1)
.reduce((s, l) => s + l.size, 0)}
</span>
</div>
);
})}
</div>
{/* Spread indicator */}
<div className="px-3 py-1 text-center text-xs font-medium border-y border-[var(--color-border)] bg-[var(--color-bg-primary)]">
{spread !== undefined ? spread.toFixed(2) : '--'}
</div>
{/* Bids */}
{bids.slice(0, 10).map((level, i) => {
const pct = (level.size / maxSize) * 100;
return (
<div
key={`bid-${level.price}`}
className="relative grid grid-cols-3 px-3 py-0.5 text-xs"
>
<div
className="absolute inset-0 bg-green-500/10"
style={{ width: `${pct}%`, right: 0, left: 'auto' }}
/>
<span className="relative text-[var(--color-green)]">
{level.price.toFixed(2)}
</span>
<span className="relative text-right">{level.size}</span>
<span className="relative text-right text-[var(--color-text-secondary)]">
{bids
.slice(0, i + 1)
.reduce((s, l) => s + l.size, 0)}
</span>
</div>
);
})}
</div>
</div>
);
}

View File

@@ -0,0 +1,149 @@
import { useState, type FormEvent } from 'react';
import { useApiPost } from '../../hooks/useApi';
interface OrderPayload {
symbol: string;
side: string;
order_type: string;
quantity: number;
price?: number;
}
export function OrderForm() {
const [symbol, setSymbol] = useState('ES.FUT');
const [side, setSide] = useState<'buy' | 'sell'>('buy');
const [orderType, setOrderType] = useState<'market' | 'limit'>('market');
const [quantity, setQuantity] = useState('1');
const [price, setPrice] = useState('');
const submitOrder = useApiPost<unknown, OrderPayload>('/trading/orders');
function handleSubmit(e: FormEvent) {
e.preventDefault();
const qty = parseFloat(quantity);
if (isNaN(qty) || qty <= 0) return;
const payload: OrderPayload = {
symbol,
side,
order_type: orderType,
quantity: qty,
};
if (orderType === 'limit') {
const p = parseFloat(price);
if (isNaN(p) || p <= 0) return;
payload.price = p;
}
submitOrder.mutate(payload);
}
return (
<div>
<div className="px-3 py-1.5 border-b border-[var(--color-border)]">
<span className="text-sm font-medium">New Order</span>
</div>
<form onSubmit={handleSubmit} className="p-3 space-y-3">
{/* Symbol */}
<div>
<label className="block text-xs text-[var(--color-text-secondary)] mb-1">Symbol</label>
<input
type="text"
value={symbol}
onChange={(e) => setSymbol(e.target.value)}
className="w-full px-2 py-1.5 text-sm rounded border border-[var(--color-border)] bg-[var(--color-bg-primary)] text-[var(--color-text-primary)] focus:border-[var(--color-accent)] outline-none"
/>
</div>
{/* Side */}
<div>
<label className="block text-xs text-[var(--color-text-secondary)] mb-1">Side</label>
<div className="grid grid-cols-2 gap-1">
<button
type="button"
onClick={() => setSide('buy')}
className={`py-1.5 text-sm rounded font-medium transition-colors ${
side === 'buy'
? 'bg-[var(--color-green)] text-white'
: 'bg-[var(--color-bg-primary)] text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)]'
}`}
>
BUY
</button>
<button
type="button"
onClick={() => setSide('sell')}
className={`py-1.5 text-sm rounded font-medium transition-colors ${
side === 'sell'
? 'bg-[var(--color-red)] text-white'
: 'bg-[var(--color-bg-primary)] text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)]'
}`}
>
SELL
</button>
</div>
</div>
{/* Type */}
<div>
<label className="block text-xs text-[var(--color-text-secondary)] mb-1">Type</label>
<select
value={orderType}
onChange={(e) => setOrderType(e.target.value as 'market' | 'limit')}
className="w-full px-2 py-1.5 text-sm rounded border border-[var(--color-border)] bg-[var(--color-bg-primary)] text-[var(--color-text-primary)] outline-none"
>
<option value="market">Market</option>
<option value="limit">Limit</option>
</select>
</div>
{/* Quantity */}
<div>
<label className="block text-xs text-[var(--color-text-secondary)] mb-1">Quantity</label>
<input
type="number"
min="0"
step="1"
value={quantity}
onChange={(e) => setQuantity(e.target.value)}
className="w-full px-2 py-1.5 text-sm rounded border border-[var(--color-border)] bg-[var(--color-bg-primary)] text-[var(--color-text-primary)] outline-none"
/>
</div>
{/* Price (limit only) */}
{orderType === 'limit' && (
<div>
<label className="block text-xs text-[var(--color-text-secondary)] mb-1">Price</label>
<input
type="number"
min="0"
step="0.01"
value={price}
onChange={(e) => setPrice(e.target.value)}
className="w-full px-2 py-1.5 text-sm rounded border border-[var(--color-border)] bg-[var(--color-bg-primary)] text-[var(--color-text-primary)] outline-none"
/>
</div>
)}
{/* Submit */}
<button
type="submit"
disabled={submitOrder.isPending}
className={`w-full py-2 text-sm font-medium rounded transition-colors ${
side === 'buy'
? 'bg-[var(--color-green)] hover:bg-green-600'
: 'bg-[var(--color-red)] hover:bg-red-600'
} text-white disabled:opacity-50`}
>
{submitOrder.isPending ? 'Submitting...' : `${side.toUpperCase()} ${symbol}`}
</button>
{submitOrder.isError && (
<p className="text-xs text-[var(--color-red)]">{submitOrder.error.message}</p>
)}
</form>
</div>
);
}

View File

@@ -0,0 +1,107 @@
import type { Order } from '../../lib/types';
import { useApiDelete } from '../../hooks/useApi';
interface Props {
orders?: Order[];
loading?: boolean;
}
export function OrdersTable({ orders = [], loading }: Props) {
return (
<div>
<div className="flex items-center justify-between px-3 py-1.5 border-b border-[var(--color-border)]">
<span className="text-sm font-medium">Orders</span>
<span className="text-xs text-[var(--color-text-secondary)]">
{orders.filter((o) => o.status === 'open' || o.status === 'pending').length} active
</span>
</div>
<div className="overflow-x-auto">
<table className="w-full text-xs">
<thead>
<tr className="text-[var(--color-text-secondary)] border-b border-[var(--color-border)]">
<th className="text-left px-3 py-2 font-medium">ID</th>
<th className="text-left px-3 py-2 font-medium">Symbol</th>
<th className="text-left px-3 py-2 font-medium">Side</th>
<th className="text-left px-3 py-2 font-medium">Type</th>
<th className="text-right px-3 py-2 font-medium">Qty</th>
<th className="text-right px-3 py-2 font-medium">Price</th>
<th className="text-left px-3 py-2 font-medium">Status</th>
<th className="text-right px-3 py-2 font-medium">Action</th>
</tr>
</thead>
<tbody>
{loading && (
<tr>
<td colSpan={8} className="text-center py-8 text-[var(--color-text-secondary)]">
Loading...
</td>
</tr>
)}
{!loading && orders.length === 0 && (
<tr>
<td colSpan={8} className="text-center py-8 text-[var(--color-text-secondary)]">
No orders
</td>
</tr>
)}
{orders.map((order) => (
<OrderRow key={order.order_id} order={order} />
))}
</tbody>
</table>
</div>
</div>
);
}
function OrderRow({ order }: { order: Order }) {
const cancelOrder = useApiDelete(`/trading/orders/${order.order_id}`);
const canCancel = order.status === 'open' || order.status === 'pending';
const statusColor = {
pending: 'text-[var(--color-yellow)]',
open: 'text-[var(--color-accent)]',
partially_filled: 'text-[var(--color-yellow)]',
filled: 'text-[var(--color-green)]',
cancelled: 'text-[var(--color-text-secondary)]',
rejected: 'text-[var(--color-red)]',
}[order.status];
return (
<tr className="border-b border-[var(--color-border)] hover:bg-[var(--color-bg-primary)]">
<td className="px-3 py-2 font-mono">{order.order_id.slice(0, 8)}</td>
<td className="px-3 py-2">{order.symbol}</td>
<td className="px-3 py-2">
<span
className={
order.side === 'buy'
? 'text-[var(--color-green)]'
: 'text-[var(--color-red)]'
}
>
{order.side.toUpperCase()}
</span>
</td>
<td className="px-3 py-2 capitalize">{order.order_type}</td>
<td className="px-3 py-2 text-right">
{order.filled_quantity}/{order.quantity}
</td>
<td className="px-3 py-2 text-right">
{order.price ? order.price.toFixed(2) : 'MKT'}
</td>
<td className={`px-3 py-2 capitalize ${statusColor}`}>{order.status}</td>
<td className="px-3 py-2 text-right">
{canCancel && (
<button
onClick={() => cancelOrder.mutate()}
disabled={cancelOrder.isPending}
className="px-2 py-0.5 text-xs rounded bg-[var(--color-red)]/20 text-[var(--color-red)] hover:bg-[var(--color-red)]/30 disabled:opacity-50"
>
Cancel
</button>
)}
</td>
</tr>
);
}

View File

@@ -0,0 +1,82 @@
import type { Position } from '../../lib/types';
interface Props {
positions?: Position[];
loading?: boolean;
}
export function PositionsTable({ positions = [], loading }: Props) {
return (
<div>
<div className="flex items-center justify-between px-3 py-1.5 border-b border-[var(--color-border)]">
<span className="text-sm font-medium">Positions</span>
<span className="text-xs text-[var(--color-text-secondary)]">
{positions.length} active
</span>
</div>
<div className="overflow-x-auto">
<table className="w-full text-xs">
<thead>
<tr className="text-[var(--color-text-secondary)] border-b border-[var(--color-border)]">
<th className="text-left px-3 py-2 font-medium">Symbol</th>
<th className="text-left px-3 py-2 font-medium">Side</th>
<th className="text-right px-3 py-2 font-medium">Qty</th>
<th className="text-right px-3 py-2 font-medium">Entry</th>
<th className="text-right px-3 py-2 font-medium">Market</th>
<th className="text-right px-3 py-2 font-medium">P&L</th>
</tr>
</thead>
<tbody>
{loading && (
<tr>
<td colSpan={6} className="text-center py-8 text-[var(--color-text-secondary)]">
Loading...
</td>
</tr>
)}
{!loading && positions.length === 0 && (
<tr>
<td colSpan={6} className="text-center py-8 text-[var(--color-text-secondary)]">
No open positions
</td>
</tr>
)}
{positions.map((pos) => (
<tr
key={`${pos.symbol}-${pos.side}`}
className="border-b border-[var(--color-border)] hover:bg-[var(--color-bg-primary)]"
>
<td className="px-3 py-2 font-medium">{pos.symbol}</td>
<td className="px-3 py-2">
<span
className={
pos.side === 'long'
? 'text-[var(--color-green)]'
: 'text-[var(--color-red)]'
}
>
{pos.side.toUpperCase()}
</span>
</td>
<td className="px-3 py-2 text-right">{pos.quantity}</td>
<td className="px-3 py-2 text-right">{pos.entry_price.toFixed(2)}</td>
<td className="px-3 py-2 text-right">{pos.current_price.toFixed(2)}</td>
<td
className={`px-3 py-2 text-right font-medium ${
pos.unrealized_pnl >= 0
? 'text-[var(--color-green)]'
: 'text-[var(--color-red)]'
}`}
>
{pos.unrealized_pnl >= 0 ? '+' : ''}
{pos.unrealized_pnl.toFixed(2)}
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
);
}

View File

@@ -1,20 +1,156 @@
import { useState, type FormEvent } from 'react';
import { PnLChart } from '../components/charts/PnLChart';
import { useApiPost } from '../hooks/useApi';
interface BacktestParams {
strategy: string;
symbol: string;
start_date: string;
end_date: string;
}
export function BacktestingDashboard() {
const [strategy, setStrategy] = useState('ml_ensemble');
const [symbol, setSymbol] = useState('ES.FUT');
const [startDate, setStartDate] = useState('2025-01-01');
const [endDate, setEndDate] = useState('2025-12-31');
const startBacktest = useApiPost<{ backtest_id: string }, BacktestParams>(
'/backtest/start',
);
function handleSubmit(e: FormEvent) {
e.preventDefault();
startBacktest.mutate({
strategy,
symbol,
start_date: startDate,
end_date: endDate,
});
}
return (
<div className="space-y-4">
<h1 className="text-xl font-bold">Backtesting Dashboard</h1>
<div className="grid grid-cols-2 gap-4">
<div className="rounded border border-[var(--color-border)] bg-[var(--color-bg-card)] p-4 text-[var(--color-text-secondary)]">
Start Backtest Form
{/* Start Form */}
<div className="rounded border border-[var(--color-border)] bg-[var(--color-bg-card)]">
<div className="px-3 py-1.5 border-b border-[var(--color-border)]">
<span className="text-sm font-medium">New Backtest</span>
</div>
<form onSubmit={handleSubmit} className="p-3 space-y-3">
<div>
<label className="block text-xs text-[var(--color-text-secondary)] mb-1">
Strategy
</label>
<select
value={strategy}
onChange={(e) => setStrategy(e.target.value)}
className="w-full px-2 py-1.5 text-sm rounded border border-[var(--color-border)] bg-[var(--color-bg-primary)] text-[var(--color-text-primary)] outline-none"
>
<option value="ml_ensemble">ML Ensemble</option>
<option value="dqn">DQN Only</option>
<option value="ppo">PPO Only</option>
<option value="tft">TFT Only</option>
<option value="mamba2">Mamba2 Only</option>
</select>
</div>
<div>
<label className="block text-xs text-[var(--color-text-secondary)] mb-1">
Symbol
</label>
<input
type="text"
value={symbol}
onChange={(e) => setSymbol(e.target.value)}
className="w-full px-2 py-1.5 text-sm rounded border border-[var(--color-border)] bg-[var(--color-bg-primary)] text-[var(--color-text-primary)] outline-none"
/>
</div>
<div className="grid grid-cols-2 gap-2">
<div>
<label className="block text-xs text-[var(--color-text-secondary)] mb-1">
Start Date
</label>
<input
type="date"
value={startDate}
onChange={(e) => setStartDate(e.target.value)}
className="w-full px-2 py-1.5 text-sm rounded border border-[var(--color-border)] bg-[var(--color-bg-primary)] text-[var(--color-text-primary)] outline-none"
/>
</div>
<div>
<label className="block text-xs text-[var(--color-text-secondary)] mb-1">
End Date
</label>
<input
type="date"
value={endDate}
onChange={(e) => setEndDate(e.target.value)}
className="w-full px-2 py-1.5 text-sm rounded border border-[var(--color-border)] bg-[var(--color-bg-primary)] text-[var(--color-text-primary)] outline-none"
/>
</div>
</div>
<button
type="submit"
disabled={startBacktest.isPending}
className="w-full py-2 text-sm font-medium rounded bg-[var(--color-accent)] text-white hover:bg-blue-600 disabled:opacity-50"
>
{startBacktest.isPending ? 'Starting...' : 'Start Backtest'}
</button>
{startBacktest.isSuccess && (
<p className="text-xs text-[var(--color-green)]">
Backtest started: {startBacktest.data.backtest_id}
</p>
)}
{startBacktest.isError && (
<p className="text-xs text-[var(--color-red)]">
{startBacktest.error.message}
</p>
)}
</form>
</div>
<div className="rounded border border-[var(--color-border)] bg-[var(--color-bg-card)] p-4 text-[var(--color-text-secondary)]">
Active Backtests
{/* Active Backtests */}
<div className="rounded border border-[var(--color-border)] bg-[var(--color-bg-card)]">
<div className="px-3 py-1.5 border-b border-[var(--color-border)]">
<span className="text-sm font-medium">Active Backtests</span>
</div>
<div className="p-4 text-sm text-[var(--color-text-secondary)] text-center">
No active backtests
</div>
</div>
</div>
<div className="h-64 rounded border border-[var(--color-border)] bg-[var(--color-bg-card)] flex items-center justify-center text-[var(--color-text-secondary)]">
Equity Curve
{/* Equity Curve */}
<div className="h-64 rounded border border-[var(--color-border)] bg-[var(--color-bg-card)]">
<PnLChart title="Equity Curve" />
</div>
<div className="rounded border border-[var(--color-border)] bg-[var(--color-bg-card)] p-4 text-[var(--color-text-secondary)]">
Trade List
{/* Trade List */}
<div className="rounded border border-[var(--color-border)] bg-[var(--color-bg-card)]">
<div className="px-3 py-1.5 border-b border-[var(--color-border)]">
<span className="text-sm font-medium">Trade List</span>
</div>
<div className="overflow-x-auto">
<table className="w-full text-xs">
<thead>
<tr className="text-[var(--color-text-secondary)] border-b border-[var(--color-border)]">
<th className="text-left px-3 py-2 font-medium">Time</th>
<th className="text-left px-3 py-2 font-medium">Symbol</th>
<th className="text-left px-3 py-2 font-medium">Side</th>
<th className="text-right px-3 py-2 font-medium">Entry</th>
<th className="text-right px-3 py-2 font-medium">Exit</th>
<th className="text-right px-3 py-2 font-medium">P&L</th>
</tr>
</thead>
<tbody>
<tr>
<td colSpan={6} className="text-center py-8 text-[var(--color-text-secondary)]">
Run a backtest to see results
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
);

View File

@@ -1,23 +1,115 @@
import { useState } from 'react';
import { useApiQuery, useApiPut } from '../hooks/useApi';
type ConfigCategory = 'trading' | 'risk' | 'ml' | 'system';
interface ConfigEntry {
key: string;
value: string;
description: string;
}
export function ConfigDashboard() {
const [activeTab, setActiveTab] = useState<ConfigCategory>('trading');
const config = useApiQuery<Record<string, unknown>>(['config'], '/config');
return (
<div className="space-y-4">
<h1 className="text-xl font-bold">Configuration</h1>
<div className="flex gap-2 border-b border-[var(--color-border)] pb-2">
{['Trading', 'Risk', 'ML', 'System'].map((tab) => (
{/* Tabs */}
<div className="flex gap-1 border-b border-[var(--color-border)] pb-0">
{(['trading', 'risk', 'ml', 'system'] as const).map((tab) => (
<button
key={tab}
className="px-3 py-1.5 text-sm rounded text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] hover:bg-[var(--color-bg-primary)]"
onClick={() => setActiveTab(tab)}
className={`px-4 py-2 text-sm capitalize rounded-t transition-colors ${
activeTab === tab
? 'bg-[var(--color-bg-card)] text-[var(--color-text-primary)] border border-[var(--color-border)] border-b-transparent -mb-px'
: 'text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)]'
}`}
>
{tab}
</button>
))}
</div>
<div className="rounded border border-[var(--color-border)] bg-[var(--color-bg-card)] p-4 text-[var(--color-text-secondary)]">
Configuration Form
{/* Config form */}
<div className="rounded border border-[var(--color-border)] bg-[var(--color-bg-card)]">
<ConfigSection category={activeTab} config={config.data} />
</div>
<div className="rounded border border-[var(--color-border)] bg-[var(--color-bg-card)] p-4 text-[var(--color-text-secondary)]">
Audit Log
{/* Audit Log */}
<div className="rounded border border-[var(--color-border)] bg-[var(--color-bg-card)]">
<div className="px-3 py-1.5 border-b border-[var(--color-border)]">
<span className="text-sm font-medium">Change Audit Log</span>
</div>
<div className="p-4 text-sm text-[var(--color-text-secondary)] text-center">
No recent configuration changes
</div>
</div>
</div>
);
}
function ConfigSection({
category,
config: _config,
}: {
category: ConfigCategory;
config?: Record<string, unknown>;
}) {
const configFields: Record<ConfigCategory, ConfigEntry[]> = {
trading: [
{ key: 'max_position_size', value: '1000', description: 'Maximum position size per symbol' },
{ key: 'max_order_value', value: '100000', description: 'Maximum order value in USD' },
{ key: 'default_slippage_bps', value: '5', description: 'Default slippage tolerance (bps)' },
{ key: 'order_timeout_ms', value: '5000', description: 'Order submission timeout' },
],
risk: [
{ key: 'var_limit_pct', value: '2.0', description: 'Portfolio VaR limit (%)' },
{ key: 'max_drawdown_pct', value: '10.0', description: 'Maximum drawdown before halt (%)' },
{ key: 'position_limit_pct', value: '25.0', description: 'Max position as % of portfolio' },
{ key: 'kill_switch_loss', value: '50000', description: 'Kill switch loss threshold (USD)' },
],
ml: [
{ key: 'ensemble_threshold', value: '0.6', description: 'Minimum ensemble confidence to trade' },
{ key: 'retrain_interval_hours', value: '24', description: 'Model retrain interval (hours)' },
{ key: 'prediction_horizon', value: '5', description: 'Prediction horizon (minutes)' },
{ key: 'regime_lookback', value: '100', description: 'Regime detection lookback (bars)' },
],
system: [
{ key: 'log_level', value: 'info', description: 'Logging level (trace/debug/info/warn/error)' },
{ key: 'grpc_timeout_ms', value: '10000', description: 'gRPC client timeout' },
{ key: 'ws_heartbeat_interval', value: '30', description: 'WebSocket heartbeat interval (s)' },
{ key: 'metrics_push_interval', value: '5', description: 'Metrics push interval (s)' },
],
};
const fields = configFields[category];
const updateConfig = useApiPut<unknown, Record<string, string>>('/config');
return (
<div className="divide-y divide-[var(--color-border)]">
{fields.map((field) => (
<div key={field.key} className="flex items-center gap-4 px-3 py-3">
<div className="flex-1">
<div className="text-sm font-mono">{field.key}</div>
<div className="text-xs text-[var(--color-text-secondary)]">
{field.description}
</div>
</div>
<input
type="text"
defaultValue={field.value}
className="w-32 px-2 py-1 text-sm text-right rounded border border-[var(--color-border)] bg-[var(--color-bg-primary)] text-[var(--color-text-primary)] outline-none focus:border-[var(--color-accent)]"
onBlur={(e) => {
if (e.target.value !== field.value) {
updateConfig.mutate({ [field.key]: e.target.value });
}
}}
/>
</div>
))}
</div>
);
}

View File

@@ -1,27 +1,84 @@
import { ModelCard } from '../components/ml/ModelCard';
import { EnsemblePanel } from '../components/ml/EnsemblePanel';
import { TrainingProgress } from '../components/ml/TrainingProgress';
import { useApiQuery } from '../hooks/useApi';
import type { MlPrediction, TrainingJob } from '../lib/types';
const MODELS = ['DQN', 'PPO', 'TFT', 'Mamba2'] as const;
export function MLDashboard() {
const predictions = useApiQuery<MlPrediction[]>(
['ml-predictions'],
'/ml/predictions',
{ refetchInterval: 5000 },
);
const trainingJobs = useApiQuery<TrainingJob[]>(
['training-jobs'],
'/training/jobs',
{ refetchInterval: 5000 },
);
const preds = predictions.data ?? [];
return (
<div className="space-y-4">
<h1 className="text-xl font-bold">ML Dashboard</h1>
{/* Model Cards */}
<div className="grid grid-cols-4 gap-4">
{['DQN', 'PPO', 'TFT', 'Mamba2'].map((model) => (
<div
key={model}
className="h-32 rounded border border-[var(--color-border)] bg-[var(--color-bg-card)] flex items-center justify-center text-[var(--color-text-secondary)]"
>
{model} Model Card
</div>
))}
{MODELS.map((model) => {
const pred = preds.find((p) => p.model === model);
return (
<div
key={model}
className="rounded border border-[var(--color-border)] bg-[var(--color-bg-card)]"
>
<ModelCard
model={model}
signal={pred?.signal}
confidence={pred?.confidence}
predictedReturn={pred?.predicted_return}
/>
</div>
);
})}
</div>
{/* Ensemble + Regime */}
<div className="grid grid-cols-2 gap-4">
<div className="h-48 rounded border border-[var(--color-border)] bg-[var(--color-bg-card)] flex items-center justify-center text-[var(--color-text-secondary)]">
Ensemble Panel
<div className="rounded border border-[var(--color-border)] bg-[var(--color-bg-card)]">
<EnsemblePanel predictions={preds} />
</div>
<div className="h-48 rounded border border-[var(--color-border)] bg-[var(--color-bg-card)] flex items-center justify-center text-[var(--color-text-secondary)]">
Regime State
<div className="rounded border border-[var(--color-border)] bg-[var(--color-bg-card)]">
<div className="px-3 py-1.5 border-b border-[var(--color-border)]">
<span className="text-sm font-medium">Market Regime</span>
</div>
<div className="p-3 space-y-2">
<div className="flex justify-between text-xs">
<span className="text-[var(--color-text-secondary)]">Current Regime</span>
<span className="text-[var(--color-accent)]">Trending</span>
</div>
<div className="flex justify-between text-xs">
<span className="text-[var(--color-text-secondary)]">Volatility</span>
<span>Medium</span>
</div>
<div className="flex justify-between text-xs">
<span className="text-[var(--color-text-secondary)]">Regime Confidence</span>
<span>72.3%</span>
</div>
<div className="flex justify-between text-xs">
<span className="text-[var(--color-text-secondary)]">Regime Duration</span>
<span>4h 23m</span>
</div>
</div>
</div>
</div>
<div className="rounded border border-[var(--color-border)] bg-[var(--color-bg-card)] p-4 text-[var(--color-text-secondary)]">
Training Progress
{/* Training Progress */}
<div className="rounded border border-[var(--color-border)] bg-[var(--color-bg-card)]">
<TrainingProgress
jobs={trainingJobs.data}
loading={trainingJobs.isLoading}
/>
</div>
</div>
);

View File

@@ -1,22 +1,64 @@
import { PnLChart } from '../components/charts/PnLChart';
import { MetricCard } from '../components/charts/MetricCard';
import { useApiQuery } from '../hooks/useApi';
import type { PerformanceMetrics } from '../lib/types';
export function PerformanceDashboard() {
const metrics = useApiQuery<PerformanceMetrics>(
['performance-metrics'],
'/performance/metrics',
{ refetchInterval: 10000 },
);
const m = metrics.data;
return (
<div className="space-y-4">
<h1 className="text-xl font-bold">Performance Dashboard</h1>
{/* Top: Metric cards */}
<div className="grid grid-cols-5 gap-4">
{['Sharpe', 'Sortino', 'Max DD', 'Win Rate', 'Total Trades'].map((metric) => (
<div
key={metric}
className="h-24 rounded border border-[var(--color-border)] bg-[var(--color-bg-card)] flex items-center justify-center text-[var(--color-text-secondary)]"
>
{metric}
<div className="rounded border border-[var(--color-border)] bg-[var(--color-bg-card)]">
<MetricCard label="Sharpe Ratio" value={m?.sharpe_ratio?.toFixed(2) ?? '--'} />
</div>
<div className="rounded border border-[var(--color-border)] bg-[var(--color-bg-card)]">
<MetricCard label="Sortino Ratio" value="--" />
</div>
<div className="rounded border border-[var(--color-border)] bg-[var(--color-bg-card)]">
<MetricCard
label="Max Drawdown"
value={m?.max_drawdown?.toFixed(2) ?? '--'}
suffix="%"
/>
</div>
<div className="rounded border border-[var(--color-border)] bg-[var(--color-bg-card)]">
<MetricCard
label="Win Rate"
value={m?.win_rate !== undefined ? (m.win_rate * 100).toFixed(1) : '--'}
suffix="%"
/>
</div>
<div className="rounded border border-[var(--color-border)] bg-[var(--color-bg-card)]">
<MetricCard label="Total Trades" value={m?.total_trades ?? '--'} />
</div>
</div>
{/* Middle: P&L Chart */}
<div className="h-64 rounded border border-[var(--color-border)] bg-[var(--color-bg-card)]">
<PnLChart title="Cumulative P&L" />
</div>
{/* Bottom: Daily returns + Heatmap */}
<div className="grid grid-cols-2 gap-4">
<div className="h-48 rounded border border-[var(--color-border)] bg-[var(--color-bg-card)]">
<PnLChart title="Daily Returns" />
</div>
<div className="h-48 rounded border border-[var(--color-border)] bg-[var(--color-bg-card)]">
<div className="px-3 py-1.5 border-b border-[var(--color-border)]">
<span className="text-sm font-medium">P&L Heatmap</span>
</div>
))}
</div>
<div className="h-64 rounded border border-[var(--color-border)] bg-[var(--color-bg-card)] flex items-center justify-center text-[var(--color-text-secondary)]">
P&L Chart
</div>
<div className="rounded border border-[var(--color-border)] bg-[var(--color-bg-card)] p-4 text-[var(--color-text-secondary)]">
P&L Heatmap
<div className="flex items-center justify-center h-full text-sm text-[var(--color-text-secondary)]">
Strategy x Time heatmap
</div>
</div>
</div>
</div>
);

View File

@@ -1,27 +1,84 @@
import { RiskGauge } from '../components/risk/RiskGauge';
import { DrawdownChart } from '../components/risk/DrawdownChart';
import { EmergencyControls } from '../components/risk/EmergencyControls';
import { useApiQuery } from '../hooks/useApi';
import { useWebSocketTopic } from '../hooks/useWebSocket';
import type { RiskMetrics } from '../lib/types';
export function RiskDashboard() {
const riskMetrics = useApiQuery<RiskMetrics>(['risk-metrics'], '/risk/metrics', {
refetchInterval: 5000,
});
const { messages: alerts } = useWebSocketTopic('risk_alert');
const metrics = riskMetrics.data;
return (
<div className="space-y-4">
<h1 className="text-xl font-bold">Risk Dashboard</h1>
{/* Top row: Risk gauges */}
<div className="grid grid-cols-3 gap-4">
<div className="h-48 rounded border border-[var(--color-border)] bg-[var(--color-bg-card)] flex items-center justify-center text-[var(--color-text-secondary)]">
VaR Gauge
<div className="h-48 rounded border border-[var(--color-border)] bg-[var(--color-bg-card)]">
<RiskGauge
label="VaR Utilization"
value={metrics?.portfolio_var ?? 0}
/>
</div>
<div className="h-48 rounded border border-[var(--color-border)] bg-[var(--color-bg-card)] flex items-center justify-center text-[var(--color-text-secondary)]">
Position Utilization
<div className="h-48 rounded border border-[var(--color-border)] bg-[var(--color-bg-card)]">
<RiskGauge
label="Position Utilization"
value={metrics?.position_utilization ?? 0}
/>
</div>
<div className="h-48 rounded border border-[var(--color-border)] bg-[var(--color-bg-card)] flex items-center justify-center text-[var(--color-text-secondary)]">
Drawdown Gauge
<div className="h-48 rounded border border-[var(--color-border)] bg-[var(--color-bg-card)]">
<RiskGauge
label="Drawdown"
value={Math.abs(metrics?.current_drawdown ?? 0)}
thresholds={{ warn: 5, danger: 10 }}
max={20}
/>
</div>
</div>
<div className="h-64 rounded border border-[var(--color-border)] bg-[var(--color-bg-card)] flex items-center justify-center text-[var(--color-text-secondary)]">
Drawdown Chart
{/* Middle: Drawdown chart */}
<div className="h-64 rounded border border-[var(--color-border)] bg-[var(--color-bg-card)]">
<DrawdownChart />
</div>
{/* Bottom row: Alerts + Emergency Controls */}
<div className="grid grid-cols-2 gap-4">
<div className="rounded border border-[var(--color-border)] bg-[var(--color-bg-card)] p-4 text-[var(--color-text-secondary)]">
Position Risk Table
<div className="rounded border border-[var(--color-border)] bg-[var(--color-bg-card)]">
<div className="px-3 py-1.5 border-b border-[var(--color-border)]">
<span className="text-sm font-medium">Risk Alerts</span>
</div>
<div className="max-h-64 overflow-auto">
{alerts.length === 0 ? (
<div className="p-4 text-sm text-[var(--color-text-secondary)] text-center">
No active alerts
</div>
) : (
<div className="divide-y divide-[var(--color-border)]">
{alerts.slice(-20).reverse().map((alert, i) => (
<div key={`alert-${alerts.length - i}`} className="px-3 py-2 text-xs">
<span
className={
(alert as { severity?: string }).severity === 'critical'
? 'text-[var(--color-red)]'
: 'text-[var(--color-yellow)]'
}
>
[{((alert as { severity?: string }).severity ?? 'info').toUpperCase()}]
</span>{' '}
{JSON.stringify((alert as { data?: unknown }).data ?? alert)}
</div>
))}
</div>
)}
</div>
</div>
<div className="rounded border border-[var(--color-border)] bg-[var(--color-bg-card)] p-4 text-[var(--color-text-secondary)]">
Emergency Controls
<div className="rounded border border-[var(--color-border)] bg-[var(--color-bg-card)]">
<EmergencyControls />
</div>
</div>
</div>

View File

@@ -1,24 +1,47 @@
import { CandlestickChart } from '../components/charts/CandlestickChart';
import { OrderBook } from '../components/trading/OrderBook';
import { PositionsTable } from '../components/trading/PositionsTable';
import { OrderForm } from '../components/trading/OrderForm';
import { OrdersTable } from '../components/trading/OrdersTable';
import { useApiQuery } from '../hooks/useApi';
import type { Position, Order } from '../lib/types';
export function TradingDashboard() {
const positions = useApiQuery<Position[]>(['positions'], '/trading/positions', {
refetchInterval: 5000,
});
const orders = useApiQuery<Order[]>(['orders'], '/trading/orders', {
refetchInterval: 5000,
});
return (
<div className="space-y-4">
<h1 className="text-xl font-bold">Trading Dashboard</h1>
<div className="grid grid-cols-3 gap-4">
<div className="col-span-2 h-80 rounded border border-[var(--color-border)] bg-[var(--color-bg-card)] flex items-center justify-center text-[var(--color-text-secondary)]">
Candlestick Chart
{/* Top row: Chart + Order Book */}
<div className="grid grid-cols-[1fr_320px] gap-4 h-80">
<div className="rounded border border-[var(--color-border)] bg-[var(--color-bg-card)] overflow-hidden">
<CandlestickChart symbol="ES.FUT" />
</div>
<div className="h-80 rounded border border-[var(--color-border)] bg-[var(--color-bg-card)] flex items-center justify-center text-[var(--color-text-secondary)]">
Order Book
<div className="rounded border border-[var(--color-border)] bg-[var(--color-bg-card)] overflow-hidden">
<OrderBook />
</div>
</div>
<div className="rounded border border-[var(--color-border)] bg-[var(--color-bg-card)] p-4 text-[var(--color-text-secondary)]">
Positions Table
{/* Middle: Positions */}
<div className="rounded border border-[var(--color-border)] bg-[var(--color-bg-card)]">
<PositionsTable
positions={positions.data}
loading={positions.isLoading}
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="rounded border border-[var(--color-border)] bg-[var(--color-bg-card)] p-4 text-[var(--color-text-secondary)]">
Orders Table
{/* Bottom: Orders + Order Form */}
<div className="grid grid-cols-[1fr_360px] gap-4">
<div className="rounded border border-[var(--color-border)] bg-[var(--color-bg-card)]">
<OrdersTable orders={orders.data} loading={orders.isLoading} />
</div>
<div className="rounded border border-[var(--color-border)] bg-[var(--color-bg-card)] p-4 text-[var(--color-text-secondary)]">
Order Form
<div className="rounded border border-[var(--color-border)] bg-[var(--color-bg-card)]">
<OrderForm />
</div>
</div>
</div>