@gabrielerandelli/minus-tracker 0.6.0 → 0.8.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.
- package/README.md +150 -30
- package/dist/cli/index.js +751 -240
- package/dist/cli/index.js.map +1 -1
- package/dist/index.cjs +474 -90
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +79 -2
- package/dist/index.d.ts +79 -2
- package/dist/index.js +474 -90
- package/dist/index.js.map +1 -1
- package/dist/mcp/index.js +1885 -0
- package/dist/mcp/index.js.map +1 -0
- package/package.json +18 -5
package/dist/index.cjs
CHANGED
|
@@ -152,6 +152,10 @@ function warningToEnglish(w) {
|
|
|
152
152
|
return `Row ${w.row}: no ECB rate for ${w.currency} on ${w.date} \u2014 skipped`;
|
|
153
153
|
case "QUANTITY_ZERO":
|
|
154
154
|
return `Row ${w.row}: quantity is 0 \u2014 skipped`;
|
|
155
|
+
case "MISSING_ISIN_INCOME":
|
|
156
|
+
return `Row ${w.row}: blank ISIN on income row \u2014 skipped`;
|
|
157
|
+
case "ORPHAN_WITHHOLDING":
|
|
158
|
+
return `Withholding row for ${w.isin} on ${w.date}: no matching income row \u2014 skipped`;
|
|
155
159
|
}
|
|
156
160
|
}
|
|
157
161
|
|
|
@@ -167,6 +171,8 @@ var REQUIRED_COLUMNS = [
|
|
|
167
171
|
"Transaction costs currency",
|
|
168
172
|
"Product"
|
|
169
173
|
];
|
|
174
|
+
var INCOME_KEYWORDS = ["DIVIDEND", "COUPON", "INTEREST", "CEDOLA"];
|
|
175
|
+
var TAX_KEYWORDS = ["DIVIDEND TAX", "WITHHOLDING", "RITENUTA"];
|
|
170
176
|
function parseCSVRow(line) {
|
|
171
177
|
const fields = [];
|
|
172
178
|
let i = 0;
|
|
@@ -208,6 +214,7 @@ function parseDate(ddmmyyyy) {
|
|
|
208
214
|
var DEGIROParser = class {
|
|
209
215
|
_warningEntries = [];
|
|
210
216
|
_snapshot;
|
|
217
|
+
_incomeRows = [];
|
|
211
218
|
constructor(snapshot) {
|
|
212
219
|
this._snapshot = snapshot;
|
|
213
220
|
}
|
|
@@ -224,6 +231,7 @@ var DEGIROParser = class {
|
|
|
224
231
|
*/
|
|
225
232
|
parse(csv) {
|
|
226
233
|
this._warningEntries = [];
|
|
234
|
+
this._incomeRows = [];
|
|
227
235
|
if (typeof csv !== "string" || csv.includes("\0")) {
|
|
228
236
|
throw new ParseError("INVALID_CSV");
|
|
229
237
|
}
|
|
@@ -248,17 +256,70 @@ var DEGIROParser = class {
|
|
|
248
256
|
}
|
|
249
257
|
const snapshot = this._snapshot ?? getActiveSnapshot();
|
|
250
258
|
const transactions = [];
|
|
259
|
+
const incomeCandidates = [];
|
|
260
|
+
const withholdingCandidates = [];
|
|
251
261
|
for (let r = 1; r < rows.length; r++) {
|
|
252
262
|
const row = rows[r];
|
|
253
263
|
const rowIndex = r + 1;
|
|
254
264
|
if (row.every((cell) => cell.trim() === "")) continue;
|
|
255
265
|
const get = (col) => (row[colIndex[col]] ?? "").trim();
|
|
256
266
|
const isin = get("ISIN");
|
|
267
|
+
const product = get("Product");
|
|
268
|
+
const qtyStr = get("Quantity");
|
|
269
|
+
const rawQty = parseFloat(qtyStr);
|
|
270
|
+
const qtyIsZeroOrBlank = qtyStr === "" || isNaN(rawQty) || rawQty === 0;
|
|
271
|
+
if (qtyIsZeroOrBlank) {
|
|
272
|
+
const upperProduct = product.toUpperCase();
|
|
273
|
+
const isTaxKeyword = TAX_KEYWORDS.some((k) => upperProduct.includes(k));
|
|
274
|
+
const isIncomeKeyword = !isTaxKeyword && INCOME_KEYWORDS.some((k) => upperProduct.includes(k));
|
|
275
|
+
if (isTaxKeyword || isIncomeKeyword) {
|
|
276
|
+
const rawLocalValue = parseFloat(get("Local value"));
|
|
277
|
+
const localValue = isNaN(rawLocalValue) ? 0 : rawLocalValue;
|
|
278
|
+
const isoDate2 = parseDate(get("Date"));
|
|
279
|
+
if (isTaxKeyword && localValue < 0) {
|
|
280
|
+
if (!isin) {
|
|
281
|
+
this._warningEntries.push({
|
|
282
|
+
code: "MISSING_ISIN_INCOME",
|
|
283
|
+
row: rowIndex
|
|
284
|
+
});
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
withholdingCandidates.push({
|
|
288
|
+
isin,
|
|
289
|
+
date: isoDate2,
|
|
290
|
+
amount: Math.abs(localValue),
|
|
291
|
+
row: rowIndex
|
|
292
|
+
});
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
if (isIncomeKeyword && localValue > 0) {
|
|
296
|
+
if (!isin) {
|
|
297
|
+
this._warningEntries.push({
|
|
298
|
+
code: "MISSING_ISIN_INCOME",
|
|
299
|
+
row: rowIndex
|
|
300
|
+
});
|
|
301
|
+
continue;
|
|
302
|
+
}
|
|
303
|
+
const incomeType = upperProduct.includes(
|
|
304
|
+
"DIVIDEND"
|
|
305
|
+
) ? "dividend" : "coupon";
|
|
306
|
+
incomeCandidates.push({
|
|
307
|
+
isin,
|
|
308
|
+
product,
|
|
309
|
+
date: isoDate2,
|
|
310
|
+
incomeType,
|
|
311
|
+
localValue,
|
|
312
|
+
currency: get("Local value currency"),
|
|
313
|
+
row: rowIndex
|
|
314
|
+
});
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
257
319
|
if (!isin) {
|
|
258
320
|
this._warningEntries.push({ code: "MISSING_ISIN", row: rowIndex });
|
|
259
321
|
continue;
|
|
260
322
|
}
|
|
261
|
-
const rawQty = parseFloat(get("Quantity"));
|
|
262
323
|
if (isNaN(rawQty) || rawQty === 0) {
|
|
263
324
|
this._warningEntries.push({ code: "QUANTITY_ZERO", row: rowIndex });
|
|
264
325
|
continue;
|
|
@@ -299,7 +360,6 @@ var DEGIROParser = class {
|
|
|
299
360
|
const rawFees = parseFloat(get("Transaction costs"));
|
|
300
361
|
const feesEUR = Math.abs(isNaN(rawFees) ? 0 : rawFees);
|
|
301
362
|
const pricePerUnit = parseFloat(get("Price"));
|
|
302
|
-
const product = get("Product");
|
|
303
363
|
transactions.push({
|
|
304
364
|
isin,
|
|
305
365
|
product,
|
|
@@ -314,6 +374,73 @@ var DEGIROParser = class {
|
|
|
314
374
|
fxRate
|
|
315
375
|
});
|
|
316
376
|
}
|
|
377
|
+
const withholdingByKey = /* @__PURE__ */ new Map();
|
|
378
|
+
for (const w of withholdingCandidates) {
|
|
379
|
+
const key = `${w.isin}|${w.date}`;
|
|
380
|
+
const existing = withholdingByKey.get(key);
|
|
381
|
+
withholdingByKey.set(key, {
|
|
382
|
+
isin: w.isin,
|
|
383
|
+
date: w.date,
|
|
384
|
+
amount: (existing?.amount ?? 0) + w.amount
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
const consumedKeys = /* @__PURE__ */ new Set();
|
|
388
|
+
const incomeRows = [];
|
|
389
|
+
for (const c of incomeCandidates) {
|
|
390
|
+
const key = `${c.isin}|${c.date}`;
|
|
391
|
+
const matchedWithholding = withholdingByKey.get(key);
|
|
392
|
+
let grossAmount;
|
|
393
|
+
let withholdingTax;
|
|
394
|
+
let fxRate;
|
|
395
|
+
if (c.currency === "EUR") {
|
|
396
|
+
grossAmount = Math.abs(c.localValue);
|
|
397
|
+
withholdingTax = matchedWithholding?.amount ?? 0;
|
|
398
|
+
fxRate = void 0;
|
|
399
|
+
} else {
|
|
400
|
+
const rate = lookupRate(c.currency, c.date, snapshot);
|
|
401
|
+
if (rate === null) {
|
|
402
|
+
if (snapshot[c.currency] === void 0) {
|
|
403
|
+
this._warningEntries.push({
|
|
404
|
+
code: "UNSUPPORTED_CURRENCY",
|
|
405
|
+
row: c.row,
|
|
406
|
+
currency: c.currency
|
|
407
|
+
});
|
|
408
|
+
} else {
|
|
409
|
+
this._warningEntries.push({
|
|
410
|
+
code: "NO_ECB_RATE",
|
|
411
|
+
row: c.row,
|
|
412
|
+
currency: c.currency,
|
|
413
|
+
date: c.date
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
continue;
|
|
417
|
+
}
|
|
418
|
+
grossAmount = Math.abs(c.localValue) / rate;
|
|
419
|
+
withholdingTax = matchedWithholding ? matchedWithholding.amount / rate : 0;
|
|
420
|
+
fxRate = rate;
|
|
421
|
+
}
|
|
422
|
+
if (matchedWithholding) consumedKeys.add(key);
|
|
423
|
+
incomeRows.push({
|
|
424
|
+
isin: c.isin,
|
|
425
|
+
product: c.product,
|
|
426
|
+
date: c.date,
|
|
427
|
+
incomeType: c.incomeType,
|
|
428
|
+
grossAmount,
|
|
429
|
+
withholdingTax,
|
|
430
|
+
currency: c.currency,
|
|
431
|
+
fxRate
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
for (const [key, w] of withholdingByKey) {
|
|
435
|
+
if (!consumedKeys.has(key)) {
|
|
436
|
+
this._warningEntries.push({
|
|
437
|
+
code: "ORPHAN_WITHHOLDING",
|
|
438
|
+
isin: w.isin,
|
|
439
|
+
date: w.date
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
this._incomeRows = incomeRows;
|
|
317
444
|
return transactions;
|
|
318
445
|
}
|
|
319
446
|
get warnings() {
|
|
@@ -322,12 +449,136 @@ var DEGIROParser = class {
|
|
|
322
449
|
get warningEntries() {
|
|
323
450
|
return this._warningEntries;
|
|
324
451
|
}
|
|
452
|
+
get incomeRows() {
|
|
453
|
+
return this._incomeRows;
|
|
454
|
+
}
|
|
325
455
|
};
|
|
326
456
|
|
|
327
|
-
// src/
|
|
457
|
+
// src/dichiarazione/engine.ts
|
|
458
|
+
var import_promises = require("fs/promises");
|
|
328
459
|
function roundHalfUp(x) {
|
|
329
460
|
return Math.sign(x) * Math.round(Math.abs(x) * 100) / 100;
|
|
330
461
|
}
|
|
462
|
+
function buildQuadroRT(bucketB, carryForward, taxYear) {
|
|
463
|
+
const plusvalenze = bucketB.plusvalenze;
|
|
464
|
+
const minusvalenze = bucketB.minusvalenze;
|
|
465
|
+
const differenza = roundHalfUp(plusvalenze - minusvalenze);
|
|
466
|
+
if (differenza > 0) {
|
|
467
|
+
const sorted = [...carryForward].sort((a, b) => a.year - b.year);
|
|
468
|
+
let remaining = differenza;
|
|
469
|
+
const carryForwardApplied = [];
|
|
470
|
+
for (const entry of sorted) {
|
|
471
|
+
if (taxYear - entry.year > 4) continue;
|
|
472
|
+
const consumed = Math.min(entry.amount, remaining);
|
|
473
|
+
if (consumed > 0) {
|
|
474
|
+
carryForwardApplied.push({
|
|
475
|
+
annoOrigine: entry.year,
|
|
476
|
+
importo: roundHalfUp(consumed)
|
|
477
|
+
});
|
|
478
|
+
remaining -= consumed;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
const imponibileNetto = roundHalfUp(remaining);
|
|
482
|
+
const imposta = roundHalfUp(imponibileNetto * 0.26);
|
|
483
|
+
return {
|
|
484
|
+
plusvalenze,
|
|
485
|
+
minusvalenze,
|
|
486
|
+
differenza,
|
|
487
|
+
carryForwardApplied,
|
|
488
|
+
imponibileNetto,
|
|
489
|
+
imposta,
|
|
490
|
+
carryForwardRiportato: []
|
|
491
|
+
};
|
|
492
|
+
} else if (differenza < 0) {
|
|
493
|
+
return {
|
|
494
|
+
plusvalenze,
|
|
495
|
+
minusvalenze,
|
|
496
|
+
differenza,
|
|
497
|
+
carryForwardApplied: [],
|
|
498
|
+
imponibileNetto: 0,
|
|
499
|
+
imposta: 0,
|
|
500
|
+
carryForwardRiportato: [
|
|
501
|
+
{ annoOrigine: taxYear, importo: roundHalfUp(Math.abs(differenza)) }
|
|
502
|
+
]
|
|
503
|
+
};
|
|
504
|
+
} else {
|
|
505
|
+
return {
|
|
506
|
+
plusvalenze,
|
|
507
|
+
minusvalenze,
|
|
508
|
+
differenza: 0,
|
|
509
|
+
carryForwardApplied: [],
|
|
510
|
+
imponibileNetto: 0,
|
|
511
|
+
imposta: 0,
|
|
512
|
+
carryForwardRiportato: []
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
function buildQuadroRM(bucketA, incomeRows, taxYear) {
|
|
517
|
+
const groups = bucketA?.groups ?? [];
|
|
518
|
+
const group26 = groups.find((g) => g.taxRate === 0.26);
|
|
519
|
+
const group125 = groups.find((g) => g.taxRate === 0.125);
|
|
520
|
+
const capitaleAliquota26 = {
|
|
521
|
+
plusvalenze: group26?.plusvalenze ?? 0,
|
|
522
|
+
imposta: group26?.imposta ?? 0
|
|
523
|
+
};
|
|
524
|
+
const capitaleAliquota125 = {
|
|
525
|
+
plusvalenze: group125?.plusvalenze ?? 0,
|
|
526
|
+
imposta: group125?.imposta ?? 0
|
|
527
|
+
};
|
|
528
|
+
const dividendiEsteri = incomeRows.filter((r) => r.incomeType === "dividend").map((r) => ({
|
|
529
|
+
isin: r.isin,
|
|
530
|
+
prodotto: r.product,
|
|
531
|
+
lordo: r.grossAmount,
|
|
532
|
+
rittenutaEstera: r.withholdingTax
|
|
533
|
+
}));
|
|
534
|
+
const cedole = incomeRows.filter((r) => r.incomeType === "coupon").map((r) => ({
|
|
535
|
+
isin: r.isin,
|
|
536
|
+
prodotto: r.product,
|
|
537
|
+
importo: r.grossAmount,
|
|
538
|
+
rittenutaEstera: r.withholdingTax
|
|
539
|
+
}));
|
|
540
|
+
return {
|
|
541
|
+
capitaleAliquota26,
|
|
542
|
+
capitaleAliquota125,
|
|
543
|
+
dividendiEsteri,
|
|
544
|
+
cedole
|
|
545
|
+
};
|
|
546
|
+
}
|
|
547
|
+
function buildDichiarazioneReport(quadroRT, quadroRM, taxYear) {
|
|
548
|
+
const version = 1;
|
|
549
|
+
const modello = "Redditi PF";
|
|
550
|
+
const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
551
|
+
return {
|
|
552
|
+
version,
|
|
553
|
+
annoImposta: taxYear,
|
|
554
|
+
modello,
|
|
555
|
+
generatedAt,
|
|
556
|
+
quadroRT,
|
|
557
|
+
quadroRM,
|
|
558
|
+
exportTo: async (path2) => {
|
|
559
|
+
await (0, import_promises.writeFile)(
|
|
560
|
+
path2,
|
|
561
|
+
JSON.stringify(
|
|
562
|
+
{
|
|
563
|
+
version,
|
|
564
|
+
annoImposta: taxYear,
|
|
565
|
+
modello,
|
|
566
|
+
generatedAt,
|
|
567
|
+
quadroRT,
|
|
568
|
+
quadroRM
|
|
569
|
+
},
|
|
570
|
+
null,
|
|
571
|
+
2
|
|
572
|
+
)
|
|
573
|
+
);
|
|
574
|
+
}
|
|
575
|
+
};
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
// src/calculator/index.ts
|
|
579
|
+
function roundHalfUp2(x) {
|
|
580
|
+
return Math.sign(x) * Math.round(Math.abs(x) * 100) / 100;
|
|
581
|
+
}
|
|
331
582
|
function inferTaxYear(transactions) {
|
|
332
583
|
const counts = {};
|
|
333
584
|
for (const t of transactions) {
|
|
@@ -419,9 +670,9 @@ var Calculator = class {
|
|
|
419
670
|
sellDate: tx.date,
|
|
420
671
|
buyPriceEUR: lot.pricePerUnitEUR,
|
|
421
672
|
sellPriceEUR: sellPricePerUnitEUR,
|
|
422
|
-
buyCostEUR:
|
|
423
|
-
sellProceedsEUR:
|
|
424
|
-
gainLossEUR:
|
|
673
|
+
buyCostEUR: roundHalfUp2(buyCostEUR),
|
|
674
|
+
sellProceedsEUR: roundHalfUp2(sellProceedsEUR),
|
|
675
|
+
gainLossEUR: roundHalfUp2(gainLossEUR),
|
|
425
676
|
buyFxRate: lot.fxRate,
|
|
426
677
|
sellFxRate: tx.fxRate
|
|
427
678
|
});
|
|
@@ -476,12 +727,12 @@ var Calculator = class {
|
|
|
476
727
|
const bucketAGroups = [...groupsByRate.entries()].map(([taxRate, g]) => ({
|
|
477
728
|
taxRate,
|
|
478
729
|
assetClasses: [...g.assetClasses],
|
|
479
|
-
plusvalenze:
|
|
480
|
-
imposta:
|
|
730
|
+
plusvalenze: roundHalfUp2(g.plusvalenze),
|
|
731
|
+
imposta: roundHalfUp2(g.plusvalenze * taxRate)
|
|
481
732
|
}));
|
|
482
733
|
const bucketAReport = {
|
|
483
734
|
groups: bucketAGroups,
|
|
484
|
-
totalImposta:
|
|
735
|
+
totalImposta: roundHalfUp2(
|
|
485
736
|
bucketAGroups.reduce((s, g) => s + g.imposta, 0)
|
|
486
737
|
)
|
|
487
738
|
};
|
|
@@ -492,8 +743,8 @@ var Calculator = class {
|
|
|
492
743
|
if (lot.gainLossEUR >= 0) bPlusvalenze += lot.gainLossEUR;
|
|
493
744
|
else bMinusvalenze += Math.abs(lot.gainLossEUR);
|
|
494
745
|
}
|
|
495
|
-
bPlusvalenze =
|
|
496
|
-
bMinusvalenze =
|
|
746
|
+
bPlusvalenze = roundHalfUp2(bPlusvalenze);
|
|
747
|
+
bMinusvalenze = roundHalfUp2(bMinusvalenze);
|
|
497
748
|
const carryForwards = [...this._options.carryForward ?? []].sort(
|
|
498
749
|
(a, b) => a.year - b.year
|
|
499
750
|
);
|
|
@@ -506,11 +757,11 @@ var Calculator = class {
|
|
|
506
757
|
carryForwardApplied += consumed;
|
|
507
758
|
remaining -= consumed;
|
|
508
759
|
}
|
|
509
|
-
carryForwardApplied =
|
|
510
|
-
const bNetResult =
|
|
760
|
+
carryForwardApplied = roundHalfUp2(carryForwardApplied);
|
|
761
|
+
const bNetResult = roundHalfUp2(
|
|
511
762
|
bPlusvalenze - bMinusvalenze - carryForwardApplied
|
|
512
763
|
);
|
|
513
|
-
const carryForwardRemaining =
|
|
764
|
+
const carryForwardRemaining = roundHalfUp2(Math.max(0, -bNetResult));
|
|
514
765
|
const bucketBReport = {
|
|
515
766
|
plusvalenze: bPlusvalenze,
|
|
516
767
|
minusvalenze: bMinusvalenze,
|
|
@@ -518,18 +769,41 @@ var Calculator = class {
|
|
|
518
769
|
carryForwardRemaining,
|
|
519
770
|
netResult: bNetResult
|
|
520
771
|
};
|
|
772
|
+
const allIncomeRows = this._options.incomeRows ?? [];
|
|
773
|
+
const filteredIncomeRows = allIncomeRows.filter(
|
|
774
|
+
(row) => new Date(row.date).getFullYear() === taxYear
|
|
775
|
+
);
|
|
776
|
+
if (filteredIncomeRows.length < allIncomeRows.length) {
|
|
777
|
+
warnings.push(`Income rows outside tax year ${taxYear} were skipped.`);
|
|
778
|
+
}
|
|
779
|
+
const quadroRT = buildQuadroRT(
|
|
780
|
+
bucketBReport,
|
|
781
|
+
this._options.carryForward ?? [],
|
|
782
|
+
taxYear
|
|
783
|
+
);
|
|
784
|
+
const quadroRM = buildQuadroRM(
|
|
785
|
+
bucketAGroups.length > 0 ? bucketAReport : void 0,
|
|
786
|
+
filteredIncomeRows,
|
|
787
|
+
taxYear
|
|
788
|
+
);
|
|
789
|
+
const dichiarazioneReport = buildDichiarazioneReport(
|
|
790
|
+
quadroRT,
|
|
791
|
+
quadroRM,
|
|
792
|
+
taxYear
|
|
793
|
+
);
|
|
521
794
|
return {
|
|
522
795
|
method,
|
|
523
796
|
taxYear,
|
|
524
|
-
plusvalenze:
|
|
525
|
-
minusvalenze:
|
|
526
|
-
netResult:
|
|
797
|
+
plusvalenze: roundHalfUp2(plusvalenze),
|
|
798
|
+
minusvalenze: roundHalfUp2(minusvalenze),
|
|
799
|
+
netResult: roundHalfUp2(plusvalenze - minusvalenze),
|
|
527
800
|
lots: matchedLots,
|
|
528
801
|
ratesUsed,
|
|
529
802
|
warnings,
|
|
530
803
|
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
531
804
|
bucketA: bucketAGroups.length > 0 ? bucketAReport : void 0,
|
|
532
|
-
bucketB: bucketBReport
|
|
805
|
+
bucketB: bucketBReport,
|
|
806
|
+
dichiarazione: dichiarazioneReport
|
|
533
807
|
};
|
|
534
808
|
} else {
|
|
535
809
|
const allIsins = [...new Set(this._transactions.map((t) => t.isin))];
|
|
@@ -544,9 +818,9 @@ var Calculator = class {
|
|
|
544
818
|
return {
|
|
545
819
|
method,
|
|
546
820
|
taxYear,
|
|
547
|
-
plusvalenze:
|
|
548
|
-
minusvalenze:
|
|
549
|
-
netResult:
|
|
821
|
+
plusvalenze: roundHalfUp2(plusvalenze),
|
|
822
|
+
minusvalenze: roundHalfUp2(minusvalenze),
|
|
823
|
+
netResult: roundHalfUp2(plusvalenze - minusvalenze),
|
|
550
824
|
lots: matchedLots,
|
|
551
825
|
ratesUsed,
|
|
552
826
|
warnings,
|
|
@@ -768,7 +1042,7 @@ function classifyByType(isin, securityType, product, warnings) {
|
|
|
768
1042
|
};
|
|
769
1043
|
}
|
|
770
1044
|
warnings.push(
|
|
771
|
-
`Unrecognized type: ${securityType}. Please classify manually.`
|
|
1045
|
+
`Unrecognized type for ${isin}: ${securityType}. Please classify manually.`
|
|
772
1046
|
);
|
|
773
1047
|
return {
|
|
774
1048
|
product,
|
|
@@ -781,6 +1055,84 @@ function classifyByType(isin, securityType, product, warnings) {
|
|
|
781
1055
|
source: "user"
|
|
782
1056
|
};
|
|
783
1057
|
}
|
|
1058
|
+
var ASSET_CLASS_DEFAULTS = {
|
|
1059
|
+
ETF: {
|
|
1060
|
+
assetClass: "ETF",
|
|
1061
|
+
bucketGain: "A",
|
|
1062
|
+
bucketLoss: "B",
|
|
1063
|
+
taxRate: 0.26,
|
|
1064
|
+
whiteListed: null
|
|
1065
|
+
},
|
|
1066
|
+
Stock: {
|
|
1067
|
+
assetClass: "Stock",
|
|
1068
|
+
bucketGain: "B",
|
|
1069
|
+
bucketLoss: "B",
|
|
1070
|
+
taxRate: 0,
|
|
1071
|
+
whiteListed: null
|
|
1072
|
+
},
|
|
1073
|
+
ETC: {
|
|
1074
|
+
assetClass: "ETC",
|
|
1075
|
+
bucketGain: "B",
|
|
1076
|
+
bucketLoss: "B",
|
|
1077
|
+
taxRate: 0,
|
|
1078
|
+
whiteListed: null
|
|
1079
|
+
},
|
|
1080
|
+
GovtBondWL: {
|
|
1081
|
+
assetClass: "GovtBondWL",
|
|
1082
|
+
bucketGain: "A",
|
|
1083
|
+
bucketLoss: "B",
|
|
1084
|
+
taxRate: 0.125,
|
|
1085
|
+
whiteListed: true
|
|
1086
|
+
},
|
|
1087
|
+
GovtBondOther: {
|
|
1088
|
+
assetClass: "GovtBondOther",
|
|
1089
|
+
bucketGain: "A",
|
|
1090
|
+
bucketLoss: "B",
|
|
1091
|
+
taxRate: 0.26,
|
|
1092
|
+
whiteListed: false
|
|
1093
|
+
},
|
|
1094
|
+
CorpBond: {
|
|
1095
|
+
assetClass: "CorpBond",
|
|
1096
|
+
bucketGain: "B",
|
|
1097
|
+
bucketLoss: "B",
|
|
1098
|
+
taxRate: 0,
|
|
1099
|
+
whiteListed: null
|
|
1100
|
+
},
|
|
1101
|
+
Derivative: {
|
|
1102
|
+
assetClass: "Derivative",
|
|
1103
|
+
bucketGain: "B",
|
|
1104
|
+
bucketLoss: "B",
|
|
1105
|
+
taxRate: 0,
|
|
1106
|
+
whiteListed: null
|
|
1107
|
+
},
|
|
1108
|
+
LeverageCert: {
|
|
1109
|
+
assetClass: "LeverageCert",
|
|
1110
|
+
bucketGain: "B",
|
|
1111
|
+
bucketLoss: "B",
|
|
1112
|
+
taxRate: 0,
|
|
1113
|
+
whiteListed: null
|
|
1114
|
+
},
|
|
1115
|
+
CapProtectedCert: {
|
|
1116
|
+
assetClass: "CapProtectedCert",
|
|
1117
|
+
bucketGain: "A",
|
|
1118
|
+
bucketLoss: "B",
|
|
1119
|
+
taxRate: 0.26,
|
|
1120
|
+
whiteListed: null
|
|
1121
|
+
}
|
|
1122
|
+
};
|
|
1123
|
+
function buildEntryFromAssetClass(assetClass, product) {
|
|
1124
|
+
const mapped = ASSET_CLASS_DEFAULTS[assetClass];
|
|
1125
|
+
return {
|
|
1126
|
+
product,
|
|
1127
|
+
assetClass: mapped.assetClass,
|
|
1128
|
+
bucketGain: mapped.bucketGain,
|
|
1129
|
+
bucketLoss: mapped.bucketLoss,
|
|
1130
|
+
taxRate: mapped.taxRate,
|
|
1131
|
+
whiteListed: mapped.whiteListed,
|
|
1132
|
+
confirmedByUser: true,
|
|
1133
|
+
source: "user"
|
|
1134
|
+
};
|
|
1135
|
+
}
|
|
784
1136
|
var Classifier = class {
|
|
785
1137
|
interactive;
|
|
786
1138
|
constructor(options) {
|
|
@@ -802,7 +1154,7 @@ var Classifier = class {
|
|
|
802
1154
|
}
|
|
803
1155
|
return parsed.classifications;
|
|
804
1156
|
}
|
|
805
|
-
async classify(transactions, sidecarPath, _httpPost = httpsPost) {
|
|
1157
|
+
async classify(transactions, sidecarPath, options, _httpPost = httpsPost) {
|
|
806
1158
|
const isinToProduct = /* @__PURE__ */ new Map();
|
|
807
1159
|
for (const tx of transactions) {
|
|
808
1160
|
if (tx.isin && !isinToProduct.has(tx.isin)) {
|
|
@@ -810,13 +1162,21 @@ var Classifier = class {
|
|
|
810
1162
|
}
|
|
811
1163
|
}
|
|
812
1164
|
const confirmed = {};
|
|
813
|
-
if (
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
1165
|
+
if (sidecarPath !== void 0) {
|
|
1166
|
+
if (fs2.existsSync(sidecarPath)) {
|
|
1167
|
+
const existingMap = await this.load(sidecarPath);
|
|
1168
|
+
for (const [isin, entry] of Object.entries(existingMap)) {
|
|
1169
|
+
if (entry.confirmedByUser) {
|
|
1170
|
+
confirmed[isin] = entry;
|
|
1171
|
+
}
|
|
818
1172
|
}
|
|
819
1173
|
}
|
|
1174
|
+
} else if (options?.existingClassification) {
|
|
1175
|
+
Object.assign(confirmed, options.existingClassification);
|
|
1176
|
+
}
|
|
1177
|
+
for (const [isin, assetClass] of Object.entries(options?.overrides ?? {})) {
|
|
1178
|
+
const product = isinToProduct.get(isin) ?? isin;
|
|
1179
|
+
confirmed[isin] = buildEntryFromAssetClass(assetClass, product);
|
|
820
1180
|
}
|
|
821
1181
|
const toProcess = [];
|
|
822
1182
|
for (const isin of isinToProduct.keys()) {
|
|
@@ -826,76 +1186,100 @@ var Classifier = class {
|
|
|
826
1186
|
}
|
|
827
1187
|
const warnings = [];
|
|
828
1188
|
const newEntries = {};
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
1189
|
+
if (options?.offline) {
|
|
1190
|
+
for (const isin of toProcess) {
|
|
1191
|
+
const product = isinToProduct.get(isin) ?? isin;
|
|
1192
|
+
warnings.push(
|
|
1193
|
+
`Unrecognized type for ${isin}: unknown. Please classify manually.`
|
|
1194
|
+
);
|
|
1195
|
+
newEntries[isin] = {
|
|
1196
|
+
product,
|
|
1197
|
+
assetClass: "Stock",
|
|
1198
|
+
bucketGain: "B",
|
|
1199
|
+
bucketLoss: "B",
|
|
1200
|
+
taxRate: 0,
|
|
1201
|
+
whiteListed: null,
|
|
1202
|
+
confirmedByUser: false,
|
|
1203
|
+
source: "user"
|
|
1204
|
+
};
|
|
833
1205
|
}
|
|
834
|
-
|
|
835
|
-
const
|
|
836
|
-
|
|
837
|
-
)
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
1206
|
+
} else {
|
|
1207
|
+
const BATCH_SIZE = 10;
|
|
1208
|
+
const totalBatches = Math.ceil(toProcess.length / BATCH_SIZE);
|
|
1209
|
+
for (let i = 0; i < toProcess.length; i += BATCH_SIZE) {
|
|
1210
|
+
if (i > 0) {
|
|
1211
|
+
await new Promise((resolve) => setTimeout(resolve, 6e3));
|
|
1212
|
+
}
|
|
1213
|
+
const batch = toProcess.slice(i, i + BATCH_SIZE);
|
|
1214
|
+
const requestBody = JSON.stringify(
|
|
1215
|
+
batch.map((isin) => ({ idType: "ID_ISIN", idValue: isin }))
|
|
1216
|
+
);
|
|
1217
|
+
let response = null;
|
|
1218
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
1219
|
+
try {
|
|
1220
|
+
const r = await _httpPost(
|
|
1221
|
+
"https://api.openfigi.com/v3/mapping",
|
|
1222
|
+
requestBody,
|
|
1223
|
+
1e4
|
|
1224
|
+
);
|
|
1225
|
+
if (r.status < 500) {
|
|
1226
|
+
response = r;
|
|
1227
|
+
break;
|
|
1228
|
+
}
|
|
1229
|
+
} catch {
|
|
1230
|
+
throw new ClassificationError("NETWORK_ERROR");
|
|
849
1231
|
}
|
|
850
|
-
}
|
|
1232
|
+
}
|
|
1233
|
+
if (response === null) {
|
|
851
1234
|
throw new ClassificationError("NETWORK_ERROR");
|
|
852
1235
|
}
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
1236
|
+
const results = JSON.parse(response.data);
|
|
1237
|
+
for (let j = 0; j < batch.length; j++) {
|
|
1238
|
+
const isin = batch[j];
|
|
1239
|
+
const result = results[j];
|
|
1240
|
+
const product = isinToProduct.get(isin) ?? isin;
|
|
1241
|
+
if (!result || result.error || !result.data || result.data.length === 0) {
|
|
1242
|
+
warnings.push(
|
|
1243
|
+
`Unrecognized type for ${isin}: unknown. Please classify manually.`
|
|
1244
|
+
);
|
|
1245
|
+
newEntries[isin] = {
|
|
1246
|
+
product,
|
|
1247
|
+
assetClass: "Stock",
|
|
1248
|
+
bucketGain: "B",
|
|
1249
|
+
bucketLoss: "B",
|
|
1250
|
+
taxRate: 0,
|
|
1251
|
+
whiteListed: null,
|
|
1252
|
+
confirmedByUser: false,
|
|
1253
|
+
source: "user"
|
|
1254
|
+
};
|
|
1255
|
+
continue;
|
|
1256
|
+
}
|
|
1257
|
+
const st = result.data[0]?.securityType;
|
|
1258
|
+
const st2 = result.data[0]?.securityType2;
|
|
1259
|
+
const typeToUse = st && isKnownType(st) ? st : st2 && isKnownType(st2) ? st2 : st ?? st2 ?? "Unknown";
|
|
1260
|
+
newEntries[isin] = classifyByType(isin, typeToUse, product, warnings);
|
|
877
1261
|
}
|
|
878
|
-
const
|
|
879
|
-
|
|
880
|
-
const typeToUse = st && isKnownType(st) ? st : st2 && isKnownType(st2) ? st2 : st ?? st2 ?? "Unknown";
|
|
881
|
-
newEntries[isin] = classifyByType(isin, typeToUse, product, warnings);
|
|
1262
|
+
const batchIndex = i / BATCH_SIZE;
|
|
1263
|
+
options?.onBatchProgress?.(batchIndex + 1, totalBatches);
|
|
882
1264
|
}
|
|
883
1265
|
}
|
|
884
1266
|
const mergedMap = { ...newEntries, ...confirmed };
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
1267
|
+
if (sidecarPath !== void 0) {
|
|
1268
|
+
const sidecarContent = JSON.stringify(
|
|
1269
|
+
{
|
|
1270
|
+
version: 1,
|
|
1271
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1272
|
+
classifications: mergedMap,
|
|
1273
|
+
warnings
|
|
1274
|
+
},
|
|
1275
|
+
null,
|
|
1276
|
+
2
|
|
1277
|
+
);
|
|
1278
|
+
try {
|
|
1279
|
+
fs2.writeFileSync(sidecarPath, sidecarContent, "utf-8");
|
|
1280
|
+
} catch {
|
|
1281
|
+
throw new ClassificationError("WRITE_ERROR");
|
|
1282
|
+
}
|
|
899
1283
|
}
|
|
900
1284
|
return mergedMap;
|
|
901
1285
|
}
|