feat: dashboard trading views -- portfolio, trades, strategies, news, backtest

Add Layout with sidebar navigation and top bar (portfolio value, trading
status indicator). Implement Portfolio page with equity curve (TradingView
lightweight-charts), positions table, and metrics row. Add TradeLog with
filters, pagination, and expandable row details. Add Strategies page with
weight allocation pie chart and weight history line chart (Recharts). Add
NewsFeed with sentiment badges and ticker filtering. Add Backtest page
with config form, run submission, and results panel. Include WebSocket
hook for real-time cache invalidation and portfolio query hooks.
This commit is contained in:
Viktor Barzin 2026-02-22 15:54:44 +00:00
parent f121f376ae
commit 8d6e666280
No known key found for this signature in database
GPG key ID: 0EB088298288D958
12 changed files with 1761 additions and 0 deletions

View file

@ -0,0 +1,108 @@
import { useEffect, useRef, useState, useCallback } from 'react';
import { useQueryClient } from '@tanstack/react-query';
export interface WebSocketEvent {
type: string;
data: Record<string, unknown>;
timestamp: string;
}
const WS_RECONNECT_DELAY = 3000;
const WS_MAX_RECONNECT_DELAY = 30000;
export function useWebSocket() {
const queryClient = useQueryClient();
const wsRef = useRef<WebSocket | null>(null);
const reconnectTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const reconnectDelayRef = useRef(WS_RECONNECT_DELAY);
const [lastEvent, setLastEvent] = useState<WebSocketEvent | null>(null);
const [isConnected, setIsConnected] = useState(false);
const handleMessage = useCallback(
(event: MessageEvent) => {
try {
const parsed: WebSocketEvent = JSON.parse(event.data);
setLastEvent(parsed);
// Invalidate relevant TanStack Query caches based on event type
switch (parsed.type) {
case 'trade_executed':
queryClient.invalidateQueries({ queryKey: ['portfolio'] });
queryClient.invalidateQueries({ queryKey: ['positions'] });
queryClient.invalidateQueries({ queryKey: ['trades'] });
break;
case 'signal_generated':
queryClient.invalidateQueries({ queryKey: ['signals'] });
break;
case 'portfolio_update':
queryClient.invalidateQueries({ queryKey: ['portfolio'] });
queryClient.invalidateQueries({ queryKey: ['equity-curve'] });
break;
case 'news_scored':
queryClient.invalidateQueries({ queryKey: ['news'] });
break;
case 'strategy_update':
queryClient.invalidateQueries({ queryKey: ['strategies'] });
break;
default:
break;
}
} catch {
// Ignore non-JSON messages
}
},
[queryClient]
);
const connect = useCallback(() => {
const token = localStorage.getItem('access_token');
if (!token) return;
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const host = window.location.host;
const wsUrl = `${protocol}//${host}/ws?token=${encodeURIComponent(token)}`;
const ws = new WebSocket(wsUrl);
wsRef.current = ws;
ws.onopen = () => {
setIsConnected(true);
reconnectDelayRef.current = WS_RECONNECT_DELAY;
};
ws.onmessage = handleMessage;
ws.onclose = () => {
setIsConnected(false);
wsRef.current = null;
// Reconnect with exponential backoff
reconnectTimeoutRef.current = setTimeout(() => {
reconnectDelayRef.current = Math.min(
reconnectDelayRef.current * 2,
WS_MAX_RECONNECT_DELAY
);
connect();
}, reconnectDelayRef.current);
};
ws.onerror = () => {
ws.close();
};
}, [handleMessage]);
useEffect(() => {
connect();
return () => {
if (reconnectTimeoutRef.current) {
clearTimeout(reconnectTimeoutRef.current);
}
if (wsRef.current) {
wsRef.current.close();
}
};
}, [connect]);
return { lastEvent, isConnected };
}