[OQI-009] feat: Add advanced MT4 trading components

- MT4PositionsManager: Container for all live positions with filtering/sorting
- AdvancedOrderEntry: Professional order form with risk calculator, SL/TP modes
- AccountHealthDashboard: Unified account metrics with health status

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Adrian Flores Cortes 2026-01-25 10:56:41 -06:00
parent bfaf76ccf8
commit bf726a595c
4 changed files with 1245 additions and 0 deletions

View File

@ -0,0 +1,391 @@
/**
* AccountHealthDashboard Component
* Unified view of account status and trading metrics
*/
import React, { useMemo } from 'react';
import {
TrendingUp,
TrendingDown,
DollarSign,
Percent,
Activity,
Award,
AlertTriangle,
Target,
BarChart3,
Clock,
Zap,
} from 'lucide-react';
import type { MT4Account, MT4Position } from '../../../services/trading.service';
interface TradeStats {
totalTrades: number;
winningTrades: number;
losingTrades: number;
largestWin: number;
largestLoss: number;
consecutiveWins: number;
consecutiveLosses: number;
}
interface AccountHealthDashboardProps {
account: MT4Account | null;
positions: MT4Position[];
tradeStats?: TradeStats;
dailyStartBalance?: number;
compact?: boolean;
}
const AccountHealthDashboard: React.FC<AccountHealthDashboardProps> = ({
account,
positions,
tradeStats,
dailyStartBalance,
compact = false,
}) => {
// Calculate metrics
const metrics = useMemo(() => {
if (!account) return null;
const unrealizedPnL = positions.reduce((sum, p) => sum + p.profit, 0);
const totalLots = positions.reduce((sum, p) => sum + p.volume, 0);
const drawdown = dailyStartBalance
? ((dailyStartBalance - account.equity) / dailyStartBalance) * 100
: ((account.balance - account.equity) / account.balance) * 100;
const marginLevel = account.margin > 0
? (account.equity / account.margin) * 100
: 9999;
const marginUsagePercent = account.balance > 0
? (account.margin / account.balance) * 100
: 0;
const winRate = tradeStats && tradeStats.totalTrades > 0
? (tradeStats.winningTrades / tradeStats.totalTrades) * 100
: null;
const profitFactor = tradeStats && tradeStats.largestLoss !== 0
? Math.abs(tradeStats.largestWin / tradeStats.largestLoss)
: null;
return {
equity: account.equity,
balance: account.balance,
freeMargin: account.free_margin,
marginUsed: account.margin,
marginLevel,
marginUsagePercent,
unrealizedPnL,
drawdown: Math.max(0, drawdown),
totalLots,
positionCount: positions.length,
winRate,
profitFactor,
};
}, [account, positions, dailyStartBalance, tradeStats]);
const formatCurrency = (value: number) => {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: account?.currency || 'USD',
minimumFractionDigits: 2,
}).format(value);
};
// Get health status
const getHealthStatus = (): 'excellent' | 'good' | 'warning' | 'critical' => {
if (!metrics) return 'warning';
if (
metrics.marginLevel < 150 ||
metrics.drawdown > 10 ||
metrics.marginUsagePercent > 50
) {
return 'critical';
}
if (
metrics.marginLevel < 300 ||
metrics.drawdown > 5 ||
metrics.marginUsagePercent > 30
) {
return 'warning';
}
if (
metrics.marginLevel > 500 &&
metrics.drawdown < 3 &&
metrics.marginUsagePercent < 20
) {
return 'excellent';
}
return 'good';
};
const healthStatus = getHealthStatus();
const healthColors = {
excellent: 'text-green-400 bg-green-500/20 border-green-500/30',
good: 'text-blue-400 bg-blue-500/20 border-blue-500/30',
warning: 'text-yellow-400 bg-yellow-500/20 border-yellow-500/30',
critical: 'text-red-400 bg-red-500/20 border-red-500/30',
};
const healthLabels = {
excellent: 'Excellent',
good: 'Good',
warning: 'Caution',
critical: 'At Risk',
};
if (!account || !metrics) {
return (
<div className="bg-gray-800 rounded-xl border border-gray-700 p-6 text-center">
<Activity className="w-10 h-10 text-gray-500 mx-auto mb-3" />
<h4 className="font-medium text-white mb-1">Account Health</h4>
<p className="text-sm text-gray-400">Connect MT4 to view metrics</p>
</div>
);
}
if (compact) {
return (
<div className={`flex items-center gap-4 p-3 rounded-lg border ${healthColors[healthStatus]}`}>
<div className="flex items-center gap-2">
<Activity className={`w-5 h-5 ${healthColors[healthStatus].split(' ')[0]}`} />
<span className={`font-medium ${healthColors[healthStatus].split(' ')[0]}`}>
{healthLabels[healthStatus]}
</span>
</div>
<div className="flex items-center gap-4 text-sm text-gray-400">
<span>Equity: {formatCurrency(metrics.equity)}</span>
<span>DD: {metrics.drawdown.toFixed(1)}%</span>
<span>Margin: {metrics.marginLevel > 9000 ? '∞' : `${metrics.marginLevel.toFixed(0)}%`}</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 ${healthColors[healthStatus]}`}>
<div className="flex items-center gap-3">
<div className={`p-2 rounded-lg ${healthColors[healthStatus]}`}>
<Activity className={`w-5 h-5 ${healthColors[healthStatus].split(' ')[0]}`} />
</div>
<div>
<h3 className="font-medium text-white">Account Health</h3>
<p className={`text-sm ${healthColors[healthStatus].split(' ')[0]}`}>
{healthLabels[healthStatus]}
</p>
</div>
</div>
{healthStatus === '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">High Risk</span>
</div>
)}
</div>
<div className="p-4 space-y-4">
{/* Main Metrics */}
<div className="grid grid-cols-2 gap-4">
{/* Equity vs Balance */}
<div className="p-4 bg-gray-900/50 rounded-lg">
<div className="flex items-center gap-2 text-gray-400 text-sm mb-2">
<DollarSign className="w-4 h-4" />
<span>Equity / Balance</span>
</div>
<p className={`text-2xl font-bold ${
metrics.equity >= metrics.balance ? 'text-green-400' : 'text-red-400'
}`}>
{formatCurrency(metrics.equity)}
</p>
<p className="text-sm text-gray-400">
of {formatCurrency(metrics.balance)}
</p>
{metrics.unrealizedPnL !== 0 && (
<p className={`text-xs mt-1 ${
metrics.unrealizedPnL >= 0 ? 'text-green-400' : 'text-red-400'
}`}>
Floating: {metrics.unrealizedPnL >= 0 ? '+' : ''}{formatCurrency(metrics.unrealizedPnL)}
</p>
)}
</div>
{/* Drawdown */}
<div className="p-4 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>Drawdown</span>
</div>
<p className={`text-2xl font-bold ${
metrics.drawdown < 3 ? 'text-green-400' :
metrics.drawdown < 7 ? 'text-yellow-400' : 'text-red-400'
}`}>
{metrics.drawdown.toFixed(2)}%
</p>
<div className="mt-2 h-2 bg-gray-700 rounded-full overflow-hidden">
<div
className={`h-full rounded-full transition-all ${
metrics.drawdown < 3 ? 'bg-green-500' :
metrics.drawdown < 7 ? 'bg-yellow-500' : 'bg-red-500'
}`}
style={{ width: `${Math.min(metrics.drawdown * 10, 100)}%` }}
/>
</div>
</div>
</div>
{/* Secondary Metrics */}
<div className="grid grid-cols-4 gap-3">
{/* Margin Level */}
<div className="text-center p-3 bg-gray-900/50 rounded-lg">
<Percent className="w-4 h-4 text-gray-400 mx-auto mb-1" />
<p className={`text-lg font-semibold ${
metrics.marginLevel > 300 ? 'text-green-400' :
metrics.marginLevel > 150 ? 'text-yellow-400' : 'text-red-400'
}`}>
{metrics.marginLevel > 9000 ? '∞' : `${metrics.marginLevel.toFixed(0)}%`}
</p>
<p className="text-xs text-gray-500">Margin Level</p>
</div>
{/* Free Margin */}
<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.freeMargin)}
</p>
<p className="text-xs text-gray-500">Free Margin</p>
</div>
{/* Positions */}
<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.positionCount}</p>
<p className="text-xs text-gray-500">Positions</p>
</div>
{/* Total Lots */}
<div className="text-center p-3 bg-gray-900/50 rounded-lg">
<Target 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>
{/* Trade Stats */}
{tradeStats && tradeStats.totalTrades > 0 && (
<div className="p-4 bg-gray-900/50 rounded-lg">
<h4 className="text-sm font-medium text-white mb-3 flex items-center gap-2">
<Award className="w-4 h-4 text-yellow-400" />
Trading Performance
</h4>
<div className="grid grid-cols-3 gap-4">
{/* Win Rate */}
<div>
<p className="text-xs text-gray-500 mb-1">Win Rate</p>
<p className={`text-lg font-bold ${
metrics.winRate && metrics.winRate >= 50 ? 'text-green-400' : 'text-red-400'
}`}>
{metrics.winRate?.toFixed(1)}%
</p>
<p className="text-xs text-gray-500">
{tradeStats.winningTrades}W / {tradeStats.losingTrades}L
</p>
</div>
{/* Profit Factor */}
<div>
<p className="text-xs text-gray-500 mb-1">Profit Factor</p>
<p className={`text-lg font-bold ${
metrics.profitFactor && metrics.profitFactor >= 1.5 ? 'text-green-400' :
metrics.profitFactor && metrics.profitFactor >= 1 ? 'text-yellow-400' : 'text-red-400'
}`}>
{metrics.profitFactor?.toFixed(2) || '-'}
</p>
</div>
{/* Streak */}
<div>
<p className="text-xs text-gray-500 mb-1">Current Streak</p>
{tradeStats.consecutiveWins > 0 ? (
<div className="flex items-center gap-1">
<Zap className="w-4 h-4 text-green-400" />
<span className="text-lg font-bold text-green-400">
{tradeStats.consecutiveWins}W
</span>
</div>
) : tradeStats.consecutiveLosses > 0 ? (
<div className="flex items-center gap-1">
<TrendingDown className="w-4 h-4 text-red-400" />
<span className="text-lg font-bold text-red-400">
{tradeStats.consecutiveLosses}L
</span>
</div>
) : (
<span className="text-lg font-bold text-gray-400">-</span>
)}
</div>
</div>
{/* Best/Worst Trade */}
<div className="flex items-center justify-between mt-3 pt-3 border-t border-gray-700">
<div className="flex items-center gap-2">
<TrendingUp className="w-4 h-4 text-green-400" />
<span className="text-sm text-gray-400">Best:</span>
<span className="text-sm text-green-400 font-medium">
+{formatCurrency(tradeStats.largestWin)}
</span>
</div>
<div className="flex items-center gap-2">
<TrendingDown className="w-4 h-4 text-red-400" />
<span className="text-sm text-gray-400">Worst:</span>
<span className="text-sm text-red-400 font-medium">
{formatCurrency(tradeStats.largestLoss)}
</span>
</div>
</div>
</div>
)}
{/* Warnings */}
{healthStatus !== 'excellent' && healthStatus !== 'good' && (
<div className={`p-3 rounded-lg border ${healthColors[healthStatus]}`}>
<div className="flex items-start gap-2">
<AlertTriangle className={`w-5 h-5 flex-shrink-0 mt-0.5 ${healthColors[healthStatus].split(' ')[0]}`} />
<div>
<p className={`font-medium ${healthColors[healthStatus].split(' ')[0]}`}>
Health Warnings
</p>
<ul className="mt-1 space-y-1 text-sm text-gray-300">
{metrics.marginLevel < 300 && (
<li> Margin level below 300%</li>
)}
{metrics.drawdown > 5 && (
<li> Drawdown exceeds 5%</li>
)}
{metrics.marginUsagePercent > 30 && (
<li> High margin usage ({metrics.marginUsagePercent.toFixed(0)}%)</li>
)}
{metrics.positionCount > 5 && (
<li> Multiple open positions</li>
)}
</ul>
</div>
</div>
</div>
)}
</div>
</div>
);
};
export default AccountHealthDashboard;

View File

@ -0,0 +1,472 @@
/**
* AdvancedOrderEntry Component
* Professional order placement form for MT4 live trading
*/
import React, { useState, useMemo } from 'react';
import {
TrendingUp,
TrendingDown,
Calculator,
Shield,
Target,
AlertTriangle,
Loader2,
Info,
Zap,
} from 'lucide-react';
import {
executeMLTrade,
calculatePositionSize,
type MT4Account,
} from '../../../services/trading.service';
type OrderType = 'market' | 'limit' | 'stop';
type OrderSide = 'buy' | 'sell';
interface AdvancedOrderEntryProps {
symbol: string;
currentPrice: number;
spread?: number;
account: MT4Account | null;
onOrderExecuted?: (ticket: number) => void;
onError?: (error: string) => void;
}
const AdvancedOrderEntry: React.FC<AdvancedOrderEntryProps> = ({
symbol,
currentPrice,
spread = 0,
account,
onOrderExecuted,
onError,
}) => {
const [orderType, setOrderType] = useState<OrderType>('market');
const [side, setSide] = useState<OrderSide>('buy');
const [volume, setVolume] = useState('0.01');
const [limitPrice, setLimitPrice] = useState('');
const [stopLossMode, setStopLossMode] = useState<'price' | 'pips' | 'percent'>('pips');
const [stopLoss, setStopLoss] = useState('');
const [takeProfitMode, setTakeProfitMode] = useState<'price' | 'pips' | 'rr'>('pips');
const [takeProfit, setTakeProfit] = useState('');
const [riskPercent, setRiskPercent] = useState('1');
const [useRiskCalculator, setUseRiskCalculator] = useState(false);
const [isSubmitting, setIsSubmitting] = useState(false);
const [showAdvanced, setShowAdvanced] = useState(false);
// Calculate pip value based on symbol
const pipValue = useMemo(() => {
return symbol.includes('JPY') ? 0.01 : 0.0001;
}, [symbol]);
// Calculate SL price from different modes
const calculateSLPrice = (): number | undefined => {
if (!stopLoss) return undefined;
const slValue = parseFloat(stopLoss);
if (isNaN(slValue)) return undefined;
switch (stopLossMode) {
case 'price':
return slValue;
case 'pips':
return side === 'buy'
? currentPrice - slValue * pipValue
: currentPrice + slValue * pipValue;
case 'percent':
const percentLoss = (slValue / 100) * currentPrice;
return side === 'buy'
? currentPrice - percentLoss
: currentPrice + percentLoss;
default:
return undefined;
}
};
// Calculate TP price from different modes
const calculateTPPrice = (): number | undefined => {
if (!takeProfit) return undefined;
const tpValue = parseFloat(takeProfit);
if (isNaN(tpValue)) return undefined;
switch (takeProfitMode) {
case 'price':
return tpValue;
case 'pips':
return side === 'buy'
? currentPrice + tpValue * pipValue
: currentPrice - tpValue * pipValue;
case 'rr':
const slPrice = calculateSLPrice();
if (!slPrice) return undefined;
const riskDistance = Math.abs(currentPrice - slPrice);
return side === 'buy'
? currentPrice + riskDistance * tpValue
: currentPrice - riskDistance * tpValue;
default:
return undefined;
}
};
// Calculate position size based on risk
const calculatedVolume = useMemo(() => {
if (!useRiskCalculator || !account || !stopLoss) return null;
const slPips = stopLossMode === 'pips' ? parseFloat(stopLoss) : null;
if (!slPips) return null;
const risk = parseFloat(riskPercent);
if (isNaN(risk)) return null;
// Simple calculation: risk amount / (sl_pips * pip_value_per_lot)
const riskAmount = (account.balance * risk) / 100;
const pipValuePerLot = symbol.includes('JPY') ? 1000 : 10; // Approximate
const lots = riskAmount / (slPips * pipValuePerLot);
return Math.max(0.01, Math.min(lots, 10)).toFixed(2);
}, [useRiskCalculator, account, stopLoss, stopLossMode, riskPercent, symbol]);
// Calculate estimated margin
const estimatedMargin = useMemo(() => {
if (!account) return null;
const lots = parseFloat(useRiskCalculator && calculatedVolume ? calculatedVolume : volume);
if (isNaN(lots)) return null;
// Approximate margin calculation (100:1 leverage)
const leverage = account.leverage || 100;
return (lots * 100000 * currentPrice) / leverage;
}, [account, volume, calculatedVolume, useRiskCalculator, currentPrice]);
// Risk/Reward ratio
const riskRewardRatio = useMemo(() => {
const slPrice = calculateSLPrice();
const tpPrice = calculateTPPrice();
if (!slPrice || !tpPrice) return null;
const risk = Math.abs(currentPrice - slPrice);
const reward = Math.abs(tpPrice - currentPrice);
if (risk === 0) return null;
return (reward / risk).toFixed(2);
}, [currentPrice, stopLoss, takeProfit, stopLossMode, takeProfitMode, side]);
const handleSubmit = async () => {
if (!account) {
onError?.('No account connected');
return;
}
setIsSubmitting(true);
try {
const finalVolume = useRiskCalculator && calculatedVolume ? calculatedVolume : volume;
const slPrice = calculateSLPrice();
const tpPrice = calculateTPPrice();
const result = await executeMLTrade({
symbol,
action: side.toUpperCase() as 'BUY' | 'SELL',
volume: parseFloat(finalVolume),
entry_price: orderType === 'market' ? currentPrice : parseFloat(limitPrice),
stop_loss: slPrice,
take_profit: tpPrice,
order_type: orderType,
});
if (result.ticket) {
onOrderExecuted?.(result.ticket);
// Reset form
setVolume('0.01');
setStopLoss('');
setTakeProfit('');
setLimitPrice('');
}
} catch (err) {
onError?.(err instanceof Error ? err.message : 'Order execution failed');
} finally {
setIsSubmitting(false);
}
};
const slPrice = calculateSLPrice();
const tpPrice = calculateTPPrice();
return (
<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>
<h3 className="font-medium text-white">Order Entry</h3>
<p className="text-sm text-gray-400">{symbol}</p>
</div>
<div className="text-right">
<p className="text-lg font-mono font-bold text-white">{currentPrice.toFixed(5)}</p>
{spread > 0 && (
<p className="text-xs text-gray-500">Spread: {spread.toFixed(1)} pips</p>
)}
</div>
</div>
<div className="p-4 space-y-4">
{/* Buy/Sell Buttons */}
<div className="grid grid-cols-2 gap-2">
<button
onClick={() => setSide('buy')}
className={`flex items-center justify-center gap-2 py-3 rounded-lg font-medium transition-colors ${
side === 'buy'
? 'bg-green-600 text-white'
: 'bg-gray-700 text-gray-300 hover:bg-gray-600'
}`}
>
<TrendingUp className="w-5 h-5" />
BUY
</button>
<button
onClick={() => setSide('sell')}
className={`flex items-center justify-center gap-2 py-3 rounded-lg font-medium transition-colors ${
side === 'sell'
? 'bg-red-600 text-white'
: 'bg-gray-700 text-gray-300 hover:bg-gray-600'
}`}
>
<TrendingDown className="w-5 h-5" />
SELL
</button>
</div>
{/* Order Type */}
<div>
<label className="block text-sm text-gray-400 mb-2">Order Type</label>
<div className="grid grid-cols-3 gap-2">
{(['market', 'limit', 'stop'] as OrderType[]).map((type) => (
<button
key={type}
onClick={() => setOrderType(type)}
className={`py-2 text-sm rounded-lg transition-colors ${
orderType === type
? 'bg-blue-600 text-white'
: 'bg-gray-700 text-gray-300 hover:bg-gray-600'
}`}
>
{type.charAt(0).toUpperCase() + type.slice(1)}
</button>
))}
</div>
</div>
{/* Limit/Stop Price */}
{orderType !== 'market' && (
<div>
<label className="block text-sm text-gray-400 mb-2">
{orderType === 'limit' ? 'Limit' : 'Stop'} Price
</label>
<input
type="number"
step="0.00001"
value={limitPrice}
onChange={(e) => setLimitPrice(e.target.value)}
placeholder={currentPrice.toFixed(5)}
className="w-full px-4 py-2 bg-gray-900 border border-gray-700 rounded-lg text-white font-mono focus:outline-none focus:border-blue-500"
/>
</div>
)}
{/* Volume / Risk Calculator Toggle */}
<div>
<div className="flex items-center justify-between mb-2">
<label className="text-sm text-gray-400">Volume (lots)</label>
<button
onClick={() => setUseRiskCalculator(!useRiskCalculator)}
className={`flex items-center gap-1 text-xs px-2 py-1 rounded transition-colors ${
useRiskCalculator
? 'bg-blue-600 text-white'
: 'bg-gray-700 text-gray-400 hover:text-white'
}`}
>
<Calculator className="w-3 h-3" />
Risk Calculator
</button>
</div>
{useRiskCalculator ? (
<div className="space-y-2">
<div className="flex items-center gap-2">
<input
type="number"
step="0.5"
min="0.1"
max="10"
value={riskPercent}
onChange={(e) => setRiskPercent(e.target.value)}
className="flex-1 px-4 py-2 bg-gray-900 border border-gray-700 rounded-lg text-white focus:outline-none focus:border-blue-500"
/>
<span className="text-gray-400">% Risk</span>
</div>
{calculatedVolume && (
<p className="text-sm text-green-400">
Calculated: {calculatedVolume} lots
</p>
)}
</div>
) : (
<input
type="number"
step="0.01"
min="0.01"
max="10"
value={volume}
onChange={(e) => setVolume(e.target.value)}
className="w-full px-4 py-2 bg-gray-900 border border-gray-700 rounded-lg text-white font-mono focus:outline-none focus:border-blue-500"
/>
)}
</div>
{/* Stop Loss */}
<div>
<div className="flex items-center justify-between mb-2">
<label className="flex items-center gap-2 text-sm text-gray-400">
<Shield className="w-4 h-4 text-red-400" />
Stop Loss
</label>
<div className="flex gap-1">
{(['pips', 'price', 'percent'] as const).map((mode) => (
<button
key={mode}
onClick={() => setStopLossMode(mode)}
className={`px-2 py-0.5 text-xs rounded ${
stopLossMode === mode
? 'bg-red-600 text-white'
: 'bg-gray-700 text-gray-400'
}`}
>
{mode}
</button>
))}
</div>
</div>
<div className="flex items-center gap-2">
<input
type="number"
step={stopLossMode === 'price' ? '0.00001' : '1'}
value={stopLoss}
onChange={(e) => setStopLoss(e.target.value)}
placeholder={stopLossMode === 'price' ? currentPrice.toFixed(5) : '20'}
className="flex-1 px-4 py-2 bg-gray-900 border border-gray-700 rounded-lg text-white font-mono focus:outline-none focus:border-red-500"
/>
{slPrice && (
<span className="text-sm text-red-400 font-mono">{slPrice.toFixed(5)}</span>
)}
</div>
</div>
{/* Take Profit */}
<div>
<div className="flex items-center justify-between mb-2">
<label className="flex items-center gap-2 text-sm text-gray-400">
<Target className="w-4 h-4 text-green-400" />
Take Profit
</label>
<div className="flex gap-1">
{(['pips', 'price', 'rr'] as const).map((mode) => (
<button
key={mode}
onClick={() => setTakeProfitMode(mode)}
className={`px-2 py-0.5 text-xs rounded ${
takeProfitMode === mode
? 'bg-green-600 text-white'
: 'bg-gray-700 text-gray-400'
}`}
>
{mode === 'rr' ? 'R:R' : mode}
</button>
))}
</div>
</div>
<div className="flex items-center gap-2">
<input
type="number"
step={takeProfitMode === 'price' ? '0.00001' : takeProfitMode === 'rr' ? '0.5' : '1'}
value={takeProfit}
onChange={(e) => setTakeProfit(e.target.value)}
placeholder={takeProfitMode === 'price' ? currentPrice.toFixed(5) : takeProfitMode === 'rr' ? '2' : '40'}
className="flex-1 px-4 py-2 bg-gray-900 border border-gray-700 rounded-lg text-white font-mono focus:outline-none focus:border-green-500"
/>
{tpPrice && (
<span className="text-sm text-green-400 font-mono">{tpPrice.toFixed(5)}</span>
)}
</div>
</div>
{/* Order Summary */}
<div className="p-3 bg-gray-900/50 rounded-lg space-y-2">
<div className="flex items-center justify-between text-sm">
<span className="text-gray-400">Risk/Reward</span>
<span className={`font-medium ${
riskRewardRatio && parseFloat(riskRewardRatio) >= 2
? 'text-green-400'
: riskRewardRatio && parseFloat(riskRewardRatio) >= 1
? 'text-yellow-400'
: 'text-red-400'
}`}>
{riskRewardRatio ? `1:${riskRewardRatio}` : '-'}
</span>
</div>
{estimatedMargin && (
<div className="flex items-center justify-between text-sm">
<span className="text-gray-400">Est. Margin</span>
<span className="text-white">${estimatedMargin.toFixed(2)}</span>
</div>
)}
{account && estimatedMargin && (
<div className="flex items-center justify-between text-sm">
<span className="text-gray-400">Free Margin After</span>
<span className={`${
account.free_margin - estimatedMargin > 0 ? 'text-green-400' : 'text-red-400'
}`}>
${(account.free_margin - estimatedMargin).toFixed(2)}
</span>
</div>
)}
</div>
{/* Warning */}
{(!slPrice || !tpPrice) && (
<div className="flex items-start gap-2 p-3 bg-yellow-500/10 border border-yellow-500/30 rounded-lg">
<AlertTriangle className="w-4 h-4 text-yellow-400 flex-shrink-0 mt-0.5" />
<p className="text-sm text-yellow-400">
{!slPrice && !tpPrice
? 'No SL or TP set. Consider adding risk management.'
: !slPrice
? 'No Stop Loss set. Position has unlimited risk.'
: 'No Take Profit set.'}
</p>
</div>
)}
{/* Submit Button */}
<button
onClick={handleSubmit}
disabled={isSubmitting || !account}
className={`w-full flex items-center justify-center gap-2 py-3 rounded-lg font-medium transition-colors disabled:opacity-50 ${
side === 'buy'
? 'bg-green-600 hover:bg-green-500 text-white'
: 'bg-red-600 hover:bg-red-500 text-white'
}`}
>
{isSubmitting ? (
<>
<Loader2 className="w-5 h-5 animate-spin" />
Executing...
</>
) : (
<>
<Zap className="w-5 h-5" />
{side === 'buy' ? 'Place Buy Order' : 'Place Sell Order'}
</>
)}
</button>
{!account && (
<p className="text-center text-sm text-gray-500">
Connect MT4 account to place orders
</p>
)}
</div>
</div>
);
};
export default AdvancedOrderEntry;

View File

@ -0,0 +1,379 @@
/**
* MT4PositionsManager Component
* Container for managing all live MT4 positions with real-time updates
*/
import React, { useState, useEffect, useCallback } from 'react';
import {
RefreshCw,
X,
Filter,
SortAsc,
SortDesc,
TrendingUp,
TrendingDown,
AlertTriangle,
Loader2,
LayoutList,
LayoutGrid,
} from 'lucide-react';
import LivePositionCard from './LivePositionCard';
import {
getMT4Positions,
closeMT4Position,
modifyMT4Position,
type MT4Position,
} from '../../../services/trading.service';
type SortField = 'profit' | 'volume' | 'symbol' | 'openTime';
type SortDirection = 'asc' | 'desc';
type FilterType = 'all' | 'buy' | 'sell' | 'profit' | 'loss';
interface MT4PositionsManagerProps {
autoRefresh?: boolean;
refreshInterval?: number;
onPositionClose?: (ticket: number) => void;
onPositionModify?: (ticket: number) => void;
compact?: boolean;
}
const MT4PositionsManager: React.FC<MT4PositionsManagerProps> = ({
autoRefresh = true,
refreshInterval = 30000,
onPositionClose,
onPositionModify,
compact = false,
}) => {
const [positions, setPositions] = useState<MT4Position[]>([]);
const [loading, setLoading] = useState(true);
const [refreshing, setRefreshing] = useState(false);
const [error, setError] = useState<string | null>(null);
const [sortField, setSortField] = useState<SortField>('openTime');
const [sortDirection, setSortDirection] = useState<SortDirection>('desc');
const [filter, setFilter] = useState<FilterType>('all');
const [showCloseAllConfirm, setShowCloseAllConfirm] = useState(false);
const [closingAll, setClosingAll] = useState(false);
const [viewMode, setViewMode] = useState<'list' | 'grid'>('list');
const fetchPositions = useCallback(async (isRefresh = false) => {
if (isRefresh) setRefreshing(true);
else setLoading(true);
try {
const data = await getMT4Positions();
setPositions(data || []);
setError(null);
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to load positions');
} finally {
setLoading(false);
setRefreshing(false);
}
}, []);
useEffect(() => {
fetchPositions();
if (autoRefresh) {
const interval = setInterval(() => fetchPositions(true), refreshInterval);
return () => clearInterval(interval);
}
}, [fetchPositions, autoRefresh, refreshInterval]);
const handleClosePosition = async (ticket: number) => {
try {
await closeMT4Position(ticket);
setPositions((prev) => prev.filter((p) => p.ticket !== ticket));
onPositionClose?.(ticket);
} catch (err) {
console.error('Failed to close position:', err);
}
};
const handleModifyPosition = async (
ticket: number,
stopLoss?: number,
takeProfit?: number
) => {
try {
await modifyMT4Position(ticket, stopLoss, takeProfit);
setPositions((prev) =>
prev.map((p) =>
p.ticket === ticket
? { ...p, stop_loss: stopLoss || p.stop_loss, take_profit: takeProfit || p.take_profit }
: p
)
);
onPositionModify?.(ticket);
} catch (err) {
console.error('Failed to modify position:', err);
}
};
const handleCloseAll = async () => {
setClosingAll(true);
try {
await Promise.all(positions.map((p) => closeMT4Position(p.ticket)));
setPositions([]);
setShowCloseAllConfirm(false);
} catch (err) {
console.error('Failed to close all positions:', err);
} finally {
setClosingAll(false);
}
};
// Filter positions
const filteredPositions = positions.filter((p) => {
switch (filter) {
case 'buy':
return p.type === 'buy';
case 'sell':
return p.type === 'sell';
case 'profit':
return p.profit >= 0;
case 'loss':
return p.profit < 0;
default:
return true;
}
});
// Sort positions
const sortedPositions = [...filteredPositions].sort((a, b) => {
let comparison = 0;
switch (sortField) {
case 'profit':
comparison = a.profit - b.profit;
break;
case 'volume':
comparison = a.volume - b.volume;
break;
case 'symbol':
comparison = a.symbol.localeCompare(b.symbol);
break;
case 'openTime':
comparison = new Date(a.open_time).getTime() - new Date(b.open_time).getTime();
break;
}
return sortDirection === 'asc' ? comparison : -comparison;
});
// Calculate totals
const totalProfit = positions.reduce((sum, p) => sum + p.profit, 0);
const totalLots = positions.reduce((sum, p) => sum + p.volume, 0);
const buyCount = positions.filter((p) => p.type === 'buy').length;
const sellCount = positions.filter((p) => p.type === 'sell').length;
const toggleSort = (field: SortField) => {
if (sortField === field) {
setSortDirection((prev) => (prev === 'asc' ? 'desc' : 'asc'));
} else {
setSortField(field);
setSortDirection('desc');
}
};
if (loading) {
return (
<div className="bg-gray-800 rounded-xl border border-gray-700 p-8 flex items-center justify-center">
<Loader2 className="w-6 h-6 text-blue-400 animate-spin" />
</div>
);
}
if (error) {
return (
<div className="bg-gray-800 rounded-xl border border-gray-700 p-6 text-center">
<AlertTriangle className="w-10 h-10 text-red-400 mx-auto mb-3" />
<h4 className="font-medium text-white mb-1">Failed to Load Positions</h4>
<p className="text-sm text-gray-400 mb-4">{error}</p>
<button
onClick={() => fetchPositions()}
className="px-4 py-2 bg-blue-600 hover:bg-blue-500 text-white rounded-lg text-sm transition-colors"
>
Retry
</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">
<h3 className="font-medium text-white">Open Positions</h3>
<span className="px-2 py-0.5 bg-gray-700 rounded text-sm text-gray-300">
{positions.length}
</span>
</div>
<div className="flex items-center gap-2">
{/* View Mode Toggle */}
{!compact && (
<div className="flex items-center bg-gray-700 rounded-lg p-0.5">
<button
onClick={() => setViewMode('list')}
className={`p-1.5 rounded ${
viewMode === 'list' ? 'bg-gray-600 text-white' : 'text-gray-400'
}`}
>
<LayoutList className="w-4 h-4" />
</button>
<button
onClick={() => setViewMode('grid')}
className={`p-1.5 rounded ${
viewMode === 'grid' ? 'bg-gray-600 text-white' : 'text-gray-400'
}`}
>
<LayoutGrid className="w-4 h-4" />
</button>
</div>
)}
<button
onClick={() => fetchPositions(true)}
disabled={refreshing}
className="p-2 text-gray-400 hover:text-white hover:bg-gray-700 rounded-lg transition-colors disabled:opacity-50"
>
<RefreshCw className={`w-4 h-4 ${refreshing ? 'animate-spin' : ''}`} />
</button>
</div>
</div>
{/* Summary Bar */}
<div className="flex items-center justify-between px-4 py-3 bg-gray-900/50 border-b border-gray-700">
<div className="flex items-center gap-6 text-sm">
<div className="flex items-center gap-2">
<TrendingUp className="w-4 h-4 text-green-400" />
<span className="text-gray-400">Buy:</span>
<span className="text-white font-medium">{buyCount}</span>
</div>
<div className="flex items-center gap-2">
<TrendingDown className="w-4 h-4 text-red-400" />
<span className="text-gray-400">Sell:</span>
<span className="text-white font-medium">{sellCount}</span>
</div>
<div className="flex items-center gap-2">
<span className="text-gray-400">Lots:</span>
<span className="text-white font-medium">{totalLots.toFixed(2)}</span>
</div>
</div>
<div className={`text-lg font-bold ${totalProfit >= 0 ? 'text-green-400' : 'text-red-400'}`}>
{totalProfit >= 0 ? '+' : ''}{totalProfit.toFixed(2)} USD
</div>
</div>
{/* Filters & Sort */}
<div className="flex items-center justify-between px-4 py-2 border-b border-gray-700">
<div className="flex items-center gap-2">
<Filter className="w-4 h-4 text-gray-500" />
{(['all', 'buy', 'sell', 'profit', 'loss'] as FilterType[]).map((f) => (
<button
key={f}
onClick={() => setFilter(f)}
className={`px-2 py-1 text-xs rounded transition-colors ${
filter === f
? 'bg-blue-600 text-white'
: 'bg-gray-700 text-gray-400 hover:text-white'
}`}
>
{f.charAt(0).toUpperCase() + f.slice(1)}
</button>
))}
</div>
<div className="flex items-center gap-2">
<span className="text-xs text-gray-500">Sort:</span>
{(['profit', 'volume', 'symbol'] as SortField[]).map((field) => (
<button
key={field}
onClick={() => toggleSort(field)}
className={`flex items-center gap-1 px-2 py-1 text-xs rounded transition-colors ${
sortField === field
? 'bg-blue-600 text-white'
: 'bg-gray-700 text-gray-400 hover:text-white'
}`}
>
{field.charAt(0).toUpperCase() + field.slice(1)}
{sortField === field &&
(sortDirection === 'asc' ? (
<SortAsc className="w-3 h-3" />
) : (
<SortDesc className="w-3 h-3" />
))}
</button>
))}
</div>
</div>
{/* Positions List */}
<div className={`p-4 ${viewMode === 'grid' ? 'grid grid-cols-2 gap-4' : 'space-y-3'}`}>
{sortedPositions.length === 0 ? (
<div className="text-center py-8 col-span-2">
<LayoutList className="w-10 h-10 text-gray-600 mx-auto mb-3" />
<p className="text-gray-400">No open positions</p>
{filter !== 'all' && (
<button
onClick={() => setFilter('all')}
className="mt-2 text-sm text-blue-400 hover:text-blue-300"
>
Clear filter
</button>
)}
</div>
) : (
sortedPositions.map((position) => (
<LivePositionCard
key={position.ticket}
position={position}
onClose={handleClosePosition}
onModify={handleModifyPosition}
/>
))
)}
</div>
{/* Close All Footer */}
{positions.length > 0 && (
<div className="px-4 py-3 border-t border-gray-700 bg-gray-900/50">
{showCloseAllConfirm ? (
<div className="flex items-center justify-between">
<span className="text-sm text-yellow-400">
Close all {positions.length} positions?
</span>
<div className="flex items-center gap-2">
<button
onClick={() => setShowCloseAllConfirm(false)}
className="px-3 py-1.5 text-sm text-gray-300 hover:text-white transition-colors"
>
Cancel
</button>
<button
onClick={handleCloseAll}
disabled={closingAll}
className="flex items-center gap-2 px-3 py-1.5 bg-red-600 hover:bg-red-500 text-white rounded-lg text-sm transition-colors disabled:opacity-50"
>
{closingAll ? (
<Loader2 className="w-4 h-4 animate-spin" />
) : (
<X className="w-4 h-4" />
)}
Confirm Close All
</button>
</div>
</div>
) : (
<button
onClick={() => setShowCloseAllConfirm(true)}
className="w-full flex items-center justify-center gap-2 py-2 bg-red-600/20 hover:bg-red-600/30 text-red-400 rounded-lg text-sm transition-colors border border-red-600/30"
>
<X className="w-4 h-4" />
Close All Positions
</button>
)}
</div>
)}
</div>
);
};
export default MT4PositionsManager;

View File

@ -37,3 +37,6 @@ export { default as ExportButton } from './ExportButton';
export { default as MT4ConnectionStatus } from './MT4ConnectionStatus';
export { default as LivePositionCard } from './LivePositionCard';
export { default as RiskMonitor } from './RiskMonitor';
export { default as MT4PositionsManager } from './MT4PositionsManager';
export { default as AdvancedOrderEntry } from './AdvancedOrderEntry';
export { default as AccountHealthDashboard } from './AccountHealthDashboard';