From bf726a595c98e2d759bd625a5a4c527df29ace84 Mon Sep 17 00:00:00 2001 From: Adrian Flores Cortes Date: Sun, 25 Jan 2026 10:56:41 -0600 Subject: [PATCH] [OQI-009] feat: Add advanced MT4 trading components - MT4PositionsManager: Container for all live positions with filtering/sorting - AdvancedOrderEntry: Professional order form with risk calculator, SL/TP modes - AccountHealthDashboard: Unified account metrics with health status Co-Authored-By: Claude Opus 4.5 --- .../components/AccountHealthDashboard.tsx | 391 +++++++++++++++ .../trading/components/AdvancedOrderEntry.tsx | 472 ++++++++++++++++++ .../components/MT4PositionsManager.tsx | 379 ++++++++++++++ src/modules/trading/components/index.ts | 3 + 4 files changed, 1245 insertions(+) create mode 100644 src/modules/trading/components/AccountHealthDashboard.tsx create mode 100644 src/modules/trading/components/AdvancedOrderEntry.tsx create mode 100644 src/modules/trading/components/MT4PositionsManager.tsx diff --git a/src/modules/trading/components/AccountHealthDashboard.tsx b/src/modules/trading/components/AccountHealthDashboard.tsx new file mode 100644 index 0000000..08233a7 --- /dev/null +++ b/src/modules/trading/components/AccountHealthDashboard.tsx @@ -0,0 +1,391 @@ +/** + * AccountHealthDashboard Component + * Unified view of account status and trading metrics + */ + +import React, { useMemo } from 'react'; +import { + TrendingUp, + TrendingDown, + DollarSign, + Percent, + Activity, + Award, + AlertTriangle, + Target, + BarChart3, + Clock, + Zap, +} from 'lucide-react'; +import type { MT4Account, MT4Position } from '../../../services/trading.service'; + +interface TradeStats { + totalTrades: number; + winningTrades: number; + losingTrades: number; + largestWin: number; + largestLoss: number; + consecutiveWins: number; + consecutiveLosses: number; +} + +interface AccountHealthDashboardProps { + account: MT4Account | null; + positions: MT4Position[]; + tradeStats?: TradeStats; + dailyStartBalance?: number; + compact?: boolean; +} + +const AccountHealthDashboard: React.FC = ({ + account, + positions, + tradeStats, + dailyStartBalance, + compact = false, +}) => { + // Calculate metrics + const metrics = useMemo(() => { + if (!account) return null; + + const unrealizedPnL = positions.reduce((sum, p) => sum + p.profit, 0); + const totalLots = positions.reduce((sum, p) => sum + p.volume, 0); + + const drawdown = dailyStartBalance + ? ((dailyStartBalance - account.equity) / dailyStartBalance) * 100 + : ((account.balance - account.equity) / account.balance) * 100; + + const marginLevel = account.margin > 0 + ? (account.equity / account.margin) * 100 + : 9999; + + const marginUsagePercent = account.balance > 0 + ? (account.margin / account.balance) * 100 + : 0; + + const winRate = tradeStats && tradeStats.totalTrades > 0 + ? (tradeStats.winningTrades / tradeStats.totalTrades) * 100 + : null; + + const profitFactor = tradeStats && tradeStats.largestLoss !== 0 + ? Math.abs(tradeStats.largestWin / tradeStats.largestLoss) + : null; + + return { + equity: account.equity, + balance: account.balance, + freeMargin: account.free_margin, + marginUsed: account.margin, + marginLevel, + marginUsagePercent, + unrealizedPnL, + drawdown: Math.max(0, drawdown), + totalLots, + positionCount: positions.length, + winRate, + profitFactor, + }; + }, [account, positions, dailyStartBalance, tradeStats]); + + const formatCurrency = (value: number) => { + return new Intl.NumberFormat('en-US', { + style: 'currency', + currency: account?.currency || 'USD', + minimumFractionDigits: 2, + }).format(value); + }; + + // Get health status + const getHealthStatus = (): 'excellent' | 'good' | 'warning' | 'critical' => { + if (!metrics) return 'warning'; + + if ( + metrics.marginLevel < 150 || + metrics.drawdown > 10 || + metrics.marginUsagePercent > 50 + ) { + return 'critical'; + } + + if ( + metrics.marginLevel < 300 || + metrics.drawdown > 5 || + metrics.marginUsagePercent > 30 + ) { + return 'warning'; + } + + if ( + metrics.marginLevel > 500 && + metrics.drawdown < 3 && + metrics.marginUsagePercent < 20 + ) { + return 'excellent'; + } + + return 'good'; + }; + + const healthStatus = getHealthStatus(); + + const healthColors = { + excellent: 'text-green-400 bg-green-500/20 border-green-500/30', + good: 'text-blue-400 bg-blue-500/20 border-blue-500/30', + warning: 'text-yellow-400 bg-yellow-500/20 border-yellow-500/30', + critical: 'text-red-400 bg-red-500/20 border-red-500/30', + }; + + const healthLabels = { + excellent: 'Excellent', + good: 'Good', + warning: 'Caution', + critical: 'At Risk', + }; + + if (!account || !metrics) { + return ( +
+ +

Account Health

+

Connect MT4 to view metrics

+
+ ); + } + + if (compact) { + return ( +
+
+ + + {healthLabels[healthStatus]} + +
+
+ Equity: {formatCurrency(metrics.equity)} + DD: {metrics.drawdown.toFixed(1)}% + Margin: {metrics.marginLevel > 9000 ? '∞' : `${metrics.marginLevel.toFixed(0)}%`} +
+
+ ); + } + + return ( +
+ {/* Header */} +
+
+
+ +
+
+

Account Health

+

+ {healthLabels[healthStatus]} +

+
+
+ {healthStatus === 'critical' && ( +
+ + High Risk +
+ )} +
+ +
+ {/* Main Metrics */} +
+ {/* Equity vs Balance */} +
+
+ + Equity / Balance +
+

= metrics.balance ? 'text-green-400' : 'text-red-400' + }`}> + {formatCurrency(metrics.equity)} +

+

+ of {formatCurrency(metrics.balance)} +

+ {metrics.unrealizedPnL !== 0 && ( +

= 0 ? 'text-green-400' : 'text-red-400' + }`}> + Floating: {metrics.unrealizedPnL >= 0 ? '+' : ''}{formatCurrency(metrics.unrealizedPnL)} +

