@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/dist/index.js
CHANGED
|
@@ -1000,7 +1000,7 @@ function classifyByType(isin, securityType, product, warnings) {
|
|
|
1000
1000
|
};
|
|
1001
1001
|
}
|
|
1002
1002
|
warnings.push(
|
|
1003
|
-
`Unrecognized type: ${securityType}. Please classify manually.`
|
|
1003
|
+
`Unrecognized type for ${isin}: ${securityType}. Please classify manually.`
|
|
1004
1004
|
);
|
|
1005
1005
|
return {
|
|
1006
1006
|
product,
|
|
@@ -1013,6 +1013,84 @@ function classifyByType(isin, securityType, product, warnings) {
|
|
|
1013
1013
|
source: "user"
|
|
1014
1014
|
};
|
|
1015
1015
|
}
|
|
1016
|
+
var ASSET_CLASS_DEFAULTS = {
|
|
1017
|
+
ETF: {
|
|
1018
|
+
assetClass: "ETF",
|
|
1019
|
+
bucketGain: "A",
|
|
1020
|
+
bucketLoss: "B",
|
|
1021
|
+
taxRate: 0.26,
|
|
1022
|
+
whiteListed: null
|
|
1023
|
+
},
|
|
1024
|
+
Stock: {
|
|
1025
|
+
assetClass: "Stock",
|
|
1026
|
+
bucketGain: "B",
|
|
1027
|
+
bucketLoss: "B",
|
|
1028
|
+
taxRate: 0,
|
|
1029
|
+
whiteListed: null
|
|
1030
|
+
},
|
|
1031
|
+
ETC: {
|
|
1032
|
+
assetClass: "ETC",
|
|
1033
|
+
bucketGain: "B",
|
|
1034
|
+
bucketLoss: "B",
|
|
1035
|
+
taxRate: 0,
|
|
1036
|
+
whiteListed: null
|
|
1037
|
+
},
|
|
1038
|
+
GovtBondWL: {
|
|
1039
|
+
assetClass: "GovtBondWL",
|
|
1040
|
+
bucketGain: "A",
|
|
1041
|
+
bucketLoss: "B",
|
|
1042
|
+
taxRate: 0.125,
|
|
1043
|
+
whiteListed: true
|
|
1044
|
+
},
|
|
1045
|
+
GovtBondOther: {
|
|
1046
|
+
assetClass: "GovtBondOther",
|
|
1047
|
+
bucketGain: "A",
|
|
1048
|
+
bucketLoss: "B",
|
|
1049
|
+
taxRate: 0.26,
|
|
1050
|
+
whiteListed: false
|
|
1051
|
+
},
|
|
1052
|
+
CorpBond: {
|
|
1053
|
+
assetClass: "CorpBond",
|
|
1054
|
+
bucketGain: "B",
|
|
1055
|
+
bucketLoss: "B",
|
|
1056
|
+
taxRate: 0,
|
|
1057
|
+
whiteListed: null
|
|
1058
|
+
},
|
|
1059
|
+
Derivative: {
|
|
1060
|
+
assetClass: "Derivative",
|
|
1061
|
+
bucketGain: "B",
|
|
1062
|
+
bucketLoss: "B",
|
|
1063
|
+
taxRate: 0,
|
|
1064
|
+
whiteListed: null
|
|
1065
|
+
},
|
|
1066
|
+
LeverageCert: {
|
|
1067
|
+
assetClass: "LeverageCert",
|
|
1068
|
+
bucketGain: "B",
|
|
1069
|
+
bucketLoss: "B",
|
|
1070
|
+
taxRate: 0,
|
|
1071
|
+
whiteListed: null
|
|
1072
|
+
},
|
|
1073
|
+
CapProtectedCert: {
|
|
1074
|
+
assetClass: "CapProtectedCert",
|
|
1075
|
+
bucketGain: "A",
|
|
1076
|
+
bucketLoss: "B",
|
|
1077
|
+
taxRate: 0.26,
|
|
1078
|
+
whiteListed: null
|
|
1079
|
+
}
|
|
1080
|
+
};
|
|
1081
|
+
function buildEntryFromAssetClass(assetClass, product) {
|
|
1082
|
+
const mapped = ASSET_CLASS_DEFAULTS[assetClass];
|
|
1083
|
+
return {
|
|
1084
|
+
product,
|
|
1085
|
+
assetClass: mapped.assetClass,
|
|
1086
|
+
bucketGain: mapped.bucketGain,
|
|
1087
|
+
bucketLoss: mapped.bucketLoss,
|
|
1088
|
+
taxRate: mapped.taxRate,
|
|
1089
|
+
whiteListed: mapped.whiteListed,
|
|
1090
|
+
confirmedByUser: true,
|
|
1091
|
+
source: "user"
|
|
1092
|
+
};
|
|
1093
|
+
}
|
|
1016
1094
|
var Classifier = class {
|
|
1017
1095
|
interactive;
|
|
1018
1096
|
constructor(options) {
|
|
@@ -1034,7 +1112,7 @@ var Classifier = class {
|
|
|
1034
1112
|
}
|
|
1035
1113
|
return parsed.classifications;
|
|
1036
1114
|
}
|
|
1037
|
-
async classify(transactions, sidecarPath, _httpPost = httpsPost) {
|
|
1115
|
+
async classify(transactions, sidecarPath, options, _httpPost = httpsPost) {
|
|
1038
1116
|
const isinToProduct = /* @__PURE__ */ new Map();
|
|
1039
1117
|
for (const tx of transactions) {
|
|
1040
1118
|
if (tx.isin && !isinToProduct.has(tx.isin)) {
|
|
@@ -1042,13 +1120,21 @@ var Classifier = class {
|
|
|
1042
1120
|
}
|
|
1043
1121
|
}
|
|
1044
1122
|
const confirmed = {};
|
|
1045
|
-
if (
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1123
|
+
if (sidecarPath !== void 0) {
|
|
1124
|
+
if (fs2.existsSync(sidecarPath)) {
|
|
1125
|
+
const existingMap = await this.load(sidecarPath);
|
|
1126
|
+
for (const [isin, entry] of Object.entries(existingMap)) {
|
|
1127
|
+
if (entry.confirmedByUser) {
|
|
1128
|
+
confirmed[isin] = entry;
|
|
1129
|
+
}
|
|
1050
1130
|
}
|
|
1051
1131
|
}
|
|
1132
|
+
} else if (options?.existingClassification) {
|
|
1133
|
+
Object.assign(confirmed, options.existingClassification);
|
|
1134
|
+
}
|
|
1135
|
+
for (const [isin, assetClass] of Object.entries(options?.overrides ?? {})) {
|
|
1136
|
+
const product = isinToProduct.get(isin) ?? isin;
|
|
1137
|
+
confirmed[isin] = buildEntryFromAssetClass(assetClass, product);
|
|
1052
1138
|
}
|
|
1053
1139
|
const toProcess = [];
|
|
1054
1140
|
for (const isin of isinToProduct.keys()) {
|
|
@@ -1058,76 +1144,100 @@ var Classifier = class {
|
|
|
1058
1144
|
}
|
|
1059
1145
|
const warnings = [];
|
|
1060
1146
|
const newEntries = {};
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1147
|
+
if (options?.offline) {
|
|
1148
|
+
for (const isin of toProcess) {
|
|
1149
|
+
const product = isinToProduct.get(isin) ?? isin;
|
|
1150
|
+
warnings.push(
|
|
1151
|
+
`Unrecognized type for ${isin}: unknown. Please classify manually.`
|
|
1152
|
+
);
|
|
1153
|
+
newEntries[isin] = {
|
|
1154
|
+
product,
|
|
1155
|
+
assetClass: "Stock",
|
|
1156
|
+
bucketGain: "B",
|
|
1157
|
+
bucketLoss: "B",
|
|
1158
|
+
taxRate: 0,
|
|
1159
|
+
whiteListed: null,
|
|
1160
|
+
confirmedByUser: false,
|
|
1161
|
+
source: "user"
|
|
1162
|
+
};
|
|
1065
1163
|
}
|
|
1066
|
-
|
|
1067
|
-
const
|
|
1068
|
-
|
|
1069
|
-
)
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1164
|
+
} else {
|
|
1165
|
+
const BATCH_SIZE = 10;
|
|
1166
|
+
const totalBatches = Math.ceil(toProcess.length / BATCH_SIZE);
|
|
1167
|
+
for (let i = 0; i < toProcess.length; i += BATCH_SIZE) {
|
|
1168
|
+
if (i > 0) {
|
|
1169
|
+
await new Promise((resolve) => setTimeout(resolve, 6e3));
|
|
1170
|
+
}
|
|
1171
|
+
const batch = toProcess.slice(i, i + BATCH_SIZE);
|
|
1172
|
+
const requestBody = JSON.stringify(
|
|
1173
|
+
batch.map((isin) => ({ idType: "ID_ISIN", idValue: isin }))
|
|
1174
|
+
);
|
|
1175
|
+
let response = null;
|
|
1176
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
1177
|
+
try {
|
|
1178
|
+
const r = await _httpPost(
|
|
1179
|
+
"https://api.openfigi.com/v3/mapping",
|
|
1180
|
+
requestBody,
|
|
1181
|
+
1e4
|
|
1182
|
+
);
|
|
1183
|
+
if (r.status < 500) {
|
|
1184
|
+
response = r;
|
|
1185
|
+
break;
|
|
1186
|
+
}
|
|
1187
|
+
} catch {
|
|
1188
|
+
throw new ClassificationError("NETWORK_ERROR");
|
|
1081
1189
|
}
|
|
1082
|
-
}
|
|
1190
|
+
}
|
|
1191
|
+
if (response === null) {
|
|
1083
1192
|
throw new ClassificationError("NETWORK_ERROR");
|
|
1084
1193
|
}
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1194
|
+
const results = JSON.parse(response.data);
|
|
1195
|
+
for (let j = 0; j < batch.length; j++) {
|
|
1196
|
+
const isin = batch[j];
|
|
1197
|
+
const result = results[j];
|
|
1198
|
+
const product = isinToProduct.get(isin) ?? isin;
|
|
1199
|
+
if (!result || result.error || !result.data || result.data.length === 0) {
|
|
1200
|
+
warnings.push(
|
|
1201
|
+
`Unrecognized type for ${isin}: unknown. Please classify manually.`
|
|
1202
|
+
);
|
|
1203
|
+
newEntries[isin] = {
|
|
1204
|
+
product,
|
|
1205
|
+
assetClass: "Stock",
|
|
1206
|
+
bucketGain: "B",
|
|
1207
|
+
bucketLoss: "B",
|
|
1208
|
+
taxRate: 0,
|
|
1209
|
+
whiteListed: null,
|
|
1210
|
+
confirmedByUser: false,
|
|
1211
|
+
source: "user"
|
|
1212
|
+
};
|
|
1213
|
+
continue;
|
|
1214
|
+
}
|
|
1215
|
+
const st = result.data[0]?.securityType;
|
|
1216
|
+
const st2 = result.data[0]?.securityType2;
|
|
1217
|
+
const typeToUse = st && isKnownType(st) ? st : st2 && isKnownType(st2) ? st2 : st ?? st2 ?? "Unknown";
|
|
1218
|
+
newEntries[isin] = classifyByType(isin, typeToUse, product, warnings);
|
|
1109
1219
|
}
|
|
1110
|
-
const
|
|
1111
|
-
|
|
1112
|
-
const typeToUse = st && isKnownType(st) ? st : st2 && isKnownType(st2) ? st2 : st ?? st2 ?? "Unknown";
|
|
1113
|
-
newEntries[isin] = classifyByType(isin, typeToUse, product, warnings);
|
|
1220
|
+
const batchIndex = i / BATCH_SIZE;
|
|
1221
|
+
options?.onBatchProgress?.(batchIndex + 1, totalBatches);
|
|
1114
1222
|
}
|
|
1115
1223
|
}
|
|
1116
1224
|
const mergedMap = { ...newEntries, ...confirmed };
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1225
|
+
if (sidecarPath !== void 0) {
|
|
1226
|
+
const sidecarContent = JSON.stringify(
|
|
1227
|
+
{
|
|
1228
|
+
version: 1,
|
|
1229
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1230
|
+
classifications: mergedMap,
|
|
1231
|
+
warnings
|
|
1232
|
+
},
|
|
1233
|
+
null,
|
|
1234
|
+
2
|
|
1235
|
+
);
|
|
1236
|
+
try {
|
|
1237
|
+
fs2.writeFileSync(sidecarPath, sidecarContent, "utf-8");
|
|
1238
|
+
} catch {
|
|
1239
|
+
throw new ClassificationError("WRITE_ERROR");
|
|
1240
|
+
}
|
|
1131
1241
|
}
|
|
1132
1242
|
return mergedMap;
|
|
1133
1243
|
}
|