[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 <noreply@anthropic.com>
This commit is contained in:
Adrian Flores Cortes 2026-01-25 10:41:01 -06:00
parent fc0ab528c3
commit 423be4062c
4 changed files with 1041 additions and 0 deletions

View File

@ -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<void>;
onModify?: (ticket: number, stopLoss?: number, takeProfit?: number) => Promise<void>;
loading?: boolean;
}
const LivePositionCard: React.FC<LivePositionCardProps> = ({
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<string>(position.stop_loss?.toString() || '');
const [newTakeProfit, setNewTakeProfit] = useState<string>(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 (
<div className={`bg-gray-800 rounded-xl border ${
isProfit ? 'border-green-500/30' : 'border-red-500/30'
} overflow-hidden transition-all`}>
{/* Main Content */}
<div className="p-4">
{/* Header Row */}
<div className="flex items-start justify-between mb-3">
<div className="flex items-center gap-3">
{/* Direction Badge */}
<div className={`p-2 rounded-lg ${
isBuy ? 'bg-green-500/20' : 'bg-red-500/20'
}`}>
{isBuy ? (
<TrendingUp className="w-5 h-5 text-green-400" />
) : (
<TrendingDown className="w-5 h-5 text-red-400" />
)}
</div>
<div>
<div className="flex items-center gap-2">
<span className="font-bold text-white">{position.symbol}</span>
<span className={`px-2 py-0.5 text-xs rounded font-medium ${
isBuy ? 'bg-green-500/20 text-green-400' : 'bg-red-500/20 text-red-400'
}`}>
{isBuy ? 'BUY' : 'SELL'}
</span>
</div>
<p className="text-sm text-gray-400">
{position.volume} lots #{position.ticket}
</p>
</div>
</div>
{/* P&L */}
<div className="text-right">
<p className={`text-lg font-bold ${isProfit ? 'text-green-400' : 'text-red-400'}`}>
{isProfit ? '+' : ''}{position.profit.toFixed(2)} USD
</p>
<p className={`text-sm ${isProfit ? 'text-green-400/70' : 'text-red-400/70'}`}>
{pips >= 0 ? '+' : ''}{pips.toFixed(1)} pips
</p>
</div>
</div>
{/* Price Info */}
<div className="grid grid-cols-3 gap-4 text-sm mb-3">
<div>
<p className="text-gray-500 text-xs mb-0.5">Entry</p>
<p className="text-white font-mono">{position.open_price.toFixed(5)}</p>
</div>
<div>
<p className="text-gray-500 text-xs mb-0.5">Current</p>
<p className={`font-mono ${isProfit ? 'text-green-400' : 'text-red-400'}`}>
{displayPrice.toFixed(5)}
</p>
</div>
<div className="text-right">
<p className="text-gray-500 text-xs mb-0.5">Open Time</p>
<p className="text-gray-300 text-xs">{formatTime(position.open_time)}</p>
</div>
</div>
{/* SL/TP Indicators */}
<div className="flex items-center gap-4 text-sm">
{position.stop_loss ? (
<div className="flex items-center gap-1.5">
<Shield className="w-4 h-4 text-red-400" />
<span className="text-gray-400">SL:</span>
<span className="text-red-400 font-mono">{position.stop_loss.toFixed(5)}</span>
</div>
) : (
<div className="flex items-center gap-1.5 text-gray-500">
<Shield className="w-4 h-4" />
<span>No SL</span>
</div>
)}
{position.take_profit ? (
<div className="flex items-center gap-1.5">
<Target className="w-4 h-4 text-green-400" />
<span className="text-gray-400">TP:</span>
<span className="text-green-400 font-mono">{position.take_profit.toFixed(5)}</span>
</div>
) : (
<div className="flex items-center gap-1.5 text-gray-500">
<Target className="w-4 h-4" />
<span>No TP</span>
</div>
)}
{riskReward && (
<div className="ml-auto text-gray-400">
R:R <span className="text-white font-medium">1:{riskReward}</span>
</div>
)}
</div>
{/* Expand/Collapse Button */}
<button
onClick={() => setIsExpanded(!isExpanded)}
className="w-full mt-3 pt-3 border-t border-gray-700 flex items-center justify-center gap-1 text-sm text-gray-400 hover:text-white transition-colors"
>
{isExpanded ? (
<>
<ChevronUp className="w-4 h-4" />
Less
</>
) : (
<>
<ChevronDown className="w-4 h-4" />
More
</>
)}
</button>
</div>
{/* Expanded Content */}
{isExpanded && (
<div className="px-4 pb-4 space-y-4">
{/* Comment */}
{position.comment && (
<div className="p-3 bg-gray-900/50 rounded-lg">
<p className="text-xs text-gray-500 mb-1">Comment</p>
<p className="text-sm text-gray-300">{position.comment}</p>
</div>
)}
{/* Modify Mode */}
{modifyMode ? (
<div className="space-y-3">
<div className="grid grid-cols-2 gap-3">
<div>
<label className="block text-xs text-gray-400 mb-1">Stop Loss</label>
<input
type="number"
step="0.00001"
value={newStopLoss}
onChange={(e) => 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"
/>
</div>
<div>
<label className="block text-xs text-gray-400 mb-1">Take Profit</label>
<input
type="number"
step="0.00001"
value={newTakeProfit}
onChange={(e) => 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"
/>
</div>
</div>
<div className="flex items-center gap-2">
<button
onClick={() => setModifyMode(false)}
className="flex-1 py-2 bg-gray-700 hover:bg-gray-600 text-white rounded-lg text-sm transition-colors"
>
Cancel
</button>
<button
onClick={handleModify}
disabled={isModifying}
className="flex-1 py-2 bg-blue-600 hover:bg-blue-500 text-white rounded-lg text-sm transition-colors disabled:opacity-50 flex items-center justify-center gap-2"
>
{isModifying ? (
<Loader2 className="w-4 h-4 animate-spin" />
) : (
'Save Changes'
)}
</button>
</div>
</div>
) : (
/* Action Buttons */
<div className="flex items-center gap-2">
{onModify && (
<button
onClick={() => setModifyMode(true)}
disabled={loading}
className="flex-1 flex items-center justify-center gap-2 py-2 bg-gray-700 hover:bg-gray-600 text-white rounded-lg text-sm transition-colors disabled:opacity-50"
>
<Edit3 className="w-4 h-4" />
Modify
</button>
)}
{onClose && (
<button
onClick={handleClose}
disabled={isClosing || loading}
className="flex-1 flex items-center justify-center gap-2 py-2 bg-red-600 hover:bg-red-500 text-white rounded-lg text-sm transition-colors disabled:opacity-50"
>
{isClosing ? (
<Loader2 className="w-4 h-4 animate-spin" />
) : (
<>
<X className="w-4 h-4" />
Close Position
</>
)}
</button>
)}
</div>
)}
</div>
)}
</div>
);
};
export default LivePositionCard;

View File

@ -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<MT4ConnectionStatusProps> = ({
onAccountLoad,
onSettingsClick,
compact = false,
autoRefresh = true,
refreshInterval = 30000,
}) => {
const [connectionState, setConnectionState] = useState<ConnectionState>('connecting');
const [account, setAccount] = useState<MT4Account | null>(null);
const [error, setError] = useState<string | null>(null);
const [lastUpdate, setLastUpdate] = useState<Date | null>(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 <Wifi className="w-4 h-4" />;
case 'connecting':
return <RefreshCw className="w-4 h-4 animate-spin" />;
case 'disconnected':
return <WifiOff className="w-4 h-4" />;
case 'error':
return <AlertTriangle className="w-4 h-4" />;
}
};
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 (
<div className="flex items-center gap-2">
<div className={`flex items-center gap-1.5 ${getStatusColor()}`}>
{getStatusIcon()}
<span className="text-sm font-medium">{getStatusText()}</span>
</div>
{connectionState === 'connected' && account && (
<span className="text-sm text-gray-400">
{formatCurrency(account.balance, account.currency)}
</span>
)}
<button
onClick={fetchAccount}
disabled={isRefreshing}
className="p-1 text-gray-400 hover:text-white transition-colors disabled:opacity-50"
title="Refresh"
>
<RefreshCw className={`w-3.5 h-3.5 ${isRefreshing ? 'animate-spin' : ''}`} />
</button>
</div>
);
}
return (
<div className="bg-gray-800 rounded-xl border border-gray-700 overflow-hidden">
{/* Header */}
<div className="flex items-center justify-between p-4 border-b border-gray-700">
<div className="flex items-center gap-3">
<div className={`p-2 rounded-lg ${
connectionState === 'connected' ? 'bg-green-500/20' : 'bg-gray-700'
}`}>
<Server className={`w-5 h-5 ${getStatusColor()}`} />
</div>
<div>
<h3 className="font-medium text-white">MT4 Connection</h3>
<div className={`flex items-center gap-1.5 text-sm ${getStatusColor()}`}>
{getStatusIcon()}
<span>{getStatusText()}</span>
</div>
</div>
</div>
<div className="flex items-center gap-2">
<button
onClick={fetchAccount}
disabled={isRefreshing}
className="p-2 text-gray-400 hover:text-white hover:bg-gray-700 rounded-lg transition-colors disabled:opacity-50"
title="Refresh connection"
>
<RefreshCw className={`w-4 h-4 ${isRefreshing ? 'animate-spin' : ''}`} />
</button>
{onSettingsClick && (
<button
onClick={onSettingsClick}
className="p-2 text-gray-400 hover:text-white hover:bg-gray-700 rounded-lg transition-colors"
title="Connection settings"
>
<Settings className="w-4 h-4" />
</button>
)}
</div>
</div>
{/* Content */}
{connectionState === 'connected' && account ? (
<div className="p-4 space-y-4">
{/* Broker Info */}
<div className="flex items-center justify-between text-sm">
<span className="text-gray-400">Broker</span>
<span className="text-white font-medium">{account.broker}</span>
</div>
<div className="flex items-center justify-between text-sm">
<span className="text-gray-400">Account</span>
<span className="text-white font-mono">{account.account_id}</span>
</div>
<div className="flex items-center justify-between text-sm">
<span className="text-gray-400">Leverage</span>
<span className="text-white">1:{account.leverage}</span>
</div>
<div className="border-t border-gray-700 pt-4 space-y-3">
{/* Balance */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-2 text-gray-400">
<DollarSign className="w-4 h-4" />
<span className="text-sm">Balance</span>
</div>
<span className="text-white font-medium">
{formatCurrency(account.balance, account.currency)}
</span>
</div>
{/* Equity */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-2 text-gray-400">
<TrendingUp className="w-4 h-4" />
<span className="text-sm">Equity</span>
</div>
<span className={`font-medium ${
account.equity >= account.balance ? 'text-green-400' : 'text-red-400'
}`}>
{formatCurrency(account.equity, account.currency)}
</span>
</div>
{/* Free Margin */}
<div className="flex items-center justify-between">
<span className="text-sm text-gray-400">Free Margin</span>
<span className="text-white">
{formatCurrency(account.free_margin, account.currency)}
</span>
</div>
{/* Margin Level */}
{marginLevel !== null && account.margin > 0 && (
<div className="pt-2">
<div className="flex items-center justify-between text-sm mb-1">
<span className="text-gray-400">Margin Level</span>
<span className={`font-medium ${
marginLevel > 200 ? 'text-green-400' :
marginLevel > 100 ? 'text-yellow-400' : 'text-red-400'
}`}>
{marginLevel.toFixed(0)}%
</span>
</div>
<div className="h-2 bg-gray-700 rounded-full overflow-hidden">
<div
className={`h-full rounded-full transition-all ${
marginLevel > 200 ? 'bg-green-500' :
marginLevel > 100 ? 'bg-yellow-500' : 'bg-red-500'
}`}
style={{ width: `${Math.min(marginLevel / 5, 100)}%` }}
/>
</div>
</div>
)}
</div>
{/* Last Update */}
{lastUpdate && (
<p className="text-xs text-gray-500 pt-2">
Last updated: {lastUpdate.toLocaleTimeString()}
</p>
)}
</div>
) : connectionState === 'error' || connectionState === 'disconnected' ? (
<div className="p-6 text-center">
<div className={`w-12 h-12 rounded-full ${
connectionState === 'error' ? 'bg-red-500/20' : 'bg-gray-700'
} flex items-center justify-center mx-auto mb-3`}>
{connectionState === 'error' ? (
<AlertTriangle className="w-6 h-6 text-red-400" />
) : (
<WifiOff className="w-6 h-6 text-gray-400" />
)}
</div>
<h4 className="font-medium text-white mb-1">
{connectionState === 'error' ? 'Connection Error' : 'Not Connected'}
</h4>
<p className="text-sm text-gray-400 mb-4">
{error || 'MT4 terminal is not connected'}
</p>
<button
onClick={fetchAccount}
className="px-4 py-2 bg-blue-600 hover:bg-blue-500 text-white rounded-lg text-sm transition-colors"
>
Retry Connection
</button>
</div>
) : (
<div className="p-6 flex items-center justify-center">
<RefreshCw className="w-6 h-6 text-blue-400 animate-spin" />
</div>
)}
</div>
);
};
export default MT4ConnectionStatus;

View File

@ -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<RiskMonitorProps> = ({
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 (
<div className="bg-gray-800 rounded-xl border border-gray-700 p-6 text-center">
<Shield className="w-10 h-10 text-gray-500 mx-auto mb-3" />
<h4 className="font-medium text-white mb-1">Risk Monitor</h4>
<p className="text-sm text-gray-400">Connect MT4 to view risk metrics</p>
</div>
);
}
if (compact) {
return (
<div className={`flex items-center gap-4 p-3 rounded-lg border ${riskBgColors[riskLevel]}`}>
<div className="flex items-center gap-2">
<Shield className={`w-5 h-5 ${riskColors[riskLevel]}`} />
<span className={`font-medium ${riskColors[riskLevel]}`}>
{riskLabels[riskLevel]}
</span>
</div>
<div className="flex items-center gap-4 text-sm text-gray-400">
<span>Margin: {metrics.marginLevel.toFixed(0)}%</span>
<span>P&L: {metrics.dailyPnLPercent >= 0 ? '+' : ''}{metrics.dailyPnLPercent.toFixed(2)}%</span>
<span>Positions: {metrics.openPositions}</span>
</div>
</div>
);
}
return (
<div className="bg-gray-800 rounded-xl border border-gray-700 overflow-hidden">
{/* Header */}
<div className={`flex items-center justify-between p-4 border-b ${riskBgColors[riskLevel]}`}>
<div className="flex items-center gap-3">
<div className={`p-2 rounded-lg ${riskBgColors[riskLevel]}`}>
<Shield className={`w-5 h-5 ${riskColors[riskLevel]}`} />
</div>
<div>
<h3 className="font-medium text-white">Risk Monitor</h3>
<p className={`text-sm ${riskColors[riskLevel]}`}>{riskLabels[riskLevel]}</p>
</div>
</div>
{riskLevel === 'critical' && (
<div className="flex items-center gap-2 px-3 py-1 bg-red-500/30 rounded-full">
<AlertTriangle className="w-4 h-4 text-red-400" />
<span className="text-sm text-red-400 font-medium">Warning</span>
</div>
)}
</div>
{/* Metrics Grid */}
<div className="p-4 space-y-4">
{/* Primary Metrics */}
<div className="grid grid-cols-2 gap-4">
{/* Margin Level */}
<div className="p-3 bg-gray-900/50 rounded-lg">
<div className="flex items-center gap-2 text-gray-400 text-sm mb-2">
<Percent className="w-4 h-4" />
<span>Margin Level</span>
</div>
<p className={`text-2xl font-bold ${
metrics.marginLevel > 200 ? 'text-green-400' :
metrics.marginLevel > 100 ? 'text-yellow-400' : 'text-red-400'
}`}>
{metrics.marginLevel > 9000 ? '∞' : `${metrics.marginLevel.toFixed(0)}%`}
</p>
<div className="mt-2 h-1.5 bg-gray-700 rounded-full overflow-hidden">
<div
className={`h-full rounded-full ${
metrics.marginLevel > 200 ? 'bg-green-500' :
metrics.marginLevel > 100 ? 'bg-yellow-500' : 'bg-red-500'
}`}
style={{ width: `${Math.min(metrics.marginLevel / 5, 100)}%` }}
/>
</div>
</div>
{/* Daily P&L */}
<div className="p-3 bg-gray-900/50 rounded-lg">
<div className="flex items-center gap-2 text-gray-400 text-sm mb-2">
<TrendingDown className="w-4 h-4" />
<span>Daily P&L</span>
</div>
<p className={`text-2xl font-bold ${
metrics.dailyPnL >= 0 ? 'text-green-400' : 'text-red-400'
}`}>
{metrics.dailyPnL >= 0 ? '+' : ''}{metrics.dailyPnLPercent.toFixed(2)}%
</p>
<p className={`text-sm ${
metrics.dailyPnL >= 0 ? 'text-green-400/70' : 'text-red-400/70'
}`}>
{formatCurrency(metrics.dailyPnL)}
</p>
</div>
</div>
{/* Secondary Metrics */}
<div className="grid grid-cols-3 gap-3">
<div className="text-center p-3 bg-gray-900/50 rounded-lg">
<Activity className="w-4 h-4 text-gray-400 mx-auto mb-1" />
<p className="text-lg font-semibold text-white">{metrics.openPositions}</p>
<p className="text-xs text-gray-500">Positions</p>
</div>
<div className="text-center p-3 bg-gray-900/50 rounded-lg">
<BarChart3 className="w-4 h-4 text-gray-400 mx-auto mb-1" />
<p className="text-lg font-semibold text-white">{metrics.totalLots.toFixed(2)}</p>
<p className="text-xs text-gray-500">Total Lots</p>
</div>
<div className="text-center p-3 bg-gray-900/50 rounded-lg">
<DollarSign className="w-4 h-4 text-gray-400 mx-auto mb-1" />
<p className="text-lg font-semibold text-white">
{formatCurrency(metrics.marginUsed)}
</p>
<p className="text-xs text-gray-500">Margin Used</p>
</div>
</div>
{/* Risk Warnings */}
<div className="space-y-2">
{/* Daily Loss Limit */}
<RiskIndicator
label="Daily Loss Limit"
current={Math.abs(metrics.dailyPnLPercent)}
max={maxDailyLoss}
unit="%"
inverted
/>
{/* Position Count */}
<RiskIndicator
label="Open Positions"
current={metrics.openPositions}
max={maxPositions}
unit=""
/>
{/* Largest Position */}
<RiskIndicator
label="Largest Position"
current={metrics.largestPosition}
max={maxLotSize}
unit=" lots"
/>
</div>
{/* Warnings */}
{riskLevel !== 'low' && (
<div className={`p-3 rounded-lg border ${riskBgColors[riskLevel]}`}>
<div className="flex items-start gap-2">
<AlertCircle className={`w-5 h-5 flex-shrink-0 mt-0.5 ${riskColors[riskLevel]}`} />
<div>
<p className={`font-medium ${riskColors[riskLevel]}`}>Risk Warnings</p>
<ul className="mt-1 space-y-1 text-sm text-gray-300">
{metrics.marginLevel < 200 && (
<li> Margin level below 200%</li>
)}
{Math.abs(metrics.dailyPnLPercent) > maxDailyLoss * 0.7 && (
<li> Approaching daily loss limit</li>
)}
{metrics.openPositions > maxPositions * 0.8 && (
<li> Many open positions</li>
)}
{metrics.totalExposure > 30 && (
<li> High account exposure</li>
)}
</ul>
</div>
</div>
</div>
)}
</div>
</div>
);
};
// Risk Indicator Sub-component
interface RiskIndicatorProps {
label: string;
current: number;
max: number;
unit: string;
inverted?: boolean;
}
const RiskIndicator: React.FC<RiskIndicatorProps> = ({
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 (
<div className="flex items-center gap-3">
<div className="flex-1">
<div className="flex items-center justify-between text-sm mb-1">
<span className="text-gray-400">{label}</span>
<span className={`font-medium ${
isDanger ? 'text-red-400' :
isWarning ? 'text-yellow-400' : 'text-white'
}`}>
{current.toFixed(current < 10 ? 2 : 0)}{unit} / {max}{unit}
</span>
</div>
<div className="h-1.5 bg-gray-700 rounded-full overflow-hidden">
<div
className={`h-full rounded-full transition-all ${
isDanger ? 'bg-red-500' :
isWarning ? 'bg-yellow-500' : 'bg-green-500'
}`}
style={{ width: `${Math.min(percentage, 100)}%` }}
/>
</div>
</div>
{isDanger ? (
<AlertTriangle className="w-4 h-4 text-red-400 flex-shrink-0" />
) : isWarning ? (
<AlertCircle className="w-4 h-4 text-yellow-400 flex-shrink-0" />
) : (
<CheckCircle className="w-4 h-4 text-green-400 flex-shrink-0" />
)}
</div>
);
};
export default RiskMonitor;

View File

@ -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';