[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 PaperTradingPanel from '../components/PaperTradingPanel';
|
||||||
import MLSignalsPanel from '../components/MLSignalsPanel';
|
import MLSignalsPanel from '../components/MLSignalsPanel';
|
||||||
import AlertsPanel from '../components/AlertsPanel';
|
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 { Interval, CrosshairData } from '../../../types/trading.types';
|
||||||
import type { MLSignal } from '../../../services/mlService';
|
import type { MLSignal } from '../../../services/mlService';
|
||||||
|
|
||||||
@ -16,6 +18,11 @@ export default function Trading() {
|
|||||||
const [isMLSignalsOpen, setIsMLSignalsOpen] = useState(true);
|
const [isMLSignalsOpen, setIsMLSignalsOpen] = useState(true);
|
||||||
const [isPaperTradingOpen, setIsPaperTradingOpen] = useState(true);
|
const [isPaperTradingOpen, setIsPaperTradingOpen] = useState(true);
|
||||||
const [isAlertsOpen, setIsAlertsOpen] = useState(false);
|
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
|
// ML Overlay states
|
||||||
const [enableMLOverlays, setEnableMLOverlays] = useState(true);
|
const [enableMLOverlays, setEnableMLOverlays] = useState(true);
|
||||||
@ -68,8 +75,15 @@ export default function Trading() {
|
|||||||
|
|
||||||
// Handle indicator toggle
|
// Handle indicator toggle
|
||||||
const handleIndicatorToggle = (indicator: string) => {
|
const handleIndicatorToggle = (indicator: string) => {
|
||||||
console.log('Toggle indicator:', indicator);
|
setActiveIndicators((prev) => {
|
||||||
// TODO: Implement indicator overlay logic
|
const newSet = new Set(prev);
|
||||||
|
if (newSet.has(indicator)) {
|
||||||
|
newSet.delete(indicator);
|
||||||
|
} else {
|
||||||
|
newSet.add(indicator);
|
||||||
|
}
|
||||||
|
return newSet;
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle crosshair move
|
// Handle crosshair move
|
||||||
@ -260,6 +274,44 @@ export default function Trading() {
|
|||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</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
|
<button
|
||||||
onClick={() => setIsPaperTradingOpen(!isPaperTradingOpen)}
|
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"
|
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>
|
</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 */}
|
{/* Paper Trading Panel - Desktop */}
|
||||||
<div className={`hidden lg:block transition-all duration-300 ${isPaperTradingOpen ? 'w-80' : 'w-0 overflow-hidden'}`}>
|
<div className={`hidden lg:block transition-all duration-300 ${isPaperTradingOpen ? 'w-80' : 'w-0 overflow-hidden'}`}>
|
||||||
{isPaperTradingOpen && (
|
{isPaperTradingOpen && (
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user