trading/dashboard/src/hooks/useWebSocket.ts
Viktor Barzin 8d6e666280
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.
2026-02-22 15:54:44 +00:00

108 lines
3.1 KiB
TypeScript

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 };
}