feat: Implement Jupiter-style trading interface with token selection
- Add 'You're paying' and 'You're receiving' sections with proper token dropdowns - Implement balance display and MAX button functionality - Add automatic receiving amount calculation based on paying amount - Enhance token selector with icons, names, and balance information - Improve leverage position value calculations and risk warnings - Update trade execution to use new paying/receiving token structure - Maintain all existing functionality including stop loss, take profit, and position management This creates a more intuitive and professional trading interface that matches Jupiter's UX patterns.
This commit is contained in:
16
.eslintrc.json
Normal file
16
.eslintrc.json
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"extends": [
|
||||||
|
"next/core-web-vitals",
|
||||||
|
"next/typescript"
|
||||||
|
],
|
||||||
|
"rules": {
|
||||||
|
"@typescript-eslint/no-unused-vars": "warn",
|
||||||
|
"@typescript-eslint/no-explicit-any": "warn",
|
||||||
|
"react/no-unescaped-entities": "warn",
|
||||||
|
"@next/next/no-html-link-for-pages": "warn",
|
||||||
|
"@next/next/no-img-element": "warn",
|
||||||
|
"react-hooks/exhaustive-deps": "warn",
|
||||||
|
"@typescript-eslint/no-require-imports": "warn",
|
||||||
|
"prefer-const": "warn"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import React, { useState } from 'react'
|
import React, { useState } from 'react'
|
||||||
import AIAnalysisPanel from '../../components/AIAnalysisPanel.tsx'
|
import AIAnalysisPanel from '../../components/AIAnalysisPanel.tsx'
|
||||||
|
import TradeExecutionPanel from '../../components/TradeExecutionPanel.js'
|
||||||
|
|
||||||
export default function AnalysisPage() {
|
export default function AnalysisPage() {
|
||||||
const [analysisResult, setAnalysisResult] = useState(null)
|
const [analysisResult, setAnalysisResult] = useState(null)
|
||||||
@@ -15,13 +16,70 @@ export default function AnalysisPage() {
|
|||||||
<div className="space-y-8">
|
<div className="space-y-8">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-3xl font-bold text-white">AI Analysis</h1>
|
<h1 className="text-3xl font-bold text-white">AI Analysis & Trading</h1>
|
||||||
<p className="text-gray-400 mt-2">Get comprehensive market insights powered by AI analysis</p>
|
<p className="text-gray-400 mt-2">Get comprehensive market insights powered by AI analysis and execute trades</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="max-w-6xl mx-auto">
|
<div className="grid grid-cols-1 xl:grid-cols-2 gap-8">
|
||||||
<AIAnalysisPanel onAnalysisComplete={handleAnalysisComplete} />
|
{/* Left Column - AI Analysis */}
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="bg-gray-900/50 rounded-lg p-6 border border-gray-800">
|
||||||
|
<h2 className="text-xl font-semibold text-white mb-4">📊 AI Market Analysis</h2>
|
||||||
|
<AIAnalysisPanel onAnalysisComplete={handleAnalysisComplete} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Right Column - Trading Panel */}
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="bg-gray-900/50 rounded-lg p-6 border border-gray-800">
|
||||||
|
<h2 className="text-xl font-semibold text-white mb-4">💰 Execute Trade</h2>
|
||||||
|
<TradeExecutionPanel
|
||||||
|
analysis={analysisResult}
|
||||||
|
symbol={currentSymbol}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Analysis Summary */}
|
||||||
|
{analysisResult && (
|
||||||
|
<div className="bg-blue-900/20 rounded-lg p-6 border border-blue-800">
|
||||||
|
<h3 className="text-lg font-semibold text-blue-400 mb-3">🎯 Analysis Summary</h3>
|
||||||
|
<div className="space-y-2 text-sm">
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-gray-400">Symbol:</span>
|
||||||
|
<span className="text-white font-medium">{currentSymbol}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-gray-400">Sentiment:</span>
|
||||||
|
<span className={`font-medium ${
|
||||||
|
analysisResult.sentiment === 'BULLISH' ? 'text-green-400' :
|
||||||
|
analysisResult.sentiment === 'BEARISH' ? 'text-red-400' : 'text-yellow-400'
|
||||||
|
}`}>
|
||||||
|
{analysisResult.sentiment}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{analysisResult.entryPrice && (
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-gray-400">Entry Price:</span>
|
||||||
|
<span className="text-white font-medium">${analysisResult.entryPrice}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{analysisResult.stopLoss && (
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-gray-400">Stop Loss:</span>
|
||||||
|
<span className="text-red-400 font-medium">${analysisResult.stopLoss}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{analysisResult.takeProfit && (
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-gray-400">Take Profit:</span>
|
||||||
|
<span className="text-green-400 font-medium">${analysisResult.takeProfit}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
'use client'
|
|
||||||
import React from 'react'
|
|
||||||
import TradingChart from '../../components/TradingChart'
|
|
||||||
|
|
||||||
export default function SimpleChartTest() {
|
|
||||||
return (
|
|
||||||
<div className="min-h-screen bg-gray-900 p-4">
|
|
||||||
<div className="max-w-7xl mx-auto">
|
|
||||||
<h1 className="text-2xl font-bold text-white mb-6">Chart Test</h1>
|
|
||||||
<TradingChart />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
595
app/chart-trading-demo/page.tsx
Normal file
595
app/chart-trading-demo/page.tsx
Normal file
@@ -0,0 +1,595 @@
|
|||||||
|
'use client'
|
||||||
|
import React, { useState, useRef, useEffect } from 'react'
|
||||||
|
import SimpleChart from '../../components/SimpleChart'
|
||||||
|
|
||||||
|
interface Position {
|
||||||
|
id: string
|
||||||
|
symbol: string
|
||||||
|
side: 'BUY' | 'SELL'
|
||||||
|
amount: number
|
||||||
|
entryPrice: number
|
||||||
|
stopLoss: number
|
||||||
|
takeProfit: number
|
||||||
|
currentPrice: number
|
||||||
|
unrealizedPnl: number
|
||||||
|
leverage: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ChartTradingDemo() {
|
||||||
|
const [selectedSymbol, setSelectedSymbol] = useState('SOL')
|
||||||
|
const [leverage, setLeverage] = useState(1)
|
||||||
|
const [payingAmount, setPayingAmount] = useState('')
|
||||||
|
const [payingToken, setPayingToken] = useState('USDC')
|
||||||
|
const [receivingAmount, setReceivingAmount] = useState('')
|
||||||
|
const [stopLoss, setStopLoss] = useState('')
|
||||||
|
const [takeProfit, setTakeProfit] = useState('')
|
||||||
|
const [isSymbolDropdownOpen, setIsSymbolDropdownOpen] = useState(false)
|
||||||
|
const [isPayingTokenDropdownOpen, setIsPayingTokenDropdownOpen] = useState(false)
|
||||||
|
const dropdownRef = useRef<HTMLDivElement>(null)
|
||||||
|
const payingTokenDropdownRef = useRef<HTMLDivElement>(null)
|
||||||
|
const [positions, setPositions] = useState<Position[]>([
|
||||||
|
// Mock position data for demo
|
||||||
|
{
|
||||||
|
id: 'demo_pos_1',
|
||||||
|
symbol: 'SOL/USDC',
|
||||||
|
side: 'BUY' as 'BUY' | 'SELL',
|
||||||
|
amount: 0.5,
|
||||||
|
entryPrice: 165.50,
|
||||||
|
stopLoss: 160.00,
|
||||||
|
takeProfit: 170.00,
|
||||||
|
currentPrice: 166.21,
|
||||||
|
unrealizedPnl: 0.355,
|
||||||
|
leverage: 2,
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
const symbols = [
|
||||||
|
{ symbol: 'SOL', name: 'Solana', price: 166.21, change: 2.45, icon: '🟣' },
|
||||||
|
{ symbol: 'BTC', name: 'Bitcoin', price: 42150.00, change: -1.23, icon: '🟠' },
|
||||||
|
{ symbol: 'ETH', name: 'Ethereum', price: 2580.50, change: 3.12, icon: '⬜' },
|
||||||
|
{ symbol: 'BONK', name: 'Bonk', price: 0.00003456, change: 15.67, icon: '🐕' },
|
||||||
|
{ symbol: 'JUP', name: 'Jupiter', price: 0.8234, change: 5.43, icon: '🪐' },
|
||||||
|
{ symbol: 'WIF', name: 'dogwifhat', price: 3.42, change: -8.21, icon: '🧢' },
|
||||||
|
]
|
||||||
|
|
||||||
|
const payingTokens = [
|
||||||
|
{ symbol: 'USDC', name: 'USD Coin', balance: 1234.56, icon: '💵' },
|
||||||
|
{ symbol: 'USDT', name: 'Tether', balance: 892.34, icon: '💰' },
|
||||||
|
{ symbol: 'SOL', name: 'Solana', balance: 5.42, icon: '🟣' },
|
||||||
|
{ symbol: 'BTC', name: 'Bitcoin', balance: 0.025, icon: '🟠' },
|
||||||
|
{ symbol: 'ETH', name: 'Ethereum', balance: 1.85, icon: '⬜' },
|
||||||
|
]
|
||||||
|
|
||||||
|
// Close dropdown when clicking outside
|
||||||
|
useEffect(() => {
|
||||||
|
function handleClickOutside(event: MouseEvent) {
|
||||||
|
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
|
||||||
|
setIsSymbolDropdownOpen(false)
|
||||||
|
}
|
||||||
|
if (payingTokenDropdownRef.current && !payingTokenDropdownRef.current.contains(event.target as Node)) {
|
||||||
|
setIsPayingTokenDropdownOpen(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('mousedown', handleClickOutside)
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('mousedown', handleClickOutside)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const getCurrentSymbolData = () => {
|
||||||
|
return symbols.find(s => s.symbol === selectedSymbol) || symbols[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
const getCurrentPayingTokenData = () => {
|
||||||
|
return payingTokens.find(t => t.symbol === payingToken) || payingTokens[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate receiving amount based on paying amount
|
||||||
|
useEffect(() => {
|
||||||
|
if (payingAmount && payingToken === 'USDC') {
|
||||||
|
const symbolPrice = getCurrentSymbolData().price
|
||||||
|
const receiving = parseFloat(payingAmount) / symbolPrice
|
||||||
|
setReceivingAmount(receiving.toFixed(6))
|
||||||
|
} else if (payingAmount && payingToken === selectedSymbol) {
|
||||||
|
setReceivingAmount(payingAmount)
|
||||||
|
} else {
|
||||||
|
setReceivingAmount('')
|
||||||
|
}
|
||||||
|
}, [payingAmount, payingToken, selectedSymbol])
|
||||||
|
|
||||||
|
const handleTrade = (side: 'BUY' | 'SELL') => {
|
||||||
|
const amount = parseFloat(payingAmount)
|
||||||
|
const symbolData = getCurrentSymbolData()
|
||||||
|
|
||||||
|
if (!amount || amount <= 0) {
|
||||||
|
alert('Please enter a valid amount')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const newPosition: Position = {
|
||||||
|
id: `pos_${Date.now()}`,
|
||||||
|
symbol: `${selectedSymbol}/USDC`,
|
||||||
|
side,
|
||||||
|
amount,
|
||||||
|
entryPrice: symbolData.price,
|
||||||
|
stopLoss: stopLoss ? parseFloat(stopLoss) : symbolData.price * (side === 'BUY' ? 0.95 : 1.05),
|
||||||
|
takeProfit: takeProfit ? parseFloat(takeProfit) : symbolData.price * (side === 'BUY' ? 1.05 : 0.95),
|
||||||
|
currentPrice: symbolData.price,
|
||||||
|
unrealizedPnl: 0,
|
||||||
|
leverage,
|
||||||
|
}
|
||||||
|
|
||||||
|
setPositions(prev => [...prev, newPosition])
|
||||||
|
setPayingAmount('')
|
||||||
|
setReceivingAmount('')
|
||||||
|
setStopLoss('')
|
||||||
|
setTakeProfit('')
|
||||||
|
alert(`${side} order placed (demo)`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleClosePosition = (positionId: string) => {
|
||||||
|
setPositions(prev => prev.filter(pos => pos.id !== positionId))
|
||||||
|
alert('Position closed (demo)')
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<style jsx>{`
|
||||||
|
.slider::-webkit-slider-thumb {
|
||||||
|
appearance: none;
|
||||||
|
height: 20px;
|
||||||
|
width: 20px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #eab308;
|
||||||
|
border: 2px solid #1f2937;
|
||||||
|
cursor: pointer;
|
||||||
|
box-shadow: 0 0 0 1px #374151;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider::-moz-range-thumb {
|
||||||
|
height: 20px;
|
||||||
|
width: 20px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #eab308;
|
||||||
|
border: 2px solid #1f2937;
|
||||||
|
cursor: pointer;
|
||||||
|
box-shadow: 0 0 0 1px #374151;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider::-webkit-slider-track {
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider::-moz-range-track {
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
`}</style>
|
||||||
|
<div className="h-screen bg-gray-900 flex flex-col">
|
||||||
|
{/* Top Bar */}
|
||||||
|
<div className="bg-gray-800 border-b border-gray-700 p-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center space-x-6">
|
||||||
|
<h1 className="text-xl font-bold text-white">Chart Trading Terminal</h1>
|
||||||
|
|
||||||
|
{/* Symbol Selector Dropdown */}
|
||||||
|
<div className="relative" ref={dropdownRef}>
|
||||||
|
<button
|
||||||
|
onClick={() => setIsSymbolDropdownOpen(!isSymbolDropdownOpen)}
|
||||||
|
className="flex items-center space-x-3 bg-gray-700 hover:bg-gray-600 rounded-lg px-4 py-2 text-white transition-all border border-gray-600"
|
||||||
|
>
|
||||||
|
<span className="text-lg">{getCurrentSymbolData().icon}</span>
|
||||||
|
<div className="text-left">
|
||||||
|
<div className="font-medium">{selectedSymbol}/USDC</div>
|
||||||
|
<div className="text-xs text-gray-400">{getCurrentSymbolData().name}</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-right">
|
||||||
|
<div className="text-sm font-mono">${getCurrentSymbolData().price.toLocaleString()}</div>
|
||||||
|
<div className={`text-xs ${getCurrentSymbolData().change >= 0 ? 'text-green-400' : 'text-red-400'}`}>
|
||||||
|
{getCurrentSymbolData().change >= 0 ? '+' : ''}{getCurrentSymbolData().change}%
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<svg className={`w-4 h-4 transition-transform ${isSymbolDropdownOpen ? 'rotate-180' : ''}`} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Dropdown Menu */}
|
||||||
|
{isSymbolDropdownOpen && (
|
||||||
|
<div className="absolute top-full left-0 mt-2 w-80 bg-gray-800 border border-gray-600 rounded-lg shadow-xl z-50 max-h-80 overflow-y-auto">
|
||||||
|
<div className="p-2">
|
||||||
|
<div className="text-xs text-gray-400 uppercase tracking-wide px-3 py-2 border-b border-gray-700">
|
||||||
|
Select Trading Pair
|
||||||
|
</div>
|
||||||
|
{symbols.map(({ symbol, name, price, change, icon }) => (
|
||||||
|
<button
|
||||||
|
key={symbol}
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedSymbol(symbol)
|
||||||
|
setIsSymbolDropdownOpen(false)
|
||||||
|
}}
|
||||||
|
className={`w-full flex items-center space-x-3 px-3 py-3 text-left rounded-lg transition-all ${
|
||||||
|
selectedSymbol === symbol
|
||||||
|
? 'bg-blue-600/20 border border-blue-500/30'
|
||||||
|
: 'hover:bg-gray-700'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<span className="text-lg">{icon}</span>
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<div className="font-medium text-white">{symbol}/USDC</div>
|
||||||
|
<div className="text-xs text-gray-400">{name}</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-right">
|
||||||
|
<div className="text-sm font-mono text-white">
|
||||||
|
${price < 1 ? price.toFixed(8) : price.toLocaleString()}
|
||||||
|
</div>
|
||||||
|
<div className={`text-xs ${change >= 0 ? 'text-green-400' : 'text-red-400'}`}>
|
||||||
|
{change >= 0 ? '+' : ''}{change}%
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{selectedSymbol === symbol && (
|
||||||
|
<div className="w-2 h-2 bg-blue-500 rounded-full"></div>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Market Status */}
|
||||||
|
<div className="flex items-center space-x-4 text-sm">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<div className="w-2 h-2 bg-green-400 rounded-full animate-pulse"></div>
|
||||||
|
<span className="text-gray-300">Demo Mode</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-gray-400">
|
||||||
|
{new Date().toLocaleTimeString()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Main Trading Interface */}
|
||||||
|
<div className="flex-1 flex">
|
||||||
|
{/* Chart Area (70% width) */}
|
||||||
|
<div className="flex-1 p-4">
|
||||||
|
<SimpleChart symbol={selectedSymbol} positions={positions} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Trading Panel (30% width) */}
|
||||||
|
<div className="w-96 border-l border-gray-700 p-4 space-y-4">
|
||||||
|
<div className="bg-gray-800 rounded-lg p-4">
|
||||||
|
<h3 className="text-white text-lg font-semibold mb-4">{selectedSymbol}/USDC</h3>
|
||||||
|
<div className="text-2xl font-bold text-white mb-2">
|
||||||
|
${getCurrentSymbolData().price.toLocaleString()}
|
||||||
|
</div>
|
||||||
|
<div className={`text-sm ${getCurrentSymbolData().change >= 0 ? 'text-green-400' : 'text-red-400'}`}>
|
||||||
|
{getCurrentSymbolData().change >= 0 ? '+' : ''}{getCurrentSymbolData().change}%
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Leverage Selector */}
|
||||||
|
<div className="mt-6">
|
||||||
|
<div className="flex items-center justify-between mb-3">
|
||||||
|
<label className="block text-gray-400 text-sm">Leverage</label>
|
||||||
|
<div className="text-sm text-yellow-400 font-medium">
|
||||||
|
{leverage}x
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Leverage Slider */}
|
||||||
|
<div className="relative">
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min="1"
|
||||||
|
max="100"
|
||||||
|
value={leverage}
|
||||||
|
onChange={(e) => setLeverage(parseInt(e.target.value))}
|
||||||
|
className="w-full h-2 bg-gray-700 rounded-lg appearance-none cursor-pointer slider"
|
||||||
|
style={{
|
||||||
|
background: `linear-gradient(to right, #eab308 0%, #eab308 ${((leverage - 1) / 99) * 100}%, #374151 ${((leverage - 1) / 99) * 100}%, #374151 100%)`
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Leverage Marks */}
|
||||||
|
<div className="flex justify-between mt-2 px-1">
|
||||||
|
{[1, 2, 5, 10, 20, 50, 100].map((mark) => (
|
||||||
|
<button
|
||||||
|
key={mark}
|
||||||
|
onClick={() => setLeverage(mark)}
|
||||||
|
className={`text-xs transition-all ${
|
||||||
|
leverage === mark
|
||||||
|
? 'text-yellow-400 font-medium'
|
||||||
|
: 'text-gray-500 hover:text-gray-300'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{mark}x
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Leverage Warning */}
|
||||||
|
{leverage > 10 && (
|
||||||
|
<div className="mt-2 flex items-center space-x-2 text-xs text-orange-400">
|
||||||
|
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
|
||||||
|
<path fillRule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clipRule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
<span>High leverage increases liquidation risk</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Quick Trade Buttons */}
|
||||||
|
<div className="mt-6 space-y-3">
|
||||||
|
<button
|
||||||
|
onClick={() => handleTrade('BUY')}
|
||||||
|
disabled={!payingAmount}
|
||||||
|
className="w-full bg-green-600 hover:bg-green-700 disabled:bg-gray-600 disabled:cursor-not-allowed text-white py-3 px-4 rounded-lg font-medium transition-all"
|
||||||
|
>
|
||||||
|
Long/Buy {leverage > 1 && `(${leverage}x)`}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => handleTrade('SELL')}
|
||||||
|
disabled={!payingAmount}
|
||||||
|
className="w-full bg-red-600 hover:bg-red-700 disabled:bg-gray-600 disabled:cursor-not-allowed text-white py-3 px-4 rounded-lg font-medium transition-all"
|
||||||
|
>
|
||||||
|
Short/Sell {leverage > 1 && `(${leverage}x)`}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Trade Form */}
|
||||||
|
<div className="mt-6 space-y-4">
|
||||||
|
{/* You're Paying Section */}
|
||||||
|
<div>
|
||||||
|
<label className="block text-gray-400 text-sm mb-2">You're paying</label>
|
||||||
|
<div className="bg-gray-700 rounded-lg p-3 border border-gray-600">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={payingAmount}
|
||||||
|
onChange={(e) => setPayingAmount(e.target.value)}
|
||||||
|
placeholder="0.00"
|
||||||
|
step="0.01"
|
||||||
|
min="0"
|
||||||
|
className="bg-transparent text-white text-lg font-medium focus:outline-none flex-1 mr-3"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Paying Token Selector */}
|
||||||
|
<div className="relative" ref={payingTokenDropdownRef}>
|
||||||
|
<button
|
||||||
|
onClick={() => setIsPayingTokenDropdownOpen(!isPayingTokenDropdownOpen)}
|
||||||
|
className="flex items-center space-x-2 bg-gray-600 hover:bg-gray-500 rounded-lg px-3 py-2 transition-all"
|
||||||
|
>
|
||||||
|
<span className="text-sm">{getCurrentPayingTokenData().icon}</span>
|
||||||
|
<span className="text-white font-medium">{payingToken}</span>
|
||||||
|
<svg className={`w-4 h-4 text-gray-300 transition-transform ${isPayingTokenDropdownOpen ? 'rotate-180' : ''}`} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Paying Token Dropdown */}
|
||||||
|
{isPayingTokenDropdownOpen && (
|
||||||
|
<div className="absolute top-full right-0 mt-2 w-64 bg-gray-800 border border-gray-600 rounded-lg shadow-xl z-50">
|
||||||
|
<div className="p-2">
|
||||||
|
{payingTokens.map(({ symbol, name, balance, icon }) => (
|
||||||
|
<button
|
||||||
|
key={symbol}
|
||||||
|
onClick={() => {
|
||||||
|
setPayingToken(symbol)
|
||||||
|
setIsPayingTokenDropdownOpen(false)
|
||||||
|
}}
|
||||||
|
className={`w-full flex items-center justify-between px-3 py-2 text-left rounded-lg transition-all ${
|
||||||
|
payingToken === symbol
|
||||||
|
? 'bg-blue-600/20 border border-blue-500/30'
|
||||||
|
: 'hover:bg-gray-700'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<span className="text-sm">{icon}</span>
|
||||||
|
<div>
|
||||||
|
<div className="text-white font-medium">{symbol}</div>
|
||||||
|
<div className="text-xs text-gray-400">{name}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-gray-400">
|
||||||
|
{balance.toLocaleString()}
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Balance and MAX button */}
|
||||||
|
<div className="flex items-center justify-between mt-2 text-xs text-gray-400">
|
||||||
|
<span>Balance: {getCurrentPayingTokenData().balance.toLocaleString()} {payingToken}</span>
|
||||||
|
<button
|
||||||
|
onClick={() => setPayingAmount(getCurrentPayingTokenData().balance.toString())}
|
||||||
|
className="text-blue-400 hover:text-blue-300"
|
||||||
|
>
|
||||||
|
MAX
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{leverage > 1 && payingAmount && (
|
||||||
|
<div className="text-xs text-yellow-400 mt-1">
|
||||||
|
Position value: ${(parseFloat(payingAmount) * leverage).toLocaleString()} ({leverage}x leverage)
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* You're Receiving Section */}
|
||||||
|
<div>
|
||||||
|
<label className="block text-gray-400 text-sm mb-2">You're receiving</label>
|
||||||
|
<div className="bg-gray-700 rounded-lg p-3 border border-gray-600">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="text-lg font-medium text-white">
|
||||||
|
{receivingAmount || '0.00'}
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-2 bg-gray-600 rounded-lg px-3 py-2">
|
||||||
|
<span className="text-sm">{getCurrentSymbolData().icon}</span>
|
||||||
|
<span className="text-white font-medium">{selectedSymbol}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-gray-400 mt-2">
|
||||||
|
Rate: 1 {payingToken} = {payingToken === 'USDC' ? (1 / getCurrentSymbolData().price).toFixed(8) : '1.00'} {selectedSymbol}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-3">
|
||||||
|
<div>
|
||||||
|
<label className="block text-gray-400 text-sm mb-2">Stop Loss</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={stopLoss}
|
||||||
|
onChange={(e) => setStopLoss(e.target.value)}
|
||||||
|
placeholder="Optional"
|
||||||
|
step="0.01"
|
||||||
|
min="0"
|
||||||
|
className="w-full bg-gray-700 text-white rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-gray-400 text-sm mb-2">Take Profit</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={takeProfit}
|
||||||
|
onChange={(e) => setTakeProfit(e.target.value)}
|
||||||
|
placeholder="Optional"
|
||||||
|
step="0.01"
|
||||||
|
min="0"
|
||||||
|
className="w-full bg-gray-700 text-white rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Risk Information */}
|
||||||
|
{payingAmount && leverage > 1 && (
|
||||||
|
<div className="bg-yellow-900/20 border border-yellow-600/20 rounded-lg p-3">
|
||||||
|
<div className="text-yellow-400 text-xs font-medium mb-1">⚠️ Leveraged Position Risk</div>
|
||||||
|
<div className="text-xs text-gray-300">
|
||||||
|
Position value: ${(parseFloat(payingAmount) * leverage).toLocaleString()} ({leverage}x leverage)
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-gray-300">
|
||||||
|
Liquidation risk: High with {leverage}x leverage
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Bottom Panel - Positions */}
|
||||||
|
<div className="border-t border-gray-700 bg-gray-800">
|
||||||
|
<div className="p-4">
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<div className="flex space-x-6">
|
||||||
|
<button className="text-white font-medium border-b-2 border-blue-500 pb-2">
|
||||||
|
Positions ({positions.length})
|
||||||
|
</button>
|
||||||
|
<button className="text-gray-400 hover:text-white pb-2">
|
||||||
|
Orders (0)
|
||||||
|
</button>
|
||||||
|
<button className="text-gray-400 hover:text-white pb-2">
|
||||||
|
History
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{positions.length > 0 && (
|
||||||
|
<div className="text-sm text-gray-400">
|
||||||
|
Total P&L: <span className={`${
|
||||||
|
positions.reduce((sum, pos) => sum + pos.unrealizedPnl, 0) >= 0
|
||||||
|
? 'text-green-400' : 'text-red-400'
|
||||||
|
}`}>
|
||||||
|
{positions.reduce((sum, pos) => sum + pos.unrealizedPnl, 0) >= 0 ? '+' : ''}
|
||||||
|
${positions.reduce((sum, pos) => sum + pos.unrealizedPnl, 0).toFixed(2)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Positions Table */}
|
||||||
|
<div className="max-h-48 overflow-y-auto">
|
||||||
|
{positions.length === 0 ? (
|
||||||
|
<div className="text-center py-8 text-gray-400">
|
||||||
|
No open positions
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-2">
|
||||||
|
{positions.map((position: Position) => (
|
||||||
|
<div
|
||||||
|
key={position.id}
|
||||||
|
className="bg-gray-900 rounded-lg p-4 flex items-center justify-between"
|
||||||
|
>
|
||||||
|
<div className="flex items-center space-x-4">
|
||||||
|
<div className={`w-3 h-3 rounded-full ${
|
||||||
|
position.side === 'BUY' ? 'bg-green-400' : 'bg-red-400'
|
||||||
|
}`}></div>
|
||||||
|
<div>
|
||||||
|
<div className="text-white font-medium">
|
||||||
|
{position.symbol} • {position.side}
|
||||||
|
{position.leverage > 1 && (
|
||||||
|
<span className="ml-2 text-yellow-400 text-sm">
|
||||||
|
{position.leverage}x
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-gray-400">
|
||||||
|
Size: {position.amount} • Entry: ${position.entryPrice?.toFixed(2)}
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-red-400">
|
||||||
|
SL: ${position.stopLoss.toFixed(2)}
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-green-400">
|
||||||
|
TP: ${position.takeProfit.toFixed(2)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="text-right">
|
||||||
|
<div className="text-white font-medium">
|
||||||
|
${(position.amount * position.currentPrice).toFixed(2)}
|
||||||
|
</div>
|
||||||
|
<div className={`text-sm ${
|
||||||
|
position.unrealizedPnl >= 0 ? 'text-green-400' : 'text-red-400'
|
||||||
|
}`}>
|
||||||
|
{position.unrealizedPnl >= 0 ? '+' : ''}${(position.unrealizedPnl || 0).toFixed(2)}
|
||||||
|
</div>
|
||||||
|
{position.leverage > 1 && (
|
||||||
|
<div className="text-xs text-yellow-400">
|
||||||
|
{position.leverage}x Leveraged
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex space-x-2">
|
||||||
|
<button
|
||||||
|
onClick={() => handleClosePosition(position.id)}
|
||||||
|
className="px-3 py-1 bg-red-600 text-white rounded text-sm hover:bg-red-700 transition-all"
|
||||||
|
>
|
||||||
|
Close
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
25
app/error.tsx
Normal file
25
app/error.tsx
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
'use client'
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
export default function Error({
|
||||||
|
error,
|
||||||
|
reset,
|
||||||
|
}: {
|
||||||
|
error: Error & { digest?: string }
|
||||||
|
reset: () => void
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-900 flex items-center justify-center">
|
||||||
|
<div className="text-center">
|
||||||
|
<h1 className="text-4xl font-bold text-red-400 mb-4">Something went wrong!</h1>
|
||||||
|
<p className="text-gray-400 mb-8">An error occurred while loading this page.</p>
|
||||||
|
<button
|
||||||
|
onClick={reset}
|
||||||
|
className="bg-blue-600 hover:bg-blue-700 text-white font-medium py-3 px-6 rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
Try again
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
21
app/not-found.tsx
Normal file
21
app/not-found.tsx
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
'use client'
|
||||||
|
import React from 'react'
|
||||||
|
import Link from 'next/link'
|
||||||
|
|
||||||
|
export default function NotFound() {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-900 flex items-center justify-center">
|
||||||
|
<div className="text-center">
|
||||||
|
<h1 className="text-6xl font-bold text-white mb-4">404</h1>
|
||||||
|
<h2 className="text-2xl font-semibold text-gray-300 mb-8">Page Not Found</h2>
|
||||||
|
<p className="text-gray-400 mb-8">The page you're looking for doesn't exist.</p>
|
||||||
|
<Link
|
||||||
|
href="/"
|
||||||
|
className="bg-blue-600 hover:bg-blue-700 text-white font-medium py-3 px-6 rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
Go Back Home
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
53
app/page.js
53
app/page.js
@@ -54,6 +54,59 @@ export default function HomePage() {
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Featured: Chart Trading Demo */}
|
||||||
|
<div className="card card-gradient">
|
||||||
|
<div className="flex items-center justify-between mb-6">
|
||||||
|
<div>
|
||||||
|
<h2 className="text-xl font-semibold text-white mb-2">🚀 Professional Chart Trading</h2>
|
||||||
|
<p className="text-gray-400">Advanced trading interface with leverage, charting, and position management</p>
|
||||||
|
</div>
|
||||||
|
<div className="text-right">
|
||||||
|
<span className="inline-block px-3 py-1 bg-yellow-600/20 text-yellow-400 rounded-full text-sm font-medium">NEW</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-6">
|
||||||
|
<div className="flex items-center space-x-3">
|
||||||
|
<div className="w-8 h-8 bg-green-500/20 rounded-full flex items-center justify-center">
|
||||||
|
<span className="text-green-400 text-sm">📈</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-white font-medium">Real-time Charts</div>
|
||||||
|
<div className="text-gray-400 text-sm">Professional candlestick visualization</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center space-x-3">
|
||||||
|
<div className="w-8 h-8 bg-yellow-500/20 rounded-full flex items-center justify-center">
|
||||||
|
<span className="text-yellow-400 text-sm">⚡</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-white font-medium">Leverage Trading</div>
|
||||||
|
<div className="text-gray-400 text-sm">1x to 100x leverage options</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center space-x-3">
|
||||||
|
<div className="w-8 h-8 bg-blue-500/20 rounded-full flex items-center justify-center">
|
||||||
|
<span className="text-blue-400 text-sm">🎯</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-white font-medium">Position Management</div>
|
||||||
|
<div className="text-gray-400 text-sm">Stop loss & take profit</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a
|
||||||
|
href="/chart-trading-demo"
|
||||||
|
className="inline-flex items-center px-6 py-3 bg-gradient-to-r from-blue-600 to-purple-600 text-white rounded-lg hover:from-blue-700 hover:to-purple-700 transition-all transform hover:scale-105 font-medium"
|
||||||
|
>
|
||||||
|
<span className="mr-2">🚀</span>
|
||||||
|
Launch Chart Trading Terminal
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,115 +0,0 @@
|
|||||||
'use client'
|
|
||||||
import React, { useEffect, useRef, useState } from 'react'
|
|
||||||
|
|
||||||
export default function SimpleChart() {
|
|
||||||
const chartContainerRef = useRef<HTMLDivElement>(null)
|
|
||||||
const [status, setStatus] = useState('Initializing...')
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!chartContainerRef.current) return
|
|
||||||
|
|
||||||
const initChart = async () => {
|
|
||||||
try {
|
|
||||||
setStatus('Loading lightweight-charts...')
|
|
||||||
console.log('Importing lightweight-charts...')
|
|
||||||
|
|
||||||
const LightweightCharts = await import('lightweight-charts')
|
|
||||||
console.log('Lightweight charts imported successfully')
|
|
||||||
setStatus('Creating chart...')
|
|
||||||
|
|
||||||
const { createChart, ColorType, CandlestickSeries } = LightweightCharts
|
|
||||||
|
|
||||||
const chart = createChart(chartContainerRef.current!, {
|
|
||||||
layout: {
|
|
||||||
background: { type: ColorType.Solid, color: '#1a1a1a' },
|
|
||||||
textColor: '#ffffff',
|
|
||||||
},
|
|
||||||
width: chartContainerRef.current!.clientWidth || 800,
|
|
||||||
height: 400,
|
|
||||||
grid: {
|
|
||||||
vertLines: { color: 'rgba(42, 46, 57, 0.5)' },
|
|
||||||
horzLines: { color: 'rgba(42, 46, 57, 0.5)' },
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
setStatus('Adding candlestick series...')
|
|
||||||
console.log('Chart created, adding candlestick series...')
|
|
||||||
|
|
||||||
const candlestickSeries = chart.addSeries(CandlestickSeries, {
|
|
||||||
upColor: '#26a69a',
|
|
||||||
downColor: '#ef5350',
|
|
||||||
borderDownColor: '#ef5350',
|
|
||||||
borderUpColor: '#26a69a',
|
|
||||||
wickDownColor: '#ef5350',
|
|
||||||
wickUpColor: '#26a69a',
|
|
||||||
})
|
|
||||||
|
|
||||||
// Generate sample data
|
|
||||||
const data = []
|
|
||||||
const baseTime = new Date(Date.now() - 100 * 60 * 1000) // 100 minutes ago
|
|
||||||
let price = 166.5
|
|
||||||
|
|
||||||
for (let i = 0; i < 100; i++) {
|
|
||||||
const currentTime = new Date(baseTime.getTime() + i * 60 * 1000) // 1 minute intervals
|
|
||||||
const timeString = currentTime.toISOString().split('T')[0] // YYYY-MM-DD format
|
|
||||||
const change = (Math.random() - 0.5) * 2 // Random price change
|
|
||||||
const open = price
|
|
||||||
const close = price + change
|
|
||||||
const high = Math.max(open, close) + Math.random() * 1
|
|
||||||
const low = Math.min(open, close) - Math.random() * 1
|
|
||||||
|
|
||||||
data.push({
|
|
||||||
time: timeString,
|
|
||||||
open: Number(open.toFixed(2)),
|
|
||||||
high: Number(high.toFixed(2)),
|
|
||||||
low: Number(low.toFixed(2)),
|
|
||||||
close: Number(close.toFixed(2)),
|
|
||||||
})
|
|
||||||
|
|
||||||
price = close
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('Setting chart data...', data.length, 'points')
|
|
||||||
candlestickSeries.setData(data)
|
|
||||||
|
|
||||||
setStatus('Chart loaded successfully!')
|
|
||||||
console.log('Chart created successfully!')
|
|
||||||
|
|
||||||
// Handle resize
|
|
||||||
const handleResize = () => {
|
|
||||||
if (chartContainerRef.current) {
|
|
||||||
chart.applyOptions({
|
|
||||||
width: chartContainerRef.current.clientWidth,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
window.addEventListener('resize', handleResize)
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
window.removeEventListener('resize', handleResize)
|
|
||||||
chart.remove()
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
|
||||||
console.error('Error creating chart:', error)
|
|
||||||
setStatus(`Error: ${errorMessage}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
initChart()
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="min-h-screen bg-gray-900 p-8">
|
|
||||||
<h1 className="text-white text-2xl mb-4">Lightweight Charts Test</h1>
|
|
||||||
<div className="text-gray-400 mb-4">Status: {status}</div>
|
|
||||||
<div
|
|
||||||
ref={chartContainerRef}
|
|
||||||
className="bg-gray-800 rounded w-full h-96"
|
|
||||||
style={{ minHeight: '400px' }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,100 +1,289 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import React, { useRef, useEffect } from 'react'
|
import React, { useRef, useEffect, useState } from 'react'
|
||||||
|
|
||||||
|
interface Position {
|
||||||
|
id: string
|
||||||
|
symbol: string
|
||||||
|
side: 'BUY' | 'SELL'
|
||||||
|
amount: number
|
||||||
|
entryPrice: number
|
||||||
|
stopLoss: number
|
||||||
|
takeProfit: number
|
||||||
|
currentPrice: number
|
||||||
|
unrealizedPnl: number
|
||||||
|
leverage: number
|
||||||
|
}
|
||||||
|
|
||||||
interface SimpleChartProps {
|
interface SimpleChartProps {
|
||||||
symbol?: string
|
symbol?: string
|
||||||
positions?: any[]
|
positions?: Position[]
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CandleData {
|
||||||
|
open: number
|
||||||
|
high: number
|
||||||
|
low: number
|
||||||
|
close: number
|
||||||
|
time: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function SimpleChart({ symbol = 'SOL/USDC', positions = [] }: SimpleChartProps) {
|
export default function SimpleChart({ symbol = 'SOL/USDC', positions = [] }: SimpleChartProps) {
|
||||||
const canvasRef = useRef<HTMLCanvasElement>(null)
|
const canvasRef = useRef<HTMLCanvasElement>(null)
|
||||||
|
const [candleData, setCandleData] = useState<CandleData[]>([])
|
||||||
|
const [timeframe, setTimeframe] = useState('5m')
|
||||||
|
|
||||||
|
const timeframes = ['1m', '5m', '15m', '1h', '4h', '1d']
|
||||||
|
|
||||||
|
// Generate realistic candlestick data
|
||||||
|
const generateCandleData = React.useCallback(() => {
|
||||||
|
const data: CandleData[] = []
|
||||||
|
const basePrice = symbol === 'SOL' ? 166.5 : symbol === 'BTC' ? 42150 : 2580
|
||||||
|
let currentPrice = basePrice
|
||||||
|
const now = Date.now()
|
||||||
|
|
||||||
|
for (let i = 60; i >= 0; i--) {
|
||||||
|
const timeOffset = i * 5 * 60 * 1000 // 5-minute intervals
|
||||||
|
const time = now - timeOffset
|
||||||
|
|
||||||
|
const volatility = basePrice * 0.002 // 0.2% volatility
|
||||||
|
const open = currentPrice
|
||||||
|
const change = (Math.random() - 0.5) * volatility * 2
|
||||||
|
const close = open + change
|
||||||
|
const high = Math.max(open, close) + Math.random() * volatility
|
||||||
|
const low = Math.min(open, close) - Math.random() * volatility
|
||||||
|
|
||||||
|
data.push({ open, high, low, close, time })
|
||||||
|
currentPrice = close
|
||||||
|
}
|
||||||
|
|
||||||
|
return data
|
||||||
|
}, [symbol])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setCandleData(generateCandleData())
|
||||||
|
}, [symbol, timeframe, generateCandleData])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const canvas = canvasRef.current
|
const canvas = canvasRef.current
|
||||||
if (!canvas) return
|
if (!canvas || candleData.length === 0) return
|
||||||
|
|
||||||
const ctx = canvas.getContext('2d')
|
const ctx = canvas.getContext('2d')
|
||||||
if (!ctx) return
|
if (!ctx) return
|
||||||
|
|
||||||
// Set canvas size
|
// Set canvas size for high DPI displays
|
||||||
canvas.width = 800
|
const rect = canvas.getBoundingClientRect()
|
||||||
canvas.height = 400
|
const dpr = window.devicePixelRatio || 1
|
||||||
|
canvas.width = rect.width * dpr
|
||||||
|
canvas.height = rect.height * dpr
|
||||||
|
ctx.scale(dpr, dpr)
|
||||||
|
|
||||||
// Clear canvas
|
const width = rect.width
|
||||||
ctx.fillStyle = '#1a1a1a'
|
const height = rect.height
|
||||||
ctx.fillRect(0, 0, canvas.width, canvas.height)
|
|
||||||
|
// Clear canvas with dark background
|
||||||
|
ctx.fillStyle = '#0f0f0f'
|
||||||
|
ctx.fillRect(0, 0, width, height)
|
||||||
|
|
||||||
|
// Calculate price range
|
||||||
|
const prices = candleData.flatMap(d => [d.high, d.low])
|
||||||
|
const maxPrice = Math.max(...prices)
|
||||||
|
const minPrice = Math.min(...prices)
|
||||||
|
const priceRange = maxPrice - minPrice
|
||||||
|
const padding = priceRange * 0.1
|
||||||
|
|
||||||
|
// Chart dimensions
|
||||||
|
const chartLeft = 60
|
||||||
|
const chartRight = width - 20
|
||||||
|
const chartTop = 40
|
||||||
|
const chartBottom = height - 60
|
||||||
|
const chartWidth = chartRight - chartLeft
|
||||||
|
const chartHeight = chartBottom - chartTop
|
||||||
|
|
||||||
// Draw grid
|
// Draw grid
|
||||||
ctx.strokeStyle = '#333'
|
ctx.strokeStyle = '#1a1a1a'
|
||||||
ctx.lineWidth = 1
|
ctx.lineWidth = 1
|
||||||
|
|
||||||
// Vertical lines
|
// Horizontal grid lines (price levels)
|
||||||
for (let x = 0; x < canvas.width; x += 40) {
|
const priceStep = (maxPrice - minPrice + padding * 2) / 8
|
||||||
|
for (let i = 0; i <= 8; i++) {
|
||||||
|
const price = minPrice - padding + i * priceStep
|
||||||
|
const y = chartBottom - ((price - (minPrice - padding)) / (maxPrice - minPrice + padding * 2)) * chartHeight
|
||||||
|
|
||||||
ctx.beginPath()
|
ctx.beginPath()
|
||||||
ctx.moveTo(x, 0)
|
ctx.moveTo(chartLeft, y)
|
||||||
ctx.lineTo(x, canvas.height)
|
ctx.lineTo(chartRight, y)
|
||||||
|
ctx.stroke()
|
||||||
|
|
||||||
|
// Price labels
|
||||||
|
ctx.fillStyle = '#666'
|
||||||
|
ctx.font = '11px Arial'
|
||||||
|
ctx.textAlign = 'right'
|
||||||
|
ctx.fillText(price.toFixed(2), chartLeft - 5, y + 4)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vertical grid lines (time)
|
||||||
|
const timeStep = chartWidth / 12
|
||||||
|
for (let i = 0; i <= 12; i++) {
|
||||||
|
const x = chartLeft + i * timeStep
|
||||||
|
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.moveTo(x, chartTop)
|
||||||
|
ctx.lineTo(x, chartBottom)
|
||||||
ctx.stroke()
|
ctx.stroke()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Horizontal lines
|
// Draw candlesticks
|
||||||
for (let y = 0; y < canvas.height; y += 40) {
|
const candleWidth = Math.max(2, chartWidth / candleData.length - 2)
|
||||||
|
|
||||||
|
candleData.forEach((candle, index) => {
|
||||||
|
const x = chartLeft + (index / (candleData.length - 1)) * chartWidth
|
||||||
|
|
||||||
|
const openY = chartBottom - ((candle.open - (minPrice - padding)) / (maxPrice - minPrice + padding * 2)) * chartHeight
|
||||||
|
const closeY = chartBottom - ((candle.close - (minPrice - padding)) / (maxPrice - minPrice + padding * 2)) * chartHeight
|
||||||
|
const highY = chartBottom - ((candle.high - (minPrice - padding)) / (maxPrice - minPrice + padding * 2)) * chartHeight
|
||||||
|
const lowY = chartBottom - ((candle.low - (minPrice - padding)) / (maxPrice - minPrice + padding * 2)) * chartHeight
|
||||||
|
|
||||||
|
const isGreen = candle.close > candle.open
|
||||||
|
const color = isGreen ? '#26a69a' : '#ef5350'
|
||||||
|
|
||||||
|
ctx.strokeStyle = color
|
||||||
|
ctx.fillStyle = color
|
||||||
|
ctx.lineWidth = 1
|
||||||
|
|
||||||
|
// Draw wick (high-low line)
|
||||||
ctx.beginPath()
|
ctx.beginPath()
|
||||||
ctx.moveTo(0, y)
|
ctx.moveTo(x, highY)
|
||||||
ctx.lineTo(canvas.width, y)
|
ctx.lineTo(x, lowY)
|
||||||
ctx.stroke()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Draw sample candlesticks
|
|
||||||
const candleWidth = 20
|
|
||||||
const basePrice = 166.5
|
|
||||||
const priceScale = 2
|
|
||||||
|
|
||||||
for (let i = 0; i < 30; i++) {
|
|
||||||
const x = 40 + i * 25
|
|
||||||
const open = basePrice + (Math.random() - 0.5) * 10
|
|
||||||
const close = open + (Math.random() - 0.5) * 5
|
|
||||||
const high = Math.max(open, close) + Math.random() * 3
|
|
||||||
const low = Math.min(open, close) - Math.random() * 3
|
|
||||||
|
|
||||||
const openY = canvas.height - (open - 150) * priceScale
|
|
||||||
const closeY = canvas.height - (close - 150) * priceScale
|
|
||||||
const highY = canvas.height - (high - 150) * priceScale
|
|
||||||
const lowY = canvas.height - (low - 150) * priceScale
|
|
||||||
|
|
||||||
// Determine color
|
|
||||||
const isGreen = close > open
|
|
||||||
ctx.fillStyle = isGreen ? '#26a69a' : '#ef5350'
|
|
||||||
ctx.strokeStyle = isGreen ? '#26a69a' : '#ef5350'
|
|
||||||
|
|
||||||
// Draw wick
|
|
||||||
ctx.lineWidth = 2
|
|
||||||
ctx.beginPath()
|
|
||||||
ctx.moveTo(x + candleWidth / 2, highY)
|
|
||||||
ctx.lineTo(x + candleWidth / 2, lowY)
|
|
||||||
ctx.stroke()
|
ctx.stroke()
|
||||||
|
|
||||||
// Draw body
|
// Draw candle body
|
||||||
const bodyTop = Math.min(openY, closeY)
|
const bodyTop = Math.min(openY, closeY)
|
||||||
const bodyHeight = Math.abs(closeY - openY)
|
const bodyHeight = Math.abs(closeY - openY)
|
||||||
ctx.fillRect(x, bodyTop, candleWidth, Math.max(bodyHeight, 2))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Draw price labels
|
if (isGreen) {
|
||||||
ctx.fillStyle = '#ffffff'
|
ctx.strokeRect(x - candleWidth / 2, bodyTop, candleWidth, Math.max(bodyHeight, 1))
|
||||||
|
} else {
|
||||||
|
ctx.fillRect(x - candleWidth / 2, bodyTop, candleWidth, Math.max(bodyHeight, 1))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Draw position overlays
|
||||||
|
positions.forEach((position) => {
|
||||||
|
if (!position.symbol.includes(symbol.replace('/USDC', ''))) return
|
||||||
|
|
||||||
|
const entryY = chartBottom - ((position.entryPrice - (minPrice - padding)) / (maxPrice - minPrice + padding * 2)) * chartHeight
|
||||||
|
const stopLossY = chartBottom - ((position.stopLoss - (minPrice - padding)) / (maxPrice - minPrice + padding * 2)) * chartHeight
|
||||||
|
const takeProfitY = chartBottom - ((position.takeProfit - (minPrice - padding)) / (maxPrice - minPrice + padding * 2)) * chartHeight
|
||||||
|
|
||||||
|
// Entry price line
|
||||||
|
ctx.strokeStyle = position.side === 'BUY' ? '#26a69a' : '#ef5350'
|
||||||
|
ctx.lineWidth = 2
|
||||||
|
ctx.setLineDash([5, 5])
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.moveTo(chartLeft, entryY)
|
||||||
|
ctx.lineTo(chartRight, entryY)
|
||||||
|
ctx.stroke()
|
||||||
|
|
||||||
|
// Stop loss line
|
||||||
|
ctx.strokeStyle = '#ef5350'
|
||||||
|
ctx.lineWidth = 1
|
||||||
|
ctx.setLineDash([3, 3])
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.moveTo(chartLeft, stopLossY)
|
||||||
|
ctx.lineTo(chartRight, stopLossY)
|
||||||
|
ctx.stroke()
|
||||||
|
|
||||||
|
// Take profit line
|
||||||
|
ctx.strokeStyle = '#26a69a'
|
||||||
|
ctx.lineWidth = 1
|
||||||
|
ctx.setLineDash([3, 3])
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.moveTo(chartLeft, takeProfitY)
|
||||||
|
ctx.lineTo(chartRight, takeProfitY)
|
||||||
|
ctx.stroke()
|
||||||
|
|
||||||
|
ctx.setLineDash([]) // Reset line dash
|
||||||
|
})
|
||||||
|
|
||||||
|
// Draw current price line
|
||||||
|
const currentPrice = candleData[candleData.length - 1]?.close || 0
|
||||||
|
const currentPriceY = chartBottom - ((currentPrice - (minPrice - padding)) / (maxPrice - minPrice + padding * 2)) * chartHeight
|
||||||
|
|
||||||
|
ctx.strokeStyle = '#ffa726'
|
||||||
|
ctx.lineWidth = 2
|
||||||
|
ctx.setLineDash([])
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.moveTo(chartLeft, currentPriceY)
|
||||||
|
ctx.lineTo(chartRight, currentPriceY)
|
||||||
|
ctx.stroke()
|
||||||
|
|
||||||
|
// Current price label
|
||||||
|
ctx.fillStyle = '#ffa726'
|
||||||
|
ctx.fillRect(chartRight - 60, currentPriceY - 10, 60, 20)
|
||||||
|
ctx.fillStyle = '#000'
|
||||||
ctx.font = '12px Arial'
|
ctx.font = '12px Arial'
|
||||||
ctx.fillText(`${symbol} - $${basePrice.toFixed(2)}`, 10, 30)
|
ctx.textAlign = 'center'
|
||||||
ctx.fillStyle = '#26a69a'
|
ctx.fillText(currentPrice.toFixed(2), chartRight - 30, currentPriceY + 4)
|
||||||
ctx.fillText('+2.45%', 10, 50)
|
|
||||||
|
|
||||||
}, [symbol, positions])
|
// Chart title
|
||||||
|
ctx.fillStyle = '#ffffff'
|
||||||
|
ctx.font = 'bold 16px Arial'
|
||||||
|
ctx.textAlign = 'left'
|
||||||
|
ctx.fillText(`${symbol} - ${timeframe}`, 20, 25)
|
||||||
|
|
||||||
|
}, [symbol, positions, candleData, timeframe])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full">
|
<div className="w-full bg-gray-900 rounded-lg border border-gray-700">
|
||||||
<canvas
|
{/* Chart Controls */}
|
||||||
ref={canvasRef}
|
<div className="flex items-center justify-between p-3 border-b border-gray-700">
|
||||||
className="w-full h-96 bg-gray-800 rounded border border-gray-600"
|
<div className="flex items-center space-x-4">
|
||||||
style={{ maxWidth: '100%', height: '400px' }}
|
<h3 className="text-white font-medium">{symbol}</h3>
|
||||||
/>
|
<div className="flex space-x-1">
|
||||||
|
{timeframes.map(tf => (
|
||||||
|
<button
|
||||||
|
key={tf}
|
||||||
|
onClick={() => setTimeframe(tf)}
|
||||||
|
className={`px-2 py-1 text-xs rounded transition-all ${
|
||||||
|
timeframe === tf
|
||||||
|
? 'bg-blue-600 text-white'
|
||||||
|
: 'bg-gray-700 text-gray-300 hover:bg-gray-600'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{tf}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="text-sm text-gray-400">
|
||||||
|
{candleData.length > 0 && (
|
||||||
|
<span>
|
||||||
|
Last: ${candleData[candleData.length - 1]?.close.toFixed(2)} •
|
||||||
|
24h Vol: ${(Math.random() * 1000000).toFixed(0)}M
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Chart Canvas */}
|
||||||
|
<div className="relative">
|
||||||
|
<canvas
|
||||||
|
ref={canvasRef}
|
||||||
|
className="w-full"
|
||||||
|
style={{ height: '400px', display: 'block' }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Legend */}
|
||||||
|
{positions.length > 0 && (
|
||||||
|
<div className="absolute top-2 right-2 bg-gray-800/90 rounded p-2 text-xs space-y-1">
|
||||||
|
<div className="text-yellow-400">— Current Price</div>
|
||||||
|
<div className="text-green-400">⋯ Take Profit</div>
|
||||||
|
<div className="text-red-400">⋯ Stop Loss</div>
|
||||||
|
<div className="text-blue-400">⋯ Entry Price</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,9 +48,27 @@ export default function TradeExecutionPanel({ analysis, symbol = 'SOL' }) {
|
|||||||
setTakeProfit(analysis.takeProfits.tp1.price.toString())
|
setTakeProfit(analysis.takeProfits.tp1.price.toString())
|
||||||
setEnableTakeProfit(true)
|
setEnableTakeProfit(true)
|
||||||
}
|
}
|
||||||
|
// Set trade type based on analysis recommendation
|
||||||
|
if (analysis.recommendation === 'BUY' || analysis.sentiment === 'BULLISH') {
|
||||||
|
setTradeType('BUY')
|
||||||
|
} else if (analysis.recommendation === 'SELL' || analysis.sentiment === 'BEARISH') {
|
||||||
|
setTradeType('SELL')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [analysis])
|
}, [analysis])
|
||||||
|
|
||||||
|
// Initialize coin selection based on symbol prop
|
||||||
|
useEffect(() => {
|
||||||
|
if (symbol && availableCoins.find(coin => coin.symbol === symbol)) {
|
||||||
|
setFromCoin(symbol)
|
||||||
|
setPerpCoin(symbol)
|
||||||
|
// If it's not a stablecoin, trade it against USDC
|
||||||
|
if (symbol !== 'USDC' && symbol !== 'USDT') {
|
||||||
|
setToCoin('USDC')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [symbol])
|
||||||
|
|
||||||
// Get recommended price from analysis
|
// Get recommended price from analysis
|
||||||
const getRecommendedPrice = () => {
|
const getRecommendedPrice = () => {
|
||||||
if (!analysis) return null
|
if (!analysis) return null
|
||||||
|
|||||||
@@ -1,254 +0,0 @@
|
|||||||
'use client'
|
|
||||||
import React, { useEffect, useRef, useState } from 'react'
|
|
||||||
|
|
||||||
interface Position {
|
|
||||||
id: string
|
|
||||||
symbol: string
|
|
||||||
side: 'LONG' | 'SHORT'
|
|
||||||
size: number
|
|
||||||
entryPrice: number
|
|
||||||
stopLoss?: number
|
|
||||||
takeProfit?: number
|
|
||||||
pnl: number
|
|
||||||
pnlPercentage: number
|
|
||||||
}
|
|
||||||
|
|
||||||
interface TradingChartProps {
|
|
||||||
symbol?: string
|
|
||||||
positions?: Position[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function TradingChart({ symbol = 'SOL/USDC', positions = [] }: TradingChartProps) {
|
|
||||||
const chartContainerRef = useRef<HTMLDivElement>(null)
|
|
||||||
const chart = useRef<any>(null)
|
|
||||||
const candlestickSeries = useRef<any>(null)
|
|
||||||
const positionLines = useRef<any[]>([])
|
|
||||||
const [isLoading, setIsLoading] = useState(true)
|
|
||||||
|
|
||||||
// Initialize chart with dynamic import
|
|
||||||
useEffect(() => {
|
|
||||||
if (!chartContainerRef.current) return
|
|
||||||
|
|
||||||
const initChart = async () => {
|
|
||||||
try {
|
|
||||||
// Dynamic import to avoid SSR issues
|
|
||||||
const LightweightCharts = await import('lightweight-charts')
|
|
||||||
const { createChart, ColorType, CrosshairMode, LineStyle, CandlestickSeries } = LightweightCharts
|
|
||||||
|
|
||||||
chart.current = createChart(chartContainerRef.current!, {
|
|
||||||
layout: {
|
|
||||||
background: { type: ColorType.Solid, color: '#1a1a1a' },
|
|
||||||
textColor: '#ffffff',
|
|
||||||
},
|
|
||||||
width: chartContainerRef.current!.clientWidth,
|
|
||||||
height: 600,
|
|
||||||
grid: {
|
|
||||||
vertLines: { color: 'rgba(42, 46, 57, 0.5)' },
|
|
||||||
horzLines: { color: 'rgba(42, 46, 57, 0.5)' },
|
|
||||||
},
|
|
||||||
crosshair: {
|
|
||||||
mode: CrosshairMode.Normal,
|
|
||||||
},
|
|
||||||
rightPriceScale: {
|
|
||||||
borderColor: 'rgba(197, 203, 206, 0.8)',
|
|
||||||
},
|
|
||||||
timeScale: {
|
|
||||||
borderColor: 'rgba(197, 203, 206, 0.8)',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
// Create candlestick series
|
|
||||||
candlestickSeries.current = chart.current.addSeries(CandlestickSeries, {
|
|
||||||
upColor: '#26a69a',
|
|
||||||
downColor: '#ef5350',
|
|
||||||
borderDownColor: '#ef5350',
|
|
||||||
borderUpColor: '#26a69a',
|
|
||||||
wickDownColor: '#ef5350',
|
|
||||||
wickUpColor: '#26a69a',
|
|
||||||
})
|
|
||||||
|
|
||||||
// Generate sample data
|
|
||||||
console.log('Generating sample data...')
|
|
||||||
const data = generateSampleData()
|
|
||||||
console.log('Sample data generated:', data.length, 'points')
|
|
||||||
console.log('First few data points:', data.slice(0, 3))
|
|
||||||
|
|
||||||
console.log('Setting chart data...')
|
|
||||||
candlestickSeries.current.setData(data)
|
|
||||||
console.log('Chart data set successfully')
|
|
||||||
|
|
||||||
// Add position overlays
|
|
||||||
console.log('Adding position overlays...')
|
|
||||||
addPositionOverlays(LineStyle)
|
|
||||||
console.log('Position overlays added')
|
|
||||||
|
|
||||||
console.log('Chart initialization complete')
|
|
||||||
setIsLoading(false)
|
|
||||||
|
|
||||||
// Handle resize
|
|
||||||
const handleResize = () => {
|
|
||||||
if (chart.current && chartContainerRef.current) {
|
|
||||||
chart.current.applyOptions({
|
|
||||||
width: chartContainerRef.current.clientWidth,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
window.addEventListener('resize', handleResize)
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
window.removeEventListener('resize', handleResize)
|
|
||||||
if (chart.current) {
|
|
||||||
chart.current.remove()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to initialize chart:', error)
|
|
||||||
console.error('Error details:', {
|
|
||||||
message: error instanceof Error ? error.message : String(error),
|
|
||||||
stack: error instanceof Error ? error.stack : undefined
|
|
||||||
})
|
|
||||||
setIsLoading(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const addPositionOverlays = (LineStyle: any) => {
|
|
||||||
if (!chart.current) return
|
|
||||||
|
|
||||||
// Clear existing lines
|
|
||||||
positionLines.current.forEach(line => {
|
|
||||||
if (line && chart.current) {
|
|
||||||
chart.current.removePriceLine(line)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
positionLines.current = []
|
|
||||||
|
|
||||||
// Add new position lines
|
|
||||||
positions.forEach(position => {
|
|
||||||
// Entry price line
|
|
||||||
const entryLine = chart.current.addPriceLine({
|
|
||||||
price: position.entryPrice,
|
|
||||||
color: '#2196F3',
|
|
||||||
lineWidth: 2,
|
|
||||||
lineStyle: LineStyle.Solid,
|
|
||||||
axisLabelVisible: true,
|
|
||||||
title: `Entry: $${position.entryPrice.toFixed(2)}`,
|
|
||||||
})
|
|
||||||
positionLines.current.push(entryLine)
|
|
||||||
|
|
||||||
// Stop loss line
|
|
||||||
if (position.stopLoss) {
|
|
||||||
const slLine = chart.current.addPriceLine({
|
|
||||||
price: position.stopLoss,
|
|
||||||
color: '#f44336',
|
|
||||||
lineWidth: 2,
|
|
||||||
lineStyle: LineStyle.Dashed,
|
|
||||||
axisLabelVisible: true,
|
|
||||||
title: `SL: $${position.stopLoss.toFixed(2)}`,
|
|
||||||
})
|
|
||||||
positionLines.current.push(slLine)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Take profit line
|
|
||||||
if (position.takeProfit) {
|
|
||||||
const tpLine = chart.current.addPriceLine({
|
|
||||||
price: position.takeProfit,
|
|
||||||
color: '#4caf50',
|
|
||||||
lineWidth: 2,
|
|
||||||
lineStyle: LineStyle.Dashed,
|
|
||||||
axisLabelVisible: true,
|
|
||||||
title: `TP: $${position.takeProfit.toFixed(2)}`,
|
|
||||||
})
|
|
||||||
positionLines.current.push(tpLine)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const generateSampleData = () => {
|
|
||||||
const data = []
|
|
||||||
const basePrice = 166.5
|
|
||||||
let currentPrice = basePrice
|
|
||||||
const baseDate = new Date()
|
|
||||||
|
|
||||||
for (let i = 0; i < 100; i++) {
|
|
||||||
// Generate data for the last 100 days, one point per day
|
|
||||||
const currentTime = new Date(baseDate.getTime() - (99 - i) * 24 * 60 * 60 * 1000)
|
|
||||||
const timeString = currentTime.toISOString().split('T')[0] // YYYY-MM-DD format
|
|
||||||
const volatility = 0.02
|
|
||||||
const change = (Math.random() - 0.5) * volatility * currentPrice
|
|
||||||
|
|
||||||
const open = currentPrice
|
|
||||||
const close = currentPrice + change
|
|
||||||
const high = Math.max(open, close) + Math.random() * 0.01 * currentPrice
|
|
||||||
const low = Math.min(open, close) - Math.random() * 0.01 * currentPrice
|
|
||||||
|
|
||||||
data.push({
|
|
||||||
time: timeString,
|
|
||||||
open: Number(open.toFixed(2)),
|
|
||||||
high: Number(high.toFixed(2)),
|
|
||||||
low: Number(low.toFixed(2)),
|
|
||||||
close: Number(close.toFixed(2)),
|
|
||||||
})
|
|
||||||
|
|
||||||
currentPrice = close
|
|
||||||
}
|
|
||||||
|
|
||||||
return data
|
|
||||||
}
|
|
||||||
|
|
||||||
initChart()
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
// Update position overlays when positions change
|
|
||||||
useEffect(() => {
|
|
||||||
if (chart.current && !isLoading && positions.length > 0) {
|
|
||||||
import('lightweight-charts').then(({ LineStyle }) => {
|
|
||||||
// Re-add position overlays (this is a simplified version)
|
|
||||||
// In a full implementation, you'd want to properly manage line updates
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}, [positions, isLoading])
|
|
||||||
|
|
||||||
if (isLoading) {
|
|
||||||
return (
|
|
||||||
<div className="h-[600px] bg-gray-900 rounded-lg flex items-center justify-center">
|
|
||||||
<div className="text-white">Loading chart...</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="relative">
|
|
||||||
{/* Chart Header */}
|
|
||||||
<div className="absolute top-4 left-4 z-10 bg-gray-800/80 rounded-lg px-3 py-2">
|
|
||||||
<div className="flex items-center space-x-4">
|
|
||||||
<div className="text-white font-bold text-lg">{symbol}</div>
|
|
||||||
<div className="text-green-400 font-semibold">$166.21</div>
|
|
||||||
<div className="text-green-400 text-sm">+1.42%</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Position Info */}
|
|
||||||
{positions.length > 0 && (
|
|
||||||
<div className="absolute top-4 right-4 z-10 bg-gray-800/80 rounded-lg px-3 py-2">
|
|
||||||
<div className="text-white text-sm">
|
|
||||||
{positions.map(position => (
|
|
||||||
<div key={position.id} className="flex items-center space-x-2">
|
|
||||||
<div className={`w-2 h-2 rounded-full ${
|
|
||||||
position.side === 'LONG' ? 'bg-green-400' : 'bg-red-400'
|
|
||||||
}`}></div>
|
|
||||||
<span>{position.side} {position.size}</span>
|
|
||||||
<span className={position.pnl >= 0 ? 'text-green-400' : 'text-red-400'}>
|
|
||||||
{position.pnl >= 0 ? '+' : ''}${position.pnl.toFixed(2)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Chart Container */}
|
|
||||||
<div ref={chartContainerRef} className="w-full h-[600px] bg-gray-900 rounded-lg" />
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
version: "2"
|
||||||
|
|
||||||
services:
|
services:
|
||||||
app:
|
app:
|
||||||
build:
|
build:
|
||||||
|
|||||||
@@ -1,14 +1,7 @@
|
|||||||
import type { NextConfig } from "next";
|
import type { NextConfig } from "next";
|
||||||
|
|
||||||
const nextConfig: NextConfig = {
|
const nextConfig: NextConfig = {
|
||||||
transpilePackages: ['lightweight-charts'],
|
// Simple configuration for stability
|
||||||
webpack: (config) => {
|
|
||||||
config.resolve.fallback = {
|
|
||||||
...config.resolve.fallback,
|
|
||||||
fs: false,
|
|
||||||
};
|
|
||||||
return config;
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default nextConfig;
|
export default nextConfig;
|
||||||
|
|||||||
@@ -7,6 +7,8 @@
|
|||||||
"dev:docker": "next dev --port 3000 --hostname 0.0.0.0",
|
"dev:docker": "next dev --port 3000 --hostname 0.0.0.0",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
|
"lint": "next lint",
|
||||||
|
"lint:fix": "next lint --fix",
|
||||||
"docker:build": "docker compose build",
|
"docker:build": "docker compose build",
|
||||||
"docker:build:optimized": "DOCKER_BUILDKIT=1 COMPOSE_DOCKER_CLI_BUILD=1 COMPOSE_BAKE=true docker compose --progress=plain build --parallel",
|
"docker:build:optimized": "DOCKER_BUILDKIT=1 COMPOSE_DOCKER_CLI_BUILD=1 COMPOSE_BAKE=true docker compose --progress=plain build --parallel",
|
||||||
"docker:build:dev": "DOCKER_BUILDKIT=1 COMPOSE_BAKE=true docker compose --progress=plain build --target development",
|
"docker:build:dev": "DOCKER_BUILDKIT=1 COMPOSE_BAKE=true docker compose --progress=plain build --target development",
|
||||||
|
|||||||
Reference in New Issue
Block a user