Merge development branch: improved UI, coin icons, and comprehensive documentation
- Add proper CoinGecko coin icons for BTC, ETH, SOL - Clean up homepage layout and remove clutter - Add comprehensive .github/copilot-instructions.md with full architecture documentation - Include timeframe fixes, trading integration improvements, and Docker optimizations - Maintain all trading functionality and AI analysis features
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
"use client"
|
||||
import React, { useState } from 'react'
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import TradeModal from './TradeModal'
|
||||
import ScreenshotGallery from './ScreenshotGallery'
|
||||
|
||||
@@ -8,7 +8,9 @@ const timeframes = [
|
||||
{ label: '1m', value: '1' },
|
||||
{ label: '5m', value: '5' },
|
||||
{ label: '15m', value: '15' },
|
||||
{ label: '30m', value: '30' },
|
||||
{ label: '1h', value: '60' },
|
||||
{ label: '2h', value: '120' },
|
||||
{ label: '4h', value: '240' },
|
||||
{ label: '1d', value: 'D' },
|
||||
{ label: '1w', value: 'W' },
|
||||
@@ -60,6 +62,7 @@ export default function AIAnalysisPanel({ onAnalysisComplete }: AIAnalysisPanelP
|
||||
const [result, setResult] = useState<any>(null)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [progress, setProgress] = useState<AnalysisProgress | null>(null)
|
||||
const [eventSource, setEventSource] = useState<EventSource | null>(null)
|
||||
const [modalOpen, setModalOpen] = useState(false)
|
||||
const [modalData, setModalData] = useState<any>(null)
|
||||
const [enlargedScreenshot, setEnlargedScreenshot] = useState<string | null>(null)
|
||||
@@ -77,6 +80,47 @@ export default function AIAnalysisPanel({ onAnalysisComplete }: AIAnalysisPanelP
|
||||
return String(value)
|
||||
}
|
||||
|
||||
// Real-time progress tracking
|
||||
const startProgressTracking = (sessionId: string) => {
|
||||
// Close existing connection
|
||||
if (eventSource) {
|
||||
eventSource.close()
|
||||
}
|
||||
|
||||
const es = new EventSource(`/api/progress/${sessionId}/stream`)
|
||||
|
||||
es.onmessage = (event) => {
|
||||
try {
|
||||
const progressData = JSON.parse(event.data)
|
||||
if (progressData.type === 'complete') {
|
||||
es.close()
|
||||
setEventSource(null)
|
||||
} else {
|
||||
setProgress(progressData)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error parsing progress data:', error)
|
||||
}
|
||||
}
|
||||
|
||||
es.onerror = (error) => {
|
||||
console.error('EventSource error:', error)
|
||||
es.close()
|
||||
setEventSource(null)
|
||||
}
|
||||
|
||||
setEventSource(es)
|
||||
}
|
||||
|
||||
// Cleanup event source on unmount
|
||||
React.useEffect(() => {
|
||||
return () => {
|
||||
if (eventSource) {
|
||||
eventSource.close()
|
||||
}
|
||||
}
|
||||
}, [eventSource])
|
||||
|
||||
const toggleLayout = (layout: string) => {
|
||||
setSelectedLayouts(prev =>
|
||||
prev.includes(layout)
|
||||
@@ -93,104 +137,11 @@ export default function AIAnalysisPanel({ onAnalysisComplete }: AIAnalysisPanelP
|
||||
)
|
||||
}
|
||||
|
||||
// Helper function to create initial progress steps
|
||||
const createProgressSteps = (timeframes: string[], layouts: string[]): ProgressStep[] => {
|
||||
const steps: ProgressStep[] = []
|
||||
|
||||
if (timeframes.length > 1) {
|
||||
steps.push({
|
||||
id: 'init',
|
||||
title: 'Initializing Multi-Timeframe Analysis',
|
||||
description: `Preparing to analyze ${timeframes.length} timeframes`,
|
||||
status: 'pending'
|
||||
})
|
||||
} else {
|
||||
steps.push({
|
||||
id: 'init',
|
||||
title: 'Initializing Analysis',
|
||||
description: `Setting up screenshot service for ${layouts.join(', ')} layout(s)`,
|
||||
status: 'pending'
|
||||
})
|
||||
}
|
||||
// Helper function to create initial progress steps (no longer used - using real-time progress)
|
||||
// const createProgressSteps = ...removed for real-time implementation
|
||||
|
||||
steps.push({
|
||||
id: 'browser',
|
||||
title: 'Starting Browser Sessions',
|
||||
description: `Launching ${layouts.length} browser session(s)`,
|
||||
status: 'pending'
|
||||
})
|
||||
|
||||
steps.push({
|
||||
id: 'auth',
|
||||
title: 'TradingView Authentication',
|
||||
description: 'Logging into TradingView accounts',
|
||||
status: 'pending'
|
||||
})
|
||||
|
||||
steps.push({
|
||||
id: 'navigation',
|
||||
title: 'Chart Navigation',
|
||||
description: 'Navigating to chart layouts and timeframes',
|
||||
status: 'pending'
|
||||
})
|
||||
|
||||
steps.push({
|
||||
id: 'loading',
|
||||
title: 'Chart Data Loading',
|
||||
description: 'Waiting for chart data and indicators to load',
|
||||
status: 'pending'
|
||||
})
|
||||
|
||||
steps.push({
|
||||
id: 'capture',
|
||||
title: 'Screenshot Capture',
|
||||
description: 'Capturing high-quality chart screenshots',
|
||||
status: 'pending'
|
||||
})
|
||||
|
||||
steps.push({
|
||||
id: 'analysis',
|
||||
title: 'AI Analysis',
|
||||
description: 'Analyzing screenshots with AI for trading insights',
|
||||
status: 'pending'
|
||||
})
|
||||
|
||||
return steps
|
||||
}
|
||||
|
||||
// Helper function to update progress
|
||||
const updateProgress = (stepId: string, status: ProgressStep['status'], details?: string) => {
|
||||
setProgress(prev => {
|
||||
if (!prev) return null
|
||||
|
||||
const updatedSteps = prev.steps.map(step => {
|
||||
if (step.id === stepId) {
|
||||
const updatedStep = {
|
||||
...step,
|
||||
status,
|
||||
details: details || step.details
|
||||
}
|
||||
|
||||
if (status === 'active' && !step.startTime) {
|
||||
updatedStep.startTime = Date.now()
|
||||
} else if ((status === 'completed' || status === 'error') && !step.endTime) {
|
||||
updatedStep.endTime = Date.now()
|
||||
}
|
||||
|
||||
return updatedStep
|
||||
}
|
||||
return step
|
||||
})
|
||||
|
||||
const currentStepIndex = updatedSteps.findIndex(step => step.status === 'active')
|
||||
|
||||
return {
|
||||
...prev,
|
||||
steps: updatedSteps,
|
||||
currentStep: currentStepIndex >= 0 ? currentStepIndex + 1 : prev.currentStep
|
||||
}
|
||||
})
|
||||
}
|
||||
// Helper function to update progress (no longer used - using real-time progress)
|
||||
// const updateProgress = ...removed for real-time implementation
|
||||
|
||||
const performAnalysis = async (analysisSymbol = symbol, analysisTimeframes = selectedTimeframes) => {
|
||||
if (loading || selectedLayouts.length === 0 || analysisTimeframes.length === 0) return
|
||||
@@ -198,13 +149,51 @@ export default function AIAnalysisPanel({ onAnalysisComplete }: AIAnalysisPanelP
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
setResult(null)
|
||||
|
||||
// Initialize progress tracking
|
||||
const steps = createProgressSteps(analysisTimeframes, selectedLayouts)
|
||||
|
||||
// Set initial progress state to show animation immediately
|
||||
setProgress({
|
||||
currentStep: 0,
|
||||
totalSteps: steps.length,
|
||||
steps,
|
||||
sessionId: 'initializing',
|
||||
currentStep: 1,
|
||||
totalSteps: 6,
|
||||
steps: [
|
||||
{
|
||||
id: 'init',
|
||||
title: 'Initializing Analysis',
|
||||
description: 'Starting AI-powered trading analysis...',
|
||||
status: 'active',
|
||||
startTime: Date.now()
|
||||
},
|
||||
{
|
||||
id: 'auth',
|
||||
title: 'TradingView Authentication',
|
||||
description: 'Logging into TradingView accounts',
|
||||
status: 'pending'
|
||||
},
|
||||
{
|
||||
id: 'navigation',
|
||||
title: 'Chart Navigation',
|
||||
description: 'Navigating to chart layouts',
|
||||
status: 'pending'
|
||||
},
|
||||
{
|
||||
id: 'loading',
|
||||
title: 'Chart Data Loading',
|
||||
description: 'Waiting for chart data and indicators',
|
||||
status: 'pending'
|
||||
},
|
||||
{
|
||||
id: 'capture',
|
||||
title: 'Screenshot Capture',
|
||||
description: 'Capturing high-quality screenshots',
|
||||
status: 'pending'
|
||||
},
|
||||
{
|
||||
id: 'analysis',
|
||||
title: 'AI Analysis',
|
||||
description: 'Analyzing screenshots with AI',
|
||||
status: 'pending'
|
||||
}
|
||||
],
|
||||
timeframeProgress: analysisTimeframes.length > 1 ? {
|
||||
current: 0,
|
||||
total: analysisTimeframes.length
|
||||
@@ -212,14 +201,8 @@ export default function AIAnalysisPanel({ onAnalysisComplete }: AIAnalysisPanelP
|
||||
})
|
||||
|
||||
try {
|
||||
updateProgress('init', 'active')
|
||||
|
||||
if (analysisTimeframes.length === 1) {
|
||||
// Single timeframe analysis
|
||||
await new Promise(resolve => setTimeout(resolve, 500)) // Brief pause for UI
|
||||
updateProgress('init', 'completed')
|
||||
updateProgress('browser', 'active', 'Starting browser session...')
|
||||
|
||||
// Single timeframe analysis with real-time progress
|
||||
const response = await fetch('/api/enhanced-screenshot', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
@@ -231,35 +214,16 @@ export default function AIAnalysisPanel({ onAnalysisComplete }: AIAnalysisPanelP
|
||||
})
|
||||
})
|
||||
|
||||
// Since we can't track internal API progress in real-time, we'll simulate logical progression
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
updateProgress('browser', 'completed')
|
||||
updateProgress('auth', 'active', 'Authenticating with TradingView...')
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 2000))
|
||||
updateProgress('auth', 'completed')
|
||||
updateProgress('navigation', 'active', `Navigating to ${analysisSymbol} chart...`)
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 2000))
|
||||
updateProgress('navigation', 'completed')
|
||||
updateProgress('loading', 'active', 'Loading chart data and indicators...')
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 3000))
|
||||
updateProgress('loading', 'completed')
|
||||
updateProgress('capture', 'active', 'Capturing screenshots...')
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
updateProgress('capture', 'error', data.error || 'Screenshot capture failed')
|
||||
throw new Error(data.error || 'Analysis failed')
|
||||
}
|
||||
|
||||
updateProgress('capture', 'completed', `Captured ${data.screenshots?.length || 0} screenshot(s)`)
|
||||
updateProgress('analysis', 'active', 'Running AI analysis...')
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
updateProgress('analysis', 'completed', 'Analysis complete!')
|
||||
// Start real-time progress tracking if sessionId is provided
|
||||
if (data.sessionId) {
|
||||
startProgressTracking(data.sessionId)
|
||||
}
|
||||
|
||||
setResult(data)
|
||||
|
||||
@@ -269,31 +233,14 @@ export default function AIAnalysisPanel({ onAnalysisComplete }: AIAnalysisPanelP
|
||||
}
|
||||
} else {
|
||||
// Multiple timeframe analysis
|
||||
await new Promise(resolve => setTimeout(resolve, 500))
|
||||
updateProgress('init', 'completed', `Starting analysis for ${analysisTimeframes.length} timeframes`)
|
||||
|
||||
const results = []
|
||||
|
||||
for (let i = 0; i < analysisTimeframes.length; i++) {
|
||||
const tf = analysisTimeframes[i]
|
||||
const timeframeLabel = timeframes.find(t => t.value === tf)?.label || tf
|
||||
|
||||
// Update timeframe progress
|
||||
setProgress(prev => prev ? {
|
||||
...prev,
|
||||
timeframeProgress: {
|
||||
...prev.timeframeProgress!,
|
||||
current: i + 1,
|
||||
currentTimeframe: timeframeLabel
|
||||
}
|
||||
} : null)
|
||||
|
||||
console.log(`🧪 Analyzing timeframe: ${timeframeLabel}`)
|
||||
|
||||
if (i === 0) {
|
||||
updateProgress('browser', 'active', `Processing ${timeframeLabel} - Starting browser...`)
|
||||
}
|
||||
|
||||
const response = await fetch('/api/enhanced-screenshot', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
@@ -305,21 +252,6 @@ export default function AIAnalysisPanel({ onAnalysisComplete }: AIAnalysisPanelP
|
||||
})
|
||||
})
|
||||
|
||||
if (i === 0) {
|
||||
updateProgress('browser', 'completed')
|
||||
updateProgress('auth', 'active', `Processing ${timeframeLabel} - Authenticating...`)
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
updateProgress('auth', 'completed')
|
||||
}
|
||||
|
||||
updateProgress('navigation', 'active', `Processing ${timeframeLabel} - Navigating to chart...`)
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
|
||||
updateProgress('loading', 'active', `Processing ${timeframeLabel} - Loading chart data...`)
|
||||
await new Promise(resolve => setTimeout(resolve, 1500))
|
||||
|
||||
updateProgress('capture', 'active', `Processing ${timeframeLabel} - Capturing screenshots...`)
|
||||
|
||||
const result = await response.json()
|
||||
results.push({
|
||||
timeframe: tf,
|
||||
@@ -327,18 +259,26 @@ export default function AIAnalysisPanel({ onAnalysisComplete }: AIAnalysisPanelP
|
||||
success: response.ok,
|
||||
result
|
||||
})
|
||||
|
||||
// Start progress tracking for the first timeframe session
|
||||
if (i === 0 && result.sessionId) {
|
||||
startProgressTracking(result.sessionId)
|
||||
}
|
||||
|
||||
updateProgress('analysis', 'active', `Processing ${timeframeLabel} - Running AI analysis...`)
|
||||
// Update timeframe progress manually for multi-timeframe
|
||||
setProgress(prev => prev ? {
|
||||
...prev,
|
||||
timeframeProgress: {
|
||||
current: i + 1,
|
||||
total: analysisTimeframes.length,
|
||||
currentTimeframe: timeframeLabel
|
||||
}
|
||||
} : null)
|
||||
|
||||
// Small delay between requests
|
||||
await new Promise(resolve => setTimeout(resolve, 2000))
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
}
|
||||
|
||||
updateProgress('navigation', 'completed')
|
||||
updateProgress('loading', 'completed')
|
||||
updateProgress('capture', 'completed', `Captured screenshots for all ${analysisTimeframes.length} timeframes`)
|
||||
updateProgress('analysis', 'completed', `Completed analysis for all timeframes!`)
|
||||
|
||||
const multiResult = {
|
||||
type: 'multi_timeframe',
|
||||
symbol: analysisSymbol,
|
||||
@@ -488,17 +428,29 @@ export default function AIAnalysisPanel({ onAnalysisComplete }: AIAnalysisPanelP
|
||||
// Trade execution API call
|
||||
const executeTrade = async (tradeData: any) => {
|
||||
try {
|
||||
// Use real DEX trading for manual trades
|
||||
const response = await fetch('/api/trading/execute-dex', {
|
||||
// Determine if this is a leveraged position or spot trade
|
||||
const leverage = parseFloat(tradeData.leverage) || 1
|
||||
const isLeveraged = leverage > 1
|
||||
|
||||
// Route to appropriate API based on leverage
|
||||
const apiEndpoint = isLeveraged ? '/api/trading/execute-drift' : '/api/trading/execute-dex'
|
||||
const tradingMode = isLeveraged ? 'PERP' : 'SPOT'
|
||||
|
||||
console.log(`🎯 Executing ${tradingMode} trade with ${leverage}x leverage via ${apiEndpoint}`)
|
||||
|
||||
const response = await fetch(apiEndpoint, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
symbol: tradeData.symbol || symbol,
|
||||
side: 'BUY', // Could be derived from analysis
|
||||
amount: parseFloat(tradeData.positionSize) || parseFloat(tradeData.size),
|
||||
amountUSD: parseFloat(tradeData.amountUSD || tradeData.positionSize || tradeData.size),
|
||||
leverage: leverage,
|
||||
stopLoss: parseFloat(tradeData.sl),
|
||||
takeProfit: parseFloat(tradeData.tp1), // Use TP1 as primary target
|
||||
useRealDEX: true, // Enable real trading for manual execution
|
||||
tradingMode: tradingMode,
|
||||
tradingPair: `${tradeData.symbol || symbol}/USDC`,
|
||||
quickSwap: false
|
||||
})
|
||||
@@ -507,17 +459,30 @@ export default function AIAnalysisPanel({ onAnalysisComplete }: AIAnalysisPanelP
|
||||
const result = await response.json()
|
||||
|
||||
if (response.ok && result.success) {
|
||||
// Show detailed success message for DEX execution
|
||||
let message = `✅ Real DEX Trade executed successfully!\n\n`
|
||||
// Show detailed success message based on trading type
|
||||
const leverage = parseFloat(tradeData.leverage) || 1
|
||||
const isLeveraged = leverage > 1
|
||||
const tradeType = isLeveraged ? 'Leveraged Perpetual Position' : 'Spot Trade'
|
||||
const platform = isLeveraged ? 'Drift Protocol' : 'Jupiter DEX'
|
||||
|
||||
let message = `✅ ${tradeType} executed successfully!\n\n`
|
||||
message += `📊 Transaction ID: ${result.trade?.txId || result.txId}\n`
|
||||
message += `💰 Symbol: ${tradeData.symbol || symbol}\n`
|
||||
message += `📈 Size: ${tradeData.positionSize || tradeData.size}\n`
|
||||
message += `🏪 DEX: ${result.trade?.dex || 'Jupiter'}\n`
|
||||
message += `📈 Size: ${tradeData.positionSize || tradeData.size} USDC\n`
|
||||
if (isLeveraged) {
|
||||
message += `⚡ Leverage: ${leverage}x (via increased position size)\n`
|
||||
message += `<EFBFBD> Actual Trade Size: $${(parseFloat(tradeData.positionSize || tradeData.size) * leverage).toFixed(2)}\n`
|
||||
}
|
||||
message += `<EFBFBD>🏪 Platform: ${platform}\n`
|
||||
|
||||
if (tradeData.sl) message += `🛑 Stop Loss: $${tradeData.sl}\n`
|
||||
if (tradeData.tp1) message += `🎯 Take Profit: $${tradeData.tp1}\n`
|
||||
if (tradeData.sl) message += `🛑 Stop Loss: $${tradeData.sl} (Jupiter Trigger Order)\n`
|
||||
if (tradeData.tp1) message += `🎯 Take Profit: $${tradeData.tp1} (Jupiter Trigger Order)\n`
|
||||
|
||||
if (result.trade?.monitoring) {
|
||||
if (result.triggerOrders?.status === 'CREATED') {
|
||||
message += `\n🔄 Trigger Orders: ACTIVE\n`
|
||||
if (result.triggerOrders.stopLossOrderId) message += `🛑 SL Order: ${result.triggerOrders.stopLossOrderId.substring(0, 8)}...\n`
|
||||
if (result.triggerOrders.takeProfitOrderId) message += `🎯 TP Order: ${result.triggerOrders.takeProfitOrderId.substring(0, 8)}...\n`
|
||||
} else if (result.trade?.monitoring || result.position) {
|
||||
message += `\n🔄 Position monitoring: ACTIVE`
|
||||
}
|
||||
|
||||
@@ -532,6 +497,8 @@ export default function AIAnalysisPanel({ onAnalysisComplete }: AIAnalysisPanelP
|
||||
alert(`❌ Trade Failed: Insufficient Balance\n\nPlease ensure you have enough tokens in your wallet.\n\nError: ${errorMsg}`)
|
||||
} else if (errorMsg.includes('Real Jupiter Perpetuals trading not yet implemented')) {
|
||||
alert(`❌ Real Trading Not Available\n\nReal Jupiter Perpetuals trading is still in development. This trade will be simulated instead.\n\nTo use real spot trading, reduce the leverage to 1x.`)
|
||||
} else if (errorMsg.includes('Trigger API error') || errorMsg.includes('trigger orders failed')) {
|
||||
alert(`⚠️ Trade Executed, But Trigger Orders Failed\n\nYour main trade was successful, but stop loss/take profit orders could not be created.\n\nError: ${errorMsg}\n\nPlease monitor your position manually.`)
|
||||
} else {
|
||||
alert(`❌ Trade Failed\n\nError: ${errorMsg}`)
|
||||
}
|
||||
@@ -579,16 +546,16 @@ export default function AIAnalysisPanel({ onAnalysisComplete }: AIAnalysisPanelP
|
||||
<label className="block text-xs font-medium text-gray-400 mb-2">Quick Timeframe Presets</label>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-2">
|
||||
<button
|
||||
onClick={() => setSelectedTimeframes(['5', '15', '60'])}
|
||||
onClick={() => setSelectedTimeframes(['5', '15', '30'])}
|
||||
className="py-2 px-3 rounded-lg text-xs font-medium bg-purple-600/20 text-purple-300 hover:bg-purple-600/30 transition-all"
|
||||
>
|
||||
🕒 Scalping (5m, 15m, 1h)
|
||||
🕒 Scalping (5m, 15m, 30m)
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setSelectedTimeframes(['60', '240', 'D'])}
|
||||
onClick={() => setSelectedTimeframes(['60', '120', '240'])}
|
||||
className="py-2 px-3 rounded-lg text-xs font-medium bg-blue-600/20 text-blue-300 hover:bg-blue-600/30 transition-all"
|
||||
>
|
||||
📊 Day Trading (1h, 4h, 1d)
|
||||
📊 Day Trading (1h, 2h, 4h)
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setSelectedTimeframes(['240', 'D', 'W'])}
|
||||
@@ -978,7 +945,23 @@ export default function AIAnalysisPanel({ onAnalysisComplete }: AIAnalysisPanelP
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4">
|
||||
{result.results.map((timeframeResult: any, index: number) => (
|
||||
{result.results
|
||||
.sort((a: any, b: any) => {
|
||||
// Sort by timeframe order: 5m, 15m, 30m, 1h, 2h, 4h, 1D
|
||||
const timeframeOrder: {[key: string]: number} = {
|
||||
'5': 1, '5m': 1,
|
||||
'15': 2, '15m': 2,
|
||||
'30': 3, '30m': 3,
|
||||
'60': 4, '1h': 4,
|
||||
'120': 5, '2h': 5,
|
||||
'240': 6, '4h': 6,
|
||||
'D': 7, '1D': 7
|
||||
}
|
||||
const orderA = timeframeOrder[a.timeframe] || timeframeOrder[a.timeframeLabel] || 999
|
||||
const orderB = timeframeOrder[b.timeframe] || timeframeOrder[b.timeframeLabel] || 999
|
||||
return orderA - orderB
|
||||
})
|
||||
.map((timeframeResult: any, index: number) => (
|
||||
<div key={index} className={`p-4 rounded-lg border ${
|
||||
timeframeResult.success
|
||||
? 'bg-green-500/5 border-green-500/30'
|
||||
@@ -1459,9 +1442,43 @@ export default function AIAnalysisPanel({ onAnalysisComplete }: AIAnalysisPanelP
|
||||
{/* Multi-timeframe Screenshot Gallery */}
|
||||
{result && result.type === 'multi_timeframe' && result.results && (
|
||||
<ScreenshotGallery
|
||||
screenshots={result.results.filter((r: any) => r.success && r.result.screenshots).flatMap((r: any) => r.result.screenshots)}
|
||||
screenshots={result.results
|
||||
.filter((r: any) => r.success && r.result.screenshots)
|
||||
.sort((a: any, b: any) => {
|
||||
// Sort by timeframe order: 5m, 15m, 30m, 1h, 2h, 4h, 1D
|
||||
const timeframeOrder: {[key: string]: number} = {
|
||||
'5': 1, '5m': 1,
|
||||
'15': 2, '15m': 2,
|
||||
'30': 3, '30m': 3,
|
||||
'60': 4, '1h': 4,
|
||||
'120': 5, '2h': 5,
|
||||
'240': 6, '4h': 6,
|
||||
'D': 7, '1D': 7
|
||||
}
|
||||
const orderA = timeframeOrder[a.timeframe] || timeframeOrder[a.timeframeLabel] || 999
|
||||
const orderB = timeframeOrder[b.timeframe] || timeframeOrder[b.timeframeLabel] || 999
|
||||
return orderA - orderB
|
||||
})
|
||||
.flatMap((r: any) => r.result.screenshots)}
|
||||
symbol={symbol}
|
||||
timeframes={result.results.filter((r: any) => r.success).map((r: any) => r.timeframeLabel)}
|
||||
timeframes={result.results
|
||||
.filter((r: any) => r.success)
|
||||
.sort((a: any, b: any) => {
|
||||
// Sort by timeframe order: 5m, 15m, 30m, 1h, 2h, 4h, 1D
|
||||
const timeframeOrder: {[key: string]: number} = {
|
||||
'5': 1, '5m': 1,
|
||||
'15': 2, '15m': 2,
|
||||
'30': 3, '30m': 3,
|
||||
'60': 4, '1h': 4,
|
||||
'120': 5, '2h': 5,
|
||||
'240': 6, '4h': 6,
|
||||
'D': 7, '1D': 7
|
||||
}
|
||||
const orderA = timeframeOrder[a.timeframe] || timeframeOrder[a.timeframeLabel] || 999
|
||||
const orderB = timeframeOrder[b.timeframe] || timeframeOrder[b.timeframeLabel] || 999
|
||||
return orderA - orderB
|
||||
})
|
||||
.map((r: any) => r.timeframeLabel)}
|
||||
enlargedImage={enlargedScreenshot}
|
||||
onImageClick={handleScreenshotClick}
|
||||
onClose={() => setEnlargedScreenshot(null)}
|
||||
|
||||
@@ -34,6 +34,56 @@ export default function ScreenshotGallery({
|
||||
|
||||
if (screenshots.length === 0) return null
|
||||
|
||||
// Utility function to convert timeframe to sortable number
|
||||
const timeframeToMinutes = (timeframe: string): number => {
|
||||
const tf = timeframe.toLowerCase()
|
||||
if (tf.includes('5m') || tf === '5') return 5
|
||||
if (tf.includes('15m') || tf === '15') return 15
|
||||
if (tf.includes('30m') || tf === '30') return 30
|
||||
if (tf.includes('1h') || tf === '60') return 60
|
||||
if (tf.includes('2h') || tf === '120') return 120
|
||||
if (tf.includes('4h') || tf === '240') return 240
|
||||
if (tf.includes('1d') || tf === 'D') return 1440
|
||||
// Default fallback
|
||||
return parseInt(tf) || 999
|
||||
}
|
||||
|
||||
// Extract timeframe from filename
|
||||
const extractTimeframeFromFilename = (filename: string) => {
|
||||
const match = filename.match(/_(\d+|D)_/)
|
||||
if (!match) return 'Unknown'
|
||||
const tf = match[1]
|
||||
if (tf === 'D') return '1D'
|
||||
if (tf === '5') return '5m'
|
||||
if (tf === '15') return '15m'
|
||||
if (tf === '30') return '30m'
|
||||
if (tf === '60') return '1h'
|
||||
if (tf === '120') return '2h'
|
||||
if (tf === '240') return '4h'
|
||||
return `${tf}m`
|
||||
}
|
||||
|
||||
// Create sorted screenshot data with timeframes
|
||||
const screenshotData = screenshots.map((screenshot, index) => {
|
||||
const screenshotUrl = typeof screenshot === 'string'
|
||||
? screenshot
|
||||
: (screenshot as any)?.url || String(screenshot)
|
||||
const filename = screenshotUrl.split('/').pop() || ''
|
||||
const timeframe = timeframes[index] || extractTimeframeFromFilename(filename)
|
||||
|
||||
return {
|
||||
screenshot,
|
||||
screenshotUrl,
|
||||
filename,
|
||||
timeframe,
|
||||
index,
|
||||
sortOrder: timeframeToMinutes(timeframe)
|
||||
}
|
||||
})
|
||||
|
||||
// Sort by timeframe (smallest to largest)
|
||||
const sortedData = screenshotData.sort((a, b) => a.sortOrder - b.sortOrder)
|
||||
|
||||
// Helper function to format screenshot URL
|
||||
const formatScreenshotUrl = (screenshot: string | any) => {
|
||||
// Handle both string URLs and screenshot objects
|
||||
@@ -56,36 +106,17 @@ export default function ScreenshotGallery({
|
||||
Chart Screenshots
|
||||
</h4>
|
||||
<div className="text-xs text-gray-400">
|
||||
{screenshots.length} captured • Click to enlarge
|
||||
{sortedData.length} captured • Click to enlarge
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{screenshots.map((screenshot, index) => {
|
||||
// Handle both string URLs and screenshot objects
|
||||
const screenshotUrl = typeof screenshot === 'string'
|
||||
? screenshot
|
||||
: (screenshot as any)?.url || String(screenshot)
|
||||
const filename = screenshotUrl.split('/').pop() || ''
|
||||
// Extract timeframe from filename (e.g., SOLUSD_5_ai_timestamp.png -> "5m")
|
||||
const extractTimeframeFromFilename = (filename: string) => {
|
||||
const match = filename.match(/_(\d+|D)_/)
|
||||
if (!match) return 'Unknown'
|
||||
const tf = match[1]
|
||||
if (tf === 'D') return '1D'
|
||||
if (tf === '5') return '5m'
|
||||
if (tf === '15') return '15m'
|
||||
if (tf === '60') return '1h'
|
||||
if (tf === '240') return '4h'
|
||||
return `${tf}m`
|
||||
}
|
||||
|
||||
const timeframe = timeframes[index] || extractTimeframeFromFilename(filename)
|
||||
const imageUrl = formatScreenshotUrl(screenshot)
|
||||
{sortedData.map((item, displayIndex) => {
|
||||
const imageUrl = formatScreenshotUrl(item.screenshot)
|
||||
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
key={displayIndex}
|
||||
className="group relative bg-gray-800/30 rounded-lg overflow-hidden border border-gray-700 hover:border-purple-500/50 transition-all cursor-pointer transform hover:scale-[1.02]"
|
||||
onClick={() => onImageClick(imageUrl)}
|
||||
>
|
||||
@@ -93,7 +124,7 @@ export default function ScreenshotGallery({
|
||||
<div className="aspect-video bg-gray-800 flex items-center justify-center relative">
|
||||
<img
|
||||
src={imageUrl}
|
||||
alt={`${symbol} - ${timeframe} chart`}
|
||||
alt={`${symbol} - ${item.timeframe} chart`}
|
||||
className="w-full h-full object-cover"
|
||||
onError={(e: any) => {
|
||||
const target = e.target as HTMLImageElement
|
||||
@@ -106,7 +137,7 @@ export default function ScreenshotGallery({
|
||||
<div className="text-center">
|
||||
<div className="text-3xl mb-2">📊</div>
|
||||
<div className="text-sm">Chart Preview</div>
|
||||
<div className="text-xs text-gray-500">{filename}</div>
|
||||
<div className="text-xs text-gray-500">{item.filename}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -125,7 +156,7 @@ export default function ScreenshotGallery({
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<div className="text-sm font-medium text-white">{symbol}</div>
|
||||
<div className="text-xs text-purple-300">{timeframe} Timeframe</div>
|
||||
<div className="text-xs text-purple-300">{item.timeframe} Timeframe</div>
|
||||
</div>
|
||||
<div className="text-xs text-gray-400">
|
||||
Click to view
|
||||
|
||||
178
components/SimpleTradingChart.tsx
Normal file
178
components/SimpleTradingChart.tsx
Normal file
@@ -0,0 +1,178 @@
|
||||
'use client'
|
||||
import React, { useEffect, useRef, useState } from 'react'
|
||||
|
||||
interface SimpleCandlestickData {
|
||||
time: string
|
||||
open: number
|
||||
high: number
|
||||
low: number
|
||||
close: number
|
||||
}
|
||||
|
||||
interface SimpleTradingChartProps {
|
||||
symbol?: string
|
||||
positions?: any[]
|
||||
}
|
||||
|
||||
export default function SimpleTradingChart({ symbol = 'SOL/USDC', positions = [] }: SimpleTradingChartProps) {
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null)
|
||||
const [data, setData] = useState<SimpleCandlestickData[]>([])
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
// Generate sample data
|
||||
useEffect(() => {
|
||||
const generateData = () => {
|
||||
const data: SimpleCandlestickData[] = []
|
||||
const basePrice = 166.5
|
||||
let currentPrice = basePrice
|
||||
const today = new Date()
|
||||
|
||||
for (let i = 29; i >= 0; i--) {
|
||||
const date = new Date(today)
|
||||
date.setDate(date.getDate() - i)
|
||||
const timeString = date.toISOString().split('T')[0]
|
||||
|
||||
const change = (Math.random() - 0.5) * 4
|
||||
const open = currentPrice
|
||||
const close = currentPrice + change
|
||||
const high = Math.max(open, close) + Math.random() * 2
|
||||
const low = Math.min(open, close) - Math.random() * 2
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
setData(generateData())
|
||||
}, [])
|
||||
|
||||
// Draw the chart on canvas
|
||||
useEffect(() => {
|
||||
if (!canvasRef.current || data.length === 0) return
|
||||
|
||||
const canvas = canvasRef.current
|
||||
const ctx = canvas.getContext('2d')
|
||||
if (!ctx) return
|
||||
|
||||
// Set canvas size
|
||||
canvas.width = 800
|
||||
canvas.height = 400
|
||||
|
||||
// Clear canvas
|
||||
ctx.fillStyle = '#1a1a1a'
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height)
|
||||
|
||||
// Calculate chart dimensions
|
||||
const padding = 50
|
||||
const chartWidth = canvas.width - 2 * padding
|
||||
const chartHeight = canvas.height - 2 * padding
|
||||
|
||||
// Find price range
|
||||
const prices = data.flatMap(d => [d.open, d.high, d.low, d.close])
|
||||
const minPrice = Math.min(...prices)
|
||||
const maxPrice = Math.max(...prices)
|
||||
const priceRange = maxPrice - minPrice
|
||||
|
||||
// Helper functions
|
||||
const getX = (index: number) => padding + (index / (data.length - 1)) * chartWidth
|
||||
const getY = (price: number) => padding + ((maxPrice - price) / priceRange) * chartHeight
|
||||
|
||||
// Draw grid
|
||||
ctx.strokeStyle = 'rgba(42, 46, 57, 0.5)'
|
||||
ctx.lineWidth = 1
|
||||
|
||||
// Horizontal grid lines
|
||||
for (let i = 0; i <= 5; i++) {
|
||||
const y = padding + (i / 5) * chartHeight
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(padding, y)
|
||||
ctx.lineTo(padding + chartWidth, y)
|
||||
ctx.stroke()
|
||||
}
|
||||
|
||||
// Vertical grid lines
|
||||
for (let i = 0; i <= 10; i++) {
|
||||
const x = padding + (i / 10) * chartWidth
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(x, padding)
|
||||
ctx.lineTo(x, padding + chartHeight)
|
||||
ctx.stroke()
|
||||
}
|
||||
|
||||
// Draw candlesticks
|
||||
const candleWidth = Math.max(2, chartWidth / data.length * 0.8)
|
||||
|
||||
data.forEach((candle, index) => {
|
||||
const x = getX(index)
|
||||
const openY = getY(candle.open)
|
||||
const closeY = getY(candle.close)
|
||||
const highY = getY(candle.high)
|
||||
const lowY = getY(candle.low)
|
||||
|
||||
const isGreen = candle.close > candle.open
|
||||
const color = isGreen ? '#26a69a' : '#ef5350'
|
||||
|
||||
// Draw wick
|
||||
ctx.strokeStyle = color
|
||||
ctx.lineWidth = 1
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(x, highY)
|
||||
ctx.lineTo(x, lowY)
|
||||
ctx.stroke()
|
||||
|
||||
// Draw body
|
||||
ctx.fillStyle = color
|
||||
const bodyTop = Math.min(openY, closeY)
|
||||
const bodyHeight = Math.abs(closeY - openY)
|
||||
ctx.fillRect(x - candleWidth / 2, bodyTop, candleWidth, Math.max(bodyHeight, 1))
|
||||
})
|
||||
|
||||
// Draw price labels
|
||||
ctx.fillStyle = '#ffffff'
|
||||
ctx.font = '12px Arial'
|
||||
ctx.textAlign = 'right'
|
||||
|
||||
for (let i = 0; i <= 5; i++) {
|
||||
const price = maxPrice - (i / 5) * priceRange
|
||||
const y = padding + (i / 5) * chartHeight
|
||||
ctx.fillText(price.toFixed(2), padding - 10, y + 4)
|
||||
}
|
||||
|
||||
// Draw title
|
||||
ctx.fillStyle = '#ffffff'
|
||||
ctx.font = 'bold 16px Arial'
|
||||
ctx.textAlign = 'left'
|
||||
ctx.fillText(symbol, padding, 30)
|
||||
|
||||
}, [data, symbol])
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="bg-gray-900 rounded-lg p-6 h-[600px] flex items-center justify-center">
|
||||
<div className="text-red-400">Chart Error: {error}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bg-gray-900 rounded-lg p-6">
|
||||
<canvas
|
||||
ref={canvasRef}
|
||||
className="border border-gray-700 rounded"
|
||||
style={{ width: '100%', maxWidth: '800px', height: '400px' }}
|
||||
/>
|
||||
<div className="mt-4 text-sm text-gray-400">
|
||||
Simple canvas-based candlestick chart • {data.length} data points
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
261
components/TradingChart.tsx
Normal file
261
components/TradingChart.tsx
Normal file
@@ -0,0 +1,261 @@
|
||||
'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[]
|
||||
onPriceUpdate?: (price: number) => void
|
||||
}
|
||||
|
||||
export default function TradingChart({ symbol = 'SOL/USDC', positions = [], onPriceUpdate }: 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 } = 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.addCandlestickSeries({
|
||||
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')
|
||||
|
||||
// Call onPriceUpdate with the latest price if provided
|
||||
if (onPriceUpdate && data.length > 0) {
|
||||
const latestPrice = data[data.length - 1].close
|
||||
onPriceUpdate(latestPrice)
|
||||
}
|
||||
|
||||
// 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>
|
||||
)
|
||||
}
|
||||
218
components/WorkingTradingChart.tsx
Normal file
218
components/WorkingTradingChart.tsx
Normal file
@@ -0,0 +1,218 @@
|
||||
'use client'
|
||||
import React, { useEffect, useRef, useState } from 'react'
|
||||
|
||||
interface CandlestickData {
|
||||
time: string
|
||||
open: number
|
||||
high: number
|
||||
low: number
|
||||
close: number
|
||||
}
|
||||
|
||||
interface TradingChartProps {
|
||||
symbol?: string
|
||||
positions?: any[]
|
||||
}
|
||||
|
||||
export default function WorkingTradingChart({ symbol = 'SOL/USDC', positions = [] }: TradingChartProps) {
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null)
|
||||
const [status, setStatus] = useState('Initializing...')
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
try {
|
||||
setStatus('Generating data...')
|
||||
|
||||
// Generate sample candlestick data
|
||||
const data: CandlestickData[] = []
|
||||
const basePrice = 166.5
|
||||
let currentPrice = basePrice
|
||||
const today = new Date()
|
||||
|
||||
for (let i = 29; i >= 0; i--) {
|
||||
const date = new Date(today)
|
||||
date.setDate(date.getDate() - i)
|
||||
|
||||
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: date.toISOString().split('T')[0],
|
||||
open: Number(open.toFixed(2)),
|
||||
high: Number(high.toFixed(2)),
|
||||
low: Number(low.toFixed(2)),
|
||||
close: Number(close.toFixed(2)),
|
||||
})
|
||||
|
||||
currentPrice = close
|
||||
}
|
||||
|
||||
setStatus('Drawing chart...')
|
||||
drawChart(data)
|
||||
setStatus('Chart ready!')
|
||||
|
||||
} catch (err) {
|
||||
const errorMessage = err instanceof Error ? err.message : String(err)
|
||||
setError(errorMessage)
|
||||
setStatus(`Error: ${errorMessage}`)
|
||||
console.error('Chart error:', err)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const drawChart = (data: CandlestickData[]) => {
|
||||
const canvas = canvasRef.current
|
||||
if (!canvas) {
|
||||
throw new Error('Canvas element not found')
|
||||
}
|
||||
|
||||
const ctx = canvas.getContext('2d')
|
||||
if (!ctx) {
|
||||
throw new Error('Could not get 2D context')
|
||||
}
|
||||
|
||||
// Set canvas size
|
||||
canvas.width = 800
|
||||
canvas.height = 400
|
||||
|
||||
// Clear canvas
|
||||
ctx.fillStyle = '#1a1a1a'
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height)
|
||||
|
||||
if (data.length === 0) {
|
||||
ctx.fillStyle = '#ffffff'
|
||||
ctx.font = '16px Arial'
|
||||
ctx.textAlign = 'center'
|
||||
ctx.fillText('No data available', canvas.width / 2, canvas.height / 2)
|
||||
return
|
||||
}
|
||||
|
||||
// Calculate price range
|
||||
const prices = data.flatMap(d => [d.open, d.high, d.low, d.close])
|
||||
const minPrice = Math.min(...prices)
|
||||
const maxPrice = Math.max(...prices)
|
||||
const priceRange = maxPrice - minPrice
|
||||
const padding = priceRange * 0.1
|
||||
|
||||
// Chart dimensions
|
||||
const chartLeft = 60
|
||||
const chartRight = canvas.width - 40
|
||||
const chartTop = 40
|
||||
const chartBottom = canvas.height - 60
|
||||
const chartWidth = chartRight - chartLeft
|
||||
const chartHeight = chartBottom - chartTop
|
||||
|
||||
// Draw grid lines
|
||||
ctx.strokeStyle = '#333333'
|
||||
ctx.lineWidth = 1
|
||||
|
||||
// Horizontal grid lines (price levels)
|
||||
for (let i = 0; i <= 5; i++) {
|
||||
const y = chartTop + (chartHeight / 5) * i
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(chartLeft, y)
|
||||
ctx.lineTo(chartRight, y)
|
||||
ctx.stroke()
|
||||
|
||||
// Price labels
|
||||
const price = maxPrice + padding - ((maxPrice + padding - (minPrice - padding)) / 5) * i
|
||||
ctx.fillStyle = '#888888'
|
||||
ctx.font = '12px Arial'
|
||||
ctx.textAlign = 'right'
|
||||
ctx.fillText(price.toFixed(2), chartLeft - 10, y + 4)
|
||||
}
|
||||
|
||||
// Vertical grid lines (time)
|
||||
const timeStep = Math.max(1, Math.floor(data.length / 6))
|
||||
for (let i = 0; i < data.length; i += timeStep) {
|
||||
const x = chartLeft + (chartWidth / (data.length - 1)) * i
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(x, chartTop)
|
||||
ctx.lineTo(x, chartBottom)
|
||||
ctx.stroke()
|
||||
|
||||
// Time labels
|
||||
ctx.fillStyle = '#888888'
|
||||
ctx.font = '12px Arial'
|
||||
ctx.textAlign = 'center'
|
||||
ctx.fillText(data[i].time.split('-')[1] + '/' + data[i].time.split('-')[2], x, chartBottom + 20)
|
||||
}
|
||||
|
||||
// Draw candlesticks
|
||||
const candleWidth = Math.max(2, chartWidth / data.length * 0.6)
|
||||
|
||||
data.forEach((candle, index) => {
|
||||
const x = chartLeft + (chartWidth / (data.length - 1)) * index
|
||||
const openY = chartBottom - ((candle.open - (minPrice - padding)) / (maxPrice + padding - (minPrice - padding))) * chartHeight
|
||||
const closeY = chartBottom - ((candle.close - (minPrice - padding)) / (maxPrice + padding - (minPrice - padding))) * chartHeight
|
||||
const highY = chartBottom - ((candle.high - (minPrice - padding)) / (maxPrice + padding - (minPrice - padding))) * chartHeight
|
||||
const lowY = chartBottom - ((candle.low - (minPrice - padding)) / (maxPrice + padding - (minPrice - padding))) * chartHeight
|
||||
|
||||
const isGreen = candle.close > candle.open
|
||||
ctx.strokeStyle = isGreen ? '#26a69a' : '#ef5350'
|
||||
ctx.fillStyle = isGreen ? '#26a69a' : '#ef5350'
|
||||
ctx.lineWidth = 1
|
||||
|
||||
// Draw wick
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(x, highY)
|
||||
ctx.lineTo(x, lowY)
|
||||
ctx.stroke()
|
||||
|
||||
// Draw body
|
||||
const bodyTop = Math.min(openY, closeY)
|
||||
const bodyHeight = Math.abs(closeY - openY)
|
||||
|
||||
if (bodyHeight < 1) {
|
||||
// Doji - draw a line
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(x - candleWidth / 2, openY)
|
||||
ctx.lineTo(x + candleWidth / 2, openY)
|
||||
ctx.stroke()
|
||||
} else {
|
||||
ctx.fillRect(x - candleWidth / 2, bodyTop, candleWidth, bodyHeight)
|
||||
}
|
||||
})
|
||||
|
||||
// Draw chart border
|
||||
ctx.strokeStyle = '#555555'
|
||||
ctx.lineWidth = 1
|
||||
ctx.strokeRect(chartLeft, chartTop, chartWidth, chartHeight)
|
||||
|
||||
// Draw title
|
||||
ctx.fillStyle = '#ffffff'
|
||||
ctx.font = 'bold 16px Arial'
|
||||
ctx.textAlign = 'left'
|
||||
ctx.fillText(symbol, chartLeft, 25)
|
||||
|
||||
// Draw current price
|
||||
const currentPrice = data[data.length - 1].close
|
||||
ctx.fillStyle = '#26a69a'
|
||||
ctx.font = 'bold 14px Arial'
|
||||
ctx.textAlign = 'right'
|
||||
ctx.fillText(`$${currentPrice.toFixed(2)}`, chartRight, 25)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<div className="mb-4">
|
||||
<div className="text-white text-sm">Status: <span className="text-green-400">{status}</span></div>
|
||||
{error && (
|
||||
<div className="text-red-400 text-sm mt-2">Error: {error}</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="bg-gray-800 rounded-lg p-4">
|
||||
<canvas
|
||||
ref={canvasRef}
|
||||
className="w-full h-auto border border-gray-600 rounded"
|
||||
style={{ maxWidth: '100%', height: 'auto' }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user