From a3bd7af7b715013b5ccb821f3c573f2b80cd2fe9 Mon Sep 17 00:00:00 2001
From: Adrian Flores Cortes
Date: Tue, 3 Feb 2026 23:55:45 -0600
Subject: [PATCH] [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
---
src/modules/trading/components/OrderForm.tsx | 31 +++++++++
.../trading/components/TradesHistory.tsx | 44 +++++++++++-
.../trading/components/TradingStatsPanel.tsx | 68 +++++++++++++++++++
3 files changed, 142 insertions(+), 1 deletion(-)
diff --git a/src/modules/trading/components/OrderForm.tsx b/src/modules/trading/components/OrderForm.tsx
index fe5cb4d..5c0b82e 100644
--- a/src/modules/trading/components/OrderForm.tsx
+++ b/src/modules/trading/components/OrderForm.tsx
@@ -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
const orderData: CreateOrderInput = {
symbol,
diff --git a/src/modules/trading/components/TradesHistory.tsx b/src/modules/trading/components/TradesHistory.tsx
index a006db5..7b2014f 100644
--- a/src/modules/trading/components/TradesHistory.tsx
+++ b/src/modules/trading/components/TradesHistory.tsx
@@ -3,6 +3,8 @@
* Displays history of closed paper trading positions
*/
+import { ArrowDownTrayIcon } from '@heroicons/react/24/solid';
+
interface Trade {
id: string;
symbol: string;
@@ -21,6 +23,35 @@ interface TradesHistoryProps {
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({
trades,
isLoading,
@@ -43,7 +74,17 @@ export default function TradesHistory({
}
return (
-
+
+
+
+
+
{trades.map((trade) => {
const isProfitable = trade.realizedPnl >= 0;
const closedDate = new Date(trade.closedAt);
@@ -127,6 +168,7 @@ export default function TradesHistory({
);
})}
+
);
}
diff --git a/src/modules/trading/components/TradingStatsPanel.tsx b/src/modules/trading/components/TradingStatsPanel.tsx
index e554168..b4ca953 100644
--- a/src/modules/trading/components/TradingStatsPanel.tsx
+++ b/src/modules/trading/components/TradingStatsPanel.tsx
@@ -35,6 +35,9 @@ interface CalculatedStats {
avgHoldTime: string;
currentStreak: number;
streakType: 'win' | 'loss' | 'none';
+ sharpeRatio: number;
+ maxDrawdown: number;
+ maxDrawdownPercent: number;
}
export const TradingStatsPanel: React.FC = ({ compact = false }) => {
@@ -61,6 +64,9 @@ export const TradingStatsPanel: React.FC = ({ compact =
avgHoldTime: '0h',
currentStreak: 0,
streakType: 'none',
+ sharpeRatio: 0,
+ maxDrawdown: 0,
+ maxDrawdownPercent: 0,
};
}
@@ -105,6 +111,40 @@ export const TradingStatsPanel: React.FC = ({ 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 {
totalTrades: closedTrades.length,
winningTrades: winningTrades.length,
@@ -119,6 +159,9 @@ export const TradingStatsPanel: React.FC = ({ compact =
avgHoldTime,
currentStreak,
streakType,
+ sharpeRatio,
+ maxDrawdown,
+ maxDrawdownPercent,
};
}, []);
@@ -345,6 +388,31 @@ export const TradingStatsPanel: React.FC = ({ compact =
+
+ {/* Advanced Metrics */}
+
+
Advanced Metrics
+
+
+
Sharpe Ratio
+
= 1 ? 'text-green-400' : stats.sharpeRatio >= 0 ? 'text-yellow-400' : 'text-red-400'}`}>
+ {stats.sharpeRatio.toFixed(2)}
+
+
+ {stats.sharpeRatio >= 2 ? 'Excellent' : stats.sharpeRatio >= 1 ? 'Good' : stats.sharpeRatio >= 0 ? 'Fair' : 'Poor'}
+
+
+
+
Max Drawdown
+
+ -${stats.maxDrawdown.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
+
+
+ {stats.maxDrawdownPercent.toFixed(1)}% from peak
+
+
+
+
>
)}