feat(assets): varlık lotları, ortalama maliyet ve kâr/zarar servisi
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,146 @@
|
||||
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}`;
|
||||
}
|
||||
Reference in New Issue
Block a user