degiro-report 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Module dependencies.
4
+ */
5
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Module dependencies.
4
+ */
5
+ import { generateReport, formatReport } from "./report.js";
6
+ import { parseCsv } from "./parser.js";
7
+ import { resolve } from "node:path";
8
+ const filePath = process.argv[2];
9
+ if (!filePath) {
10
+ console.error("Usage: degiro-report <path-to-degiro-csv>");
11
+ process.exit(1);
12
+ }
13
+ const transactions = parseCsv(resolve(filePath));
14
+ const reports = generateReport(transactions);
15
+ console.log(formatReport(reports));
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Module dependencies.
3
+ */
4
+ /**
5
+ * Export `Transaction` type.
6
+ */
7
+ export interface Transaction {
8
+ date: string;
9
+ time: string;
10
+ product: string;
11
+ isin: string;
12
+ quantity: number;
13
+ price: number;
14
+ valueEur: number;
15
+ fees: number;
16
+ totalEur: number;
17
+ }
18
+ /**
19
+ * Parse CSV.
20
+ */
21
+ export declare function parseCsv(filePath: string): Transaction[];
package/dist/parser.js ADDED
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Module dependencies.
3
+ */
4
+ import { readFileSync } from "node:fs";
5
+ /**
6
+ * Parse a European-format number string like "1.234,56" into a JS number.
7
+ */
8
+ function parseEuNumber(raw) {
9
+ if (!raw || raw.trim() === "")
10
+ return 0;
11
+ const replacedSeparators = raw.replace(/\./g, '').replace(",", ".");
12
+ return parseFloat(replacedSeparators);
13
+ }
14
+ /**
15
+ * Parse a DEGIRO Transactions CSV that uses commas as field delimiters
16
+ * and European number format (commas as decimal separators inside quoted fields).
17
+ *
18
+ * Fields containing commas are wrapped in double-quotes by DEGIRO.
19
+ */
20
+ function splitCsvLine(line) {
21
+ const fields = [];
22
+ let parsedField = "";
23
+ let inQuotes = false;
24
+ for (let i = 0; i < line.length; i++) {
25
+ const character = line[i];
26
+ if (character === '"') {
27
+ inQuotes = !inQuotes;
28
+ }
29
+ else if (character === "," && !inQuotes) {
30
+ fields.push(parsedField);
31
+ parsedField = "";
32
+ }
33
+ else {
34
+ parsedField += character;
35
+ }
36
+ }
37
+ fields.push(parsedField);
38
+ return fields;
39
+ }
40
+ /**
41
+ * Parse CSV.
42
+ */
43
+ export function parseCsv(filePath) {
44
+ const content = readFileSync(filePath, "utf-8");
45
+ const lines = content.split("\n").filter((l) => l.trim().length > 0);
46
+ // Skip header
47
+ const dataLines = lines.slice(1);
48
+ return dataLines.map((line) => {
49
+ const cols = splitCsvLine(line);
50
+ // Columns:
51
+ // 0 Date
52
+ // 1 Time
53
+ // 2 Product
54
+ // 3 ISIN
55
+ // 4 Reference exchange
56
+ // 5 Venue
57
+ // 6 Quantity
58
+ // 7 Price
59
+ // 8 (price currency)
60
+ // 9 Local value
61
+ // 10 (local value currency)
62
+ // 11 Value EUR
63
+ // 12 Exchange rate
64
+ // 13 AutoFX Fee
65
+ // 14 Transaction and/or third party fees EUR
66
+ // 15 Total EUR
67
+ // 16 Order ID (sometimes empty before the UUID)
68
+ return {
69
+ date: cols[0],
70
+ time: cols[1],
71
+ product: cols[2],
72
+ isin: cols[3],
73
+ quantity: parseEuNumber(cols[6]),
74
+ price: parseEuNumber(cols[7]),
75
+ valueEur: parseEuNumber(cols[11]),
76
+ fees: parseEuNumber(cols[14]),
77
+ totalEur: parseEuNumber(cols[15]),
78
+ };
79
+ });
80
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Module dependencies.
3
+ */
4
+ import type { Transaction } from "./parser.js";
5
+ /**
6
+ * Export `AssetReport` type.
7
+ */
8
+ export interface AssetReport {
9
+ product: string;
10
+ isin: string;
11
+ buyQuantity: number;
12
+ buyTotalEur: number;
13
+ sellQuantity: number;
14
+ sellTotalEur: number;
15
+ totalFees: number;
16
+ breakEvenPrice: number;
17
+ avgSellPrice: number;
18
+ remainingQuantity: number;
19
+ realizedPnl: number;
20
+ }
21
+ /**
22
+ * Generate report from transactions list.
23
+ */
24
+ export declare function generateReport(transactions: Transaction[]): AssetReport[];
25
+ export declare function formatReport(reports: AssetReport[]): string;
package/dist/report.js ADDED
@@ -0,0 +1,133 @@
1
+ /**
2
+ * Module dependencies.
3
+ */
4
+ import chalk from "chalk";
5
+ /**
6
+ * Generate report from transactions list.
7
+ */
8
+ export function generateReport(transactions) {
9
+ const transactionsByAsset = new Map();
10
+ for (const tx of transactions) {
11
+ const key = tx.isin;
12
+ if (!transactionsByAsset.has(key)) {
13
+ transactionsByAsset.set(key, []);
14
+ }
15
+ transactionsByAsset.get(key).push(tx);
16
+ }
17
+ const reports = [];
18
+ for (const [isin, txs] of transactionsByAsset) {
19
+ // Sort chronologically (oldest first) for FIFO cost basis
20
+ txs.sort((a, b) => {
21
+ const dateA = parseDate(a.date, a.time);
22
+ const dateB = parseDate(b.date, b.time);
23
+ return dateA.getTime() - dateB.getTime();
24
+ });
25
+ const product = txs[0].product;
26
+ let buyQuantity = 0;
27
+ let buyTotalEur = 0; // total cost (positive number)
28
+ let sellQuantity = 0;
29
+ let sellTotalEur = 0; // total proceeds (positive number)
30
+ let totalFees = 0;
31
+ // FIFO lot queue: each entry is { qty, pricePerUnit }
32
+ const lots = [];
33
+ let realizedPnl = 0;
34
+ for (const tx of txs) {
35
+ totalFees += Math.abs(tx.fees);
36
+ if (tx.quantity > 0) {
37
+ // Buy
38
+ const cost = Math.abs(tx.valueEur);
39
+ buyQuantity += tx.quantity;
40
+ buyTotalEur += cost;
41
+ lots.push({ qty: tx.quantity, pricePerUnit: tx.price });
42
+ }
43
+ else {
44
+ // Sell
45
+ const qty = Math.abs(tx.quantity);
46
+ const proceeds = Math.abs(tx.valueEur);
47
+ sellQuantity += qty;
48
+ sellTotalEur += proceeds;
49
+ const sellPrice = tx.price;
50
+ // Match against FIFO lots
51
+ let remaining = qty;
52
+ while (remaining > 0 && lots.length > 0) {
53
+ const lot = lots[0];
54
+ const matched = Math.min(remaining, lot.qty);
55
+ realizedPnl += matched * (sellPrice - lot.pricePerUnit);
56
+ lot.qty -= matched;
57
+ remaining -= matched;
58
+ if (lot.qty <= 0)
59
+ lots.shift();
60
+ }
61
+ }
62
+ }
63
+ const remainingQuantity = buyQuantity - sellQuantity;
64
+ const breakEvenPrice = buyQuantity > 0 ? buyTotalEur / buyQuantity : 0;
65
+ const avgSellPrice = sellQuantity > 0 ? sellTotalEur / sellQuantity : 0;
66
+ reports.push({
67
+ product,
68
+ isin,
69
+ buyQuantity,
70
+ buyTotalEur,
71
+ sellQuantity,
72
+ sellTotalEur,
73
+ totalFees,
74
+ breakEvenPrice,
75
+ avgSellPrice,
76
+ remainingQuantity,
77
+ realizedPnl,
78
+ });
79
+ }
80
+ // Sort by product name
81
+ reports.sort((a, b) => a.product.localeCompare(b.product));
82
+ return reports;
83
+ }
84
+ /**
85
+ * Parse date.
86
+ */
87
+ function parseDate(date, time) {
88
+ // date = "DD-MM-YYYY", time = "HH:MM"
89
+ const [d, m, y] = date.split("-");
90
+ return new Date(`${y}-${m}-${d}T${time}:00`);
91
+ }
92
+ export function formatReport(reports) {
93
+ const lines = [];
94
+ const eur = (n) => n.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
95
+ const printPnL = (n, text) => (n >= 0 ? chalk.green(text) : chalk.red(text));
96
+ lines.push("=".repeat(80));
97
+ lines.push(" DEGIRO PORTFOLIO REPORT");
98
+ lines.push("=".repeat(80));
99
+ lines.push("");
100
+ let totalRealizedPnl = 0;
101
+ let totalFees = 0;
102
+ let totalBought = 0;
103
+ let totalSold = 0;
104
+ for (const r of reports) {
105
+ totalRealizedPnl += r.realizedPnl;
106
+ totalFees += r.totalFees;
107
+ totalBought += r.buyTotalEur;
108
+ totalSold += r.sellTotalEur;
109
+ const netPnl = r.realizedPnl - r.totalFees;
110
+ lines.push(` ${r.product}`);
111
+ lines.push(` ISIN: ${r.isin}`);
112
+ lines.push("-".repeat(80));
113
+ lines.push(` Bought: ${r.buyQuantity} units @ avg ${eur(r.breakEvenPrice)} EUR = ${eur(r.buyTotalEur)} EUR`);
114
+ lines.push(` Sold: ${r.sellQuantity} units @ avg ${eur(r.avgSellPrice)} EUR = ${eur(r.sellTotalEur)} EUR`);
115
+ lines.push(` Remaining: ${r.remainingQuantity} units`);
116
+ lines.push(` Break-even price: ${eur(r.breakEvenPrice)} EUR`);
117
+ lines.push(` Total fees: ${chalk.yellow(`${eur(r.totalFees)} EUR`)}`);
118
+ lines.push(` Realized P&L: ${printPnL(r.realizedPnl, `${r.realizedPnl >= 0 ? "+" : ""}${eur(r.realizedPnl)} EUR`)}`);
119
+ lines.push(` Net P&L (w/fees): ${printPnL(netPnl, `${netPnl >= 0 ? "+" : ""}${eur(netPnl)} EUR`)}`);
120
+ lines.push("");
121
+ }
122
+ const totalNetPnl = totalRealizedPnl - totalFees;
123
+ lines.push("=".repeat(80));
124
+ lines.push(" SUMMARY");
125
+ lines.push("=".repeat(80));
126
+ lines.push(` Total bought: ${eur(totalBought)} EUR`);
127
+ lines.push(` Total sold: ${eur(totalSold)} EUR`);
128
+ lines.push(` Total fees: ${chalk.yellow(`${eur(totalFees)} EUR`)}`);
129
+ lines.push(` Total realized P&L: ${printPnL(totalRealizedPnl, `${totalRealizedPnl >= 0 ? "+" : ""}${eur(totalRealizedPnl)} EUR`)}`);
130
+ lines.push(` Net P&L (w/fees): ${printPnL(totalNetPnl, `${totalNetPnl >= 0 ? "+" : ""}${eur(totalNetPnl)} EUR`)}`);
131
+ lines.push("");
132
+ return lines.join("\n");
133
+ }
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "degiro-report",
3
+ "version": "1.0.0",
4
+ "description": "Parse Degiro transaction CSVs and generate portfolio reports",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "bin": {
8
+ "degiro-report": "dist/index.js"
9
+ },
10
+ "files": [
11
+ "dist"
12
+ ],
13
+ "scripts": {
14
+ "build": "tsc",
15
+ "prepublishOnly": "npm run build"
16
+ },
17
+ "keywords": [
18
+ "degiro",
19
+ "portfolio",
20
+ "transactions",
21
+ "report",
22
+ "csv"
23
+ ],
24
+ "license": "MIT",
25
+ "devDependencies": {
26
+ "@types/node": "^22.0.0",
27
+ "typescript": "^5.7.0"
28
+ },
29
+ "dependencies": {
30
+ "chalk": "^5.6.2"
31
+ }
32
+ }