[OQI-003] feat: Add TradingStatsPanel and OrderBookPanel components

- Create TradingStatsPanel with trading metrics (win rate, P&L, profit factor)
- Create OrderBookPanel with bids/asks visualization and spread
- Update Trading.tsx with toggle buttons for all panels
- Implement indicator toggle logic with Set state
- Add stats panel: current streak, avg hold time, portfolio summary
- Add order book: depth bars, click-to-fill, configurable rows

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Adrian Flores Cortes 2026-01-25 09:51:07 -06:00
parent 49d6492c91
commit b798e2678c
3 changed files with 618 additions and 2 deletions

View File

@ -0,0 +1,183 @@
/**
* OrderBookPanel Component
* Displays market depth with bids and asks
*/
import React, { useEffect, useState, useCallback } from 'react';
import {
Bars3BottomLeftIcon,
ArrowPathIcon,
} from '@heroicons/react/24/solid';
import { useTradingStore } from '../../../stores/tradingStore';
import type { OrderBook } from '../../../types/trading.types';
interface OrderBookPanelProps {
symbol: string;
onPriceClick?: (price: number) => void;
}
export const OrderBookPanel: React.FC<OrderBookPanelProps> = ({ symbol, onPriceClick }) => {
const { orderBook, loadingOrderBook, fetchOrderBook } = useTradingStore();
const [displayRows, setDisplayRows] = useState(10);
// Fetch order book on mount and symbol change
useEffect(() => {
fetchOrderBook(symbol, 20);
// Refresh every 5 seconds
const interval = setInterval(() => {
fetchOrderBook(symbol, 20);
}, 5000);
return () => clearInterval(interval);
}, [symbol, fetchOrderBook]);
// Calculate max quantity for bar width
const getMaxQuantity = useCallback((book: OrderBook | null): number => {
if (!book) return 1;
const allQuantities = [...book.bids.map((b) => b[1]), ...book.asks.map((a) => a[1])];
return Math.max(...allQuantities, 1);
}, []);
const maxQty = getMaxQuantity(orderBook);
// Format price
const formatPrice = (price: number): string => {
if (price >= 1000) return price.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 });
if (price >= 1) return price.toFixed(4);
return price.toFixed(8);
};
// Format quantity
const formatQty = (qty: number): string => {
if (qty >= 1000000) return `${(qty / 1000000).toFixed(2)}M`;
if (qty >= 1000) return `${(qty / 1000).toFixed(2)}K`;
return qty.toFixed(4);
};
// Calculate spread
const getSpread = (): { value: number; percentage: number } | null => {
if (!orderBook || orderBook.asks.length === 0 || orderBook.bids.length === 0) return null;
const bestAsk = orderBook.asks[0][0];
const bestBid = orderBook.bids[0][0];
const spread = bestAsk - bestBid;
const percentage = (spread / bestAsk) * 100;
return { value: spread, percentage };
};
const spread = getSpread();
return (
<div className="card p-4 space-y-3 h-full flex flex-col">
{/* Header */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Bars3BottomLeftIcon className="w-5 h-5 text-purple-400" />
<h3 className="text-sm font-semibold text-white">Order Book</h3>
</div>
<div className="flex items-center gap-2">
<select
value={displayRows}
onChange={(e) => setDisplayRows(Number(e.target.value))}
className="text-xs bg-gray-700 border border-gray-600 rounded px-1 py-0.5 text-white"
>
<option value={5}>5</option>
<option value={10}>10</option>
<option value={15}>15</option>
</select>
<button
onClick={() => fetchOrderBook(symbol, 20)}
disabled={loadingOrderBook}
className="p-1 text-gray-400 hover:text-white rounded"
>
<ArrowPathIcon className={`w-4 h-4 ${loadingOrderBook ? 'animate-spin' : ''}`} />
</button>
</div>
</div>
{/* Column Headers */}
<div className="grid grid-cols-3 text-xs text-gray-500 border-b border-gray-700 pb-1">
<span>Price</span>
<span className="text-right">Amount</span>
<span className="text-right">Total</span>
</div>
{/* Order Book Content */}
<div className="flex-1 overflow-hidden flex flex-col">
{/* Asks (Sell orders) - reversed to show lowest ask at bottom */}
<div className="flex-1 overflow-y-auto flex flex-col-reverse">
{orderBook?.asks.slice(0, displayRows).map(([price, qty], idx) => {
const total = orderBook.asks
.slice(0, idx + 1)
.reduce((sum, [, q]) => sum + q, 0);
const barWidth = (qty / maxQty) * 100;
return (
<div
key={`ask-${price}`}
className="relative grid grid-cols-3 text-xs py-0.5 hover:bg-gray-800 cursor-pointer group"
onClick={() => onPriceClick?.(price)}
>
{/* Background bar */}
<div
className="absolute right-0 top-0 bottom-0 bg-red-500/10 group-hover:bg-red-500/20 transition-colors"
style={{ width: `${barWidth}%` }}
/>
{/* Content */}
<span className="relative text-red-400 font-mono">{formatPrice(price)}</span>
<span className="relative text-right text-gray-300 font-mono">{formatQty(qty)}</span>
<span className="relative text-right text-gray-500 font-mono">{formatQty(total)}</span>
</div>
);
})}
</div>
{/* Spread */}
{spread && (
<div className="py-2 border-y border-gray-700 text-center">
<span className="text-xs text-gray-400">Spread: </span>
<span className="text-xs text-white font-mono">{formatPrice(spread.value)}</span>
<span className="text-xs text-gray-500 ml-1">({spread.percentage.toFixed(3)}%)</span>
</div>
)}
{/* Bids (Buy orders) */}
<div className="flex-1 overflow-y-auto">
{orderBook?.bids.slice(0, displayRows).map(([price, qty], idx) => {
const total = orderBook.bids
.slice(0, idx + 1)
.reduce((sum, [, q]) => sum + q, 0);
const barWidth = (qty / maxQty) * 100;
return (
<div
key={`bid-${price}`}
className="relative grid grid-cols-3 text-xs py-0.5 hover:bg-gray-800 cursor-pointer group"
onClick={() => onPriceClick?.(price)}
>
{/* Background bar */}
<div
className="absolute right-0 top-0 bottom-0 bg-green-500/10 group-hover:bg-green-500/20 transition-colors"
style={{ width: `${barWidth}%` }}
/>
{/* Content */}
<span className="relative text-green-400 font-mono">{formatPrice(price)}</span>
<span className="relative text-right text-gray-300 font-mono">{formatQty(qty)}</span>
<span className="relative text-right text-gray-500 font-mono">{formatQty(total)}</span>
</div>
);
})}
</div>
</div>
{/* Empty State */}
{!loadingOrderBook && (!orderBook || (orderBook.asks.length === 0 && orderBook.bids.length === 0)) && (
<div className="text-center py-4 text-gray-500 text-sm">
No order book data available
</div>
)}
</div>
);
};
export default OrderBookPanel;

