Fix trading chart: use lightweight-charts instead of broken chart.js
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useEffect, useRef } from 'react'
|
||||
import { createChart, IChartApi, ISeriesApi, CandlestickData, Time } from 'lightweight-charts'
|
||||
|
||||
interface Trade {
|
||||
id: string
|
||||
@@ -63,154 +64,167 @@ interface IndicatorState {
|
||||
funding: boolean
|
||||
}
|
||||
|
||||
interface PatternMatch {
|
||||
index: number
|
||||
type: string
|
||||
}
|
||||
const getCandleLimit = (tf: string) => ({ '15m': 80, '1h': 100, '4h': 60, '1D': 90 }[tf] || 100)
|
||||
|
||||
// Indicator calculations
|
||||
const 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]! * (1 - multiplier))
|
||||
}
|
||||
}
|
||||
return ema
|
||||
}
|
||||
export default function TradingChart() {
|
||||
const chartContainerRef = useRef<HTMLDivElement>(null)
|
||||
const chartRef = useRef<IChartApi | null>(null)
|
||||
const candlestickSeriesRef = useRef<ISeriesApi<"Candlestick"> | null>(null)
|
||||
const volumeSeriesRef = useRef<ISeriesApi<"Histogram"> | null>(null)
|
||||
const ema20Ref = useRef<ISeriesApi<"Line"> | null>(null)
|
||||
const ema50Ref = useRef<ISeriesApi<"Line"> | null>(null)
|
||||
const ema200Ref = useRef<ISeriesApi<"Line"> | null>(null)
|
||||
|
||||
const calculateSMA = (data: number[], period: number): (number | null)[] => {
|
||||
const sma: (number | null)[] = []
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
if (i < period - 1) sma.push(null)
|
||||
else { let sum = 0; for (let j = 0; j < period; j++) sum += data[i - j]; sma.push(sum / period) }
|
||||
}
|
||||
return sma
|
||||
}
|
||||
|
||||
const calculateStdDev = (data: number[], period: number): (number | null)[] => {
|
||||
const stdDev: (number | null)[] = []
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
if (i < period - 1) stdDev.push(null)
|
||||
else {
|
||||
const slice = data.slice(i - period + 1, i + 1)
|
||||
const mean = slice.reduce((a, b) => a + b, 0) / period
|
||||
stdDev.push(Math.sqrt(slice.reduce((a, b) => a + Math.pow(b - mean, 2), 0) / period))
|
||||
}
|
||||
}
|
||||
return stdDev
|
||||
}
|
||||
|
||||
const calculateBollingerBands = (data: number[], period: number = 20, stdDevMult: number = 2) => {
|
||||
const sma = calculateSMA(data, period)
|
||||
const stdDev = calculateStdDev(data, period)
|
||||
const upper: (number | null)[] = [], lower: (number | null)[] = []
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
if (sma[i] === null || stdDev[i] === null) { upper.push(null); lower.push(null) }
|
||||
else { upper.push(sma[i]! + stdDev[i]! * stdDevMult); lower.push(sma[i]! - stdDev[i]! * stdDevMult) }
|
||||
}
|
||||
return { middle: sma, upper, lower }
|
||||
}
|
||||
|
||||
const calculateRSI = (data: number[], period: number = 14): (number | null)[] => {
|
||||
const rsi: (number | null)[] = []
|
||||
let gains: number[] = [], losses: number[] = []
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
if (i === 0) { rsi.push(null); continue }
|
||||
const change = data[i] - data[i - 1]
|
||||
gains.push(change > 0 ? change : 0)
|
||||
losses.push(change < 0 ? Math.abs(change) : 0)
|
||||
if (i < period) rsi.push(null)
|
||||
else {
|
||||
const avgGain = gains.slice(-period).reduce((a, b) => a + b, 0) / period
|
||||
const avgLoss = losses.slice(-period).reduce((a, b) => a + b, 0) / period
|
||||
const rs = avgLoss === 0 ? 100 : avgGain / avgLoss
|
||||
rsi.push(100 - (100 / (1 + rs)))
|
||||
}
|
||||
}
|
||||
return rsi
|
||||
}
|
||||
|
||||
const calculateMACD = (data: number[], fast: number = 12, slow: number = 26, signal: number = 9) => {
|
||||
const emaFast = calculateEMA(data, fast)
|
||||
const emaSlow = calculateEMA(data, slow)
|
||||
const macdLine: (number | null)[] = []
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
if (emaFast[i] === null || emaSlow[i] === null) macdLine.push(null)
|
||||
else macdLine.push(emaFast[i]! - emaSlow[i]!)
|
||||
}
|
||||
const validMacd = macdLine.filter((v): v is number => v !== null)
|
||||
const signalLine = calculateEMA(validMacd, signal)
|
||||
const signalLineAligned: (number | null)[] = []
|
||||
let signalIdx = 0
|
||||
for (let i = 0; i < macdLine.length; i++) {
|
||||
if (macdLine[i] === null) signalLineAligned.push(null)
|
||||
else { signalLineAligned.push(signalLine[signalIdx] ?? null); signalIdx++ }
|
||||
}
|
||||
const histogram: (number | null)[] = []
|
||||
for (let i = 0; i < macdLine.length; i++) {
|
||||
if (macdLine[i] === null || signalLineAligned[i] === null) histogram.push(null)
|
||||
else histogram.push(macdLine[i]! - signalLineAligned[i]!)
|
||||
}
|
||||
return { macd: macdLine, signal: signalLineAligned, histogram }
|
||||
}
|
||||
|
||||
const detectPatterns = (data: ChartData[]): PatternMatch[] => {
|
||||
const patterns: PatternMatch[] = []
|
||||
const closes = data.map(d => d.close)
|
||||
const highs = data.map(d => d.high)
|
||||
const lows = data.map(d => d.low)
|
||||
for (let i = 20; i < data.length - 5; i++) {
|
||||
if (highs[i] > highs[i-1] && highs[i] > highs[i+1] && highs[i-5] > highs[i-4] && Math.abs(highs[i] - highs[i-5]) < highs[i] * 0.02)
|
||||
patterns.push({ index: i, type: 'double_top' })
|
||||
if (lows[i] < lows[i-1] && lows[i] < lows[i+1] && lows[i-5] < lows[i-4] && Math.abs(lows[i] - lows[i-5]) < lows[i] * 0.02)
|
||||
patterns.push({ index: i, type: 'double_bottom' })
|
||||
if (closes[i] > highs[i-5] && closes[i-1] < highs[i-5]) patterns.push({ index: i, type: 'breakout' })
|
||||
if (closes[i] < lows[i-5] && closes[i-1] > lows[i-5]) patterns.push({ index: i, type: 'breakdown' })
|
||||
}
|
||||
return patterns
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
Chart: any
|
||||
}
|
||||
}
|
||||
|
||||
export function TradingChart() {
|
||||
const mainChartRef = useRef<HTMLCanvasElement>(null)
|
||||
const rsiChartRef = useRef<HTMLCanvasElement>(null)
|
||||
const macdChartRef = useRef<HTMLCanvasElement>(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<ChartData[]>([])
|
||||
const [secondChartData, setSecondChartData] = useState<ChartData[]>([])
|
||||
const [trades, setTrades] = useState<Trade[]>([])
|
||||
const [thothView, setThothView] = useState<Record<string, ThothView>>({})
|
||||
const [priceData, setPriceData] = useState<PriceData>({ price: 0, change24h: 0 })
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [patterns, setPatterns] = useState<PatternMatch[]>([])
|
||||
const [trades, setTrades] = useState<Trade[]>([])
|
||||
const [thothView, setThothView] = useState<Record<string, ThothView>>({})
|
||||
const [indicators, setIndicators] = useState<IndicatorState>({
|
||||
ema20: false, ema50: false, ema200: false, bb: false, rsi: false, macd: false, thoth: true, volume: true, srZones: true, news: false, patterns: true, fib: false, countdown: true, calendar: false, correlation: false, funding: false
|
||||
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 mainChartRefInstance = useRef<any>(null)
|
||||
const rsiChartRefInstance = useRef<any>(null)
|
||||
const macdChartRefInstance = useRef<any>(null)
|
||||
|
||||
const getCandleLimit = (tf: string) => ({ '15m': 80, '1h': 100, '4h': 60, '1D': 90 }[tf] || 100)
|
||||
|
||||
useEffect(() => { fetchChartData(); fetchSecondChartData(); fetchPriceData(); const i = setInterval(() => { fetchChartData(); fetchSecondChartData(); fetchPriceData(); }, 60000); return () => clearInterval(i) }, [selectedAsset, selectedTimeframe, secondTimeframe])
|
||||
useEffect(() => { fetchTrades(); fetchThothView() }, [])
|
||||
|
||||
// Initialize chart
|
||||
useEffect(() => {
|
||||
if (chartData.length > 0) { setPatterns(detectPatterns(chartData)); renderCharts() }
|
||||
return () => { if (mainChartRefInstance.current) mainChartRefInstance.current.destroy(); if (rsiChartRefInstance.current) rsiChartRefInstance.current.destroy(); if (macdChartRefInstance.current) macdChartRefInstance.current.destroy() }
|
||||
if (!chartContainerRef.current) 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)
|
||||
chart.remove()
|
||||
}
|
||||
}, [])
|
||||
|
||||
// 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) as Time,
|
||||
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) as Time,
|
||||
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) as Time, 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) as Time, 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) as Time, 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 () => {
|
||||
@@ -222,7 +236,6 @@ export function TradingChart() {
|
||||
setPriceData({ price: data[idMap[selectedAsset]].usd, change24h: data[idMap[selectedAsset]].usd_24h_change })
|
||||
} catch (e) {
|
||||
console.warn("Price fetch failed, using fallback")
|
||||
// Fallback prices
|
||||
const fallbackPrices: Record<string, { price: number; change24h: number }> = {
|
||||
'BTC': { price: 105000, change24h: 2.5 },
|
||||
'SOL': { price: 180, change24h: -1.2 },
|
||||
@@ -244,125 +257,138 @@ export function TradingChart() {
|
||||
finally { setLoading(false) }
|
||||
}
|
||||
|
||||
const fetchSecondChartData = async () => {
|
||||
if (!secondTimeframe) return
|
||||
const fetchTrades = async () => {
|
||||
try {
|
||||
const symbol = selectedAsset === 'BTC' ? 'BTCUSDT' : selectedAsset === 'SOL' ? 'SOLUSDT' : 'ETHUSDT'
|
||||
const interval = { '15m': '15m', '1h': '1h', '4h': '4h', '1D': '1d' }[secondTimeframe] || '1h'
|
||||
const res = await fetch(`https://api.binance.com/api/v3/klines?symbol=${symbol}&interval=${interval}&limit=${getCandleLimit(secondTimeframe)}`)
|
||||
const data = await res.json()
|
||||
setSecondChartData(data.map((k: any[]) => ({ time: k[0], open: parseFloat(k[1]), high: parseFloat(k[2]), low: parseFloat(k[3]), close: parseFloat(k[4]) })))
|
||||
} catch (e) { console.error("Second chart fetch error:", e) }
|
||||
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(() => { fetchTrades(); fetchThothView() }, [])
|
||||
|
||||
const toggleIndicator = (key: keyof IndicatorState) => setIndicators(p => ({ ...p, [key]: !p[key] }))
|
||||
|
||||
const renderCharts = () => {
|
||||
const script = document.createElement('script')
|
||||
script.src = 'https://cdn.jsdelivr.net/npm/chart.js'
|
||||
script.onload = () => { renderMainChart(); if (indicators.rsi) renderRSIChart(); if (indicators.macd) renderMACDChart() }
|
||||
document.head.appendChild(script)
|
||||
}
|
||||
|
||||
const renderMainChart = () => {
|
||||
if (!mainChartRef.current) return
|
||||
if (mainChartRefInstance.current) mainChartRefInstance.current.destroy()
|
||||
const ctx = mainChartRef.current.getContext('2d')
|
||||
if (!ctx) return
|
||||
|
||||
const closes = chartData.map(d => d.close)
|
||||
const labels = chartData.map(d => new Date(d.time))
|
||||
const ema20 = calculateEMA(closes, 20), ema50 = calculateEMA(closes, 50), ema200 = calculateEMA(closes, 200), bb = calculateBollingerBands(closes, 20, 2)
|
||||
const minP = Math.min(...chartData.flatMap(d => [d.high, d.low])), maxP = Math.max(...chartData.flatMap(d => [d.high, d.low])), pad = (maxP - minP) * 0.15
|
||||
|
||||
const datasets: any[] = [{ type: 'bar', label: 'Price', data: chartData.map(d => [d.low, d.high]), backgroundColor: chartData.map(d => d.close >= d.open ? '#22c55e' : '#ef4444'), borderColor: chartData.map(d => d.close >= d.open ? '#22c55e' : '#ef4444'), borderWidth: 1, borderSkipped: false }]
|
||||
|
||||
if (indicators.volume && chartData[0]?.volume) datasets.push({ type: 'bar', label: 'Volume', data: chartData.map(d => d.volume || 0), backgroundColor: chartData.map(d => d.close >= d.open ? 'rgba(34,197,94,0.3)' : 'rgba(239,68,68,0.3)'), borderWidth: 0, yAxisID: 'y_vol' })
|
||||
if (indicators.ema20) datasets.push({ type: 'line', data: ema20, borderColor: '#eab308', borderWidth: 2, pointRadius: 0, tension: 0.4, yAxisID: 'y' })
|
||||
if (indicators.ema50) datasets.push({ type: 'line', data: ema50, borderColor: '#3b82f6', borderWidth: 2, pointRadius: 0, tension: 0.4, yAxisID: 'y' })
|
||||
if (indicators.ema200) datasets.push({ type: 'line', data: ema200, borderColor: '#ffffff', borderWidth: 2, pointRadius: 0, tension: 0.4, yAxisID: 'y' })
|
||||
if (indicators.bb) { datasets.push({ type: 'line', data: bb.upper, borderColor: '#a855f7', borderWidth: 1, pointRadius: 0, yAxisID: 'y' }); datasets.push({ type: 'line', data: bb.lower, borderColor: '#a855f7', borderWidth: 1, pointRadius: 0, backgroundColor: 'rgba(168,85,247,0.1)', fill: '-1', yAxisID: 'y' }) }
|
||||
|
||||
const currentView = thothView[selectedAsset]
|
||||
if (indicators.srZones && currentView?.support_zones) currentView.support_zones.forEach(z => datasets.push({ type: 'line', data: chartData.map(() => z.level), borderColor: 'rgba(34,197,94,0.5)', borderWidth: 2, borderDash: [5,5], pointRadius: 0, yAxisID: 'y' }))
|
||||
if (indicators.srZones && currentView?.resistance_zones) currentView.resistance_zones.forEach(z => datasets.push({ type: 'line', data: chartData.map(() => z.level), borderColor: 'rgba(239,68,68,0.5)', borderWidth: 2, borderDash: [5,5], pointRadius: 0, yAxisID: 'y' }))
|
||||
|
||||
if (indicators.patterns && patterns.length) datasets.push({ type: 'scatter', data: patterns.map(p => ({ x: labels[p.index], y: chartData[p.index]?.high || 0 })), backgroundColor: patterns.map(p => p.type === 'breakout' ? '#22c55e' : p.type === 'breakdown' ? '#ef4444' : '#fbbf24'), pointStyle: 'star', pointRadius: 10, yAxisID: 'y' })
|
||||
if (indicators.thoth && currentView?.bias_history) {
|
||||
const bc = currentView.bias_history.map(b => { let ci = 0, md = Infinity; chartData.forEach((d, i) => { const df = Math.abs(d.time - b.time); if (df < md) { md = df; ci = i } }); return { idx: ci, bias: b.bias } })
|
||||
datasets.push({ type: 'scatter', data: bc.map(b => ({ x: labels[b.idx], y: chartData[b.idx]?.high || 0 })), backgroundColor: bc.map(b => b.bias === 'bullish' ? '#fbbf24' : b.bias === 'bearish' ? '#ef4444' : '#a0a0a0'), pointStyle: 'rectRot', pointRadius: 12, yAxisID: 'y' })
|
||||
}
|
||||
|
||||
const scales: any = { x: { display: true, grid: { color: '#1a1a2e' }, ticks: { color: '#a0a0a0', maxTicksLimit: 12 } }, y: { position: 'right', min: minP - pad, max: maxP + pad, grid: { color: '#1a1a2e' }, ticks: { color: '#a0a0a0', callback: (v: any) => '$' + v.toFixed(0) } } }
|
||||
if (indicators.volume && chartData[0]?.volume) scales.y_vol = { display: false, max: Math.max(...chartData.map(d => d.volume || 0)) * 3 }
|
||||
|
||||
mainChartRefInstance.current = new window.Chart(ctx, { type: 'bar', data: { labels, datasets }, options: { responsive: true, maintainAspectRatio: false, interaction: { intersect: false, mode: 'index' }, plugins: { legend: { display: false }, tooltip: { backgroundColor: '#1a1a2e', titleColor: '#fff', bodyColor: '#a0a0a0', borderColor: '#2a2a4e', borderWidth: 1 } }, scales } })
|
||||
}
|
||||
|
||||
const renderRSIChart = () => {
|
||||
if (!rsiChartRef.current) return
|
||||
if (rsiChartRefInstance.current) rsiChartRefInstance.current.destroy()
|
||||
const ctx = rsiChartRef.current.getContext('2d')
|
||||
if (!ctx) return
|
||||
const rsi = calculateRSI(chartData.map(d => d.close), 14)
|
||||
rsiChartRefInstance.current = new window.Chart(ctx, { type: 'line', data: { labels: chartData.map(d => d.time), datasets: [{ data: rsi, borderColor: '#a0a0a0', borderWidth: 2, pointRadius: 0, tension: 0.4 }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false } }, scales: { x: { display: false }, y: { min: 0, max: 100, position: 'right', grid: { color: '#1a1a2e' }, ticks: { color: '#a0a0a0' } } } } })
|
||||
}
|
||||
|
||||
const renderMACDChart = () => {
|
||||
if (!macdChartRef.current) return
|
||||
if (macdChartRefInstance.current) macdChartRefInstance.current.destroy()
|
||||
const ctx = macdChartRef.current.getContext('2d')
|
||||
if (!ctx) return
|
||||
const { macd, signal, histogram } = calculateMACD(chartData.map(d => d.close))
|
||||
macdChartRefInstance.current = new window.Chart(ctx, { type: 'bar', data: { labels: chartData.map(d => d.time), datasets: [{ data: histogram, backgroundColor: histogram.map(v => v === null ? 'transparent' : v >= 0 ? '#22c55e' : '#ef4444'), borderWidth: 0 }, { type: 'line', data: macd, borderColor: '#3b82f6', borderWidth: 2, pointRadius: 0 }, { type: 'line', data: signal, borderColor: '#f59e0b', borderWidth: 2, pointRadius: 0 }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false } }, scales: { x: { display: false }, y: { position: 'right', grid: { color: '#1a1a2e' }, ticks: { color: '#a0a0a0' } } } } })
|
||||
}
|
||||
|
||||
const closedTrades = trades.filter(t => t.result === 'win' || t.result === 'loss')
|
||||
const wins = closedTrades.filter(t => t.result === 'win').length, winRate = closedTrades.length ? Math.round(wins / closedTrades.length * 100) : 0, totalPnl = closedTrades.reduce((s, t) => s + (t.pnl || 0), 0), avgRr = closedTrades.length ? closedTrades.reduce((s, t) => s + (t.rr || 0), 0) / closedTrades.length : 0
|
||||
const wins = closedTrades.filter(t => 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 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'
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* Asset Selection */}
|
||||
<div className="flex gap-4 justify-between flex-wrap">
|
||||
<div className="flex gap-2">{(['BTC', 'SOL', 'ETH'] as const).map(a => <button key={a} onClick={() => setSelectedAsset(a)} className={`px-4 py-2 rounded-lg font-medium ${selectedAsset === a ? 'bg-brand-pink text-white' : 'bg-white/10 text-white/70'}`}>{a}</button>)}</div>
|
||||
<div className="flex gap-2">{(['15m', '1h', '4h', '1D'] as const).map(tf => <button key={tf} onClick={() => setSelectedTimeframe(tf)} className={`px-3 py-1 rounded text-sm ${selectedTimeframe === tf ? 'bg-green-500/20 text-green-400 border border-green-500/30' : 'bg-white/10 text-white/50'}`}>{tf}</button>)}</div>
|
||||
<div className="flex gap-2">
|
||||
{(['BTC', 'SOL', 'ETH'] as const).map(a => (
|
||||
<button key={a} onClick={() => setSelectedAsset(a)} className={`px-4 py-2 rounded-lg font-medium ${selectedAsset === a ? 'bg-brand-pink text-white' : 'bg-white/10 text-white/70'}`}>
|
||||
{a}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
{(['15m', '1h', '4h', '1D'] as const).map(tf => (
|
||||
<button key={tf} onClick={() => setSelectedTimeframe(tf)} className={`px-3 py-1 rounded text-sm ${selectedTimeframe === tf ? 'bg-green-500/20 text-green-400 border border-green-500/30' : 'bg-white/10 text-white/50'}`}>
|
||||
{tf}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2 items-center"><span className="text-white/50 text-sm">Compare:</span><button onClick={() => setSecondTimeframe(null)} className={`px-2 py-1 rounded text-xs ${!secondTimeframe ? 'bg-brand-pink text-white' : 'bg-white/10 text-white/50'}`}>None</button>{(['15m', '1h', '4h', '1D'] as const).map(tf => <button key={tf} onClick={() => setSecondTimeframe(tf)} className={`px-2 py-1 rounded text-xs ${secondTimeframe === tf ? 'bg-brand-pink text-white' : 'bg-white/10 text-white/50'}`}>{tf}</button>)}</div>
|
||||
{/* Indicators */}
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
{(Object.keys(indicators) as (keyof IndicatorState)[]).map(k => (
|
||||
<button key={k} onClick={() => toggleIndicator(k)} className={`px-3 py-1 rounded text-sm ${indicators[k] ? 'bg-brand-pink text-white' : 'bg-white/10 text-white/50'}`}>
|
||||
{k === 'thoth' ? '👁 THOTH' : k === 'srZones' ? 'S/R' : k === 'volume' ? 'VOL' : k.toUpperCase()}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2 flex-wrap">{(Object.keys(indicators) as (keyof IndicatorState)[]).map(k => <button key={k} onClick={() => toggleIndicator(k)} className={`px-3 py-1 rounded text-sm ${indicators[k] ? 'bg-brand-pink text-white' : 'bg-white/10 text-white/50'}`}>{k === 'thoth' ? '👁 THOTH' : k === 'srZones' ? 'S/R' : k === 'volume' ? 'VOL' : k === 'news' ? 'NEWS' : k === 'patterns' ? 'PATTERNS' : k === 'fib' ? 'FIB' : k === 'countdown' ? '⏱️' : k === 'calendar' ? '📅' : k === 'correlation' ? '📊 CORR' : k === 'funding' ? '💰 FUND' : k.toUpperCase()}</button>)}</div>
|
||||
{/* Price Display */}
|
||||
<div className="flex justify-between p-4 rounded-lg bg-black/50 border border-white/10">
|
||||
<div>
|
||||
<span className="text-2xl font-bold">{selectedAsset}/USD</span>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div className="text-2xl font-bold">${priceData.price.toLocaleString()}</div>
|
||||
<div className={`text-sm ${priceData.change24h >= 0 ? 'text-green-400' : 'text-red-400'}`}>
|
||||
{priceData.change24h >= 0 ? '↑' : '↓'} {Math.abs(priceData.change24h).toFixed(2)}%
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between p-4 rounded-lg bg-black/50 border border-white/10"><div><span className="text-2xl font-bold">{selectedAsset}/USD</span></div><div className="text-right"><div className="text-2xl font-bold">${priceData.price.toLocaleString()}</div><div className={`text-sm ${priceData.change24h >= 0 ? 'text-green-400' : 'text-red-400'}`}>{priceData.change24h >= 0 ? '↑' : '↓'} {Math.abs(priceData.change24h).toFixed(2)}%</div></div></div>
|
||||
{/* THOTH View */}
|
||||
{cv && (
|
||||
<div className="rounded-lg border border-brand-pink/30 bg-brand-pink/5 p-4">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<span className="text-xl">👁️</span>
|
||||
<h3 className="font-bold text-brand-pink">THOTH'S VIEW</h3>
|
||||
<span className="text-xs text-white/40 ml-auto">{new Date(cv.updated_at).toLocaleString()}</span>
|
||||
</div>
|
||||
<p className="text-white/90 mb-4 italic">"{cv.thought}"</p>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-3 text-sm">
|
||||
<div><p className="text-white/50 text-xs">Trend</p><p className="font-medium">{getTE(cv.trend)} {cv.trend}</p></div>
|
||||
<div><p className="text-white/50 text-xs">Phase</p><p className="font-medium">{cv.phase}</p></div>
|
||||
<div><p className="text-white/50 text-xs">Key Level</p><p className="font-medium">${cv.key_level.toLocaleString()}</p></div>
|
||||
<div><p className="text-white/50 text-xs">Bias</p><p className={`font-medium ${getBC(cv.bias)}`}>{cv.bias.toUpperCase()} ({cv.confidence}/10)</p></div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{cv && <div className="rounded-lg border border-brand-pink/30 bg-brand-pink/5 p-4"><div className="flex items-center gap-2 mb-3"><span className="text-xl">👁️</span><h3 className="font-bold text-brand-pink">THOTH'S VIEW</h3><span className="text-xs text-white/40 ml-auto">{new Date(cv.updated_at).toLocaleString()}</span></div><p className="text-white/90 mb-4 italic">"{cv.thought}"</p><div className="grid grid-cols-2 md:grid-cols-4 gap-3 text-sm"><div><p className="text-white/50 text-xs">Trend</p><p className="font-medium">{getTE(cv.trend)} {cv.trend}</p></div><div><p className="text-white/50 text-xs">Phase</p><p className="font-medium">{cv.phase}</p></div><div><p className="text-white/50 text-xs">Key Level</p><p className="font-medium">${cv.key_level.toLocaleString()}</p></div><div><p className="text-white/50 text-xs">Bias</p><p className={`font-medium ${getBC(cv.bias)}`}>{cv.bias.toUpperCase()} ({cv.confidence}/10)</p></div></div></div>}
|
||||
{/* Chart */}
|
||||
<div className="relative rounded-lg bg-black/50 border border-white/10" style={{ height: '420px' }}>
|
||||
{loading && (
|
||||
<div className="absolute inset-0 flex items-center justify-center bg-black/50 z-10">
|
||||
<span className="text-white/50">Loading...</span>
|
||||
</div>
|
||||
)}
|
||||
<div ref={chartContainerRef} className="w-full h-full" />
|
||||
</div>
|
||||
|
||||
<div className="relative rounded-lg bg-black/50 border border-white/10" style={{ height: '400px' }}>{loading && <div className="absolute inset-0 flex items-center justify-center bg-black/50 z-10"><span className="text-white/50">Loading...</span></div>}<canvas ref={mainChartRef} /></div>
|
||||
|
||||
{secondTimeframe && secondChartData.length > 0 && <div className="rounded-lg bg-black/50 border border-white/10" style={{ height: '200px' }}><div className="px-4 py-2 border-b border-white/10 text-sm font-bold">{secondTimeframe} Chart</div><SecondChart data={secondChartData} /></div>}
|
||||
|
||||
{indicators.rsi && <div className="rounded-lg bg-black/50 border border-white/10" style={{ height: '150px' }}><canvas ref={rsiChartRef} /></div>}
|
||||
{indicators.macd && <div className="rounded-lg bg-black/50 border border-white/10" style={{ height: '150px' }}><canvas ref={macdChartRef} /></div>}
|
||||
|
||||
<div className="grid grid-cols-5 gap-3">{[{ v: trades.filter(t => t.result === 'open').length, l: 'Open' }, { v: totalPnl, l: 'P&L', c: 'text-green-400' }, { v: winRate + '%', l: 'Win Rate' }, { v: closedTrades.length, l: 'Trades' }, { v: avgRr.toFixed(1) + ':1', l: 'Avg R:R' }].map((s, i) => <div key={i} className="p-3 rounded-lg bg-white/5 border border-white/10 text-center"><p className={`text-2xl font-bold ${s.c || ''}`}>{s.v}</p><p className="text-xs text-white/50">{s.l}</p></div>)}</div>
|
||||
|
||||
<div className="rounded-lg border border-white/10 bg-white/5 overflow-hidden"><div className="px-4 py-3 border-b border-white/10"><h3 className="font-bold">📊 Trade History</h3></div>{trades.length === 0 ? <div className="p-8 text-center text-white/50">No trades yet</div> : <div className="max-h-48 overflow-y-auto"><table className="w-full text-sm"><thead className="bg-white/5 text-white/70 sticky top-0"><tr><th className="px-4 py-2 text-left">Date</th><th className="px-4 py-2">Asset</th><th className="px-4 py-2">Dir</th><th className="px-4 py-2 text-right">Entry</th><th className="px-4 py-2 text-right">SL</th><th className="px-4 py-2 text-right">TP</th><th className="px-4 py-2 text-right">R:R</th><th className="px-4 py-2 text-right">Result</th></tr></thead><tbody>{trades.map((t, i) => <tr key={i} className={`border-t border-white/5 ${t.result === 'win' ? 'bg-green-500/10' : t.result === 'loss' ? 'bg-red-500/10' : 'bg-yellow-500/10'}`}><td className="px-4 py-2">{t.date}</td><td className="px-4 py-2 text-center">{t.pair}</td><td className="px-4 py-2 text-center"><span className={t.direction === 'long' ? 'text-green-400' : 'text-red-400'}>{t.direction.toUpperCase()}</span></td><td className="px-4 py-2 text-right">${t.entry.toLocaleString()}</td><td className="px-4 py-2 text-right text-red-400">${t.stopLoss.toLocaleString()}</td><td className="px-4 py-2 text-right text-green-400">${t.takeProfit.toLocaleString()}</td><td className="px-4 py-2 text-right">{t.rr?.toFixed(1)}:1</td><td className="px-4 py-2 text-right">{t.result === 'win' ? '✅' : t.result === 'loss' ? '❌' : '⏳'}</td></tr>)}</tbody></table></div>}</div>
|
||||
{/* Stats */}
|
||||
<div className="grid grid-cols-4 gap-4">
|
||||
<div className="p-4 rounded-lg bg-white/5">
|
||||
<p className="text-white/50 text-xs">Trades</p>
|
||||
<p className="text-xl font-bold">{closedTrades.length}</p>
|
||||
</div>
|
||||
<div className="p-4 rounded-lg bg-white/5">
|
||||
<p className="text-white/50 text-xs">Win Rate</p>
|
||||
<p className="text-xl font-bold">{winRate}%</p>
|
||||
</div>
|
||||
<div className="p-4 rounded-lg bg-white/5">
|
||||
<p className="text-white/50 text-xs">Total PnL</p>
|
||||
<p className={`text-xl font-bold ${totalPnl >= 0 ? 'text-green-400' : 'text-red-400'}`}>${totalPnl.toFixed(2)}</p>
|
||||
</div>
|
||||
<div className="p-4 rounded-lg bg-white/5">
|
||||
<p className="text-white/50 text-xs">Avg R:R</p>
|
||||
<p className="text-xl font-bold">{avgRr.toFixed(2)}R</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function SecondChart({ data }: { data: ChartData[] }) {
|
||||
const ref = useRef<HTMLCanvasElement>(null)
|
||||
useEffect(() => {
|
||||
if (!ref.current || !data.length) return
|
||||
const ctx = ref.current.getContext('2d')
|
||||
if (!ctx) return
|
||||
if (ref.current.chart) ref.current.chart.destroy()
|
||||
const chart = new window.Chart(ctx, { type: 'line', data: { labels: data.map(d => new Date(d.time).toLocaleTimeString()), datasets: [{ data: data.map(d => d.close), borderColor: '#a0a0a0', borderWidth: 2, pointRadius: 0, tension: 0.4 }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false } }, scales: { x: { display: false }, y: { position: 'right', grid: { color: '#1a1a2e' }, ticks: { color: '#a0a0a0' } } } } })
|
||||
ref.current.chart = chart
|
||||
}, [data])
|
||||
return <canvas ref={ref} />
|
||||
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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user