890ba9f521
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
147 lines
4.9 KiB
TypeScript
147 lines
4.9 KiB
TypeScript
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}`;
|
||
}
|