642 lines
18 KiB
TypeScript
642 lines
18 KiB
TypeScript
import { DataTable } from '@/components/ui';
|
|
import type { ColumnDef } from '@tanstack/react-table';
|
|
import React from 'react';
|
|
|
|
interface PortfolioItem {
|
|
symbol: string;
|
|
quantity: number;
|
|
avgPrice: number;
|
|
currentPrice: number;
|
|
value: number;
|
|
change: number;
|
|
changePercent: number;
|
|
// Additional columns for stress testing
|
|
volume: number;
|
|
marketCap: number;
|
|
pe: number;
|
|
pb: number;
|
|
roe: number;
|
|
debt: number;
|
|
revenue: number;
|
|
earnings: number;
|
|
dividend: number;
|
|
beta: number;
|
|
rsi: number;
|
|
macd: number;
|
|
sma20: number;
|
|
sma50: number;
|
|
sma200: number;
|
|
support: number;
|
|
resistance: number;
|
|
volatility: number;
|
|
sharpe: number;
|
|
alpha: number;
|
|
correlation: number;
|
|
sector: string;
|
|
industry: string;
|
|
country: string;
|
|
exchange: string;
|
|
currency: string;
|
|
lastUpdate: string;
|
|
analyst1: string;
|
|
analyst2: string;
|
|
analyst3: string;
|
|
rating1: number;
|
|
rating2: number;
|
|
rating3: number;
|
|
target1: number;
|
|
target2: number;
|
|
target3: number;
|
|
risk: string;
|
|
esg: number;
|
|
}
|
|
|
|
export function PortfolioTable() {
|
|
// Generate 100,000 rows of sample data
|
|
const data: PortfolioItem[] = React.useMemo(() => {
|
|
const symbols = [
|
|
'AAPL',
|
|
'GOOGL',
|
|
'MSFT',
|
|
'TSLA',
|
|
'AMZN',
|
|
'META',
|
|
'NFLX',
|
|
'NVDA',
|
|
'AMD',
|
|
'INTC',
|
|
'CRM',
|
|
'ORCL',
|
|
'IBM',
|
|
'CSCO',
|
|
'UBER',
|
|
'LYFT',
|
|
'SNAP',
|
|
'TWTR',
|
|
'SPOT',
|
|
'SQ',
|
|
];
|
|
const sectors = [
|
|
'Technology',
|
|
'Healthcare',
|
|
'Finance',
|
|
'Energy',
|
|
'Consumer',
|
|
'Industrial',
|
|
'Materials',
|
|
'Utilities',
|
|
'Real Estate',
|
|
'Telecom',
|
|
];
|
|
const industries = [
|
|
'Software',
|
|
'Hardware',
|
|
'Biotech',
|
|
'Banking',
|
|
'Oil & Gas',
|
|
'Retail',
|
|
'Manufacturing',
|
|
'Mining',
|
|
'Utilities',
|
|
'REITs',
|
|
];
|
|
const countries = [
|
|
'USA',
|
|
'Canada',
|
|
'UK',
|
|
'Germany',
|
|
'Japan',
|
|
'China',
|
|
'India',
|
|
'Brazil',
|
|
'Australia',
|
|
'France',
|
|
];
|
|
const exchanges = ['NYSE', 'NASDAQ', 'LSE', 'TSX', 'Nikkei', 'SSE', 'BSE', 'ASX', 'Euronext'];
|
|
const currencies = ['USD', 'CAD', 'GBP', 'EUR', 'JPY', 'CNY', 'INR', 'BRL', 'AUD'];
|
|
const analysts = [
|
|
'Goldman Sachs',
|
|
'Morgan Stanley',
|
|
'JPMorgan',
|
|
'Bank of America',
|
|
'Wells Fargo',
|
|
'Credit Suisse',
|
|
'Deutsche Bank',
|
|
'Barclays',
|
|
'UBS',
|
|
'Citigroup',
|
|
];
|
|
const risks = ['Low', 'Medium', 'High', 'Very High'];
|
|
|
|
return Array.from({ length: 100000 }, (_, i) => {
|
|
const basePrice = 50 + Math.random() * 500;
|
|
const change = (Math.random() - 0.5) * 20;
|
|
const quantity = Math.floor(Math.random() * 1000) + 1;
|
|
|
|
return {
|
|
symbol: `${symbols[i % symbols.length]}${Math.floor(i / symbols.length)}`,
|
|
quantity,
|
|
avgPrice: basePrice,
|
|
currentPrice: basePrice + change,
|
|
value: (basePrice + change) * quantity,
|
|
change: change * quantity,
|
|
changePercent: (change / basePrice) * 100,
|
|
volume: Math.floor(Math.random() * 10000000),
|
|
marketCap: Math.floor(Math.random() * 1000000000000),
|
|
pe: Math.random() * 50 + 5,
|
|
pb: Math.random() * 10 + 0.5,
|
|
roe: Math.random() * 30 + 5,
|
|
debt: Math.random() * 50,
|
|
revenue: Math.floor(Math.random() * 100000000000),
|
|
earnings: Math.floor(Math.random() * 10000000000),
|
|
dividend: Math.random() * 5,
|
|
beta: Math.random() * 3 + 0.5,
|
|
rsi: Math.random() * 100,
|
|
macd: (Math.random() - 0.5) * 10,
|
|
sma20: basePrice + (Math.random() - 0.5) * 10,
|
|
sma50: basePrice + (Math.random() - 0.5) * 20,
|
|
sma200: basePrice + (Math.random() - 0.5) * 50,
|
|
support: basePrice - Math.random() * 20,
|
|
resistance: basePrice + Math.random() * 20,
|
|
volatility: Math.random() * 100,
|
|
sharpe: Math.random() * 3,
|
|
alpha: (Math.random() - 0.5) * 20,
|
|
correlation: (Math.random() - 0.5) * 2,
|
|
sector: sectors[Math.floor(Math.random() * sectors.length)] || 'Technology',
|
|
industry: industries[Math.floor(Math.random() * industries.length)] || 'Software',
|
|
country: countries[Math.floor(Math.random() * countries.length)] || 'USA',
|
|
exchange: exchanges[Math.floor(Math.random() * exchanges.length)] || 'NYSE',
|
|
currency: currencies[Math.floor(Math.random() * currencies.length)] || 'USD',
|
|
lastUpdate: new Date(Date.now() - Math.random() * 86400000).toISOString(),
|
|
analyst1: analysts[Math.floor(Math.random() * analysts.length)] || 'Goldman Sachs',
|
|
analyst2: analysts[Math.floor(Math.random() * analysts.length)] || 'Morgan Stanley',
|
|
analyst3: analysts[Math.floor(Math.random() * analysts.length)] || 'JPMorgan',
|
|
rating1: Math.random() * 5 + 1,
|
|
rating2: Math.random() * 5 + 1,
|
|
rating3: Math.random() * 5 + 1,
|
|
target1: basePrice + (Math.random() - 0.3) * 50,
|
|
target2: basePrice + (Math.random() - 0.3) * 50,
|
|
target3: basePrice + (Math.random() - 0.3) * 50,
|
|
risk: risks[Math.floor(Math.random() * risks.length)] || 'Medium',
|
|
esg: Math.random() * 100,
|
|
};
|
|
});
|
|
}, []);
|
|
|
|
const columns: ColumnDef<PortfolioItem>[] = [
|
|
{
|
|
id: 'symbol',
|
|
header: 'Symbol',
|
|
accessorKey: 'symbol',
|
|
size: 120,
|
|
cell: ({ getValue }) => (
|
|
<span className="font-mono font-bold text-primary-400">{getValue() as string}</span>
|
|
),
|
|
},
|
|
{
|
|
id: 'quantity',
|
|
header: 'Quantity',
|
|
accessorKey: 'quantity',
|
|
size: 100,
|
|
cell: ({ getValue }) => (
|
|
<span className="font-mono">{(getValue() as number).toLocaleString()}</span>
|
|
),
|
|
},
|
|
{
|
|
id: 'avgPrice',
|
|
header: 'Avg Price',
|
|
accessorKey: 'avgPrice',
|
|
size: 120,
|
|
cell: ({ getValue }) => (
|
|
<span className="font-mono">${(getValue() as number).toFixed(2)}</span>
|
|
),
|
|
},
|
|
{
|
|
id: 'currentPrice',
|
|
header: 'Current Price',
|
|
accessorKey: 'currentPrice',
|
|
size: 120,
|
|
cell: ({ getValue }) => (
|
|
<span className="font-mono">${(getValue() as number).toFixed(2)}</span>
|
|
),
|
|
},
|
|
{
|
|
id: 'value',
|
|
header: 'Value',
|
|
accessorKey: 'value',
|
|
size: 120,
|
|
cell: ({ getValue }) => (
|
|
<span className="font-mono font-bold">${(getValue() as number).toLocaleString()}</span>
|
|
),
|
|
},
|
|
{
|
|
id: 'change',
|
|
header: 'P&L',
|
|
accessorKey: 'change',
|
|
size: 120,
|
|
cell: ({ getValue }) => {
|
|
const value = getValue() as number;
|
|
const isPositive = value >= 0;
|
|
return (
|
|
<span className={`font-mono font-medium ${isPositive ? 'text-success' : 'text-danger'}`}>
|
|
{isPositive ? '+' : ''}${value.toLocaleString()}
|
|
</span>
|
|
);
|
|
},
|
|
},
|
|
{
|
|
id: 'changePercent',
|
|
header: 'P&L %',
|
|
accessorKey: 'changePercent',
|
|
size: 100,
|
|
cell: ({ getValue }) => {
|
|
const value = getValue() as number;
|
|
const isPositive = value >= 0;
|
|
return (
|
|
<span className={`font-mono font-medium ${isPositive ? 'text-success' : 'text-danger'}`}>
|
|
{isPositive ? '+' : ''}
|
|
{value.toFixed(2)}%
|
|
</span>
|
|
);
|
|
},
|
|
},
|
|
{
|
|
id: 'volume',
|
|
header: 'Volume',
|
|
accessorKey: 'volume',
|
|
size: 120,
|
|
cell: ({ getValue }) => (
|
|
<span className="font-mono text-text-secondary">
|
|
{(getValue() as number).toLocaleString()}
|
|
</span>
|
|
),
|
|
},
|
|
{
|
|
id: 'marketCap',
|
|
header: 'Market Cap',
|
|
accessorKey: 'marketCap',
|
|
size: 120,
|
|
cell: ({ getValue }) => {
|
|
const value = getValue() as number;
|
|
if (value >= 1e12) {return <span className="font-mono">${(value / 1e12).toFixed(2)}T</span>;}
|
|
if (value >= 1e9) {return <span className="font-mono">${(value / 1e9).toFixed(2)}B</span>;}
|
|
if (value >= 1e6) {return <span className="font-mono">${(value / 1e6).toFixed(2)}M</span>;}
|
|
return <span className="font-mono">${value.toLocaleString()}</span>;
|
|
},
|
|
},
|
|
{
|
|
id: 'pe',
|
|
header: 'P/E',
|
|
accessorKey: 'pe',
|
|
size: 80,
|
|
cell: ({ getValue }) => (
|
|
<span className="font-mono">{(getValue() as number).toFixed(2)}</span>
|
|
),
|
|
},
|
|
{
|
|
id: 'pb',
|
|
header: 'P/B',
|
|
accessorKey: 'pb',
|
|
size: 80,
|
|
cell: ({ getValue }) => (
|
|
<span className="font-mono">{(getValue() as number).toFixed(2)}</span>
|
|
),
|
|
},
|
|
{
|
|
id: 'roe',
|
|
header: 'ROE %',
|
|
accessorKey: 'roe',
|
|
size: 80,
|
|
cell: ({ getValue }) => (
|
|
<span className="font-mono">{(getValue() as number).toFixed(1)}%</span>
|
|
),
|
|
},
|
|
{
|
|
id: 'debt',
|
|
header: 'Debt %',
|
|
accessorKey: 'debt',
|
|
size: 80,
|
|
cell: ({ getValue }) => (
|
|
<span className="font-mono">{(getValue() as number).toFixed(1)}%</span>
|
|
),
|
|
},
|
|
{
|
|
id: 'revenue',
|
|
header: 'Revenue',
|
|
accessorKey: 'revenue',
|
|
size: 120,
|
|
cell: ({ getValue }) => {
|
|
const value = getValue() as number;
|
|
if (value >= 1e9) {return <span className="font-mono">${(value / 1e9).toFixed(2)}B</span>;}
|
|
if (value >= 1e6) {return <span className="font-mono">${(value / 1e6).toFixed(2)}M</span>;}
|
|
return <span className="font-mono">${value.toLocaleString()}</span>;
|
|
},
|
|
},
|
|
{
|
|
id: 'earnings',
|
|
header: 'Earnings',
|
|
accessorKey: 'earnings',
|
|
size: 120,
|
|
cell: ({ getValue }) => {
|
|
const value = getValue() as number;
|
|
if (value >= 1e9) {return <span className="font-mono">${(value / 1e9).toFixed(2)}B</span>;}
|
|
if (value >= 1e6) {return <span className="font-mono">${(value / 1e6).toFixed(2)}M</span>;}
|
|
return <span className="font-mono">${value.toLocaleString()}</span>;
|
|
},
|
|
},
|
|
{
|
|
id: 'dividend',
|
|
header: 'Dividend %',
|
|
accessorKey: 'dividend',
|
|
size: 100,
|
|
cell: ({ getValue }) => {
|
|
const value = getValue() as number;
|
|
return <span className="font-mono text-success">{value.toFixed(2)}%</span>;
|
|
},
|
|
},
|
|
{
|
|
id: 'beta',
|
|
header: 'Beta',
|
|
accessorKey: 'beta',
|
|
size: 80,
|
|
cell: ({ getValue }) => {
|
|
const value = getValue() as number;
|
|
const color = value > 1 ? 'text-warning' : value < 1 ? 'text-success' : 'text-text-primary';
|
|
return <span className={`font-mono ${color}`}>{value.toFixed(2)}</span>;
|
|
},
|
|
},
|
|
{
|
|
id: 'rsi',
|
|
header: 'RSI',
|
|
accessorKey: 'rsi',
|
|
size: 80,
|
|
cell: ({ getValue }) => {
|
|
const value = getValue() as number;
|
|
const color =
|
|
value > 70 ? 'text-danger' : value < 30 ? 'text-success' : 'text-text-primary';
|
|
return <span className={`font-mono ${color}`}>{value.toFixed(1)}</span>;
|
|
},
|
|
},
|
|
{
|
|
id: 'macd',
|
|
header: 'MACD',
|
|
accessorKey: 'macd',
|
|
size: 80,
|
|
cell: ({ getValue }) => {
|
|
const value = getValue() as number;
|
|
const color = value > 0 ? 'text-success' : 'text-danger';
|
|
return <span className={`font-mono ${color}`}>{value.toFixed(2)}</span>;
|
|
},
|
|
},
|
|
{
|
|
id: 'sma20',
|
|
header: 'SMA 20',
|
|
accessorKey: 'sma20',
|
|
size: 100,
|
|
cell: ({ getValue }) => (
|
|
<span className="font-mono">${(getValue() as number).toFixed(2)}</span>
|
|
),
|
|
},
|
|
{
|
|
id: 'sma50',
|
|
header: 'SMA 50',
|
|
accessorKey: 'sma50',
|
|
size: 100,
|
|
cell: ({ getValue }) => (
|
|
<span className="font-mono">${(getValue() as number).toFixed(2)}</span>
|
|
),
|
|
},
|
|
{
|
|
id: 'sma200',
|
|
header: 'SMA 200',
|
|
accessorKey: 'sma200',
|
|
size: 100,
|
|
cell: ({ getValue }) => (
|
|
<span className="font-mono">${(getValue() as number).toFixed(2)}</span>
|
|
),
|
|
},
|
|
{
|
|
id: 'support',
|
|
header: 'Support',
|
|
accessorKey: 'support',
|
|
size: 100,
|
|
cell: ({ getValue }) => (
|
|
<span className="font-mono text-success">${(getValue() as number).toFixed(2)}</span>
|
|
),
|
|
},
|
|
{
|
|
id: 'resistance',
|
|
header: 'Resistance',
|
|
accessorKey: 'resistance',
|
|
size: 100,
|
|
cell: ({ getValue }) => (
|
|
<span className="font-mono text-danger">${(getValue() as number).toFixed(2)}</span>
|
|
),
|
|
},
|
|
{
|
|
id: 'volatility',
|
|
header: 'Volatility',
|
|
accessorKey: 'volatility',
|
|
size: 100,
|
|
cell: ({ getValue }) => (
|
|
<span className="font-mono">{(getValue() as number).toFixed(1)}%</span>
|
|
),
|
|
},
|
|
{
|
|
id: 'sharpe',
|
|
header: 'Sharpe',
|
|
accessorKey: 'sharpe',
|
|
size: 80,
|
|
cell: ({ getValue }) => (
|
|
<span className="font-mono">{(getValue() as number).toFixed(2)}</span>
|
|
),
|
|
},
|
|
{
|
|
id: 'alpha',
|
|
header: 'Alpha',
|
|
accessorKey: 'alpha',
|
|
size: 80,
|
|
cell: ({ getValue }) => {
|
|
const value = getValue() as number;
|
|
const color = value > 0 ? 'text-success' : 'text-danger';
|
|
return <span className={`font-mono ${color}`}>{value.toFixed(2)}</span>;
|
|
},
|
|
},
|
|
{
|
|
id: 'correlation',
|
|
header: 'Correlation',
|
|
accessorKey: 'correlation',
|
|
size: 100,
|
|
cell: ({ getValue }) => (
|
|
<span className="font-mono">{(getValue() as number).toFixed(2)}</span>
|
|
),
|
|
},
|
|
{
|
|
id: 'sector',
|
|
header: 'Sector',
|
|
accessorKey: 'sector',
|
|
size: 150,
|
|
cell: ({ getValue }) => <span className="text-text-secondary">{getValue() as string}</span>,
|
|
},
|
|
{
|
|
id: 'industry',
|
|
header: 'Industry',
|
|
accessorKey: 'industry',
|
|
size: 150,
|
|
cell: ({ getValue }) => <span className="text-text-secondary">{getValue() as string}</span>,
|
|
},
|
|
{
|
|
id: 'country',
|
|
header: 'Country',
|
|
accessorKey: 'country',
|
|
size: 100,
|
|
cell: ({ getValue }) => <span className="text-text-secondary">{getValue() as string}</span>,
|
|
},
|
|
{
|
|
id: 'exchange',
|
|
header: 'Exchange',
|
|
accessorKey: 'exchange',
|
|
size: 100,
|
|
cell: ({ getValue }) => <span className="text-text-secondary">{getValue() as string}</span>,
|
|
},
|
|
{
|
|
id: 'currency',
|
|
header: 'Currency',
|
|
accessorKey: 'currency',
|
|
size: 80,
|
|
cell: ({ getValue }) => (
|
|
<span className="font-mono text-text-secondary">{getValue() as string}</span>
|
|
),
|
|
},
|
|
{
|
|
id: 'lastUpdate',
|
|
header: 'Last Update',
|
|
accessorKey: 'lastUpdate',
|
|
size: 150,
|
|
cell: ({ getValue }) => (
|
|
<span className="text-xs text-text-muted">
|
|
{new Date(getValue() as string).toLocaleString()}
|
|
</span>
|
|
),
|
|
},
|
|
{
|
|
id: 'analyst1',
|
|
header: 'Analyst 1',
|
|
accessorKey: 'analyst1',
|
|
size: 120,
|
|
cell: ({ getValue }) => (
|
|
<span className="text-xs text-text-secondary">{getValue() as string}</span>
|
|
),
|
|
},
|
|
{
|
|
id: 'analyst2',
|
|
header: 'Analyst 2',
|
|
accessorKey: 'analyst2',
|
|
size: 120,
|
|
cell: ({ getValue }) => (
|
|
<span className="text-xs text-text-secondary">{getValue() as string}</span>
|
|
),
|
|
},
|
|
{
|
|
id: 'analyst3',
|
|
header: 'Analyst 3',
|
|
accessorKey: 'analyst3',
|
|
size: 120,
|
|
cell: ({ getValue }) => (
|
|
<span className="text-xs text-text-secondary">{getValue() as string}</span>
|
|
),
|
|
},
|
|
{
|
|
id: 'rating1',
|
|
header: 'Rating 1',
|
|
accessorKey: 'rating1',
|
|
size: 80,
|
|
cell: ({ getValue }) => {
|
|
const value = getValue() as number;
|
|
const color = value >= 4 ? 'text-success' : value >= 3 ? 'text-warning' : 'text-danger';
|
|
return <span className={`font-mono ${color}`}>{value.toFixed(1)}</span>;
|
|
},
|
|
},
|
|
{
|
|
id: 'rating2',
|
|
header: 'Rating 2',
|
|
accessorKey: 'rating2',
|
|
size: 80,
|
|
cell: ({ getValue }) => {
|
|
const value = getValue() as number;
|
|
const color = value >= 4 ? 'text-success' : value >= 3 ? 'text-warning' : 'text-danger';
|
|
return <span className={`font-mono ${color}`}>{value.toFixed(1)}</span>;
|
|
},
|
|
},
|
|
{
|
|
id: 'rating3',
|
|
header: 'Rating 3',
|
|
accessorKey: 'rating3',
|
|
size: 80,
|
|
cell: ({ getValue }) => {
|
|
const value = getValue() as number;
|
|
const color = value >= 4 ? 'text-success' : value >= 3 ? 'text-warning' : 'text-danger';
|
|
return <span className={`font-mono ${color}`}>{value.toFixed(1)}</span>;
|
|
},
|
|
},
|
|
{
|
|
id: 'target1',
|
|
header: 'Target 1',
|
|
accessorKey: 'target1',
|
|
size: 100,
|
|
cell: ({ getValue }) => (
|
|
<span className="font-mono">${(getValue() as number).toFixed(2)}</span>
|
|
),
|
|
},
|
|
{
|
|
id: 'target2',
|
|
header: 'Target 2',
|
|
accessorKey: 'target2',
|
|
size: 100,
|
|
cell: ({ getValue }) => (
|
|
<span className="font-mono">${(getValue() as number).toFixed(2)}</span>
|
|
),
|
|
},
|
|
{
|
|
id: 'target3',
|
|
header: 'Target 3',
|
|
accessorKey: 'target3',
|
|
size: 100,
|
|
cell: ({ getValue }) => (
|
|
<span className="font-mono">${(getValue() as number).toFixed(2)}</span>
|
|
),
|
|
},
|
|
{
|
|
id: 'risk',
|
|
header: 'Risk Level',
|
|
accessorKey: 'risk',
|
|
size: 100,
|
|
cell: ({ getValue }) => {
|
|
const value = getValue() as string;
|
|
const color =
|
|
value === 'Low' ? 'text-success' : value === 'Medium' ? 'text-warning' : 'text-danger';
|
|
return <span className={`px-2 py-1 rounded text-xs font-medium ${color}`}>{value}</span>;
|
|
},
|
|
},
|
|
{
|
|
id: 'esg',
|
|
header: 'ESG Score',
|
|
accessorKey: 'esg',
|
|
size: 100,
|
|
cell: ({ getValue }) => {
|
|
const value = getValue() as number;
|
|
const color = value >= 70 ? 'text-success' : value >= 40 ? 'text-warning' : 'text-danger';
|
|
return <span className={`font-mono ${color}`}>{value.toFixed(0)}</span>;
|
|
},
|
|
},
|
|
];
|
|
|
|
return (
|
|
<DataTable
|
|
data={data}
|
|
columns={columns}
|
|
onRowClick={_row => {/* Handle row click */}}
|
|
className="border border-border rounded-lg"
|
|
/>
|
|
);
|
|
}
|