initial setup
This commit is contained in:
commit
232a63dfe8
61 changed files with 4985 additions and 0 deletions
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue