[OQI-009] feat: Add MT4 trading dashboard and risk management components

- MT4LiveTradesPanel: Real-time positions dashboard with aggregate metrics
- PositionModifierDialog: Modal for modifying SL/TP with price/pips modes
- RiskBasedPositionSizer: Position size calculator based on risk percentage
- TradeAlertsNotificationCenter: Unified notification hub for MT4 events

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Adrian Flores Cortes 2026-01-25 11:50:11 -06:00
parent c7626f841c
commit 51c0a846c0
5 changed files with 1544 additions and 0 deletions

View File

@ -0,0 +1,389 @@
/**
* MT4LiveTradesPanel Component
* Real-time positions dashboard with live P&L updates via WebSocket
*/
import React, { useState, useMemo } from 'react';
import {
TrendingUp,
TrendingDown,
X,
Edit3,
RefreshCw,
AlertTriangle,
DollarSign,
Percent,
Activity,
ChevronDown,
ChevronUp,
Filter,
MoreVertical,
} from 'lucide-react';
import { useMT4WebSocket, MT4Position } from '../hooks/useMT4WebSocket';
import { closeMT4Position } from '../../../services/trading.service';
interface MT4LiveTradesPanelProps {
onModifyPosition?: (position: MT4Position) => void;
onClosePosition?: (ticket: number) => void;
onPositionSelect?: (position: MT4Position) => void;
autoRefresh?: boolean;
compact?: boolean;
}
type SortField = 'profit' | 'openTime' | 'symbol' | 'lots';
type SortDirection = 'asc' | 'desc';
type FilterType = 'all' | 'profitable' | 'losing';
const MT4LiveTradesPanel: React.FC<MT4LiveTradesPanelProps> = ({
onModifyPosition,
onClosePosition,
onPositionSelect,
autoRefresh = true,
compact = false,
}) => {
const { connected, positions, account, error } = useMT4WebSocket({
autoConnect: autoRefresh,
});
const [sortField, setSortField] = useState<SortField>('openTime');
const [sortDirection, setSortDirection] = useState<SortDirection>('desc');
const [filterType, setFilterType] = useState<FilterType>('all');
const [expandedTicket, setExpandedTicket] = useState<number | null>(null);
const [closingTicket, setClosingTicket] = useState<number | null>(null);
const [showCloseAllConfirm, setShowCloseAllConfirm] = useState(false);
// Calculate aggregate metrics
const metrics = useMemo(() => {
const totalProfit = positions.reduce((sum, p) => sum + p.profit, 0);
const totalSwap = positions.reduce((sum, p) => sum + p.swap, 0);
const totalCommission = positions.reduce((sum, p) => sum + p.commission, 0);
const winningCount = positions.filter((p) => p.profit > 0).length;
const losingCount = positions.filter((p) => p.profit < 0).length;
const totalLots = positions.reduce((sum, p) => sum + p.lots, 0);
return {
totalProfit,
totalSwap,
totalCommission,
netProfit: totalProfit + totalSwap - totalCommission,
winningCount,
losingCount,
totalLots,
marginLevel: account?.marginLevel || 0,
};
}, [positions, account]);
// Filter and sort positions
const filteredPositions = useMemo(() => {
let result = [...positions];
// Filter
if (filterType === 'profitable') {
result = result.filter((p) => p.profit > 0);
} else if (filterType === 'losing') {
result = result.filter((p) => p.profit < 0);
}
// Sort
result.sort((a, b) => {
let comparison = 0;
switch (sortField) {
case 'profit':
comparison = a.profit - b.profit;
break;
case 'openTime':
comparison = new Date(a.openTime).getTime() - new Date(b.openTime).getTime();
break;
case 'symbol':
comparison = a.symbol.localeCompare(b.symbol);
break;
case 'lots':
comparison = a.lots - b.lots;
break;
}
return sortDirection === 'asc' ? comparison : -comparison;
});
return result;
}, [positions, filterType, sortField, sortDirection]);
const handleSort = (field: SortField) => {
if (sortField === field) {
setSortDirection(sortDirection === 'asc' ? 'desc' : 'asc');
} else {
setSortField(field);
setSortDirection('desc');
}
};
const handleClosePosition = async (ticket: number) => {
setClosingTicket(ticket);
try {
await closeMT4Position(ticket);
onClosePosition?.(ticket);
} catch (err) {
console.error('Failed to close position:', err);
} finally {
setClosingTicket(null);
}
};
const handleCloseAll = async () => {
setShowCloseAllConfirm(false);
for (const position of positions) {
await handleClosePosition(position.ticket);
}
};
const formatTime = (dateString: string) => {
const date = new Date(dateString);
return date.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' });
};
const getMarginLevelColor = (level: number) => {
if (level >= 200) return 'text-green-400';
if (level >= 100) return 'text-yellow-400';
return 'text-red-400';
};
const getProfitColor = (profit: number) => {
if (profit > 0) return 'text-green-400';
if (profit < 0) return 'text-red-400';
return 'text-gray-400';
};
return (
<div className={`bg-gray-800/50 rounded-xl border border-gray-700 ${compact ? 'p-3' : 'p-4'}`}>
{/* Header */}
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-3">
<div className={`p-2 rounded-lg ${connected ? 'bg-green-500/20' : 'bg-red-500/20'}`}>
<Activity className={`w-5 h-5 ${connected ? 'text-green-400' : 'text-red-400'}`} />
</div>
<div>
<h3 className="font-semibold text-white">Live Positions</h3>
<p className="text-xs text-gray-500">
{positions.length} open {connected ? 'Connected' : 'Disconnected'}
</p>
</div>
</div>
<div className="flex items-center gap-2">
{/* Filter */}
<select
value={filterType}
onChange={(e) => setFilterType(e.target.value as FilterType)}
className="px-2 py-1 bg-gray-900 border border-gray-700 rounded text-xs text-white"
>
<option value="all">All</option>
<option value="profitable">Winning</option>
<option value="losing">Losing</option>
</select>
{/* Close All Button */}
{positions.length > 0 && (
<button
onClick={() => setShowCloseAllConfirm(true)}
className="px-3 py-1 bg-red-600/20 hover:bg-red-600/30 text-red-400 rounded text-xs font-medium transition-colors"
>
Close All
</button>
)}
</div>
</div>
{/* Summary Metrics */}
<div className="grid grid-cols-4 gap-3 mb-4">
<div className="p-3 bg-gray-900/50 rounded-lg text-center">
<div className={`text-lg font-bold ${getProfitColor(metrics.netProfit)}`}>
{metrics.netProfit >= 0 ? '+' : ''}{metrics.netProfit.toFixed(2)}
</div>
<div className="text-xs text-gray-500">Net P&L</div>
</div>
<div className="p-3 bg-gray-900/50 rounded-lg text-center">
<div className="text-lg font-bold text-white">
<span className="text-green-400">{metrics.winningCount}</span>
<span className="text-gray-600">/</span>
<span className="text-red-400">{metrics.losingCount}</span>
</div>
<div className="text-xs text-gray-500">Win/Lose</div>
</div>
<div className="p-3 bg-gray-900/50 rounded-lg text-center">
<div className="text-lg font-bold text-white">{metrics.totalLots.toFixed(2)}</div>
<div className="text-xs text-gray-500">Total Lots</div>
</div>
<div className="p-3 bg-gray-900/50 rounded-lg text-center">
<div className={`text-lg font-bold ${getMarginLevelColor(metrics.marginLevel)}`}>
{metrics.marginLevel.toFixed(0)}%
</div>
<div className="text-xs text-gray-500">Margin Level</div>
</div>
</div>
{/* Positions List */}
{error ? (
<div className="text-center py-8">
<AlertTriangle className="w-10 h-10 text-red-400 mx-auto mb-2" />
<p className="text-gray-400">{error}</p>
</div>
) : filteredPositions.length === 0 ? (
<div className="text-center py-8">
<Activity className="w-10 h-10 text-gray-600 mx-auto mb-2" />
<p className="text-gray-400">No open positions</p>
</div>
) : (
<div className="space-y-2 max-h-[400px] overflow-y-auto">
{filteredPositions.map((position) => (
<div
key={position.ticket}
className={`bg-gray-900/50 rounded-lg border transition-all ${
expandedTicket === position.ticket
? 'border-blue-500/50'
: 'border-gray-700/50 hover:border-gray-600'
}`}
>
{/* Main Row */}
<div
onClick={() => {
setExpandedTicket(expandedTicket === position.ticket ? null : position.ticket);
onPositionSelect?.(position);
}}
className="flex items-center justify-between p-3 cursor-pointer"
>
<div className="flex items-center gap-3">
<div className={`p-1.5 rounded ${position.type === 'BUY' ? 'bg-green-500/20' : 'bg-red-500/20'}`}>
{position.type === 'BUY' ? (
<TrendingUp className="w-4 h-4 text-green-400" />
) : (
<TrendingDown className="w-4 h-4 text-red-400" />
)}
</div>
<div>
<div className="flex items-center gap-2">
<span className="font-medium text-white">{position.symbol}</span>
<span className="text-xs text-gray-500">{position.lots} lots</span>
</div>
<div className="text-xs text-gray-500">
#{position.ticket} {formatTime(position.openTime)}
</div>
</div>
</div>
<div className="flex items-center gap-3">
<div className="text-right">
<div className={`font-mono font-medium ${getProfitColor(position.profit)}`}>
{position.profit >= 0 ? '+' : ''}{position.profit.toFixed(2)}
</div>
<div className="text-xs text-gray-500">
{position.openPrice.toFixed(5)} {position.currentPrice.toFixed(5)}
</div>
</div>
{expandedTicket === position.ticket ? (
<ChevronUp className="w-4 h-4 text-gray-500" />
) : (
<ChevronDown className="w-4 h-4 text-gray-500" />
)}
</div>
</div>
{/* Expanded Details */}
{expandedTicket === position.ticket && (
<div className="px-3 pb-3 pt-1 border-t border-gray-700/50">
<div className="grid grid-cols-4 gap-3 text-xs mb-3">
<div>
<span className="text-gray-500">Entry:</span>
<span className="text-white ml-1 font-mono">{position.openPrice.toFixed(5)}</span>
</div>
<div>
<span className="text-gray-500">Current:</span>
<span className="text-white ml-1 font-mono">{position.currentPrice.toFixed(5)}</span>
</div>
<div>
<span className="text-gray-500">SL:</span>
<span className={`ml-1 font-mono ${position.stopLoss ? 'text-red-400' : 'text-gray-600'}`}>
{position.stopLoss?.toFixed(5) || 'None'}
</span>
</div>
<div>
<span className="text-gray-500">TP:</span>
<span className={`ml-1 font-mono ${position.takeProfit ? 'text-green-400' : 'text-gray-600'}`}>
{position.takeProfit?.toFixed(5) || 'None'}
</span>
</div>
</div>
<div className="flex items-center justify-between">
<div className="text-xs text-gray-500">
Swap: <span className="text-gray-400">{position.swap.toFixed(2)}</span>
{' • '}
Comm: <span className="text-gray-400">{position.commission.toFixed(2)}</span>
</div>
<div className="flex items-center gap-2">
{onModifyPosition && (
<button
onClick={(e) => {
e.stopPropagation();
onModifyPosition(position);
}}
className="flex items-center gap-1 px-2 py-1 text-xs text-blue-400 hover:bg-blue-500/10 rounded transition-colors"
>
<Edit3 className="w-3 h-3" />
Modify
</button>
)}
<button
onClick={(e) => {
e.stopPropagation();
handleClosePosition(position.ticket);
}}
disabled={closingTicket === position.ticket}
className="flex items-center gap-1 px-2 py-1 text-xs text-red-400 hover:bg-red-500/10 rounded transition-colors disabled:opacity-50"
>
<X className="w-3 h-3" />
{closingTicket === position.ticket ? 'Closing...' : 'Close'}
</button>
</div>
</div>
</div>
)}
</div>
))}
</div>
)}
{/* Close All Confirmation Modal */}
{showCloseAllConfirm && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/70">
<div className="w-full max-w-sm bg-gray-800 rounded-xl p-6">
<div className="text-center">
<AlertTriangle className="w-12 h-12 text-red-400 mx-auto mb-4" />
<h3 className="text-lg font-semibold text-white mb-2">Close All Positions?</h3>
<p className="text-gray-400 text-sm mb-4">
This will close {positions.length} positions with a net P&L of{' '}
<span className={getProfitColor(metrics.netProfit)}>
{metrics.netProfit >= 0 ? '+' : ''}{metrics.netProfit.toFixed(2)}
</span>
</p>
<div className="flex items-center gap-3">
<button
onClick={() => setShowCloseAllConfirm(false)}
className="flex-1 py-2.5 bg-gray-700 hover:bg-gray-600 text-white rounded-lg font-medium"
>
Cancel
</button>
<button
onClick={handleCloseAll}
className="flex-1 py-2.5 bg-red-600 hover:bg-red-500 text-white rounded-lg font-medium"
>
Close All
</button>
</div>
</div>
</div>
</div>
)}
</div>
);
};
export default MT4LiveTradesPanel;

