feat: implement dynamic position calculator with leverage slider
- Added comprehensive PositionCalculator component with real-time PnL calculations - Implemented dynamic leverage adjustment with slider (1x to 100x) - Added investment amount input for position sizing - Integrated liquidation price calculations based on leverage and maintenance margin - Added real-time price fetching from multiple sources (CoinGecko, CoinCap, Binance) - Implemented automatic stop loss and take profit extraction from AI analysis - Added risk/reward ratio calculations and position metrics - Included trading fee calculations and net investment display - Added position type selection (Long/Short) with dynamic PnL calculation - Integrated high leverage warning system for risk management - Added advanced settings for customizable trading fees and maintenance margins - Automatically updates calculations when analysis parameters change - Supports both manual price input and real-time market data - Fully responsive design with gradient styling matching app theme
This commit is contained in:
@@ -2,6 +2,8 @@
|
|||||||
import React, { useState, useEffect } from 'react'
|
import React, { useState, useEffect } from 'react'
|
||||||
import TradeModal from './TradeModal'
|
import TradeModal from './TradeModal'
|
||||||
import ScreenshotGallery from './ScreenshotGallery'
|
import ScreenshotGallery from './ScreenshotGallery'
|
||||||
|
import PositionCalculator from './PositionCalculator'
|
||||||
|
import PriceFetcher from '../lib/price-fetcher'
|
||||||
|
|
||||||
const layouts = (process.env.NEXT_PUBLIC_TRADINGVIEW_LAYOUTS || 'ai,Diy module').split(',').map(l => l.trim())
|
const layouts = (process.env.NEXT_PUBLIC_TRADINGVIEW_LAYOUTS || 'ai,Diy module').split(',').map(l => l.trim())
|
||||||
const timeframes = [
|
const timeframes = [
|
||||||
@@ -65,6 +67,7 @@ export default function AIAnalysisPanel({ onAnalysisComplete }: AIAnalysisPanelP
|
|||||||
const [enlargedScreenshot, setEnlargedScreenshot] = useState<string | null>(null)
|
const [enlargedScreenshot, setEnlargedScreenshot] = useState<string | null>(null)
|
||||||
const [tradeModalOpen, setTradeModalOpen] = useState(false)
|
const [tradeModalOpen, setTradeModalOpen] = useState(false)
|
||||||
const [tradeModalData, setTradeModalData] = useState<any>(null)
|
const [tradeModalData, setTradeModalData] = useState<any>(null)
|
||||||
|
const [currentPrice, setCurrentPrice] = useState<number>(0)
|
||||||
|
|
||||||
// Helper function to safely render any value
|
// Helper function to safely render any value
|
||||||
const safeRender = (value: any): string => {
|
const safeRender = (value: any): string => {
|
||||||
@@ -118,6 +121,25 @@ export default function AIAnalysisPanel({ onAnalysisComplete }: AIAnalysisPanelP
|
|||||||
}
|
}
|
||||||
}, [eventSource])
|
}, [eventSource])
|
||||||
|
|
||||||
|
// Fetch current price when symbol changes
|
||||||
|
React.useEffect(() => {
|
||||||
|
const fetchPrice = async () => {
|
||||||
|
try {
|
||||||
|
const price = await PriceFetcher.getCurrentPrice(symbol)
|
||||||
|
setCurrentPrice(price)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching price:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchPrice()
|
||||||
|
|
||||||
|
// Set up periodic price updates every 30 seconds
|
||||||
|
const interval = setInterval(fetchPrice, 30000)
|
||||||
|
|
||||||
|
return () => clearInterval(interval)
|
||||||
|
}, [symbol])
|
||||||
|
|
||||||
const toggleLayout = (layout: string) => {
|
const toggleLayout = (layout: string) => {
|
||||||
setSelectedLayouts(prev =>
|
setSelectedLayouts(prev =>
|
||||||
prev.includes(layout)
|
prev.includes(layout)
|
||||||
@@ -1375,6 +1397,26 @@ export default function AIAnalysisPanel({ onAnalysisComplete }: AIAnalysisPanelP
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Position Calculator */}
|
||||||
|
{result && result.analysis && (
|
||||||
|
<div className="mt-6">
|
||||||
|
<PositionCalculator
|
||||||
|
analysis={result.analysis}
|
||||||
|
currentPrice={
|
||||||
|
result.analysis.entry?.price ||
|
||||||
|
result.analysis.entry ||
|
||||||
|
(typeof result.analysis.entry === 'string' ? parseFloat(result.analysis.entry.replace(/[^0-9.-]+/g, '')) : 0) ||
|
||||||
|
currentPrice ||
|
||||||
|
0
|
||||||
|
}
|
||||||
|
symbol={symbol}
|
||||||
|
onPositionChange={(position) => {
|
||||||
|
console.log('Position calculation updated:', position)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{result && !result.analysis && result.screenshots && (
|
{result && !result.analysis && result.screenshots && (
|
||||||
<div className="mt-6 p-4 bg-yellow-500/10 border border-yellow-500/30 rounded-lg">
|
<div className="mt-6 p-4 bg-yellow-500/10 border border-yellow-500/30 rounded-lg">
|
||||||
<h3 className="text-lg font-bold text-yellow-400 mb-2">Screenshots Captured</h3>
|
<h3 className="text-lg font-bold text-yellow-400 mb-2">Screenshots Captured</h3>
|
||||||
|
|||||||
407
components/PositionCalculator.tsx
Normal file
407
components/PositionCalculator.tsx
Normal file
@@ -0,0 +1,407 @@
|
|||||||
|
// Dynamic Position Calculator Component
|
||||||
|
"use client"
|
||||||
|
import React, { useState, useEffect } from 'react'
|
||||||
|
|
||||||
|
interface PositionCalculatorProps {
|
||||||
|
analysis?: any // The AI analysis results
|
||||||
|
currentPrice?: number
|
||||||
|
symbol?: string
|
||||||
|
onPositionChange?: (position: PositionCalculation) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PositionCalculation {
|
||||||
|
investmentAmount: number
|
||||||
|
leverage: number
|
||||||
|
positionSize: number
|
||||||
|
entryPrice: number
|
||||||
|
stopLoss: number
|
||||||
|
takeProfit: number
|
||||||
|
liquidationPrice: number
|
||||||
|
maxLoss: number
|
||||||
|
maxProfit: number
|
||||||
|
riskRewardRatio: number
|
||||||
|
marginRequired: number
|
||||||
|
tradingFee: number
|
||||||
|
netInvestment: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function PositionCalculator({
|
||||||
|
analysis,
|
||||||
|
currentPrice = 0,
|
||||||
|
symbol = 'BTCUSD',
|
||||||
|
onPositionChange
|
||||||
|
}: PositionCalculatorProps) {
|
||||||
|
const [investmentAmount, setInvestmentAmount] = useState<number>(100)
|
||||||
|
const [leverage, setLeverage] = useState<number>(10)
|
||||||
|
const [positionType, setPositionType] = useState<'long' | 'short'>('long')
|
||||||
|
const [calculation, setCalculation] = useState<PositionCalculation | null>(null)
|
||||||
|
const [showAdvanced, setShowAdvanced] = useState(false)
|
||||||
|
|
||||||
|
// Trading parameters
|
||||||
|
const [tradingFee, setTradingFee] = useState<number>(0.1) // 0.1% fee
|
||||||
|
const [maintenanceMargin, setMaintenanceMargin] = useState<number>(0.5) // 0.5% maintenance margin
|
||||||
|
|
||||||
|
// Calculate position metrics
|
||||||
|
const calculatePosition = () => {
|
||||||
|
if (!currentPrice || currentPrice <= 0) return null
|
||||||
|
|
||||||
|
const positionSize = investmentAmount * leverage
|
||||||
|
const marginRequired = investmentAmount
|
||||||
|
const fee = positionSize * (tradingFee / 100)
|
||||||
|
const netInvestment = investmentAmount + fee
|
||||||
|
|
||||||
|
// Get AI analysis targets if available
|
||||||
|
let entryPrice = currentPrice
|
||||||
|
let stopLoss = 0
|
||||||
|
let takeProfit = 0
|
||||||
|
|
||||||
|
if (analysis && analysis.analysis) {
|
||||||
|
// Try to extract entry, stop loss, and take profit from AI analysis
|
||||||
|
const analysisText = analysis.analysis.toLowerCase()
|
||||||
|
|
||||||
|
// Look for entry price
|
||||||
|
const entryMatch = analysisText.match(/entry[:\s]*[\$]?(\d+\.?\d*)/i)
|
||||||
|
if (entryMatch) {
|
||||||
|
entryPrice = parseFloat(entryMatch[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Look for stop loss
|
||||||
|
const stopMatch = analysisText.match(/stop[:\s]*[\$]?(\d+\.?\d*)/i)
|
||||||
|
if (stopMatch) {
|
||||||
|
stopLoss = parseFloat(stopMatch[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Look for take profit
|
||||||
|
const profitMatch = analysisText.match(/(?:take profit|target)[:\s]*[\$]?(\d+\.?\d*)/i)
|
||||||
|
if (profitMatch) {
|
||||||
|
takeProfit = parseFloat(profitMatch[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no specific targets found, use percentage-based defaults
|
||||||
|
if (!stopLoss) {
|
||||||
|
stopLoss = positionType === 'long'
|
||||||
|
? entryPrice * 0.95 // 5% stop loss for long
|
||||||
|
: entryPrice * 1.05 // 5% stop loss for short
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!takeProfit) {
|
||||||
|
takeProfit = positionType === 'long'
|
||||||
|
? entryPrice * 1.10 // 10% take profit for long
|
||||||
|
: entryPrice * 0.90 // 10% take profit for short
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Default targets if no analysis
|
||||||
|
stopLoss = positionType === 'long'
|
||||||
|
? currentPrice * 0.95
|
||||||
|
: currentPrice * 1.05
|
||||||
|
takeProfit = positionType === 'long'
|
||||||
|
? currentPrice * 1.10
|
||||||
|
: currentPrice * 0.90
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate liquidation price
|
||||||
|
const liquidationPrice = positionType === 'long'
|
||||||
|
? entryPrice * (1 - (1 / leverage) + (maintenanceMargin / 100))
|
||||||
|
: entryPrice * (1 + (1 / leverage) - (maintenanceMargin / 100))
|
||||||
|
|
||||||
|
// Calculate PnL
|
||||||
|
const stopLossChange = positionType === 'long'
|
||||||
|
? (stopLoss - entryPrice) / entryPrice
|
||||||
|
: (entryPrice - stopLoss) / entryPrice
|
||||||
|
const takeProfitChange = positionType === 'long'
|
||||||
|
? (takeProfit - entryPrice) / entryPrice
|
||||||
|
: (entryPrice - takeProfit) / entryPrice
|
||||||
|
|
||||||
|
const maxLoss = positionSize * Math.abs(stopLossChange)
|
||||||
|
const maxProfit = positionSize * Math.abs(takeProfitChange)
|
||||||
|
const riskRewardRatio = maxProfit / maxLoss
|
||||||
|
|
||||||
|
const result: PositionCalculation = {
|
||||||
|
investmentAmount,
|
||||||
|
leverage,
|
||||||
|
positionSize,
|
||||||
|
entryPrice,
|
||||||
|
stopLoss,
|
||||||
|
takeProfit,
|
||||||
|
liquidationPrice,
|
||||||
|
maxLoss,
|
||||||
|
maxProfit,
|
||||||
|
riskRewardRatio,
|
||||||
|
marginRequired,
|
||||||
|
tradingFee: fee,
|
||||||
|
netInvestment
|
||||||
|
}
|
||||||
|
|
||||||
|
setCalculation(result)
|
||||||
|
onPositionChange?.(result)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recalculate when parameters change
|
||||||
|
useEffect(() => {
|
||||||
|
calculatePosition()
|
||||||
|
}, [investmentAmount, leverage, positionType, currentPrice, analysis, tradingFee, maintenanceMargin])
|
||||||
|
|
||||||
|
const formatCurrency = (amount: number, decimals: number = 2) => {
|
||||||
|
return new Intl.NumberFormat('en-US', {
|
||||||
|
style: 'currency',
|
||||||
|
currency: 'USD',
|
||||||
|
minimumFractionDigits: decimals,
|
||||||
|
maximumFractionDigits: decimals
|
||||||
|
}).format(amount)
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatPrice = (price: number) => {
|
||||||
|
return new Intl.NumberFormat('en-US', {
|
||||||
|
style: 'decimal',
|
||||||
|
minimumFractionDigits: 2,
|
||||||
|
maximumFractionDigits: 2
|
||||||
|
}).format(price)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-gradient-to-br from-gray-800/50 to-gray-900/50 border border-gray-700 rounded-lg p-6">
|
||||||
|
<div className="flex items-center justify-between mb-6">
|
||||||
|
<h3 className="text-xl font-bold text-white flex items-center">
|
||||||
|
<span className="w-6 h-6 bg-gradient-to-br from-green-400 to-green-600 rounded-lg flex items-center justify-center mr-3 text-sm">
|
||||||
|
📊
|
||||||
|
</span>
|
||||||
|
Position Calculator
|
||||||
|
</h3>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowAdvanced(!showAdvanced)}
|
||||||
|
className="text-sm text-gray-400 hover:text-white transition-colors"
|
||||||
|
>
|
||||||
|
{showAdvanced ? '🔽 Hide Advanced' : '🔼 Show Advanced'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Input Controls */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
|
||||||
|
{/* Investment Amount */}
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-300 mb-2">
|
||||||
|
Investment Amount ($)
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={investmentAmount}
|
||||||
|
onChange={(e) => setInvestmentAmount(Number(e.target.value))}
|
||||||
|
className="w-full px-3 py-2 bg-gray-700 border border-gray-600 rounded-lg text-white focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
min="1"
|
||||||
|
step="1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Position Type */}
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-300 mb-2">
|
||||||
|
Position Type
|
||||||
|
</label>
|
||||||
|
<div className="flex space-x-2">
|
||||||
|
<button
|
||||||
|
onClick={() => setPositionType('long')}
|
||||||
|
className={`flex-1 px-3 py-2 rounded-lg text-sm font-medium transition-all ${
|
||||||
|
positionType === 'long'
|
||||||
|
? 'bg-green-600 text-white'
|
||||||
|
: 'bg-gray-700 text-gray-300 hover:bg-gray-600'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
📈 Long
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setPositionType('short')}
|
||||||
|
className={`flex-1 px-3 py-2 rounded-lg text-sm font-medium transition-all ${
|
||||||
|
positionType === 'short'
|
||||||
|
? 'bg-red-600 text-white'
|
||||||
|
: 'bg-gray-700 text-gray-300 hover:bg-gray-600'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
📉 Short
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Leverage Slider */}
|
||||||
|
<div className="md:col-span-2">
|
||||||
|
<label className="block text-sm font-medium text-gray-300 mb-2">
|
||||||
|
Leverage: {leverage}x
|
||||||
|
</label>
|
||||||
|
<div className="relative">
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min="1"
|
||||||
|
max="100"
|
||||||
|
value={leverage}
|
||||||
|
onChange={(e) => setLeverage(Number(e.target.value))}
|
||||||
|
className="w-full h-2 bg-gray-700 rounded-lg appearance-none cursor-pointer slider"
|
||||||
|
style={{
|
||||||
|
background: `linear-gradient(to right, #10B981 0%, #10B981 ${leverage}%, #374151 ${leverage}%, #374151 100%)`
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div className="flex justify-between text-xs text-gray-400 mt-1">
|
||||||
|
<span>1x</span>
|
||||||
|
<span>25x</span>
|
||||||
|
<span>50x</span>
|
||||||
|
<span>100x</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Advanced Settings */}
|
||||||
|
{showAdvanced && (
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6 p-4 bg-gray-800/30 rounded-lg border border-gray-600">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-300 mb-2">
|
||||||
|
Trading Fee (%)
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={tradingFee}
|
||||||
|
onChange={(e) => setTradingFee(Number(e.target.value))}
|
||||||
|
className="w-full px-3 py-2 bg-gray-700 border border-gray-600 rounded-lg text-white focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
min="0"
|
||||||
|
max="1"
|
||||||
|
step="0.01"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-300 mb-2">
|
||||||
|
Maintenance Margin (%)
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={maintenanceMargin}
|
||||||
|
onChange={(e) => setMaintenanceMargin(Number(e.target.value))}
|
||||||
|
className="w-full px-3 py-2 bg-gray-700 border border-gray-600 rounded-lg text-white focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
min="0.1"
|
||||||
|
max="5"
|
||||||
|
step="0.1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Calculation Results */}
|
||||||
|
{calculation && (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{/* Position Summary */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
|
<div className="bg-gray-800/30 rounded-lg p-4 border border-gray-600">
|
||||||
|
<div className="text-sm text-gray-400 mb-1">Position Size</div>
|
||||||
|
<div className="text-lg font-bold text-white">
|
||||||
|
{formatCurrency(calculation.positionSize)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="bg-gray-800/30 rounded-lg p-4 border border-gray-600">
|
||||||
|
<div className="text-sm text-gray-400 mb-1">Entry Price</div>
|
||||||
|
<div className="text-lg font-bold text-white">
|
||||||
|
${formatPrice(calculation.entryPrice)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="bg-gray-800/30 rounded-lg p-4 border border-gray-600">
|
||||||
|
<div className="text-sm text-gray-400 mb-1">Margin Required</div>
|
||||||
|
<div className="text-lg font-bold text-white">
|
||||||
|
{formatCurrency(calculation.marginRequired)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Risk Metrics */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div className="bg-red-900/20 border border-red-700 rounded-lg p-4">
|
||||||
|
<div className="text-sm text-red-400 mb-2">🚨 Risk Metrics</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-gray-300">Stop Loss:</span>
|
||||||
|
<span className="text-red-400 font-bold">${formatPrice(calculation.stopLoss)}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-gray-300">Max Loss:</span>
|
||||||
|
<span className="text-red-400 font-bold">{formatCurrency(calculation.maxLoss)}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-gray-300">Liquidation:</span>
|
||||||
|
<span className="text-red-500 font-bold">${formatPrice(calculation.liquidationPrice)}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-green-900/20 border border-green-700 rounded-lg p-4">
|
||||||
|
<div className="text-sm text-green-400 mb-2">💰 Profit Potential</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-gray-300">Take Profit:</span>
|
||||||
|
<span className="text-green-400 font-bold">${formatPrice(calculation.takeProfit)}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-gray-300">Max Profit:</span>
|
||||||
|
<span className="text-green-400 font-bold">{formatCurrency(calculation.maxProfit)}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-gray-300">Risk/Reward:</span>
|
||||||
|
<span className="text-green-400 font-bold">1:{calculation.riskRewardRatio.toFixed(2)}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Fee Breakdown */}
|
||||||
|
<div className="bg-gray-800/30 rounded-lg p-4 border border-gray-600">
|
||||||
|
<div className="text-sm text-gray-400 mb-2">💸 Fee Breakdown</div>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-gray-300">Trading Fee:</span>
|
||||||
|
<span className="text-yellow-400">{formatCurrency(calculation.tradingFee)}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-gray-300">Net Investment:</span>
|
||||||
|
<span className="text-white font-bold">{formatCurrency(calculation.netInvestment)}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-gray-300">Leverage:</span>
|
||||||
|
<span className="text-blue-400 font-bold">{leverage}x</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Risk Warning */}
|
||||||
|
{leverage > 50 && (
|
||||||
|
<div className="bg-red-900/30 border border-red-600 rounded-lg p-4">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<span className="text-red-400 text-lg">⚠️</span>
|
||||||
|
<span className="text-red-400 font-bold">High Leverage Warning</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-red-300 text-sm mt-2">
|
||||||
|
Using {leverage}x leverage is extremely risky. A small price movement against your position could result in liquidation.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<style jsx>{`
|
||||||
|
.slider::-webkit-slider-thumb {
|
||||||
|
appearance: none;
|
||||||
|
height: 20px;
|
||||||
|
width: 20px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #10B981;
|
||||||
|
cursor: pointer;
|
||||||
|
border: 2px solid #065F46;
|
||||||
|
box-shadow: 0 0 0 1px #065F46;
|
||||||
|
}
|
||||||
|
.slider::-moz-range-thumb {
|
||||||
|
height: 20px;
|
||||||
|
width: 20px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #10B981;
|
||||||
|
cursor: pointer;
|
||||||
|
border: 2px solid #065F46;
|
||||||
|
box-shadow: 0 0 0 1px #065F46;
|
||||||
|
}
|
||||||
|
`}</style>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
152
lib/price-fetcher.ts
Normal file
152
lib/price-fetcher.ts
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
// Price fetcher utility for getting current market prices
|
||||||
|
export class PriceFetcher {
|
||||||
|
private static cache = new Map<string, { price: number; timestamp: number }>()
|
||||||
|
private static readonly CACHE_DURATION = 30000 // 30 seconds
|
||||||
|
|
||||||
|
static async getCurrentPrice(symbol: string): Promise<number> {
|
||||||
|
const cacheKey = symbol.toUpperCase()
|
||||||
|
const cached = this.cache.get(cacheKey)
|
||||||
|
|
||||||
|
// Return cached price if recent
|
||||||
|
if (cached && Date.now() - cached.timestamp < this.CACHE_DURATION) {
|
||||||
|
return cached.price
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Try multiple price sources
|
||||||
|
let price = await this.fetchFromCoinGecko(symbol)
|
||||||
|
|
||||||
|
if (!price) {
|
||||||
|
price = await this.fetchFromCoinCap(symbol)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!price) {
|
||||||
|
price = await this.fetchFromBinance(symbol)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (price) {
|
||||||
|
this.cache.set(cacheKey, { price, timestamp: Date.now() })
|
||||||
|
return price
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching price:', error)
|
||||||
|
return cached?.price || 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async fetchFromCoinGecko(symbol: string): Promise<number | null> {
|
||||||
|
try {
|
||||||
|
const coinId = this.getCoinGeckoId(symbol)
|
||||||
|
if (!coinId) return null
|
||||||
|
|
||||||
|
const response = await fetch(
|
||||||
|
`https://api.coingecko.com/api/v3/simple/price?ids=${coinId}&vs_currencies=usd`,
|
||||||
|
{ cache: 'no-cache' }
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!response.ok) return null
|
||||||
|
|
||||||
|
const data = await response.json()
|
||||||
|
return data[coinId]?.usd || null
|
||||||
|
} catch (error) {
|
||||||
|
console.error('CoinGecko fetch error:', error)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async fetchFromCoinCap(symbol: string): Promise<number | null> {
|
||||||
|
try {
|
||||||
|
const asset = this.getCoinCapAsset(symbol)
|
||||||
|
if (!asset) return null
|
||||||
|
|
||||||
|
const response = await fetch(
|
||||||
|
`https://api.coincap.io/v2/assets/${asset}`,
|
||||||
|
{ cache: 'no-cache' }
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!response.ok) return null
|
||||||
|
|
||||||
|
const data = await response.json()
|
||||||
|
return parseFloat(data.data?.priceUsd) || null
|
||||||
|
} catch (error) {
|
||||||
|
console.error('CoinCap fetch error:', error)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async fetchFromBinance(symbol: string): Promise<number | null> {
|
||||||
|
try {
|
||||||
|
const binanceSymbol = this.getBinanceSymbol(symbol)
|
||||||
|
if (!binanceSymbol) return null
|
||||||
|
|
||||||
|
const response = await fetch(
|
||||||
|
`https://api.binance.com/api/v3/ticker/price?symbol=${binanceSymbol}`,
|
||||||
|
{ cache: 'no-cache' }
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!response.ok) return null
|
||||||
|
|
||||||
|
const data = await response.json()
|
||||||
|
return parseFloat(data.price) || null
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Binance fetch error:', error)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static getCoinGeckoId(symbol: string): string | null {
|
||||||
|
const mapping: Record<string, string> = {
|
||||||
|
'BTCUSD': 'bitcoin',
|
||||||
|
'ETHUSD': 'ethereum',
|
||||||
|
'SOLUSD': 'solana',
|
||||||
|
'SUIUSD': 'sui',
|
||||||
|
'ADAUSD': 'cardano',
|
||||||
|
'DOTUSD': 'polkadot',
|
||||||
|
'AVAXUSD': 'avalanche-2',
|
||||||
|
'LINKUSD': 'chainlink',
|
||||||
|
'MATICUSD': 'matic-network',
|
||||||
|
'UNIUSD': 'uniswap'
|
||||||
|
}
|
||||||
|
return mapping[symbol.toUpperCase()] || null
|
||||||
|
}
|
||||||
|
|
||||||
|
private static getCoinCapAsset(symbol: string): string | null {
|
||||||
|
const mapping: Record<string, string> = {
|
||||||
|
'BTCUSD': 'bitcoin',
|
||||||
|
'ETHUSD': 'ethereum',
|
||||||
|
'SOLUSD': 'solana',
|
||||||
|
'SUIUSD': 'sui',
|
||||||
|
'ADAUSD': 'cardano',
|
||||||
|
'DOTUSD': 'polkadot',
|
||||||
|
'AVAXUSD': 'avalanche',
|
||||||
|
'LINKUSD': 'chainlink',
|
||||||
|
'MATICUSD': 'polygon',
|
||||||
|
'UNIUSD': 'uniswap'
|
||||||
|
}
|
||||||
|
return mapping[symbol.toUpperCase()] || null
|
||||||
|
}
|
||||||
|
|
||||||
|
private static getBinanceSymbol(symbol: string): string | null {
|
||||||
|
const mapping: Record<string, string> = {
|
||||||
|
'BTCUSD': 'BTCUSDT',
|
||||||
|
'ETHUSD': 'ETHUSDT',
|
||||||
|
'SOLUSD': 'SOLUSDT',
|
||||||
|
'SUIUSD': 'SUIUSDT',
|
||||||
|
'ADAUSD': 'ADAUSDT',
|
||||||
|
'DOTUSD': 'DOTUSDT',
|
||||||
|
'AVAXUSD': 'AVAXUSDT',
|
||||||
|
'LINKUSD': 'LINKUSDT',
|
||||||
|
'MATICUSD': 'MATICUSDT',
|
||||||
|
'UNIUSD': 'UNIUSDT'
|
||||||
|
}
|
||||||
|
return mapping[symbol.toUpperCase()] || null
|
||||||
|
}
|
||||||
|
|
||||||
|
static clearCache(): void {
|
||||||
|
this.cache.clear()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PriceFetcher
|
||||||
Reference in New Issue
Block a user