Rewrite trading chart - clean implementation
This commit is contained in:
@@ -2,10 +2,6 @@
|
|||||||
|
|
||||||
import { useState, useEffect, useRef } from 'react'
|
import { useState, useEffect, useRef } from 'react'
|
||||||
|
|
||||||
// Lazy load lightweight-charts only on client
|
|
||||||
let createChart: any = null
|
|
||||||
let lightweightCharts: any = null
|
|
||||||
|
|
||||||
interface Trade {
|
interface Trade {
|
||||||
id: string
|
id: string
|
||||||
date: string
|
date: string
|
||||||
@@ -71,18 +67,16 @@ const getCandleLimit = (tf: string) => ({ '15m': 80, '1h': 100, '4h': 60, '1D':
|
|||||||
|
|
||||||
export default function TradingChart() {
|
export default function TradingChart() {
|
||||||
const chartContainerRef = useRef<HTMLDivElement>(null)
|
const chartContainerRef = useRef<HTMLDivElement>(null)
|
||||||
const chartRef = useRef<IChartApi | null>(null)
|
const chartRef = useRef<any>(null)
|
||||||
const candlestickSeriesRef = useRef<ISeriesApi<"Candlestick"> | null>(null)
|
const candlestickSeriesRef = useRef<any>(null)
|
||||||
const volumeSeriesRef = useRef<ISeriesApi<"Histogram"> | null>(null)
|
const volumeSeriesRef = useRef<any>(null)
|
||||||
const ema20Ref = useRef<ISeriesApi<"Line"> | null>(null)
|
const ema20Ref = useRef<any>(null)
|
||||||
const ema50Ref = useRef<ISeriesApi<"Line"> | null>(null)
|
const ema50Ref = useRef<any>(null)
|
||||||
const ema200Ref = useRef<ISeriesApi<"Line"> | null>(null)
|
const ema200Ref = useRef<any>(null)
|
||||||
|
|
||||||
const [selectedAsset, setSelectedAsset] = useState<'BTC' | 'SOL' | 'ETH'>('BTC')
|
const [selectedAsset, setSelectedAsset] = useState<'BTC' | 'SOL' | 'ETH'>('BTC')
|
||||||
const [selectedTimeframe, setSelectedTimeframe] = useState<'15m' | '1h' | '4h' | '1D'>('1h')
|
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 [chartData, setChartData] = useState<ChartData[]>([])
|
||||||
const [secondChartData, setSecondChartData] = useState<ChartData[]>([])
|
|
||||||
const [priceData, setPriceData] = useState<PriceData>({ price: 0, change24h: 0 })
|
const [priceData, setPriceData] = useState<PriceData>({ price: 0, change24h: 0 })
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
const [trades, setTrades] = useState<Trade[]>([])
|
const [trades, setTrades] = useState<Trade[]>([])
|
||||||
@@ -90,187 +84,85 @@ export default function TradingChart() {
|
|||||||
const [indicators, setIndicators] = useState<IndicatorState>({
|
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: false, fib: false, countdown: false, 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 [chartReady, setChartReady] = useState(false)
|
|
||||||
const [mounted, setMounted] = useState(false)
|
const [mounted, setMounted] = useState(false)
|
||||||
|
|
||||||
// Prevent SSR issues
|
// Initialize on mount (client only)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setMounted(true)
|
setMounted(true)
|
||||||
}, [])
|
|
||||||
|
|
||||||
// Initialize chart - run only on client
|
let chartInstance: any = null
|
||||||
useEffect(() => {
|
let candleSeries: any = null
|
||||||
let mounted = true
|
let volSeries: any = null
|
||||||
|
let e20: any = null
|
||||||
|
let e50: any = null
|
||||||
|
let e200: any = null
|
||||||
|
|
||||||
const initChart = async () => {
|
const initChart = async () => {
|
||||||
|
if (!chartContainerRef.current) return
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const charts = await import('lightweight-charts')
|
const { createChart } = await import('lightweight-charts')
|
||||||
if (!mounted) return
|
|
||||||
createChart = charts.createChart
|
chartInstance = createChart(chartContainerRef.current, {
|
||||||
setChartReady(true)
|
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) {
|
} catch (e) {
|
||||||
console.error('Failed to load charts:', e)
|
console.error('Chart init error:', e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
initChart()
|
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 () => {
|
return () => {
|
||||||
window.removeEventListener('resize', handleResize)
|
if (chartInstance) {
|
||||||
if (chartRef.current) chartRef.current.remove()
|
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 () => {
|
const fetchPriceData = async () => {
|
||||||
try {
|
try {
|
||||||
const idMap: Record<string, string> = { 'BTC': 'bitcoin', 'SOL': 'solana', 'ETH': 'ethereum' }
|
const idMap: Record<string, string> = { '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`)
|
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()
|
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) {
|
} catch (e) {
|
||||||
console.warn("Price fetch failed, using fallback")
|
console.warn('Price fetch failed')
|
||||||
const fallbackPrices: Record<string, { price: number; change24h: number }> = {
|
const fallback: Record<string, { price: number; change24h: number }> = { 'BTC': { price: 105000, change24h: 2.5 }, 'SOL': { price: 180, change24h: -1.2 }, 'ETH': { price: 3200, change24h: 1.8 } }
|
||||||
'BTC': { price: 105000, change24h: 2.5 },
|
setPriceData(fallback[selectedAsset] || { price: 0, change24h: 0 })
|
||||||
'SOL': { price: 180, change24h: -1.2 },
|
|
||||||
'ETH': { price: 3200, change24h: 1.8 }
|
|
||||||
}
|
|
||||||
setPriceData(fallbackPrices[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 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 res = await fetch(`https://api.binance.com/api/v3/klines?symbol=${symbol}&interval=${interval}&limit=${getCandleLimit(selectedTimeframe)}`)
|
||||||
const data = await res.json()
|
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]) })))
|
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]) }))
|
||||||
} catch (e) { console.error("Chart fetch error:", e) }
|
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) }
|
finally { setLoading(false) }
|
||||||
}
|
}
|
||||||
|
|
||||||
const fetchTrades = async () => {
|
const fetchTrades = async () => { try { const res = await fetch('/api/trading/trades'); if (res.ok) setTrades((await res.json()).trades || []) } catch (e) { console.warn(e) } }
|
||||||
try {
|
const fetchThothView = async () => { try { const res = await fetch('/thoth_view.json'); if (res.ok) setThothView(await res.json()) } catch (e) { console.warn(e) } }
|
||||||
const res = await fetch('/api/trading/trades');
|
|
||||||
if (res.ok) setTrades((await res.json()).trades || [])
|
|
||||||
} catch (e) { console.warn(e) }
|
|
||||||
}
|
|
||||||
|
|
||||||
const fetchThothView = async () => {
|
useEffect(() => {
|
||||||
try {
|
fetchChartData()
|
||||||
const res = await fetch('/thoth_view.json');
|
fetchPriceData()
|
||||||
if (res.ok) setThothView(await res.json())
|
}, [selectedAsset, selectedTimeframe])
|
||||||
} catch (e) { console.warn(e) }
|
|
||||||
}
|
|
||||||
|
|
||||||
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: Trade) => t.result === 'win' || t.result === 'loss')
|
||||||
|
const wins = closedTrades.filter((t: Trade) => t.result === 'win').length
|
||||||
const closedTrades = trades.filter(t => t.result === 'win' || t.result === 'loss')
|
|
||||||
const wins = closedTrades.filter(t => t.result === 'win').length
|
|
||||||
const winRate = closedTrades.length ? Math.round(wins / closedTrades.length * 100) : 0
|
const winRate = closedTrades.length ? Math.round(wins / closedTrades.length * 100) : 0
|
||||||
const totalPnl = closedTrades.reduce((s, t) => s + (t.pnl || 0), 0)
|
const totalPnl = closedTrades.reduce((s: number, t: Trade) => s + (t.pnl || 0), 0)
|
||||||
const avgRr = closedTrades.length ? closedTrades.reduce((s, t) => s + (t.rr || 0), 0) / closedTrades.length : 0
|
const avgRr = closedTrades.length ? closedTrades.reduce((s: number, t: Trade) => s + (t.rr || 0), 0) / closedTrades.length : 0
|
||||||
|
|
||||||
const cv = thothView[selectedAsset]
|
const cv = thothView[selectedAsset]
|
||||||
const getTE = (t: string) => t === 'uptrend' ? '🟢' : t === 'downtrend' ? '🔴' : '⚪️'
|
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'
|
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) {
|
if (!mounted) {
|
||||||
return (
|
return <div className="flex items-center justify-center h-96"><div className="text-white/50">Loading chart...</div></div>
|
||||||
<div className="flex items-center justify-center h-96">
|
|
||||||
<div className="text-white/50">Loading trading chart...</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{/* Asset Selection */}
|
|
||||||
<div className="flex gap-4 justify-between flex-wrap">
|
<div className="flex gap-4 justify-between flex-wrap">
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
{(['BTC', 'SOL', 'ETH'] as const).map(a => (
|
{(['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'}`}>
|
<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>
|
||||||
{a}
|
|
||||||
</button>
|
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
{(['15m', '1h', '4h', '1D'] as const).map(tf => (
|
{(['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'}`}>
|
<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>
|
||||||
{tf}
|
|
||||||
</button>
|
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Indicators */}
|
|
||||||
<div className="flex gap-2 flex-wrap">
|
<div className="flex gap-2 flex-wrap">
|
||||||
{(Object.keys(indicators) as (keyof IndicatorState)[]).map(k => (
|
{(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'}`}>
|
<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'}`}>
|
||||||
@@ -352,20 +235,14 @@ export default function TradingChart() {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Price Display */}
|
|
||||||
<div className="flex justify-between p-4 rounded-lg bg-black/50 border border-white/10">
|
<div className="flex justify-between p-4 rounded-lg bg-black/50 border border-white/10">
|
||||||
<div>
|
<div><span className="text-2xl font-bold">{selectedAsset}/USD</span></div>
|
||||||
<span className="text-2xl font-bold">{selectedAsset}/USD</span>
|
|
||||||
</div>
|
|
||||||
<div className="text-right">
|
<div className="text-right">
|
||||||
<div className="text-2xl font-bold">${priceData.price.toLocaleString()}</div>
|
<div className="text-2xl font-bold">${priceData.price.toLocaleString()}</div>
|
||||||
<div className={`text-sm ${priceData.change24h >= 0 ? 'text-green-400' : 'text-red-400'}`}>
|
<div className={`text-sm ${priceData.change24h >= 0 ? 'text-green-400' : 'text-red-400'}`}>{priceData.change24h >= 0 ? '↑' : '↓'} {Math.abs(priceData.change24h).toFixed(2)}%</div>
|
||||||
{priceData.change24h >= 0 ? '↑' : '↓'} {Math.abs(priceData.change24h).toFixed(2)}%
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* THOTH View */}
|
|
||||||
{cv && (
|
{cv && (
|
||||||
<div className="rounded-lg border border-brand-pink/30 bg-brand-pink/5 p-4">
|
<div className="rounded-lg border border-brand-pink/30 bg-brand-pink/5 p-4">
|
||||||
<div className="flex items-center gap-2 mb-3">
|
<div className="flex items-center gap-2 mb-3">
|
||||||
@@ -383,50 +260,17 @@ export default function TradingChart() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Chart */}
|
|
||||||
<div className="relative rounded-lg bg-black/50 border border-white/10" style={{ height: '420px' }}>
|
<div className="relative rounded-lg bg-black/50 border border-white/10" style={{ height: '420px' }}>
|
||||||
{loading && (
|
{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 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 ref={chartContainerRef} className="w-full h-full" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Stats */}
|
|
||||||
<div className="grid grid-cols-4 gap-4">
|
<div className="grid grid-cols-4 gap-4">
|
||||||
<div className="p-4 rounded-lg bg-white/5">
|
<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>
|
||||||
<p className="text-white/50 text-xs">Trades</p>
|
<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>
|
||||||
<p className="text-xl font-bold">{closedTrades.length}</p>
|
<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>
|
<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 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>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
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