[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:
parent
49d6492c91
commit
b798e2678c
183
src/modules/trading/components/OrderBookPanel.tsx
Normal file
183
src/modules/trading/components/OrderBookPanel.tsx
Normal 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;
|
||||
363
src/modules/trading/components/TradingStatsPanel.tsx
Normal file
363
src/modules/trading/components/TradingStatsPanel.tsx
Normal 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;
|
||||
@ -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 && (
|
||||
|
||||
Loading…
Reference in New Issue
Block a user