Fix: load lightweight-charts from CDN instead of npm import

This commit is contained in:
root
2026-02-22 22:29:38 +00:00
parent 81b549dae0
commit 1107706085
+155 -132
View File
@@ -39,129 +39,105 @@ interface ThothView {
resistance_zones?: { level: number; strength: number }[]
}
interface PriceData {
price: number
change24h: number
}
interface IndicatorState {
ema20: boolean
ema50: boolean
ema200: boolean
bb: boolean
rsi: boolean
macd: boolean
thoth: boolean
volume: boolean
srZones: boolean
news: boolean
patterns: boolean
fib: boolean
countdown: boolean
calendar: boolean
correlation: boolean
funding: boolean
}
const getCandleLimit = (tf: string) => ({ '15m': 80, '1h': 100, '4h': 60, '1D': 90 }[tf] || 100)
export default function TradingChart() {
const chartContainerRef = useRef<HTMLDivElement>(null)
const chartRef = useRef<any>(null)
const candlestickSeriesRef = useRef<any>(null)
const volumeSeriesRef = useRef<any>(null)
const ema20Ref = useRef<any>(null)
const ema50Ref = useRef<any>(null)
const ema200Ref = useRef<any>(null)
const seriesRef = useRef<any>(null)
const [selectedAsset, setSelectedAsset] = useState<'BTC' | 'SOL' | 'ETH'>('BTC')
const [selectedTimeframe, setSelectedTimeframe] = useState<'15m' | '1h' | '4h' | '1D'>('1h')
const [chartData, setChartData] = useState<ChartData[]>([])
const [priceData, setPriceData] = useState<PriceData>({ price: 0, change24h: 0 })
const [priceData, setPriceData] = useState<{ price: number; change24h: number }>({ price: 0, change24h: 0 })
const [loading, setLoading] = useState(true)
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: false, fib: false, countdown: false, calendar: false, correlation: false, funding: false
})
const [mounted, setMounted] = useState(false)
// Initialize on mount (client only)
// Load chart library from CDN on mount
useEffect(() => {
setMounted(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 { 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('Chart init error:', e)
}
}
initChart()
// Load lightweight-charts from CDN
const script = document.createElement('script')
script.src = 'https://unpkg.com/lightweight-charts@4.1.0/dist/lightweight-charts.standalone.production.js'
script.onload = initChart
script.onerror = () => console.error('Failed to load chart library')
document.head.appendChild(script)
return () => {
if (chartInstance) {
chartInstance.remove()
if (chartRef.current) {
chartRef.current.remove()
}
}
}, [])
const initChart = () => {
if (!chartContainerRef.current || !(window as any).LightweightCharts) return
const chart = (window as any).LightweightCharts.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',
})
chartRef.current = chart
seriesRef.current = candlestickSeries
// Handle resize
const handleResize = () => {
if (chartContainerRef.current && chartRef.current) {
chartRef.current.applyOptions({ width: chartContainerRef.current.clientWidth })
}
}
window.addEventListener('resize', handleResize)
// Fetch data
fetchChartData()
fetchPriceData()
fetchTrades()
fetchThothView()
}
const fetchPriceData = async () => {
try {
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 data = await res.json()
setPriceData({ price: data[idMap[selectedAsset]]?.usd || 0, change24h: data[idMap[selectedAsset]]?.usd_24h_change || 0 })
setPriceData({
price: data[idMap[selectedAsset]]?.usd || 0,
change24h: data[idMap[selectedAsset]]?.usd_24h_change || 0
})
} catch (e) {
console.warn('Price fetch failed')
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 } }
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 }
}
setPriceData(fallback[selectedAsset] || { price: 0, change24h: 0 })
}
}
@@ -173,43 +149,66 @@ 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()
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)
const formattedData = 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(formattedData)
// 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 (seriesRef.current) {
seriesRef.current.setData(formattedData)
}
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()
}
if (chartRef.current) chartRef.current.timeScale().fitContent()
} catch (e) { console.error('Chart fetch error:', e) }
finally { setLoading(false) }
} catch (e) {
console.error('Chart fetch error:', e)
}
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 fetchThothView = async () => { try { const res = await fetch('/thoth_view.json'); if (res.ok) setThothView(await res.json()) } 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) }
}
useEffect(() => {
fetchChartData()
fetchPriceData()
}, [selectedAsset, selectedTimeframe])
if (mounted && chartRef.current) {
fetchChartData()
fetchPriceData()
}
}, [selectedAsset, selectedTimeframe, mounted])
const toggleIndicator = (key: keyof IndicatorState) => setIndicators((p: any) => ({ ...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 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 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'
if (!mounted) {
return <div className="flex items-center justify-center h-96"><div className="text-white/50">Loading chart...</div></div>
return (
<div className="flex items-center justify-center h-96">
<div className="text-white/50">Loading chart...</div>
</div>
)
}
return (
@@ -217,29 +216,37 @@ export default function TradingChart() {
<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>
<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>
<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 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 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>
<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 className={`text-sm ${priceData.change24h >= 0 ? 'text-green-400' : 'text-red-400'}`}>
{priceData.change24h >= 0 ? '↑' : '↓'} {Math.abs(priceData.change24h).toFixed(2)}%
</div>
</div>
</div>
@@ -261,15 +268,31 @@ export default function TradingChart() {
)}
<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>}
{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="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 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>
)