Files
butcem/src/shared/portfolio.ts
T

147 lines
4.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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}`;
}