View File

@ -0,0 +1,363 @@
/**
* TradingStatsPanel Component
* Displays trading performance statistics and metrics
*/
import React, { useEffect, useState, useCallback } from 'react';
import {
ChartBarIcon,
ArrowTrendingUpIcon,
ArrowTrendingDownIcon,
ArrowPathIcon,
CurrencyDollarIcon,
ScaleIcon,
FireIcon,
ShieldCheckIcon,
} from '@heroicons/react/24/solid';
import { tradingService } from '../../../services/trading.service';
import type { PaperTrade, AccountSummary } from '../../../types/trading.types';
interface TradingStatsPanelProps {
compact?: boolean;
}
interface CalculatedStats {
totalTrades: number;
winningTrades: number;
losingTrades: number;
winRate: number;
totalPnL: number;
avgWin: number;
avgLoss: number;
profitFactor: number;
largestWin: number;
largestLoss: number;
avgHoldTime: string;
currentStreak: number;
streakType: 'win' | 'loss' | 'none';
}
export const TradingStatsPanel: React.FC<TradingStatsPanelProps> = ({ compact = false }) => {
const [trades, setTrades] = useState<PaperTrade[]>([]);
const [portfolio, setPortfolio] = useState<AccountSummary | null>(null);
const [stats, setStats] = useState<CalculatedStats | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
// Calculate statistics from trades
const calculateStats = useCallback((tradeList: PaperTrade[]): CalculatedStats => {
if (tradeList.length === 0) {
return {
totalTrades: 0,
winningTrades: 0,
losingTrades: 0,
winRate: 0,
totalPnL: 0,
avgWin: 0,
avgLoss: 0,
profitFactor: 0,
largestWin: 0,
largestLoss: 0,
avgHoldTime: '0h',
currentStreak: 0,
streakType: 'none',
};
}
const closedTrades = tradeList.filter((t) => t.pnl !== undefined && t.pnl !== null);
const winningTrades = closedTrades.filter((t) => (t.pnl || 0) > 0);
const losingTrades = closedTrades.filter((t) => (t.pnl || 0) < 0);
const totalPnL = closedTrades.reduce((sum, t) => sum + (t.pnl || 0), 0);
const totalWins = winningTrades.reduce((sum, t) => sum + (t.pnl || 0), 0);
const totalLosses = Math.abs(losingTrades.reduce((sum, t) => sum + (t.pnl || 0), 0));
// Calculate average hold time
let avgHoldMs = 0;
const tradesWithTime = closedTrades.filter((t) => t.closedAt && t.openedAt);
if (tradesWithTime.length > 0) {
avgHoldMs =
tradesWithTime.reduce((sum, t) => {
const open = new Date(t.openedAt!).getTime();
const close = new Date(t.closedAt!).getTime();
return sum + (close - open);
}, 0) / tradesWithTime.length;
}
const avgHoldHours = Math.round(avgHoldMs / (1000 * 60 * 60));
const avgHoldTime = avgHoldHours < 24 ? `${avgHoldHours}h` : `${Math.round(avgHoldHours / 24)}d`;
// Calculate streak
let currentStreak = 0;
let streakType: 'win' | 'loss' | 'none' = 'none';
const sortedTrades = [...closedTrades].sort(
(a, b) => new Date(b.closedAt || 0).getTime() - new Date(a.closedAt || 0).getTime()
);
if (sortedTrades.length > 0) {
const firstPnl = sortedTrades[0].pnl || 0;
streakType = firstPnl > 0 ? 'win' : firstPnl < 0 ? 'loss' : 'none';
for (const trade of sortedTrades) {
const pnl = trade.pnl || 0;
if ((streakType === 'win' && pnl > 0) || (streakType === 'loss' && pnl < 0)) {
currentStreak++;
} else {
break;
}
}
}
return {
totalTrades: closedTrades.length,
winningTrades: winningTrades.length,
losingTrades: losingTrades.length,
winRate: closedTrades.length > 0 ? (winningTrades.length / closedTrades.length) * 100 : 0,
totalPnL,
avgWin: winningTrades.length > 0 ? totalWins / winningTrades.length : 0,
avgLoss: losingTrades.length > 0 ? totalLosses / losingTrades.length : 0,
profitFactor: totalLosses > 0 ? totalWins / totalLosses : totalWins > 0 ? Infinity : 0,
largestWin: winningTrades.length > 0 ? Math.max(...winningTrades.map((t) => t.pnl || 0)) : 0,
largestLoss: losingTrades.length > 0 ? Math.min(...losingTrades.map((t) => t.pnl || 0)) : 0,
avgHoldTime,
currentStreak,
streakType,
};
}, []);
// Fetch data
const fetchData = useCallback(async () => {
setLoading(true);
setError(null);
try {
const [tradesData, portfolioData] = await Promise.all([
tradingService.getPaperTrades(100),
tradingService.getPaperPortfolio(),
]);
setTrades(tradesData);
setPortfolio(portfolioData);
setStats(calculateStats(tradesData));
} catch (err) {
setError('Failed to fetch trading stats');
console.error('Stats fetch error:', err);
} finally {
setLoading(false);
}
}, [calculateStats]);
useEffect(() => {
fetchData();
}, [fetchData]);
// Format currency
const formatCurrency = (value: number) => {
const prefix = value >= 0 ? '+' : '';
return `${prefix}$${Math.abs(value).toLocaleString(undefined, {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
})}`;
};
if (compact) {
return (
<div className="card p-3 space-y-3">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<ChartBarIcon className="w-4 h-4 text-blue-400" />
<h4 className="text-sm font-medium text-white">Stats</h4>
</div>
<button
onClick={fetchData}
disabled={loading}
className="p-1 text-gray-400 hover:text-white rounded"
>
<ArrowPathIcon className={`w-3 h-3 ${loading ? 'animate-spin' : ''}`} />
</button>
</div>
{stats && (
<div className="grid grid-cols-2 gap-2 text-xs">
<div>
<span className="text-gray-400">Win Rate</span>
<p className={`font-bold ${stats.winRate >= 50 ? 'text-green-400' : 'text-red-400'}`}>
{stats.winRate.toFixed(1)}%
</p>
</div>
<div>
<span className="text-gray-400">Total P&L</span>
<p className={`font-bold ${stats.totalPnL >= 0 ? 'text-green-400' : 'text-red-400'}`}>
{formatCurrency(stats.totalPnL)}
</p>
</div>
<div>
<span className="text-gray-400">Trades</span>
<p className="font-bold text-white">{stats.totalTrades}</p>
</div>
<div>
<span className="text-gray-400">Profit Factor</span>
<p className={`font-bold ${stats.profitFactor >= 1 ? 'text-green-400' : 'text-red-400'}`}>
{stats.profitFactor === Infinity ? '∞' : stats.profitFactor.toFixed(2)}
</p>
</div>
</div>
)}
</div>
);
}
return (
<div className="card p-4 space-y-4 h-full overflow-y-auto">
{/* Header */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<ChartBarIcon className="w-5 h-5 text-blue-400" />
<h3 className="text-lg font-semibold text-white">Trading Statistics</h3>
</div>
<button
onClick={fetchData}
disabled={loading}
className="p-1.5 text-gray-400 hover:text-white hover:bg-gray-700 rounded transition-colors disabled:opacity-50"
>
<ArrowPathIcon className={`w-4 h-4 ${loading ? 'animate-spin' : ''}`} />
</button>
</div>
{error && (
<div className="p-3 bg-red-900/20 border border-red-800 rounded-lg text-sm text-red-400">
{error}
</div>
)}
{/* Portfolio Summary */}
{portfolio && (
<div className="grid grid-cols-2 gap-3">
<div className="p-3 bg-gray-800 rounded-lg">
<div className="flex items-center gap-2 mb-1">
<CurrencyDollarIcon className="w-4 h-4 text-green-400" />
<span className="text-xs text-gray-400">Total Equity</span>
</div>
<p className="text-lg font-bold text-white">
${portfolio.totalEquity?.toLocaleString(undefined, { minimumFractionDigits: 2 }) || '0.00'}
</p>
</div>
<div className="p-3 bg-gray-800 rounded-lg">
<div className="flex items-center gap-2 mb-1">
<ScaleIcon className="w-4 h-4 text-blue-400" />
<span className="text-xs text-gray-400">Available</span>
</div>
<p className="text-lg font-bold text-white">
${portfolio.availableBalance?.toLocaleString(undefined, { minimumFractionDigits: 2 }) || '0.00'}
</p>
</div>
</div>
)}
{/* Performance Metrics */}
{stats && (
<>
{/* Win/Loss Stats */}
<div className="space-y-2">
<h4 className="text-sm font-medium text-gray-400">Performance</h4>
<div className="grid grid-cols-3 gap-2">
<div className="text-center p-2 bg-gray-800 rounded">
<p className="text-xs text-gray-400">Trades</p>
<p className="text-lg font-bold text-white">{stats.totalTrades}</p>
</div>
<div className="text-center p-2 bg-green-900/30 rounded">
<p className="text-xs text-green-400">Wins</p>
<p className="text-lg font-bold text-green-400">{stats.winningTrades}</p>
</div>
<div className="text-center p-2 bg-red-900/30 rounded">
<p className="text-xs text-red-400">Losses</p>
<p className="text-lg font-bold text-red-400">{stats.losingTrades}</p>
</div>
</div>
</div>
{/* Win Rate Bar */}
<div className="space-y-2">
<div className="flex justify-between text-sm">
<span className="text-gray-400">Win Rate</span>
<span className={`font-bold ${stats.winRate >= 50 ? 'text-green-400' : 'text-red-400'}`}>
{stats.winRate.toFixed(1)}%
</span>
</div>
<div className="h-2 bg-gray-700 rounded-full overflow-hidden">
<div
className={`h-full transition-all ${stats.winRate >= 50 ? 'bg-green-500' : 'bg-red-500'}`}
style={{ width: `${Math.min(stats.winRate, 100)}%` }}
/>
</div>
</div>
{/* P&L Stats */}
<div className="grid grid-cols-2 gap-3">
<div className="p-3 bg-gray-800 rounded-lg">
<p className="text-xs text-gray-400 mb-1">Total P&L</p>
<p className={`text-lg font-bold ${stats.totalPnL >= 0 ? 'text-green-400' : 'text-red-400'}`}>
{formatCurrency(stats.totalPnL)}
</p>
</div>
<div className="p-3 bg-gray-800 rounded-lg">
<div className="flex items-center gap-1 mb-1">
<ShieldCheckIcon className="w-3 h-3 text-blue-400" />
<p className="text-xs text-gray-400">Profit Factor</p>
</div>
<p className={`text-lg font-bold ${stats.profitFactor >= 1 ? 'text-green-400' : 'text-red-400'}`}>
{stats.profitFactor === Infinity ? '∞' : stats.profitFactor.toFixed(2)}
</p>
</div>
</div>
{/* Average Win/Loss */}
<div className="grid grid-cols-2 gap-3">
<div className="p-3 bg-green-900/20 rounded-lg border border-green-900/50">
<div className="flex items-center gap-2 mb-1">
<ArrowTrendingUpIcon className="w-4 h-4 text-green-400" />
<p className="text-xs text-green-400">Avg Win</p>
</div>
<p className="text-sm font-bold text-green-400">{formatCurrency(stats.avgWin)}</p>
<p className="text-xs text-gray-500 mt-1">
Best: {formatCurrency(stats.largestWin)}
</p>
</div>
<div className="p-3 bg-red-900/20 rounded-lg border border-red-900/50">
<div className="flex items-center gap-2 mb-1">
<ArrowTrendingDownIcon className="w-4 h-4 text-red-400" />
<p className="text-xs text-red-400">Avg Loss</p>
</div>
<p className="text-sm font-bold text-red-400">-${stats.avgLoss.toFixed(2)}</p>
<p className="text-xs text-gray-500 mt-1">
Worst: {formatCurrency(stats.largestLoss)}
</p>
</div>
</div>
{/* Additional Stats */}
<div className="grid grid-cols-2 gap-3">
<div className="p-3 bg-gray-800 rounded-lg">
<p className="text-xs text-gray-400 mb-1">Avg Hold Time</p>
<p className="text-sm font-bold text-white">{stats.avgHoldTime}</p>
</div>
<div className="p-3 bg-gray-800 rounded-lg">
<div className="flex items-center gap-1 mb-1">
<FireIcon className={`w-3 h-3 ${stats.streakType === 'win' ? 'text-green-400' : stats.streakType === 'loss' ? 'text-red-400' : 'text-gray-400'}`} />
<p className="text-xs text-gray-400">Current Streak</p>
</div>
<p className={`text-sm font-bold ${stats.streakType === 'win' ? 'text-green-400' : stats.streakType === 'loss' ? 'text-red-400' : 'text-gray-400'}`}>
{stats.currentStreak} {stats.streakType !== 'none' ? (stats.streakType === 'win' ? 'Wins' : 'Losses') : '-'}
</p>
</div>
</div>
</>
)}
{/* Empty State */}
{!loading && stats?.totalTrades === 0 && (
<div className="text-center py-8 text-gray-500">
<ChartBarIcon className="w-10 h-10 mx-auto mb-3 opacity-50" />
<p className="text-sm">No trading history yet</p>
<p className="text-xs mt-1">Start paper trading to see your statistics</p>
</div>
)}
</div>
);
};
export default TradingStatsPanel;

View File

@ -6,6 +6,8 @@ import WatchlistSidebar from '../components/WatchlistSidebar';
import PaperTradingPanel from '../components/PaperTradingPanel';
import MLSignalsPanel from '../components/MLSignalsPanel';
import AlertsPanel from '../components/AlertsPanel';
import TradingStatsPanel from '../components/TradingStatsPanel';
import OrderBookPanel from '../components/OrderBookPanel';
import type { Interval, CrosshairData } from '../../../types/trading.types';
import type { MLSignal } from '../../../services/mlService';
@ -16,6 +18,11 @@ export default function Trading() {
const [isMLSignalsOpen, setIsMLSignalsOpen] = useState(true);
const [isPaperTradingOpen, setIsPaperTradingOpen] = useState(true);
const [isAlertsOpen, setIsAlertsOpen] = useState(false);
const [isStatsOpen, setIsStatsOpen] = useState(false);
const [isOrderBookOpen, setIsOrderBookOpen] = useState(false);
// Active indicators state
const [activeIndicators, setActiveIndicators] = useState<Set<string>>(new Set());
// ML Overlay states
const [enableMLOverlays, setEnableMLOverlays] = useState(true);
@ -68,8 +75,15 @@ export default function Trading() {
// Handle indicator toggle
const handleIndicatorToggle = (indicator: string) => {
console.log('Toggle indicator:', indicator);
// TODO: Implement indicator overlay logic
setActiveIndicators((prev) => {
const newSet = new Set(prev);
if (newSet.has(indicator)) {
newSet.delete(indicator);
} else {
newSet.add(indicator);
}
return newSet;
});
};
// Handle crosshair move
@ -260,6 +274,44 @@ export default function Trading() {
/>
</svg>
</button>
<button
onClick={() => setIsStatsOpen(!isStatsOpen)}
className={`hidden lg:flex items-center justify-center w-8 h-8 rounded transition-colors ${
isStatsOpen
? 'text-blue-400 bg-blue-900/30'
: 'text-gray-400 hover:text-white hover:bg-gray-800'
}`}
aria-label="Toggle stats"
title="Trading Statistics"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"
/>
</svg>
</button>
<button
onClick={() => setIsOrderBookOpen(!isOrderBookOpen)}
className={`hidden lg:flex items-center justify-center w-8 h-8 rounded transition-colors ${
isOrderBookOpen
? 'text-purple-400 bg-purple-900/30'
: 'text-gray-400 hover:text-white hover:bg-gray-800'
}`}
aria-label="Toggle order book"
title="Order Book"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M4 6h16M4 10h16M4 14h16M4 18h16"
/>
</svg>
</button>
<button
onClick={() => setIsPaperTradingOpen(!isPaperTradingOpen)}
className="hidden lg:flex items-center justify-center w-8 h-8 text-gray-400 hover:text-white hover:bg-gray-800 rounded transition-colors"
@ -401,6 +453,24 @@ export default function Trading() {
)}
</div>
{/* Trading Stats Panel - Desktop */}
<div className={`hidden lg:block transition-all duration-300 ${isStatsOpen ? 'w-80' : 'w-0 overflow-hidden'}`}>
{isStatsOpen && (
<div className="h-[600px]">
<TradingStatsPanel />
</div>
)}
</div>
{/* Order Book Panel - Desktop */}
<div className={`hidden lg:block transition-all duration-300 ${isOrderBookOpen ? 'w-64' : 'w-0 overflow-hidden'}`}>
{isOrderBookOpen && (
<div className="h-[600px]">
<OrderBookPanel symbol={selectedSymbol} />
</div>
)}
</div>
{/* Paper Trading Panel - Desktop */}
<div className={`hidden lg:block transition-all duration-300 ${isPaperTradingOpen ? 'w-80' : 'w-0 overflow-hidden'}`}>
{isPaperTradingOpen && (