FINAL FIX: Docker API URL resolution for internal service communication
- Fixed internal API calls in Docker environment to use port 3000 instead of 9000 - Added DOCKER_ENV detection to properly route internal fetch requests - Resolves ECONNREFUSED errors when APIs try to call each other within container - Trade validation now works correctly in Docker: 5 USD position validates properly - Successfully tested: amountUSD field properly passed through validation pipeline - Both development and Docker environments now fully functional
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"
|
||||||
|
}
|
||||||
|
}
|
||||||
85
app/api-test/page.tsx
Normal file
85
app/api-test/page.tsx
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
'use client'
|
||||||
|
import React, { useEffect, useRef, useState } from 'react'
|
||||||
|
|
||||||
|
export default function ChartAPITest() {
|
||||||
|
const chartContainerRef = useRef<HTMLDivElement>(null)
|
||||||
|
const [logs, setLogs] = useState<string[]>([])
|
||||||
|
|
||||||
|
const addLog = (message: string) => {
|
||||||
|
console.log(message)
|
||||||
|
setLogs(prev => [...prev, `${new Date().toLocaleTimeString()}: ${message}`])
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!chartContainerRef.current) return
|
||||||
|
|
||||||
|
const testLightweightCharts = async () => {
|
||||||
|
try {
|
||||||
|
addLog('Importing lightweight-charts...')
|
||||||
|
|
||||||
|
const LightweightCharts = await import('lightweight-charts')
|
||||||
|
addLog('Import successful')
|
||||||
|
|
||||||
|
// Log what's available in the import
|
||||||
|
addLog('Available exports: ' + Object.keys(LightweightCharts).join(', '))
|
||||||
|
|
||||||
|
const { createChart } = LightweightCharts
|
||||||
|
addLog('createChart function available: ' + (typeof createChart))
|
||||||
|
|
||||||
|
// Create chart
|
||||||
|
const chart = createChart(chartContainerRef.current!, {
|
||||||
|
width: 600,
|
||||||
|
height: 300,
|
||||||
|
})
|
||||||
|
addLog('Chart created')
|
||||||
|
|
||||||
|
// Log chart methods
|
||||||
|
const chartMethods = Object.getOwnPropertyNames(Object.getPrototypeOf(chart))
|
||||||
|
addLog('Chart methods: ' + chartMethods.join(', '))
|
||||||
|
|
||||||
|
// Try to find the correct method for adding series
|
||||||
|
if ('addCandlestickSeries' in chart) {
|
||||||
|
addLog('addCandlestickSeries method found!')
|
||||||
|
} else if ('addCandles' in chart) {
|
||||||
|
addLog('addCandles method found!')
|
||||||
|
} else if ('addSeries' in chart) {
|
||||||
|
addLog('addSeries method found!')
|
||||||
|
} else {
|
||||||
|
addLog('No obvious candlestick method found')
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||||
|
addLog(`Error: ${errorMessage}`)
|
||||||
|
console.error('Error:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
testLightweightCharts()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-900 p-8">
|
||||||
|
<h1 className="text-white text-2xl mb-4">Lightweight Charts API Test</h1>
|
||||||
|
|
||||||
|
<div className="mb-4">
|
||||||
|
<div
|
||||||
|
ref={chartContainerRef}
|
||||||
|
className="bg-gray-800 border border-gray-600 rounded"
|
||||||
|
style={{ width: '600px', height: '300px' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h2 className="text-white text-lg mb-2">API Investigation Logs</h2>
|
||||||
|
<div className="bg-gray-800 p-4 rounded max-h-96 overflow-y-auto">
|
||||||
|
{logs.map((log, index) => (
|
||||||
|
<div key={index} className="text-gray-300 text-sm font-mono mb-1">
|
||||||
|
{log}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -18,10 +18,10 @@ export async function POST(request) {
|
|||||||
toCoin
|
toCoin
|
||||||
} = body
|
} = body
|
||||||
|
|
||||||
// Get the base URL from the request
|
// For Docker environment, use internal port 3000. For dev, use the host header
|
||||||
const host = request.headers.get('host') || 'localhost:3000'
|
const host = request.headers.get('host') || 'localhost:3000'
|
||||||
const protocol = host.includes('localhost') ? 'http' : 'https'
|
const isDocker = process.env.DOCKER_ENV === 'true'
|
||||||
const baseUrl = `${protocol}://${host}`
|
const baseUrl = isDocker ? 'http://localhost:3000' : `http://${host}`
|
||||||
|
|
||||||
console.log('🔄 Execute DEX trade request:', {
|
console.log('🔄 Execute DEX trade request:', {
|
||||||
symbol,
|
symbol,
|
||||||
|
|||||||
@@ -106,10 +106,10 @@ export async function POST(request) {
|
|||||||
const body = await request.json()
|
const body = await request.json()
|
||||||
const { action, orderId, ...orderData } = body
|
const { action, orderId, ...orderData } = body
|
||||||
|
|
||||||
// Get the base URL from the request
|
// For Docker environment, use internal port 3000. For dev, use the host header
|
||||||
const host = request.headers.get('host') || 'localhost:3000'
|
const host = request.headers.get('host') || 'localhost:3000'
|
||||||
const protocol = host.includes('localhost') ? 'http' : 'https'
|
const isDocker = process.env.DOCKER_ENV === 'true'
|
||||||
const baseUrl = `${protocol}://${host}`
|
const baseUrl = isDocker ? 'http://localhost:3000' : `http://${host}`
|
||||||
|
|
||||||
if (action === 'add') {
|
if (action === 'add') {
|
||||||
// Load existing orders
|
// Load existing orders
|
||||||
|
|||||||
@@ -7,10 +7,10 @@ export async function POST(request) {
|
|||||||
|
|
||||||
console.log(`🔍 Validating trade: ${side} ${amount} ${symbol} (USD: ${amountUSD})`)
|
console.log(`🔍 Validating trade: ${side} ${amount} ${symbol} (USD: ${amountUSD})`)
|
||||||
|
|
||||||
// Get the base URL from the request or use localhost for development
|
// For Docker environment, use internal port 3000. For dev, use the host header
|
||||||
const host = request.headers.get('host') || 'localhost:3000'
|
const host = request.headers.get('host') || 'localhost:3000'
|
||||||
const protocol = host.includes('localhost') ? 'http' : 'https'
|
const isDocker = process.env.DOCKER_ENV === 'true'
|
||||||
const baseUrl = `${protocol}://${host}`
|
const baseUrl = isDocker ? 'http://localhost:3000' : `http://${host}`
|
||||||
|
|
||||||
// Fetch real wallet balance from the wallet API
|
// Fetch real wallet balance from the wallet API
|
||||||
let walletBalance
|
let walletBalance
|
||||||
|
|||||||
12
app/canvas-chart/page.tsx
Normal file
12
app/canvas-chart/page.tsx
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
'use client'
|
||||||
|
import React from 'react'
|
||||||
|
import SimpleTradingChart from '../../components/SimpleTradingChart'
|
||||||
|
|
||||||
|
export default function SimpleChartPage() {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-900 p-8">
|
||||||
|
<h1 className="text-white text-3xl mb-6">Simple Canvas Chart Test</h1>
|
||||||
|
<SimpleTradingChart symbol="SOL/USDC" positions={[]} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
96
app/cdn-test/page.tsx
Normal file
96
app/cdn-test/page.tsx
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
'use client'
|
||||||
|
import React, { useEffect, useRef, useState } from 'react'
|
||||||
|
|
||||||
|
export default function StandaloneTest() {
|
||||||
|
const chartContainerRef = useRef<HTMLDivElement>(null)
|
||||||
|
const [status, setStatus] = useState('Starting...')
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const initChart = async () => {
|
||||||
|
try {
|
||||||
|
setStatus('Testing CDN version...')
|
||||||
|
|
||||||
|
// Try using the CDN version instead
|
||||||
|
if (typeof window !== 'undefined' && !window.LightweightCharts) {
|
||||||
|
setStatus('Loading CDN script...')
|
||||||
|
|
||||||
|
const script = document.createElement('script')
|
||||||
|
script.src = 'https://unpkg.com/lightweight-charts@5.0.8/dist/lightweight-charts.standalone.production.js'
|
||||||
|
script.onload = () => {
|
||||||
|
setStatus('CDN loaded, creating chart...')
|
||||||
|
createChartWithCDN()
|
||||||
|
}
|
||||||
|
script.onerror = () => {
|
||||||
|
setStatus('CDN load failed')
|
||||||
|
}
|
||||||
|
document.head.appendChild(script)
|
||||||
|
} else if (window.LightweightCharts) {
|
||||||
|
setStatus('CDN already loaded, creating chart...')
|
||||||
|
createChartWithCDN()
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error:', error)
|
||||||
|
setStatus(`Error: ${error}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const createChartWithCDN = () => {
|
||||||
|
try {
|
||||||
|
if (!chartContainerRef.current) {
|
||||||
|
setStatus('No container')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const { createChart, CandlestickSeries } = (window as any).LightweightCharts
|
||||||
|
|
||||||
|
setStatus('Creating chart with CDN...')
|
||||||
|
const chart = createChart(chartContainerRef.current, {
|
||||||
|
width: 800,
|
||||||
|
height: 400,
|
||||||
|
layout: {
|
||||||
|
background: { color: '#1a1a1a' },
|
||||||
|
textColor: '#ffffff',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
setStatus('Adding series...')
|
||||||
|
const series = chart.addSeries(CandlestickSeries, {
|
||||||
|
upColor: '#26a69a',
|
||||||
|
downColor: '#ef5350',
|
||||||
|
})
|
||||||
|
|
||||||
|
setStatus('Setting data...')
|
||||||
|
series.setData([
|
||||||
|
{ time: '2025-07-14', open: 100, high: 105, low: 95, close: 102 },
|
||||||
|
{ time: '2025-07-15', open: 102, high: 107, low: 98, close: 104 },
|
||||||
|
{ time: '2025-07-16', open: 104, high: 109, low: 101, close: 106 },
|
||||||
|
])
|
||||||
|
|
||||||
|
setStatus('Chart created successfully!')
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('CDN chart error:', error)
|
||||||
|
setStatus(`CDN Error: ${error}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
initChart()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-900 p-8">
|
||||||
|
<h1 className="text-white text-3xl mb-4">CDN Chart Test</h1>
|
||||||
|
<div className="text-green-400 text-lg mb-4">Status: {status}</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
ref={chartContainerRef}
|
||||||
|
className="border-2 border-blue-500 bg-gray-800"
|
||||||
|
style={{
|
||||||
|
width: '800px',
|
||||||
|
height: '400px',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
111
app/chart-debug/page.tsx
Normal file
111
app/chart-debug/page.tsx
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
'use client'
|
||||||
|
import React, { useEffect, useRef, useState } from 'react'
|
||||||
|
|
||||||
|
export default function ChartDebug() {
|
||||||
|
const chartContainerRef = useRef<HTMLDivElement>(null)
|
||||||
|
const [logs, setLogs] = useState<string[]>([])
|
||||||
|
const [chartCreated, setChartCreated] = useState(false)
|
||||||
|
|
||||||
|
const addLog = (message: string) => {
|
||||||
|
console.log(message)
|
||||||
|
setLogs(prev => [...prev, `${new Date().toLocaleTimeString()}: ${message}`])
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!chartContainerRef.current) {
|
||||||
|
addLog('Chart container ref not available')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const initChart = async () => {
|
||||||
|
try {
|
||||||
|
addLog('Starting chart initialization...')
|
||||||
|
|
||||||
|
// Import lightweight-charts
|
||||||
|
const LightweightCharts = await import('lightweight-charts')
|
||||||
|
addLog('Lightweight charts imported successfully')
|
||||||
|
|
||||||
|
const { createChart, CandlestickSeries } = LightweightCharts
|
||||||
|
addLog('createChart and CandlestickSeries extracted')
|
||||||
|
|
||||||
|
// Create chart with minimal options
|
||||||
|
const chart = createChart(chartContainerRef.current!, {
|
||||||
|
width: 600,
|
||||||
|
height: 300,
|
||||||
|
})
|
||||||
|
addLog('Chart created successfully')
|
||||||
|
setChartCreated(true)
|
||||||
|
|
||||||
|
// Add candlestick series with the correct v5 API
|
||||||
|
const candlestickSeries = chart.addSeries(CandlestickSeries, {
|
||||||
|
upColor: '#26a69a',
|
||||||
|
downColor: '#ef5350',
|
||||||
|
borderDownColor: '#ef5350',
|
||||||
|
borderUpColor: '#26a69a',
|
||||||
|
wickDownColor: '#ef5350',
|
||||||
|
wickUpColor: '#26a69a',
|
||||||
|
})
|
||||||
|
addLog('Candlestick series added')
|
||||||
|
|
||||||
|
// Very simple test data
|
||||||
|
const testData = [
|
||||||
|
{ time: '2023-01-01', open: 100, high: 110, low: 95, close: 105 },
|
||||||
|
{ time: '2023-01-02', open: 105, high: 115, low: 100, close: 110 },
|
||||||
|
{ time: '2023-01-03', open: 110, high: 120, low: 105, close: 115 },
|
||||||
|
{ time: '2023-01-04', open: 115, high: 125, low: 110, close: 120 },
|
||||||
|
{ time: '2023-01-05', open: 120, high: 130, low: 115, close: 125 },
|
||||||
|
]
|
||||||
|
|
||||||
|
addLog(`Setting data with ${testData.length} points`)
|
||||||
|
candlestickSeries.setData(testData)
|
||||||
|
addLog('Data set successfully - chart should be visible now')
|
||||||
|
|
||||||
|
// Cleanup function
|
||||||
|
return () => {
|
||||||
|
addLog('Cleaning up chart')
|
||||||
|
chart.remove()
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||||
|
addLog(`Error: ${errorMessage}`)
|
||||||
|
console.error('Chart error:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
initChart()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-900 p-8">
|
||||||
|
<h1 className="text-white text-2xl mb-4">Chart Debug Test</h1>
|
||||||
|
|
||||||
|
<div className="mb-4">
|
||||||
|
<h2 className="text-white text-lg mb-2">Status</h2>
|
||||||
|
<div className="text-gray-400">
|
||||||
|
Chart Created: {chartCreated ? '✅ Yes' : '❌ No'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mb-4">
|
||||||
|
<h2 className="text-white text-lg mb-2">Chart Container</h2>
|
||||||
|
<div
|
||||||
|
ref={chartContainerRef}
|
||||||
|
className="bg-gray-800 border border-gray-600 rounded"
|
||||||
|
style={{ width: '600px', height: '300px' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h2 className="text-white text-lg mb-2">Debug Logs</h2>
|
||||||
|
<div className="bg-gray-800 p-4 rounded max-h-60 overflow-y-auto">
|
||||||
|
{logs.map((log, index) => (
|
||||||
|
<div key={index} className="text-gray-300 text-sm font-mono">
|
||||||
|
{log}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
14
app/chart-test/page.tsx
Normal file
14
app/chart-test/page.tsx
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
'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>
|
||||||
|
)
|
||||||
|
}
|
||||||
198
app/chart-trading/page.tsx
Normal file
198
app/chart-trading/page.tsx
Normal file
@@ -0,0 +1,198 @@
|
|||||||
|
'use client'
|
||||||
|
import React, { useState, useEffect } from 'react'
|
||||||
|
import TradingChart from '../../components/TradingChart'
|
||||||
|
import CompactTradingPanel from '../../components/CompactTradingPanel'
|
||||||
|
import PositionsPanel from '../../components/PositionsPanel'
|
||||||
|
|
||||||
|
export default function ChartTradingPage() {
|
||||||
|
const [currentPrice, setCurrentPrice] = useState(166.21)
|
||||||
|
const [positions, setPositions] = useState([])
|
||||||
|
const [selectedSymbol, setSelectedSymbol] = useState('SOL')
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchPositions()
|
||||||
|
const interval = setInterval(fetchPositions, 10000) // Update every 10 seconds
|
||||||
|
return () => clearInterval(interval)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const fetchPositions = async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/trading/positions')
|
||||||
|
const data = await response.json()
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
setPositions(data.positions || [])
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch positions:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleTrade = async (tradeData: any) => {
|
||||||
|
try {
|
||||||
|
console.log('Executing trade:', tradeData)
|
||||||
|
|
||||||
|
// For perpetual trades, use the execute-perp endpoint
|
||||||
|
const response = await fetch('/api/trading/execute-perp', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(tradeData)
|
||||||
|
})
|
||||||
|
|
||||||
|
const result = await response.json()
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
alert(`Trade executed successfully! ${result.message}`)
|
||||||
|
fetchPositions() // Refresh positions
|
||||||
|
} else {
|
||||||
|
alert(`Trade failed: ${result.error || result.message}`)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Trade execution error:', error)
|
||||||
|
alert('Trade execution failed. Please try again.')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlePriceUpdate = (price: number) => {
|
||||||
|
setCurrentPrice(price)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<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">Trading Terminal</h1>
|
||||||
|
|
||||||
|
{/* Symbol Selector */}
|
||||||
|
<div className="flex space-x-2">
|
||||||
|
{['SOL', 'BTC', 'ETH'].map(symbol => (
|
||||||
|
<button
|
||||||
|
key={symbol}
|
||||||
|
onClick={() => setSelectedSymbol(symbol)}
|
||||||
|
className={`px-4 py-2 rounded-lg text-sm font-medium transition-all ${
|
||||||
|
selectedSymbol === symbol
|
||||||
|
? 'bg-blue-600 text-white'
|
||||||
|
: 'bg-gray-700 text-gray-300 hover:bg-gray-600'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{symbol}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</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">Market Open</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-gray-400">
|
||||||
|
Server Time: {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">
|
||||||
|
<TradingChart
|
||||||
|
symbol={selectedSymbol}
|
||||||
|
positions={positions}
|
||||||
|
onPriceUpdate={handlePriceUpdate}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Trading Panel (30% width) */}
|
||||||
|
<div className="w-96 border-l border-gray-700 p-4 space-y-4">
|
||||||
|
<CompactTradingPanel
|
||||||
|
symbol={selectedSymbol}
|
||||||
|
currentPrice={currentPrice}
|
||||||
|
onTrade={handleTrade}
|
||||||
|
/>
|
||||||
|
</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
|
||||||
|
</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="text-green-400">+$0.00</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: any) => (
|
||||||
|
<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}
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-gray-400">
|
||||||
|
Size: {position.amount} • Entry: ${position.entryPrice?.toFixed(2)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="text-right">
|
||||||
|
<div className="text-white font-medium">
|
||||||
|
${position.totalValue?.toFixed(2) || '0.00'}
|
||||||
|
</div>
|
||||||
|
<div className={`text-sm ${
|
||||||
|
(position.unrealizedPnl || 0) >= 0 ? 'text-green-400' : 'text-red-400'
|
||||||
|
}`}>
|
||||||
|
{(position.unrealizedPnl || 0) >= 0 ? '+' : ''}${(position.unrealizedPnl || 0).toFixed(2)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex space-x-2">
|
||||||
|
<button className="px-3 py-1 bg-gray-700 text-gray-300 rounded text-sm hover:bg-gray-600">
|
||||||
|
Modify
|
||||||
|
</button>
|
||||||
|
<button className="px-3 py-1 bg-red-600 text-white rounded text-sm hover:bg-red-700">
|
||||||
|
Close
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
119
app/debug-chart/page.tsx
Normal file
119
app/debug-chart/page.tsx
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
'use client'
|
||||||
|
import React, { useEffect, useRef, useState } from 'react'
|
||||||
|
|
||||||
|
export default function DebugChart() {
|
||||||
|
const chartContainerRef = useRef<HTMLDivElement>(null)
|
||||||
|
const [logs, setLogs] = useState<string[]>([])
|
||||||
|
const [error, setError] = useState<string | null>(null)
|
||||||
|
|
||||||
|
const addLog = (message: string) => {
|
||||||
|
console.log(message)
|
||||||
|
setLogs(prev => [...prev, `${new Date().toLocaleTimeString()}: ${message}`])
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
addLog('Component mounted')
|
||||||
|
|
||||||
|
if (!chartContainerRef.current) {
|
||||||
|
addLog('ERROR: No chart container ref')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
addLog('Chart container found')
|
||||||
|
|
||||||
|
const initChart = async () => {
|
||||||
|
try {
|
||||||
|
addLog('Starting chart initialization...')
|
||||||
|
|
||||||
|
addLog('Importing lightweight-charts...')
|
||||||
|
const LightweightChartsModule = await import('lightweight-charts')
|
||||||
|
addLog('Import successful')
|
||||||
|
|
||||||
|
addLog('Available exports: ' + Object.keys(LightweightChartsModule).join(', '))
|
||||||
|
|
||||||
|
const { createChart, CandlestickSeries } = LightweightChartsModule
|
||||||
|
addLog('Extracted createChart and CandlestickSeries')
|
||||||
|
|
||||||
|
addLog('Creating chart...')
|
||||||
|
const chart = createChart(chartContainerRef.current!, {
|
||||||
|
width: 600,
|
||||||
|
height: 300,
|
||||||
|
layout: {
|
||||||
|
textColor: '#ffffff',
|
||||||
|
background: { color: '#1a1a1a' },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
addLog('Chart created successfully')
|
||||||
|
|
||||||
|
addLog('Adding candlestick series...')
|
||||||
|
const candlestickSeries = chart.addSeries(CandlestickSeries, {
|
||||||
|
upColor: '#26a69a',
|
||||||
|
downColor: '#ef5350',
|
||||||
|
})
|
||||||
|
addLog('Series added successfully')
|
||||||
|
|
||||||
|
addLog('Generating test data...')
|
||||||
|
const data = [
|
||||||
|
{ time: '2025-07-10', open: 100, high: 110, low: 95, close: 105 },
|
||||||
|
{ time: '2025-07-11', open: 105, high: 115, low: 100, close: 110 },
|
||||||
|
{ time: '2025-07-12', open: 110, high: 120, low: 105, close: 115 },
|
||||||
|
{ time: '2025-07-13', open: 115, high: 125, low: 110, close: 118 },
|
||||||
|
{ time: '2025-07-14', open: 118, high: 128, low: 113, close: 122 },
|
||||||
|
{ time: '2025-07-15', open: 122, high: 132, low: 117, close: 125 },
|
||||||
|
{ time: '2025-07-16', open: 125, high: 135, low: 120, close: 130 },
|
||||||
|
]
|
||||||
|
addLog(`Generated ${data.length} data points`)
|
||||||
|
|
||||||
|
addLog('Setting data on series...')
|
||||||
|
candlestickSeries.setData(data)
|
||||||
|
addLog('Data set successfully - chart should be visible!')
|
||||||
|
|
||||||
|
addLog('Chart initialization complete')
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
const errorMessage = err instanceof Error ? err.message : String(err)
|
||||||
|
const errorStack = err instanceof Error ? err.stack : 'No stack trace'
|
||||||
|
addLog(`ERROR: ${errorMessage}`)
|
||||||
|
console.error('Chart initialization error:', err)
|
||||||
|
setError(`${errorMessage}\n\nStack: ${errorStack}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
initChart()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-900 p-8">
|
||||||
|
<h1 className="text-white text-2xl mb-4">Debug Chart Test</h1>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
||||||
|
<div>
|
||||||
|
<h2 className="text-white text-lg mb-2">Chart</h2>
|
||||||
|
<div
|
||||||
|
ref={chartContainerRef}
|
||||||
|
className="bg-gray-800 border border-gray-600 rounded"
|
||||||
|
style={{ width: '600px', height: '300px' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h2 className="text-white text-lg mb-2">Debug Logs</h2>
|
||||||
|
<div className="bg-gray-800 p-4 rounded h-80 overflow-y-auto">
|
||||||
|
{logs.map((log, index) => (
|
||||||
|
<div key={index} className="text-gray-300 text-sm font-mono mb-1">
|
||||||
|
{log}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div className="mt-4 bg-red-900/20 border border-red-500 p-4 rounded">
|
||||||
|
<h3 className="text-red-400 font-semibold mb-2">Error Details:</h3>
|
||||||
|
<pre className="text-red-300 text-xs whitespace-pre-wrap">{error}</pre>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
72
app/direct-chart/page.tsx
Normal file
72
app/direct-chart/page.tsx
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
'use client'
|
||||||
|
import React, { useEffect } from 'react'
|
||||||
|
|
||||||
|
export default function DirectChart() {
|
||||||
|
useEffect(() => {
|
||||||
|
const container = document.getElementById('chart-container')
|
||||||
|
if (!container) return
|
||||||
|
|
||||||
|
const initChart = async () => {
|
||||||
|
try {
|
||||||
|
console.log('Starting direct chart...')
|
||||||
|
|
||||||
|
// Import with explicit .mjs extension
|
||||||
|
const module = await import('lightweight-charts/dist/lightweight-charts.production.mjs')
|
||||||
|
console.log('Module loaded:', module)
|
||||||
|
|
||||||
|
const { createChart, CandlestickSeries } = module
|
||||||
|
console.log('Functions extracted')
|
||||||
|
|
||||||
|
const chart = createChart(container, {
|
||||||
|
width: 800,
|
||||||
|
height: 400,
|
||||||
|
layout: {
|
||||||
|
background: { color: '#1a1a1a' },
|
||||||
|
textColor: '#ffffff',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
console.log('Chart created')
|
||||||
|
|
||||||
|
const series = chart.addSeries(CandlestickSeries, {
|
||||||
|
upColor: '#26a69a',
|
||||||
|
downColor: '#ef5350',
|
||||||
|
})
|
||||||
|
console.log('Series added')
|
||||||
|
|
||||||
|
series.setData([
|
||||||
|
{ time: '2025-07-14', open: 100, high: 105, low: 95, close: 102 },
|
||||||
|
{ time: '2025-07-15', open: 102, high: 107, low: 98, close: 104 },
|
||||||
|
{ time: '2025-07-16', open: 104, high: 109, low: 101, close: 106 },
|
||||||
|
])
|
||||||
|
console.log('Data set - should be visible!')
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Direct chart error:', error)
|
||||||
|
const statusDiv = document.getElementById('status')
|
||||||
|
if (statusDiv) {
|
||||||
|
statusDiv.textContent = `Error: ${error}`
|
||||||
|
statusDiv.className = 'text-red-400 text-lg mb-4'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add a small delay to ensure DOM is ready
|
||||||
|
setTimeout(initChart, 100)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-900 p-8">
|
||||||
|
<h1 className="text-white text-3xl mb-4">Direct DOM Chart</h1>
|
||||||
|
<div id="status" className="text-yellow-400 text-lg mb-4">Loading...</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
id="chart-container"
|
||||||
|
className="border-2 border-green-500 bg-gray-800"
|
||||||
|
style={{
|
||||||
|
width: '800px',
|
||||||
|
height: '400px',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</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>
|
||||||
|
)
|
||||||
|
}
|
||||||
78
app/minimal-chart/page.tsx
Normal file
78
app/minimal-chart/page.tsx
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
'use client'
|
||||||
|
import React, { useEffect, useRef, useState } from 'react'
|
||||||
|
|
||||||
|
export default function MinimalChartTest() {
|
||||||
|
const chartContainerRef = useRef<HTMLDivElement>(null)
|
||||||
|
const [status, setStatus] = useState('Starting...')
|
||||||
|
const [error, setError] = useState<string | null>(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!chartContainerRef.current) {
|
||||||
|
setStatus('No container ref')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const initChart = async () => {
|
||||||
|
try {
|
||||||
|
setStatus('Loading lightweight-charts...')
|
||||||
|
console.log('Starting chart init...')
|
||||||
|
|
||||||
|
const LightweightCharts = await import('lightweight-charts')
|
||||||
|
console.log('Lightweight charts loaded:', LightweightCharts)
|
||||||
|
setStatus('Charts library loaded')
|
||||||
|
|
||||||
|
const { createChart, CandlestickSeries } = LightweightCharts
|
||||||
|
console.log('createChart:', typeof createChart)
|
||||||
|
console.log('CandlestickSeries:', CandlestickSeries)
|
||||||
|
|
||||||
|
setStatus('Creating chart...')
|
||||||
|
const chart = createChart(chartContainerRef.current!, {
|
||||||
|
width: 800,
|
||||||
|
height: 400,
|
||||||
|
})
|
||||||
|
console.log('Chart created:', chart)
|
||||||
|
setStatus('Chart created')
|
||||||
|
|
||||||
|
setStatus('Adding series...')
|
||||||
|
const series = chart.addSeries(CandlestickSeries, {})
|
||||||
|
console.log('Series created:', series)
|
||||||
|
setStatus('Series added')
|
||||||
|
|
||||||
|
setStatus('Adding data...')
|
||||||
|
const data = [
|
||||||
|
{ time: '2025-01-01', open: 100, high: 110, low: 90, close: 105 },
|
||||||
|
{ time: '2025-01-02', open: 105, high: 115, low: 95, close: 110 },
|
||||||
|
{ time: '2025-01-03', open: 110, high: 120, low: 100, close: 115 },
|
||||||
|
]
|
||||||
|
series.setData(data)
|
||||||
|
console.log('Data set')
|
||||||
|
setStatus('Chart ready!')
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Chart init error:', err)
|
||||||
|
const errorMsg = err instanceof Error ? err.message : String(err)
|
||||||
|
setError(errorMsg)
|
||||||
|
setStatus(`Error: ${errorMsg}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
initChart()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-900 p-8">
|
||||||
|
<h1 className="text-white text-2xl mb-4">Minimal Chart Test</h1>
|
||||||
|
<div className="text-gray-300 mb-4">Status: {status}</div>
|
||||||
|
{error && (
|
||||||
|
<div className="text-red-400 mb-4 p-4 bg-red-900/20 rounded">
|
||||||
|
Error: {error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div
|
||||||
|
ref={chartContainerRef}
|
||||||
|
className="bg-gray-800 border border-gray-600"
|
||||||
|
style={{ width: '800px', height: '400px' }}
|
||||||
|
/>
|
||||||
|
</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>
|
||||||
|
)
|
||||||
|
}
|
||||||
115
app/simple-chart/page.tsx
Normal file
115
app/simple-chart/page.tsx
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
'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>
|
||||||
|
)
|
||||||
|
}
|
||||||
95
app/simple-test/page.tsx
Normal file
95
app/simple-test/page.tsx
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
'use client'
|
||||||
|
import React, { useEffect, useRef, useState } from 'react'
|
||||||
|
|
||||||
|
export default function SimpleTest() {
|
||||||
|
const chartContainerRef = useRef<HTMLDivElement>(null)
|
||||||
|
const [status, setStatus] = useState('Initializing...')
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const initChart = async () => {
|
||||||
|
try {
|
||||||
|
setStatus('Importing library...')
|
||||||
|
|
||||||
|
// Test if we can import the library
|
||||||
|
const module = await import('lightweight-charts')
|
||||||
|
setStatus('Library imported')
|
||||||
|
|
||||||
|
// Test if we can extract functions
|
||||||
|
const { createChart, CandlestickSeries } = module
|
||||||
|
setStatus('Functions extracted')
|
||||||
|
|
||||||
|
if (!chartContainerRef.current) {
|
||||||
|
setStatus('No container element')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setStatus('Creating chart...')
|
||||||
|
|
||||||
|
// Create chart with explicit dimensions
|
||||||
|
const chart = createChart(chartContainerRef.current, {
|
||||||
|
width: 800,
|
||||||
|
height: 400,
|
||||||
|
layout: {
|
||||||
|
background: { color: '#1a1a1a' },
|
||||||
|
textColor: '#ffffff',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
setStatus('Chart created')
|
||||||
|
|
||||||
|
// Add series
|
||||||
|
const series = chart.addSeries(CandlestickSeries, {
|
||||||
|
upColor: '#00ff00',
|
||||||
|
downColor: '#ff0000',
|
||||||
|
})
|
||||||
|
|
||||||
|
setStatus('Series added')
|
||||||
|
|
||||||
|
// Add simple data
|
||||||
|
series.setData([
|
||||||
|
{ time: '2025-07-14', open: 100, high: 105, low: 95, close: 102 },
|
||||||
|
{ time: '2025-07-15', open: 102, high: 107, low: 98, close: 104 },
|
||||||
|
{ time: '2025-07-16', open: 104, high: 109, low: 101, close: 106 },
|
||||||
|
])
|
||||||
|
|
||||||
|
setStatus('Data set - Chart should be visible!')
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error:', error)
|
||||||
|
setStatus(`Error: ${error}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
initChart()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-900 p-8">
|
||||||
|
<h1 className="text-white text-3xl mb-4">Simple Chart Test</h1>
|
||||||
|
<div className="text-green-400 text-lg mb-4">Status: {status}</div>
|
||||||
|
|
||||||
|
<div className="bg-red-500 p-2 mb-4 text-white">
|
||||||
|
This red border should help us see if the container is properly sized
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
ref={chartContainerRef}
|
||||||
|
className="border-4 border-yellow-500 bg-gray-800"
|
||||||
|
style={{
|
||||||
|
width: '800px',
|
||||||
|
height: '400px',
|
||||||
|
display: 'block',
|
||||||
|
position: 'relative'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="text-white p-4">
|
||||||
|
Container content - this should be replaced by the chart
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="text-gray-400 mt-4">
|
||||||
|
Container ref: {chartContainerRef.current ? 'Available' : 'Not available'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
12
app/test-chart/page.tsx
Normal file
12
app/test-chart/page.tsx
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
'use client'
|
||||||
|
import React from 'react'
|
||||||
|
import WorkingTradingChart from '../../components/WorkingTradingChart'
|
||||||
|
|
||||||
|
export default function TestChartPage() {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-900 p-8">
|
||||||
|
<h1 className="text-white text-3xl mb-6">Working Chart Test</h1>
|
||||||
|
<WorkingTradingChart symbol="SOL/USDC" positions={[]} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
16
app/test-trading/page.tsx
Normal file
16
app/test-trading/page.tsx
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
'use client'
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
export default function SimpleTradingPage() {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-900 p-8">
|
||||||
|
<h1 className="text-white text-3xl mb-6">Trading Dashboard</h1>
|
||||||
|
<div className="bg-gray-800 p-6 rounded-lg">
|
||||||
|
<h2 className="text-white text-xl mb-4">Chart Area</h2>
|
||||||
|
<div className="bg-gray-700 h-96 rounded flex items-center justify-center">
|
||||||
|
<span className="text-gray-300">Chart will load here</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
77
app/working-chart/page.tsx
Normal file
77
app/working-chart/page.tsx
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
'use client'
|
||||||
|
import React, { useEffect, useRef } from 'react'
|
||||||
|
|
||||||
|
export default function WorkingChart() {
|
||||||
|
const chartContainerRef = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!chartContainerRef.current) return
|
||||||
|
|
||||||
|
const initChart = async () => {
|
||||||
|
try {
|
||||||
|
const { createChart, CandlestickSeries } = await import('lightweight-charts')
|
||||||
|
|
||||||
|
const chart = createChart(chartContainerRef.current!, {
|
||||||
|
width: 800,
|
||||||
|
height: 400,
|
||||||
|
layout: {
|
||||||
|
textColor: '#ffffff',
|
||||||
|
background: { color: '#1a1a1a' },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const candlestickSeries = chart.addSeries(CandlestickSeries, {
|
||||||
|
upColor: '#26a69a',
|
||||||
|
downColor: '#ef5350',
|
||||||
|
})
|
||||||
|
|
||||||
|
// Simple working data - last 30 days
|
||||||
|
const data = []
|
||||||
|
const today = new Date()
|
||||||
|
let price = 166.5
|
||||||
|
|
||||||
|
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 = price
|
||||||
|
const close = price + 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)),
|
||||||
|
})
|
||||||
|
|
||||||
|
price = close
|
||||||
|
}
|
||||||
|
|
||||||
|
candlestickSeries.setData(data)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
chart.remove()
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Chart error:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
initChart()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-900 p-8">
|
||||||
|
<h1 className="text-white text-2xl mb-4">Working Chart Test</h1>
|
||||||
|
<div
|
||||||
|
ref={chartContainerRef}
|
||||||
|
className="bg-gray-800 border border-gray-600"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
270
components/CompactTradingPanel.tsx
Normal file
270
components/CompactTradingPanel.tsx
Normal file
@@ -0,0 +1,270 @@
|
|||||||
|
'use client'
|
||||||
|
import React, { useState } from 'react'
|
||||||
|
|
||||||
|
interface CompactTradingPanelProps {
|
||||||
|
symbol: string
|
||||||
|
currentPrice: number
|
||||||
|
onTrade?: (tradeData: any) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function CompactTradingPanel({
|
||||||
|
symbol = 'SOL',
|
||||||
|
currentPrice = 166.21,
|
||||||
|
onTrade
|
||||||
|
}: CompactTradingPanelProps) {
|
||||||
|
const [side, setSide] = useState<'LONG' | 'SHORT'>('LONG')
|
||||||
|
const [orderType, setOrderType] = useState<'MARKET' | 'LIMIT'>('MARKET')
|
||||||
|
const [amount, setAmount] = useState('')
|
||||||
|
const [price, setPrice] = useState(currentPrice.toString())
|
||||||
|
const [leverage, setLeverage] = useState(1)
|
||||||
|
const [stopLoss, setStopLoss] = useState('')
|
||||||
|
const [takeProfit, setTakeProfit] = useState('')
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
|
||||||
|
// Update price when currentPrice changes
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (orderType === 'MARKET') {
|
||||||
|
setPrice(currentPrice.toString())
|
||||||
|
}
|
||||||
|
}, [currentPrice, orderType])
|
||||||
|
|
||||||
|
const calculateLiquidationPrice = () => {
|
||||||
|
const entryPrice = parseFloat(price) || currentPrice
|
||||||
|
const leverage_ratio = leverage || 1
|
||||||
|
if (side === 'LONG') {
|
||||||
|
return entryPrice * (1 - 1 / leverage_ratio)
|
||||||
|
} else {
|
||||||
|
return entryPrice * (1 + 1 / leverage_ratio)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const calculatePositionSize = () => {
|
||||||
|
const amt = parseFloat(amount) || 0
|
||||||
|
const entryPrice = parseFloat(price) || currentPrice
|
||||||
|
return amt * entryPrice
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleTrade = async () => {
|
||||||
|
if (!amount || parseFloat(amount) <= 0) {
|
||||||
|
alert('Please enter a valid amount')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(true)
|
||||||
|
try {
|
||||||
|
const tradeData = {
|
||||||
|
symbol,
|
||||||
|
side: side === 'LONG' ? 'BUY' : 'SELL',
|
||||||
|
amount: parseFloat(amount),
|
||||||
|
price: orderType === 'MARKET' ? currentPrice : parseFloat(price),
|
||||||
|
type: orderType.toLowerCase(),
|
||||||
|
leverage,
|
||||||
|
stopLoss: stopLoss ? parseFloat(stopLoss) : undefined,
|
||||||
|
takeProfit: takeProfit ? parseFloat(takeProfit) : undefined,
|
||||||
|
tradingMode: 'PERP'
|
||||||
|
}
|
||||||
|
|
||||||
|
onTrade?.(tradeData)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Trade execution failed:', error)
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const leverageOptions = [1, 2, 3, 5, 10, 20, 25, 50, 100]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-gray-900 rounded-lg p-4 space-y-4">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<h3 className="text-lg font-semibold text-white">{symbol}/USDC</h3>
|
||||||
|
<div className="text-sm text-gray-400">
|
||||||
|
${currentPrice.toFixed(2)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Long/Short Toggle */}
|
||||||
|
<div className="grid grid-cols-2 gap-2">
|
||||||
|
<button
|
||||||
|
onClick={() => setSide('LONG')}
|
||||||
|
className={`p-3 rounded-lg font-semibold transition-all ${
|
||||||
|
side === 'LONG'
|
||||||
|
? 'bg-green-600 text-white'
|
||||||
|
: 'bg-gray-800 text-gray-400 hover:bg-gray-700'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
Long/Buy
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setSide('SHORT')}
|
||||||
|
className={`p-3 rounded-lg font-semibold transition-all ${
|
||||||
|
side === 'SHORT'
|
||||||
|
? 'bg-red-600 text-white'
|
||||||
|
: 'bg-gray-800 text-gray-400 hover:bg-gray-700'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
Short/Sell
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Order Type */}
|
||||||
|
<div className="grid grid-cols-2 gap-2">
|
||||||
|
<button
|
||||||
|
onClick={() => setOrderType('MARKET')}
|
||||||
|
className={`p-2 rounded text-sm transition-all ${
|
||||||
|
orderType === 'MARKET'
|
||||||
|
? 'bg-blue-600 text-white'
|
||||||
|
: 'bg-gray-800 text-gray-400 hover:bg-gray-700'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
Market
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setOrderType('LIMIT')}
|
||||||
|
className={`p-2 rounded text-sm transition-all ${
|
||||||
|
orderType === 'LIMIT'
|
||||||
|
? 'bg-blue-600 text-white'
|
||||||
|
: 'bg-gray-800 text-gray-400 hover:bg-gray-700'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
Limit
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Leverage Slider */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<label className="text-sm text-gray-400">Leverage</label>
|
||||||
|
<span className="text-white font-semibold">{leverage}x</span>
|
||||||
|
</div>
|
||||||
|
<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"
|
||||||
|
/>
|
||||||
|
<div className="flex justify-between text-xs text-gray-500 mt-1">
|
||||||
|
{leverageOptions.map(lev => (
|
||||||
|
<span key={lev}>{lev}x</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Amount Input */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-sm text-gray-400">Amount ({symbol})</label>
|
||||||
|
<div className="relative">
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={amount}
|
||||||
|
onChange={(e) => setAmount(e.target.value)}
|
||||||
|
placeholder="0.00"
|
||||||
|
className="w-full p-3 bg-gray-800 border border-gray-700 rounded-lg text-white placeholder-gray-400 focus:border-blue-500 focus:outline-none"
|
||||||
|
/>
|
||||||
|
<div className="absolute right-3 top-3 flex space-x-1">
|
||||||
|
<button
|
||||||
|
onClick={() => setAmount('0.25')}
|
||||||
|
className="px-2 py-1 text-xs bg-gray-700 text-gray-300 rounded hover:bg-gray-600"
|
||||||
|
>
|
||||||
|
25%
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setAmount('0.5')}
|
||||||
|
className="px-2 py-1 text-xs bg-gray-700 text-gray-300 rounded hover:bg-gray-600"
|
||||||
|
>
|
||||||
|
50%
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setAmount('1.0')}
|
||||||
|
className="px-2 py-1 text-xs bg-gray-700 text-gray-300 rounded hover:bg-gray-600"
|
||||||
|
>
|
||||||
|
MAX
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Price Input (for limit orders) */}
|
||||||
|
{orderType === 'LIMIT' && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-sm text-gray-400">Price (USDC)</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={price}
|
||||||
|
onChange={(e) => setPrice(e.target.value)}
|
||||||
|
placeholder="0.00"
|
||||||
|
className="w-full p-3 bg-gray-800 border border-gray-700 rounded-lg text-white placeholder-gray-400 focus:border-blue-500 focus:outline-none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Stop Loss & Take Profit */}
|
||||||
|
<div className="grid grid-cols-2 gap-2">
|
||||||
|
<div className="space-y-1">
|
||||||
|
<label className="text-xs text-gray-400">Stop Loss</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={stopLoss}
|
||||||
|
onChange={(e) => setStopLoss(e.target.value)}
|
||||||
|
placeholder="Optional"
|
||||||
|
className="w-full p-2 bg-gray-800 border border-gray-700 rounded text-white placeholder-gray-400 text-sm focus:border-red-500 focus:outline-none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<label className="text-xs text-gray-400">Take Profit</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={takeProfit}
|
||||||
|
onChange={(e) => setTakeProfit(e.target.value)}
|
||||||
|
placeholder="Optional"
|
||||||
|
className="w-full p-2 bg-gray-800 border border-gray-700 rounded text-white placeholder-gray-400 text-sm focus:border-green-500 focus:outline-none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Position Info */}
|
||||||
|
<div className="space-y-2 p-3 bg-gray-800 rounded-lg">
|
||||||
|
<div className="flex justify-between text-sm">
|
||||||
|
<span className="text-gray-400">Position Size:</span>
|
||||||
|
<span className="text-white">${calculatePositionSize().toFixed(2)}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between text-sm">
|
||||||
|
<span className="text-gray-400">Liquidation Price:</span>
|
||||||
|
<span className="text-orange-400">${calculateLiquidationPrice().toFixed(2)}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between text-sm">
|
||||||
|
<span className="text-gray-400">Available:</span>
|
||||||
|
<span className="text-white">$5,000.00</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Trade Button */}
|
||||||
|
<button
|
||||||
|
onClick={handleTrade}
|
||||||
|
disabled={loading || !amount}
|
||||||
|
className={`w-full p-4 rounded-lg font-semibold text-white transition-all ${
|
||||||
|
side === 'LONG'
|
||||||
|
? 'bg-green-600 hover:bg-green-700 disabled:bg-gray-600'
|
||||||
|
: 'bg-red-600 hover:bg-red-700 disabled:bg-gray-600'
|
||||||
|
} disabled:cursor-not-allowed`}
|
||||||
|
>
|
||||||
|
{loading ? 'Executing...' : `${side === 'LONG' ? 'Long' : 'Short'} ${symbol}`}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Quick Actions */}
|
||||||
|
<div className="flex space-x-2 text-xs">
|
||||||
|
<button className="flex-1 p-2 bg-gray-800 text-gray-400 rounded hover:bg-gray-700">
|
||||||
|
Close All
|
||||||
|
</button>
|
||||||
|
<button className="flex-1 p-2 bg-gray-800 text-gray-400 rounded hover:bg-gray-700">
|
||||||
|
Reverse
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
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>
|
||||||
|
)
|
||||||
|
}
|
||||||
254
components/TradingChart.tsx
Normal file
254
components/TradingChart.tsx
Normal file
@@ -0,0 +1,254 @@
|
|||||||
|
'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>
|
||||||
|
)
|
||||||
|
}
|
||||||
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