+ )} +
+ + {/* Drawdown */} +
+
+ + Drawdown +
+

+ {metrics.drawdown.toFixed(2)}% +

+
+
+
+
+
+ + {/* Secondary Metrics */} +
+ {/* Margin Level */} +
+ +

300 ? 'text-green-400' : + metrics.marginLevel > 150 ? 'text-yellow-400' : 'text-red-400' + }`}> + {metrics.marginLevel > 9000 ? '∞' : `${metrics.marginLevel.toFixed(0)}%`} +

+

Margin Level

+
+ + {/* Free Margin */} +
+ +

+ {formatCurrency(metrics.freeMargin)} +

+

Free Margin

+
+ + {/* Positions */} +
+ +

{metrics.positionCount}

+

Positions

+
+ + {/* Total Lots */} +
+ +

{metrics.totalLots.toFixed(2)}

+

Total Lots

+
+
+ + {/* Trade Stats */} + {tradeStats && tradeStats.totalTrades > 0 && ( +
+

+ + Trading Performance +

+
+ {/* Win Rate */} +
+

Win Rate

+

= 50 ? 'text-green-400' : 'text-red-400' + }`}> + {metrics.winRate?.toFixed(1)}% +

+

+ {tradeStats.winningTrades}W / {tradeStats.losingTrades}L +

+
+ + {/* Profit Factor */} +
+

Profit Factor

+

