From 0e3a2d72550819d5605aa1495973f921ec4cccca Mon Sep 17 00:00:00 2001 From: mindesbunister Date: Wed, 16 Jul 2025 14:56:53 +0200 Subject: [PATCH] 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. --- .eslintrc.json | 16 + app/analysis/page.js | 66 +++- app/chart-test/page.tsx | 14 - app/chart-trading-demo/page.tsx | 595 ++++++++++++++++++++++++++++++ app/error.tsx | 25 ++ app/not-found.tsx | 21 ++ app/page.js | 53 +++ app/simple-chart/page.tsx | 115 ------ components/SimpleChart.tsx | 315 ++++++++++++---- components/TradeExecutionPanel.js | 18 + components/TradingChart.tsx | 254 ------------- docker-compose.yml | 2 + next.config.ts | 9 +- package.json | 2 + 14 files changed, 1047 insertions(+), 458 deletions(-) create mode 100644 .eslintrc.json delete mode 100644 app/chart-test/page.tsx create mode 100644 app/chart-trading-demo/page.tsx create mode 100644 app/error.tsx create mode 100644 app/not-found.tsx delete mode 100644 app/simple-chart/page.tsx delete mode 100644 components/TradingChart.tsx diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..d12b170 --- /dev/null +++ b/.eslintrc.json @@ -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" + } +} diff --git a/app/analysis/page.js b/app/analysis/page.js index 0760a63..5a8560b 100644 --- a/app/analysis/page.js +++ b/app/analysis/page.js @@ -1,6 +1,7 @@ 'use client' import React, { useState } from 'react' import AIAnalysisPanel from '../../components/AIAnalysisPanel.tsx' +import TradeExecutionPanel from '../../components/TradeExecutionPanel.js' export default function AnalysisPage() { const [analysisResult, setAnalysisResult] = useState(null) @@ -15,13 +16,70 @@ export default function AnalysisPage() {
-

AI Analysis

-

Get comprehensive market insights powered by AI analysis

+

AI Analysis & Trading

+

Get comprehensive market insights powered by AI analysis and execute trades

-
- +
+ {/* Left Column - AI Analysis */} +
+
+

๐Ÿ“Š AI Market Analysis

+ +
+
+ + {/* Right Column - Trading Panel */} +
+
+

๐Ÿ’ฐ Execute Trade

