[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