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.
108 lines
3.1 KiB
TypeScript
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 };
|
|
}
|