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:
83
web-dashboard/src/components/charts/CandlestickChart.tsx
Normal file
83
web-dashboard/src/components/charts/CandlestickChart.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
30
web-dashboard/src/components/charts/MetricCard.tsx
Normal file
30
web-dashboard/src/components/charts/MetricCard.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
61
web-dashboard/src/components/charts/PnLChart.tsx
Normal file
61
web-dashboard/src/components/charts/PnLChart.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
103
web-dashboard/src/components/ml/EnsemblePanel.tsx
Normal file
103
web-dashboard/src/components/ml/EnsemblePanel.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
59
web-dashboard/src/components/ml/ModelCard.tsx
Normal file
59
web-dashboard/src/components/ml/ModelCard.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
83
web-dashboard/src/components/ml/TrainingProgress.tsx
Normal file
83
web-dashboard/src/components/ml/TrainingProgress.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
54
web-dashboard/src/components/risk/DrawdownChart.tsx
Normal file
54
web-dashboard/src/components/risk/DrawdownChart.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
91
web-dashboard/src/components/risk/EmergencyControls.tsx
Normal file
91
web-dashboard/src/components/risk/EmergencyControls.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
52
web-dashboard/src/components/risk/RiskGauge.tsx
Normal file
52
web-dashboard/src/components/risk/RiskGauge.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
97
web-dashboard/src/components/trading/OrderBook.tsx
Normal file
97
web-dashboard/src/components/trading/OrderBook.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
149
web-dashboard/src/components/trading/OrderForm.tsx
Normal file
149
web-dashboard/src/components/trading/OrderForm.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
107
web-dashboard/src/components/trading/OrdersTable.tsx
Normal file
107
web-dashboard/src/components/trading/OrdersTable.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
82
web-dashboard/src/components/trading/PositionsTable.tsx
Normal file
82
web-dashboard/src/components/trading/PositionsTable.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user