@gabrielerandelli/minus-tracker 0.7.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 +179 -69
- package/dist/cli/index.js.map +1 -1
- package/dist/index.cjs +179 -69
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +11 -1
- package/dist/index.d.ts +11 -1
- package/dist/index.js +179 -69
- 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 +9 -3
package/README.md
CHANGED
|
@@ -16,10 +16,22 @@ Il tool elabora i dati partendo direttamente dal formato CSV esportato da DEGIRO
|
|
|
16
16
|
- Calcolo di plusvalenze e minusvalenze con gestione dei lotti via **LIFO o FIFO** (configurabile)
|
|
17
17
|
- **Parser integrato** per i file CSV di DEGIRO
|
|
18
18
|
- Gestione **multivaluta** con tassi storici BCE (EUR, USD, GBP, CHF)
|
|
19
|
+
- **Classificazione a due categorie** (Bucket A/B) degli strumenti finanziari — azioni, ETF, titoli
|
|
20
|
+
di stato, derivati — con classificazione automatica via OpenFIGI e sidecar JSON persistente
|
|
21
|
+
- **Modello Redditi PF**: genera Quadro RT (redditi diversi) e Quadro RM (redditi di capitale) a
|
|
22
|
+
partire dai lotti calcolati, con riporto delle minusvalenze pregresse (regola dei 4 anni)
|
|
19
23
|
- Suite di test allineata alle **FAQ dell'Agenzia delle Entrate**
|
|
20
24
|
- Output disponibile in **italiano** (default) o **inglese** (`--lang en`)
|
|
21
25
|
- Disponibile come pacchetto NPM con supporto CLI
|
|
22
26
|
|
|
27
|
+
**Novità in v0.8.0:**
|
|
28
|
+
|
|
29
|
+
- **Modalità stateless per `Classifier`**: `classify()` può ora funzionare senza alcun accesso al
|
|
30
|
+
filesystem (nessun sidecar), utile per l'integrazione in agenti/automazioni
|
|
31
|
+
- **Server MCP** (`minus-tracker-mcp`): espone `parse_transactions`, `classify_instruments` e
|
|
32
|
+
`calculate_gains` come tool MCP su stdio, per l'uso diretto da parte di agenti AI — vedi la
|
|
33
|
+
sezione [Server MCP](#server-mcp) più sotto
|
|
34
|
+
|
|
23
35
|
### Avvio rapido
|
|
24
36
|
|
|
25
37
|
Se non hai ancora un export di DEGIRO a disposizione, puoi testare il tool con il file di esempio incluso nel pacchetto.
|
|
@@ -85,15 +97,16 @@ npx @gabrielerandelli/minus-tracker calc trades.csv
|
|
|
85
97
|
|
|
86
98
|
### Utilizzo CLI
|
|
87
99
|
|
|
88
|
-
| Comando | Flag principali
|
|
89
|
-
| ---------------------- |
|
|
90
|
-
| `calc <file.csv>` | `--method LIFO\|FIFO` (default: LIFO), `--lang it\|en`, `--json` | Aggiorna i tassi BCE automaticamente se la snapshot ha più di 7 giorni |
|
|
91
|
-
| `
|
|
92
|
-
| `
|
|
93
|
-
| `rates --
|
|
94
|
-
| `
|
|
95
|
-
| `config --
|
|
96
|
-
| `
|
|
100
|
+
| Comando | Flag principali | Note |
|
|
101
|
+
| ---------------------- | -------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
|
|
102
|
+
| `calc <file.csv>` | `--method LIFO\|FIFO` (default: LIFO), `--lang it\|en`, `--json`, `--export-dichiarazione [path]`, `--carry-forward` | Aggiorna i tassi BCE automaticamente se la snapshot ha più di 7 giorni; usa il sidecar di `classify` se presente |
|
|
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 |
|
|
97
110
|
|
|
98
111
|
Precedenza lingua: `--lang` > lingua salvata > italiano (default).
|
|
99
112
|
|
|
@@ -128,6 +141,7 @@ npm install @gabrielerandelli/minus-tracker
|
|
|
128
141
|
import {
|
|
129
142
|
DEGIROParser,
|
|
130
143
|
Calculator,
|
|
144
|
+
Classifier,
|
|
131
145
|
ParseError,
|
|
132
146
|
CalculationError,
|
|
133
147
|
} from "@gabrielerandelli/minus-tracker";
|
|
@@ -140,19 +154,25 @@ if (parser.warnings.length > 0) {
|
|
|
140
154
|
console.warn("Righe saltate:", parser.warnings);
|
|
141
155
|
}
|
|
142
156
|
|
|
143
|
-
// 2.
|
|
144
|
-
const
|
|
145
|
-
const report: GainsReport = new Calculator(
|
|
157
|
+
// 2. Classificazione (Bucket A/B) — facoltativa, abilita Quadro RT/RM nel report
|
|
158
|
+
const classification = await new Classifier().classify(
|
|
146
159
|
transactions,
|
|
147
|
-
|
|
148
|
-
)
|
|
160
|
+
"trades.classify.json", // sidecar persistente; omettilo per la modalità stateless
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
// 3. Calcolo
|
|
164
|
+
const method: LotMethod = "LIFO"; // oppure "FIFO"
|
|
165
|
+
const report: GainsReport = new Calculator(transactions, parser.warnings, {
|
|
166
|
+
classification,
|
|
167
|
+
}).calculateGains(method);
|
|
149
168
|
|
|
150
169
|
console.log(report.plusvalenze); // numero in EUR
|
|
151
170
|
console.log(report.minusvalenze); // numero in EUR (valore assoluto)
|
|
152
171
|
console.log(report.netResult); // plusvalenze - minusvalenze
|
|
153
172
|
console.log(report.lots); // MatchedLot[] — dettaglio per lotto
|
|
173
|
+
console.log(report.dichiarazione); // Quadro RT/RM, se classification è stata passata
|
|
154
174
|
|
|
155
|
-
//
|
|
175
|
+
// 4. Gestione errori
|
|
156
176
|
try {
|
|
157
177
|
const txs = parser.parse(csvString);
|
|
158
178
|
const r = new Calculator(txs, parser.warnings).calculateGains("LIFO");
|
|
@@ -177,6 +197,46 @@ try {
|
|
|
177
197
|
|
|
178
198
|
**Prerequisiti:** Node.js ≥ 24 ([nodejs.org](https://nodejs.org))
|
|
179
199
|
|
|
200
|
+
### Server MCP
|
|
201
|
+
|
|
202
|
+
A partire dalla v0.8.0, minus-tracker include un binario `minus-tracker-mcp` che espone un
|
|
203
|
+
[server MCP](https://modelcontextprotocol.io) su stdio, pensato per l'uso diretto da parte di
|
|
204
|
+
agenti AI (senza passare dalla CLI):
|
|
205
|
+
|
|
206
|
+
```bash
|
|
207
|
+
npx -p @gabrielerandelli/minus-tracker minus-tracker-mcp
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
Configurazione tipica per un client MCP (es. Claude Desktop, `claude_desktop_config.json`):
|
|
211
|
+
|
|
212
|
+
```json
|
|
213
|
+
{
|
|
214
|
+
"mcpServers": {
|
|
215
|
+
"minus-tracker": {
|
|
216
|
+
"command": "npx",
|
|
217
|
+
"args": [
|
|
218
|
+
"-y",
|
|
219
|
+
"-p",
|
|
220
|
+
"@gabrielerandelli/minus-tracker",
|
|
221
|
+
"minus-tracker-mcp"
|
|
222
|
+
]
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
Il server espone 3 tool:
|
|
229
|
+
|
|
230
|
+
| Tool | Descrizione |
|
|
231
|
+
| ---------------------- | ------------------------------------------------------------------------------ |
|
|
232
|
+
| `parse_transactions` | Effettua il parsing di un CSV DEGIRO in `transactions`/`warnings`/`incomeRows` |
|
|
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 |
|
|
235
|
+
|
|
236
|
+
`classify_instruments` supporta `existingClassification`, `overrides` e `offline: true` per
|
|
237
|
+
funzionare senza rete e senza accesso al filesystem — pensato per essere invocato ripetutamente
|
|
238
|
+
da un agente su più chiamate, mantenendo lo stato lato client.
|
|
239
|
+
|
|
180
240
|
### Domande frequenti
|
|
181
241
|
|
|
182
242
|
**Il CSV viene rifiutato con "colonna mancante" o "CSV non valido"**
|
|
@@ -251,10 +311,22 @@ It loads data following the CSV format used by DEGIRO.
|
|
|
251
311
|
- Capital-gains/loss calculation with configurable **LIFO and FIFO** lot matching
|
|
252
312
|
- **DEGIRO CSV parser**
|
|
253
313
|
- **Multi-currency** handling with historical ECB rates (EUR, USD, GBP, CHF)
|
|
314
|
+
- **Two-bucket classification** (Bucket A/B) of financial instruments — stocks, ETFs, government
|
|
315
|
+
bonds, derivatives — with automatic OpenFIGI-backed classification and a persistent JSON sidecar
|
|
316
|
+
- **Modello Redditi PF** generation: Quadro RT (capital gains) and Quadro RM (capital income) built
|
|
317
|
+
from the calculated lots, with prior-year loss carryforward (4-year rule)
|
|
254
318
|
- Test suite based on **Agenzia Entrate FAQ**
|
|
255
319
|
- Output in **Italian** (default) or **English** (`--lang en`)
|
|
256
320
|
- minus-tracker is an NPM package with CLI support
|
|
257
321
|
|
|
322
|
+
**New in v0.8.0:**
|
|
323
|
+
|
|
324
|
+
- **Stateless mode for `Classifier`**: `classify()` can now run with zero filesystem access (no
|
|
325
|
+
sidecar file), useful for embedding in agents/automations
|
|
326
|
+
- **MCP server** (`minus-tracker-mcp`): exposes `parse_transactions`, `classify_instruments`, and
|
|
327
|
+
`calculate_gains` as MCP tools over stdio, for direct use by AI agents — see the
|
|
328
|
+
[MCP Server](#mcp-server) section below
|
|
329
|
+
|
|
258
330
|
### Quick Start
|
|
259
331
|
|
|
260
332
|
Don't have a DEGIRO export yet? Use the sample file bundled with the package.
|
|
@@ -321,15 +393,16 @@ npx @gabrielerandelli/minus-tracker calc trades.csv
|
|
|
321
393
|
|
|
322
394
|
### CLI Usage
|
|
323
395
|
|
|
324
|
-
| Command | Key flags
|
|
325
|
-
| ---------------------- |
|
|
326
|
-
| `calc <file.csv>` | `--method LIFO\|FIFO` (default: LIFO), `--lang it\|en`, `--json` | Auto-fetches ECB rates if snapshot is more than 7 days old |
|
|
327
|
-
| `
|
|
328
|
-
| `
|
|
329
|
-
| `rates --
|
|
330
|
-
| `
|
|
331
|
-
| `config --
|
|
332
|
-
| `
|
|
396
|
+
| Command | Key flags | Notes |
|
|
397
|
+
| ---------------------- | -------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- |
|
|
398
|
+
| `calc <file.csv>` | `--method LIFO\|FIFO` (default: LIFO), `--lang it\|en`, `--json`, `--export-dichiarazione [path]`, `--carry-forward` | Auto-fetches ECB rates if snapshot is more than 7 days old; uses `classify`'s sidecar when present |
|
|
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 |
|
|
333
406
|
|
|
334
407
|
Language precedence: `--lang` flag > saved config > Italian (default).
|
|
335
408
|
|
|
@@ -364,6 +437,7 @@ npm install @gabrielerandelli/minus-tracker
|
|
|
364
437
|
import {
|
|
365
438
|
DEGIROParser,
|
|
366
439
|
Calculator,
|
|
440
|
+
Classifier,
|
|
367
441
|
ParseError,
|
|
368
442
|
CalculationError,
|
|
369
443
|
} from "@gabrielerandelli/minus-tracker";
|
|
@@ -376,19 +450,25 @@ if (parser.warnings.length > 0) {
|
|
|
376
450
|
console.warn("Skipped rows:", parser.warnings);
|
|
377
451
|
}
|
|
378
452
|
|
|
379
|
-
// 2.
|
|
380
|
-
const
|
|
381
|
-
const report: GainsReport = new Calculator(
|
|
453
|
+
// 2. Classify (Bucket A/B) — optional, enables Quadro RT/RM in the report
|
|
454
|
+
const classification = await new Classifier().classify(
|
|
382
455
|
transactions,
|
|
383
|
-
|
|
384
|
-
)
|
|
456
|
+
"trades.classify.json", // persistent sidecar; omit for stateless mode
|
|
457
|
+
);
|
|
458
|
+
|
|
459
|
+
// 3. Calculate
|
|
460
|
+
const method: LotMethod = "LIFO"; // or "FIFO"
|
|
461
|
+
const report: GainsReport = new Calculator(transactions, parser.warnings, {
|
|
462
|
+
classification,
|
|
463
|
+
}).calculateGains(method);
|
|
385
464
|
|
|
386
465
|
console.log(report.plusvalenze); // EUR capital gains (number)
|
|
387
466
|
console.log(report.minusvalenze); // EUR capital losses (number, absolute value)
|
|
388
467
|
console.log(report.netResult); // plusvalenze - minusvalenze
|
|
389
468
|
console.log(report.lots); // MatchedLot[] — per-lot breakdown
|
|
469
|
+
console.log(report.dichiarazione); // Quadro RT/RM, when classification was passed
|
|
390
470
|
|
|
391
|
-
//
|
|
471
|
+
// 4. Error handling
|
|
392
472
|
try {
|
|
393
473
|
const txs = parser.parse(csvString);
|
|
394
474
|
const r = new Calculator(txs, parser.warnings).calculateGains("LIFO");
|
|
@@ -411,6 +491,46 @@ minus-tracker is **pure tax math** — no UI, auth, database, or PDF.
|
|
|
411
491
|
|
|
412
492
|
**Prerequisites:** Node.js ≥ 24 ([nodejs.org](https://nodejs.org))
|
|
413
493
|
|
|
494
|
+
### MCP Server
|
|
495
|
+
|
|
496
|
+
As of v0.8.0, minus-tracker ships a `minus-tracker-mcp` binary exposing an
|
|
497
|
+
[MCP server](https://modelcontextprotocol.io) over stdio, for direct use by AI agents (no need to
|
|
498
|
+
go through the CLI):
|
|
499
|
+
|
|
500
|
+
```bash
|
|
501
|
+
npx -p @gabrielerandelli/minus-tracker minus-tracker-mcp
|
|
502
|
+
```
|
|
503
|
+
|
|
504
|
+
Typical MCP client configuration (e.g. Claude Desktop, `claude_desktop_config.json`):
|
|
505
|
+
|
|
506
|
+
```json
|
|
507
|
+
{
|
|
508
|
+
"mcpServers": {
|
|
509
|
+
"minus-tracker": {
|
|
510
|
+
"command": "npx",
|
|
511
|
+
"args": [
|
|
512
|
+
"-y",
|
|
513
|
+
"-p",
|
|
514
|
+
"@gabrielerandelli/minus-tracker",
|
|
515
|
+
"minus-tracker-mcp"
|
|
516
|
+
]
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
```
|
|
521
|
+
|
|
522
|
+
The server exposes 3 tools:
|
|
523
|
+
|
|
524
|
+
| Tool | Description |
|
|
525
|
+
| ---------------------- | --------------------------------------------------------------------- |
|
|
526
|
+
| `parse_transactions` | Parses a DEGIRO CSV into `transactions`/`warnings`/`incomeRows` |
|
|
527
|
+
| `classify_instruments` | Classifies ISINs into Bucket A/B (stateless mode — no sidecar file) |
|
|
528
|
+
| `calculate_gains` | Calculates gains/losses (LIFO/FIFO) and, when available, Quadro RT/RM |
|
|
529
|
+
|
|
530
|
+
`classify_instruments` supports `existingClassification`, `overrides`, and `offline: true` to run
|
|
531
|
+
without network access or filesystem access — designed to be called repeatedly by an agent across
|
|
532
|
+
multiple calls, with state kept client-side.
|
|
533
|
+
|
|
414
534
|
### FAQ / Troubleshooting
|
|
415
535
|
|
|
416
536
|
**My CSV is rejected with "missing column" or "invalid CSV"**
|
package/dist/cli/index.js
CHANGED
|
@@ -1246,7 +1246,7 @@ function classifyByType(isin, securityType, product, warnings) {
|
|
|
1246
1246
|
};
|
|
1247
1247
|
}
|
|
1248
1248
|
warnings.push(
|
|
1249
|
-
`Unrecognized type: ${securityType}. Please classify manually.`
|
|
1249
|
+
`Unrecognized type for ${isin}: ${securityType}. Please classify manually.`
|
|
1250
1250
|
);
|
|
1251
1251
|
return {
|
|
1252
1252
|
product,
|
|
@@ -1259,6 +1259,84 @@ function classifyByType(isin, securityType, product, warnings) {
|
|
|
1259
1259
|
source: "user"
|
|
1260
1260
|
};
|
|
1261
1261
|
}
|
|
1262
|
+
var ASSET_CLASS_DEFAULTS = {
|
|
1263
|
+
ETF: {
|
|
1264
|
+
assetClass: "ETF",
|
|
1265
|
+
bucketGain: "A",
|
|
1266
|
+
bucketLoss: "B",
|
|
1267
|
+
taxRate: 0.26,
|
|
1268
|
+
whiteListed: null
|
|
1269
|
+
},
|
|
1270
|
+
Stock: {
|
|
1271
|
+
assetClass: "Stock",
|
|
1272
|
+
bucketGain: "B",
|
|
1273
|
+
bucketLoss: "B",
|
|
1274
|
+
taxRate: 0,
|
|
1275
|
+
whiteListed: null
|
|
1276
|
+
},
|
|
1277
|
+
ETC: {
|
|
1278
|
+
assetClass: "ETC",
|
|
1279
|
+
bucketGain: "B",
|
|
1280
|
+
bucketLoss: "B",
|
|
1281
|
+
taxRate: 0,
|
|
1282
|
+
whiteListed: null
|
|
1283
|
+
},
|
|
1284
|
+
GovtBondWL: {
|
|
1285
|
+
assetClass: "GovtBondWL",
|
|
1286
|
+
bucketGain: "A",
|
|
1287
|
+
bucketLoss: "B",
|
|
1288
|
+
taxRate: 0.125,
|
|
1289
|
+
whiteListed: true
|
|
1290
|
+
},
|
|
1291
|
+
GovtBondOther: {
|
|
1292
|
+
assetClass: "GovtBondOther",
|
|
1293
|
+
bucketGain: "A",
|
|
1294
|
+
bucketLoss: "B",
|
|
1295
|
+
taxRate: 0.26,
|
|
1296
|
+
whiteListed: false
|
|
1297
|
+
},
|
|
1298
|
+
CorpBond: {
|
|
1299
|
+
assetClass: "CorpBond",
|
|
1300
|
+
bucketGain: "B",
|
|
1301
|
+
bucketLoss: "B",
|
|
1302
|
+
taxRate: 0,
|
|
1303
|
+
whiteListed: null
|
|
1304
|
+
},
|
|
1305
|
+
Derivative: {
|
|
1306
|
+
assetClass: "Derivative",
|
|
1307
|
+
bucketGain: "B",
|
|
1308
|
+
bucketLoss: "B",
|
|
1309
|
+
taxRate: 0,
|
|
1310
|
+
whiteListed: null
|
|
1311
|
+
},
|
|
1312
|
+
LeverageCert: {
|
|
1313
|
+
assetClass: "LeverageCert",
|
|
1314
|
+
bucketGain: "B",
|
|
1315
|
+
bucketLoss: "B",
|
|
1316
|
+
taxRate: 0,
|
|
1317
|
+
whiteListed: null
|
|
1318
|
+
},
|
|
1319
|
+
CapProtectedCert: {
|
|
1320
|
+
assetClass: "CapProtectedCert",
|
|
1321
|
+
bucketGain: "A",
|
|
1322
|
+
bucketLoss: "B",
|
|
1323
|
+
taxRate: 0.26,
|
|
1324
|
+
whiteListed: null
|
|
1325
|
+
}
|
|
1326
|
+
};
|
|
1327
|
+
function buildEntryFromAssetClass(assetClass, product) {
|
|
1328
|
+
const mapped = ASSET_CLASS_DEFAULTS[assetClass];
|
|
1329
|
+
return {
|
|
1330
|
+
product,
|
|
1331
|
+
assetClass: mapped.assetClass,
|
|
1332
|
+
bucketGain: mapped.bucketGain,
|
|
1333
|
+
bucketLoss: mapped.bucketLoss,
|
|
1334
|
+
taxRate: mapped.taxRate,
|
|
1335
|
+
whiteListed: mapped.whiteListed,
|
|
1336
|
+
confirmedByUser: true,
|
|
1337
|
+
source: "user"
|
|
1338
|
+
};
|
|
1339
|
+
}
|
|
1262
1340
|
var Classifier = class {
|
|
1263
1341
|
interactive;
|
|
1264
1342
|
constructor(options) {
|
|
@@ -1280,7 +1358,7 @@ var Classifier = class {
|
|
|
1280
1358
|
}
|
|
1281
1359
|
return parsed.classifications;
|
|
1282
1360
|
}
|
|
1283
|
-
async classify(transactions, sidecarPath, _httpPost = httpsPost) {
|
|
1361
|
+
async classify(transactions, sidecarPath, options, _httpPost = httpsPost) {
|
|
1284
1362
|
const isinToProduct = /* @__PURE__ */ new Map();
|
|
1285
1363
|
for (const tx of transactions) {
|
|
1286
1364
|
if (tx.isin && !isinToProduct.has(tx.isin)) {
|
|
@@ -1288,13 +1366,21 @@ var Classifier = class {
|
|
|
1288
1366
|
}
|
|
1289
1367
|
}
|
|
1290
1368
|
const confirmed = {};
|
|
1291
|
-
if (
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1369
|
+
if (sidecarPath !== void 0) {
|
|
1370
|
+
if (fs3.existsSync(sidecarPath)) {
|
|
1371
|
+
const existingMap = await this.load(sidecarPath);
|
|
1372
|
+
for (const [isin, entry] of Object.entries(existingMap)) {
|
|
1373
|
+
if (entry.confirmedByUser) {
|
|
1374
|
+
confirmed[isin] = entry;
|
|
1375
|
+
}
|
|
1296
1376
|
}
|
|
1297
1377
|
}
|
|
1378
|
+
} else if (options?.existingClassification) {
|
|
1379
|
+
Object.assign(confirmed, options.existingClassification);
|
|
1380
|
+
}
|
|
1381
|
+
for (const [isin, assetClass] of Object.entries(options?.overrides ?? {})) {
|
|
1382
|
+
const product = isinToProduct.get(isin) ?? isin;
|
|
1383
|
+
confirmed[isin] = buildEntryFromAssetClass(assetClass, product);
|
|
1298
1384
|
}
|
|
1299
1385
|
const toProcess = [];
|
|
1300
1386
|
for (const isin of isinToProduct.keys()) {
|
|
@@ -1304,76 +1390,100 @@ var Classifier = class {
|
|
|
1304
1390
|
}
|
|
1305
1391
|
const warnings = [];
|
|
1306
1392
|
const newEntries = {};
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1393
|
+
if (options?.offline) {
|
|
1394
|
+
for (const isin of toProcess) {
|
|
1395
|
+
const product = isinToProduct.get(isin) ?? isin;
|
|
1396
|
+
warnings.push(
|
|
1397
|
+
`Unrecognized type for ${isin}: unknown. Please classify manually.`
|
|
1398
|
+
);
|
|
1399
|
+
newEntries[isin] = {
|
|
1400
|
+
product,
|
|
1401
|
+
assetClass: "Stock",
|
|
1402
|
+
bucketGain: "B",
|
|
1403
|
+
bucketLoss: "B",
|
|
1404
|
+
taxRate: 0,
|
|
1405
|
+
whiteListed: null,
|
|
1406
|
+
confirmedByUser: false,
|
|
1407
|
+
source: "user"
|
|
1408
|
+
};
|
|
1311
1409
|
}
|
|
1312
|
-
|
|
1313
|
-
const
|
|
1314
|
-
|
|
1315
|
-
)
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1410
|
+
} else {
|
|
1411
|
+
const BATCH_SIZE = 10;
|
|
1412
|
+
const totalBatches = Math.ceil(toProcess.length / BATCH_SIZE);
|
|
1413
|
+
for (let i = 0; i < toProcess.length; i += BATCH_SIZE) {
|
|
1414
|
+
if (i > 0) {
|
|
1415
|
+
await new Promise((resolve2) => setTimeout(resolve2, 6e3));
|
|
1416
|
+
}
|
|
1417
|
+
const batch = toProcess.slice(i, i + BATCH_SIZE);
|
|
1418
|
+
const requestBody = JSON.stringify(
|
|
1419
|
+
batch.map((isin) => ({ idType: "ID_ISIN", idValue: isin }))
|
|
1420
|
+
);
|
|
1421
|
+
let response = null;
|
|
1422
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
1423
|
+
try {
|
|
1424
|
+
const r = await _httpPost(
|
|
1425
|
+
"https://api.openfigi.com/v3/mapping",
|
|
1426
|
+
requestBody,
|
|
1427
|
+
1e4
|
|
1428
|
+
);
|
|
1429
|
+
if (r.status < 500) {
|
|
1430
|
+
response = r;
|
|
1431
|
+
break;
|
|
1432
|
+
}
|
|
1433
|
+
} catch {
|
|
1434
|
+
throw new ClassificationError("NETWORK_ERROR");
|
|
1327
1435
|
}
|
|
1328
|
-
}
|
|
1436
|
+
}
|
|
1437
|
+
if (response === null) {
|
|
1329
1438
|
throw new ClassificationError("NETWORK_ERROR");
|
|
1330
1439
|
}
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1440
|
+
const results = JSON.parse(response.data);
|
|
1441
|
+
for (let j = 0; j < batch.length; j++) {
|
|
1442
|
+
const isin = batch[j];
|
|
1443
|
+
const result = results[j];
|
|
1444
|
+
const product = isinToProduct.get(isin) ?? isin;
|
|
1445
|
+
if (!result || result.error || !result.data || result.data.length === 0) {
|
|
1446
|
+
warnings.push(
|
|
1447
|
+
`Unrecognized type for ${isin}: unknown. Please classify manually.`
|
|
1448
|
+
);
|
|
1449
|
+
newEntries[isin] = {
|
|
1450
|
+
product,
|
|
1451
|
+
assetClass: "Stock",
|
|
1452
|
+
bucketGain: "B",
|
|
1453
|
+
bucketLoss: "B",
|
|
1454
|
+
taxRate: 0,
|
|
1455
|
+
whiteListed: null,
|
|
1456
|
+
confirmedByUser: false,
|
|
1457
|
+
source: "user"
|
|
1458
|
+
};
|
|
1459
|
+
continue;
|
|
1460
|
+
}
|
|
1461
|
+
const st = result.data[0]?.securityType;
|
|
1462
|
+
const st2 = result.data[0]?.securityType2;
|
|
1463
|
+
const typeToUse = st && isKnownType(st) ? st : st2 && isKnownType(st2) ? st2 : st ?? st2 ?? "Unknown";
|
|
1464
|
+
newEntries[isin] = classifyByType(isin, typeToUse, product, warnings);
|
|
1355
1465
|
}
|
|
1356
|
-
const
|
|
1357
|
-
|
|
1358
|
-
const typeToUse = st && isKnownType(st) ? st : st2 && isKnownType(st2) ? st2 : st ?? st2 ?? "Unknown";
|
|
1359
|
-
newEntries[isin] = classifyByType(isin, typeToUse, product, warnings);
|
|
1466
|
+
const batchIndex = i / BATCH_SIZE;
|
|
1467
|
+
options?.onBatchProgress?.(batchIndex + 1, totalBatches);
|
|
1360
1468
|
}
|
|
1361
1469
|
}
|
|
1362
1470
|
const mergedMap = { ...newEntries, ...confirmed };
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1471
|
+
if (sidecarPath !== void 0) {
|
|
1472
|
+
const sidecarContent = JSON.stringify(
|
|
1473
|
+
{
|
|
1474
|
+
version: 1,
|
|
1475
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1476
|
+
classifications: mergedMap,
|
|
1477
|
+
warnings
|
|
1478
|
+
},
|
|
1479
|
+
null,
|
|
1480
|
+
2
|
|
1481
|
+
);
|
|
1482
|
+
try {
|
|
1483
|
+
fs3.writeFileSync(sidecarPath, sidecarContent, "utf-8");
|
|
1484
|
+
} catch {
|
|
1485
|
+
throw new ClassificationError("WRITE_ERROR");
|
|
1486
|
+
}
|
|
1377
1487
|
}
|
|
1378
1488
|
return mergedMap;
|
|
1379
1489
|
}
|