[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:
parent
bfaf76ccf8
commit
bf726a595c
391
src/modules/trading/components/AccountHealthDashboard.tsx
Normal file
391
src/modules/trading/components/AccountHealthDashboard.tsx
Normal 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;
|
||||||
472
src/modules/trading/components/AdvancedOrderEntry.tsx
Normal file
472
src/modules/trading/components/AdvancedOrderEntry.tsx
Normal 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;
|
||||||
379
src/modules/trading/components/MT4PositionsManager.tsx
Normal file
379
src/modules/trading/components/MT4PositionsManager.tsx
Normal 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;
|
||||||
@ -37,3 +37,6 @@ export { default as ExportButton } from './ExportButton';
|
|||||||
export { default as MT4ConnectionStatus } from './MT4ConnectionStatus';
|
export { default as MT4ConnectionStatus } from './MT4ConnectionStatus';
|
||||||
export { default as LivePositionCard } from './LivePositionCard';
|
export { default as LivePositionCard } from './LivePositionCard';
|
||||||
export { default as RiskMonitor } from './RiskMonitor';
|
export { default as RiskMonitor } from './RiskMonitor';
|
||||||
|
export { default as MT4PositionsManager } from './MT4PositionsManager';
|
||||||
|
export { default as AdvancedOrderEntry } from './AdvancedOrderEntry';
|
||||||
|
export { default as AccountHealthDashboard } from './AccountHealthDashboard';
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user