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:
parent
f121f376ae
commit
8d6e666280
12 changed files with 1761 additions and 0 deletions
83
dashboard/src/hooks/usePortfolio.ts
Normal file
83
dashboard/src/hooks/usePortfolio.ts
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
import { useQuery } from '@tanstack/react-query';
|
||||
import client from '../api/client';
|
||||
|
||||
export interface PortfolioSummary {
|
||||
total_value: number;
|
||||
cash: number;
|
||||
buying_power: number;
|
||||
daily_pnl: number;
|
||||
daily_pnl_pct: number;
|
||||
total_pnl: number;
|
||||
total_pnl_pct: number;
|
||||
trading_active: boolean;
|
||||
}
|
||||
|
||||
export interface Position {
|
||||
id: string;
|
||||
ticker: string;
|
||||
qty: number;
|
||||
avg_entry: number;
|
||||
current_price: number;
|
||||
unrealized_pnl: number;
|
||||
unrealized_pnl_pct: number;
|
||||
stop_loss: number | null;
|
||||
take_profit: number | null;
|
||||
}
|
||||
|
||||
export interface EquityPoint {
|
||||
timestamp: string;
|
||||
value: number;
|
||||
}
|
||||
|
||||
export interface PortfolioMetrics {
|
||||
roi: number;
|
||||
sharpe: number;
|
||||
win_rate: number;
|
||||
max_drawdown: number;
|
||||
total_trades: number;
|
||||
avg_hold_duration: string;
|
||||
}
|
||||
|
||||
export function usePortfolio() {
|
||||
return useQuery<PortfolioSummary>({
|
||||
queryKey: ['portfolio'],
|
||||
queryFn: async () => {
|
||||
const response = await client.get('/portfolio');
|
||||
return response.data;
|
||||
},
|
||||
refetchInterval: 30_000,
|
||||
});
|
||||
}
|
||||
|
||||
export function usePositions() {
|
||||
return useQuery<Position[]>({
|
||||
queryKey: ['positions'],
|
||||
queryFn: async () => {
|
||||
const response = await client.get('/portfolio/positions');
|
||||
return response.data;
|
||||
},
|
||||
refetchInterval: 15_000,
|
||||
});
|
||||
}
|
||||
|
||||
export function useEquityCurve(period: string = '1M') {
|
||||
return useQuery<EquityPoint[]>({
|
||||
queryKey: ['equity-curve', period],
|
||||
queryFn: async () => {
|
||||
const response = await client.get('/portfolio/history', {
|
||||
params: { period },
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function usePortfolioMetrics() {
|
||||
return useQuery<PortfolioMetrics>({
|
||||
queryKey: ['portfolio', 'metrics'],
|
||||
queryFn: async () => {
|
||||
const response = await client.get('/portfolio/metrics');
|
||||
return response.data;
|
||||
},
|
||||
});
|
||||
}
|
||||
108
dashboard/src/hooks/useWebSocket.ts
Normal file
108
dashboard/src/hooks/useWebSocket.ts
Normal 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 };
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue