import { QUANTITY_SCALE } from "./quantity"; import { addDays, addMonths } from "./recurrence"; /** * Varlık takibi hesap motoru (Bölüm 6.6): ağırlıklı ortalama maliyet, gerçekleşmiş ve * gerçekleşmemiş kâr/zarar. Miktarlar binde bir tamsayı, tutarlar kuruş. * * - Alım (`buy`): miktar ve maliyet artar; ödenen TL "yatırılan" sayılır. * - Gelen hediye/miras (`gift_in`): miktar artar; maliyet = girildiği günkü değer (yatırım değil). * - Satış (`sell`): ortalama maliyetle maliyet düşer; satış tutarı − düşen maliyet = gerçekleşmiş K/Z. * - Hediye/çıkış (`gift_out`): ortalama maliyetle maliyet düşer; kâr/zarar yazılmaz. */ export const MOVEMENT_KINDS = ["buy", "gift_in", "sell", "gift_out"] as const; export type MovementKind = (typeof MOVEMENT_KINDS)[number]; export const isInflow = (k: MovementKind) => k === "buy" || k === "gift_in"; export type MovementInput = { kind: MovementKind; quantity: number; /** Alımda ödenen, satışta alınan, gelen hediyede değer; çıkışta yok sayılır. */ amountMinor: number; }; export type MovementStep = { /** Satış/çıkışta ortalama maliyetle düşülen maliyet. */ costOutMinor: number; /** Yalnızca satışta: satış tutarı − düşülen maliyet. */ realizedMinor: number; /** Hareketten sonraki miktar ve kalan maliyet. */ quantityAfter: number; costAfterMinor: number; }; export type PortfolioRun = { steps: MovementStep[]; quantity: number; /** Kalan miktarın maliyet esası. */ costMinor: number; /** Alımlarda ödenen toplam TL. */ investedMinor: number; /** Satışlardan alınan toplam TL. */ proceedsMinor: number; realizedMinor: number; /** Gelen hediyelerin girişteki değeri. */ giftInMinor: number; /** Çıkışlarla düşen maliyet. */ giftOutCostMinor: number; /** Miktarın eksiye düştüğü ilk hareketin sırası (geçerli dizide null). */ negativeAt: number | null; }; /** Hareketleri kronolojik sırada (çağıran sıralar) işler. */ export function runMovements(movements: MovementInput[]): PortfolioRun { let quantity = 0; let cost = 0; const r: PortfolioRun = { steps: [], quantity: 0, costMinor: 0, investedMinor: 0, proceedsMinor: 0, realizedMinor: 0, giftInMinor: 0, giftOutCostMinor: 0, negativeAt: null, }; movements.forEach((m, i) => { let costOut = 0; let realized = 0; if (isInflow(m.kind)) { quantity += m.quantity; cost += m.amountMinor; if (m.kind === "buy") r.investedMinor += m.amountMinor; else r.giftInMinor += m.amountMinor; } else { if (m.quantity > quantity) { if (r.negativeAt === null) r.negativeAt = i; costOut = cost; } else { costOut = m.quantity === quantity ? cost : Math.round((cost * m.quantity) / quantity); } quantity = Math.max(0, quantity - m.quantity); cost -= costOut; if (m.kind === "sell") { realized = m.amountMinor - costOut; r.proceedsMinor += m.amountMinor; r.realizedMinor += realized; } else r.giftOutCostMinor += costOut; } r.steps.push({ costOutMinor: costOut, realizedMinor: realized, quantityAfter: quantity, costAfterMinor: cost, }); }); r.quantity = quantity; r.costMinor = cost; return r; } export type HoldingSummary = { quantity: number; costMinor: number; /** Birim başına ağırlıklı ortalama maliyet (miktar 0 ise null). */ avgCostMinor: number | null; /** Güncel alış fiyatıyla değer (fiyat yoksa null). */ currentValueMinor: number | null; unrealizedMinor: number | null; /** Gerçekleşmemiş K/Z oranı, baz puan (%12,34 → 1234); maliyet 0 ise null. */ unrealizedBp: number | null; realizedMinor: number; investedMinor: number; proceedsMinor: number; }; /** Özet: `priceMinor` değerleme fiyatıdır (Faz 0: varlıklar alış fiyatıyla). */ export function holdingSummary(run: PortfolioRun, priceMinor: number | null): HoldingSummary { const avgCostMinor = run.quantity > 0 ? Math.round((run.costMinor * QUANTITY_SCALE) / run.quantity) : null; const currentValueMinor = priceMinor === null ? null : Math.round((run.quantity * priceMinor) / QUANTITY_SCALE); const unrealizedMinor = currentValueMinor === null ? null : currentValueMinor - run.costMinor; const unrealizedBp = unrealizedMinor === null || run.costMinor <= 0 ? null : Math.round((unrealizedMinor * 10_000) / run.costMinor); return { quantity: run.quantity, costMinor: run.costMinor, avgCostMinor, currentValueMinor, unrealizedMinor, unrealizedBp, realizedMinor: run.realizedMinor, investedMinor: run.investedMinor, proceedsMinor: run.proceedsMinor, }; } /** Baz puanı "%12,34" / "−%3,5" biçiminde. */ export function formatBp(bp: number): string { const s = (Math.abs(bp) / 100).toLocaleString("tr-TR", { maximumFractionDigits: 2 }); return `${bp < 0 ? "−" : bp > 0 ? "+" : ""}%${s}`; } // --- Dönemsel rapor (Bölüm 6.6) --- export type DatedMovement = MovementInput & { date: string }; /** Dönem noktaları: başlangıç, aradaki ay sonları ve bitiş ("YYYY-MM-DD", artan, tekil). */ export function periodPoints(from: string, to: string): string[] { const out = [from]; let monthEnd = addMonths(`${from.slice(0, 7)}-01`, 0, "last"); while (monthEnd < to) { if (monthEnd > from) out.push(monthEnd); monthEnd = addMonths(addDays(monthEnd, 1), 0, "last"); } if (to > from) out.push(to); return out; } export type PeriodPoint = { date: string; valueMinor: number | null; costMinor: number }; export type PeriodReport = { from: string; to: string; /** Dönem başındaki (başlangıç gününden önceki hareketlerle) değer. */ startValueMinor: number | null; endValueMinor: number | null; /** Dönemdeki alımlarda ödenen. */ investedMinor: number; /** Dönemdeki satışlardan alınan. */ proceedsMinor: number; /** Dönemde gelen hediyelerin girişteki değeri. */ giftInMinor: number; /** Dönemde çıkan hediyelerin o günkü değeri (fiyat yoksa maliyeti). */ giftOutMinor: number; /** * Dönem kârı: bitiş değeri + satış + çıkan hediye − başlangıç değeri − alım − gelen hediye. * Fiyat eksikse null. */ profitMinor: number | null; /** Kârın, dönemde bağlanan sermayeye (başlangıç + alım + gelen hediye) oranı (baz puan). */ profitBp: number | null; points: PeriodPoint[]; }; /** * Bir varlığın dönem raporu. `movements` kronolojik sıralı; `priceAt(date)` o tarihteki (veya * öncesindeki en yakın) değerleme fiyatı (alış), yoksa null. */ export function periodReport( movements: DatedMovement[], priceAt: (date: string) => number | null, from: string, to: string, ): PeriodReport { const valueAt = (date: string, inclusive: boolean) => { const run = runMovements(movements.filter((m) => (inclusive ? m.date <= date : m.date < date))); const price = priceAt(date); return { run, value: run.quantity === 0 ? 0 : price === null ? null : Math.round((run.quantity * price) / QUANTITY_SCALE), }; }; const start = valueAt(from, false); const end = valueAt(to, true); const all = runMovements(movements); let invested = 0; let proceeds = 0; let giftIn = 0; let giftOut = 0; movements.forEach((m, i) => { if (m.date < from || m.date > to) return; if (m.kind === "buy") invested += m.amountMinor; else if (m.kind === "sell") proceeds += m.amountMinor; else if (m.kind === "gift_in") giftIn += m.amountMinor; else { const price = priceAt(m.date); giftOut += price === null ? all.steps[i]!.costOutMinor : Math.round((m.quantity * price) / QUANTITY_SCALE); } }); const profitMinor = start.value === null || end.value === null ? null : end.value + proceeds + giftOut - start.value - invested - giftIn; const base = (start.value ?? 0) + invested + giftIn; const points = periodPoints(from, to).map((date, i) => { const p = i === 0 ? start : valueAt(date, true); return { date, valueMinor: p.value, costMinor: p.run.costMinor }; }); return { from, to, startValueMinor: start.value, endValueMinor: end.value, investedMinor: invested, proceedsMinor: proceeds, giftInMinor: giftIn, giftOutMinor: giftOut, profitMinor, profitBp: profitMinor === null || base <= 0 ? null : Math.round((profitMinor * 10_000) / base), points, }; } /** Birden çok varlığın raporlarını toplar (noktalar aynı tarihlerde). Eksik fiyatlı değerler atlanır. */ export function sumReports(reports: PeriodReport[], from: string, to: string): PeriodReport { const add = (a: number | null, b: number | null) => (a === null || b === null ? null : a + b); const points = periodPoints(from, to).map((date, i) => ({ date, valueMinor: reports.reduce((a, r) => add(a, r.points[i]?.valueMinor ?? null), 0), costMinor: reports.reduce((a, r) => a + (r.points[i]?.costMinor ?? 0), 0), })); const sum = (k: "investedMinor" | "proceedsMinor" | "giftInMinor" | "giftOutMinor") => reports.reduce((a, r) => a + r[k], 0); const startValueMinor = reports.reduce((a, r) => add(a, r.startValueMinor), 0); const endValueMinor = reports.reduce((a, r) => add(a, r.endValueMinor), 0); const profitMinor = reports.reduce((a, r) => add(a, r.profitMinor), 0); const base = (startValueMinor ?? 0) + sum("investedMinor") + sum("giftInMinor"); return { from, to, startValueMinor, endValueMinor, investedMinor: sum("investedMinor"), proceedsMinor: sum("proceedsMinor"), giftInMinor: sum("giftInMinor"), giftOutMinor: sum("giftOutMinor"), profitMinor, profitBp: profitMinor === null || base <= 0 ? null : Math.round((profitMinor * 10_000) / base), points, }; }