View File

@ -0,0 +1,365 @@
/**
* PositionModifierDialog Component
* Modal for modifying SL/TP on open positions with preview
*/
import React, { useState, useEffect, useMemo } from 'react';
import {
X,
TrendingUp,
TrendingDown,
Target,
Shield,
Calculator,
Loader2,
AlertCircle,
Check,
RefreshCw,
} from 'lucide-react';
import { modifyMT4Position } from '../../../services/trading.service';
import { MT4Position } from '../hooks/useMT4WebSocket';
interface PositionModifierDialogProps {
position: MT4Position;
onClose: () => void;
onSuccess?: (ticket: number) => void;
}
type InputMode = 'price' | 'pips' | 'percent';
const PositionModifierDialog: React.FC<PositionModifierDialogProps> = ({
position,
onClose,
onSuccess,
}) => {
const [slPrice, setSlPrice] = useState<string>(position.stopLoss?.toString() || '');
const [tpPrice, setTpPrice] = useState<string>(position.takeProfit?.toString() || '');
const [inputMode, setInputMode] = useState<InputMode>('price');
const [slPips, setSlPips] = useState<string>('');
const [tpPips, setTpPips] = useState<string>('');
const [isSubmitting, setIsSubmitting] = useState(false);
const [error, setError] = useState<string | null>(null);
const [success, setSuccess] = useState(false);
const isBuy = position.type === 'BUY';
const pipValue = position.symbol.includes('JPY') ? 0.01 : 0.0001;
// Calculate SL/TP from pips
useEffect(() => {
if (inputMode === 'pips') {
if (slPips) {
const pips = parseFloat(slPips);
const price = isBuy
? position.currentPrice - pips * pipValue
: position.currentPrice + pips * pipValue;
setSlPrice(price.toFixed(5));
}
if (tpPips) {
const pips = parseFloat(tpPips);
const price = isBuy
? position.currentPrice + pips * pipValue
: position.currentPrice - pips * pipValue;
setTpPrice(price.toFixed(5));
}
}
}, [slPips, tpPips, inputMode, position.currentPrice, isBuy, pipValue]);
// Calculate potential loss/profit
const calculations = useMemo(() => {
const sl = parseFloat(slPrice) || 0;
const tp = parseFloat(tpPrice) || 0;
const current = position.currentPrice;
const entry = position.openPrice;
// Pips calculation
const slPipsValue = sl ? Math.abs(current - sl) / pipValue : 0;
const tpPipsValue = tp ? Math.abs(tp - current) / pipValue : 0;
// Approximate P&L (simplified - assumes $10/pip per lot for majors)
const pipValueUsd = 10 * position.lots;
const maxLoss = sl ? slPipsValue * pipValueUsd : null;
const maxProfit = tp ? tpPipsValue * pipValueUsd : null;
// Risk/Reward ratio
const riskReward = maxLoss && maxProfit ? maxProfit / maxLoss : null;
// Breakeven price
const breakeven = entry;
return {
slPips: slPipsValue,
tpPips: tpPipsValue,
maxLoss,
maxProfit,
riskReward,
breakeven,
};
}, [slPrice, tpPrice, position, pipValue]);
const handleSetBreakeven = () => {
setSlPrice(position.openPrice.toFixed(5));
setInputMode('price');
};
const handleSetTrailingPreset = (pips: number) => {
const price = isBuy
? position.currentPrice - pips * pipValue
: position.currentPrice + pips * pipValue;
setSlPrice(price.toFixed(5));
setInputMode('price');
};
const handleSubmit = async () => {
setIsSubmitting(true);
setError(null);
try {
await modifyMT4Position(position.ticket, {
stopLoss: slPrice ? parseFloat(slPrice) : undefined,
takeProfit: tpPrice ? parseFloat(tpPrice) : undefined,
});
setSuccess(true);
onSuccess?.(position.ticket);
// Close after brief success display
setTimeout(() => onClose(), 1500);
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to modify position');
} finally {
setIsSubmitting(false);
}
};
const handleRemoveSL = () => setSlPrice('');
const handleRemoveTP = () => setTpPrice('');
return (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/70">
<div className="w-full max-w-md bg-gray-800 rounded-xl shadow-2xl">
{/* 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 ${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>
<h2 className="font-semibold text-white">Modify Position</h2>
<p className="text-sm text-gray-400">
{position.symbol} #{position.ticket}
</p>
</div>
</div>
<button
onClick={onClose}
className="p-2 text-gray-400 hover:text-white hover:bg-gray-700 rounded-lg"
>
<X className="w-5 h-5" />
</button>
</div>
{/* Content */}
<div className="p-4 space-y-4">
{/* Position Info */}
<div className="grid grid-cols-3 gap-3 p-3 bg-gray-900/50 rounded-lg">
<div className="text-center">
<div className="text-xs text-gray-500">Entry</div>
<div className="font-mono text-white">{position.openPrice.toFixed(5)}</div>
</div>
<div className="text-center">
<div className="text-xs text-gray-500">Current</div>
<div className="font-mono text-white">{position.currentPrice.toFixed(5)}</div>
</div>
<div className="text-center">
<div className="text-xs text-gray-500">P&L</div>
<div className={`font-mono ${position.profit >= 0 ? 'text-green-400' : 'text-red-400'}`}>
{position.profit >= 0 ? '+' : ''}{position.profit.toFixed(2)}
</div>
</div>
</div>
{/* Input Mode Toggle */}
<div className="flex items-center gap-2">
<span className="text-xs text-gray-500">Input:</span>
{(['price', 'pips'] as InputMode[]).map((mode) => (
<button
key={mode}
onClick={() => setInputMode(mode)}
className={`px-3 py-1 text-xs rounded ${
inputMode === mode
? 'bg-blue-600 text-white'
: 'bg-gray-700 text-gray-300 hover:bg-gray-600'
}`}
>
{mode.charAt(0).toUpperCase() + mode.slice(1)}
</button>
))}
</div>
{/* Stop Loss */}
<div>
<div className="flex items-center justify-between mb-2">
<label className="flex items-center gap-2 text-sm text-gray-300">
<Shield className="w-4 h-4 text-red-400" />
Stop Loss
</label>
{slPrice && (
<button
onClick={handleRemoveSL}
className="text-xs text-red-400 hover:text-red-300"
>
Remove
</button>
)}
</div>
{inputMode === 'price' ? (
<input
type="number"
value={slPrice}
onChange={(e) => setSlPrice(e.target.value)}
step="0.00001"
placeholder="Stop Loss Price"
className="w-full px-4 py-2.5 bg-gray-900 border border-gray-700 rounded-lg text-white font-mono focus:outline-none focus:border-red-500"
/>
) : (
<input
type="number"
value={slPips}
onChange={(e) => setSlPips(e.target.value)}
placeholder="Pips from current price"
className="w-full px-4 py-2.5 bg-gray-900 border border-gray-700 rounded-lg text-white focus:outline-none focus:border-red-500"
/>
)}
{slPrice && (
<div className="mt-1 text-xs text-gray-500">
{calculations.slPips.toFixed(1)} pips Max loss: ${calculations.maxLoss?.toFixed(2) || '—'}
</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-300">
<Target className="w-4 h-4 text-green-400" />
Take Profit
</label>
{tpPrice && (
<button
onClick={handleRemoveTP}
className="text-xs text-green-400 hover:text-green-300"
>
Remove
</button>
)}
</div>
{inputMode === 'price' ? (
<input
type="number"
value={tpPrice}
onChange={(e) => setTpPrice(e.target.value)}
step="0.00001"
placeholder="Take Profit Price"
className="w-full px-4 py-2.5 bg-gray-900 border border-gray-700 rounded-lg text-white font-mono focus:outline-none focus:border-green-500"
/>
) : (
<input
type="number"
value={tpPips}
onChange={(e) => setTpPips(e.target.value)}
placeholder="Pips from current price"
className="w-full px-4 py-2.5 bg-gray-900 border border-gray-700 rounded-lg text-white focus:outline-none focus:border-green-500"
/>
)}
{tpPrice && (
<div className="mt-1 text-xs text-gray-500">
{calculations.tpPips.toFixed(1)} pips Max profit: ${calculations.maxProfit?.toFixed(2) || '—'}
</div>
)}
</div>
{/* Quick Actions */}
<div className="flex flex-wrap gap-2">
<button
onClick={handleSetBreakeven}
className="flex items-center gap-1 px-3 py-1.5 bg-gray-700 hover:bg-gray-600 text-gray-300 rounded text-xs"
>
<RefreshCw className="w-3 h-3" />
Breakeven
</button>
<button
onClick={() => handleSetTrailingPreset(20)}
className="px-3 py-1.5 bg-gray-700 hover:bg-gray-600 text-gray-300 rounded text-xs"
>
SL -20 pips
</button>
<button
onClick={() => handleSetTrailingPreset(50)}
className="px-3 py-1.5 bg-gray-700 hover:bg-gray-600 text-gray-300 rounded text-xs"
>
SL -50 pips
</button>
</div>
{/* Risk/Reward Preview */}
{calculations.riskReward && (
<div className="flex items-center justify-between p-3 bg-blue-500/10 border border-blue-500/30 rounded-lg">
<div className="flex items-center gap-2">
<Calculator className="w-4 h-4 text-blue-400" />
<span className="text-sm text-blue-400">Risk/Reward</span>
</div>
<span className="text-white font-bold">1:{calculations.riskReward.toFixed(2)}</span>
</div>
)}
{/* Error Message */}
{error && (
<div className="flex items-center gap-2 p-3 bg-red-500/10 border border-red-500/30 rounded-lg">
<AlertCircle className="w-5 h-5 text-red-400" />
<span className="text-sm text-red-400">{error}</span>
</div>
)}
{/* Success Message */}
{success && (
<div className="flex items-center gap-2 p-3 bg-green-500/10 border border-green-500/30 rounded-lg">
<Check className="w-5 h-5 text-green-400" />
<span className="text-sm text-green-400">Position modified successfully</span>
</div>
)}
</div>
{/* Footer */}
<div className="flex items-center gap-3 p-4 border-t border-gray-700">
<button
onClick={onClose}
disabled={isSubmitting}
className="flex-1 py-2.5 bg-gray-700 hover:bg-gray-600 text-white rounded-lg font-medium disabled:opacity-50"
>
Cancel
</button>
<button
onClick={handleSubmit}
disabled={isSubmitting || (!slPrice && !tpPrice)}
className="flex-1 flex items-center justify-center gap-2 py-2.5 bg-blue-600 hover:bg-blue-500 text-white rounded-lg font-medium disabled:opacity-50"
>
{isSubmitting ? (
<>
<Loader2 className="w-4 h-4 animate-spin" />
Modifying...
</>
) : (
'Apply Changes'
)}
</button>
</div>
</div>
</div>
);
};
export default PositionModifierDialog;

View File

@ -0,0 +1,390 @@
/**
* RiskBasedPositionSizer Component
* Standalone risk calculator for position sizing before entry
*/
import React, { useState, useMemo } from 'react';
import {
Calculator,
DollarSign,
Percent,
TrendingUp,
TrendingDown,
Target,
Shield,
AlertTriangle,
Info,
Copy,
Check,
} from 'lucide-react';
interface RiskBasedPositionSizerProps {
accountBalance?: number;
defaultSymbol?: string;
defaultRiskPercent?: number;
onCalculate?: (result: CalculationResult) => void;
onApplyToOrder?: (lots: number) => void;
compact?: boolean;
}
interface CalculationResult {
lots: number;
riskAmount: number;
potentialLoss: number;
potentialProfit: number;
riskRewardRatio: number;
pipValue: number;
slPips: number;
tpPips: number;
}
interface RiskScenario {
riskPercent: number;
lots: number;
riskAmount: number;
}
const RiskBasedPositionSizer: React.FC<RiskBasedPositionSizerProps> = ({
accountBalance = 10000,
defaultSymbol = 'EURUSD',
defaultRiskPercent = 1,
onCalculate,
onApplyToOrder,
compact = false,
}) => {
const [balance, setBalance] = useState<string>(accountBalance.toString());
const [riskPercent, setRiskPercent] = useState<string>(defaultRiskPercent.toString());
const [symbol, setSymbol] = useState(defaultSymbol);
const [entryPrice, setEntryPrice] = useState<string>('');
const [stopLoss, setStopLoss] = useState<string>('');
const [takeProfit, setTakeProfit] = useState<string>('');
const [tradeType, setTradeType] = useState<'BUY' | 'SELL'>('BUY');
const [copied, setCopied] = useState(false);
const pipValue = symbol.includes('JPY') ? 0.01 : 0.0001;
const pipValueUsd = 10; // Approximate USD value per pip per lot for majors
// Calculate position size and metrics
const calculation = useMemo<CalculationResult | null>(() => {
const balanceNum = parseFloat(balance) || 0;
const riskPercentNum = parseFloat(riskPercent) || 0;
const entry = parseFloat(entryPrice) || 0;
const sl = parseFloat(stopLoss) || 0;
const tp = parseFloat(takeProfit) || 0;
if (!balanceNum || !riskPercentNum || !entry || !sl) {
return null;
}
// Calculate pips to SL
const slPips = Math.abs(entry - sl) / pipValue;
const tpPips = tp ? Math.abs(tp - entry) / pipValue : 0;
// Risk amount in currency
const riskAmount = (balanceNum * riskPercentNum) / 100;
// Position size (lots)
const lots = riskAmount / (slPips * pipValueUsd);
const roundedLots = Math.floor(lots * 100) / 100; // Round down to 0.01
// Potential P&L
const potentialLoss = roundedLots * slPips * pipValueUsd;
const potentialProfit = tp ? roundedLots * tpPips * pipValueUsd : 0;
// Risk/Reward ratio
const riskRewardRatio = potentialProfit && potentialLoss ? potentialProfit / potentialLoss : 0;
const result: CalculationResult = {
lots: roundedLots,
riskAmount,
potentialLoss,
potentialProfit,
riskRewardRatio,
pipValue: roundedLots * pipValueUsd,
slPips,
tpPips,
};
onCalculate?.(result);
return result;
}, [balance, riskPercent, entryPrice, stopLoss, takeProfit, symbol, onCalculate]);
// Generate risk scenarios
const scenarios: RiskScenario[] = useMemo(() => {
const balanceNum = parseFloat(balance) || 0;
const entry = parseFloat(entryPrice) || 0;
const sl = parseFloat(stopLoss) || 0;
if (!balanceNum || !entry || !sl) return [];
const slPips = Math.abs(entry - sl) / pipValue;
const percents = [0.5, 1, 2, 5];
return percents.map((pct) => {
const risk = (balanceNum * pct) / 100;
const lots = Math.floor((risk / (slPips * pipValueUsd)) * 100) / 100;
return {
riskPercent: pct,
lots,
riskAmount: risk,
};
});
}, [balance, entryPrice, stopLoss, symbol]);
const handleCopyLots = async () => {
if (calculation) {
await navigator.clipboard.writeText(calculation.lots.toFixed(2));
setCopied(true);
setTimeout(() => setCopied(false), 2000);
}
};
const handleApply = () => {
if (calculation && onApplyToOrder) {
onApplyToOrder(calculation.lots);
}
};
const isValidSL = () => {
const entry = parseFloat(entryPrice) || 0;
const sl = parseFloat(stopLoss) || 0;
if (!entry || !sl) return true;
if (tradeType === 'BUY') {
return sl < entry;
} else {
return sl > entry;
}
};
return (
<div className={`bg-gray-800/50 rounded-xl border border-gray-700 ${compact ? 'p-3' : 'p-4'}`}>
{/* Header */}
<div className="flex items-center gap-2 mb-4">
<Calculator className="w-5 h-5 text-blue-400" />
<h3 className="font-semibold text-white">Position Sizer</h3>
</div>
<div className="space-y-4">
{/* Account Balance */}
<div>
<label className="block text-sm text-gray-400 mb-1.5">Account Balance</label>
<div className="relative">
<DollarSign className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-500" />
<input
type="number"
value={balance}
onChange={(e) => setBalance(e.target.value)}
className="w-full pl-10 pr-4 py-2.5 bg-gray-900 border border-gray-700 rounded-lg text-white focus:outline-none focus:border-blue-500"
/>
</div>
</div>
{/* Risk Percent */}
<div>
<label className="block text-sm text-gray-400 mb-1.5">Risk Per Trade</label>
<div className="relative">
<Percent className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-500" />
<input
type="number"
value={riskPercent}
onChange={(e) => setRiskPercent(e.target.value)}
step="0.5"
min="0.1"
max="10"
className="w-full pl-10 pr-4 py-2.5 bg-gray-900 border border-gray-700 rounded-lg text-white focus:outline-none focus:border-blue-500"
/>
</div>
<div className="flex gap-2 mt-2">
{[0.5, 1, 2, 3].map((pct) => (
<button
key={pct}
onClick={() => setRiskPercent(pct.toString())}
className={`px-3 py-1 text-xs rounded ${
parseFloat(riskPercent) === pct
? 'bg-blue-600 text-white'
: 'bg-gray-700 text-gray-300 hover:bg-gray-600'
}`}
>
{pct}%
</button>
))}
</div>
</div>
{/* Trade Type */}
<div>
<label className="block text-sm text-gray-400 mb-1.5">Trade Direction</label>
<div className="grid grid-cols-2 gap-2">
<button
onClick={() => setTradeType('BUY')}
className={`flex items-center justify-center gap-2 py-2.5 rounded-lg font-medium transition-colors ${
tradeType === 'BUY'
? 'bg-green-600 text-white'
: 'bg-gray-700 text-gray-300 hover:bg-gray-600'
}`}
>
<TrendingUp className="w-4 h-4" />
BUY
</button>
<button
onClick={() => setTradeType('SELL')}
className={`flex items-center justify-center gap-2 py-2.5 rounded-lg font-medium transition-colors ${
tradeType === 'SELL'
? 'bg-red-600 text-white'
: 'bg-gray-700 text-gray-300 hover:bg-gray-600'
}`}
>
<TrendingDown className="w-4 h-4" />
SELL
</button>
</div>
</div>
{/* Entry, SL, TP */}
<div className="grid grid-cols-3 gap-3">
<div>
<label className="block text-xs text-gray-500 mb-1">Entry Price</label>
<input
type="number"
value={entryPrice}
onChange={(e) => setEntryPrice(e.target.value)}
step="0.00001"
placeholder="1.08500"
className="w-full px-3 py-2 bg-gray-900 border border-gray-700 rounded-lg text-white text-sm font-mono focus:outline-none focus:border-blue-500"
/>
</div>
<div>
<label className="flex items-center gap-1 text-xs text-gray-500 mb-1">
<Shield className="w-3 h-3 text-red-400" />
Stop Loss
</label>
<input
type="number"
value={stopLoss}
onChange={(e) => setStopLoss(e.target.value)}
step="0.00001"
placeholder="1.08300"
className={`w-full px-3 py-2 bg-gray-900 border rounded-lg text-white text-sm font-mono focus:outline-none ${
!isValidSL() ? 'border-red-500' : 'border-gray-700 focus:border-red-500'
}`}
/>
</div>
<div>
<label className="flex items-center gap-1 text-xs text-gray-500 mb-1">
<Target className="w-3 h-3 text-green-400" />
Take Profit
</label>
<input
type="number"
value={takeProfit}
onChange={(e) => setTakeProfit(e.target.value)}
step="0.00001"
placeholder="1.08900"
className="w-full px-3 py-2 bg-gray-900 border border-gray-700 rounded-lg text-white text-sm font-mono focus:outline-none focus:border-green-500"
/>
</div>
</div>
{/* Invalid SL Warning */}
{!isValidSL() && (
<div className="flex items-center gap-2 p-2 bg-red-500/10 border border-red-500/30 rounded-lg">
<AlertTriangle className="w-4 h-4 text-red-400" />
<span className="text-xs text-red-400">
Stop loss should be {tradeType === 'BUY' ? 'below' : 'above'} entry price for {tradeType} trades
</span>
</div>
)}
{/* Result */}
{calculation && isValidSL() && (
<div className="p-4 bg-blue-500/10 border border-blue-500/30 rounded-xl space-y-3">
<div className="flex items-center justify-between">
<span className="text-sm text-gray-400">Recommended Position Size</span>
<div className="flex items-center gap-2">
<span className="text-2xl font-bold text-white">{calculation.lots.toFixed(2)}</span>
<span className="text-gray-400">lots</span>
<button
onClick={handleCopyLots}
className="p-1.5 text-gray-400 hover:text-white hover:bg-gray-700 rounded"
title="Copy"
>
{copied ? (
<Check className="w-4 h-4 text-green-400" />
) : (
<Copy className="w-4 h-4" />
)}
</button>
</div>
</div>
<div className="grid grid-cols-2 gap-3 pt-3 border-t border-blue-500/20">
<div>
<div className="text-xs text-gray-500">Risk Amount</div>
<div className="text-red-400 font-medium">${calculation.riskAmount.toFixed(2)}</div>
</div>
<div>
<div className="text-xs text-gray-500">Max Loss</div>
<div className="text-red-400 font-medium">${calculation.potentialLoss.toFixed(2)}</div>
</div>
{calculation.potentialProfit > 0 && (
<>
<div>
<div className="text-xs text-gray-500">Potential Profit</div>
<div className="text-green-400 font-medium">${calculation.potentialProfit.toFixed(2)}</div>
</div>
<div>
<div className="text-xs text-gray-500">R:R Ratio</div>
<div className="text-white font-medium">1:{calculation.riskRewardRatio.toFixed(2)}</div>
</div>
</>
)}
</div>
<div className="flex items-center gap-2 pt-2 text-xs text-gray-500">
<Info className="w-3 h-3" />
<span>SL: {calculation.slPips.toFixed(1)} pips Pip value: ${calculation.pipValue.toFixed(2)}</span>
</div>
{onApplyToOrder && (
<button
onClick={handleApply}
className="w-full py-2.5 bg-blue-600 hover:bg-blue-500 text-white rounded-lg font-medium transition-colors"
>
Apply to Order
</button>
)}
</div>
)}
{/* Risk Scenarios */}
{scenarios.length > 0 && !compact && (
<div>
<div className="flex items-center gap-2 mb-2">
<span className="text-xs text-gray-500">Quick Scenarios</span>
</div>
<div className="grid grid-cols-4 gap-2">
{scenarios.map((scenario) => (
<button
key={scenario.riskPercent}
onClick={() => setRiskPercent(scenario.riskPercent.toString())}
className={`p-2 rounded-lg text-center transition-colors ${
parseFloat(riskPercent) === scenario.riskPercent
? 'bg-blue-600/20 border border-blue-500/50'
: 'bg-gray-900/50 border border-gray-700 hover:border-gray-600'
}`}
>
<div className="text-xs text-gray-400">{scenario.riskPercent}%</div>
<div className="text-white font-bold">{scenario.lots.toFixed(2)}</div>
<div className="text-xs text-red-400">${scenario.riskAmount.toFixed(0)}</div>
</button>
))}
</div>
</div>
)}
</div>
</div>
);
};
export default RiskBasedPositionSizer;

View File

@ -0,0 +1,395 @@
/**
* TradeAlertsNotificationCenter Component
* Unified notification hub for MT4-specific trading events
*/
import React, { useState, useEffect, useMemo } from 'react';
import {
Bell,
BellOff,
X,
Check,
AlertTriangle,
TrendingUp,
TrendingDown,
Target,
Shield,
Wifi,
WifiOff,
Volume2,
VolumeX,
Trash2,
Filter,
Clock,
DollarSign,
} from 'lucide-react';
export type AlertType =
| 'trade_opened'
| 'trade_closed'
| 'sl_hit'
| 'tp_hit'
| 'margin_warning'
| 'margin_call'
| 'price_alert'
| 'connection_lost'
| 'connection_restored'
| 'order_filled'
| 'order_rejected';
export type AlertPriority = 'low' | 'medium' | 'high' | 'critical';
export interface TradeAlert {
id: string;
type: AlertType;
priority: AlertPriority;
title: string;
message: string;
symbol?: string;
ticket?: number;
profit?: number;
timestamp: string;
read: boolean;
}
interface TradeAlertsNotificationCenterProps {
alerts?: TradeAlert[];
onMarkRead?: (alertId: string) => void;
onMarkAllRead?: () => void;
onDelete?: (alertId: string) => void;
onClearAll?: () => void;
soundEnabled?: boolean;
onSoundToggle?: (enabled: boolean) => void;
maxVisible?: number;
compact?: boolean;
}
const TradeAlertsNotificationCenter: React.FC<TradeAlertsNotificationCenterProps> = ({
alerts = [],
onMarkRead,
onMarkAllRead,
onDelete,
onClearAll,
soundEnabled = true,
onSoundToggle,
maxVisible = 50,
compact = false,
}) => {
const [filterType, setFilterType] = useState<AlertType | 'all'>('all');
const [showUnreadOnly, setShowUnreadOnly] = useState(false);
const [expandedId, setExpandedId] = useState<string | null>(null);
const unreadCount = alerts.filter((a) => !a.read).length;
// Filter alerts
const filteredAlerts = useMemo(() => {
let result = [...alerts];
if (filterType !== 'all') {
result = result.filter((a) => a.type === filterType);
}
if (showUnreadOnly) {
result = result.filter((a) => !a.read);
}
return result.slice(0, maxVisible);
}, [alerts, filterType, showUnreadOnly, maxVisible]);
// Play sound for new critical alerts
useEffect(() => {
if (soundEnabled && alerts.length > 0) {
const latestAlert = alerts[0];
if (!latestAlert.read && latestAlert.priority === 'critical') {
// In a real app, play notification sound
console.log('Playing alert sound');
}
}
}, [alerts, soundEnabled]);
const getAlertIcon = (type: AlertType, priority: AlertPriority) => {
switch (type) {
case 'trade_opened':
case 'order_filled':
return <TrendingUp className="w-4 h-4 text-blue-400" />;
case 'trade_closed':
return <Check className="w-4 h-4 text-gray-400" />;
case 'sl_hit':
return <Shield className="w-4 h-4 text-red-400" />;
case 'tp_hit':
return <Target className="w-4 h-4 text-green-400" />;
case 'margin_warning':
return <AlertTriangle className="w-4 h-4 text-yellow-400" />;
case 'margin_call':
return <AlertTriangle className="w-4 h-4 text-red-400" />;
case 'price_alert':
return <Bell className="w-4 h-4 text-purple-400" />;
case 'connection_lost':
return <WifiOff className="w-4 h-4 text-red-400" />;
case 'connection_restored':
return <Wifi className="w-4 h-4 text-green-400" />;
case 'order_rejected':
return <X className="w-4 h-4 text-red-400" />;
default:
return <Bell className="w-4 h-4 text-gray-400" />;
}
};
const getPriorityColor = (priority: AlertPriority) => {
switch (priority) {
case 'critical':
return 'border-red-500/50 bg-red-500/10';
case 'high':
return 'border-orange-500/50 bg-orange-500/10';
case 'medium':
return 'border-yellow-500/50 bg-yellow-500/10';
default:
return 'border-gray-700 bg-gray-800/50';
}
};
const formatTime = (timestamp: string) => {
const date = new Date(timestamp);
const now = new Date();
const diff = now.getTime() - date.getTime();
if (diff < 60000) return 'Just now';
if (diff < 3600000) return `${Math.floor(diff / 60000)}m ago`;
if (diff < 86400000) return `${Math.floor(diff / 3600000)}h ago`;
return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
};
const getTypeLabel = (type: AlertType) => {
const labels: Record<AlertType, string> = {
trade_opened: 'Trade Opened',
trade_closed: 'Trade Closed',
sl_hit: 'Stop Loss Hit',
tp_hit: 'Take Profit Hit',
margin_warning: 'Margin Warning',
margin_call: 'Margin Call',
price_alert: 'Price Alert',
connection_lost: 'Disconnected',
connection_restored: 'Connected',
order_filled: 'Order Filled',
order_rejected: 'Order Rejected',
};
return labels[type] || type;
};
return (
<div className={`bg-gray-800/50 rounded-xl border border-gray-700 ${compact ? 'p-3' : 'p-4'}`}>
{/* Header */}
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-3">
<div className="relative">
<Bell className="w-5 h-5 text-blue-400" />
{unreadCount > 0 && (
<span className="absolute -top-1 -right-1 w-4 h-4 bg-red-500 rounded-full text-[10px] text-white flex items-center justify-center font-bold">
{unreadCount > 9 ? '9+' : unreadCount}
</span>
)}
</div>
<div>
<h3 className="font-semibold text-white">Trade Alerts</h3>
<p className="text-xs text-gray-500">{alerts.length} total {unreadCount} unread</p>
</div>
</div>
<div className="flex items-center gap-2">
{/* Sound Toggle */}
<button
onClick={() => onSoundToggle?.(!soundEnabled)}
className={`p-2 rounded-lg transition-colors ${
soundEnabled
? 'text-blue-400 bg-blue-500/10'
: 'text-gray-500 hover:bg-gray-700'
}`}
title={soundEnabled ? 'Sound On' : 'Sound Off'}
>
{soundEnabled ? <Volume2 className="w-4 h-4" /> : <VolumeX className="w-4 h-4" />}
</button>
{/* Mark All Read */}
{unreadCount > 0 && (
<button
onClick={onMarkAllRead}
className="p-2 text-gray-400 hover:text-white hover:bg-gray-700 rounded-lg transition-colors"
title="Mark all as read"
>
<Check className="w-4 h-4" />
</button>
)}
{/* Clear All */}
{alerts.length > 0 && onClearAll && (
<button
onClick={onClearAll}
className="p-2 text-gray-400 hover:text-red-400 hover:bg-gray-700 rounded-lg transition-colors"
title="Clear all"
>
<Trash2 className="w-4 h-4" />
</button>
)}
</div>
</div>
{/* Filters */}
<div className="flex items-center gap-2 mb-4 overflow-x-auto pb-2">
<Filter className="w-4 h-4 text-gray-500 flex-shrink-0" />
<button
onClick={() => setFilterType('all')}
className={`px-2 py-1 text-xs rounded whitespace-nowrap ${
filterType === 'all' ? 'bg-blue-600 text-white' : 'bg-gray-700 text-gray-300'
}`}
>
All
</button>
<button
onClick={() => setFilterType('trade_opened')}
className={`px-2 py-1 text-xs rounded whitespace-nowrap ${
filterType === 'trade_opened' ? 'bg-blue-600 text-white' : 'bg-gray-700 text-gray-300'
}`}
>
Trades
</button>
<button
onClick={() => setFilterType('sl_hit')}
className={`px-2 py-1 text-xs rounded whitespace-nowrap ${
filterType === 'sl_hit' ? 'bg-blue-600 text-white' : 'bg-gray-700 text-gray-300'
}`}
>
SL/TP
</button>
<button
onClick={() => setFilterType('margin_warning')}
className={`px-2 py-1 text-xs rounded whitespace-nowrap ${
filterType === 'margin_warning' ? 'bg-blue-600 text-white' : 'bg-gray-700 text-gray-300'
}`}
>
Margin
</button>
<button
onClick={() => setShowUnreadOnly(!showUnreadOnly)}
className={`px-2 py-1 text-xs rounded whitespace-nowrap ${
showUnreadOnly ? 'bg-blue-600 text-white' : 'bg-gray-700 text-gray-300'
}`}
>
Unread
</button>
</div>
{/* Alerts List */}
{filteredAlerts.length === 0 ? (
<div className="text-center py-8">
<BellOff className="w-10 h-10 text-gray-600 mx-auto mb-2" />
<p className="text-gray-400">No alerts</p>
</div>
) : (
<div className="space-y-2 max-h-[400px] overflow-y-auto">
{filteredAlerts.map((alert) => (
<div
key={alert.id}
onClick={() => {
if (!alert.read) onMarkRead?.(alert.id);
setExpandedId(expandedId === alert.id ? null : alert.id);
}}
className={`p-3 rounded-lg border cursor-pointer transition-all ${getPriorityColor(alert.priority)} ${
!alert.read ? 'ring-1 ring-blue-500/30' : ''
}`}
>
<div className="flex items-start gap-3">
<div className="p-1.5 bg-gray-900/50 rounded-lg flex-shrink-0">
{getAlertIcon(alert.type, alert.priority)}
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center justify-between gap-2">
<div className="flex items-center gap-2">
<span className={`font-medium ${alert.read ? 'text-gray-400' : 'text-white'}`}>
{alert.title}
</span>
{!alert.read && (
<span className="w-2 h-2 bg-blue-500 rounded-full" />
)}
</div>
<span className="text-xs text-gray-500 flex-shrink-0">
{formatTime(alert.timestamp)}
</span>
</div>
<p className="text-sm text-gray-400 mt-0.5 truncate">{alert.message}</p>
{/* Expanded Content */}
{expandedId === alert.id && (
<div className="mt-3 pt-3 border-t border-gray-700/50">
<div className="flex flex-wrap gap-3 text-xs">
<div className="flex items-center gap-1">
<Clock className="w-3 h-3 text-gray-500" />
<span className="text-gray-400">
{new Date(alert.timestamp).toLocaleString()}
</span>
</div>
{alert.symbol && (
<div className="flex items-center gap-1">
<TrendingUp className="w-3 h-3 text-gray-500" />
<span className="text-gray-400">{alert.symbol}</span>
</div>
)}
{alert.ticket && (
<span className="text-gray-500">#{alert.ticket}</span>
)}
{alert.profit !== undefined && (
<div className="flex items-center gap-1">
<DollarSign className="w-3 h-3 text-gray-500" />
<span className={alert.profit >= 0 ? 'text-green-400' : 'text-red-400'}>
{alert.profit >= 0 ? '+' : ''}{alert.profit.toFixed(2)}
</span>
</div>
)}
</div>
{onDelete && (
<button
onClick={(e) => {
e.stopPropagation();
onDelete(alert.id);
}}
className="mt-2 text-xs text-red-400 hover:text-red-300"
>
Delete alert
</button>
)}
</div>
)}
</div>
</div>
</div>
))}
</div>
)}
{/* Alert Type Legend */}
{!compact && (
<div className="mt-4 pt-4 border-t border-gray-700">
<div className="flex flex-wrap gap-3 text-xs text-gray-500">
<span className="flex items-center gap-1">
<span className="w-2 h-2 bg-red-500 rounded-full" />
Critical
</span>
<span className="flex items-center gap-1">
<span className="w-2 h-2 bg-orange-500 rounded-full" />
High
</span>
<span className="flex items-center gap-1">
<span className="w-2 h-2 bg-yellow-500 rounded-full" />
Medium
</span>
<span className="flex items-center gap-1">
<span className="w-2 h-2 bg-gray-500 rounded-full" />
Low
</span>
</div>
</div>
)}
</div>
);
};
export default TradeAlertsNotificationCenter;

View File

@ -43,3 +43,8 @@ export { default as AccountHealthDashboard } from './AccountHealthDashboard';
export { default as QuickOrderPanel } from './QuickOrderPanel';
export { default as TradeExecutionHistory } from './TradeExecutionHistory';
export { default as TradingMetricsCard } from './TradingMetricsCard';
export { default as MT4LiveTradesPanel } from './MT4LiveTradesPanel';
export { default as PositionModifierDialog } from './PositionModifierDialog';
export { default as RiskBasedPositionSizer } from './RiskBasedPositionSizer';
export { default as TradeAlertsNotificationCenter } from './TradeAlertsNotificationCenter';
export type { TradeAlert, AlertType, AlertPriority } from './TradeAlertsNotificationCenter';