initial setup

This commit is contained in:
Bojan Kucera 2025-06-02 08:15:20 -04:00
commit 232a63dfe8
61 changed files with 4985 additions and 0 deletions

View file

@ -0,0 +1,455 @@
import { useState, useEffect, useCallback, useMemo } from 'react';
import {
Card,
Title,
Text,
Metric,
Flex,
Badge,
Grid,
AreaChart,
DonutChart,
BarChart,
LineChart,
Tab,
TabGroup,
TabList,
TabPanel,
TabPanels,
Button,
} from '@tremor/react';
import type { MarketData, OHLCV, HealthStatus } from '@stock-bot/shared-types';
const API_BASE = 'http://localhost:3001';
interface DashboardData {
marketData: MarketData[];
ohlcvData: OHLCV[];
serviceHealth: HealthStatus | null;
lastUpdate: Date | null;
}
export function TradingDashboard() {
const [data, setData] = useState<DashboardData>({
marketData: [],
ohlcvData: [],
serviceHealth: null,
lastUpdate: null,
});
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [wsConnected, setWsConnected] = useState(false);
const symbols = useMemo(() => ['AAPL', 'GOOGL', 'MSFT', 'TSLA', 'AMZN'], []);
// Memoized fetch functions
const fetchServiceHealth = useCallback(async () => {
try {
const response = await fetch(`${API_BASE}/health`);
const health = await response.json();
return health;
} catch (error) {
console.error('Error fetching service health:', error);
return null;
}
}, []);
const fetchMarketData = useCallback(async () => {
try {
const promises = symbols.map(async (symbol) => {
const response = await fetch(`${API_BASE}/api/market-data/${symbol}`);
const result = await response.json();
return result.success ? result.data : null;
});
const results = await Promise.all(promises);
return results.filter(Boolean);
} catch (error) {
console.error('Error fetching market data:', error);
return [];
}
}, [symbols]);
const fetchOHLCVData = useCallback(async (symbol: string = 'AAPL') => {
try {
const response = await fetch(`${API_BASE}/api/ohlcv/${symbol}?limit=50`);
const result = await response.json();
return result.success ? result.data : [];
} catch (error) {
console.error('Error fetching OHLCV data:', error);
return [];
}
}, []);
// Load all data function
const loadData = useCallback(async () => {
setLoading(true);
try {
const [health, marketData, ohlcvData] = await Promise.all([
fetchServiceHealth(),
fetchMarketData(),
fetchOHLCVData(),
]);
setData({
serviceHealth: health,
marketData,
ohlcvData,
lastUpdate: new Date(),
});
setError(null);
} catch (err) {
setError(err instanceof Error ? err.message : 'Unknown error');
} finally {
setLoading(false);
}
}, [fetchServiceHealth, fetchMarketData, fetchOHLCVData]);
// WebSocket connection and data loading
useEffect(() => {
let ws: WebSocket | null = null;
const connectWebSocket = () => {
try {
ws = new WebSocket('ws://localhost:3001');
ws.onopen = () => {
console.log('WebSocket connected');
setWsConnected(true);
};
ws.onmessage = (event) => {
try {
const message = JSON.parse(event.data);
if (message.type === 'market-data' && message.data) {
// Update specific symbol data
setData(prev => ({
...prev,
marketData: prev.marketData.map(item =>
item.symbol === message.data.symbol ? message.data : item
),
lastUpdate: new Date(),
}));
}
} catch (error) {
console.error('Error parsing WebSocket message:', error);
}
};
ws.onclose = () => {
console.log('WebSocket disconnected');
setWsConnected(false);
// Reconnect after 5 seconds
setTimeout(connectWebSocket, 5000);
};
ws.onerror = (error) => {
console.error('WebSocket error:', error);
setWsConnected(false);
};
} catch (error) {
console.error('Failed to connect WebSocket:', error);
setWsConnected(false);
}
};
// Initial data load
loadData();
// Set up periodic refresh
const interval = setInterval(loadData, 30000); // Refresh every 30 seconds
// Attempt WebSocket connection
connectWebSocket();
return () => {
clearInterval(interval);
if (ws) {
ws.close();
}
};
}, [loadData]);
// Prepare chart data
const chartData = data.ohlcvData.map((item) => ({
time: new Date(item.timestamp).toLocaleTimeString(),
price: item.close,
volume: item.volume,
}));
// Prepare portfolio allocation data (demo)
const portfolioData = data.marketData.map((item, index) => ({
name: item.symbol,
value: (index + 1) * 20000, // Demo allocation
}));
// Calculate total portfolio value
const totalValue = portfolioData.reduce((sum, item) => sum + item.value, 0);
// Performance metrics (demo)
const performanceData = [
{ date: 'Mon', value: 98000 },
{ date: 'Tue', value: 102000 },
{ date: 'Wed', value: 105000 },
{ date: 'Thu', value: 103000 },
{ date: 'Fri', value: 108000 },
];
if (loading && data.marketData.length === 0) {
return (
<div className="p-8 bg-slate-50 min-h-screen">
<div className="max-w-7xl mx-auto">
<div className="text-center">
<Title>Loading Trading Dashboard...</Title>
<Text className="mt-4">Connecting to market data services</Text>
</div>
</div>
</div>
);
}
return (
<div className="p-8 bg-slate-50 min-h-screen">
<div className="max-w-7xl mx-auto">
{/* Header */}
<div className="mb-8">
<Flex justifyContent="between" alignItems="center">
<div>
<Title className="text-3xl font-bold">🤖 Stock Bot Dashboard</Title>
<Text className="mt-2">Real-time market data monitoring</Text>
</div>
<div className="text-right">
<Button onClick={loadData} disabled={loading}>
{loading ? 'Refreshing...' : 'Refresh Data'}
</Button>
{data.lastUpdate && (
<Text className="mt-2">
Last update: {data.lastUpdate.toLocaleTimeString()}
</Text>
)}
</div>
</Flex>
</div>
{error && (
<Card className="mb-6">
<Text color="red">Error: {error}</Text>
</Card>
)}
{/* Top Metrics */}
<Grid numItems={1} numItemsSm={2} numItemsLg={4} className="gap-6 mb-8">
<Card>
<Text>Portfolio Value</Text>
<Metric>${totalValue.toLocaleString()}</Metric>
<Flex className="mt-4">
<Badge color="emerald">+8.2%</Badge>
</Flex>
</Card>
<Card>
<Text>Service Status</Text>
<Flex alignItems="center" className="mt-2">
<Badge
color={data.serviceHealth?.status === 'healthy' ? 'emerald' : 'red'}
>
{data.serviceHealth?.status || 'Unknown'}
</Badge>
</Flex>
<Flex alignItems="center" className="mt-2">
<Text className="text-sm mr-2">WebSocket:</Text>
<Badge
color={wsConnected ? 'emerald' : 'red'}
size="sm"
>
{wsConnected ? 'Connected' : 'Disconnected'}
</Badge>
</Flex>
</Card>
<Card>
<Text>Active Symbols</Text>
<Metric>{data.marketData.length}</Metric>
</Card>
<Card>
<Text>Daily P&L</Text>
<Metric color="emerald">+$2,450</Metric>
<Flex className="mt-4">
<Badge color="emerald">+2.3%</Badge>
</Flex>
</Card>
</Grid>
{/* Main Content Tabs */}
<TabGroup>
<TabList className="mb-8">
<Tab>Market Data</Tab>
<Tab>Portfolio</Tab>
<Tab>Charts</Tab>
<Tab>Performance</Tab>
</TabList>
<TabPanels>
{/* Market Data Tab */}
<TabPanel>
<Grid numItems={1} numItemsLg={2} className="gap-6">
<Card>
<Title>Live Prices</Title>
<div className="mt-6">
{data.marketData.map((item) => (
<div
key={item.symbol}
className="flex justify-between items-center py-3 border-b border-gray-200 last:border-b-0"
>
<div>
<Text className="font-semibold">{item.symbol}</Text>
<Text className="text-sm text-gray-500">
Vol: {item.volume.toLocaleString()}
</Text>
</div>
<div className="text-right">
<Text className="font-bold text-lg">
${item.price.toFixed(2)}
</Text>
<Text className="text-sm text-gray-500">
Bid: ${item.bid.toFixed(2)} | Ask: ${item.ask.toFixed(2)}
</Text>
</div>
</div>
))}
</div>
</Card>
<Card>
<Title>Market Overview</Title>
<BarChart
className="mt-6"
data={data.marketData}
index="symbol"
categories={["price"]}
colors={["blue"]}
valueFormatter={(value) => `$${value.toFixed(2)}`}
/>
</Card>
</Grid>
</TabPanel>
{/* Portfolio Tab */}
<TabPanel>
<Grid numItems={1} numItemsLg={2} className="gap-6">
<Card>
<Title>Portfolio Allocation</Title>
<DonutChart
className="mt-6"
data={portfolioData}
category="value"
index="name"
valueFormatter={(value) => `$${value.toLocaleString()}`}
colors={["slate", "violet", "indigo", "rose", "cyan"]}
/>
</Card>
<Card>
<Title>Holdings</Title>
<div className="mt-6">
{portfolioData.map((item) => (
<div
key={item.name}
className="flex justify-between items-center py-3 border-b border-gray-200 last:border-b-0"
>
<Text className="font-semibold">{item.name}</Text>
<div className="text-right">
<Text className="font-bold">
${item.value.toLocaleString()}
</Text>
<Text className="text-sm text-gray-500">
{((item.value / totalValue) * 100).toFixed(1)}%
</Text>
</div>
</div>
))}
</div>
</Card>
</Grid>
</TabPanel>
{/* Charts Tab */}
<TabPanel>
<Grid numItems={1} className="gap-6">
<Card>
<Title>AAPL Price Chart</Title>
<LineChart
className="mt-6"
data={chartData}
index="time"
categories={["price"]}
colors={["indigo"]}
valueFormatter={(value) => `$${value.toFixed(2)}`}
yAxisWidth={60}
/>
</Card>
<Card>
<Title>Volume Analysis</Title>
<AreaChart
className="mt-6"
data={chartData}
index="time"
categories={["volume"]}
colors={["emerald"]}
valueFormatter={(value) => value.toLocaleString()}
yAxisWidth={80}
/>
</Card>
</Grid>
</TabPanel>
{/* Performance Tab */}
<TabPanel>
<Grid numItems={1} numItemsLg={2} className="gap-6">
<Card>
<Title>Weekly Performance</Title>
<AreaChart
className="mt-6"
data={performanceData}
index="date"
categories={["value"]}
colors={["emerald"]}
valueFormatter={(value) => `$${value.toLocaleString()}`}
/>
</Card>
<Card>
<Title>Performance Metrics</Title>
<div className="mt-6 space-y-4">
<div className="flex justify-between">
<Text>Total Return</Text>
<Badge color="emerald">+12.5%</Badge>
</div>
<div className="flex justify-between">
<Text>Sharpe Ratio</Text>
<Badge color="blue">1.8</Badge>
</div>
<div className="flex justify-between">
<Text>Max Drawdown</Text>
<Badge color="red">-5.2%</Badge>
</div>
<div className="flex justify-between">
<Text>Win Rate</Text>
<Badge color="emerald">68%</Badge>
</div>
<div className="flex justify-between">
<Text>Avg Trade</Text>
<Badge color="indigo">+$245</Badge>
</div>
</div>
</Card>
</Grid>
</TabPanel>
</TabPanels>
</TabGroup>
</div>
</div>
);
}