'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' // ─── Types ───────────────────────────────── 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 } // ─── Custom Candlestick Shape ────────────── 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) // Scale helpers from the chart coordinate system 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) } // We'll use the bar as just the body const bodyH = Math.max(1, Math.abs(height)) const wickX = x + width / 2 return ( {/* Upper wick */} {/* Body */} {/* Lower wick */} ) } // ─── Chart Periods ───────────────────────── const PERIODS = [ { label: '1A', days: 30 }, { label: '3A', days: 90 }, { label: '6A', days: 180 }, { label: '1Y', days: 365 }, ] // ─── Overlay Toggle Options ──────────────── 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'] // ─── Sub-panel Options ───────────────────── 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' }, ] // ─── Main Component ──────────────────────── 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(null) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) const [period, setPeriod] = useState(180) const [overlays, setOverlays] = useState>(new Set(['sma20', 'bollinger'])) const [subPanel, setSubPanel] = useState('volume') const [showSR, setShowSR] = useState(true) const [showFib, setShowFib] = useState(false) const [showPatterns, setShowPatterns] = useState(true) const [activeTab, setActiveTab] = useState<'formations' | 'candles' | 'summary'>('summary') // Fetch analysis data 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 }) }, []) // Merged chart data 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, // For candlestick body bar: we draw from open to close 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', // For Bollinger band area bbRange: ind.bbUpper != null && ind.bbLower != null ? [ind.bbLower, ind.bbUpper] : undefined, // Volume coloring volColor: bullish ? 'rgba(34,197,94,0.5)' : 'rgba(239,68,68,0.5)', // MACD histogram coloring macdHistColor: (ind.macdHist ?? 0) >= 0 ? '#22c55e' : '#ef4444', // Pattern markers 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]) // Price domain with padding 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 (
) } if (error || !data) { return (

{t('Analiz yüklenemedi:', 'Analysis could not be loaded:')} {error || t('Veri bulunamadı', 'No data found')}

) } 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 (
{/* ─── Signal Banner ───────────────────── */}
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' }`}>
15 ? 'text-emerald-400' : summary.score < -15 ? 'text-red-400' : 'text-yellow-400' }`}> {summary.overallSignal}
{t('Skor:', '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' }} />
0 ? 'text-emerald-400' : summary.score < 0 ? 'text-red-400' : 'text-gray-400'}`}> {summary.score > 0 ? '+' : ''}{summary.score}
{/* ─── Controls ────────────────────────── */}
{/* Period selector */}
{PERIODS.map((p) => ( ))}
{/* Overlay toggles */}
{OVERLAYS.map((o) => ( ))}
{/* S/R toggle */} {/* Fibonacci toggle */} {/* Pattern markers toggle */}
{/* ─── Main Price Chart ────────────────── */}
v.toFixed(2)} tickLine={{ stroke: '#4b5563' }} axisLine={{ stroke: '#374151' }} width={65} /> } /> {/* Bollinger Bands Area */} {overlays.has('bollinger') && ( <> )} {/* Support/Resistance lines */} {showSR && supportResistance.map((sr, i) => ( = 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) => ( ))} {/* Price line (close) */} {/* Candlestick-like body bars */} {chartData.map((entry, i) => ( ))} {/* Moving averages overlays */} {overlays.has('sma20') && ( )} {overlays.has('sma50') && ( )} {overlays.has('sma200') && ( )} {overlays.has('ema') && ( <> )} {/* Pattern markers */} {showPatterns && chartData.some((d) => d.hasPattern) && ( 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 const pattern = props.payload.patternInfo?.[0] const color = pattern?.type === 'bullish' ? '#22c55e' : pattern?.type === 'bearish' ? '#ef4444' : '#f59e0b' return ( {pattern?.type === 'bullish' ? '▲' : pattern?.type === 'bearish' ? '▼' : '◆'} ) }} connectNulls={false} isAnimationActive={false} /> )} ''} />
{/* ─── Sub-Panel (RSI / MACD / Volume / Stochastic) ─ */}
{/* Sub-panel tabs */}
{subPanels.map((sp) => ( ))}
{/* Volume sub-panel — each component must be a direct child of ComposedChart (no Fragment wrapper) */} {subPanel === 'volume' && v >= 1e6 ? `${(v / 1e6).toFixed(1)}M` : v >= 1e3 ? `${(v / 1e3).toFixed(0)}K` : `${v}`} axisLine={{ stroke: '#374151' }} tickLine={false} width={50} />} {subPanel === 'volume' && } />} {subPanel === 'volume' && ( {chartData.map((entry, i) => ( ))} )} {/* RSI sub-panel */} {subPanel === 'rsi' && } {subPanel === 'rsi' && } />} {subPanel === 'rsi' && } {subPanel === 'rsi' && } {subPanel === 'rsi' && } {subPanel === 'rsi' && } {subPanel === 'rsi' && } {/* MACD sub-panel */} {subPanel === 'macd' && } {subPanel === 'macd' && } />} {subPanel === 'macd' && } {subPanel === 'macd' && ( {chartData.map((entry, i) => ( = 0 ? 'rgba(34,197,94,0.7)' : 'rgba(239,68,68,0.7)'} /> ))} )} {subPanel === 'macd' && } {subPanel === 'macd' && } {/* Stochastic sub-panel */} {subPanel === 'stochastic' && } {subPanel === 'stochastic' && } />} {subPanel === 'stochastic' && } {subPanel === 'stochastic' && } {subPanel === 'stochastic' && } {subPanel === 'stochastic' && }
{/* ─── Analysis Tabs ───────────────────── */}
{[ { 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) => ( ))}
{/* Summary Tab */} {activeTab === 'summary' && (
{/* Trend info */}

{t('Trend Analizi', 'Trend Analysis')}

{trend.description}

{t('Trend Gücü:', 'Trend Strength:')}
60 ? 'bg-emerald-500' : trend.strength > 40 ? 'bg-yellow-500' : 'bg-red-500'}`} style={{ width: `${trend.strength}%` }} />
{trend.strength}/100
{/* Signal reasons */}

{t('Sinyal Sebepleri', 'Signal Reasons')}

{summary.reasons.map((reason, i) => (
{reason}
))}
{/* Support & Resistance */} {supportResistance.length > 0 && (

{t('Destek & Direnç Seviyeleri', 'Support & Resistance Levels')}

{supportResistance.map((sr, i) => (
{sr.type === 'support' ? t('Destek', 'Support') : t('Direnç', 'Resistance')}
{cs}{sr.price.toFixed(2)}
{t('Güç:', 'Strength:')} {Array.from({ length: 5 }).map((_, j) => ( ))}
))}
)} {/* Fibonacci */} {fibonacci.length > 0 && (

{t('Fibonacci Düzeltme Seviyeleri', 'Fibonacci Retracement Levels')}

{fibonacci.map((fib, i) => (
{fib.label} {cs}{fib.price.toFixed(2)}
))}
)}
)} {/* Chart Formations Tab */} {activeTab === 'formations' && (
{chartFormations.length === 0 ? (

{t('Bu periyotta grafik formasyonu tespit edilmedi.', 'No chart pattern was detected in this period.')}

) : ( chartFormations.map((f, i) => (
{f.type === 'bullish' ? '▲' : f.type === 'bearish' ? '▼' : '◆'}
{f.name}
({f.nameEn})
{f.reliability === 'high' ? t('Yüksek', 'High') : f.reliability === 'medium' ? t('Orta', 'Medium') : t('Düşük', 'Low')} {t('güvenilirlik', 'reliability')}

{f.description}

{t('Başlangıç:', 'Start:')} {formatDate(f.startDate)} {t('Bitiş:', 'End:')} {formatDate(f.endDate)} {f.targetPrice && ( {t('Hedef:', 'Target:')} {cs}{f.targetPrice.toFixed(2)} )}
)) )}
)} {/* Candlestick Patterns Tab */} {activeTab === 'candles' && (
{candlestickPatterns.length === 0 ? (

{t('Bu periyotta mum formasyonu tespit edilmedi.', 'No candlestick pattern was detected in this period.')}

) : ( candlestickPatterns.slice(-15).reverse().map((p, i) => (
{p.name} ({p.nameEn})
{p.reliability === 'high' ? '★★★' : p.reliability === 'medium' ? '★★' : '★'} {formatDate(p.date)}

{p.description}

)) )}
)}
) } // ─── Helper Components ───────────────────── 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 ( {label}: {trendText} ) } 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) } // ─── Custom Tooltips ─────────────────────── 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 (
{label}
Açılış: {cs}{d.open?.toFixed(2)} Yüksek: {cs}{d.high?.toFixed(2)} Düşük: {cs}{d.low?.toFixed(2)} Kapanış: {cs}{d.close?.toFixed(2)} Hacim: {formatNumber(d.volume || 0)}
{d.sma20 && (
{d.sma20 &&
SMA20{cs}{d.sma20.toFixed(2)}
} {d.sma50 &&
SMA50{cs}{d.sma50.toFixed(2)}
} {d.rsi != null &&
RSI{d.rsi.toFixed(1)}
}
)} {d.hasPattern && (d.patternInfo?.length ?? 0) > 0 && (
{d.patternInfo?.map((p: { type: string; name: string }, i: number) => (
● {p.name}
))}
)}
) } function SubTooltip({ active, payload, type }: TooltipProps & { type: string }) { if (!active || !payload?.length) return null const d = payload[0]?.payload if (!d) return null return (
{d.dateFormatted}
{type === 'volume' && (
Hacim: {formatNumber(d.volume || 0)}
)} {type === 'rsi' && d.rsi != null && (
70 ? 'text-red-400' : d.rsi < 30 ? 'text-emerald-400' : 'text-orange-400'}`}> RSI: {d.rsi.toFixed(1)}
)} {type === 'macd' && (
{d.macd != null &&
MACD: {d.macd.toFixed(4)}
} {d.macdSignal != null &&
Sinyal: {d.macdSignal.toFixed(4)}
} {d.macdHist != null &&
= 0 ? 'text-emerald-400' : 'text-red-400'}`}>Hist: {d.macdHist.toFixed(4)}
}
)} {type === 'stochastic' && (
{d.stochK != null &&
%K: {d.stochK.toFixed(1)}
} {d.stochD != null &&
%D: {d.stochD.toFixed(1)}
}
)}
) }