= 1.5 ? 'text-green-400' : + metrics.profitFactor && metrics.profitFactor >= 1 ? 'text-yellow-400' : 'text-red-400' + }`}> + {metrics.profitFactor?.toFixed(2) || '-'} +

+
+ + {/* Streak */} +
+

Current Streak

+ {tradeStats.consecutiveWins > 0 ? ( +
+ + + {tradeStats.consecutiveWins}W + +
+ ) : tradeStats.consecutiveLosses > 0 ? ( +
+ + + {tradeStats.consecutiveLosses}L + +
+ ) : ( + - + )} +
+
+ + {/* Best/Worst Trade */} +
+
+ + Best: + + +{formatCurrency(tradeStats.largestWin)} + +
+
+ + Worst: + + {formatCurrency(tradeStats.largestLoss)} + +
+
+
+ )} + + {/* Warnings */} + {healthStatus !== 'excellent' && healthStatus !== 'good' && ( +
+
+ +
+

+ Health Warnings +

+
    + {metrics.marginLevel < 300 && ( +
  • • Margin level below 300%
  • + )} + {metrics.drawdown > 5 && ( +
  • • Drawdown exceeds 5%
  • + )} + {metrics.marginUsagePercent > 30 && ( +
  • • High margin usage ({metrics.marginUsagePercent.toFixed(0)}%)
  • + )} + {metrics.positionCount > 5 && ( +
  • • Multiple open positions
  • + )} +
+
+
+
+ )} +
+
+ ); +}; + +export default AccountHealthDashboard; diff --git a/src/modules/trading/components/AdvancedOrderEntry.tsx b/src/modules/trading/components/AdvancedOrderEntry.tsx new file mode 100644 index 0000000..425799c --- /dev/null +++ b/src/modules/trading/components/AdvancedOrderEntry.tsx @@ -0,0 +1,472 @@ +/** + * AdvancedOrderEntry Component + * Professional order placement form for MT4 live trading + */ + +import React, { useState, useMemo } from 'react'; +import { + TrendingUp, + TrendingDown, + Calculator, + Shield, + Target, + AlertTriangle, + Loader2, + Info, + Zap, +} from 'lucide-react'; +import { + executeMLTrade, + calculatePositionSize, + type MT4Account, +} from '../../../services/trading.service'; + +type OrderType = 'market' | 'limit' | 'stop'; +type OrderSide = 'buy' | 'sell'; + +interface AdvancedOrderEntryProps { + symbol: string; + currentPrice: number; + spread?: number; + account: MT4Account | null; + onOrderExecuted?: (ticket: number) => void; + onError?: (error: string) => void; +} + +const AdvancedOrderEntry: React.FC = ({ + symbol, + currentPrice, + spread = 0, + account, + onOrderExecuted, + onError, +}) => { + const [orderType, setOrderType] = useState('market'); + const [side, setSide] = useState('buy'); + const [volume, setVolume] = useState('0.01'); + const [limitPrice, setLimitPrice] = useState(''); + const [stopLossMode, setStopLossMode] = useState<'price' | 'pips' | 'percent'>('pips'); + const [stopLoss, setStopLoss] = useState(''); + const [takeProfitMode, setTakeProfitMode] = useState<'price' | 'pips' | 'rr'>('pips'); + const [takeProfit, setTakeProfit] = useState(''); + const [riskPercent, setRiskPercent] = useState('1'); + const [useRiskCalculator, setUseRiskCalculator] = useState(false); + const [isSubmitting, setIsSubmitting] = useState(false); + const [showAdvanced, setShowAdvanced] = useState(false); + + // Calculate pip value based on symbol + const pipValue = useMemo(() => { + return symbol.includes('JPY') ? 0.01 : 0.0001; + }, [symbol]); + + // Calculate SL price from different modes + const calculateSLPrice = (): number | undefined => { + if (!stopLoss) return undefined; + const slValue = parseFloat(stopLoss); + if (isNaN(slValue)) return undefined; + + switch (stopLossMode) { + case 'price': + return slValue; + case 'pips': + return side === 'buy' + ? currentPrice - slValue * pipValue + : currentPrice + slValue * pipValue; + case 'percent': + const percentLoss = (slValue / 100) * currentPrice; + return side === 'buy' + ? currentPrice - percentLoss + : currentPrice + percentLoss; + default: + return undefined; + } + }; + + // Calculate TP price from different modes + const calculateTPPrice = (): number | undefined => { + if (!takeProfit) return undefined; + const tpValue = parseFloat(takeProfit); + if (isNaN(tpValue)) return undefined; + + switch (takeProfitMode) { + case 'price': + return tpValue; + case 'pips': + return side === 'buy' + ? currentPrice + tpValue * pipValue + : currentPrice - tpValue * pipValue; + case 'rr': + const slPrice = calculateSLPrice(); + if (!slPrice) return undefined; + const riskDistance = Math.abs(currentPrice - slPrice); + return side === 'buy' + ? currentPrice + riskDistance * tpValue + : currentPrice - riskDistance * tpValue; + default: + return undefined; + } + }; + + // Calculate position size based on risk + const calculatedVolume = useMemo(() => { + if (!useRiskCalculator || !account || !stopLoss) return null; + const slPips = stopLossMode === 'pips' ? parseFloat(stopLoss) : null; + if (!slPips) return null; + + const risk = parseFloat(riskPercent); + if (isNaN(risk)) return null; + + // Simple calculation: risk amount / (sl_pips * pip_value_per_lot) + const riskAmount = (account.balance * risk) / 100; + const pipValuePerLot = symbol.includes('JPY') ? 1000 : 10; // Approximate + const lots = riskAmount / (slPips * pipValuePerLot); + return Math.max(0.01, Math.min(lots, 10)).toFixed(2); + }, [useRiskCalculator, account, stopLoss, stopLossMode, riskPercent, symbol]); + + // Calculate estimated margin + const estimatedMargin = useMemo(() => { + if (!account) return null; + const lots = parseFloat(useRiskCalculator && calculatedVolume ? calculatedVolume : volume); + if (isNaN(lots)) return null; + // Approximate margin calculation (100:1 leverage) + const leverage = account.leverage || 100; + return (lots * 100000 * currentPrice) / leverage; + }, [account, volume, calculatedVolume, useRiskCalculator, currentPrice]); + + // Risk/Reward ratio + const riskRewardRatio = useMemo(() => { + const slPrice = calculateSLPrice(); + const tpPrice = calculateTPPrice(); + if (!slPrice || !tpPrice) return null; + const risk = Math.abs(currentPrice - slPrice); + const reward = Math.abs(tpPrice - currentPrice); + if (risk === 0) return null; + return (reward / risk).toFixed(2); + }, [currentPrice, stopLoss, takeProfit, stopLossMode, takeProfitMode, side]); + + const handleSubmit = async () => { + if (!account) { + onError?.('No account connected'); + return; + } + + setIsSubmitting(true); + try { + const finalVolume = useRiskCalculator && calculatedVolume ? calculatedVolume : volume; + const slPrice = calculateSLPrice(); + const tpPrice = calculateTPPrice(); + + const result = await executeMLTrade({ + symbol, + action: side.toUpperCase() as 'BUY' | 'SELL', + volume: parseFloat(finalVolume), + entry_price: orderType === 'market' ? currentPrice : parseFloat(limitPrice), + stop_loss: slPrice, + take_profit: tpPrice, + order_type: orderType, + }); + + if (result.ticket) { + onOrderExecuted?.(result.ticket); + // Reset form + setVolume('0.01'); + setStopLoss(''); + setTakeProfit(''); + setLimitPrice(''); + } + } catch (err) { + onError?.(err instanceof Error ? err.message : 'Order execution failed'); + } finally { + setIsSubmitting(false); + } + }; + + const slPrice = calculateSLPrice(); + const tpPrice = calculateTPPrice(); + + return ( +
+ {/* Header */} +
+
+

Order Entry

+

{symbol}

+
+
+

{currentPrice.toFixed(5)}

+ {spread > 0 && ( +

Spread: {spread.toFixed(1)} pips

+ )} +
+
+ +
+ {/* Buy/Sell Buttons */} +
+ + +
+ + {/* Order Type */} +
+ +
+ {(['market', 'limit', 'stop'] as OrderType[]).map((type) => ( + + ))} +
+
+ + {/* Limit/Stop Price */} + {orderType !== 'market' && ( +
+ + setLimitPrice(e.target.value)} + placeholder={currentPrice.toFixed(5)} + className="w-full px-4 py-2 bg-gray-900 border border-gray-700 rounded-lg text-white font-mono focus:outline-none focus:border-blue-500" + /> +
+ )} + + {/* Volume / Risk Calculator Toggle */} +
+
+ + +
+ + {useRiskCalculator ? ( +
+
+ setRiskPercent(e.target.value)} + className="flex-1 px-4 py-2 bg-gray-900 border border-gray-700 rounded-lg text-white focus:outline-none focus:border-blue-500" + /> + % Risk +
+ {calculatedVolume && ( +

+ Calculated: {calculatedVolume} lots +

+ )} +
+ ) : ( + setVolume(e.target.value)} + className="w-full px-4 py-2 bg-gray-900 border border-gray-700 rounded-lg text-white font-mono focus:outline-none focus:border-blue-500" + /> + )} +
+ + {/* Stop Loss */} +
+
+ +
+ {(['pips', 'price', 'percent'] as const).map((mode) => ( + + ))} +
+
+
+ setStopLoss(e.target.value)} + placeholder={stopLossMode === 'price' ? currentPrice.toFixed(5) : '20'} + className="flex-1 px-4 py-2 bg-gray-900 border border-gray-700 rounded-lg text-white font-mono focus:outline-none focus:border-red-500" + /> + {slPrice && ( + {slPrice.toFixed(5)} + )} +
+
+ + {/* Take Profit */} +
+
+ +
+ {(['pips', 'price', 'rr'] as const).map((mode) => ( + + ))} +
+
+
+ setTakeProfit(e.target.value)} + placeholder={takeProfitMode === 'price' ? currentPrice.toFixed(5) : takeProfitMode === 'rr' ? '2' : '40'} + className="flex-1 px-4 py-2 bg-gray-900 border border-gray-700 rounded-lg text-white font-mono focus:outline-none focus:border-green-500" + /> + {tpPrice && ( + {tpPrice.toFixed(5)} + )} +
+
+ + {/* Order Summary */} +
+
+ Risk/Reward + = 2 + ? 'text-green-400' + : riskRewardRatio && parseFloat(riskRewardRatio) >= 1 + ? 'text-yellow-400' + : 'text-red-400' + }`}> + {riskRewardRatio ? `1:${riskRewardRatio}` : '-'} + +
+ {estimatedMargin && ( +
+ Est. Margin + ${estimatedMargin.toFixed(2)} +
+ )} + {account && estimatedMargin && ( +
+ Free Margin After + 0 ? 'text-green-400' : 'text-red-400' + }`}> + ${(account.free_margin - estimatedMargin).toFixed(2)} + +
+ )} +
+ + {/* Warning */} + {(!slPrice || !tpPrice) && ( +
+ +

+ {!slPrice && !tpPrice + ? 'No SL or TP set. Consider adding risk management.' + : !slPrice + ? 'No Stop Loss set. Position has unlimited risk.' + : 'No Take Profit set.'} +

+
+ )} + + {/* Submit Button */} + + + {!account && ( +

+ Connect MT4 account to place orders +

+ )} +
+
+ ); +}; + +export default AdvancedOrderEntry; diff --git a/src/modules/trading/components/MT4PositionsManager.tsx b/src/modules/trading/components/MT4PositionsManager.tsx new file mode 100644 index 0000000..aae191e --- /dev/null +++ b/src/modules/trading/components/MT4PositionsManager.tsx @@ -0,0 +1,379 @@ +/** + * MT4PositionsManager Component + * Container for managing all live MT4 positions with real-time updates + */ + +import React, { useState, useEffect, useCallback } from 'react'; +import { + RefreshCw, + X, + Filter, + SortAsc, + SortDesc, + TrendingUp, + TrendingDown, + AlertTriangle, + Loader2, + LayoutList, + LayoutGrid, +} from 'lucide-react'; +import LivePositionCard from './LivePositionCard'; +import { + getMT4Positions, + closeMT4Position, + modifyMT4Position, + type MT4Position, +} from '../../../services/trading.service'; + +type SortField = 'profit' | 'volume' | 'symbol' | 'openTime'; +type SortDirection = 'asc' | 'desc'; +type FilterType = 'all' | 'buy' | 'sell' | 'profit' | 'loss'; + +interface MT4PositionsManagerProps { + autoRefresh?: boolean; + refreshInterval?: number; + onPositionClose?: (ticket: number) => void; + onPositionModify?: (ticket: number) => void; + compact?: boolean; +} + +const MT4PositionsManager: React.FC = ({ + autoRefresh = true, + refreshInterval = 30000, + onPositionClose, + onPositionModify, + compact = false, +}) => { + const [positions, setPositions] = useState([]); + const [loading, setLoading] = useState(true); + const [refreshing, setRefreshing] = useState(false); + const [error, setError] = useState(null); + const [sortField, setSortField] = useState('openTime'); + const [sortDirection, setSortDirection] = useState('desc'); + const [filter, setFilter] = useState('all'); + const [showCloseAllConfirm, setShowCloseAllConfirm] = useState(false); + const [closingAll, setClosingAll] = useState(false); + const [viewMode, setViewMode] = useState<'list' | 'grid'>('list'); + + const fetchPositions = useCallback(async (isRefresh = false) => { + if (isRefresh) setRefreshing(true); + else setLoading(true); + + try { + const data = await getMT4Positions(); + setPositions(data || []); + setError(null); + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to load positions'); + } finally { + setLoading(false); + setRefreshing(false); + } + }, []); + + useEffect(() => { + fetchPositions(); + + if (autoRefresh) { + const interval = setInterval(() => fetchPositions(true), refreshInterval); + return () => clearInterval(interval); + } + }, [fetchPositions, autoRefresh, refreshInterval]); + + const handleClosePosition = async (ticket: number) => { + try { + await closeMT4Position(ticket); + setPositions((prev) => prev.filter((p) => p.ticket !== ticket)); + onPositionClose?.(ticket); + } catch (err) { + console.error('Failed to close position:', err); + } + }; + + const handleModifyPosition = async ( + ticket: number, + stopLoss?: number, + takeProfit?: number + ) => { + try { + await modifyMT4Position(ticket, stopLoss, takeProfit); + setPositions((prev) => + prev.map((p) => + p.ticket === ticket + ? { ...p, stop_loss: stopLoss || p.stop_loss, take_profit: takeProfit || p.take_profit } + : p + ) + ); + onPositionModify?.(ticket); + } catch (err) { + console.error('Failed to modify position:', err); + } + }; + + const handleCloseAll = async () => { + setClosingAll(true); + try { + await Promise.all(positions.map((p) => closeMT4Position(p.ticket))); + setPositions([]); + setShowCloseAllConfirm(false); + } catch (err) { + console.error('Failed to close all positions:', err); + } finally { + setClosingAll(false); + } + }; + + // Filter positions + const filteredPositions = positions.filter((p) => { + switch (filter) { + case 'buy': + return p.type === 'buy'; + case 'sell': + return p.type === 'sell'; + case 'profit': + return p.profit >= 0; + case 'loss': + return p.profit < 0; + default: + return true; + } + }); + + // Sort positions + const sortedPositions = [...filteredPositions].sort((a, b) => { + let comparison = 0; + switch (sortField) { + case 'profit': + comparison = a.profit - b.profit; + break; + case 'volume': + comparison = a.volume - b.volume; + break; + case 'symbol': + comparison = a.symbol.localeCompare(b.symbol); + break; + case 'openTime': + comparison = new Date(a.open_time).getTime() - new Date(b.open_time).getTime(); + break; + } + return sortDirection === 'asc' ? comparison : -comparison; + }); + + // Calculate totals + const totalProfit = positions.reduce((sum, p) => sum + p.profit, 0); + const totalLots = positions.reduce((sum, p) => sum + p.volume, 0); + const buyCount = positions.filter((p) => p.type === 'buy').length; + const sellCount = positions.filter((p) => p.type === 'sell').length; + + const toggleSort = (field: SortField) => { + if (sortField === field) { + setSortDirection((prev) => (prev === 'asc' ? 'desc' : 'asc')); + } else { + setSortField(field); + setSortDirection('desc'); + } + }; + + if (loading) { + return ( +
+ +
+ ); + } + + if (error) { + return ( +
+ +

Failed to Load Positions

+

{error}

+ +
+ ); + } + + return ( +
+ {/* Header */} +
+
+

Open Positions

+ + {positions.length} + +
+
+ {/* View Mode Toggle */} + {!compact && ( +
+ + +
+ )} + +
+
+ + {/* Summary Bar */} +
+
+
+ + Buy: + {buyCount} +
+
+ + Sell: + {sellCount} +
+
+ Lots: + {totalLots.toFixed(2)} +
+
+
= 0 ? 'text-green-400' : 'text-red-400'}`}> + {totalProfit >= 0 ? '+' : ''}{totalProfit.toFixed(2)} USD +
+
+ + {/* Filters & Sort */} +
+
+ + {(['all', 'buy', 'sell', 'profit', 'loss'] as FilterType[]).map((f) => ( + + ))} +
+
+ Sort: + {(['profit', 'volume', 'symbol'] as SortField[]).map((field) => ( + + ))} +
+
+ + {/* Positions List */} +
+ {sortedPositions.length === 0 ? ( +
+ +

No open positions

+ {filter !== 'all' && ( + + )} +
+ ) : ( + sortedPositions.map((position) => ( + + )) + )} +
+ + {/* Close All Footer */} + {positions.length > 0 && ( +
+ {showCloseAllConfirm ? ( +
+ + Close all {positions.length} positions? + +
+ + +
+
+ ) : ( + + )} +
+ )} +
+ ); +}; + +export default MT4PositionsManager; diff --git a/src/modules/trading/components/index.ts b/src/modules/trading/components/index.ts index c4dacdf..e215eea 100644 --- a/src/modules/trading/components/index.ts +++ b/src/modules/trading/components/index.ts @@ -37,3 +37,6 @@ export { default as ExportButton } from './ExportButton'; export { default as MT4ConnectionStatus } from './MT4ConnectionStatus'; export { default as LivePositionCard } from './LivePositionCard'; export { default as RiskMonitor } from './RiskMonitor'; +export { default as MT4PositionsManager } from './MT4PositionsManager'; +export { default as AdvancedOrderEntry } from './AdvancedOrderEntry'; +export { default as AccountHealthDashboard } from './AccountHealthDashboard';