- 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>
333 lines
12 KiB
TypeScript
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;
|