[SPRINT-2] feat(trading): Add TP/SL validation, metrics and CSV export

SUBTASK-003: Trading Core Features
- OrderForm: Add TP/SL validation based on trade direction (LONG/SHORT)
- TradingStatsPanel: Add Sharpe Ratio and Max Drawdown metrics
- TradesHistory: Add Export CSV button with full trade details

Metrics already implemented: Win Rate, Profit Factor, Total PnL
Alerts system: Already fully implemented (create, list, delete, toggle)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Adrian Flores Cortes 2026-02-03 23:55:45 -06:00
parent ad7171da2c
commit a3bd7af7b7
3 changed files with 142 additions and 1 deletions

View File

@ -63,6 +63,37 @@ export default function OrderForm({
} }
} }
// Validate TP/SL based on direction
const entryPriceForValidation = orderType === 'market' ? currentPrice : parseFloat(limitPrice) || currentPrice;
if (stopLoss) {
const sl = parseFloat(stopLoss);
if (sl > 0) {
if (direction === 'long' && sl >= entryPriceForValidation) {
setError('Stop Loss must be below entry price for LONG positions');
return;
}
if (direction === 'short' && sl <= entryPriceForValidation) {
setError('Stop Loss must be above entry price for SHORT positions');
return;
}
}
}
if (takeProfit) {
const tp = parseFloat(takeProfit);
if (tp > 0) {
if (direction === 'long' && tp <= entryPriceForValidation) {
setError('Take Profit must be above entry price for LONG positions');
return;
}
if (direction === 'short' && tp >= entryPriceForValidation) {
setError('Take Profit must be below entry price for SHORT positions');
return;
}
}
}
// Build order data // Build order data
const orderData: CreateOrderInput = { const orderData: CreateOrderInput = {
symbol, symbol,

View File

@ -3,6 +3,8 @@
* Displays history of closed paper trading positions * Displays history of closed paper trading positions
*/ */
import { ArrowDownTrayIcon } from '@heroicons/react/24/solid';
interface Trade { interface Trade {
id: string; id: string;
symbol: string; symbol: string;
@ -21,6 +23,35 @@ interface TradesHistoryProps {
isLoading: boolean; isLoading: boolean;
} }
function exportToCSV(trades: Trade[]): void {
const headers = ['Date', 'Symbol', 'Direction', 'Entry Price', 'Exit Price', 'Quantity', 'P&L', 'P&L %', 'Reason'];
const rows = trades.map((trade) => {
const closedDate = new Date(trade.closedAt);
return [
closedDate.toISOString(),
trade.symbol,
trade.direction.toUpperCase(),
trade.entryPrice.toFixed(8),
trade.exitPrice.toFixed(8),
trade.lotSize.toString(),
trade.realizedPnl.toFixed(2),
trade.realizedPnlPercent.toFixed(2),
trade.closeReason || '',
];
});
const csvContent = [headers.join(','), ...rows.map((row) => row.join(','))].join('\n');
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.setAttribute('href', url);
link.setAttribute('download', `trades_history_${new Date().toISOString().split('T')[0]}.csv`);
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
}
export default function TradesHistory({ export default function TradesHistory({
trades, trades,
isLoading, isLoading,
@ -43,7 +74,17 @@ export default function TradesHistory({
} }
return ( return (
<div className="divide-y divide-gray-700 max-h-[400px] overflow-y-auto"> <div className="flex flex-col h-full">
<div className="flex justify-end p-2 border-b border-gray-700">
<button
onClick={() => exportToCSV(trades)}
className="flex items-center gap-1.5 px-3 py-1.5 text-sm bg-blue-600 hover:bg-blue-700 text-white rounded transition-colors"
>
<ArrowDownTrayIcon className="w-4 h-4" />
Export CSV
</button>
</div>
<div className="divide-y divide-gray-700 max-h-[400px] overflow-y-auto">
{trades.map((trade) => { {trades.map((trade) => {
const isProfitable = trade.realizedPnl >= 0; const isProfitable = trade.realizedPnl >= 0;
const closedDate = new Date(trade.closedAt); const closedDate = new Date(trade.closedAt);
@ -127,6 +168,7 @@ export default function TradesHistory({
</div> </div>
); );
})} })}
</div>
</div> </div>
); );
} }

View File

@ -35,6 +35,9 @@ interface CalculatedStats {
avgHoldTime: string; avgHoldTime: string;
currentStreak: number; currentStreak: number;
streakType: 'win' | 'loss' | 'none'; streakType: 'win' | 'loss' | 'none';
sharpeRatio: number;
maxDrawdown: number;
maxDrawdownPercent: number;
} }
export const TradingStatsPanel: React.FC<TradingStatsPanelProps> = ({ compact = false }) => { export const TradingStatsPanel: React.FC<TradingStatsPanelProps> = ({ compact = false }) => {
@ -61,6 +64,9 @@ export const TradingStatsPanel: React.FC<TradingStatsPanelProps> = ({ compact =
avgHoldTime: '0h', avgHoldTime: '0h',
currentStreak: 0, currentStreak: 0,
streakType: 'none', streakType: 'none',
sharpeRatio: 0,
maxDrawdown: 0,
maxDrawdownPercent: 0,
}; };
} }
@ -105,6 +111,40 @@ export const TradingStatsPanel: React.FC<TradingStatsPanelProps> = ({ compact =
} }
} }
// Calculate Sharpe Ratio (annualized, assuming daily returns)
let sharpeRatio = 0;
if (closedTrades.length >= 2) {
const returns = closedTrades.map((t) => t.realizedPnl || 0);
const avgReturn = returns.reduce((a, b) => a + b, 0) / returns.length;
const variance = returns.reduce((sum, r) => sum + Math.pow(r - avgReturn, 2), 0) / returns.length;
const stdDev = Math.sqrt(variance);
if (stdDev > 0) {
sharpeRatio = (avgReturn / stdDev) * Math.sqrt(252);
}
}
// Calculate Max Drawdown
let maxDrawdown = 0;
let maxDrawdownPercent = 0;
if (closedTrades.length > 0) {
const chronologicalTrades = [...closedTrades].sort(
(a, b) => new Date(a.closedAt || 0).getTime() - new Date(b.closedAt || 0).getTime()
);
let cumulativePnl = 0;
let peak = 0;
for (const trade of chronologicalTrades) {
cumulativePnl += trade.realizedPnl || 0;
if (cumulativePnl > peak) {
peak = cumulativePnl;
}
const drawdown = peak - cumulativePnl;
if (drawdown > maxDrawdown) {
maxDrawdown = drawdown;
maxDrawdownPercent = peak > 0 ? (drawdown / peak) * 100 : 0;
}
}
}
return { return {
totalTrades: closedTrades.length, totalTrades: closedTrades.length,
winningTrades: winningTrades.length, winningTrades: winningTrades.length,
@ -119,6 +159,9 @@ export const TradingStatsPanel: React.FC<TradingStatsPanelProps> = ({ compact =
avgHoldTime, avgHoldTime,
currentStreak, currentStreak,
streakType, streakType,
sharpeRatio,
maxDrawdown,
maxDrawdownPercent,
}; };
}, []); }, []);
@ -345,6 +388,31 @@ export const TradingStatsPanel: React.FC<TradingStatsPanelProps> = ({ compact =
</p> </p>
</div> </div>
</div> </div>
{/* Advanced Metrics */}
<div className="space-y-2">
<h4 className="text-sm font-medium text-gray-400">Advanced Metrics</h4>
<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">Sharpe Ratio</p>
<p className={`text-sm font-bold ${stats.sharpeRatio >= 1 ? 'text-green-400' : stats.sharpeRatio >= 0 ? 'text-yellow-400' : 'text-red-400'}`}>
{stats.sharpeRatio.toFixed(2)}
</p>
<p className="text-xs text-gray-500 mt-0.5">
{stats.sharpeRatio >= 2 ? 'Excellent' : stats.sharpeRatio >= 1 ? 'Good' : stats.sharpeRatio >= 0 ? 'Fair' : 'Poor'}
</p>
</div>
<div className="p-3 bg-gray-800 rounded-lg">
<p className="text-xs text-gray-400 mb-1">Max Drawdown</p>
<p className="text-sm font-bold text-red-400">
-${stats.maxDrawdown.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
</p>
<p className="text-xs text-gray-500 mt-0.5">
{stats.maxDrawdownPercent.toFixed(1)}% from peak
</p>
</div>
</div>
</div>
</> </>
)} )}