[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:
parent
fc0ab528c3
commit
423be4062c
307
src/modules/trading/components/LivePositionCard.tsx
Normal file
307
src/modules/trading/components/LivePositionCard.tsx
Normal 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;
|
||||
310
src/modules/trading/components/MT4ConnectionStatus.tsx
Normal file
310
src/modules/trading/components/MT4ConnectionStatus.tsx
Normal 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;
|
||||
385
src/modules/trading/components/RiskMonitor.tsx
Normal file
385
src/modules/trading/components/RiskMonitor.tsx
Normal 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;
|
||||
39
src/modules/trading/components/index.ts
Normal file
39
src/modules/trading/components/index.ts
Normal 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';
|
||||
Loading…
Reference in New Issue
Block a user