From 423be4062cf50d906df664ff7addf6bc39c802b9 Mon Sep 17 00:00:00 2001 From: Adrian Flores Cortes Date: Sun, 25 Jan 2026 10:41:01 -0600 Subject: [PATCH] [OQI-009] feat: Add MT4 live trading components - MT4ConnectionStatus: Connection indicator with account info display - LivePositionCard: Real-time position card with P&L, modify/close actions - RiskMonitor: Risk management dashboard with metrics and warnings - index.ts: Centralized exports for all trading components Co-Authored-By: Claude Opus 4.5 --- .../trading/components/LivePositionCard.tsx | 307 ++++++++++++++ .../components/MT4ConnectionStatus.tsx | 310 ++++++++++++++ .../trading/components/RiskMonitor.tsx | 385 ++++++++++++++++++ src/modules/trading/components/index.ts | 39 ++ 4 files changed, 1041 insertions(+) create mode 100644 src/modules/trading/components/LivePositionCard.tsx create mode 100644 src/modules/trading/components/MT4ConnectionStatus.tsx create mode 100644 src/modules/trading/components/RiskMonitor.tsx create mode 100644 src/modules/trading/components/index.ts diff --git a/src/modules/trading/components/LivePositionCard.tsx b/src/modules/trading/components/LivePositionCard.tsx new file mode 100644 index 0000000..4453a10 --- /dev/null +++ b/src/modules/trading/components/LivePositionCard.tsx @@ -0,0 +1,307 @@ +/** + * LivePositionCard Component + * Displays a single live MT4 position with real-time P&L and actions + */ + +import React, { useState } from 'react'; +import { + TrendingUp, + TrendingDown, + X, + Edit3, + Target, + Shield, + Clock, + Loader2, + ChevronDown, + ChevronUp, +} from 'lucide-react'; +import type { MT4Position } from '../../../services/trading.service'; + +interface LivePositionCardProps { + position: MT4Position; + currentPrice?: number; + onClose?: (ticket: number) => Promise; + onModify?: (ticket: number, stopLoss?: number, takeProfit?: number) => Promise; + loading?: boolean; +} + +const LivePositionCard: React.FC = ({ + position, + currentPrice, + onClose, + onModify, + loading = false, +}) => { + const [isExpanded, setIsExpanded] = useState(false); + const [isClosing, setIsClosing] = useState(false); + const [isModifying, setIsModifying] = useState(false); + const [modifyMode, setModifyMode] = useState(false); + const [newStopLoss, setNewStopLoss] = useState(position.stop_loss?.toString() || ''); + const [newTakeProfit, setNewTakeProfit] = useState(position.take_profit?.toString() || ''); + + const isBuy = position.type === 'buy'; + const isProfit = position.profit >= 0; + const displayPrice = currentPrice || position.current_price; + + // Calculate pips + const pipMultiplier = position.symbol.includes('JPY') ? 100 : 10000; + const priceDiff = isBuy + ? (displayPrice - position.open_price) + : (position.open_price - displayPrice); + const pips = priceDiff * pipMultiplier; + + const formatTime = (timeString: string) => { + const date = new Date(timeString); + return date.toLocaleString('es-ES', { + month: 'short', + day: 'numeric', + hour: '2-digit', + minute: '2-digit', + }); + }; + + const handleClose = async () => { + if (!onClose) return; + setIsClosing(true); + try { + await onClose(position.ticket); + } finally { + setIsClosing(false); + } + }; + + const handleModify = async () => { + if (!onModify) return; + setIsModifying(true); + try { + const sl = newStopLoss ? parseFloat(newStopLoss) : undefined; + const tp = newTakeProfit ? parseFloat(newTakeProfit) : undefined; + await onModify(position.ticket, sl, tp); + setModifyMode(false); + } finally { + setIsModifying(false); + } + }; + + const calculateRiskReward = () => { + if (!position.stop_loss || !position.take_profit) return null; + const risk = Math.abs(position.open_price - position.stop_loss); + const reward = Math.abs(position.take_profit - position.open_price); + if (risk === 0) return null; + return (reward / risk).toFixed(2); + }; + + const riskReward = calculateRiskReward(); + + return ( +
+ {/* Main Content */} +
+ {/* Header Row */} +
+
+ {/* Direction Badge */} +
+ {isBuy ? ( + + ) : ( + + )} +
+
+
+ {position.symbol} + + {isBuy ? 'BUY' : 'SELL'} + +
+

+ {position.volume} lots • #{position.ticket} +

+
+
+ + {/* P&L */} +
+

+ {isProfit ? '+' : ''}{position.profit.toFixed(2)} USD +

+

+ {pips >= 0 ? '+' : ''}{pips.toFixed(1)} pips +

+
+
+ + {/* Price Info */} +
+
+

Entry

+

{position.open_price.toFixed(5)}

+
+
+

Current

+

+ {displayPrice.toFixed(5)} +

+
+
+

Open Time

+

{formatTime(position.open_time)}

+
+
+ + {/* SL/TP Indicators */} +
+ {position.stop_loss ? ( +
+ + SL: + {position.stop_loss.toFixed(5)} +
+ ) : ( +
+ + No SL +
+ )} + {position.take_profit ? ( +
+ + TP: + {position.take_profit.toFixed(5)} +
+ ) : ( +
+ + No TP +
+ )} + {riskReward && ( +
+ R:R 1:{riskReward} +
+ )} +
+ + {/* Expand/Collapse Button */} + +
+ + {/* Expanded Content */} + {isExpanded && ( +
+ {/* Comment */} + {position.comment && ( +
+

Comment

+

{position.comment}

+
+ )} + + {/* Modify Mode */} + {modifyMode ? ( +
+
+
+ + setNewStopLoss(e.target.value)} + placeholder="0.00000" + className="w-full px-3 py-2 bg-gray-900 border border-gray-700 rounded-lg text-white font-mono text-sm focus:outline-none focus:border-red-500" + /> +
+
+ + setNewTakeProfit(e.target.value)} + placeholder="0.00000" + className="w-full px-3 py-2 bg-gray-900 border border-gray-700 rounded-lg text-white font-mono text-sm focus:outline-none focus:border-green-500" + /> +
+
+
+ + +
+
+ ) : ( + /* Action Buttons */ +
+ {onModify && ( + + )} + {onClose && ( + + )} +
+ )} +
+ )} +
+ ); +}; + +export default LivePositionCard; diff --git a/src/modules/trading/components/MT4ConnectionStatus.tsx b/src/modules/trading/components/MT4ConnectionStatus.tsx new file mode 100644 index 0000000..5da0337 --- /dev/null +++ b/src/modules/trading/components/MT4ConnectionStatus.tsx @@ -0,0 +1,310 @@ +/** + * MT4ConnectionStatus Component + * Displays MT4 broker connection status with account info + */ + +import React, { useEffect, useState, useCallback } from 'react'; +import { + Wifi, + WifiOff, + RefreshCw, + AlertTriangle, + CheckCircle, + Server, + DollarSign, + TrendingUp, + Settings, +} from 'lucide-react'; +import { getMT4Account, type MT4Account } from '../../../services/trading.service'; + +interface MT4ConnectionStatusProps { + onAccountLoad?: (account: MT4Account | null) => void; + onSettingsClick?: () => void; + compact?: boolean; + autoRefresh?: boolean; + refreshInterval?: number; +} + +type ConnectionState = 'connecting' | 'connected' | 'disconnected' | 'error'; + +const MT4ConnectionStatus: React.FC = ({ + onAccountLoad, + onSettingsClick, + compact = false, + autoRefresh = true, + refreshInterval = 30000, +}) => { + const [connectionState, setConnectionState] = useState('connecting'); + const [account, setAccount] = useState(null); + const [error, setError] = useState(null); + const [lastUpdate, setLastUpdate] = useState(null); + const [isRefreshing, setIsRefreshing] = useState(false); + + const fetchAccount = useCallback(async () => { + setIsRefreshing(true); + try { + const data = await getMT4Account(); + if (data && data.connected) { + setAccount(data); + setConnectionState('connected'); + setError(null); + onAccountLoad?.(data); + } else if (data && !data.connected) { + setAccount(data); + setConnectionState('disconnected'); + setError('MT4 terminal not connected'); + onAccountLoad?.(null); + } else { + setConnectionState('disconnected'); + setError('No MT4 account configured'); + onAccountLoad?.(null); + } + setLastUpdate(new Date()); + } catch (err) { + setConnectionState('error'); + setError(err instanceof Error ? err.message : 'Connection error'); + onAccountLoad?.(null); + } finally { + setIsRefreshing(false); + } + }, [onAccountLoad]); + + useEffect(() => { + fetchAccount(); + + if (autoRefresh) { + const interval = setInterval(fetchAccount, refreshInterval); + return () => clearInterval(interval); + } + }, [fetchAccount, autoRefresh, refreshInterval]); + + const getStatusColor = () => { + switch (connectionState) { + case 'connected': + return 'text-green-400'; + case 'connecting': + return 'text-yellow-400'; + case 'disconnected': + return 'text-gray-400'; + case 'error': + return 'text-red-400'; + } + }; + + const getStatusIcon = () => { + switch (connectionState) { + case 'connected': + return ; + case 'connecting': + return ; + case 'disconnected': + return ; + case 'error': + return ; + } + }; + + const getStatusText = () => { + switch (connectionState) { + case 'connected': + return 'Connected'; + case 'connecting': + return 'Connecting...'; + case 'disconnected': + return 'Disconnected'; + case 'error': + return 'Error'; + } + }; + + const formatCurrency = (value: number, currency: string = 'USD') => { + return new Intl.NumberFormat('en-US', { + style: 'currency', + currency: currency, + minimumFractionDigits: 2, + }).format(value); + }; + + const marginLevel = account?.margin && account.margin > 0 + ? ((account.equity / account.margin) * 100) + : null; + + if (compact) { + return ( +
+
+ {getStatusIcon()} + {getStatusText()} +
+ {connectionState === 'connected' && account && ( + + {formatCurrency(account.balance, account.currency)} + + )} + +
+ ); + } + + return ( +
+ {/* Header */} +
+
+
+ +
+
+

MT4 Connection

+
+ {getStatusIcon()} + {getStatusText()} +
+
+
+
+ + {onSettingsClick && ( + + )} +
+
+ + {/* Content */} + {connectionState === 'connected' && account ? ( +
+ {/* Broker Info */} +
+ Broker + {account.broker} +
+
+ Account + {account.account_id} +
+
+ Leverage + 1:{account.leverage} +
+ +
+ {/* Balance */} +
+
+ + Balance +
+ + {formatCurrency(account.balance, account.currency)} + +
+ + {/* Equity */} +
+
+ + Equity +
+ = account.balance ? 'text-green-400' : 'text-red-400' + }`}> + {formatCurrency(account.equity, account.currency)} + +
+ + {/* Free Margin */} +
+ Free Margin + + {formatCurrency(account.free_margin, account.currency)} + +
+ + {/* Margin Level */} + {marginLevel !== null && account.margin > 0 && ( +
+
+ Margin Level + 200 ? 'text-green-400' : + marginLevel > 100 ? 'text-yellow-400' : 'text-red-400' + }`}> + {marginLevel.toFixed(0)}% + +
+
+
200 ? 'bg-green-500' : + marginLevel > 100 ? 'bg-yellow-500' : 'bg-red-500' + }`} + style={{ width: `${Math.min(marginLevel / 5, 100)}%` }} + /> +
+
+ )} +
+ + {/* Last Update */} + {lastUpdate && ( +

+ Last updated: {lastUpdate.toLocaleTimeString()} +

+ )} +
+ ) : connectionState === 'error' || connectionState === 'disconnected' ? ( +
+
+ {connectionState === 'error' ? ( + + ) : ( + + )} +
+

+ {connectionState === 'error' ? 'Connection Error' : 'Not Connected'} +

+

+ {error || 'MT4 terminal is not connected'} +

+ +
+ ) : ( +
+ +
+ )} +
+ ); +}; + +export default MT4ConnectionStatus; diff --git a/src/modules/trading/components/RiskMonitor.tsx b/src/modules/trading/components/RiskMonitor.tsx new file mode 100644 index 0000000..00f4c33 --- /dev/null +++ b/src/modules/trading/components/RiskMonitor.tsx @@ -0,0 +1,385 @@ +/** + * RiskMonitor Component + * Displays real-time risk metrics and warnings + */ + +import React from 'react'; +import { + Shield, + AlertTriangle, + TrendingDown, + Percent, + DollarSign, + Activity, + BarChart3, + AlertCircle, + CheckCircle, +} from 'lucide-react'; +import type { MT4Account, MT4Position } from '../../../services/trading.service'; + +interface RiskMetrics { + totalExposure: number; + marginUsed: number; + marginLevel: number; + unrealizedPnL: number; + dailyPnL: number; + dailyPnLPercent: number; + maxDrawdown: number; + openPositions: number; + totalLots: number; + largestPosition: number; + riskPerTrade: number; +} + +interface RiskMonitorProps { + account: MT4Account | null; + positions: MT4Position[]; + dailyStartBalance?: number; + maxDailyLoss?: number; + maxDrawdownLimit?: number; + maxPositions?: number; + maxLotSize?: number; + compact?: boolean; +} + +const RiskMonitor: React.FC = ({ + account, + positions, + dailyStartBalance, + maxDailyLoss = 5, + maxDrawdownLimit = 10, + maxPositions = 10, + maxLotSize = 1.0, + compact = false, +}) => { + // Calculate risk metrics + const calculateMetrics = (): RiskMetrics | null => { + 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 largestPosition = positions.length > 0 + ? Math.max(...positions.map(p => p.volume)) + : 0; + + const marginLevel = account.margin > 0 + ? (account.equity / account.margin) * 100 + : 9999; + + const dailyPnL = dailyStartBalance + ? account.equity - dailyStartBalance + : unrealizedPnL; + const dailyPnLPercent = dailyStartBalance + ? (dailyPnL / dailyStartBalance) * 100 + : (dailyPnL / account.balance) * 100; + + // Simple exposure calculation (could be enhanced) + const totalExposure = (account.margin / account.balance) * 100; + + return { + totalExposure, + marginUsed: account.margin, + marginLevel, + unrealizedPnL, + dailyPnL, + dailyPnLPercent, + maxDrawdown: Math.min(0, dailyPnLPercent), // Simplified + openPositions: positions.length, + totalLots, + largestPosition, + riskPerTrade: totalLots > 0 ? (account.margin / positions.length) : 0, + }; + }; + + const metrics = calculateMetrics(); + + // Risk level assessment + const getRiskLevel = (): 'low' | 'medium' | 'high' | 'critical' => { + if (!metrics) return 'low'; + + if ( + metrics.marginLevel < 100 || + Math.abs(metrics.dailyPnLPercent) > maxDailyLoss || + metrics.openPositions > maxPositions + ) { + return 'critical'; + } + + if ( + metrics.marginLevel < 200 || + Math.abs(metrics.dailyPnLPercent) > maxDailyLoss * 0.7 || + metrics.totalExposure > 50 + ) { + return 'high'; + } + + if ( + metrics.marginLevel < 500 || + Math.abs(metrics.dailyPnLPercent) > maxDailyLoss * 0.5 || + metrics.totalExposure > 30 + ) { + return 'medium'; + } + + return 'low'; + }; + + const riskLevel = getRiskLevel(); + + const riskColors = { + low: 'text-green-400', + medium: 'text-yellow-400', + high: 'text-orange-400', + critical: 'text-red-400', + }; + + const riskBgColors = { + low: 'bg-green-500/20 border-green-500/30', + medium: 'bg-yellow-500/20 border-yellow-500/30', + high: 'bg-orange-500/20 border-orange-500/30', + critical: 'bg-red-500/20 border-red-500/30', + }; + + const riskLabels = { + low: 'Low Risk', + medium: 'Moderate', + high: 'High Risk', + critical: 'Critical', + }; + + const formatCurrency = (value: number) => { + return new Intl.NumberFormat('en-US', { + style: 'currency', + currency: account?.currency || 'USD', + minimumFractionDigits: 2, + }).format(value); + }; + + if (!account || !metrics) { + return ( +
+ +

Risk Monitor

+

Connect MT4 to view risk metrics

+
+ ); + } + + if (compact) { + return ( +
+
+ + + {riskLabels[riskLevel]} + +
+
+ Margin: {metrics.marginLevel.toFixed(0)}% + P&L: {metrics.dailyPnLPercent >= 0 ? '+' : ''}{metrics.dailyPnLPercent.toFixed(2)}% + Positions: {metrics.openPositions} +
+
+ ); + } + + return ( +
+ {/* Header */} +
+
+
+ +
+
+

Risk Monitor

+

{riskLabels[riskLevel]}

+
+
+ {riskLevel === 'critical' && ( +
+ + Warning +
+ )} +
+ + {/* Metrics Grid */} +
+ {/* Primary Metrics */} +
+ {/* Margin Level */} +
+
+ + Margin Level +
+

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

+
+
200 ? 'bg-green-500' : + metrics.marginLevel > 100 ? 'bg-yellow-500' : 'bg-red-500' + }`} + style={{ width: `${Math.min(metrics.marginLevel / 5, 100)}%` }} + /> +
+
+ + {/* Daily P&L */} +
+
+ + Daily P&L +
+

