- Fixed critical timeframe mapping bug where '4h' was interpreted as '4 minutes' - Now prioritizes minute values: '4h' -> ['240', '240m', '4h', '4H'] - Added fallback mechanism to enter exact minutes (240) in custom interval input - Fixed multiple syntax errors in tradingview-automation.ts: * Missing closing parentheses in console.log statements * Missing parentheses in writeFile and JSON.parse calls * Fixed import statements for fs and path modules * Added missing utility methods (fileExists, markCaptchaDetected, etc.) - Enhanced timeframe selection with comprehensive hour mappings (1h, 2h, 4h, 6h, 12h) - Added detailed logging for debugging timeframe selection - Application now builds successfully without syntax errors - Interval selection should work correctly for all common timeframes Key improvements: ✅ 4h chart selection now works correctly (240 minutes, not 4 minutes) ✅ All TypeScript compilation errors resolved ✅ Enhanced debugging output for timeframe mapping ✅ Robust fallback mechanisms for interval selection ✅ Docker integration and manual CAPTCHA handling maintained
756 lines
27 KiB
TypeScript
756 lines
27 KiB
TypeScript
"use client"
|
|
import React, { useState, useEffect } from 'react'
|
|
|
|
interface TradeParams {
|
|
symbol: string
|
|
side: 'LONG' | 'SHORT'
|
|
amount: number
|
|
leverage: number
|
|
orderType: 'MARKET' | 'LIMIT'
|
|
price?: number
|
|
stopLoss?: number
|
|
takeProfit?: number
|
|
stopLossType?: 'PRICE' | 'PERCENTAGE'
|
|
takeProfitType?: 'PRICE' | 'PERCENTAGE'
|
|
}
|
|
|
|
interface MarketData {
|
|
symbol: string
|
|
price: number
|
|
change24h: number
|
|
}
|
|
|
|
interface AccountData {
|
|
totalCollateral: number
|
|
freeCollateral: number
|
|
leverage: number
|
|
maintenanceMargin: number
|
|
}
|
|
|
|
interface BalanceApiResponse {
|
|
totalCollateral?: number
|
|
freeCollateral?: number
|
|
leverage?: number
|
|
marginRequirement?: number
|
|
}
|
|
|
|
interface TradingInfoApiResponse {
|
|
totalCollateral?: number
|
|
availableCollateral?: number
|
|
accountLeverage?: number
|
|
maintenanceMargin?: number
|
|
maxPositionSize?: number
|
|
requiredMargin?: number
|
|
}
|
|
|
|
export default function AdvancedTradingPanel() {
|
|
// Trading form state
|
|
const [symbol, setSymbol] = useState('SOLUSD')
|
|
const [side, setSide] = useState<'LONG' | 'SHORT'>('LONG')
|
|
const [orderType, setOrderType] = useState<'MARKET' | 'LIMIT'>('MARKET')
|
|
const [leverage, setLeverage] = useState(1)
|
|
const [positionSize, setPositionSize] = useState('')
|
|
const [limitPrice, setLimitPrice] = useState('')
|
|
const [loading, setLoading] = useState(false)
|
|
const [result, setResult] = useState<any>(null)
|
|
|
|
// Risk Management
|
|
const [enableStopLoss, setEnableStopLoss] = useState(false)
|
|
const [enableTakeProfit, setEnableTakeProfit] = useState(false)
|
|
const [stopLossType, setStopLossType] = useState<'PRICE' | 'PERCENTAGE'>('PERCENTAGE')
|
|
const [takeProfitType, setTakeProfitType] = useState<'PRICE' | 'PERCENTAGE'>('PERCENTAGE')
|
|
const [stopLossValue, setStopLossValue] = useState('')
|
|
const [takeProfitValue, setTakeProfitValue] = useState('')
|
|
|
|
// Market and account data
|
|
const [marketData, setMarketData] = useState<MarketData>({ symbol: 'SOLUSD', price: 160, change24h: -2.1 })
|
|
const [accountData, setAccountData] = useState<AccountData>({
|
|
totalCollateral: 0,
|
|
freeCollateral: 0,
|
|
leverage: 0,
|
|
maintenanceMargin: 0
|
|
})
|
|
const [positions, setPositions] = useState<any[]>([])
|
|
const [closingPosition, setClosingPosition] = useState<string | null>(null)
|
|
|
|
// Calculated values
|
|
const [liquidationPrice, setLiquidationPrice] = useState(0)
|
|
const [requiredMargin, setRequiredMargin] = useState(0)
|
|
const [maxPositionSize, setMaxPositionSize] = useState(0)
|
|
const [stopLossPrice, setStopLossPrice] = useState(0)
|
|
const [takeProfitPrice, setTakeProfitPrice] = useState(0)
|
|
const [riskRewardRatio, setRiskRewardRatio] = useState(0)
|
|
const [potentialLoss, setPotentialLoss] = useState(0)
|
|
const [potentialProfit, setPotentialProfit] = useState(0)
|
|
|
|
const availableSymbols = [
|
|
'SOLUSD', 'BTCUSD', 'ETHUSD', 'DOTUSD', 'AVAXUSD', 'ADAUSD',
|
|
'MATICUSD', 'LINKUSD', 'ATOMUSD', 'NEARUSD', 'APTUSD', 'ORBSUSD',
|
|
'RNDUSD', 'WIFUSD', 'JUPUSD', 'TNSUSD', 'DOGEUSD', 'PEPE1KUSD',
|
|
'POPCATUSD', 'BOMERUSD'
|
|
]
|
|
|
|
// Fetch account data on component mount
|
|
useEffect(() => {
|
|
fetchAccountData()
|
|
fetchPositions()
|
|
}, [])
|
|
|
|
// Recalculate when inputs change
|
|
useEffect(() => {
|
|
calculateTradingMetrics()
|
|
}, [positionSize, leverage, side, marketData.price, stopLossValue, takeProfitValue, stopLossType, takeProfitType, enableStopLoss, enableTakeProfit])
|
|
|
|
const fetchAccountData = async () => {
|
|
try {
|
|
// Fetch both balance and trading info
|
|
const [balanceResponse, tradingInfoResponse] = await Promise.all([
|
|
fetch('/api/drift/balance'),
|
|
fetch('/api/drift/trading-info')
|
|
])
|
|
|
|
let balanceData: BalanceApiResponse = {}
|
|
let tradingInfoData: TradingInfoApiResponse = {}
|
|
|
|
if (balanceResponse.ok) {
|
|
balanceData = await balanceResponse.json()
|
|
}
|
|
|
|
if (tradingInfoResponse.ok) {
|
|
tradingInfoData = await tradingInfoResponse.json()
|
|
}
|
|
|
|
// Combine data with fallbacks
|
|
setAccountData({
|
|
totalCollateral: balanceData.totalCollateral || tradingInfoData.totalCollateral || 0,
|
|
freeCollateral: balanceData.freeCollateral || tradingInfoData.availableCollateral || 0,
|
|
leverage: balanceData.leverage || tradingInfoData.accountLeverage || 0,
|
|
maintenanceMargin: balanceData.marginRequirement || tradingInfoData.maintenanceMargin || 0
|
|
})
|
|
|
|
// Update max position size from trading info if available
|
|
if (tradingInfoData.maxPositionSize) {
|
|
setMaxPositionSize(tradingInfoData.maxPositionSize)
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to fetch account data:', error)
|
|
}
|
|
}
|
|
|
|
const fetchPositions = async () => {
|
|
try {
|
|
const response = await fetch('/api/drift/positions')
|
|
if (response.ok) {
|
|
const data = await response.json()
|
|
setPositions(data.positions || [])
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to fetch positions:', error)
|
|
}
|
|
}
|
|
|
|
const calculateTradingMetrics = async () => {
|
|
if (!positionSize || !marketData.price) return
|
|
|
|
const size = parseFloat(positionSize)
|
|
const entryPrice = marketData.price
|
|
const notionalValue = size * entryPrice
|
|
|
|
// Try to get accurate calculations from the API first
|
|
try {
|
|
const response = await fetch(`/api/drift/trading-info?symbol=${symbol}&side=${side}&amount=${size}&leverage=${leverage}`)
|
|
if (response.ok) {
|
|
const apiData = await response.json()
|
|
|
|
// Use API calculations if available
|
|
if (apiData.requiredMargin !== undefined) setRequiredMargin(apiData.requiredMargin)
|
|
if (apiData.maxPositionSize !== undefined) setMaxPositionSize(apiData.maxPositionSize)
|
|
if (apiData.liquidationPrice !== undefined) setLiquidationPrice(apiData.liquidationPrice)
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to fetch trading calculations from API:', error)
|
|
}
|
|
|
|
// Fallback to local calculations
|
|
const margin = notionalValue / leverage
|
|
if (requiredMargin === 0) setRequiredMargin(margin)
|
|
|
|
// Calculate max position size based on available collateral
|
|
if (maxPositionSize === 0) {
|
|
const maxNotional = accountData.freeCollateral * leverage
|
|
const maxSize = maxNotional / entryPrice
|
|
setMaxPositionSize(maxSize)
|
|
}
|
|
|
|
// Calculate liquidation price
|
|
if (liquidationPrice === 0) {
|
|
// Simplified liquidation calculation (actual Drift uses more complex formula)
|
|
const maintenanceMarginRate = 0.05 // 5% maintenance margin
|
|
const liquidationBuffer = notionalValue * maintenanceMarginRate
|
|
|
|
let liqPrice = 0
|
|
if (side === 'LONG') {
|
|
// For long: liquidation when position value + margin = liquidation buffer
|
|
liqPrice = entryPrice * (1 - (margin - liquidationBuffer) / notionalValue)
|
|
} else {
|
|
// For short: liquidation when position value - margin = liquidation buffer
|
|
liqPrice = entryPrice * (1 + (margin - liquidationBuffer) / notionalValue)
|
|
}
|
|
|
|
setLiquidationPrice(Math.max(0, liqPrice))
|
|
}
|
|
|
|
// Calculate Stop Loss and Take Profit prices
|
|
let slPrice = 0
|
|
let tpPrice = 0
|
|
let potLoss = 0
|
|
let potProfit = 0
|
|
|
|
if (enableStopLoss && stopLossValue) {
|
|
if (stopLossType === 'PERCENTAGE') {
|
|
const slPercentage = parseFloat(stopLossValue) / 100
|
|
if (side === 'LONG') {
|
|
slPrice = entryPrice * (1 - slPercentage)
|
|
} else {
|
|
slPrice = entryPrice * (1 + slPercentage)
|
|
}
|
|
} else {
|
|
slPrice = parseFloat(stopLossValue)
|
|
}
|
|
|
|
// Calculate potential loss
|
|
if (slPrice > 0) {
|
|
potLoss = Math.abs(entryPrice - slPrice) * size
|
|
}
|
|
}
|
|
|
|
if (enableTakeProfit && takeProfitValue) {
|
|
if (takeProfitType === 'PERCENTAGE') {
|
|
const tpPercentage = parseFloat(takeProfitValue) / 100
|
|
if (side === 'LONG') {
|
|
tpPrice = entryPrice * (1 + tpPercentage)
|
|
} else {
|
|
tpPrice = entryPrice * (1 - tpPercentage)
|
|
}
|
|
} else {
|
|
tpPrice = parseFloat(takeProfitValue)
|
|
}
|
|
|
|
// Calculate potential profit
|
|
if (tpPrice > 0) {
|
|
potProfit = Math.abs(tpPrice - entryPrice) * size
|
|
}
|
|
}
|
|
|
|
setStopLossPrice(slPrice)
|
|
setTakeProfitPrice(tpPrice)
|
|
setPotentialLoss(potLoss)
|
|
setPotentialProfit(potProfit)
|
|
|
|
// Calculate Risk/Reward Ratio
|
|
if (potLoss > 0 && potProfit > 0) {
|
|
setRiskRewardRatio(potProfit / potLoss)
|
|
} else {
|
|
setRiskRewardRatio(0)
|
|
}
|
|
}
|
|
|
|
const handleTrade = async () => {
|
|
if (!positionSize || parseFloat(positionSize) <= 0) {
|
|
setResult({ success: false, error: 'Please enter a valid position size' })
|
|
return
|
|
}
|
|
|
|
if (requiredMargin > accountData.freeCollateral) {
|
|
setResult({ success: false, error: 'Insufficient collateral for this trade' })
|
|
return
|
|
}
|
|
|
|
if (orderType === 'LIMIT' && (!limitPrice || parseFloat(limitPrice) <= 0)) {
|
|
setResult({ success: false, error: 'Please enter a valid limit price' })
|
|
return
|
|
}
|
|
|
|
setLoading(true)
|
|
setResult(null)
|
|
|
|
try {
|
|
const tradeParams: TradeParams = {
|
|
symbol,
|
|
side,
|
|
amount: parseFloat(positionSize),
|
|
leverage,
|
|
orderType,
|
|
price: orderType === 'LIMIT' ? parseFloat(limitPrice) : undefined,
|
|
stopLoss: enableStopLoss && stopLossPrice > 0 ? stopLossPrice : undefined,
|
|
takeProfit: enableTakeProfit && takeProfitPrice > 0 ? takeProfitPrice : undefined,
|
|
stopLossType: enableStopLoss ? stopLossType : undefined,
|
|
takeProfitType: enableTakeProfit ? takeProfitType : undefined
|
|
}
|
|
|
|
const response = await fetch('/api/drift/trade', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(tradeParams)
|
|
})
|
|
|
|
const data = await response.json()
|
|
setResult(data)
|
|
|
|
if (data.success) {
|
|
// Refresh account data and positions after successful trade
|
|
await fetchAccountData()
|
|
await fetchPositions()
|
|
// Reset form
|
|
setPositionSize('')
|
|
setLimitPrice('')
|
|
}
|
|
} catch (error) {
|
|
setResult({ success: false, error: 'Trade execution failed' })
|
|
}
|
|
|
|
setLoading(false)
|
|
}
|
|
|
|
const closePosition = async (symbol: string, amount?: number) => {
|
|
setClosingPosition(symbol)
|
|
try {
|
|
const response = await fetch('/api/drift/close-position', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ symbol, amount })
|
|
})
|
|
|
|
const data = await response.json()
|
|
|
|
if (data.success) {
|
|
setResult({ success: true, message: `Position in ${symbol} closed successfully`, txId: data.txId })
|
|
// Refresh positions and account data
|
|
await fetchPositions()
|
|
await fetchAccountData()
|
|
} else {
|
|
setResult({ success: false, error: data.error })
|
|
}
|
|
} catch (error) {
|
|
setResult({ success: false, error: 'Failed to close position' })
|
|
}
|
|
setClosingPosition(null)
|
|
}
|
|
|
|
const setMaxSize = () => {
|
|
setPositionSize(maxPositionSize.toFixed(4))
|
|
}
|
|
|
|
const setPercentageSize = (percentage: number) => {
|
|
const size = (maxPositionSize * percentage / 100).toFixed(4)
|
|
setPositionSize(size)
|
|
}
|
|
|
|
return (
|
|
<div className="card card-gradient">
|
|
<div className="flex items-center justify-between mb-6">
|
|
<h2 className="text-lg font-bold text-white flex items-center">
|
|
<span className="w-8 h-8 bg-gradient-to-br from-blue-400 to-purple-600 rounded-lg flex items-center justify-center mr-3">
|
|
⚡
|
|
</span>
|
|
Advanced Trading
|
|
</h2>
|
|
<span className="text-xs text-gray-400">Drift Protocol</span>
|
|
</div>
|
|
|
|
{/* Market Info */}
|
|
<div className="mb-6 p-4 bg-gray-800/50 rounded-lg">
|
|
<div className="flex items-center justify-between">
|
|
<select
|
|
value={symbol}
|
|
onChange={(e) => setSymbol(e.target.value)}
|
|
className="bg-gray-700 text-white px-3 py-2 rounded border border-gray-600 focus:border-blue-400"
|
|
>
|
|
{availableSymbols.map(sym => (
|
|
<option key={sym} value={sym}>{sym}</option>
|
|
))}
|
|
</select>
|
|
<div className="text-right">
|
|
<div className="text-lg font-bold text-white">${marketData.price.toFixed(2)}</div>
|
|
<div className={`text-sm ${marketData.change24h >= 0 ? 'text-green-400' : 'text-red-400'}`}>
|
|
{marketData.change24h >= 0 ? '+' : ''}{marketData.change24h.toFixed(2)}%
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Side Selection */}
|
|
<div className="mb-4">
|
|
<label className="block text-gray-400 text-sm font-medium mb-2">Position Side</label>
|
|
<div className="grid grid-cols-2 gap-2">
|
|
<button
|
|
onClick={() => setSide('LONG')}
|
|
className={`py-2 px-4 rounded font-medium transition-colors ${
|
|
side === 'LONG'
|
|
? 'bg-green-500 text-white'
|
|
: 'bg-gray-700 text-gray-300 hover:bg-gray-600'
|
|
}`}
|
|
>
|
|
LONG
|
|
</button>
|
|
<button
|
|
onClick={() => setSide('SHORT')}
|
|
className={`py-2 px-4 rounded font-medium transition-colors ${
|
|
side === 'SHORT'
|
|
? 'bg-red-500 text-white'
|
|
: 'bg-gray-700 text-gray-300 hover:bg-gray-600'
|
|
}`}
|
|
>
|
|
SHORT
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Order Type */}
|
|
<div className="mb-4">
|
|
<label className="block text-gray-400 text-sm font-medium mb-2">Order Type</label>
|
|
<div className="grid grid-cols-2 gap-2">
|
|
<button
|
|
onClick={() => setOrderType('MARKET')}
|
|
className={`py-2 px-4 rounded font-medium transition-colors ${
|
|
orderType === 'MARKET'
|
|
? 'bg-blue-500 text-white'
|
|
: 'bg-gray-700 text-gray-300 hover:bg-gray-600'
|
|
}`}
|
|
>
|
|
MARKET
|
|
</button>
|
|
<button
|
|
onClick={() => setOrderType('LIMIT')}
|
|
className={`py-2 px-4 rounded font-medium transition-colors ${
|
|
orderType === 'LIMIT'
|
|
? 'bg-blue-500 text-white'
|
|
: 'bg-gray-700 text-gray-300 hover:bg-gray-600'
|
|
}`}
|
|
>
|
|
LIMIT
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Limit Price (if LIMIT order) */}
|
|
{orderType === 'LIMIT' && (
|
|
<div className="mb-4">
|
|
<label className="block text-gray-400 text-sm font-medium mb-2">Limit Price</label>
|
|
<input
|
|
type="number"
|
|
value={limitPrice}
|
|
onChange={(e) => setLimitPrice(e.target.value)}
|
|
placeholder="Enter limit price"
|
|
className="w-full bg-gray-700 text-white px-3 py-2 rounded border border-gray-600 focus:border-blue-400"
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
{/* Leverage Slider */}
|
|
<div className="mb-4">
|
|
<label className="block text-gray-400 text-sm font-medium mb-2">
|
|
Leverage: {leverage}x
|
|
</label>
|
|
<input
|
|
type="range"
|
|
min="1"
|
|
max="20"
|
|
value={leverage}
|
|
onChange={(e) => setLeverage(parseInt(e.target.value))}
|
|
className="w-full h-2 bg-gray-700 rounded-lg appearance-none cursor-pointer"
|
|
/>
|
|
<div className="flex justify-between text-xs text-gray-500 mt-1">
|
|
<span>1x</span>
|
|
<span>10x</span>
|
|
<span>20x</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Position Size */}
|
|
<div className="mb-4">
|
|
<label className="block text-gray-400 text-sm font-medium mb-2">Position Size</label>
|
|
<div className="relative">
|
|
<input
|
|
type="number"
|
|
value={positionSize}
|
|
onChange={(e) => setPositionSize(e.target.value)}
|
|
placeholder="Enter position size"
|
|
className="w-full bg-gray-700 text-white px-3 py-2 pr-16 rounded border border-gray-600 focus:border-blue-400"
|
|
/>
|
|
<button
|
|
onClick={setMaxSize}
|
|
className="absolute right-2 top-1/2 transform -translate-y-1/2 text-xs text-blue-400 hover:text-blue-300"
|
|
>
|
|
MAX
|
|
</button>
|
|
</div>
|
|
|
|
{/* Quick Size Buttons */}
|
|
<div className="grid grid-cols-4 gap-1 mt-2">
|
|
{[25, 50, 75, 100].map(pct => (
|
|
<button
|
|
key={pct}
|
|
onClick={() => setPercentageSize(pct)}
|
|
className="py-1 px-2 text-xs bg-gray-700 text-gray-300 rounded hover:bg-gray-600"
|
|
>
|
|
{pct}%
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Risk Management - Stop Loss */}
|
|
<div className="mb-4">
|
|
<div className="flex items-center justify-between mb-2">
|
|
<label className="text-gray-400 text-sm font-medium">Stop Loss</label>
|
|
<button
|
|
onClick={() => setEnableStopLoss(!enableStopLoss)}
|
|
className={`px-3 py-1 rounded text-xs font-medium transition-colors ${
|
|
enableStopLoss
|
|
? 'bg-red-500 text-white'
|
|
: 'bg-gray-700 text-gray-300 hover:bg-gray-600'
|
|
}`}
|
|
>
|
|
{enableStopLoss ? 'ON' : 'OFF'}
|
|
</button>
|
|
</div>
|
|
|
|
{enableStopLoss && (
|
|
<div className="space-y-2">
|
|
<div className="grid grid-cols-2 gap-2">
|
|
<button
|
|
onClick={() => setStopLossType('PERCENTAGE')}
|
|
className={`py-2 px-3 rounded text-sm font-medium transition-colors ${
|
|
stopLossType === 'PERCENTAGE'
|
|
? 'bg-red-500 text-white'
|
|
: 'bg-gray-700 text-gray-300 hover:bg-gray-600'
|
|
}`}
|
|
>
|
|
%
|
|
</button>
|
|
<button
|
|
onClick={() => setStopLossType('PRICE')}
|
|
className={`py-2 px-3 rounded text-sm font-medium transition-colors ${
|
|
stopLossType === 'PRICE'
|
|
? 'bg-red-500 text-white'
|
|
: 'bg-gray-700 text-gray-300 hover:bg-gray-600'
|
|
}`}
|
|
>
|
|
$
|
|
</button>
|
|
</div>
|
|
<input
|
|
type="number"
|
|
value={stopLossValue}
|
|
onChange={(e) => setStopLossValue(e.target.value)}
|
|
placeholder={stopLossType === 'PERCENTAGE' ? 'e.g., 5' : 'e.g., 150.00'}
|
|
className="w-full bg-gray-700 text-white px-3 py-2 rounded border border-gray-600 focus:border-red-400"
|
|
/>
|
|
{stopLossType === 'PERCENTAGE' && (
|
|
<div className="grid grid-cols-3 gap-1">
|
|
{[2, 5, 10].map(pct => (
|
|
<button
|
|
key={pct}
|
|
onClick={() => setStopLossValue(pct.toString())}
|
|
className="py-1 px-2 text-xs bg-red-600/20 text-red-400 rounded hover:bg-red-600/30"
|
|
>
|
|
{pct}%
|
|
</button>
|
|
))}
|
|
</div>
|
|
)}
|
|
{stopLossPrice > 0 && (
|
|
<div className="text-xs text-red-400">
|
|
Stop Loss Price: ${stopLossPrice.toFixed(2)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Risk Management - Take Profit */}
|
|
<div className="mb-4">
|
|
<div className="flex items-center justify-between mb-2">
|
|
<label className="text-gray-400 text-sm font-medium">Take Profit</label>
|
|
<button
|
|
onClick={() => setEnableTakeProfit(!enableTakeProfit)}
|
|
className={`px-3 py-1 rounded text-xs font-medium transition-colors ${
|
|
enableTakeProfit
|
|
? 'bg-green-500 text-white'
|
|
: 'bg-gray-700 text-gray-300 hover:bg-gray-600'
|
|
}`}
|
|
>
|
|
{enableTakeProfit ? 'ON' : 'OFF'}
|
|
</button>
|
|
</div>
|
|
|
|
{enableTakeProfit && (
|
|
<div className="space-y-2">
|
|
<div className="grid grid-cols-2 gap-2">
|
|
<button
|
|
onClick={() => setTakeProfitType('PERCENTAGE')}
|
|
className={`py-2 px-3 rounded text-sm font-medium transition-colors ${
|
|
takeProfitType === 'PERCENTAGE'
|
|
? 'bg-green-500 text-white'
|
|
: 'bg-gray-700 text-gray-300 hover:bg-gray-600'
|
|
}`}
|
|
>
|
|
%
|
|
</button>
|
|
<button
|
|
onClick={() => setTakeProfitType('PRICE')}
|
|
className={`py-2 px-3 rounded text-sm font-medium transition-colors ${
|
|
takeProfitType === 'PRICE'
|
|
? 'bg-green-500 text-white'
|
|
: 'bg-gray-700 text-gray-300 hover:bg-gray-600'
|
|
}`}
|
|
>
|
|
$
|
|
</button>
|
|
</div>
|
|
<input
|
|
type="number"
|
|
value={takeProfitValue}
|
|
onChange={(e) => setTakeProfitValue(e.target.value)}
|
|
placeholder={takeProfitType === 'PERCENTAGE' ? 'e.g., 10' : 'e.g., 180.00'}
|
|
className="w-full bg-gray-700 text-white px-3 py-2 rounded border border-gray-600 focus:border-green-400"
|
|
/>
|
|
{takeProfitPrice > 0 && (
|
|
<div className="text-xs text-green-400">
|
|
Take Profit Price: ${takeProfitPrice.toFixed(2)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Trading Metrics */}
|
|
<div className="mb-6 space-y-3 p-4 bg-gray-800/30 rounded-lg">
|
|
<div className="flex justify-between text-sm">
|
|
<span className="text-gray-400">Required Margin:</span>
|
|
<span className="text-white font-mono">${requiredMargin.toFixed(2)}</span>
|
|
</div>
|
|
<div className="flex justify-between text-sm">
|
|
<span className="text-gray-400">Available Collateral:</span>
|
|
<span className="text-white font-mono">${accountData.freeCollateral.toFixed(2)}</span>
|
|
</div>
|
|
<div className="flex justify-between text-sm">
|
|
<span className="text-gray-400">Max Position Size:</span>
|
|
<span className="text-white font-mono">{maxPositionSize.toFixed(4)}</span>
|
|
</div>
|
|
<div className="flex justify-between text-sm">
|
|
<span className="text-gray-400">Est. Liquidation Price:</span>
|
|
<span className={`font-mono ${liquidationPrice > 0 ? 'text-red-400' : 'text-gray-500'}`}>
|
|
{liquidationPrice > 0 ? `$${liquidationPrice.toFixed(2)}` : '--'}
|
|
</span>
|
|
</div>
|
|
|
|
{/* Risk Management Metrics */}
|
|
{(enableStopLoss || enableTakeProfit) && (
|
|
<>
|
|
<div className="border-t border-gray-700 pt-3">
|
|
<div className="text-xs text-gray-500 mb-2 font-medium">RISK MANAGEMENT</div>
|
|
</div>
|
|
|
|
{enableStopLoss && potentialLoss > 0 && (
|
|
<div className="flex justify-between text-sm">
|
|
<span className="text-gray-400">Max Loss:</span>
|
|
<span className="font-mono text-red-400">-${potentialLoss.toFixed(2)}</span>
|
|
</div>
|
|
)}
|
|
|
|
{enableTakeProfit && potentialProfit > 0 && (
|
|
<div className="flex justify-between text-sm">
|
|
<span className="text-gray-400">Max Profit:</span>
|
|
<span className="font-mono text-green-400">+${potentialProfit.toFixed(2)}</span>
|
|
</div>
|
|
)}
|
|
|
|
{riskRewardRatio > 0 && (
|
|
<div className="flex justify-between text-sm">
|
|
<span className="text-gray-400">Risk/Reward:</span>
|
|
<span className={`font-mono font-bold ${
|
|
riskRewardRatio >= 2 ? 'text-green-400' :
|
|
riskRewardRatio >= 1 ? 'text-yellow-400' : 'text-red-400'
|
|
}`}>
|
|
1:{riskRewardRatio.toFixed(2)}
|
|
</span>
|
|
</div>
|
|
)}
|
|
</>
|
|
)}
|
|
</div>
|
|
|
|
{/* Open Positions */}
|
|
{positions.length > 0 && (
|
|
<div className="mb-6 p-4 bg-gray-800/30 rounded-lg">
|
|
<div className="flex items-center justify-between mb-3">
|
|
<div className="text-sm font-medium text-gray-400">Open Positions</div>
|
|
<div className="text-xs text-gray-500">{positions.length} position{positions.length !== 1 ? 's' : ''}</div>
|
|
</div>
|
|
<div className="space-y-2">
|
|
{positions.slice(0, 3).map((position, index) => (
|
|
<div key={index} className="flex items-center justify-between p-2 bg-gray-700/50 rounded">
|
|
<div className="flex-1">
|
|
<div className="flex items-center gap-2">
|
|
<span className="text-sm font-medium text-white">{position.symbol}</span>
|
|
<span className={`text-xs px-2 py-0.5 rounded ${
|
|
position.side === 'LONG' ? 'bg-green-500/20 text-green-400' : 'bg-red-500/20 text-red-400'
|
|
}`}>
|
|
{position.side}
|
|
</span>
|
|
</div>
|
|
<div className="text-xs text-gray-400">
|
|
Size: {position.size.toFixed(4)} | PnL: <span className={position.pnl >= 0 ? 'text-green-400' : 'text-red-400'}>
|
|
${position.pnl.toFixed(2)}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
<button
|
|
onClick={() => closePosition(position.symbol)}
|
|
disabled={closingPosition === position.symbol}
|
|
className="px-2 py-1 text-xs bg-red-600 hover:bg-red-700 disabled:bg-gray-600 text-white rounded transition-colors"
|
|
>
|
|
{closingPosition === position.symbol ? '...' : 'Close'}
|
|
</button>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Trade Button */}
|
|
<button
|
|
onClick={handleTrade}
|
|
disabled={loading || !positionSize || requiredMargin > accountData.freeCollateral}
|
|
className={`w-full py-3 px-4 rounded font-bold transition-colors ${
|
|
side === 'LONG'
|
|
? 'bg-green-500 hover:bg-green-600 disabled:bg-gray-600'
|
|
: 'bg-red-500 hover:bg-red-600 disabled:bg-gray-600'
|
|
} text-white disabled:cursor-not-allowed`}
|
|
>
|
|
{loading ? 'Executing...' : `${side} ${symbol}`}
|
|
</button>
|
|
|
|
{/* Result Display */}
|
|
{result && (
|
|
<div className={`mt-4 p-3 rounded text-sm ${
|
|
result.success ? 'bg-green-500/20 text-green-400' : 'bg-red-500/20 text-red-400'
|
|
}`}>
|
|
{result.success ? (
|
|
<div>
|
|
<div className="font-medium">Trade Executed Successfully!</div>
|
|
{result.txId && (
|
|
<div className="text-xs mt-1 opacity-75">TX: {result.txId}</div>
|
|
)}
|
|
</div>
|
|
) : (
|
|
<div className="font-medium">{result.error}</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|