trading-platform-frontend-v2/src/modules/trading/components/QuickOrderPanel.tsx
Adrian Flores Cortes 4d2c00ac30 [OQI-009] feat: Add MT4 trading components and WebSocket hook
- QuickOrderPanel: One-click trading with lot presets
- TradeExecutionHistory: Session trade history with P&L stats
- TradingMetricsCard: Daily trading metrics and performance
- useMT4WebSocket: Real-time account/position updates hook

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-25 11:23:33 -06:00

333 lines
12 KiB
TypeScript

/**
* QuickOrderPanel Component
* Compact order entry for fast one-click trading
*/
import React, { useState, useCallback } from 'react';
import {
TrendingUp,
TrendingDown,
Loader2,
Settings,
Zap,
AlertTriangle,
Check,
} from 'lucide-react';
import { executeMT4Trade } from '../../../services/trading.service';
interface QuickOrderPanelProps {
symbol: string;
currentPrice: number;
spread?: number;
accountBalance?: number;
onOrderExecuted?: (ticket: number, type: 'buy' | 'sell') => void;
onError?: (error: string) => void;
compact?: boolean;
}
interface LotPreset {
label: string;
lots: number;
}
const DEFAULT_PRESETS: LotPreset[] = [
{ label: '0.01', lots: 0.01 },
{ label: '0.05', lots: 0.05 },
{ label: '0.10', lots: 0.10 },
{ label: '0.25', lots: 0.25 },
{ label: '0.50', lots: 0.50 },
{ label: '1.00', lots: 1.00 },
];
const QuickOrderPanel: React.FC<QuickOrderPanelProps> = ({
symbol,
currentPrice,
spread = 0,
accountBalance = 0,
onOrderExecuted,
onError,
compact = false,
}) => {
const [selectedLots, setSelectedLots] = useState<number>(0.01);
const [customLots, setCustomLots] = useState<string>('');
const [isExecuting, setIsExecuting] = useState<'buy' | 'sell' | null>(null);
const [lastResult, setLastResult] = useState<{ type: 'success' | 'error'; message: string } | null>(null);
const [showSettings, setShowSettings] = useState(false);
const [slPips, setSlPips] = useState<number>(20);
const [tpPips, setTpPips] = useState<number>(40);
const [useSLTP, setUseSLTP] = useState(true);
const activeLots = customLots ? parseFloat(customLots) : selectedLots;
const bidPrice = currentPrice - spread / 2;
const askPrice = currentPrice + spread / 2;
// Calculate pip value (simplified - assumes forex major pairs)
const pipValue = symbol.includes('JPY') ? 0.01 : 0.0001;
const calculateSLTP = (type: 'buy' | 'sell') => {
const price = type === 'buy' ? askPrice : bidPrice;
if (!useSLTP) return { sl: undefined, tp: undefined };
if (type === 'buy') {
return {
sl: price - slPips * pipValue,
tp: price + tpPips * pipValue,
};
} else {
return {
sl: price + slPips * pipValue,
tp: price - tpPips * pipValue,
};
}
};
const estimateRisk = () => {
if (!accountBalance || !useSLTP) return null;
// Rough estimate: lot * pip value * SL pips
const riskPerPip = activeLots * 10; // ~$10 per pip per lot for majors
const totalRisk = riskPerPip * slPips;
const riskPercent = (totalRisk / accountBalance) * 100;
return { totalRisk, riskPercent };
};
const risk = estimateRisk();
const executeOrder = useCallback(async (type: 'buy' | 'sell') => {
if (isExecuting || activeLots <= 0) return;
setIsExecuting(type);
setLastResult(null);
try {
const { sl, tp } = calculateSLTP(type);
const result = await executeMT4Trade({
symbol,
type: type === 'buy' ? 'BUY' : 'SELL',
lots: activeLots,
stopLoss: sl,
takeProfit: tp,
});
setLastResult({ type: 'success', message: `#${result.ticket} ${type.toUpperCase()} ${activeLots} lots` });
onOrderExecuted?.(result.ticket, type);
// Clear success message after 3 seconds
setTimeout(() => setLastResult(null), 3000);
} catch (err) {
const errorMessage = err instanceof Error ? err.message : 'Order failed';
setLastResult({ type: 'error', message: errorMessage });
onError?.(errorMessage);
} finally {
setIsExecuting(null);
}
}, [symbol, activeLots, useSLTP, slPips, tpPips, askPrice, bidPrice, onOrderExecuted, onError]);
if (compact) {
return (
<div className="flex items-center gap-2 p-2 bg-gray-800/50 rounded-lg">
<span className="text-sm font-mono text-gray-400">{symbol}</span>
<select
value={selectedLots}
onChange={(e) => setSelectedLots(parseFloat(e.target.value))}
className="px-2 py-1 bg-gray-900 border border-gray-700 rounded text-sm text-white"
>
{DEFAULT_PRESETS.map((preset) => (
<option key={preset.lots} value={preset.lots}>{preset.label}</option>
))}
</select>
<button
onClick={() => executeOrder('buy')}
disabled={!!isExecuting}
className="px-3 py-1 bg-green-600 hover:bg-green-500 text-white rounded text-sm font-medium disabled:opacity-50"
>
{isExecuting === 'buy' ? <Loader2 className="w-4 h-4 animate-spin" /> : 'BUY'}
</button>
<button
onClick={() => executeOrder('sell')}
disabled={!!isExecuting}
className="px-3 py-1 bg-red-600 hover:bg-red-500 text-white rounded text-sm font-medium disabled:opacity-50"
>
{isExecuting === 'sell' ? <Loader2 className="w-4 h-4 animate-spin" /> : 'SELL'}
</button>
</div>
);
}
return (
<div className="p-4 bg-gray-800/50 rounded-xl border border-gray-700">
{/* Header */}
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-2">
<Zap className="w-5 h-5 text-yellow-400" />
<h3 className="font-semibold text-white">Quick Order</h3>
</div>
<button
onClick={() => setShowSettings(!showSettings)}
className={`p-1.5 rounded-lg transition-colors ${
showSettings ? 'bg-blue-600 text-white' : 'text-gray-400 hover:text-white hover:bg-gray-700'
}`}
>
<Settings className="w-4 h-4" />
</button>
</div>
{/* Symbol & Price */}
<div className="text-center mb-4">
<div className="text-lg font-bold text-white">{symbol}</div>
<div className="flex items-center justify-center gap-4 mt-1">
<span className="text-red-400 font-mono">{bidPrice.toFixed(5)}</span>
<span className="text-xs text-gray-500">|</span>
<span className="text-green-400 font-mono">{askPrice.toFixed(5)}</span>
</div>
{spread > 0 && (
<div className="text-xs text-gray-500 mt-1">
Spread: {(spread / pipValue).toFixed(1)} pips
</div>
)}
</div>
{/* Lot Size Presets */}
<div className="mb-4">
<label className="block text-xs text-gray-400 mb-2">Lot Size</label>
<div className="grid grid-cols-6 gap-1">
{DEFAULT_PRESETS.map((preset) => (
<button
key={preset.lots}
onClick={() => {
setSelectedLots(preset.lots);
setCustomLots('');
}}
className={`py-1.5 text-xs font-medium rounded transition-colors ${
selectedLots === preset.lots && !customLots
? 'bg-blue-600 text-white'
: 'bg-gray-700 text-gray-300 hover:bg-gray-600'
}`}
>
{preset.label}
</button>
))}
</div>
<input
type="number"
value={customLots}
onChange={(e) => setCustomLots(e.target.value)}
placeholder="Custom lots..."
step="0.01"
min="0.01"
className="w-full mt-2 px-3 py-2 bg-gray-900 border border-gray-700 rounded-lg text-white text-sm placeholder-gray-500 focus:outline-none focus:border-blue-500"
/>
</div>
{/* Settings Panel */}
{showSettings && (
<div className="mb-4 p-3 bg-gray-900/50 rounded-lg space-y-3">
<label className="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
checked={useSLTP}
onChange={(e) => setUseSLTP(e.target.checked)}
className="w-4 h-4 rounded border-gray-600 bg-gray-900 text-blue-600"
/>
<span className="text-sm text-gray-300">Auto SL/TP</span>
</label>
{useSLTP && (
<div className="grid grid-cols-2 gap-3">
<div>
<label className="block text-xs text-gray-400 mb-1">SL (pips)</label>
<input
type="number"
value={slPips}
onChange={(e) => setSlPips(parseInt(e.target.value) || 0)}
className="w-full px-2 py-1.5 bg-gray-800 border border-gray-700 rounded text-sm text-white"
/>
</div>
<div>
<label className="block text-xs text-gray-400 mb-1">TP (pips)</label>
<input
type="number"
value={tpPips}
onChange={(e) => setTpPips(parseInt(e.target.value) || 0)}
className="w-full px-2 py-1.5 bg-gray-800 border border-gray-700 rounded text-sm text-white"
/>
</div>
</div>
)}
</div>
)}
{/* Risk Warning */}
{risk && risk.riskPercent > 2 && (
<div className="flex items-center gap-2 p-2 mb-4 bg-yellow-500/10 border border-yellow-500/30 rounded-lg">
<AlertTriangle className="w-4 h-4 text-yellow-400 flex-shrink-0" />
<span className="text-xs text-yellow-400">
Risk: ${risk.totalRisk.toFixed(2)} ({risk.riskPercent.toFixed(1)}% of balance)
</span>
</div>
)}
{/* Buy/Sell Buttons */}
<div className="grid grid-cols-2 gap-3">
<button
onClick={() => executeOrder('sell')}
disabled={!!isExecuting || activeLots <= 0}
className="flex flex-col items-center gap-1 py-3 bg-red-600 hover:bg-red-500 text-white rounded-lg font-medium transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
{isExecuting === 'sell' ? (
<Loader2 className="w-5 h-5 animate-spin" />
) : (
<>
<TrendingDown className="w-5 h-5" />
<span>SELL</span>
<span className="text-xs opacity-75">{bidPrice.toFixed(5)}</span>
</>
)}
</button>
<button
onClick={() => executeOrder('buy')}
disabled={!!isExecuting || activeLots <= 0}
className="flex flex-col items-center gap-1 py-3 bg-green-600 hover:bg-green-500 text-white rounded-lg font-medium transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
{isExecuting === 'buy' ? (
<Loader2 className="w-5 h-5 animate-spin" />
) : (
<>
<TrendingUp className="w-5 h-5" />
<span>BUY</span>
<span className="text-xs opacity-75">{askPrice.toFixed(5)}</span>
</>
)}
</button>
</div>
{/* Result Message */}
{lastResult && (
<div className={`flex items-center gap-2 mt-3 p-2 rounded-lg ${
lastResult.type === 'success'
? 'bg-green-500/10 border border-green-500/30'
: 'bg-red-500/10 border border-red-500/30'
}`}>
{lastResult.type === 'success' ? (
<Check className="w-4 h-4 text-green-400" />
) : (
<AlertTriangle className="w-4 h-4 text-red-400" />
)}
<span className={`text-xs ${
lastResult.type === 'success' ? 'text-green-400' : 'text-red-400'
}`}>
{lastResult.message}
</span>
</div>
)}
{/* Active Lots Display */}
<div className="mt-3 text-center text-xs text-gray-500">
Trading <span className="text-white font-medium">{activeLots.toFixed(2)}</span> lots
</div>
</div>
);
};
export default QuickOrderPanel;