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

277 lines
12 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'
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<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 [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 [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)
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()
return () => {
if (chartInstance) {
chartInstance.remove()
}
}
}, [])
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 })
} 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 } }
setPriceData(fallback[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()
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 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])
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 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 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="space-y-4">
<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>
<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 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>
{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>
)}
<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="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>
)
}