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:
mindesbunister
2025-07-16 16:35:09 +02:00
parent befe860188
commit bd04e0b6a8
24 changed files with 2091 additions and 9 deletions

View 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>
)
}

View 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
View 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>
)
}

View 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>
)
}