diff --git a/components/mission-control/TradingChart.tsx b/components/mission-control/TradingChart.tsx index 8fa4a5e..824d338 100644 --- a/components/mission-control/TradingChart.tsx +++ b/components/mission-control/TradingChart.tsx @@ -2,10 +2,6 @@ import { useState, useEffect, useRef } from 'react' -// Lazy load lightweight-charts only on client -let createChart: any = null -let lightweightCharts: any = null - interface Trade { id: string date: string @@ -71,18 +67,16 @@ const getCandleLimit = (tf: string) => ({ '15m': 80, '1h': 100, '4h': 60, '1D': export default function TradingChart() { const chartContainerRef = useRef(null) - const chartRef = useRef(null) - const candlestickSeriesRef = useRef | null>(null) - const volumeSeriesRef = useRef | null>(null) - const ema20Ref = useRef | null>(null) - const ema50Ref = useRef | null>(null) - const ema200Ref = useRef | null>(null) + const chartRef = useRef(null) + const candlestickSeriesRef = useRef(null) + const volumeSeriesRef = useRef(null) + const ema20Ref = useRef(null) + const ema50Ref = useRef(null) + const ema200Ref = useRef(null) const [selectedAsset, setSelectedAsset] = useState<'BTC' | 'SOL' | 'ETH'>('BTC') const [selectedTimeframe, setSelectedTimeframe] = useState<'15m' | '1h' | '4h' | '1D'>('1h') - const [secondTimeframe, setSecondTimeframe] = useState<'15m' | '1h' | '4h' | '1D' | null>(null) const [chartData, setChartData] = useState([]) - const [secondChartData, setSecondChartData] = useState([]) const [priceData, setPriceData] = useState({ price: 0, change24h: 0 }) const [loading, setLoading] = useState(true) const [trades, setTrades] = useState([]) @@ -90,187 +84,85 @@ export default function TradingChart() { const [indicators, setIndicators] = useState({ ema20: false, ema50: false, ema200: false, bb: false, rsi: false, macd: false, thoth: true, volume: true, srZones: true, news: false, patterns: false, fib: false, countdown: false, calendar: false, correlation: false, funding: false }) - const [chartReady, setChartReady] = useState(false) const [mounted, setMounted] = useState(false) - // Prevent SSR issues + // Initialize on mount (client only) useEffect(() => { setMounted(true) - }, []) - - // Initialize chart - run only on client - useEffect(() => { - let mounted = true + let chartInstance: any = null + let candleSeries: any = null + let volSeries: any = null + let e20: any = null + let e50: any = null + let e200: any = null + const initChart = async () => { + if (!chartContainerRef.current) return + try { - const charts = await import('lightweight-charts') - if (!mounted) return - createChart = charts.createChart - setChartReady(true) + const { createChart } = await import('lightweight-charts') + + chartInstance = createChart(chartContainerRef.current, { + layout: { background: { color: '#0a0a0f' }, textColor: '#a0a0a0' }, + grid: { vertLines: { color: '#1a1a2e' }, horzLines: { color: '#1a1a2e' } }, + width: chartContainerRef.current.clientWidth, + height: 400, + timeScale: { timeVisible: true, secondsVisible: false }, + rightPriceScale: { borderColor: '#2a2a4e' }, + }) + + candleSeries = chartInstance.addCandlestickSeries({ + upColor: '#22c55e', downColor: '#ef4444', + borderUpColor: '#22c55e', borderDownColor: '#ef4444', + wickUpColor: '#22c55e', wickDownColor: '#ef4444', + }) + + volSeries = chartInstance.addHistogramSeries({ color: '#26a69a', priceFormat: { type: 'volume' }, priceScaleId: '' }) + volSeries.priceScale().applyOptions({ scaleMargins: { top: 0.8, bottom: 0 } }) + + chartRef.current = chartInstance + candlestickSeriesRef.current = candleSeries + volumeSeriesRef.current = volSeries + ema20Ref.current = e20 + ema50Ref.current = e50 + ema200Ref.current = e200 + + const handleResize = () => { + if (chartContainerRef.current) chartInstance.applyOptions({ width: chartContainerRef.current.clientWidth }) + } + window.addEventListener('resize', handleResize) + + // Fetch initial data + fetchChartData() + fetchPriceData() + fetchTrades() + fetchThothView() + } catch (e) { - console.error('Failed to load charts:', e) + console.error('Chart init error:', e) } } - + initChart() - return () => { mounted = false } - }, []) - - // Setup chart after library loads - useEffect(() => { - if (!chartReady || !chartContainerRef.current || !createChart) return - - const chart = createChart(chartContainerRef.current, { - layout: { - background: { color: '#0a0a0f' }, - textColor: '#a0a0a0', - }, - grid: { - vertLines: { color: '#1a1a2e' }, - horzLines: { color: '#1a1a2e' }, - }, - width: chartContainerRef.current.clientWidth, - height: 400, - timeScale: { - timeVisible: true, - secondsVisible: false, - }, - rightPriceScale: { - borderColor: '#2a2a4e', - }, - }) - - const candlestickSeries = chart.addCandlestickSeries({ - upColor: '#22c55e', - downColor: '#ef4444', - borderUpColor: '#22c55e', - borderDownColor: '#ef4444', - wickUpColor: '#22c55e', - wickDownColor: '#ef4444', - }) - - const volumeSeries = chart.addHistogramSeries({ - color: '#26a69a', - priceFormat: { type: 'volume' }, - priceScaleId: '', - }) - - volumeSeries.priceScale().applyOptions({ - scaleMargins: { top: 0.8, bottom: 0 }, - }) - - chartRef.current = chart - candlestickSeriesRef.current = candlestickSeries - volumeSeriesRef.current = volumeSeries - - const handleResize = () => { - if (chartContainerRef.current) { - chart.applyOptions({ width: chartContainerRef.current.clientWidth }) - } - } - - window.addEventListener('resize', handleResize) return () => { - window.removeEventListener('resize', handleResize) - if (chartRef.current) chartRef.current.remove() + if (chartInstance) { + chartInstance.remove() + } } - }, [chartReady]) - - // Fetch data when asset/timeframe changes - useEffect(() => { - fetchChartData() - fetchPriceData() - }, [selectedAsset, selectedTimeframe]) - - // Update chart when data or indicators change - useEffect(() => { - if (!candlestickSeriesRef.current || chartData.length === 0) return - - const candleData: CandlestickData[] = chartData.map(d => ({ - time: (d.time / 1000) number, - open: d.open, - high: d.high, - low: d.low, - close: d.close, - })) - - candlestickSeriesRef.current.setData(candleData) - - // Volume - if (volumeSeriesRef.current && chartData[0]?.volume) { - const volumeData = chartData.map(d => ({ - time: (d.time / 1000) number, - value: d.volume || 0, - color: d.close >= d.open ? 'rgba(34, 197, 94, 0.5)' : 'rgba(239, 68, 68, 0.5)', - })) - volumeSeriesRef.current.setData(volumeData) - volumeSeriesRef.current.applyOptions({ visible: indicators.volume }) - } - - // EMAs - if (indicators.ema20) { - if (!ema20Ref.current && chartRef.current) { - ema20Ref.current = chartRef.current.addLineSeries({ color: '#eab308', lineWidth: 2 }) - } - if (ema20Ref.current) { - const closes = chartData.map(d => d.close) - const ema20 = calculateEMA(closes, 20) - ema20Ref.current.setData(ema20.map((v, i) => ({ time: (chartData[i].time / 1000) number, value: v }))) - ema20Ref.current.applyOptions({ visible: true }) - } - } else if (ema20Ref.current) { - ema20Ref.current.applyOptions({ visible: false }) - } - - if (indicators.ema50) { - if (!ema50Ref.current && chartRef.current) { - ema50Ref.current = chartRef.current.addLineSeries({ color: '#3b82f6', lineWidth: 2 }) - } - if (ema50Ref.current) { - const closes = chartData.map(d => d.close) - const ema50 = calculateEMA(closes, 50) - ema50Ref.current.setData(ema50.map((v, i) => ({ time: (chartData[i].time / 1000) number, value: v }))) - ema50Ref.current.applyOptions({ visible: true }) - } - } else if (ema50Ref.current) { - ema50Ref.current.applyOptions({ visible: false }) - } - - if (indicators.ema200) { - if (!ema200Ref.current && chartRef.current) { - ema200Ref.current = chartRef.current.addLineSeries({ color: '#ffffff', lineWidth: 2 }) - } - if (ema200Ref.current) { - const closes = chartData.map(d => d.close) - const ema200 = calculateEMA(closes, 200) - ema200Ref.current.setData(ema200.map((v, i) => ({ time: (chartData[i].time / 1000) number, value: v }))) - ema200Ref.current.applyOptions({ visible: true }) - } - } else if (ema200Ref.current) { - ema200Ref.current.applyOptions({ visible: false }) - } - - // Fit content - chartRef.current?.timeScale().fitContent() - }, [chartData, indicators]) + }, []) const fetchPriceData = async () => { try { const idMap: Record = { 'BTC': 'bitcoin', 'SOL': 'solana', 'ETH': 'ethereum' } const res = await fetch(`https://api.coingecko.com/api/v3/simple/price?ids=${idMap[selectedAsset]}&vs_currencies=usd&include_24hr_change=true`) - if (!res.ok) throw new Error('CoinGecko error') const data = await res.json() - setPriceData({ price: data[idMap[selectedAsset]].usd, change24h: data[idMap[selectedAsset]].usd_24h_change }) + setPriceData({ price: data[idMap[selectedAsset]]?.usd || 0, change24h: data[idMap[selectedAsset]]?.usd_24h_change || 0 }) } catch (e) { - console.warn("Price fetch failed, using fallback") - const fallbackPrices: Record = { - 'BTC': { price: 105000, change24h: 2.5 }, - 'SOL': { price: 180, change24h: -1.2 }, - 'ETH': { price: 3200, change24h: 1.8 } - } - setPriceData(fallbackPrices[selectedAsset] || { price: 0, change24h: 0 }) + console.warn('Price fetch failed') + const fallback: Record = { 'BTC': { price: 105000, change24h: 2.5 }, 'SOL': { price: 180, change24h: -1.2 }, 'ETH': { price: 3200, change24h: 1.8 } } + setPriceData(fallback[selectedAsset] || { price: 0, change24h: 0 }) } } @@ -281,69 +173,60 @@ export default function TradingChart() { const interval = { '15m': '15m', '1h': '1h', '4h': '4h', '1D': '1d' }[selectedTimeframe] || '1h' const res = await fetch(`https://api.binance.com/api/v3/klines?symbol=${symbol}&interval=${interval}&limit=${getCandleLimit(selectedTimeframe)}`) const data = await res.json() - setChartData(data.map((k: any[]) => ({ time: k[0], open: parseFloat(k[1]), high: parseFloat(k[2]), low: parseFloat(k[3]), close: parseFloat(k[4]), volume: parseFloat(k[5]) }))) - } catch (e) { console.error("Chart fetch error:", e) } + const chartData = data.map((k: any[]) => ({ time: k[0] / 1000, open: parseFloat(k[1]), high: parseFloat(k[2]), low: parseFloat(k[3]), close: parseFloat(k[4]), volume: parseFloat(k[5]) })) + setChartData(chartData) + + // Update chart + if (candlestickSeriesRef.current) { + candlestickSeriesRef.current.setData(chartData.map((d: any) => ({ time: d.time, open: d.open, high: d.high, low: d.low, close: d.close }))) + } + if (volumeSeriesRef.current && chartData[0]?.volume) { + volumeSeriesRef.current.setData(chartData.map((d: any) => ({ time: d.time, value: d.volume || 0, color: d.close >= d.open ? 'rgba(34,197,94,0.5)' : 'rgba(239,68,68,0.5)' }))) + } + if (chartRef.current) chartRef.current.timeScale().fitContent() + } catch (e) { console.error('Chart fetch error:', e) } finally { setLoading(false) } } - const fetchTrades = async () => { - try { - const res = await fetch('/api/trading/trades'); - if (res.ok) setTrades((await res.json()).trades || []) - } catch (e) { console.warn(e) } - } + const fetchTrades = async () => { try { const res = await fetch('/api/trading/trades'); if (res.ok) setTrades((await res.json()).trades || []) } catch (e) { console.warn(e) } } + const fetchThothView = async () => { try { const res = await fetch('/thoth_view.json'); if (res.ok) setThothView(await res.json()) } catch (e) { console.warn(e) } } - const fetchThothView = async () => { - try { - const res = await fetch('/thoth_view.json'); - if (res.ok) setThothView(await res.json()) - } catch (e) { console.warn(e) } - } + useEffect(() => { + fetchChartData() + fetchPriceData() + }, [selectedAsset, selectedTimeframe]) - useEffect(() => { fetchTrades(); fetchThothView() }, []) + const toggleIndicator = (key: keyof IndicatorState) => setIndicators((p: any) => ({ ...p, [key]: !p[key] })) - const toggleIndicator = (key: keyof IndicatorState) => setIndicators(p => ({ ...p, [key]: !p[key] })) - - const closedTrades = trades.filter(t => t.result === 'win' || t.result === 'loss') - const wins = closedTrades.filter(t => t.result === 'win').length + const closedTrades = trades.filter((t: Trade) => t.result === 'win' || t.result === 'loss') + const wins = closedTrades.filter((t: Trade) => t.result === 'win').length const winRate = closedTrades.length ? Math.round(wins / closedTrades.length * 100) : 0 - const totalPnl = closedTrades.reduce((s, t) => s + (t.pnl || 0), 0) - const avgRr = closedTrades.length ? closedTrades.reduce((s, t) => s + (t.rr || 0), 0) / closedTrades.length : 0 + const totalPnl = closedTrades.reduce((s: number, t: Trade) => s + (t.pnl || 0), 0) + const avgRr = closedTrades.length ? closedTrades.reduce((s: number, t: Trade) => s + (t.rr || 0), 0) / closedTrades.length : 0 const cv = thothView[selectedAsset] const getTE = (t: string) => t === 'uptrend' ? '🟢' : t === 'downtrend' ? '🔴' : '⚪️' const getBC = (b: string) => b === 'bullish' ? 'text-green-400' : b === 'bearish' ? 'text-red-400' : 'text-yellow-400' - // Show loading until mounted (SSR safe) if (!mounted) { - return ( -
-
Loading trading chart...
-
- ) + return
Loading chart...
} return (
- {/* Asset Selection */}
{(['BTC', 'SOL', 'ETH'] as const).map(a => ( - + ))}
{(['15m', '1h', '4h', '1D'] as const).map(tf => ( - + ))}
- {/* Indicators */}
{(Object.keys(indicators) as (keyof IndicatorState)[]).map(k => (
- {/* Price Display */}
-
- {selectedAsset}/USD -
+
{selectedAsset}/USD
${priceData.price.toLocaleString()}
-
= 0 ? 'text-green-400' : 'text-red-400'}`}> - {priceData.change24h >= 0 ? '↑' : '↓'} {Math.abs(priceData.change24h).toFixed(2)}% -
+
= 0 ? 'text-green-400' : 'text-red-400'}`}>{priceData.change24h >= 0 ? '↑' : '↓'} {Math.abs(priceData.change24h).toFixed(2)}%
- {/* THOTH View */} {cv && (
@@ -383,50 +260,17 @@ export default function TradingChart() {
)} - {/* Chart */}
- {loading && ( -
- Loading... -
- )} + {loading &&
Loading...
}
- {/* Stats */}
-
-

Trades

-

{closedTrades.length}

-
-
-

Win Rate

-

{winRate}%

-
-
-

Total PnL

-

= 0 ? 'text-green-400' : 'text-red-400'}`}>${totalPnl.toFixed(2)}

-
-
-

Avg R:R

-

{avgRr.toFixed(2)}R

-
+

Trades

{closedTrades.length}

+

Win Rate

{winRate}%

+

Total PnL

= 0 ? 'text-green-400' : 'text-red-400'}`}>${totalPnl.toFixed(2)}

+

Avg R:R

{avgRr.toFixed(2)}R

) } - -function calculateEMA(data: number[], period: number): (number | null)[] { - const ema: (number | null)[] = [] - const multiplier = 2 / (period + 1) - for (let i = 0; i < data.length; i++) { - if (i < period - 1) ema.push(null) - else if (i === period - 1) { - let sum = 0; for (let j = 0; j < period; j++) sum += data[i - j] - ema.push(sum / period) - } else { - ema.push(data[i] * multiplier + (ema[i - 1] ?? data[i]) * (1 - multiplier)) - } - } - return ema -}