safety(web-dashboard): theme-consistent chart colors, a11y attributes, WS reconnect, loading skeletons
- Replace all hardcoded hex colors in chart components with CSS custom properties (getCssVar helper), making charts respect theme changes - Add scope="col" to all table headers (PositionsTable, OrdersTable, BacktestingDashboard trade list) - Connect form labels to inputs via htmlFor/id pairs (OrderForm, BacktestingDashboard) - Add role="alert" to validation error messages - Add aria-label to cancel button, progressbar role to backtest progress - Add WsManager.reconnect() method with backoff reset - Add Reconnect button to StatusBar when WS disconnected - Add shimmer skeleton loading states to CandlestickChart and PnLChart Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,10 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { createChart, CandlestickSeries, type IChartApi, type CandlestickData, type Time } from 'lightweight-charts';
|
||||
|
||||
function getCssVar(name: string): string {
|
||||
return getComputedStyle(document.documentElement).getPropertyValue(name).trim();
|
||||
}
|
||||
|
||||
interface Props {
|
||||
symbol: string;
|
||||
data?: CandlestickData<Time>[];
|
||||
@@ -16,34 +20,40 @@ export function CandlestickChart({ symbol, data = [] }: Props) {
|
||||
useEffect(() => {
|
||||
if (!containerRef.current) return;
|
||||
|
||||
const bgCard = getCssVar('--color-bg-card');
|
||||
const textSecondary = getCssVar('--color-text-secondary');
|
||||
const border = getCssVar('--color-border');
|
||||
const green = getCssVar('--color-green');
|
||||
const red = getCssVar('--color-red');
|
||||
|
||||
const chart = createChart(containerRef.current, {
|
||||
layout: {
|
||||
background: { color: '#1e293b' },
|
||||
textColor: '#94a3b8',
|
||||
background: { color: bgCard },
|
||||
textColor: textSecondary,
|
||||
},
|
||||
grid: {
|
||||
vertLines: { color: '#334155' },
|
||||
horzLines: { color: '#334155' },
|
||||
vertLines: { color: border },
|
||||
horzLines: { color: border },
|
||||
},
|
||||
crosshair: {
|
||||
mode: 0, // Normal
|
||||
},
|
||||
timeScale: {
|
||||
borderColor: '#334155',
|
||||
borderColor: border,
|
||||
timeVisible: true,
|
||||
},
|
||||
rightPriceScale: {
|
||||
borderColor: '#334155',
|
||||
borderColor: border,
|
||||
},
|
||||
});
|
||||
|
||||
const series = chart.addSeries(CandlestickSeries, {
|
||||
upColor: '#22c55e',
|
||||
downColor: '#ef4444',
|
||||
borderUpColor: '#22c55e',
|
||||
borderDownColor: '#ef4444',
|
||||
wickUpColor: '#22c55e',
|
||||
wickDownColor: '#ef4444',
|
||||
upColor: green,
|
||||
downColor: red,
|
||||
borderUpColor: green,
|
||||
borderDownColor: red,
|
||||
wickUpColor: green,
|
||||
wickDownColor: red,
|
||||
});
|
||||
|
||||
chartRef.current = chart;
|
||||
@@ -77,7 +87,25 @@ export function CandlestickChart({ symbol, data = [] }: Props) {
|
||||
<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 className="relative flex-1 min-h-0">
|
||||
<div ref={containerRef} className="absolute inset-0" />
|
||||
{data.length === 0 && (
|
||||
<div className="absolute inset-0 flex items-center justify-center bg-[var(--color-bg-card)]/80">
|
||||
<div className="text-center">
|
||||
<div className="flex gap-1 mb-2 justify-center">
|
||||
{Array.from({ length: 8 }, (_, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="w-2 rounded-sm bg-[var(--color-border)] animate-pulse"
|
||||
style={{ height: `${16 + i * 4}px`, animationDelay: `${i * 100}ms` }}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<span className="text-xs text-[var(--color-text-secondary)]">Awaiting market data...</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import { useMemo } from 'react';
|
||||
import { AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts';
|
||||
|
||||
function getCssVar(name: string): string {
|
||||
return getComputedStyle(document.documentElement).getPropertyValue(name).trim();
|
||||
}
|
||||
|
||||
interface PnLPoint {
|
||||
date: string;
|
||||
pnl: number;
|
||||
@@ -11,6 +16,12 @@ interface Props {
|
||||
}
|
||||
|
||||
export function PnLChart({ data = [], title = 'P&L' }: Props) {
|
||||
const colors = useMemo(() => ({
|
||||
border: getCssVar('--color-border'),
|
||||
textSecondary: getCssVar('--color-text-secondary'),
|
||||
bgCard: getCssVar('--color-bg-card'),
|
||||
accent: getCssVar('--color-accent'),
|
||||
}), []);
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
<div className="px-3 py-1.5 border-b border-[var(--color-border)]">
|
||||
@@ -20,16 +31,16 @@ export function PnLChart({ data = [], title = 'P&L' }: Props) {
|
||||
{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' }} />
|
||||
<CartesianGrid strokeDasharray="3 3" stroke={colors.border} />
|
||||
<XAxis dataKey="date" tick={{ fontSize: 10, fill: colors.textSecondary }} />
|
||||
<YAxis
|
||||
tick={{ fontSize: 10, fill: '#94a3b8' }}
|
||||
tick={{ fontSize: 10, fill: colors.textSecondary }}
|
||||
tickFormatter={(v: number) => `$${v.toLocaleString()}`}
|
||||
/>
|
||||
<Tooltip
|
||||
contentStyle={{
|
||||
backgroundColor: '#1e293b',
|
||||
border: '1px solid #334155',
|
||||
backgroundColor: colors.bgCard,
|
||||
border: `1px solid ${colors.border}`,
|
||||
borderRadius: 4,
|
||||
fontSize: 12,
|
||||
}}
|
||||
@@ -37,22 +48,33 @@ export function PnLChart({ data = [], title = 'P&L' }: Props) {
|
||||
/>
|
||||
<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} />
|
||||
<stop offset="5%" stopColor={colors.accent} stopOpacity={0.3} />
|
||||
<stop offset="95%" stopColor={colors.accent} stopOpacity={0} />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey="pnl"
|
||||
stroke="#3b82f6"
|
||||
stroke={colors.accent}
|
||||
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 className="flex flex-col justify-end h-full p-4">
|
||||
<div className="flex items-end gap-1 h-full">
|
||||
{Array.from({ length: 16 }, (_, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="flex-1 rounded-t-sm bg-[var(--color-border)] animate-pulse"
|
||||
style={{ height: `${15 + Math.abs(Math.sin(i * 0.7)) * 60}%`, animationDelay: `${i * 80}ms` }}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div className="text-center text-xs text-[var(--color-text-secondary)] mt-2">
|
||||
No P&L data available
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useWebSocket } from '../../hooks/useWebSocket';
|
||||
import { useAuth } from '../../hooks/useAuth';
|
||||
import { getTokenClaims } from '../../lib/auth';
|
||||
import { wsManager } from '../../lib/websocket';
|
||||
|
||||
const EMPTY_TOPICS: string[] = [];
|
||||
|
||||
@@ -19,7 +20,18 @@ export function StatusBar() {
|
||||
}`}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
WS: {connected ? 'Connected' : 'Disconnected'}
|
||||
WS: {connected ? 'Connected' : (
|
||||
<>
|
||||
Disconnected
|
||||
<button
|
||||
onClick={() => wsManager.reconnect()}
|
||||
className="ml-1 px-1.5 py-0.5 rounded text-[var(--color-accent)] hover:bg-[var(--color-accent)]/10 transition-colors"
|
||||
aria-label="Reconnect WebSocket"
|
||||
>
|
||||
Reconnect
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</span>
|
||||
{authenticated && claims && (
|
||||
<span>User: {claims.sub}</span>
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import { useMemo } from 'react';
|
||||
import { AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts';
|
||||
|
||||
function getCssVar(name: string): string {
|
||||
return getComputedStyle(document.documentElement).getPropertyValue(name).trim();
|
||||
}
|
||||
|
||||
interface DrawdownPoint {
|
||||
time: string;
|
||||
drawdown: number;
|
||||
@@ -10,6 +15,12 @@ interface Props {
|
||||
}
|
||||
|
||||
export function DrawdownChart({ data = [] }: Props) {
|
||||
const colors = useMemo(() => ({
|
||||
border: getCssVar('--color-border'),
|
||||
textSecondary: getCssVar('--color-text-secondary'),
|
||||
bgCard: getCssVar('--color-bg-card'),
|
||||
red: getCssVar('--color-red'),
|
||||
}), []);
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
<div className="px-3 py-1.5 border-b border-[var(--color-border)]">
|
||||
@@ -19,17 +30,17 @@ export function DrawdownChart({ data = [] }: Props) {
|
||||
{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' }} />
|
||||
<CartesianGrid strokeDasharray="3 3" stroke={colors.border} />
|
||||
<XAxis dataKey="time" tick={{ fontSize: 10, fill: colors.textSecondary }} />
|
||||
<YAxis
|
||||
tick={{ fontSize: 10, fill: '#94a3b8' }}
|
||||
tick={{ fontSize: 10, fill: colors.textSecondary }}
|
||||
tickFormatter={(v: number) => `${v.toFixed(1)}%`}
|
||||
domain={['dataMin', 0]}
|
||||
/>
|
||||
<Tooltip
|
||||
contentStyle={{
|
||||
backgroundColor: '#1e293b',
|
||||
border: '1px solid #334155',
|
||||
backgroundColor: colors.bgCard,
|
||||
border: `1px solid ${colors.border}`,
|
||||
borderRadius: 4,
|
||||
fontSize: 12,
|
||||
}}
|
||||
@@ -37,8 +48,8 @@ export function DrawdownChart({ data = [] }: Props) {
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey="drawdown"
|
||||
stroke="#ef4444"
|
||||
fill="#ef444433"
|
||||
stroke={colors.red}
|
||||
fill={`${colors.red}33`}
|
||||
strokeWidth={2}
|
||||
/>
|
||||
</AreaChart>
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import { useMemo } from 'react';
|
||||
import { RadialBarChart, RadialBar, PolarAngleAxis } from 'recharts';
|
||||
|
||||
function getCssVar(name: string): string {
|
||||
return getComputedStyle(document.documentElement).getPropertyValue(name).trim();
|
||||
}
|
||||
|
||||
interface Props {
|
||||
label: string;
|
||||
value: number;
|
||||
@@ -13,6 +18,10 @@ export function RiskGauge({
|
||||
max = 100,
|
||||
thresholds = { warn: 60, danger: 80 },
|
||||
}: Props) {
|
||||
const colors = useMemo(() => ({
|
||||
border: getCssVar('--color-border'),
|
||||
}), []);
|
||||
|
||||
const pct = Math.min((value / max) * 100, 100);
|
||||
|
||||
const color =
|
||||
@@ -39,7 +48,7 @@ export function RiskGauge({
|
||||
endAngle={0}
|
||||
>
|
||||
<PolarAngleAxis type="number" domain={[0, 100]} tick={false} />
|
||||
<RadialBar dataKey="value" cornerRadius={6} background={{ fill: '#334155' }} />
|
||||
<RadialBar dataKey="value" cornerRadius={6} background={{ fill: colors.border }} />
|
||||
</RadialBarChart>
|
||||
<div className="text-center -mt-8">
|
||||
<div className="text-lg font-bold" style={{ color }}>
|
||||
|
||||
@@ -84,8 +84,9 @@ export function OrderForm() {
|
||||
<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>
|
||||
<label htmlFor="order-symbol" className="block text-xs text-[var(--color-text-secondary)] mb-1">Symbol</label>
|
||||
<input
|
||||
id="order-symbol"
|
||||
type="text"
|
||||
value={symbol}
|
||||
onChange={(e) => {
|
||||
@@ -101,7 +102,7 @@ export function OrderForm() {
|
||||
}`}
|
||||
/>
|
||||
{validationErrors.symbol && (
|
||||
<p className="text-xs text-[var(--color-red)] mt-0.5">{validationErrors.symbol}</p>
|
||||
<p className="text-xs text-[var(--color-red)] mt-0.5" role="alert">{validationErrors.symbol}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -140,8 +141,9 @@ export function OrderForm() {
|
||||
|
||||
{/* Type */}
|
||||
<div>
|
||||
<label className="block text-xs text-[var(--color-text-secondary)] mb-1">Type</label>
|
||||
<label htmlFor="order-type" className="block text-xs text-[var(--color-text-secondary)] mb-1">Type</label>
|
||||
<select
|
||||
id="order-type"
|
||||
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"
|
||||
@@ -153,8 +155,9 @@ export function OrderForm() {
|
||||
|
||||
{/* Quantity */}
|
||||
<div>
|
||||
<label className="block text-xs text-[var(--color-text-secondary)] mb-1">Quantity</label>
|
||||
<label htmlFor="order-quantity" className="block text-xs text-[var(--color-text-secondary)] mb-1">Quantity</label>
|
||||
<input
|
||||
id="order-quantity"
|
||||
type="number"
|
||||
min="0"
|
||||
step="1"
|
||||
@@ -170,15 +173,16 @@ export function OrderForm() {
|
||||
}`}
|
||||
/>
|
||||
{validationErrors.quantity && (
|
||||
<p className="text-xs text-[var(--color-red)] mt-0.5">{validationErrors.quantity}</p>
|
||||
<p className="text-xs text-[var(--color-red)] mt-0.5" role="alert">{validationErrors.quantity}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Price (limit only) */}
|
||||
{orderType === 'limit' && (
|
||||
<div>
|
||||
<label className="block text-xs text-[var(--color-text-secondary)] mb-1">Price</label>
|
||||
<label htmlFor="order-price" className="block text-xs text-[var(--color-text-secondary)] mb-1">Price</label>
|
||||
<input
|
||||
id="order-price"
|
||||
type="number"
|
||||
min="0"
|
||||
step="0.01"
|
||||
@@ -194,7 +198,7 @@ export function OrderForm() {
|
||||
}`}
|
||||
/>
|
||||
{validationErrors.price && (
|
||||
<p className="text-xs text-[var(--color-red)] mt-0.5">{validationErrors.price}</p>
|
||||
<p className="text-xs text-[var(--color-red)] mt-0.5" role="alert">{validationErrors.price}</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
@@ -213,7 +217,7 @@ export function OrderForm() {
|
||||
</button>
|
||||
|
||||
{submitOrder.isError && (
|
||||
<p className="text-xs text-[var(--color-red)]">{submitOrder.error.message}</p>
|
||||
<p className="text-xs text-[var(--color-red)]" role="alert">{submitOrder.error.message}</p>
|
||||
)}
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -20,14 +20,14 @@ export function OrdersTable({ orders = [], loading }: Props) {
|
||||
<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>
|
||||
<th scope="col" className="text-left px-3 py-2 font-medium">ID</th>
|
||||
<th scope="col" className="text-left px-3 py-2 font-medium">Symbol</th>
|
||||
<th scope="col" className="text-left px-3 py-2 font-medium">Side</th>
|
||||
<th scope="col" className="text-left px-3 py-2 font-medium">Type</th>
|
||||
<th scope="col" className="text-right px-3 py-2 font-medium">Qty</th>
|
||||
<th scope="col" className="text-right px-3 py-2 font-medium">Price</th>
|
||||
<th scope="col" className="text-left px-3 py-2 font-medium">Status</th>
|
||||
<th scope="col" className="text-right px-3 py-2 font-medium">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -96,13 +96,14 @@ function OrderRow({ order }: { order: Order }) {
|
||||
<button
|
||||
onClick={() => cancelOrder.mutate()}
|
||||
disabled={cancelOrder.isPending}
|
||||
aria-label={`Cancel order ${order.order_id.slice(0, 8)}`}
|
||||
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"
|
||||
>
|
||||
{cancelOrder.isPending ? 'Cancelling...' : 'Cancel'}
|
||||
</button>
|
||||
)}
|
||||
{cancelOrder.isError && (
|
||||
<span className="text-xs text-[var(--color-red)]">
|
||||
<span className="text-xs text-[var(--color-red)]" role="alert">
|
||||
{cancelOrder.error.message}
|
||||
</span>
|
||||
)}
|
||||
|
||||
@@ -19,12 +19,12 @@ export function PositionsTable({ positions = [], loading }: Props) {
|
||||
<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>
|
||||
<th scope="col" className="text-left px-3 py-2 font-medium">Symbol</th>
|
||||
<th scope="col" className="text-left px-3 py-2 font-medium">Side</th>
|
||||
<th scope="col" className="text-right px-3 py-2 font-medium">Qty</th>
|
||||
<th scope="col" className="text-right px-3 py-2 font-medium">Entry</th>
|
||||
<th scope="col" className="text-right px-3 py-2 font-medium">Market</th>
|
||||
<th scope="col" className="text-right px-3 py-2 font-medium">P&L</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
||||
@@ -79,6 +79,22 @@ export class WsManager {
|
||||
this._connected = false;
|
||||
}
|
||||
|
||||
/** Manually trigger reconnection (resets backoff) */
|
||||
reconnect(): void {
|
||||
this.disconnect();
|
||||
this.attempts = 0;
|
||||
this.backoff = 1000;
|
||||
this.connect();
|
||||
}
|
||||
|
||||
get reconnectAttempts(): number {
|
||||
return this.attempts;
|
||||
}
|
||||
|
||||
get maxReconnectAttempts(): number {
|
||||
return this.maxAttempts;
|
||||
}
|
||||
|
||||
/** Subscribe to data topics */
|
||||
subscribe(topics: string[]): void {
|
||||
topics.forEach((t) => this.subscribedTopics.add(t));
|
||||
|
||||
@@ -111,10 +111,11 @@ export function BacktestingDashboard() {
|
||||
</div>
|
||||
<form onSubmit={handleSubmit} className="p-3 space-y-3">
|
||||
<div>
|
||||
<label className="block text-xs text-[var(--color-text-secondary)] mb-1">
|
||||
<label htmlFor="bt-strategy" className="block text-xs text-[var(--color-text-secondary)] mb-1">
|
||||
Strategy
|
||||
</label>
|
||||
<select
|
||||
id="bt-strategy"
|
||||
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"
|
||||
@@ -127,10 +128,11 @@ export function BacktestingDashboard() {
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs text-[var(--color-text-secondary)] mb-1">
|
||||
<label htmlFor="bt-symbol" className="block text-xs text-[var(--color-text-secondary)] mb-1">
|
||||
Symbol
|
||||
</label>
|
||||
<input
|
||||
id="bt-symbol"
|
||||
type="text"
|
||||
value={symbol}
|
||||
onChange={(e) => {
|
||||
@@ -144,15 +146,16 @@ export function BacktestingDashboard() {
|
||||
}`}
|
||||
/>
|
||||
{validationErrors.symbol && (
|
||||
<p className="text-xs text-[var(--color-red)] mt-0.5">{validationErrors.symbol}</p>
|
||||
<p className="text-xs text-[var(--color-red)] mt-0.5" role="alert">{validationErrors.symbol}</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<div>
|
||||
<label className="block text-xs text-[var(--color-text-secondary)] mb-1">
|
||||
<label htmlFor="bt-start-date" className="block text-xs text-[var(--color-text-secondary)] mb-1">
|
||||
Start Date
|
||||
</label>
|
||||
<input
|
||||
id="bt-start-date"
|
||||
type="date"
|
||||
value={startDate}
|
||||
onChange={(e) => {
|
||||
@@ -167,10 +170,11 @@ export function BacktestingDashboard() {
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs text-[var(--color-text-secondary)] mb-1">
|
||||
<label htmlFor="bt-end-date" className="block text-xs text-[var(--color-text-secondary)] mb-1">
|
||||
End Date
|
||||
</label>
|
||||
<input
|
||||
id="bt-end-date"
|
||||
type="date"
|
||||
value={endDate}
|
||||
onChange={(e) => {
|
||||
@@ -186,7 +190,7 @@ export function BacktestingDashboard() {
|
||||
</div>
|
||||
</div>
|
||||
{validationErrors.dateRange && (
|
||||
<p className="text-xs text-[var(--color-red)] mt-0.5">{validationErrors.dateRange}</p>
|
||||
<p className="text-xs text-[var(--color-red)] mt-0.5" role="alert">{validationErrors.dateRange}</p>
|
||||
)}
|
||||
<button
|
||||
type="submit"
|
||||
@@ -201,7 +205,7 @@ export function BacktestingDashboard() {
|
||||
</p>
|
||||
)}
|
||||
{startBacktest.isError && (
|
||||
<p className="text-xs text-[var(--color-red)]">
|
||||
<p className="text-xs text-[var(--color-red)]" role="alert">
|
||||
{startBacktest.error.message}
|
||||
</p>
|
||||
)}
|
||||
@@ -233,6 +237,10 @@ export function BacktestingDashboard() {
|
||||
<div
|
||||
className="bg-[var(--color-accent)] h-1.5 rounded-full transition-all"
|
||||
style={{ width: `${(backtestStatus.data.progress * 100).toFixed(0)}%` }}
|
||||
role="progressbar"
|
||||
aria-valuenow={Math.round(backtestStatus.data.progress * 100)}
|
||||
aria-valuemin={0}
|
||||
aria-valuemax={100}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
@@ -261,12 +269,12 @@ export function BacktestingDashboard() {
|
||||
<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>
|
||||
<th scope="col" className="text-left px-3 py-2 font-medium">Time</th>
|
||||
<th scope="col" className="text-left px-3 py-2 font-medium">Symbol</th>
|
||||
<th scope="col" className="text-left px-3 py-2 font-medium">Side</th>
|
||||
<th scope="col" className="text-right px-3 py-2 font-medium">Entry</th>
|
||||
<th scope="col" className="text-right px-3 py-2 font-medium">Exit</th>
|
||||
<th scope="col" className="text-right px-3 py-2 font-medium">P&L</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
||||
Reference in New Issue
Block a user