[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:
parent
ad7171da2c
commit
a3bd7af7b7
@ -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,
|
||||
|
||||
@ -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,6 +74,16 @@ export default function TradesHistory({
|
||||
}
|
||||
|
||||
return (
|
||||
<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) => {
|
||||
const isProfitable = trade.realizedPnl >= 0;
|
||||
@ -128,5 +169,6 @@ export default function TradesHistory({
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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<TradingStatsPanelProps> = ({ compact = false }) => {
|
||||
@ -61,6 +64,9 @@ export const TradingStatsPanel: React.FC<TradingStatsPanelProps> = ({ compact =
|
||||
avgHoldTime: '0h',
|
||||
currentStreak: 0,
|
||||
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 {
|
||||
totalTrades: closedTrades.length,
|
||||
winningTrades: winningTrades.length,
|
||||
@ -119,6 +159,9 @@ export const TradingStatsPanel: React.FC<TradingStatsPanelProps> = ({ compact =
|
||||
avgHoldTime,
|
||||
currentStreak,
|
||||
streakType,
|
||||
sharpeRatio,
|
||||
maxDrawdown,
|
||||
maxDrawdownPercent,
|
||||
};
|
||||
}, []);
|
||||
|
||||
@ -345,6 +388,31 @@ export const TradingStatsPanel: React.FC<TradingStatsPanelProps> = ({ compact =
|
||||
</p>
|
||||
</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>
|
||||
</>
|
||||
)}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user