borsa / nextjs-app /src /components /AdvancedStockChart.tsx
veteroner's picture
fix: make sync endpoint market-aware β€” prevent US scan results from overwriting BIST file
ce7f322
'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 (
<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>
)
}
// ─── 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<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')
// 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 (
<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>
{/* ─── Sub-Panel (RSI / MACD / Volume / Stochastic) ─ */}
<div className="bg-gray-900 rounded-xl border border-gray-800 p-4">
{/* Sub-panel tabs */}
<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>
{/* ─── Analysis Tabs ───────────────────── */}
<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>
)
}
// ─── 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 (
<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)
}
// ─── 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 (
<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>
)
}