import { QUANTITY_SCALE } from "./quantity"; /** * 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}`; }