feat(assets): dönemsel rapor, net varlık ve aynî kayıt bağlantısı
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
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
|
||||
@@ -144,3 +145,141 @@ 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<number | null>((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<number | null>((a, r) => add(a, r.startValueMinor), 0);
|
||||
const endValueMinor = reports.reduce<number | null>((a, r) => add(a, r.endValueMinor), 0);
|
||||
const profitMinor = reports.reduce<number | null>((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,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user