@gabrielerandelli/minus-tracker 0.8.0 → 0.8.2
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 +48 -32
- package/dist/cli/index.js +83 -55
- package/dist/cli/index.js.map +1 -1
- package/dist/index.cjs +25 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +14 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +25 -3
- package/dist/index.js.map +1 -1
- package/dist/mcp/index.js +43 -5
- package/dist/mcp/index.js.map +1 -1
- package/package.json +10 -3
package/README.md
CHANGED
|
@@ -2,6 +2,21 @@
|
|
|
2
2
|
|
|
3
3
|
[](./LICENSE)
|
|
4
4
|
[](https://www.npmjs.com/package/@gabrielerandelli/minus-tracker)
|
|
5
|
+
[](https://www.npmjs.com/package/@gabrielerandelli/minus-tracker)
|
|
6
|
+
[](https://nodejs.org)
|
|
7
|
+
[](https://modelcontextprotocol.io)
|
|
8
|
+
|
|
9
|
+
⚠️ **Ausilio al calcolo, non consulenza fiscale** — vedi l'[Avvertenza](#avvertenza).
|
|
10
|
+
⚠️ **Calculation aid only, not tax advice** — see the [Disclaimer](#disclaimer).
|
|
11
|
+
|
|
12
|
+
🤖 **Novità: server MCP** — qualsiasi agente AI può calcolare plusvalenze/minusvalenze italiane direttamente, senza passare dalla CLI.
|
|
13
|
+
🤖 **New: MCP server** — any AI agent can compute Italian capital gains/losses directly, no CLI required.
|
|
14
|
+
|
|
15
|
+
✅ Validato su tutti i 12 scenari FAQ dell'Agenzia delle Entrate · 120 test automatici · zero dipendenze runtime nella libreria core.
|
|
16
|
+
✅ Validated against all 12 Agenzia delle Entrate FAQ scenarios · 120 automated tests · zero runtime dependencies in the core library.
|
|
17
|
+
|
|
18
|
+
_L'unico tool open-source che trasforma un export DEGIRO grezzo direttamente in plusvalenze/minusvalenze corrette secondo l'Agenzia delle Entrate — come CLI, libreria, o strumento MCP richiamabile dal tuo agente AI._
|
|
19
|
+
_The only open-source tool that turns a raw DEGIRO export directly into Agenzia-Entrate-correct capital gains/losses — as a CLI, a library, or an MCP tool your AI agent can call._
|
|
5
20
|
|
|
6
21
|
---
|
|
7
22
|
|
|
@@ -16,8 +31,9 @@ Il tool elabora i dati partendo direttamente dal formato CSV esportato da DEGIRO
|
|
|
16
31
|
- Calcolo di plusvalenze e minusvalenze con gestione dei lotti via **LIFO o FIFO** (configurabile)
|
|
17
32
|
- **Parser integrato** per i file CSV di DEGIRO
|
|
18
33
|
- Gestione **multivaluta** con tassi storici BCE (EUR, USD, GBP, CHF)
|
|
19
|
-
- **Classificazione
|
|
20
|
-
di stato, derivati — con classificazione automatica
|
|
34
|
+
- **Classificazione fiscale degli strumenti finanziari in due categorie** (Bucket A/B) — azioni,
|
|
35
|
+
ETF, titoli di stato, derivati — con classificazione automatica tramite OpenFIGI e sidecar JSON
|
|
36
|
+
persistente
|
|
21
37
|
- **Modello Redditi PF**: genera Quadro RT (redditi diversi) e Quadro RM (redditi di capitale) a
|
|
22
38
|
partire dai lotti calcolati, con riporto delle minusvalenze pregresse (regola dei 4 anni)
|
|
23
39
|
- Suite di test allineata alle **FAQ dell'Agenzia delle Entrate**
|
|
@@ -27,7 +43,7 @@ Il tool elabora i dati partendo direttamente dal formato CSV esportato da DEGIRO
|
|
|
27
43
|
**Novità in v0.8.0:**
|
|
28
44
|
|
|
29
45
|
- **Modalità stateless per `Classifier`**: `classify()` può ora funzionare senza alcun accesso al
|
|
30
|
-
filesystem (nessun sidecar), utile per l'integrazione
|
|
46
|
+
filesystem (nessun sidecar), utile per l'integrazione con agenti e automazioni
|
|
31
47
|
- **Server MCP** (`minus-tracker-mcp`): espone `parse_transactions`, `classify_instruments` e
|
|
32
48
|
`calculate_gains` come tool MCP su stdio, per l'uso diretto da parte di agenti AI — vedi la
|
|
33
49
|
sezione [Server MCP](#server-mcp) più sotto
|
|
@@ -97,16 +113,16 @@ npx @gabrielerandelli/minus-tracker calc trades.csv
|
|
|
97
113
|
|
|
98
114
|
### Utilizzo CLI
|
|
99
115
|
|
|
100
|
-
| Comando | Flag principali | Note
|
|
101
|
-
| ---------------------- | -------------------------------------------------------------------------------------------------------------------- |
|
|
102
|
-
| `calc <file.csv>` | `--method LIFO\|FIFO` (default: LIFO), `--lang it\|en`, `--json`, `--export-dichiarazione [path]`, `--carry-forward` |
|
|
103
|
-
| `classify <file.csv>` | `--offline` | Classifica gli strumenti (Bucket A/B) e crea/aggiorna il sidecar `*.classify.json`
|
|
104
|
-
| `validate <file.csv>` | `--lang it\|en` | Exit 0 con avvisi; exit 1 in caso di errori bloccanti
|
|
105
|
-
| `rates --check` | — | Mostra la copertura della snapshot BCE in locale
|
|
106
|
-
| `rates --update` | — | Scarica i tassi aggiornati dall'API BCE
|
|
107
|
-
| `config --lang it\|en` | — | Salva la lingua preferita
|
|
108
|
-
| `config --show` | — | Mostra la lingua correntemente impostata
|
|
109
|
-
| `stress-test` | `--range N-M`, `--keep`, `--json`, `--output-dir` | Documentato in fondo
|
|
116
|
+
| Comando | Flag principali | Note |
|
|
117
|
+
| ---------------------- | -------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
118
|
+
| `calc <file.csv>` | `--method LIFO\|FIFO` (default: LIFO), `--lang it\|en`, `--json`, `--export-dichiarazione [path]`, `--carry-forward` | Non aggiorna mai i tassi BCE da solo — esegui `rates --update` periodicamente; usa il sidecar di `classify` se presente |
|
|
119
|
+
| `classify <file.csv>` | `--offline` | Classifica gli strumenti (Bucket A/B) e crea/aggiorna il sidecar `*.classify.json`. Richiede un terminale interattivo (TTY), oppure il flag `--offline` in contesti scriptati/CI |
|
|
120
|
+
| `validate <file.csv>` | `--lang it\|en` | Exit 0 con avvisi; exit 1 in caso di errori bloccanti |
|
|
121
|
+
| `rates --check` | — | Mostra la copertura della snapshot BCE in locale |
|
|
122
|
+
| `rates --update` | — | Scarica i tassi aggiornati dall'API BCE |
|
|
123
|
+
| `config --lang it\|en` | — | Salva la lingua preferita |
|
|
124
|
+
| `config --show` | — | Mostra la lingua correntemente impostata |
|
|
125
|
+
| `stress-test` | `--range N-M`, `--keep`, `--json`, `--output-dir` | Documentato in fondo |
|
|
110
126
|
|
|
111
127
|
Precedenza lingua: `--lang` > lingua salvata > italiano (default).
|
|
112
128
|
|
|
@@ -157,7 +173,7 @@ if (parser.warnings.length > 0) {
|
|
|
157
173
|
// 2. Classificazione (Bucket A/B) — facoltativa, abilita Quadro RT/RM nel report
|
|
158
174
|
const classification = await new Classifier().classify(
|
|
159
175
|
transactions,
|
|
160
|
-
"trades.classify.json", // sidecar persistente;
|
|
176
|
+
"trades.classify.json", // sidecar persistente; assente in modalità stateless
|
|
161
177
|
);
|
|
162
178
|
|
|
163
179
|
// 3. Calcolo
|
|
@@ -227,15 +243,15 @@ Configurazione tipica per un client MCP (es. Claude Desktop, `claude_desktop_con
|
|
|
227
243
|
|
|
228
244
|
Il server espone 3 tool:
|
|
229
245
|
|
|
230
|
-
| Tool | Descrizione
|
|
231
|
-
| ---------------------- |
|
|
232
|
-
| `parse_transactions` |
|
|
233
|
-
| `classify_instruments` | Classifica gli ISIN in Bucket A/B (modalità stateless — nessun sidecar)
|
|
234
|
-
| `calculate_gains` | Calcola plusvalenze/minusvalenze (LIFO/FIFO) e, se disponibile, Quadro RT/RM
|
|
246
|
+
| Tool | Descrizione |
|
|
247
|
+
| ---------------------- | -------------------------------------------------------------------------------------- |
|
|
248
|
+
| `parse_transactions` | Esegue il parsing di un CSV DEGIRO, restituendo `transactions`/`warnings`/`incomeRows` |
|
|
249
|
+
| `classify_instruments` | Classifica gli ISIN in Bucket A/B (modalità stateless — nessun sidecar) |
|
|
250
|
+
| `calculate_gains` | Calcola plusvalenze/minusvalenze (LIFO/FIFO) e, se disponibile, Quadro RT/RM |
|
|
235
251
|
|
|
236
252
|
`classify_instruments` supporta `existingClassification`, `overrides` e `offline: true` per
|
|
237
253
|
funzionare senza rete e senza accesso al filesystem — pensato per essere invocato ripetutamente
|
|
238
|
-
da un agente
|
|
254
|
+
da un agente in chiamate successive, mantenendo lo stato lato client.
|
|
239
255
|
|
|
240
256
|
### Domande frequenti
|
|
241
257
|
|
|
@@ -249,7 +265,7 @@ Le righe vengono saltate (senza bloccare il calcolo) quando: l'ISIN è vuoto, la
|
|
|
249
265
|
Il CSV contiene una vendita per un titolo senza un acquisto precedente nel file. L'acquisto potrebbe essere in un export di un anno precedente non incluso. Usa `validate` per esaminare le transazioni parsate.
|
|
250
266
|
|
|
251
267
|
**I tassi BCE sono scaduti**
|
|
252
|
-
Esegui `minus-tracker rates --update`. Il comando `calc`
|
|
268
|
+
Esegui `minus-tracker rates --update`. Il comando `calc` non aggiorna mai i tassi da solo: l'aggiornamento è sempre un'azione esplicita.
|
|
253
269
|
|
|
254
270
|
**LIFO o FIFO?**
|
|
255
271
|
Il Regime Dichiarativo utilizza il LIFO come metodo predefinito ai sensi della normativa fiscale italiana. Il FIFO è disponibile per confronto. Consulta il tuo commercialista per conferma.
|
|
@@ -393,16 +409,16 @@ npx @gabrielerandelli/minus-tracker calc trades.csv
|
|
|
393
409
|
|
|
394
410
|
### CLI Usage
|
|
395
411
|
|
|
396
|
-
| Command | Key flags | Notes
|
|
397
|
-
| ---------------------- | -------------------------------------------------------------------------------------------------------------------- |
|
|
398
|
-
| `calc <file.csv>` | `--method LIFO\|FIFO` (default: LIFO), `--lang it\|en`, `--json`, `--export-dichiarazione [path]`, `--carry-forward` |
|
|
399
|
-
| `classify <file.csv>` | `--offline` | Classifies instruments (Bucket A/B) and creates/updates the `*.classify.json` sidecar
|
|
400
|
-
| `validate <file.csv>` | `--lang it\|en` | Exit 0 with warnings; exit 1 on hard errors
|
|
401
|
-
| `rates --check` | — | Shows bundled ECB snapshot coverage
|
|
402
|
-
| `rates --update` | — | Fetches fresh rates from the ECB API
|
|
403
|
-
| `config --lang it\|en` | — | Saves language preference
|
|
404
|
-
| `config --show` | — | Shows current language setting
|
|
405
|
-
| `stress-test` | `--range N-M`, `--keep`, `--json`, `--output-dir` | Documented below
|
|
412
|
+
| Command | Key flags | Notes |
|
|
413
|
+
| ---------------------- | -------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
414
|
+
| `calc <file.csv>` | `--method LIFO\|FIFO` (default: LIFO), `--lang it\|en`, `--json`, `--export-dichiarazione [path]`, `--carry-forward` | Never fetches ECB rates on its own — run `rates --update` periodically; uses `classify`'s sidecar when present |
|
|
415
|
+
| `classify <file.csv>` | `--offline` | Classifies instruments (Bucket A/B) and creates/updates the `*.classify.json` sidecar. Requires an interactive terminal (TTY), or the `--offline` flag in scripted/CI contexts |
|
|
416
|
+
| `validate <file.csv>` | `--lang it\|en` | Exit 0 with warnings; exit 1 on hard errors |
|
|
417
|
+
| `rates --check` | — | Shows bundled ECB snapshot coverage |
|
|
418
|
+
| `rates --update` | — | Fetches fresh rates from the ECB API |
|
|
419
|
+
| `config --lang it\|en` | — | Saves language preference |
|
|
420
|
+
| `config --show` | — | Shows current language setting |
|
|
421
|
+
| `stress-test` | `--range N-M`, `--keep`, `--json`, `--output-dir` | Documented below |
|
|
406
422
|
|
|
407
423
|
Language precedence: `--lang` flag > saved config > Italian (default).
|
|
408
424
|
|
|
@@ -543,7 +559,7 @@ Rows are skipped (without aborting the calculation) when: the ISIN is empty, the
|
|
|
543
559
|
The CSV contains a SELL for a position that has no prior BUY in the same file. The BUY may be in a prior year's export that was not included. Use `validate` to inspect the parsed transactions.
|
|
544
560
|
|
|
545
561
|
**ECB rates are outdated**
|
|
546
|
-
Run `minus-tracker rates --update`. The `calc` command
|
|
562
|
+
Run `minus-tracker rates --update`. The `calc` command never fetches rates on its own — refreshing is always an explicit, user-invoked action.
|
|
547
563
|
|
|
548
564
|
**LIFO or FIFO?**
|
|
549
565
|
LIFO is the standard lot-matching method under Italian tax law for the Regime Dichiarativo. FIFO is available for comparison or other jurisdictions. Consult your tax advisor for confirmation.
|
package/dist/cli/index.js
CHANGED
|
@@ -9,6 +9,9 @@ var it = {
|
|
|
9
9
|
errorInvalidCsv: "CSV non valido: impossibile analizzare il file",
|
|
10
10
|
errorMissingColumn: (col) => `Colonna obbligatoria mancante: ${col}`,
|
|
11
11
|
errorNoOpenLots: (isin, date) => `Nessun lotto aperto per ISIN ${isin} in data ${date}`,
|
|
12
|
+
errorCannotReadFile: (path7) => `Impossibile leggere il file: ${path7}`,
|
|
13
|
+
errorCannotLoadSidecar: (path7) => `Impossibile caricare il sidecar: ${path7}`,
|
|
14
|
+
errorCannotWriteExport: (path7) => `Impossibile scrivere l'export della dichiarazione: ${path7}`,
|
|
12
15
|
warnMissingIsin: (row) => `Riga ${row}: ISIN mancante \u2014 riga ignorata`,
|
|
13
16
|
warnUnsupportedCurrency: (row, currency) => `Riga ${row}: valuta non supportata ${currency} \u2014 riga ignorata`,
|
|
14
17
|
warnNoEcbRate: (row, currency, date) => `Riga ${row}: nessun tasso BCE per ${currency} in data ${date} \u2014 riga ignorata`,
|
|
@@ -88,7 +91,7 @@ var it = {
|
|
|
88
91
|
quadroRMRitenuta: "Ritenuta estera (credito)",
|
|
89
92
|
quadroRMCedole: "[RM-C] Cedole obbligazionarie",
|
|
90
93
|
dichiarazioneDisclaimer: "minus-tracker \xE8 un ausilio al calcolo, non consulenza fiscale.",
|
|
91
|
-
warnNoDichiarazioneSidecar: "
|
|
94
|
+
warnNoDichiarazioneSidecar: "Suggerimento: esegui prima 'minus-tracker classify <file>' per abilitare il Quadro RT/RM.",
|
|
92
95
|
warnNoCarryForwardProvided: "(eventuali perdite pregresse non applicate)"
|
|
93
96
|
};
|
|
94
97
|
|
|
@@ -98,6 +101,9 @@ var en = {
|
|
|
98
101
|
errorInvalidCsv: "Invalid CSV: unable to parse",
|
|
99
102
|
errorMissingColumn: (col) => `Missing required column: ${col}`,
|
|
100
103
|
errorNoOpenLots: (isin, date) => `No open lots for ISIN ${isin} on ${date}`,
|
|
104
|
+
errorCannotReadFile: (path7) => `Cannot read file: ${path7}`,
|
|
105
|
+
errorCannotLoadSidecar: (path7) => `Cannot load sidecar: ${path7}`,
|
|
106
|
+
errorCannotWriteExport: (path7) => `Cannot write dichiarazione export: ${path7}`,
|
|
101
107
|
warnMissingIsin: (row) => `Row ${row}: missing ISIN \u2014 skipped`,
|
|
102
108
|
warnUnsupportedCurrency: (row, currency) => `Row ${row}: unsupported currency ${currency} \u2014 skipped`,
|
|
103
109
|
warnNoEcbRate: (row, currency, date) => `Row ${row}: no ECB rate for ${currency} on ${date} \u2014 skipped`,
|
|
@@ -177,7 +183,7 @@ var en = {
|
|
|
177
183
|
quadroRMRitenuta: "Foreign withholding (credit)",
|
|
178
184
|
quadroRMCedole: "[RM-C] Bond coupons",
|
|
179
185
|
dichiarazioneDisclaimer: "minus-tracker \xE8 un ausilio al calcolo, non consulenza fiscale.",
|
|
180
|
-
warnNoDichiarazioneSidecar: "
|
|
186
|
+
warnNoDichiarazioneSidecar: "Tip: run 'minus-tracker classify <file>' first to enable Quadro RT/RM.",
|
|
181
187
|
warnNoCarryForwardProvided: "(prior-year losses not applied)"
|
|
182
188
|
};
|
|
183
189
|
|
|
@@ -377,6 +383,16 @@ var REQUIRED_COLUMNS = [
|
|
|
377
383
|
];
|
|
378
384
|
var INCOME_KEYWORDS = ["DIVIDEND", "COUPON", "INTEREST", "CEDOLA"];
|
|
379
385
|
var TAX_KEYWORDS = ["DIVIDEND TAX", "WITHHOLDING", "RITENUTA"];
|
|
386
|
+
var BINARY_GARBAGE_CONTROL_CHAR_RATIO = 0.1;
|
|
387
|
+
function looksLikeBinaryGarbage(firstLine) {
|
|
388
|
+
if (firstLine.length === 0) return false;
|
|
389
|
+
let controlChars = 0;
|
|
390
|
+
for (let i = 0; i < firstLine.length; i++) {
|
|
391
|
+
const code = firstLine.charCodeAt(i);
|
|
392
|
+
if (code < 32 && code !== 9) controlChars++;
|
|
393
|
+
}
|
|
394
|
+
return controlChars / firstLine.length > BINARY_GARBAGE_CONTROL_CHAR_RATIO;
|
|
395
|
+
}
|
|
380
396
|
function parseCSVRow(line) {
|
|
381
397
|
const fields = [];
|
|
382
398
|
let i = 0;
|
|
@@ -439,6 +455,10 @@ var DEGIROParser = class {
|
|
|
439
455
|
if (typeof csv !== "string" || csv.includes("\0")) {
|
|
440
456
|
throw new ParseError("INVALID_CSV");
|
|
441
457
|
}
|
|
458
|
+
const firstLine = csv.split("\n", 1)[0] ?? "";
|
|
459
|
+
if (looksLikeBinaryGarbage(firstLine)) {
|
|
460
|
+
throw new ParseError("INVALID_CSV");
|
|
461
|
+
}
|
|
442
462
|
let rows;
|
|
443
463
|
try {
|
|
444
464
|
rows = parseCSV(csv);
|
|
@@ -954,12 +974,19 @@ var Calculator = class {
|
|
|
954
974
|
);
|
|
955
975
|
let remaining = bPlusvalenze - bMinusvalenze;
|
|
956
976
|
let carryForwardApplied = 0;
|
|
977
|
+
const carryForwardEntriesRemaining = [];
|
|
957
978
|
for (const entry of carryForwards) {
|
|
958
979
|
if (taxYear - entry.year > 4) continue;
|
|
959
|
-
|
|
960
|
-
const consumed = Math.min(entry.amount, remaining);
|
|
980
|
+
const consumed = remaining > 0 ? Math.min(entry.amount, remaining) : 0;
|
|
961
981
|
carryForwardApplied += consumed;
|
|
962
982
|
remaining -= consumed;
|
|
983
|
+
const residual = roundHalfUp2(entry.amount - consumed);
|
|
984
|
+
if (residual > 0) {
|
|
985
|
+
carryForwardEntriesRemaining.push({
|
|
986
|
+
annoOrigine: entry.year,
|
|
987
|
+
importo: residual
|
|
988
|
+
});
|
|
989
|
+
}
|
|
963
990
|
}
|
|
964
991
|
carryForwardApplied = roundHalfUp2(carryForwardApplied);
|
|
965
992
|
const bNetResult = roundHalfUp2(
|
|
@@ -971,6 +998,7 @@ var Calculator = class {
|
|
|
971
998
|
minusvalenze: bMinusvalenze,
|
|
972
999
|
carryForwardApplied,
|
|
973
1000
|
carryForwardRemaining,
|
|
1001
|
+
carryForwardEntriesRemaining,
|
|
974
1002
|
netResult: bNetResult
|
|
975
1003
|
};
|
|
976
1004
|
const allIncomeRows = this._options.incomeRows ?? [];
|
|
@@ -1015,7 +1043,7 @@ var Calculator = class {
|
|
|
1015
1043
|
const hasOther = allIsins.some((isin) => !isin.startsWith("IE"));
|
|
1016
1044
|
if (hasIePrefix && hasOther) {
|
|
1017
1045
|
warnings.push(
|
|
1018
|
-
"
|
|
1046
|
+
"WARNING: CSV contains mixed instrument types (e.g. ETFs + Stocks).\n Single-bucket calculation may not be fiscally correct.\n Run: minus-tracker classify trades.csv"
|
|
1019
1047
|
);
|
|
1020
1048
|
}
|
|
1021
1049
|
}
|
|
@@ -1689,8 +1717,7 @@ async function runCalc(positional, flags, s, stdout, stderr) {
|
|
|
1689
1717
|
try {
|
|
1690
1718
|
csv = fs4.readFileSync(filePath, "utf8");
|
|
1691
1719
|
} catch {
|
|
1692
|
-
stderr.write(
|
|
1693
|
-
`);
|
|
1720
|
+
stderr.write(s.errorCannotReadFile(filePath) + "\n");
|
|
1694
1721
|
return 1;
|
|
1695
1722
|
}
|
|
1696
1723
|
const method = flags["method"] ?? "LIFO";
|
|
@@ -1705,8 +1732,7 @@ async function runCalc(positional, flags, s, stdout, stderr) {
|
|
|
1705
1732
|
const classifier = new Classifier({ interactive: false });
|
|
1706
1733
|
classification = await classifier.load(sidecarPath);
|
|
1707
1734
|
} catch {
|
|
1708
|
-
stderr.write(
|
|
1709
|
-
`);
|
|
1735
|
+
stderr.write(s.errorCannotLoadSidecar(sidecarPath) + "\n");
|
|
1710
1736
|
return 1;
|
|
1711
1737
|
}
|
|
1712
1738
|
}
|
|
@@ -1776,8 +1802,7 @@ async function runCalc(positional, flags, s, stdout, stderr) {
|
|
|
1776
1802
|
try {
|
|
1777
1803
|
await report.dichiarazione.exportTo(exportPath);
|
|
1778
1804
|
} catch {
|
|
1779
|
-
stderr.write(
|
|
1780
|
-
`);
|
|
1805
|
+
stderr.write(s.errorCannotWriteExport(exportPath) + "\n");
|
|
1781
1806
|
return 1;
|
|
1782
1807
|
}
|
|
1783
1808
|
}
|
|
@@ -1869,18 +1894,45 @@ function getSnapshotPath() {
|
|
|
1869
1894
|
}
|
|
1870
1895
|
return path4.join(configDir, "minus-tracker", "ecb-rates.json");
|
|
1871
1896
|
}
|
|
1897
|
+
function countMissingBusinessDays(startDate, endDate, dates) {
|
|
1898
|
+
let missing = 0;
|
|
1899
|
+
const cursor = /* @__PURE__ */ new Date(startDate + "T00:00:00Z");
|
|
1900
|
+
const end = /* @__PURE__ */ new Date(endDate + "T00:00:00Z");
|
|
1901
|
+
while (cursor <= end) {
|
|
1902
|
+
const day = cursor.getUTCDay();
|
|
1903
|
+
const iso = cursor.toISOString().slice(0, 10);
|
|
1904
|
+
if (day !== 0 && day !== 6 && dates[iso] === void 0) missing++;
|
|
1905
|
+
cursor.setUTCDate(cursor.getUTCDate() + 1);
|
|
1906
|
+
}
|
|
1907
|
+
return missing;
|
|
1908
|
+
}
|
|
1872
1909
|
function getCoverage(snapshot) {
|
|
1873
1910
|
let start = "9999-12-31";
|
|
1874
1911
|
let end = "0000-01-01";
|
|
1875
1912
|
const currencies = [];
|
|
1913
|
+
const gapEntries = [];
|
|
1876
1914
|
for (const [ccy, dates] of Object.entries(snapshot)) {
|
|
1877
1915
|
currencies.push(ccy);
|
|
1878
|
-
|
|
1916
|
+
const keys = Object.keys(dates).sort();
|
|
1917
|
+
for (const d of keys) {
|
|
1879
1918
|
if (d < start) start = d;
|
|
1880
1919
|
if (d > end) end = d;
|
|
1881
1920
|
}
|
|
1921
|
+
if (keys.length > 0) {
|
|
1922
|
+
const missing = countMissingBusinessDays(
|
|
1923
|
+
keys[0],
|
|
1924
|
+
keys[keys.length - 1],
|
|
1925
|
+
dates
|
|
1926
|
+
);
|
|
1927
|
+
if (missing > 0) gapEntries.push(`${ccy}: ${missing}`);
|
|
1928
|
+
}
|
|
1882
1929
|
}
|
|
1883
|
-
return {
|
|
1930
|
+
return {
|
|
1931
|
+
start,
|
|
1932
|
+
end,
|
|
1933
|
+
currencies: currencies.sort().join(", "),
|
|
1934
|
+
gaps: gapEntries.sort().join(", ")
|
|
1935
|
+
};
|
|
1884
1936
|
}
|
|
1885
1937
|
async function fetchEcbData(currency) {
|
|
1886
1938
|
return new Promise((resolve2, reject) => {
|
|
@@ -1944,9 +1996,9 @@ async function updateRates(snapshotPath, stdout, stderr, s) {
|
|
|
1944
1996
|
async function runRates(positional, flags, s, stdout, stderr) {
|
|
1945
1997
|
if (flags["check"]) {
|
|
1946
1998
|
const snapshot = getActiveSnapshot();
|
|
1947
|
-
const { start, end, currencies } = getCoverage(snapshot);
|
|
1999
|
+
const { start, end, currencies, gaps } = getCoverage(snapshot);
|
|
1948
2000
|
stdout.write(s.ratesCoverage(start, end, currencies) + "\n");
|
|
1949
|
-
stdout.write(s.ratesGapsNone + "\n");
|
|
2001
|
+
stdout.write((gaps ? s.ratesGaps(gaps) : s.ratesGapsNone) + "\n");
|
|
1950
2002
|
return 0;
|
|
1951
2003
|
}
|
|
1952
2004
|
if (flags["update"]) {
|
|
@@ -2340,6 +2392,7 @@ async function runStressTest(_positionals, flags, stdout, stderr) {
|
|
|
2340
2392
|
const cliBin = process.argv[1];
|
|
2341
2393
|
let outputDir;
|
|
2342
2394
|
const outputDirFlag = flags["output-dir"];
|
|
2395
|
+
const isAutoGenerated = !outputDirFlag;
|
|
2343
2396
|
if (outputDirFlag) {
|
|
2344
2397
|
outputDir = outputDirFlag;
|
|
2345
2398
|
fs7.mkdirSync(outputDir, { recursive: true });
|
|
@@ -2380,8 +2433,15 @@ async function runStressTest(_positionals, flags, stdout, stderr) {
|
|
|
2380
2433
|
} else {
|
|
2381
2434
|
stdout.write(formatTable(report) + "\n");
|
|
2382
2435
|
}
|
|
2383
|
-
if (
|
|
2384
|
-
|
|
2436
|
+
if (isAutoGenerated) {
|
|
2437
|
+
if (!flags["keep"]) {
|
|
2438
|
+
fs7.rmSync(outputDir, { recursive: true, force: true });
|
|
2439
|
+
}
|
|
2440
|
+
} else if (!flags["keep"]) {
|
|
2441
|
+
stdout.write(
|
|
2442
|
+
`Note: --output-dir was user-specified \u2014 leaving ${outputDir} in place.
|
|
2443
|
+
`
|
|
2444
|
+
);
|
|
2385
2445
|
}
|
|
2386
2446
|
return report.failed === 0 ? 0 : 1;
|
|
2387
2447
|
}
|
|
@@ -2427,43 +2487,9 @@ async function runClassify(positional, flags, s, stdout, stderr) {
|
|
|
2427
2487
|
const sidecarPath = base + ".classify.json";
|
|
2428
2488
|
if (offline) {
|
|
2429
2489
|
stdout.write(s.classifyOfflineWarning + "\n");
|
|
2430
|
-
const
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
isinToProduct.set(tx.isin, tx.product);
|
|
2434
|
-
}
|
|
2435
|
-
}
|
|
2436
|
-
const classifications = {};
|
|
2437
|
-
for (const [isin, product] of isinToProduct.entries()) {
|
|
2438
|
-
classifications[isin] = {
|
|
2439
|
-
product,
|
|
2440
|
-
assetClass: "ETF",
|
|
2441
|
-
bucketGain: "A",
|
|
2442
|
-
bucketLoss: "B",
|
|
2443
|
-
taxRate: 0.26,
|
|
2444
|
-
whiteListed: null,
|
|
2445
|
-
confirmedByUser: true,
|
|
2446
|
-
source: "user"
|
|
2447
|
-
};
|
|
2448
|
-
}
|
|
2449
|
-
const sidecarContent = JSON.stringify(
|
|
2450
|
-
{
|
|
2451
|
-
version: 1,
|
|
2452
|
-
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2453
|
-
classifications,
|
|
2454
|
-
warnings: []
|
|
2455
|
-
},
|
|
2456
|
-
null,
|
|
2457
|
-
2
|
|
2458
|
-
);
|
|
2459
|
-
try {
|
|
2460
|
-
fs8.writeFileSync(sidecarPath, sidecarContent, "utf-8");
|
|
2461
|
-
} catch {
|
|
2462
|
-
stderr.write(`Cannot write sidecar: ${sidecarPath}
|
|
2463
|
-
`);
|
|
2464
|
-
return 1;
|
|
2465
|
-
}
|
|
2466
|
-
stdout.write(s.classifyWritten(sidecarPath) + "\n");
|
|
2490
|
+
const classifier2 = new Classifier({ interactive: false });
|
|
2491
|
+
await classifier2.classify(transactions, sidecarPath, { offline: true });
|
|
2492
|
+
stdout.write(s.classifyWritten(path6.resolve(sidecarPath)) + "\n");
|
|
2467
2493
|
return 0;
|
|
2468
2494
|
}
|
|
2469
2495
|
const classifier = new Classifier({ interactive: true });
|
|
@@ -2512,7 +2538,9 @@ async function main() {
|
|
|
2512
2538
|
range: { type: "string" },
|
|
2513
2539
|
keep: { type: "boolean", default: false },
|
|
2514
2540
|
"output-dir": { type: "string" },
|
|
2515
|
-
offline: { type: "boolean", default: false }
|
|
2541
|
+
offline: { type: "boolean", default: false },
|
|
2542
|
+
"carry-forward": { type: "string", multiple: true },
|
|
2543
|
+
"export-dichiarazione": { type: "string" }
|
|
2516
2544
|
},
|
|
2517
2545
|
allowPositionals: true,
|
|
2518
2546
|
strict: false
|