Files
sitemente/components/mission-control/TradingChart.tsx
T

418 lines
15 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client'
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
pair: string
direction: 'long' | 'short'
entry: number
stopLoss: number
takeProfit: number
result?: 'win' | 'loss' | 'open'
pnl?: number
rr?: number
}
interface ChartData {
time: number
open: number
high: number
low: number
close: number
volume?: number
}
interface ThothView {
thought: string
trend: string
phase: string
key_level: number
bias: string
confidence: number
reason: string
next_action: string
updated_at: string
bias_history?: { time: number; bias: string; price: number }[]
support_zones?: { level: number; strength: number }[]
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<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 [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 [priceData, setPriceData] = useState<PriceData>({ 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 [chartReady, setChartReady] = useState(false)
// Initialize chart - run only on client
useEffect(() => {
let mounted = true
const initChart = async () => {
try {
const charts = await import('lightweight-charts')
if (!mounted) return
createChart = charts.createChart
setChartReady(true)
} catch (e) {
console.error('Failed to load charts:', 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()
}
}, [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<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`)
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 })
} catch (e) {
console.warn("Price fetch failed, using fallback")
const fallbackPrices: 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(fallbackPrices[selectedAsset] || { price: 0, change24h: 0 })
}
}
const fetchChartData = async () => {
setLoading(true)
try {
const symbol = selectedAsset === 'BTC' ? 'BTCUSDT' : selectedAsset === 'SOL' ? 'SOLUSDT' : 'ETHUSDT'
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) }
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 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 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, 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>
{/* 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>
{/* 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>
{/* 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>
)}
{/* 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>
{/* 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 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
}