9a60085d14
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
146 lines
5.4 KiB
TypeScript
146 lines
5.4 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
||
import {
|
||
formatBp,
|
||
holdingSummary,
|
||
periodPoints,
|
||
periodReport,
|
||
runMovements,
|
||
sumReports,
|
||
type DatedMovement,
|
||
type MovementInput,
|
||
} from "./portfolio";
|
||
|
||
const buy = (quantity: number, amountMinor: number): MovementInput => ({
|
||
kind: "buy",
|
||
quantity,
|
||
amountMinor,
|
||
});
|
||
|
||
describe("ağırlıklı ortalama maliyet", () => {
|
||
it("örnek senaryo: 10 gr 25.000 TL'ye, her ay ekleme", () => {
|
||
// 10 gr × 2.500, 2 gr × 2.600, 2 gr × 2.700 → 14 gr, 35.600 TL; ort. 2.542,857…
|
||
const run = runMovements([buy(10_000, 25_000_00), buy(2_000, 5_200_00), buy(2_000, 5_400_00)]);
|
||
expect(run.quantity).toBe(14_000);
|
||
expect(run.costMinor).toBe(35_600_00);
|
||
expect(run.investedMinor).toBe(35_600_00);
|
||
const s = holdingSummary(run, 3_000_00);
|
||
expect(s.avgCostMinor).toBe(2_542_86);
|
||
expect(s.currentValueMinor).toBe(42_000_00);
|
||
expect(s.unrealizedMinor).toBe(6_400_00);
|
||
// 6.400 / 35.600 = %17,98
|
||
expect(s.unrealizedBp).toBe(1798);
|
||
});
|
||
|
||
it("satış: ortalama maliyetle düşer, gerçekleşmiş kâr/zarar", () => {
|
||
const run = runMovements([
|
||
buy(10_000, 30_000_00),
|
||
buy(10_000, 40_000_00),
|
||
{ kind: "sell", quantity: 5_000, amountMinor: 20_000_00 },
|
||
]);
|
||
// ort. 3.500 → 5 gr maliyet 17.500; satış 20.000 → +2.500
|
||
expect(run.steps[2]).toMatchObject({ costOutMinor: 17_500_00, realizedMinor: 2_500_00 });
|
||
expect(run.quantity).toBe(15_000);
|
||
expect(run.costMinor).toBe(52_500_00);
|
||
expect(run.proceedsMinor).toBe(20_000_00);
|
||
// Kalan ortalama değişmez.
|
||
expect(holdingSummary(run, null).avgCostMinor).toBe(3_500_00);
|
||
});
|
||
|
||
it("hediye çıkışı K/Z yazmaz; gelen hediye yatırım sayılmaz; tamamı çıkınca maliyet sıfır", () => {
|
||
const run = runMovements([
|
||
buy(3_000, 9_000_00),
|
||
{ kind: "gift_in", quantity: 1_000, amountMinor: 3_300_00 },
|
||
{ kind: "gift_out", quantity: 2_000, amountMinor: 0 },
|
||
{ kind: "sell", quantity: 2_000, amountMinor: 7_000_00 },
|
||
]);
|
||
expect(run.investedMinor).toBe(9_000_00);
|
||
expect(run.giftInMinor).toBe(3_300_00);
|
||
// ort. 3.075 → çıkış 6.150, kalan 6.150; satış tümü → K/Z 850
|
||
expect(run.giftOutCostMinor).toBe(6_150_00);
|
||
expect(run.realizedMinor).toBe(850_00);
|
||
expect(run.quantity).toBe(0);
|
||
expect(run.costMinor).toBe(0);
|
||
const s = holdingSummary(run, 3_500_00);
|
||
expect(s).toMatchObject({ avgCostMinor: null, currentValueMinor: 0, unrealizedBp: null });
|
||
});
|
||
|
||
it("eldekinden fazla çıkış işaretlenir", () => {
|
||
const run = runMovements([
|
||
buy(1_000, 100_00),
|
||
{ kind: "sell", quantity: 2_000, amountMinor: 1 },
|
||
]);
|
||
expect(run.negativeAt).toBe(1);
|
||
expect(runMovements([buy(1_000, 100_00)]).negativeAt).toBeNull();
|
||
});
|
||
|
||
it("oran biçimi", () => {
|
||
expect(formatBp(1798)).toBe("+%17,98");
|
||
expect(formatBp(-350)).toBe("−%3,5");
|
||
expect(formatBp(0)).toBe("%0");
|
||
});
|
||
});
|
||
|
||
describe("dönemsel rapor", () => {
|
||
it("noktalar: başlangıç, ay sonları, bitiş", () => {
|
||
expect(periodPoints("2026-05-06", "2026-08-15")).toEqual([
|
||
"2026-05-06",
|
||
"2026-05-31",
|
||
"2026-06-30",
|
||
"2026-07-31",
|
||
"2026-08-15",
|
||
]);
|
||
expect(periodPoints("2026-05-31", "2026-06-30")).toEqual(["2026-05-31", "2026-06-30"]);
|
||
});
|
||
|
||
// Örnek: 10 gr'ı 25.000'e aldım, her ay 1 gr ekledim; 6 ay sonra ne kazandım?
|
||
const prices: Record<string, number> = {
|
||
"2026-01-01": 2_500_00,
|
||
"2026-02-01": 2_600_00,
|
||
"2026-03-01": 2_700_00,
|
||
"2026-07-01": 3_000_00,
|
||
};
|
||
const priceAt = (date: string) => {
|
||
const keys = Object.keys(prices)
|
||
.filter((k) => k <= date)
|
||
.sort();
|
||
return keys.length ? prices[keys.at(-1)!]! : null;
|
||
};
|
||
const ms: DatedMovement[] = [
|
||
{ kind: "buy", date: "2026-01-01", quantity: 10_000, amountMinor: 25_000_00 },
|
||
{ kind: "buy", date: "2026-02-01", quantity: 1_000, amountMinor: 2_600_00 },
|
||
{ kind: "buy", date: "2026-03-01", quantity: 1_000, amountMinor: 2_700_00 },
|
||
{ kind: "gift_out", date: "2026-07-01", quantity: 1_000, amountMinor: 0 },
|
||
];
|
||
|
||
it("örnek senaryo: yatırılan, dönem sonu değer, kâr", () => {
|
||
const r = periodReport(ms, priceAt, "2026-01-01", "2026-07-02");
|
||
expect(r.startValueMinor).toBe(0);
|
||
expect(r.investedMinor).toBe(30_300_00);
|
||
// 11 gr × 3.000 = 33.000; çıkan 1 gr o günkü değeri 3.000
|
||
expect(r.endValueMinor).toBe(33_000_00);
|
||
expect(r.giftOutMinor).toBe(3_000_00);
|
||
expect(r.profitMinor).toBe(5_700_00);
|
||
expect(r.profitBp).toBe(1881);
|
||
expect(r.points.map((p) => p.valueMinor)).toEqual([
|
||
0, 25_000_00, 28_600_00, 32_400_00, 32_400_00, 32_400_00, 32_400_00, 33_000_00,
|
||
]);
|
||
});
|
||
|
||
it("başlangıçta eldeki değer dönem içi kârdan düşülür; fiyat yoksa null", () => {
|
||
const r = periodReport(ms, priceAt, "2026-03-15", "2026-07-02");
|
||
expect(r.startValueMinor).toBe(32_400_00);
|
||
expect(r.investedMinor).toBe(0);
|
||
expect(r.profitMinor).toBe(33_000_00 + 3_000_00 - 32_400_00);
|
||
expect(periodReport(ms, () => null, "2026-01-01", "2026-07-02").profitMinor).toBeNull();
|
||
});
|
||
|
||
it("toplam", () => {
|
||
const a = periodReport(ms, priceAt, "2026-01-01", "2026-07-02");
|
||
const t = sumReports([a, a], "2026-01-01", "2026-07-02");
|
||
expect(t.investedMinor).toBe(60_600_00);
|
||
expect(t.profitMinor).toBe(11_400_00);
|
||
expect(t.points.at(-1)!.valueMinor).toBe(66_000_00);
|
||
expect(sumReports([], "2026-01-01", "2026-01-31").points).toHaveLength(2);
|
||
});
|
||
});
|