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

309 lines
11 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 }[]
}
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 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<{ 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 [mounted, setMounted] = useState(false)
// Load chart library from CDN on mount
useEffect(() => {
setMounted(true)
// 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 (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
})
} catch (e) {
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 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 (seriesRef.current) {
seriesRef.current.setData(formattedData)
}
if (chartRef.current) {
chartRef.current.timeScale().fitContent()
}
} 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) }
}
useEffect(() => {
if (mounted && chartRef.current) {
fetchChartData()
fetchPriceData()
}
}, [selectedAsset, selectedTimeframe, mounted])
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'
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>
{/* Indicator toggles */}
<div className="flex gap-2 flex-wrap">
<button className="px-3 py-1 rounded text-sm bg-brand-pink text-white">VOL</button>
<button className="px-3 py-1 rounded text-sm bg-white/10 text-white/50">EMA 20</button>
<button className="px-3 py-1 rounded text-sm bg-white/10 text-white/50">EMA 50</button>
<button className="px-3 py-1 rounded text-sm bg-white/10 text-white/50">S/R</button>
<button className="px-3 py-1 rounded text-sm bg-white/10 text-white/50">👁 THOTH</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>
)
}