= 0 ? 'text-green-400' : 'text-red-400' + }`}> + {metrics.dailyPnL >= 0 ? '+' : ''}{metrics.dailyPnLPercent.toFixed(2)}% +

+

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

+
+
+ + {/* Secondary Metrics */} +
+
+ +

{metrics.openPositions}

+

Positions

+
+
+ +

{metrics.totalLots.toFixed(2)}

+

Total Lots

+
+
+ +

+ {formatCurrency(metrics.marginUsed)} +

+

Margin Used

+
+
+ + {/* Risk Warnings */} +
+ {/* Daily Loss Limit */} + + + {/* Position Count */} + + + {/* Largest Position */} + +
+ + {/* Warnings */} + {riskLevel !== 'low' && ( +
+
+ +
+

Risk Warnings

+
    + {metrics.marginLevel < 200 && ( +
  • • Margin level below 200%
  • + )} + {Math.abs(metrics.dailyPnLPercent) > maxDailyLoss * 0.7 && ( +
  • • Approaching daily loss limit
  • + )} + {metrics.openPositions > maxPositions * 0.8 && ( +
  • • Many open positions
  • + )} + {metrics.totalExposure > 30 && ( +
  • • High account exposure
  • + )} +
+
+
+
+ )} +
+
+ ); +}; + +// Risk Indicator Sub-component +interface RiskIndicatorProps { + label: string; + current: number; + max: number; + unit: string; + inverted?: boolean; +} + +const RiskIndicator: React.FC = ({ + label, + current, + max, + unit, + inverted = false, +}) => { + const percentage = (current / max) * 100; + const isWarning = inverted ? current > max * 0.7 : percentage > 70; + const isDanger = inverted ? current > max : percentage > 100; + + return ( +
+
+
+ {label} + + {current.toFixed(current < 10 ? 2 : 0)}{unit} / {max}{unit} + +
+
+
+
+
+ {isDanger ? ( + + ) : isWarning ? ( + + ) : ( + + )} +
+ ); +}; + +export default RiskMonitor; diff --git a/src/modules/trading/components/index.ts b/src/modules/trading/components/index.ts new file mode 100644 index 0000000..c4dacdf --- /dev/null +++ b/src/modules/trading/components/index.ts @@ -0,0 +1,39 @@ +/** + * Trading Components - Index Export + * OQI-003: Trading y Charts + * OQI-009: Trading Execution (MT4 Gateway) + */ + +// Chart Components +export { default as CandlestickChart } from './CandlestickChart'; +export { default as CandlestickChartWithML } from './CandlestickChartWithML'; +export { default as TradingChart } from './TradingChart'; +export { default as ChartToolbar } from './ChartToolbar'; + +// Order & Position Components +export { default as OrderForm } from './OrderForm'; +export { default as PositionsList } from './PositionsList'; +export { default as TradesHistory } from './TradesHistory'; + +// Watchlist Components +export { default as WatchlistSidebar } from './WatchlistSidebar'; +export { default as WatchlistItem } from './WatchlistItem'; +export { default as AddSymbolModal } from './AddSymbolModal'; + +// Account & Stats Components +export { default as AccountSummary } from './AccountSummary'; +export { default as TradingStatsPanel } from './TradingStatsPanel'; + +// Panel Components +export { default as MLSignalsPanel } from './MLSignalsPanel'; +export { default as AlertsPanel } from './AlertsPanel'; +export { default as OrderBookPanel } from './OrderBookPanel'; +export { default as PaperTradingPanel } from './PaperTradingPanel'; + +// Utility Components +export { default as ExportButton } from './ExportButton'; + +// MT4 Gateway Components (OQI-009) +export { default as MT4ConnectionStatus } from './MT4ConnectionStatus'; +export { default as LivePositionCard } from './LivePositionCard'; +export { default as RiskMonitor } from './RiskMonitor';