| 'use client' |
|
|
| import { useEffect, useState, useMemo, useCallback } from 'react' |
| import { |
| ComposedChart, |
| Line, |
| Bar, |
| Area, |
| XAxis, |
| YAxis, |
| CartesianGrid, |
| Tooltip, |
| ResponsiveContainer, |
| ReferenceLine, |
| Legend, |
| Cell, |
| Brush, |
| } from 'recharts' |
| import { formatCurrency, formatNumber, formatDate } from '@/lib/utils' |
|
|
| |
|
|
| interface OHLCVItem { |
| date: string |
| open: number |
| high: number |
| low: number |
| close: number |
| volume: number |
| } |
|
|
| interface IndicatorItem { |
| date: string |
| close: number |
| sma20: number | null |
| sma50: number | null |
| sma200: number | null |
| ema12: number | null |
| ema26: number | null |
| rsi: number | null |
| macd: number | null |
| macdSignal: number | null |
| macdHist: number | null |
| bbUpper: number | null |
| bbMiddle: number | null |
| bbLower: number | null |
| stochK: number | null |
| stochD: number | null |
| atr: number | null |
| volume: number |
| obv: number | null |
| } |
|
|
| interface SRLevel { |
| price: number |
| type: 'support' | 'resistance' |
| strength: number |
| touches: number |
| } |
|
|
| interface FibLevel { |
| level: number |
| label: string |
| price: number |
| } |
|
|
| interface CandlestickPattern { |
| index: number |
| date: string |
| name: string |
| nameEn: string |
| type: 'bullish' | 'bearish' | 'neutral' |
| reliability: 'low' | 'medium' | 'high' |
| description: string |
| } |
|
|
| interface ChartFormation { |
| name: string |
| nameEn: string |
| type: 'bullish' | 'bearish' | 'neutral' |
| startIndex: number |
| endIndex: number |
| startDate: string |
| endDate: string |
| reliability: 'low' | 'medium' | 'high' |
| description: string |
| targetPrice?: number |
| priceAtDetection: number |
| } |
|
|
| interface TrendAnalysis { |
| shortTerm: string |
| mediumTerm: string |
| longTerm: string |
| strength: number |
| description: string |
| } |
|
|
| interface AnalysisSummary { |
| overallSignal: string |
| score: number |
| reasons: string[] |
| } |
|
|
| interface StockAnalysisData { |
| ok: boolean |
| symbol: string |
| ohlcv: OHLCVItem[] |
| indicators: IndicatorItem[] |
| candlestickPatterns: CandlestickPattern[] |
| chartFormations: ChartFormation[] |
| supportResistance: SRLevel[] |
| fibonacci: FibLevel[] |
| trend: TrendAnalysis |
| summary: AnalysisSummary |
| } |
|
|
| |
|
|
| interface CandlestickBarProps { |
| x: number; y: number; width: number; height: number; |
| payload?: { open: number; close: number; high: number; low: number }; |
| background?: { height: number }; |
| yAxisDomain?: [number, number]; |
| } |
|
|
| function CandlestickBar(props: CandlestickBarProps) { |
| const { x, y, width, height, payload } = props |
| if (!payload) return null |
| const { open, close, high, low } = payload |
| const bullish = close >= open |
| const color = bullish ? '#22c55e' : '#ef4444' |
| const bodyTop = Math.min(open, close) |
| const bodyBottom = Math.max(open, close) |
|
|
| |
| const chartHeight = props.background?.height || 300 |
| const yScale = (val: number) => { |
| const domain = props.yAxisDomain || [low, high] |
| const range = domain[1] - domain[0] |
| if (range === 0) return y |
| return y + chartHeight * (1 - (val - domain[0]) / range) |
| } |
|
|
| |
| const bodyH = Math.max(1, Math.abs(height)) |
| const wickX = x + width / 2 |
|
|
| return ( |
| <g> |
| {/* Upper wick */} |
| <line x1={wickX} y1={y} x2={wickX} y2={y - 2} stroke={color} strokeWidth={1} /> |
| {/* Body */} |
| <rect x={x} y={y} width={width} height={Math.max(bodyH, 1)} fill={color} stroke={color} strokeWidth={0.5} rx={1} /> |
| {/* Lower wick */} |
| <line x1={wickX} y1={y + bodyH} x2={wickX} y2={y + bodyH + 2} stroke={color} strokeWidth={1} /> |
| </g> |
| ) |
| } |
|
|
| |
|
|
| const PERIODS = [ |
| { label: '1A', days: 30 }, |
| { label: '3A', days: 90 }, |
| { label: '6A', days: 180 }, |
| { label: '1Y', days: 365 }, |
| ] |
|
|
| |
|
|
| const OVERLAYS = [ |
| { key: 'sma20', label: 'SMA 20', color: '#f59e0b' }, |
| { key: 'sma50', label: 'SMA 50', color: '#3b82f6' }, |
| { key: 'sma200', label: 'SMA 200', color: '#a855f7' }, |
| { key: 'bollinger', label: 'Bollinger', color: '#6366f1' }, |
| { key: 'ema', label: 'EMA 12/26', color: '#14b8a6' }, |
| ] as const |
|
|
| type OverlayKey = typeof OVERLAYS[number]['key'] |
|
|
| |
|
|
| type SubPanel = 'rsi' | 'macd' | 'stochastic' | 'volume' | 'none' |
|
|
| const SUB_PANELS: { key: SubPanel; label: string }[] = [ |
| { key: 'volume', label: 'Hacim' }, |
| { key: 'rsi', label: 'RSI' }, |
| { key: 'macd', label: 'MACD' }, |
| { key: 'stochastic', label: 'Stokastik' }, |
| ] |
|
|
| |
|
|
| export default function AdvancedStockChart({ symbol, market = 'bist' }: { symbol: string; market?: 'bist' | 'us' }) { |
| const isUS = market === 'us' |
| const cs = isUS ? '$' : 'βΊ' |
| const t = useCallback((tr: string, en: string) => isUS ? en : tr, [isUS]) |
| const [data, setData] = useState<StockAnalysisData | null>(null) |
| const [loading, setLoading] = useState(true) |
| const [error, setError] = useState<string | null>(null) |
| const [period, setPeriod] = useState(180) |
| const [overlays, setOverlays] = useState<Set<OverlayKey>>(new Set(['sma20', 'bollinger'])) |
| const [subPanel, setSubPanel] = useState<SubPanel>('volume') |
| const [showSR, setShowSR] = useState(true) |
| const [showFib, setShowFib] = useState(false) |
| const [showPatterns, setShowPatterns] = useState(true) |
| const [activeTab, setActiveTab] = useState<'formations' | 'candles' | 'summary'>('summary') |
|
|
| |
| useEffect(() => { |
| let cancelled = false |
| setLoading(true) |
| setError(null) |
|
|
| fetch(`/api/stocks/${encodeURIComponent(symbol)}/analysis?days=${period}&market=${market}`) |
| .then((r) => r.json()) |
| .then((json) => { |
| if (cancelled) return |
| if (!json.ok) throw new Error(json.error || t('Analiz verisi alΔ±namadΔ±', 'Failed to load analysis data')) |
| setData(json) |
| }) |
| .catch((e) => { if (!cancelled) setError(e.message) }) |
| .finally(() => { if (!cancelled) setLoading(false) }) |
|
|
| return () => { cancelled = true } |
| }, [market, period, symbol, t]) |
|
|
| const toggleOverlay = useCallback((key: OverlayKey) => { |
| setOverlays((prev) => { |
| const next = new Set(prev) |
| if (next.has(key)) next.delete(key); else next.add(key) |
| return next |
| }) |
| }, []) |
|
|
| |
| const chartData = useMemo(() => { |
| if (!data) return [] |
| return data.ohlcv.map((item, i) => { |
| const ind = data.indicators[i] || {} |
| const bullish = item.close >= item.open |
| return { |
| ...item, |
| ...ind, |
| |
| bodyLow: Math.min(item.open, item.close), |
| bodyHigh: Math.max(item.open, item.close), |
| bodySize: Math.abs(item.close - item.open), |
| wickHigh: item.high - Math.max(item.open, item.close), |
| wickLow: Math.min(item.open, item.close) - item.low, |
| bullish, |
| color: bullish ? '#22c55e' : '#ef4444', |
| |
| bbRange: ind.bbUpper != null && ind.bbLower != null ? [ind.bbLower, ind.bbUpper] : undefined, |
| |
| volColor: bullish ? 'rgba(34,197,94,0.5)' : 'rgba(239,68,68,0.5)', |
| |
| macdHistColor: (ind.macdHist ?? 0) >= 0 ? '#22c55e' : '#ef4444', |
| |
| hasPattern: data.candlestickPatterns.some((p) => p.index === i), |
| patternInfo: data.candlestickPatterns.filter((p) => p.index === i), |
| dateFormatted: formatDateShort(item.date, isUS ? 'en-US' : 'tr-TR'), |
| } |
| }) |
| }, [data, isUS]) |
|
|
| |
| const priceDomain = useMemo(() => { |
| if (!chartData.length) return [0, 100] |
| let min = Infinity, max = -Infinity |
| for (const d of chartData) { |
| if (d.low < min) min = d.low |
| if (d.high > max) max = d.high |
| if (overlays.has('bollinger')) { |
| if (d.bbLower != null && d.bbLower < min) min = d.bbLower |
| if (d.bbUpper != null && d.bbUpper > max) max = d.bbUpper |
| } |
| } |
| const pad = (max - min) * 0.05 |
| return [Math.max(0, min - pad), max + pad] |
| }, [chartData, overlays]) |
|
|
| if (loading) { |
| return ( |
| <div className="bg-gray-900 rounded-2xl p-8 animate-pulse"> |
| <div className="h-8 bg-gray-800 rounded w-48 mb-6" /> |
| <div className="h-96 bg-gray-800 rounded-xl" /> |
| </div> |
| ) |
| } |
|
|
| if (error || !data) { |
| return ( |
| <div className="bg-gray-900 rounded-2xl p-8 border border-red-800"> |
| <p className="text-red-400">{t('Analiz yΓΌklenemedi:', 'Analysis could not be loaded:')} {error || t('Veri bulunamadΔ±', 'No data found')}</p> |
| </div> |
| ) |
| } |
|
|
| const { summary, trend, chartFormations, candlestickPatterns, supportResistance, fibonacci } = data |
| const subPanels = SUB_PANELS.map((panel) => ({ |
| ...panel, |
| label: panel.key === 'volume' ? t('Hacim', 'Volume') : panel.key === 'stochastic' ? t('Stokastik', 'Stochastic') : panel.label, |
| })) |
| const lastPrice = data.ohlcv[data.ohlcv.length - 1]?.close ?? 0 |
|
|
| return ( |
| <div className="space-y-4"> |
| {/* βββ Signal Banner βββββββββββββββββββββ */} |
| <div className={`rounded-xl p-4 border ${ |
| summary.score > 15 ? 'bg-emerald-950/50 border-emerald-700' : |
| summary.score < -15 ? 'bg-red-950/50 border-red-700' : |
| 'bg-yellow-950/50 border-yellow-700' |
| }`}> |
| <div className="flex items-center justify-between flex-wrap gap-4"> |
| <div className="flex items-center gap-4"> |
| <span className={`text-2xl font-bold ${ |
| summary.score > 15 ? 'text-emerald-400' : |
| summary.score < -15 ? 'text-red-400' : |
| 'text-yellow-400' |
| }`}> |
| {summary.overallSignal} |
| </span> |
| <div className="flex items-center gap-2"> |
| <span className="text-gray-400 text-sm">{t('Skor:', 'Score:')}</span> |
| <div className="w-32 h-3 bg-gray-800 rounded-full overflow-hidden"> |
| <div |
| className={`h-full rounded-full transition-all ${ |
| summary.score > 15 ? 'bg-emerald-500' : |
| summary.score < -15 ? 'bg-red-500' : 'bg-yellow-500' |
| }`} |
| style={{ width: `${Math.abs(summary.score)}%`, marginLeft: summary.score < 0 ? `${100 - Math.abs(summary.score)}%` : '0' }} |
| /> |
| </div> |
| <span className={`text-sm font-mono ${summary.score > 0 ? 'text-emerald-400' : summary.score < 0 ? 'text-red-400' : 'text-gray-400'}`}> |
| {summary.score > 0 ? '+' : ''}{summary.score} |
| </span> |
| </div> |
| </div> |
| <div className="flex items-center gap-3 text-sm"> |
| <TrendBadge label={t('KΔ±sa', 'Short')} trend={trend.shortTerm} isUS={isUS} /> |
| <TrendBadge label={t('Orta', 'Medium')} trend={trend.mediumTerm} isUS={isUS} /> |
| <TrendBadge label={t('Uzun', 'Long')} trend={trend.longTerm} isUS={isUS} /> |
| </div> |
| </div> |
| </div> |
| |
| {/* βββ Controls ββββββββββββββββββββββββββ */} |
| <div className="bg-gray-900 rounded-xl p-4 border border-gray-800"> |
| <div className="flex flex-wrap items-center gap-3"> |
| {/* Period selector */} |
| <div className="flex bg-gray-800 rounded-lg p-0.5"> |
| {PERIODS.map((p) => ( |
| <button |
| key={p.days} |
| onClick={() => setPeriod(p.days)} |
| className={`px-3 py-1.5 text-xs font-medium rounded-md transition-all ${ |
| period === p.days ? 'bg-blue-600 text-white shadow-lg' : 'text-gray-400 hover:text-white' |
| }`} |
| > |
| {p.label} |
| </button> |
| ))} |
| </div> |
| |
| <div className="w-px h-6 bg-gray-700" /> |
| |
| {/* Overlay toggles */} |
| <div className="flex flex-wrap gap-1.5"> |
| {OVERLAYS.map((o) => ( |
| <button |
| key={o.key} |
| onClick={() => toggleOverlay(o.key)} |
| className={`px-2.5 py-1 text-xs rounded-md border transition-all ${ |
| overlays.has(o.key) |
| ? 'border-transparent text-white' |
| : 'border-gray-700 text-gray-500 hover:text-gray-300' |
| }`} |
| style={overlays.has(o.key) ? { backgroundColor: o.color + '33', borderColor: o.color } : {}} |
| > |
| <span className="inline-block w-2 h-2 rounded-full mr-1.5" style={{ backgroundColor: o.color }} /> |
| {o.label} |
| </button> |
| ))} |
| </div> |
| |
| <div className="w-px h-6 bg-gray-700" /> |
| |
| {/* S/R toggle */} |
| <button |
| onClick={() => setShowSR((v) => !v)} |
| className={`px-2.5 py-1 text-xs rounded-md border transition-all ${ |
| showSR ? 'bg-orange-900/30 border-orange-600 text-orange-300' : 'border-gray-700 text-gray-500' |
| }`} |
| > |
| {t('D/R', 'S/R')} |
| </button> |
| |
| {/* Fibonacci toggle */} |
| <button |
| onClick={() => setShowFib((v) => !v)} |
| className={`px-2.5 py-1 text-xs rounded-md border transition-all ${ |
| showFib ? 'bg-purple-900/30 border-purple-600 text-purple-300' : 'border-gray-700 text-gray-500' |
| }`} |
| > |
| Fib |
| </button> |
| |
| {/* Pattern markers toggle */} |
| <button |
| onClick={() => setShowPatterns((v) => !v)} |
| className={`px-2.5 py-1 text-xs rounded-md border transition-all ${ |
| showPatterns ? 'bg-cyan-900/30 border-cyan-600 text-cyan-300' : 'border-gray-700 text-gray-500' |
| }`} |
| > |
| {t('Formasyonlar', 'Patterns')} |
| </button> |
| </div> |
| </div> |
| |
| {/* βββ Main Price Chart ββββββββββββββββββ */} |
| <div className="bg-gray-900 rounded-xl border border-gray-800 p-4"> |
| <ResponsiveContainer width="100%" height={420}> |
| <ComposedChart data={chartData} margin={{ top: 10, right: 10, bottom: 0, left: 10 }}> |
| <CartesianGrid strokeDasharray="3 3" stroke="#1f2937" /> |
| <XAxis |
| dataKey="dateFormatted" |
| tick={{ fill: '#9ca3af', fontSize: 11 }} |
| tickLine={{ stroke: '#4b5563' }} |
| axisLine={{ stroke: '#374151' }} |
| interval="preserveStartEnd" |
| minTickGap={50} |
| /> |
| <YAxis |
| domain={priceDomain} |
| tick={{ fill: '#9ca3af', fontSize: 11 }} |
| tickFormatter={(v: number) => v.toFixed(2)} |
| tickLine={{ stroke: '#4b5563' }} |
| axisLine={{ stroke: '#374151' }} |
| width={65} |
| /> |
| <Tooltip content={<PriceTooltip currencySymbol={cs} />} /> |
| |
| {/* Bollinger Bands Area */} |
| {overlays.has('bollinger') && ( |
| <> |
| <Area type="monotone" dataKey="bbUpper" stroke="none" fill="#6366f1" fillOpacity={0.06} /> |
| <Area type="monotone" dataKey="bbLower" stroke="none" fill="#6366f1" fillOpacity={0.06} /> |
| <Line type="monotone" dataKey="bbUpper" stroke="#6366f1" strokeWidth={1} dot={false} strokeDasharray="4 2" opacity={0.6} /> |
| <Line type="monotone" dataKey="bbLower" stroke="#6366f1" strokeWidth={1} dot={false} strokeDasharray="4 2" opacity={0.6} /> |
| <Line type="monotone" dataKey="bbMiddle" stroke="#6366f1" strokeWidth={1} dot={false} strokeDasharray="2 2" opacity={0.4} /> |
| </> |
| )} |
| |
| {/* Support/Resistance lines */} |
| {showSR && supportResistance.map((sr, i) => ( |
| <ReferenceLine |
| key={`sr-${i}`} |
| y={sr.price} |
| stroke={sr.type === 'support' ? '#22c55e' : '#ef4444'} |
| strokeDasharray="6 3" |
| strokeWidth={sr.strength >= 3 ? 2 : 1} |
| opacity={0.7} |
| label={{ |
| value: `${sr.type === 'support' ? 'D' : 'R'}: ${cs}${sr.price.toFixed(2)}`, |
| fill: sr.type === 'support' ? '#22c55e' : '#ef4444', |
| fontSize: 10, |
| position: sr.type === 'support' ? 'insideBottomRight' : 'insideTopRight', |
| }} |
| /> |
| ))} |
| |
| {/* Fibonacci levels */} |
| {showFib && fibonacci.map((fib, i) => ( |
| <ReferenceLine |
| key={`fib-${i}`} |
| y={fib.price} |
| stroke="#a855f7" |
| strokeDasharray="3 6" |
| strokeWidth={fib.level === 0.618 ? 2 : 1} |
| opacity={0.5} |
| label={{ |
| value: `${fib.label}`, |
| fill: '#a855f7', |
| fontSize: 9, |
| position: 'insideRight', |
| }} |
| /> |
| ))} |
| |
| {/* Price line (close) */} |
| <Line |
| type="monotone" |
| dataKey="close" |
| stroke="#60a5fa" |
| strokeWidth={2} |
| dot={false} |
| activeDot={{ r: 4, fill: '#60a5fa', stroke: '#1e3a5f' }} |
| /> |
| |
| {/* Candlestick-like body bars */} |
| <Bar dataKey="bodySize" stackId="candle" barSize={5} isAnimationActive={false}> |
| {chartData.map((entry, i) => ( |
| <Cell key={i} fill={entry.bullish ? '#22c55e' : '#ef4444'} /> |
| ))} |
| </Bar> |
| |
| {/* Moving averages overlays */} |
| {overlays.has('sma20') && ( |
| <Line type="monotone" dataKey="sma20" stroke="#f59e0b" strokeWidth={1.5} dot={false} connectNulls /> |
| )} |
| {overlays.has('sma50') && ( |
| <Line type="monotone" dataKey="sma50" stroke="#3b82f6" strokeWidth={1.5} dot={false} connectNulls /> |
| )} |
| {overlays.has('sma200') && ( |
| <Line type="monotone" dataKey="sma200" stroke="#a855f7" strokeWidth={1.5} dot={false} connectNulls /> |
| )} |
| {overlays.has('ema') && ( |
| <> |
| <Line type="monotone" dataKey="ema12" stroke="#14b8a6" strokeWidth={1} dot={false} connectNulls strokeDasharray="4 2" /> |
| <Line type="monotone" dataKey="ema26" stroke="#f97316" strokeWidth={1} dot={false} connectNulls strokeDasharray="4 2" /> |
| </> |
| )} |
| |
| {/* Pattern markers */} |
| {showPatterns && chartData.some((d) => d.hasPattern) && ( |
| <Line |
| type="monotone" |
| dataKey={(d: { hasPattern?: boolean; high: number }) => d.hasPattern ? d.high * 1.01 : undefined} |
| stroke="none" |
| dot={(props: { key?: string; cx: number; cy: number; payload?: { hasPattern?: boolean; patternInfo?: Array<{ type: string; name: string }> } }) => { |
| if (!props.payload?.hasPattern) return <g key={props.key} /> |
| const pattern = props.payload.patternInfo?.[0] |
| const color = pattern?.type === 'bullish' ? '#22c55e' : pattern?.type === 'bearish' ? '#ef4444' : '#f59e0b' |
| return ( |
| <g key={props.key}> |
| <circle cx={props.cx} cy={props.cy - 10} r={5} fill={color} opacity={0.8} /> |
| <text x={props.cx} y={props.cy - 10} textAnchor="middle" dy={3} fill="white" fontSize={8} fontWeight="bold"> |
| {pattern?.type === 'bullish' ? 'β²' : pattern?.type === 'bearish' ? 'βΌ' : 'β'} |
| </text> |
| </g> |
| ) |
| }} |
| connectNulls={false} |
| isAnimationActive={false} |
| /> |
| )} |
| |
| <Brush |
| dataKey="dateFormatted" |
| height={25} |
| stroke="#4b5563" |
| fill="#111827" |
| tickFormatter={() => ''} |
| /> |
| </ComposedChart> |
| </ResponsiveContainer> |
| </div> |
|
|
| {} |
| <div className="bg-gray-900 rounded-xl border border-gray-800 p-4"> |
| {} |
| <div className="flex gap-1 mb-3"> |
| {subPanels.map((sp) => ( |
| <button |
| key={sp.key} |
| onClick={() => setSubPanel(sp.key)} |
| className={`px-3 py-1.5 text-xs font-medium rounded-lg transition-all ${ |
| subPanel === sp.key ? 'bg-gray-700 text-white' : 'text-gray-500 hover:text-gray-300' |
| }`} |
| > |
| {sp.label} |
| </button> |
| ))} |
| </div> |
|
|
| <ResponsiveContainer width="100%" height={160}> |
| <ComposedChart data={chartData} margin={{ top: 5, right: 10, bottom: 0, left: 10 }}> |
| <CartesianGrid strokeDasharray="3 3" stroke="#1f2937" /> |
| <XAxis dataKey="dateFormatted" tick={{ fill: '#6b7280', fontSize: 10 }} axisLine={{ stroke: '#374151' }} tickLine={false} interval="preserveStartEnd" minTickGap={60} /> |
| |
| {/* Volume sub-panel β each component must be a direct child of ComposedChart (no Fragment wrapper) */} |
| {subPanel === 'volume' && <YAxis tick={{ fill: '#6b7280', fontSize: 10 }} tickFormatter={(v: number) => v >= 1e6 ? `${(v / 1e6).toFixed(1)}M` : v >= 1e3 ? `${(v / 1e3).toFixed(0)}K` : `${v}`} axisLine={{ stroke: '#374151' }} tickLine={false} width={50} />} |
| {subPanel === 'volume' && <Tooltip content={<SubTooltip type="volume" />} />} |
| {subPanel === 'volume' && ( |
| <Bar dataKey="volume" isAnimationActive={false}> |
| {chartData.map((entry, i) => ( |
| <Cell key={i} fill={entry.bullish ? 'rgba(34,197,94,0.6)' : 'rgba(239,68,68,0.6)'} /> |
| ))} |
| </Bar> |
| )} |
| |
| {/* RSI sub-panel */} |
| {subPanel === 'rsi' && <YAxis domain={[0, 100]} tick={{ fill: '#6b7280', fontSize: 10 }} axisLine={{ stroke: '#374151' }} tickLine={false} width={35} ticks={[20, 30, 50, 70, 80]} />} |
| {subPanel === 'rsi' && <Tooltip content={<SubTooltip type="rsi" />} />} |
| {subPanel === 'rsi' && <ReferenceLine y={70} stroke="#ef4444" strokeDasharray="4 4" opacity={0.5} />} |
| {subPanel === 'rsi' && <ReferenceLine y={30} stroke="#22c55e" strokeDasharray="4 4" opacity={0.5} />} |
| {subPanel === 'rsi' && <ReferenceLine y={50} stroke="#6b7280" strokeDasharray="2 4" opacity={0.3} />} |
| {subPanel === 'rsi' && <Area type="monotone" dataKey="rsi" stroke="none" fill="#f59e0b" fillOpacity={0.1} />} |
| {subPanel === 'rsi' && <Line type="monotone" dataKey="rsi" stroke="#f59e0b" strokeWidth={1.5} dot={false} connectNulls />} |
| |
| {/* MACD sub-panel */} |
| {subPanel === 'macd' && <YAxis tick={{ fill: '#6b7280', fontSize: 10 }} axisLine={{ stroke: '#374151' }} tickLine={false} width={50} />} |
| {subPanel === 'macd' && <Tooltip content={<SubTooltip type="macd" />} />} |
| {subPanel === 'macd' && <ReferenceLine y={0} stroke="#6b7280" strokeDasharray="2 4" opacity={0.5} />} |
| {subPanel === 'macd' && ( |
| <Bar dataKey="macdHist" isAnimationActive={false}> |
| {chartData.map((entry, i) => ( |
| <Cell key={i} fill={(entry.macdHist ?? 0) >= 0 ? 'rgba(34,197,94,0.7)' : 'rgba(239,68,68,0.7)'} /> |
| ))} |
| </Bar> |
| )} |
| {subPanel === 'macd' && <Line type="monotone" dataKey="macd" stroke="#3b82f6" strokeWidth={1.5} dot={false} connectNulls />} |
| {subPanel === 'macd' && <Line type="monotone" dataKey="macdSignal" stroke="#f97316" strokeWidth={1.5} dot={false} connectNulls />} |
| |
| {/* Stochastic sub-panel */} |
| {subPanel === 'stochastic' && <YAxis domain={[0, 100]} tick={{ fill: '#6b7280', fontSize: 10 }} axisLine={{ stroke: '#374151' }} tickLine={false} width={35} ticks={[20, 50, 80]} />} |
| {subPanel === 'stochastic' && <Tooltip content={<SubTooltip type="stochastic" />} />} |
| {subPanel === 'stochastic' && <ReferenceLine y={80} stroke="#ef4444" strokeDasharray="4 4" opacity={0.5} />} |
| {subPanel === 'stochastic' && <ReferenceLine y={20} stroke="#22c55e" strokeDasharray="4 4" opacity={0.5} />} |
| {subPanel === 'stochastic' && <Line type="monotone" dataKey="stochK" stroke="#60a5fa" strokeWidth={1.5} dot={false} connectNulls />} |
| {subPanel === 'stochastic' && <Line type="monotone" dataKey="stochD" stroke="#fb923c" strokeWidth={1.5} dot={false} connectNulls strokeDasharray="4 2" />} |
| </ComposedChart> |
| </ResponsiveContainer> |
| </div> |
|
|
| {} |
| <div className="bg-gray-900 rounded-xl border border-gray-800"> |
| <div className="flex border-b border-gray-800"> |
| {[ |
| { key: 'summary' as const, label: t('Γzet & Sebepler', 'Summary & Reasons') }, |
| { key: 'formations' as const, label: `${t('Formasyonlar', 'Patterns')} (${chartFormations.length})` }, |
| { key: 'candles' as const, label: `${t('Mum FormasyonlarΔ±', 'Candlestick Patterns')} (${candlestickPatterns.length})` }, |
| ].map((tab) => ( |
| <button |
| key={tab.key} |
| onClick={() => setActiveTab(tab.key)} |
| className={`px-5 py-3.5 text-sm font-medium transition-all ${ |
| activeTab === tab.key |
| ? 'text-blue-400 border-b-2 border-blue-500 bg-blue-950/20' |
| : 'text-gray-500 hover:text-gray-300' |
| }`} |
| > |
| {tab.label} |
| </button> |
| ))} |
| </div> |
|
|
| <div className="p-5"> |
| {/* Summary Tab */} |
| {activeTab === 'summary' && ( |
| <div className="space-y-4"> |
| {/* Trend info */} |
| <div className="bg-gray-800/50 rounded-lg p-4"> |
| <h4 className="text-sm font-semibold text-gray-300 mb-2">{t('Trend Analizi', 'Trend Analysis')}</h4> |
| <p className="text-gray-400 text-sm leading-relaxed">{trend.description}</p> |
| <div className="flex items-center gap-2 mt-3"> |
| <span className="text-gray-500 text-xs">{t('Trend GΓΌcΓΌ:', 'Trend Strength:')}</span> |
| <div className="w-24 h-2 bg-gray-700 rounded-full overflow-hidden"> |
| <div |
| className={`h-full rounded-full ${trend.strength > 60 ? 'bg-emerald-500' : trend.strength > 40 ? 'bg-yellow-500' : 'bg-red-500'}`} |
| style={{ width: `${trend.strength}%` }} |
| /> |
| </div> |
| <span className="text-xs text-gray-400">{trend.strength}/100</span> |
| </div> |
| </div> |
| |
| {/* Signal reasons */} |
| <div> |
| <h4 className="text-sm font-semibold text-gray-300 mb-3">{t('Sinyal Sebepleri', 'Signal Reasons')}</h4> |
| <div className="space-y-2"> |
| {summary.reasons.map((reason, i) => ( |
| <div key={i} className="flex items-start gap-2 text-sm"> |
| <span className={`mt-0.5 text-xs ${ |
| reason.toLowerCase().includes('yΓΌkseliΕ') || reason.toLowerCase().includes('uptrend') || reason.toLowerCase().includes('bullish') || reason.toLowerCase().includes('alΔ±m') || reason.toLowerCase().includes('buy') || reason.toLowerCase().includes('satΔ±m bΓΆlgesinde') || reason.includes('AL') |
| ? 'text-emerald-400' |
| : reason.toLowerCase().includes('dΓΌΕΓΌΕ') || reason.toLowerCase().includes('downtrend') || reason.toLowerCase().includes('bearish') || reason.toLowerCase().includes('satΔ±Ε') || reason.toLowerCase().includes('sell') || reason.toLowerCase().includes('alΔ±m bΓΆlgesinde') || reason.includes('SAT') |
| ? 'text-red-400' |
| : 'text-yellow-400' |
| }`}>β</span> |
| <span className="text-gray-300">{reason}</span> |
| </div> |
| ))} |
| </div> |
| </div> |
| |
| {/* Support & Resistance */} |
| {supportResistance.length > 0 && ( |
| <div> |
| <h4 className="text-sm font-semibold text-gray-300 mb-3">{t('Destek & DirenΓ§ Seviyeleri', 'Support & Resistance Levels')}</h4> |
| <div className="grid grid-cols-2 sm:grid-cols-3 gap-2"> |
| {supportResistance.map((sr, i) => ( |
| <div key={`${sr.type}-${sr.price}`} className={`rounded-lg p-3 border ${ |
| sr.type === 'support' |
| ? 'bg-emerald-950/30 border-emerald-800/50' |
| : 'bg-red-950/30 border-red-800/50' |
| }`}> |
| <div className="text-xs text-gray-500 mb-1"> |
| {sr.type === 'support' ? t('Destek', 'Support') : t('DirenΓ§', 'Resistance')} |
| </div> |
| <div className={`text-lg font-mono font-bold ${ |
| sr.type === 'support' ? 'text-emerald-400' : 'text-red-400' |
| }`}> |
| {cs}{sr.price.toFixed(2)} |
| </div> |
| <div className="flex items-center gap-1 mt-1"> |
| <span className="text-xs text-gray-500">{t('GΓΌΓ§:', 'Strength:')}</span> |
| {Array.from({ length: 5 }).map((_, j) => ( |
| <span key={j} className={`w-1.5 h-1.5 rounded-full ${ |
| j < sr.strength ? (sr.type === 'support' ? 'bg-emerald-500' : 'bg-red-500') : 'bg-gray-700' |
| }`} /> |
| ))} |
| </div> |
| </div> |
| ))} |
| </div> |
| </div> |
| )} |
| |
| {/* Fibonacci */} |
| {fibonacci.length > 0 && ( |
| <div> |
| <h4 className="text-sm font-semibold text-gray-300 mb-3">{t('Fibonacci DΓΌzeltme Seviyeleri', 'Fibonacci Retracement Levels')}</h4> |
| <div className="flex flex-wrap gap-2"> |
| {fibonacci.map((fib, i) => ( |
| <div key={fib.label} className="bg-purple-950/30 border border-purple-800/40 rounded-lg px-3 py-2"> |
| <span className="text-purple-400 text-xs font-mono">{fib.label}</span> |
| <span className="text-gray-300 text-sm ml-2 font-mono">{cs}{fib.price.toFixed(2)}</span> |
| </div> |
| ))} |
| </div> |
| </div> |
| )} |
| </div> |
| )} |
| |
| {/* Chart Formations Tab */} |
| {activeTab === 'formations' && ( |
| <div className="space-y-3"> |
| {chartFormations.length === 0 ? ( |
| <p className="text-gray-500 text-sm py-4 text-center">{t('Bu periyotta grafik formasyonu tespit edilmedi.', 'No chart pattern was detected in this period.')}</p> |
| ) : ( |
| chartFormations.map((f, i) => ( |
| <div key={`${f.name}-${f.startDate}`} className={`rounded-lg p-4 border ${ |
| f.type === 'bullish' ? 'bg-emerald-950/20 border-emerald-800/40' : |
| f.type === 'bearish' ? 'bg-red-950/20 border-red-800/40' : |
| 'bg-yellow-950/20 border-yellow-800/40' |
| }`}> |
| <div className="flex items-center justify-between mb-2"> |
| <div className="flex items-center gap-2"> |
| <span className={`text-lg ${ |
| f.type === 'bullish' ? 'text-emerald-400' : f.type === 'bearish' ? 'text-red-400' : 'text-yellow-400' |
| }`}> |
| {f.type === 'bullish' ? 'β²' : f.type === 'bearish' ? 'βΌ' : 'β'} |
| </span> |
| <h5 className="text-white font-semibold">{f.name}</h5> |
| <span className="text-gray-500 text-xs">({f.nameEn})</span> |
| </div> |
| <div className="flex items-center gap-2"> |
| <span className={`text-xs px-2 py-0.5 rounded ${ |
| f.reliability === 'high' ? 'bg-blue-900/50 text-blue-300' : |
| f.reliability === 'medium' ? 'bg-yellow-900/50 text-yellow-300' : |
| 'bg-gray-800 text-gray-400' |
| }`}> |
| {f.reliability === 'high' ? t('YΓΌksek', 'High') : f.reliability === 'medium' ? t('Orta', 'Medium') : t('DΓΌΕΓΌk', 'Low')} {t('gΓΌvenilirlik', 'reliability')} |
| </span> |
| </div> |
| </div> |
| <p className="text-gray-300 text-sm leading-relaxed">{f.description}</p> |
| <div className="flex items-center gap-4 mt-3 text-xs text-gray-500"> |
| <span>{t('BaΕlangΔ±Γ§:', 'Start:')} {formatDate(f.startDate)}</span> |
| <span>{t('BitiΕ:', 'End:')} {formatDate(f.endDate)}</span> |
| {f.targetPrice && ( |
| <span className={`font-mono ${f.type === 'bullish' ? 'text-emerald-400' : 'text-red-400'}`}> |
| {t('Hedef:', 'Target:')} {cs}{f.targetPrice.toFixed(2)} |
| </span> |
| )} |
| </div> |
| </div> |
| )) |
| )} |
| </div> |
| )} |
| |
| {/* Candlestick Patterns Tab */} |
| {activeTab === 'candles' && ( |
| <div className="space-y-2"> |
| {candlestickPatterns.length === 0 ? ( |
| <p className="text-gray-500 text-sm py-4 text-center">{t('Bu periyotta mum formasyonu tespit edilmedi.', 'No candlestick pattern was detected in this period.')}</p> |
| ) : ( |
| candlestickPatterns.slice(-15).reverse().map((p, i) => ( |
| <div key={`${p.name}-${p.date}`} className={`rounded-lg p-3 border ${ |
| p.type === 'bullish' ? 'bg-emerald-950/15 border-emerald-900/40' : |
| p.type === 'bearish' ? 'bg-red-950/15 border-red-900/40' : |
| 'bg-gray-800/30 border-gray-700/40' |
| }`}> |
| <div className="flex items-center justify-between"> |
| <div className="flex items-center gap-2"> |
| <span className={`w-2 h-2 rounded-full ${ |
| p.type === 'bullish' ? 'bg-emerald-500' : p.type === 'bearish' ? 'bg-red-500' : 'bg-yellow-500' |
| }`} /> |
| <span className="text-white text-sm font-semibold">{p.name}</span> |
| <span className="text-gray-600 text-xs">({p.nameEn})</span> |
| </div> |
| <div className="flex items-center gap-2"> |
| <span className={`text-xs ${ |
| p.reliability === 'high' ? 'text-blue-400' : p.reliability === 'medium' ? 'text-yellow-400' : 'text-gray-500' |
| }`}> |
| {p.reliability === 'high' ? 'β
β
β
' : p.reliability === 'medium' ? 'β
β
' : 'β
'} |
| </span> |
| <span className="text-xs text-gray-500">{formatDate(p.date)}</span> |
| </div> |
| </div> |
| <p className="text-gray-400 text-xs mt-1.5 leading-relaxed">{p.description}</p> |
| </div> |
| )) |
| )} |
| </div> |
| )} |
| </div> |
| </div> |
| </div> |
| ) |
| } |
|
|
| |
|
|
| function TrendBadge({ label, trend, isUS = false }: { label: string; trend: string; isUS?: boolean }) { |
| const trendText = isUS |
| ? trend === 'YΓΌkseliΕ' ? 'Uptrend' : trend === 'DΓΌΕΓΌΕ' ? 'Downtrend' : trend === 'Yatay' ? 'Sideways' : trend |
| : trend |
| const color = trend === 'YΓΌkseliΕ' || trend === 'Uptrend' ? 'text-emerald-400 bg-emerald-950/50' : |
| trend === 'DΓΌΕΓΌΕ' || trend === 'Downtrend' ? 'text-red-400 bg-red-950/50' : |
| 'text-yellow-400 bg-yellow-950/50' |
| return ( |
| <span className={`px-2 py-1 rounded text-xs font-medium ${color}`}> |
| {label}: {trendText} |
| </span> |
| ) |
| } |
|
|
| function formatDateShort(dateStr: string, locale = 'tr-TR'): string { |
| const d = new Date(dateStr) |
| return new Intl.DateTimeFormat(locale, { day: '2-digit', month: 'short' }).format(d) |
| } |
|
|
| |
|
|
| interface ChartDataPoint { |
| open?: number; close?: number; high?: number; low?: number; |
| volume?: number; bullish?: boolean; dateFormatted?: string; |
| sma20?: number; sma50?: number; rsi?: number; |
| macd?: number; macdSignal?: number; macdHist?: number; |
| stochK?: number; stochD?: number; |
| hasPattern?: boolean; patternInfo?: Array<{ type: string; name: string }>; |
| [key: string]: unknown; |
| } |
|
|
| interface TooltipProps { active?: boolean; payload?: Array<{ payload: ChartDataPoint }>; label?: string } |
|
|
| function PriceTooltip({ active, payload, label, currencySymbol = 'βΊ' }: TooltipProps & { currencySymbol?: string }) { |
| if (!active || !payload?.length) return null |
| const d = payload[0]?.payload |
| if (!d) return null |
| const cs = currencySymbol |
|
|
| return ( |
| <div className="bg-gray-950 border border-gray-700 rounded-lg p-3 shadow-xl text-xs"> |
| <div className="font-semibold text-gray-200 mb-2">{label}</div> |
| <div className="grid grid-cols-2 gap-x-4 gap-y-1"> |
| <span className="text-gray-500">AΓ§Δ±lΔ±Ε:</span> |
| <span className="text-gray-300 font-mono">{cs}{d.open?.toFixed(2)}</span> |
| <span className="text-gray-500">YΓΌksek:</span> |
| <span className="text-emerald-400 font-mono">{cs}{d.high?.toFixed(2)}</span> |
| <span className="text-gray-500">DΓΌΕΓΌk:</span> |
| <span className="text-red-400 font-mono">{cs}{d.low?.toFixed(2)}</span> |
| <span className="text-gray-500">KapanΔ±Ε:</span> |
| <span className={`font-mono font-bold ${d.bullish ? 'text-emerald-400' : 'text-red-400'}`}>{cs}{d.close?.toFixed(2)}</span> |
| <span className="text-gray-500">Hacim:</span> |
| <span className="text-gray-300 font-mono">{formatNumber(d.volume || 0)}</span> |
| </div> |
| {d.sma20 && ( |
| <div className="mt-2 pt-2 border-t border-gray-700 space-y-0.5"> |
| {d.sma20 && <div className="flex justify-between"><span className="text-yellow-400">SMA20</span><span className="text-gray-300 font-mono">{cs}{d.sma20.toFixed(2)}</span></div>} |
| {d.sma50 && <div className="flex justify-between"><span className="text-blue-400">SMA50</span><span className="text-gray-300 font-mono">{cs}{d.sma50.toFixed(2)}</span></div>} |
| {d.rsi != null && <div className="flex justify-between"><span className="text-orange-400">RSI</span><span className="text-gray-300 font-mono">{d.rsi.toFixed(1)}</span></div>} |
| </div> |
| )} |
| {d.hasPattern && (d.patternInfo?.length ?? 0) > 0 && ( |
| <div className="mt-2 pt-2 border-t border-gray-700"> |
| {d.patternInfo?.map((p: { type: string; name: string }, i: number) => ( |
| <div key={i} className={`text-xs ${p.type === 'bullish' ? 'text-emerald-400' : p.type === 'bearish' ? 'text-red-400' : 'text-yellow-400'}`}> |
| β {p.name} |
| </div> |
| ))} |
| </div> |
| )} |
| </div> |
| ) |
| } |
|
|
| function SubTooltip({ active, payload, type }: TooltipProps & { type: string }) { |
| if (!active || !payload?.length) return null |
| const d = payload[0]?.payload |
| if (!d) return null |
|
|
| return ( |
| <div className="bg-gray-950 border border-gray-700 rounded-lg p-2 shadow-xl text-xs"> |
| <div className="text-gray-400 mb-1">{d.dateFormatted}</div> |
| {type === 'volume' && ( |
| <div className="text-gray-200">Hacim: <span className="font-mono">{formatNumber(d.volume || 0)}</span></div> |
| )} |
| {type === 'rsi' && d.rsi != null && ( |
| <div className={`font-mono ${d.rsi > 70 ? 'text-red-400' : d.rsi < 30 ? 'text-emerald-400' : 'text-orange-400'}`}> |
| RSI: {d.rsi.toFixed(1)} |
| </div> |
| )} |
| {type === 'macd' && ( |
| <div className="space-y-0.5"> |
| {d.macd != null && <div className="text-blue-400 font-mono">MACD: {d.macd.toFixed(4)}</div>} |
| {d.macdSignal != null && <div className="text-orange-400 font-mono">Sinyal: {d.macdSignal.toFixed(4)}</div>} |
| {d.macdHist != null && <div className={`font-mono ${d.macdHist >= 0 ? 'text-emerald-400' : 'text-red-400'}`}>Hist: {d.macdHist.toFixed(4)}</div>} |
| </div> |
| )} |
| {type === 'stochastic' && ( |
| <div className="space-y-0.5"> |
| {d.stochK != null && <div className="text-blue-400 font-mono">%K: {d.stochK.toFixed(1)}</div>} |
| {d.stochD != null && <div className="text-orange-400 font-mono">%D: {d.stochD.toFixed(1)}</div>} |
| </div> |
| )} |
| </div> |
| ) |
| } |
|
|