'use client' import { useState, useEffect, useRef, useCallback } from 'react' import { AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, ReferenceLine, ReferenceDot, ResponsiveContainer, } from 'recharts' import { TrendingUp, TrendingDown, RefreshCw, Eye } from 'lucide-react' // ─── Types ──────────────────────────────── interface PricePoint { date: string open: number close: number high: number low: number volume: number } interface TradeMarker { type: 'BUY' | 'SELL' date: string price: number } interface PositionChartProps { symbol: string entryPrice: number entryDate: string quantity: number /** Closed trades to show SELL markers */ exitPrice?: number exitDate?: string exitReason?: string /** Polling intervals in ms */ priceRefreshMs?: number chartRefreshMs?: number } // ─── Formatting ─────────────────────────── function fmtMoney(n: number) { return new Intl.NumberFormat('tr-TR', { style: 'currency', currency: 'TRY', minimumFractionDigits: 2, }).format(n) } // ─── Custom Tooltip ─────────────────────── function ChartTooltip({ active, payload, label }: { active?: boolean; payload?: Array<{ value: number }>; label?: string }) { if (!active || !payload?.length) return null return (

{label}

{fmtMoney(payload[0].value)}

) } // ─── BUY/SELL Marker Shape ──────────────── function BuyMarker(props: { cx?: number; cy?: number }) { const { cx = 0, cy = 0 } = props return ( {/* Green upward triangle */} AL ) } function SellMarker(props: { cx?: number; cy?: number }) { const { cx = 0, cy = 0 } = props return ( {/* Red downward triangle */} SAT ) } // ─── Main Component ─────────────────────── export default function PositionChart({ symbol, entryPrice, entryDate, quantity, exitPrice, exitDate, exitReason, priceRefreshMs = 10_000, chartRefreshMs = 60_000, }: PositionChartProps) { const [priceData, setPriceData] = useState([]) const [currentPrice, setCurrentPrice] = useState(null) const [prevPrice, setPrevPrice] = useState(null) const [loading, setLoading] = useState(true) const [lastUpdate, setLastUpdate] = useState('') const [expanded, setExpanded] = useState(true) const mountedRef = useRef(true) const isClosed = !!exitDate // ─── Fetch full chart data ───────────── const fetchChart = useCallback(async () => { try { const res = await fetch(`/api/stocks/${symbol}/prices?days=30&interval=1d`, { credentials: 'include', }) const json = await res.json() if (!mountedRef.current) return if (json.ok && json.data?.length > 0) { setPriceData(json.data) const last = json.data[json.data.length - 1] if (last) { setPrevPrice(currentPrice) setCurrentPrice(last.close) } } } catch { // silently fail, will retry } finally { if (mountedRef.current) setLoading(false) } }, [symbol]) // eslint-disable-line react-hooks/exhaustive-deps // ─── Fetch latest price (lightweight) ── const fetchPrice = useCallback(async () => { try { const res = await fetch(`/api/stocks/${symbol}/prices?days=1&interval=1d`, { credentials: 'include', }) const json = await res.json() if (!mountedRef.current) return if (json.ok && json.data?.length > 0) { const last = json.data[json.data.length - 1] setPrevPrice(currentPrice) setCurrentPrice(last.close) setLastUpdate(new Date().toLocaleTimeString('tr-TR')) } } catch { // silently fail } }, [symbol]) // eslint-disable-line react-hooks/exhaustive-deps // ─── Initial load + chart refresh ────── useEffect(() => { mountedRef.current = true fetchChart() if (chartRefreshMs > 0) { const interval = setInterval(fetchChart, chartRefreshMs) return () => { mountedRef.current = false clearInterval(interval) } } return () => { mountedRef.current = false } }, [fetchChart, chartRefreshMs]) // ─── Price polling (only for open positions) ── useEffect(() => { if (isClosed || priceRefreshMs <= 0) return const interval = setInterval(fetchPrice, priceRefreshMs) return () => clearInterval(interval) }, [fetchPrice, priceRefreshMs, isClosed]) // ─── Calculations ────────────────────── const displayPrice = isClosed ? (exitPrice ?? entryPrice) : (currentPrice ?? entryPrice) const pnl = (displayPrice - entryPrice) * quantity const pnlPct = ((displayPrice - entryPrice) / entryPrice) * 100 const totalValue = displayPrice * quantity const priceDirection = prevPrice !== null && currentPrice !== null ? currentPrice > prevPrice ? 'up' : currentPrice < prevPrice ? 'down' : 'same' : 'same' // ─── Find marker positions in data ───── const entryDateShort = entryDate.slice(0, 10) const exitDateShort = exitDate?.slice(0, 10) const entryPointIdx = priceData.findIndex(d => d.date >= entryDateShort) const exitPointIdx = exitDateShort ? priceData.findIndex(d => d.date >= exitDateShort) : -1 // ─── Stop loss / take profit lines ───── const stopLoss = entryPrice * 0.95 const takeProfit = entryPrice * 1.10 if (loading) { return (
) } return (
{/* ─── Header ─────────────────────── */}
setExpanded(!expanded)} >

{symbol}

{isClosed ? `Kapalı — ${exitReason || ''}` : 'Açık Pozisyon'} {!isClosed && lastUpdate && ( {lastUpdate} )}
{/* Current Price with animation */}
{displayPrice.toFixed(2)} ₺ {priceDirection === 'up' && } {priceDirection === 'down' && }
= 0 ? 'text-green-600' : 'text-red-600'}`}> {pnl >= 0 ? '+' : ''}{fmtMoney(pnl)} ({pnlPct >= 0 ? '+' : ''}{pnlPct.toFixed(2)}%)
{/* ─── Expanded Content ───────────── */} {expanded && (
{/* Stats Row */}
Adet

{quantity.toLocaleString('tr-TR')} lot

Giriş Fiyatı

{entryPrice.toFixed(2)} ₺

Toplam Değer

{fmtMoney(totalValue)}

Stop Loss

{stopLoss.toFixed(2)} ₺

Take Profit

{takeProfit.toFixed(2)} ₺

{/* Chart */} {priceData.length > 0 ? (
= 0 ? '#22c55e' : '#ef4444'} stopOpacity={0.3} /> = 0 ? '#22c55e' : '#ef4444'} stopOpacity={0.02} /> v.slice(5)} interval="preserveStartEnd" /> v.toFixed(1)} width={55} /> } /> {/* Price area */} = 0 ? '#22c55e' : '#ef4444'} strokeWidth={2} fill={`url(#gradient-${symbol})`} dot={false} animationDuration={500} /> {/* Entry price reference line */} {/* Stop loss line */} {!isClosed && ( )} {/* Take profit line */} {!isClosed && ( )} {/* BUY marker */} {entryPointIdx >= 0 && ( } /> )} {/* SELL marker */} {exitPointIdx >= 0 && exitPrice && ( } /> )}
) : (
Fiyat verisi bulunamadı
)}
)}
) }