'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ı
)}
)}
)
}