+ +
+ + {/* Analysis Summary */} + {analysisResult && ( +
+

๐ŸŽฏ Analysis Summary

+
+
+ Symbol: + {currentSymbol} +
+
+ Sentiment: + + {analysisResult.sentiment} + +
+ {analysisResult.entryPrice && ( +
+ Entry Price: + ${analysisResult.entryPrice} +
+ )} + {analysisResult.stopLoss && ( +
+ Stop Loss: + ${analysisResult.stopLoss} +
+ )} + {analysisResult.takeProfit && ( +
+ Take Profit: + ${analysisResult.takeProfit} +
+ )} +
+
+ )} +
) diff --git a/app/chart-test/page.tsx b/app/chart-test/page.tsx deleted file mode 100644 index dbb1798..0000000 --- a/app/chart-test/page.tsx +++ /dev/null @@ -1,14 +0,0 @@ -'use client' -import React from 'react' -import TradingChart from '../../components/TradingChart' - -export default function SimpleChartTest() { - return ( -
-
-

Chart Test

- -
-
- ) -} diff --git a/app/chart-trading-demo/page.tsx b/app/chart-trading-demo/page.tsx new file mode 100644 index 0000000..3e46069 --- /dev/null +++ b/app/chart-trading-demo/page.tsx @@ -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(null) + const payingTokenDropdownRef = useRef(null) + const [positions, setPositions] = useState([ + // 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 ( + <> + +
+ {/* Top Bar */} +
+
+
+

Chart Trading Terminal

+ + {/* Symbol Selector Dropdown */} +
+ + + {/* Dropdown Menu */} + {isSymbolDropdownOpen && ( +
+
+
+ Select Trading Pair +
+ {symbols.map(({ symbol, name, price, change, icon }) => ( + + ))} +
+
+ )} +
+
+ + {/* Market Status */} +
+
+
+ Demo Mode +
+
+ {new Date().toLocaleTimeString()} +
+
+
+
+ + {/* Main Trading Interface */} +
+ {/* Chart Area (70% width) */} +
+ +
+ + {/* Trading Panel (30% width) */} +
+
+

{selectedSymbol}/USDC

+
+ ${getCurrentSymbolData().price.toLocaleString()} +
+
= 0 ? 'text-green-400' : 'text-red-400'}`}> + {getCurrentSymbolData().change >= 0 ? '+' : ''}{getCurrentSymbolData().change}% +
+ + {/* Leverage Selector */} +
+
+ +
+ {leverage}x +
+
+ + {/* Leverage Slider */} +
+ 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 */} +
+ {[1, 2, 5, 10, 20, 50, 100].map((mark) => ( + + ))} +
+
+ + {/* Leverage Warning */} + {leverage > 10 && ( +
+ + + + High leverage increases liquidation risk +
+ )} +
+ + {/* Quick Trade Buttons */} +
+ + +
+ + {/* Trade Form */} +
+ {/* You're Paying Section */} +
+ +
+
+ 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 */} +
+ + + {/* Paying Token Dropdown */} + {isPayingTokenDropdownOpen && ( +
+
+ {payingTokens.map(({ symbol, name, balance, icon }) => ( + + ))} +
+
+ )} +
+
+ + {/* Balance and MAX button */} +
+ Balance: {getCurrentPayingTokenData().balance.toLocaleString()} {payingToken} + +
+
+ {leverage > 1 && payingAmount && ( +
+ Position value: ${(parseFloat(payingAmount) * leverage).toLocaleString()} ({leverage}x leverage) +
+ )} +
+ + {/* You're Receiving Section */} +
+ +
+
+
+ {receivingAmount || '0.00'} +
+
+ {getCurrentSymbolData().icon} + {selectedSymbol} +
+
+
+ Rate: 1 {payingToken} = {payingToken === 'USDC' ? (1 / getCurrentSymbolData().price).toFixed(8) : '1.00'} {selectedSymbol} +
+
+
+ +
+
+ + 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" + /> +
+
+ + 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" + /> +
+
+ + {/* Risk Information */} + {payingAmount && leverage > 1 && ( +
+
โš ๏ธ Leveraged Position Risk
+
+ Position value: ${(parseFloat(payingAmount) * leverage).toLocaleString()} ({leverage}x leverage) +
+
+ Liquidation risk: High with {leverage}x leverage +
+
+ )} +
+
+
+
+ + {/* Bottom Panel - Positions */} +
+
+
+
+ + + +
+ + {positions.length > 0 && ( +
+ Total P&L: 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)} + +
+ )} +
+ + {/* Positions Table */} +
+ {positions.length === 0 ? ( +
+ No open positions +
+ ) : ( +
+ {positions.map((position: Position) => ( +
+
+
+
+
+ {position.symbol} โ€ข {position.side} + {position.leverage > 1 && ( + + {position.leverage}x + + )} +
+
+ Size: {position.amount} โ€ข Entry: ${position.entryPrice?.toFixed(2)} +
+
+ SL: ${position.stopLoss.toFixed(2)} +
+
+ TP: ${position.takeProfit.toFixed(2)} +
+
+
+ +
+
+ ${(position.amount * position.currentPrice).toFixed(2)} +
+
= 0 ? 'text-green-400' : 'text-red-400' + }`}> + {position.unrealizedPnl >= 0 ? '+' : ''}${(position.unrealizedPnl || 0).toFixed(2)} +
+ {position.leverage > 1 && ( +
+ {position.leverage}x Leveraged +
+ )} +
+ +
+ +
+
+ ))} +
+ )} +
+
+
+
+ + ) +} \ No newline at end of file diff --git a/app/error.tsx b/app/error.tsx new file mode 100644 index 0000000..0bb7d15 --- /dev/null +++ b/app/error.tsx @@ -0,0 +1,25 @@ +'use client' +import React from 'react' + +export default function Error({ + error, + reset, +}: { + error: Error & { digest?: string } + reset: () => void +}) { + return ( +
+
+

Something went wrong!

+

An error occurred while loading this page.

+ +
+
+ ) +} diff --git a/app/not-found.tsx b/app/not-found.tsx new file mode 100644 index 0000000..0e704d3 --- /dev/null +++ b/app/not-found.tsx @@ -0,0 +1,21 @@ +'use client' +import React from 'react' +import Link from 'next/link' + +export default function NotFound() { + return ( +
+
+

404

+

Page Not Found

+

The page you're looking for doesn't exist.

+ + Go Back Home + +
+
+ ) +} diff --git a/app/page.js b/app/page.js index 5833dbd..928e976 100644 --- a/app/page.js +++ b/app/page.js @@ -54,6 +54,59 @@ export default function HomePage() {
+ + {/* Featured: Chart Trading Demo */} +
+
+
+

๐Ÿš€ Professional Chart Trading

+

Advanced trading interface with leverage, charting, and position management

+
+
+ NEW +
+
+ +
+
+
+ ๐Ÿ“ˆ +
+
+
Real-time Charts
+
Professional candlestick visualization
+
+
+ +
+
+ โšก +
+
+
Leverage Trading
+
1x to 100x leverage options
+
+
+ +
+
+ ๐ŸŽฏ +
+
+
Position Management
+
Stop loss & take profit
+
+
+
+ + + ๐Ÿš€ + Launch Chart Trading Terminal + +
) } diff --git a/app/simple-chart/page.tsx b/app/simple-chart/page.tsx deleted file mode 100644 index 7990537..0000000 --- a/app/simple-chart/page.tsx +++ /dev/null @@ -1,115 +0,0 @@ -'use client' -import React, { useEffect, useRef, useState } from 'react' - -export default function SimpleChart() { - const chartContainerRef = useRef(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 ( -
-

Lightweight Charts Test

-
Status: {status}
-
-
- ) -} diff --git a/components/SimpleChart.tsx b/components/SimpleChart.tsx index 5e4c406..76b9023 100644 --- a/components/SimpleChart.tsx +++ b/components/SimpleChart.tsx @@ -1,100 +1,289 @@ '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 { 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) { const canvasRef = useRef(null) + const [candleData, setCandleData] = useState([]) + 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(() => { const canvas = canvasRef.current - if (!canvas) return + if (!canvas || candleData.length === 0) return const ctx = canvas.getContext('2d') if (!ctx) return - // Set canvas size - canvas.width = 800 - canvas.height = 400 + // Set canvas size for high DPI displays + const rect = canvas.getBoundingClientRect() + const dpr = window.devicePixelRatio || 1 + canvas.width = rect.width * dpr + canvas.height = rect.height * dpr + ctx.scale(dpr, dpr) - // Clear canvas - ctx.fillStyle = '#1a1a1a' - ctx.fillRect(0, 0, canvas.width, canvas.height) + const width = rect.width + const height = rect.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 - ctx.strokeStyle = '#333' + ctx.strokeStyle = '#1a1a1a' ctx.lineWidth = 1 - // Vertical lines - for (let x = 0; x < canvas.width; x += 40) { + // Horizontal grid lines (price levels) + 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.moveTo(x, 0) - ctx.lineTo(x, canvas.height) + ctx.moveTo(chartLeft, y) + 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() } - // Horizontal lines - for (let y = 0; y < canvas.height; y += 40) { + // Draw candlesticks + 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.moveTo(0, y) - ctx.lineTo(canvas.width, y) - 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.moveTo(x, highY) + ctx.lineTo(x, lowY) ctx.stroke() - // Draw body + // Draw candle body const bodyTop = Math.min(openY, closeY) const bodyHeight = Math.abs(closeY - openY) - ctx.fillRect(x, bodyTop, candleWidth, Math.max(bodyHeight, 2)) - } + + if (isGreen) { + ctx.strokeRect(x - candleWidth / 2, bodyTop, candleWidth, Math.max(bodyHeight, 1)) + } else { + ctx.fillRect(x - candleWidth / 2, bodyTop, candleWidth, Math.max(bodyHeight, 1)) + } + }) - // Draw price labels - ctx.fillStyle = '#ffffff' + // 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.fillText(`${symbol} - $${basePrice.toFixed(2)}`, 10, 30) - ctx.fillStyle = '#26a69a' - ctx.fillText('+2.45%', 10, 50) + ctx.textAlign = 'center' + ctx.fillText(currentPrice.toFixed(2), chartRight - 30, currentPriceY + 4) - }, [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 ( -
- +
+ {/* Chart Controls */} +
+
+

{symbol}

+
+ {timeframes.map(tf => ( + + ))} +
+
+ +
+ {candleData.length > 0 && ( + + Last: ${candleData[candleData.length - 1]?.close.toFixed(2)} โ€ข + 24h Vol: ${(Math.random() * 1000000).toFixed(0)}M + + )} +
+
+ + {/* Chart Canvas */} +
+ + + {/* Legend */} + {positions.length > 0 && ( +
+
โ€” Current Price
+
โ‹ฏ Take Profit
+
โ‹ฏ Stop Loss
+
โ‹ฏ Entry Price
+
+ )} +
) } diff --git a/components/TradeExecutionPanel.js b/components/TradeExecutionPanel.js index b835215..14baa03 100644 --- a/components/TradeExecutionPanel.js +++ b/components/TradeExecutionPanel.js @@ -48,9 +48,27 @@ export default function TradeExecutionPanel({ analysis, symbol = 'SOL' }) { setTakeProfit(analysis.takeProfits.tp1.price.toString()) 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]) + // 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 const getRecommendedPrice = () => { if (!analysis) return null diff --git a/components/TradingChart.tsx b/components/TradingChart.tsx deleted file mode 100644 index 492a2e8..0000000 --- a/components/TradingChart.tsx +++ /dev/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(null) - const chart = useRef(null) - const candlestickSeries = useRef(null) - const positionLines = useRef([]) - 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 ( -
-
Loading chart...
-
- ) - } - - return ( -
- {/* Chart Header */} -
-
-
{symbol}
-
$166.21
-
+1.42%
-
-
- - {/* Position Info */} - {positions.length > 0 && ( -
-
- {positions.map(position => ( -
-
- {position.side} {position.size} - = 0 ? 'text-green-400' : 'text-red-400'}> - {position.pnl >= 0 ? '+' : ''}${position.pnl.toFixed(2)} - -
- ))} -
-
- )} - - {/* Chart Container */} -
-
- ) -} diff --git a/docker-compose.yml b/docker-compose.yml index d272d91..c2ace2f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,3 +1,5 @@ +version: "2" + services: app: build: diff --git a/next.config.ts b/next.config.ts index 0cec214..50380fb 100644 --- a/next.config.ts +++ b/next.config.ts @@ -1,14 +1,7 @@ import type { NextConfig } from "next"; const nextConfig: NextConfig = { - transpilePackages: ['lightweight-charts'], - webpack: (config) => { - config.resolve.fallback = { - ...config.resolve.fallback, - fs: false, - }; - return config; - }, + // Simple configuration for stability }; export default nextConfig; diff --git a/package.json b/package.json index eba69ad..be8b17f 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,8 @@ "dev:docker": "next dev --port 3000 --hostname 0.0.0.0", "build": "next build", "start": "next start", + "lint": "next lint", + "lint:fix": "next lint --fix", "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:dev": "DOCKER_BUILDKIT=1 COMPOSE_BAKE=true docker compose --progress=plain build --target development",