diff --git a/src/modules/trading/components/OrderBookPanel.tsx b/src/modules/trading/components/OrderBookPanel.tsx new file mode 100644 index 0000000..71b9070 --- /dev/null +++ b/src/modules/trading/components/OrderBookPanel.tsx @@ -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 = ({ 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 ( +
+ {/* Header */} +
+
+ +

Order Book

+
+
+ + +
+
+ + {/* Column Headers */} +
+ Price + Amount + Total +
+ + {/* Order Book Content */} +
+ {/* Asks (Sell orders) - reversed to show lowest ask at bottom */} +
+ {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 ( +
onPriceClick?.(price)} + > + {/* Background bar */} +
+ {/* Content */} + {formatPrice(price)} + {formatQty(qty)} + {formatQty(total)} +
+ ); + })} +
+ + {/* Spread */} + {spread && ( +
+ Spread: + {formatPrice(spread.value)} + ({spread.percentage.toFixed(3)}%) +
+ )} + + {/* Bids (Buy orders) */} +
+ {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 ( +
onPriceClick?.(price)} + > + {/* Background bar */} +
+ {/* Content */} + {formatPrice(price)} + {formatQty(qty)} + {formatQty(total)} +
+ ); + })} +
+
+ + {/* Empty State */} + {!loadingOrderBook && (!orderBook || (orderBook.asks.length === 0 && orderBook.bids.length === 0)) && ( +
+ No order book data available +
+ )} +
+ ); +}; + +export default OrderBookPanel; diff --git a/src/modules/trading/components/TradingStatsPanel.tsx b/src/modules/trading/components/TradingStatsPanel.tsx new file mode 100644 index 0000000..8cd69cd --- /dev/null +++ b/src/modules/trading/components/TradingStatsPanel.tsx @@ -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 = ({ compact = false }) => { + const [trades, setTrades] = useState([]); + const [portfolio, setPortfolio] = useState(null); + const [stats, setStats] = useState(null); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(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 ( +
+
+
+ +

Stats

+
+ +
+ {stats && ( +
+
+ Win Rate +

= 50 ? 'text-green-400' : 'text-red-400'}`}> + {stats.winRate.toFixed(1)}% +

+
+
+ Total P&L +

= 0 ? 'text-green-400' : 'text-red-400'}`}> + {formatCurrency(stats.totalPnL)} +

+
+
+ Trades +

{stats.totalTrades}

+
+
+ Profit Factor +

= 1 ? 'text-green-400' : 'text-red-400'}`}> + {stats.profitFactor === Infinity ? '∞' : stats.profitFactor.toFixed(2)} +

+
+
+ )} +
+ ); + } + + return ( +
+ {/* Header */} +
+
+ +

Trading Statistics

+
+ +
+ + {error && ( +
+ {error} +
+ )} + + {/* Portfolio Summary */} + {portfolio && ( +
+
+
+ + Total Equity +
+

+ ${portfolio.totalEquity?.toLocaleString(undefined, { minimumFractionDigits: 2 }) || '0.00'} +

+
+
+
+ + Available +
+

+ ${portfolio.availableBalance?.toLocaleString(undefined, { minimumFractionDigits: 2 }) || '0.00'} +

+
+
+ )} + + {/* Performance Metrics */} + {stats && ( + <> + {/* Win/Loss Stats */} +
+

Performance

+
+
+

Trades

+

{stats.totalTrades}

+
+
+

Wins

+

{stats.winningTrades}

+
+
+

Losses

+

{stats.losingTrades}

+
+
+
+ + {/* Win Rate Bar */} +
+
+ Win Rate + = 50 ? 'text-green-400' : 'text-red-400'}`}> + {stats.winRate.toFixed(1)}% + +
+
+
= 50 ? 'bg-green-500' : 'bg-red-500'}`} + style={{ width: `${Math.min(stats.winRate, 100)}%` }} + /> +
+
+ + {/* P&L Stats */} +
+
+

Total P&L

+

= 0 ? 'text-green-400' : 'text-red-400'}`}> + {formatCurrency(stats.totalPnL)} +

+
+
+
+ +

Profit Factor

+
+

= 1 ? 'text-green-400' : 'text-red-400'}`}> + {stats.profitFactor === Infinity ? '∞' : stats.profitFactor.toFixed(2)} +

+
+
+ + {/* Average Win/Loss */} +
+
+
+ +

Avg Win

+
+

{formatCurrency(stats.avgWin)}

+

+ Best: {formatCurrency(stats.largestWin)} +

+
+
+
+ +

Avg Loss

+
+

-${stats.avgLoss.toFixed(2)}

+

+ Worst: {formatCurrency(stats.largestLoss)} +

+
+
+ + {/* Additional Stats */} +
+
+

Avg Hold Time

+

{stats.avgHoldTime}

+
+
+
+ +

Current Streak

+
+

+ {stats.currentStreak} {stats.streakType !== 'none' ? (stats.streakType === 'win' ? 'Wins' : 'Losses') : '-'} +

+
+
+ + )} + + {/* Empty State */} + {!loading && stats?.totalTrades === 0 && ( +
+ +

No trading history yet

+

Start paper trading to see your statistics

+
+ )} +
+ ); +}; + +export default TradingStatsPanel; diff --git a/src/modules/trading/pages/Trading.tsx b/src/modules/trading/pages/Trading.tsx index d0d1ab4..cac74d4 100644 --- a/src/modules/trading/pages/Trading.tsx +++ b/src/modules/trading/pages/Trading.tsx @@ -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>(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() { /> + +
+ {/* Trading Stats Panel - Desktop */} +
+ {isStatsOpen && ( +
+ +
+ )} +
+ + {/* Order Book Panel - Desktop */} +
+ {isOrderBookOpen && ( +
+ +
+ )} +
+ {/* Paper Trading Panel - Desktop */}
{isPaperTradingOpen && (