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:
2026-11-06 00:19:05 +03:00
parent 20e5a90df4
commit 9a60085d14
9 changed files with 806 additions and 19 deletions
+139
View File
@@ -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,
};
}