300 lines
10 KiB
TypeScript
300 lines
10 KiB
TypeScript
'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>
|
||
|
||
<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>
|
||
)
|
||
}
|