pp-portfolio-classifier 0.0.1 → 0.0.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/package.json +2 -1
- package/src/index.ts +1 -0
- package/dist/classifier.js +0 -195
- package/dist/morningstar-api.js +0 -137
- package/dist/types.js +0 -2
- package/dist/xml-helper.js +0 -244
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pp-portfolio-classifier",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.2",
|
|
4
4
|
"description": "Automatic classification of Portfolio Performance securities using MorningStar Direct Web Services ",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"portfolio-performance",
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
"build": "tsc",
|
|
15
15
|
"dev": "ts-node src/index.ts",
|
|
16
16
|
"lint": "prettier --check ./src && eslint ./src && tsc --noEmit",
|
|
17
|
+
"publish": "yarn bluid && npm publish",
|
|
17
18
|
"start": "node dist/index.ts"
|
|
18
19
|
},
|
|
19
20
|
"dependencies": {
|
package/src/index.ts
CHANGED
package/dist/classifier.js
DELETED
|
@@ -1,195 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
-
};
|
|
5
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.Classifier = void 0;
|
|
7
|
-
const config_1 = __importDefault(require("config"));
|
|
8
|
-
const lodash_1 = require("lodash");
|
|
9
|
-
class Classifier {
|
|
10
|
-
constructor(xmlHandler, api) {
|
|
11
|
-
this.xmlHandler = xmlHandler;
|
|
12
|
-
this.api = api;
|
|
13
|
-
this.taxonomiesConfig = config_1.default.get("taxonomies");
|
|
14
|
-
this.globalMappings = config_1.default.has("mappings") ? config_1.default.get("mappings") : {};
|
|
15
|
-
}
|
|
16
|
-
getMapping(mappingOrKey) {
|
|
17
|
-
if (!mappingOrKey)
|
|
18
|
-
return {};
|
|
19
|
-
if (typeof mappingOrKey === "string") {
|
|
20
|
-
if (this.globalMappings[mappingOrKey]) {
|
|
21
|
-
return this.globalMappings[mappingOrKey];
|
|
22
|
-
}
|
|
23
|
-
console.warn(` [Config] Mapping key '${mappingOrKey}' not found in global mappings.`);
|
|
24
|
-
return {};
|
|
25
|
-
}
|
|
26
|
-
return mappingOrKey;
|
|
27
|
-
}
|
|
28
|
-
async classifySecurity(security) {
|
|
29
|
-
if (!security.isin && !security.isinOverride)
|
|
30
|
-
return;
|
|
31
|
-
if (security.ignoreTaxonomies === true)
|
|
32
|
-
return;
|
|
33
|
-
const securityInfo = await this.api.getSecurityData(security.isinOverride || security.isin);
|
|
34
|
-
if (!securityInfo) {
|
|
35
|
-
console.log(` > No data found for ${security.name}.`);
|
|
36
|
-
return;
|
|
37
|
-
}
|
|
38
|
-
console.log(` > Data retrieved for ${security.name}. Type: ${securityInfo.type}. Processing taxonomies...`);
|
|
39
|
-
if (securityInfo.type === "Stock") {
|
|
40
|
-
await this.classifyStock(security, securityInfo.secid, securityInfo.data);
|
|
41
|
-
}
|
|
42
|
-
else if (securityInfo.type === "Fund") {
|
|
43
|
-
await this.classifyFund(security, securityInfo.data);
|
|
44
|
-
}
|
|
45
|
-
else {
|
|
46
|
-
console.error(` > Unknown security type: ${securityInfo.type}! Skipping...`);
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
async classifyFund(security, data) {
|
|
50
|
-
for (const [taxonomyId, taxConfig] of Object.entries(this.taxonomiesConfig)) {
|
|
51
|
-
if (!taxConfig.active)
|
|
52
|
-
continue;
|
|
53
|
-
if (security.ignoreTaxonomies !== undefined &&
|
|
54
|
-
(security.ignoreTaxonomies === true || security.ignoreTaxonomies.includes(taxonomyId)))
|
|
55
|
-
continue;
|
|
56
|
-
console.log(` > ${taxonomyId}`);
|
|
57
|
-
// 1. Extract Data
|
|
58
|
-
const sourceData = (0, lodash_1.get)(data, taxConfig.sourcePath);
|
|
59
|
-
if (!sourceData) {
|
|
60
|
-
console.warn(` [${taxonomyId}] No data found at path '${taxConfig.sourcePath}'`);
|
|
61
|
-
continue;
|
|
62
|
-
}
|
|
63
|
-
if (!Array.isArray(sourceData)) {
|
|
64
|
-
console.warn(` [${taxonomyId}] Data at '${taxConfig.sourcePath}' is not an array.`);
|
|
65
|
-
continue;
|
|
66
|
-
}
|
|
67
|
-
// 2. Filter Data
|
|
68
|
-
// console.log(` [${taxonomyId}] Filtering ${sourceData.length} items...`);
|
|
69
|
-
let filteredData = sourceData;
|
|
70
|
-
if (taxConfig.filter) {
|
|
71
|
-
filteredData = (0, lodash_1.filter)(sourceData, taxConfig.filter);
|
|
72
|
-
}
|
|
73
|
-
if (filteredData.length === 0) {
|
|
74
|
-
console.log(` [${taxonomyId}] No items match the filter.`);
|
|
75
|
-
continue;
|
|
76
|
-
}
|
|
77
|
-
// 2.1 Extract BreakdownValues (Flattening)
|
|
78
|
-
// The data we want is usually nested in a 'BreakdownValues' array inside the filtered items
|
|
79
|
-
let itemsToProcess = [];
|
|
80
|
-
for (const group of filteredData) {
|
|
81
|
-
if (group.BreakdownValues && Array.isArray(group.BreakdownValues)) {
|
|
82
|
-
itemsToProcess.push(...group.BreakdownValues);
|
|
83
|
-
}
|
|
84
|
-
else {
|
|
85
|
-
itemsToProcess.push(group);
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
// 3. Map and Assign
|
|
89
|
-
// console.log(` [${taxonomyId}] Assigning ${itemsToProcess.length} items...`);
|
|
90
|
-
const assignments = [];
|
|
91
|
-
const mapping = this.getMapping(taxConfig.mapping);
|
|
92
|
-
for (const item of itemsToProcess) {
|
|
93
|
-
const key = item[taxConfig.keyField];
|
|
94
|
-
const value = parseFloat(item[taxConfig.valueField]);
|
|
95
|
-
if (key && value > 0) {
|
|
96
|
-
if (!Object.keys(mapping).length) {
|
|
97
|
-
assignments.push({ path: [key], weight: value * 100 });
|
|
98
|
-
}
|
|
99
|
-
else if (key in mapping) {
|
|
100
|
-
const targetClass = mapping[key];
|
|
101
|
-
if (targetClass) {
|
|
102
|
-
const path = Array.isArray(targetClass) ? targetClass : [targetClass];
|
|
103
|
-
assignments.push({ path, weight: value * 100 });
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
else {
|
|
107
|
-
console.log(` [${taxonomyId}] Unmapped key: '${key}' (Value: ${value})`);
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
this.xmlHandler.updateSecurityAssignments(taxConfig.name || taxonomyId, security.uuid, assignments);
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
async classifyStock(security, secid, data) {
|
|
115
|
-
if (!secid && !data) {
|
|
116
|
-
console.warn(` > Cannot classify stock ${security.name} without data.`);
|
|
117
|
-
return;
|
|
118
|
-
}
|
|
119
|
-
for (const [taxonomyId, taxConfig] of Object.entries(this.taxonomiesConfig)) {
|
|
120
|
-
if (!taxConfig.active)
|
|
121
|
-
continue;
|
|
122
|
-
if (security.ignoreTaxonomies !== undefined &&
|
|
123
|
-
(security.ignoreTaxonomies === true || security.ignoreTaxonomies.includes(taxonomyId)))
|
|
124
|
-
continue;
|
|
125
|
-
if (!taxConfig.stockConfig)
|
|
126
|
-
continue;
|
|
127
|
-
console.log(` > ${taxonomyId}`);
|
|
128
|
-
const stockConfig = taxConfig.stockConfig;
|
|
129
|
-
if (stockConfig.isSingleValue && stockConfig.value) {
|
|
130
|
-
// Case 1: Static value (e.g., Asset Type is always 'Stocks')
|
|
131
|
-
const path = Array.isArray(stockConfig.value) ? stockConfig.value : [stockConfig.value];
|
|
132
|
-
this.xmlHandler.updateSecurityAssignments(taxConfig.name || taxonomyId, security.uuid, [
|
|
133
|
-
{ path, weight: 10000 },
|
|
134
|
-
]); // 100% weight
|
|
135
|
-
}
|
|
136
|
-
else if (stockConfig.sourcePath) {
|
|
137
|
-
// Case 2: Value from Data (SAL or Initial)
|
|
138
|
-
let key;
|
|
139
|
-
if (stockConfig.salEndpoint) {
|
|
140
|
-
if (!secid) {
|
|
141
|
-
console.warn(` [${taxonomyId}] Cannot fetch SAL data without secid.`);
|
|
142
|
-
continue;
|
|
143
|
-
}
|
|
144
|
-
const salData = await this.api.getSalData(secid, stockConfig.salEndpoint);
|
|
145
|
-
if (!salData) {
|
|
146
|
-
console.warn(` [${taxonomyId}] No SAL data retrieved.`);
|
|
147
|
-
continue;
|
|
148
|
-
}
|
|
149
|
-
key = (0, lodash_1.get)(salData, stockConfig.sourcePath);
|
|
150
|
-
}
|
|
151
|
-
else if (stockConfig.viewId) {
|
|
152
|
-
const id = security.isinOverride || security.isin;
|
|
153
|
-
if (!id) {
|
|
154
|
-
console.warn(` [${taxonomyId}] Cannot fetch view data without ID.`);
|
|
155
|
-
continue;
|
|
156
|
-
}
|
|
157
|
-
const viewResult = await this.api.getSecurityData(id, stockConfig.viewId);
|
|
158
|
-
// console.debug(security, viewResult);
|
|
159
|
-
if (viewResult && viewResult.data) {
|
|
160
|
-
key = (0, lodash_1.get)(viewResult.data, stockConfig.sourcePath);
|
|
161
|
-
// console.debug(security.name, viewResult.data.QuantitativeFairValue, stockConfig.sourcePath, key);
|
|
162
|
-
}
|
|
163
|
-
else {
|
|
164
|
-
console.warn(` [${taxonomyId}] No data retrieved for view '${stockConfig.viewId}'.`);
|
|
165
|
-
continue;
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
else {
|
|
169
|
-
// Use data from initial request
|
|
170
|
-
key = (0, lodash_1.get)(data, stockConfig.sourcePath);
|
|
171
|
-
}
|
|
172
|
-
const mapping = this.getMapping(stockConfig.mapping);
|
|
173
|
-
if (key) {
|
|
174
|
-
// console.debug(security.name, key, mapping[key]);
|
|
175
|
-
if (!Object.keys(mapping).length) {
|
|
176
|
-
this.xmlHandler.updateSecurityAssignments(taxConfig.name || taxonomyId, security.uuid, [
|
|
177
|
-
{ path: [key], weight: 10000 },
|
|
178
|
-
]); // 100% weight
|
|
179
|
-
}
|
|
180
|
-
else if (key in mapping) {
|
|
181
|
-
const targetClass = mapping[key];
|
|
182
|
-
const path = Array.isArray(targetClass) ? targetClass : [targetClass];
|
|
183
|
-
this.xmlHandler.updateSecurityAssignments(taxConfig.name || taxonomyId, security.uuid, [
|
|
184
|
-
{ path, weight: 10000 },
|
|
185
|
-
]); // 100% weight
|
|
186
|
-
}
|
|
187
|
-
else {
|
|
188
|
-
console.warn(` [${taxonomyId}] Unmapped stock value '${key}'.`);
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
exports.Classifier = Classifier;
|
package/dist/morningstar-api.js
DELETED
|
@@ -1,137 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
-
};
|
|
5
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.MorningstarAPI = void 0;
|
|
7
|
-
const axios_1 = __importDefault(require("axios"));
|
|
8
|
-
const config_1 = __importDefault(require("config"));
|
|
9
|
-
class MorningstarAPI {
|
|
10
|
-
constructor() {
|
|
11
|
-
this.bearerToken = "";
|
|
12
|
-
this.domain = config_1.default.get("morningstar.domain");
|
|
13
|
-
this.baseUrl = config_1.default.get("morningstar.baseUrl");
|
|
14
|
-
this.salBaseUrl = config_1.default.get("morningstar.salBaseUrl");
|
|
15
|
-
this.viewId = config_1.default.get("morningstar.viewId");
|
|
16
|
-
}
|
|
17
|
-
async getBearerToken() {
|
|
18
|
-
if (this.bearerToken)
|
|
19
|
-
return this.bearerToken;
|
|
20
|
-
console.log("Fetching Morningstar Bearer Token...");
|
|
21
|
-
const url = `https://www.morningstar.${this.domain}/Common/funds/snapshot/PortfolioSAL.aspx`;
|
|
22
|
-
try {
|
|
23
|
-
const response = await axios_1.default.get(url);
|
|
24
|
-
const tokenRegex = /const maasToken \=\s\"(.+)\"/;
|
|
25
|
-
const match = response.data.match(tokenRegex);
|
|
26
|
-
if (match && match[1]) {
|
|
27
|
-
this.bearerToken = match[1];
|
|
28
|
-
// console.log("Got Bearer Token:", this.bearerToken);
|
|
29
|
-
return this.bearerToken;
|
|
30
|
-
}
|
|
31
|
-
else {
|
|
32
|
-
throw new Error("Token regex failed");
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
catch (error) {
|
|
36
|
-
console.error("Failed to get Bearer Token", error);
|
|
37
|
-
throw error;
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
/**
|
|
41
|
-
* Récupère des données complémentaires via une vue spécifique (ex: 'snapshot').
|
|
42
|
-
* Peut être utilisé avec un ISIN ou un SecId.
|
|
43
|
-
*/
|
|
44
|
-
async getSecurityData(id, viewId, idType = "ISIN") {
|
|
45
|
-
const token = await this.getBearerToken();
|
|
46
|
-
const url = `${this.baseUrl}/securities/${id}`;
|
|
47
|
-
const params = {
|
|
48
|
-
idtype: idType,
|
|
49
|
-
viewid: viewId || this.viewId,
|
|
50
|
-
currencyId: "EUR",
|
|
51
|
-
responseViewFormat: "json",
|
|
52
|
-
languageId: "en-UK",
|
|
53
|
-
};
|
|
54
|
-
const headers = { Authorization: `Bearer ${token}`, accept: "*/*" };
|
|
55
|
-
try {
|
|
56
|
-
const response = await axios_1.default.get(url, { params, headers });
|
|
57
|
-
if (response.data && response.data.length > 0) {
|
|
58
|
-
const securityInfo = response.data[0];
|
|
59
|
-
// if (isin == "NL0011585146") console.debug(isin, securityInfo);
|
|
60
|
-
return {
|
|
61
|
-
type: securityInfo.Type,
|
|
62
|
-
data: securityInfo,
|
|
63
|
-
secid: securityInfo.Id || id, // Fallback to ISIN if SecId is missing (SAL API often accepts ISIN)
|
|
64
|
-
};
|
|
65
|
-
}
|
|
66
|
-
// If response is empty (200 OK but no data), try fallback
|
|
67
|
-
console.log(` > ecint API returned empty for ${id}, trying website search fallback...`);
|
|
68
|
-
return this.findSecidFromWebsite(id);
|
|
69
|
-
}
|
|
70
|
-
catch (error) {
|
|
71
|
-
if (error.response?.status === 401) {
|
|
72
|
-
// This can happen for stocks not covered by this endpoint. Try the fallback.
|
|
73
|
-
console.log(` > ecint API failed for ${id}, trying website search fallback...`);
|
|
74
|
-
return this.findSecidFromWebsite(id);
|
|
75
|
-
}
|
|
76
|
-
console.warn(` Warning: Could not fetch data for ${id} from ecint API. Error: ${error.message}`);
|
|
77
|
-
return null;
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
/**
|
|
81
|
-
* Récupère des données détaillées pour les actions via l'API SAL.
|
|
82
|
-
*/
|
|
83
|
-
async getSalData(secid, endpoint) {
|
|
84
|
-
const token = await this.getBearerToken();
|
|
85
|
-
let url = `${this.salBaseUrl}/${endpoint}`;
|
|
86
|
-
let params = {
|
|
87
|
-
languageId: "en-UK",
|
|
88
|
-
locale: "en",
|
|
89
|
-
};
|
|
90
|
-
const headers = { Authorization: `Bearer ${token}`, accept: "*/*" };
|
|
91
|
-
if (endpoint === "stock/equityOverview") {
|
|
92
|
-
url += `/${secid}/data`;
|
|
93
|
-
params.version = "4.65.0";
|
|
94
|
-
}
|
|
95
|
-
else {
|
|
96
|
-
// companyProfile
|
|
97
|
-
url += `/${secid}`;
|
|
98
|
-
}
|
|
99
|
-
try {
|
|
100
|
-
const response = await axios_1.default.get(url, { params, headers });
|
|
101
|
-
return response.data;
|
|
102
|
-
}
|
|
103
|
-
catch (error) {
|
|
104
|
-
console.warn(` Warning: Could not fetch SAL data for secid ${secid} from endpoint ${endpoint}.`);
|
|
105
|
-
console.debug(url, params);
|
|
106
|
-
return null;
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
/**
|
|
110
|
-
* Fallback: Trouve le SecId d'un titre en cherchant sur le site de Morningstar.
|
|
111
|
-
* C'est utile pour les actions qui ne sont pas dans l'API ecint.
|
|
112
|
-
*/
|
|
113
|
-
async findSecidFromWebsite(isin) {
|
|
114
|
-
const url = `https://global.morningstar.com/api/v1/${this.domain}/search/securities`;
|
|
115
|
-
const params = { query: `((isin ~= "${isin}"))` };
|
|
116
|
-
const headers = { "user-agent": "Mozilla/5.0" };
|
|
117
|
-
try {
|
|
118
|
-
const response = await axios_1.default.get(url, { params, headers });
|
|
119
|
-
const results = response.data?.results;
|
|
120
|
-
if (results && results.length > 0) {
|
|
121
|
-
const secid = results[0].securityID;
|
|
122
|
-
const universe = results[0].universe;
|
|
123
|
-
const type = universe === "EQ" ? "Stock" : "Unknown";
|
|
124
|
-
if (type === "Stock") {
|
|
125
|
-
console.log(` > Found secid '${secid}' for stock ${isin} via fallback.`);
|
|
126
|
-
return { type: "Stock", data: null, secid: secid };
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
return null;
|
|
130
|
-
}
|
|
131
|
-
catch (error) {
|
|
132
|
-
console.warn(` Warning: Fallback search for ${isin} failed.`);
|
|
133
|
-
return null;
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
exports.MorningstarAPI = MorningstarAPI;
|
package/dist/types.js
DELETED
package/dist/xml-helper.js
DELETED
|
@@ -1,244 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
-
if (k2 === undefined) k2 = k;
|
|
4
|
-
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
-
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
-
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
-
}
|
|
8
|
-
Object.defineProperty(o, k2, desc);
|
|
9
|
-
}) : (function(o, m, k, k2) {
|
|
10
|
-
if (k2 === undefined) k2 = k;
|
|
11
|
-
o[k2] = m[k];
|
|
12
|
-
}));
|
|
13
|
-
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
-
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
-
}) : function(o, v) {
|
|
16
|
-
o["default"] = v;
|
|
17
|
-
});
|
|
18
|
-
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
-
var ownKeys = function(o) {
|
|
20
|
-
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
-
var ar = [];
|
|
22
|
-
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
-
return ar;
|
|
24
|
-
};
|
|
25
|
-
return ownKeys(o);
|
|
26
|
-
};
|
|
27
|
-
return function (mod) {
|
|
28
|
-
if (mod && mod.__esModule) return mod;
|
|
29
|
-
var result = {};
|
|
30
|
-
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
-
__setModuleDefault(result, mod);
|
|
32
|
-
return result;
|
|
33
|
-
};
|
|
34
|
-
})();
|
|
35
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.XMLHandler = void 0;
|
|
37
|
-
const crypto_1 = require("crypto");
|
|
38
|
-
const fast_xml_parser_1 = require("fast-xml-parser");
|
|
39
|
-
const fs = __importStar(require("fs"));
|
|
40
|
-
const options = {
|
|
41
|
-
ignoreAttributes: false,
|
|
42
|
-
attributeNamePrefix: "@_",
|
|
43
|
-
format: true,
|
|
44
|
-
indentBy: " ",
|
|
45
|
-
suppressEmptyNode: true,
|
|
46
|
-
isArray: (name, jpath, isLeafNode, isAttribute) => {
|
|
47
|
-
// Force ces éléments à être des tableaux même s'il n'y en a qu'un seul
|
|
48
|
-
const arrayTags = [
|
|
49
|
-
"securities.security",
|
|
50
|
-
"taxonomies.taxonomy",
|
|
51
|
-
"assignments.assignment",
|
|
52
|
-
"children.classification",
|
|
53
|
-
];
|
|
54
|
-
if (arrayTags.some((tag) => jpath.endsWith(tag)))
|
|
55
|
-
return true;
|
|
56
|
-
return false;
|
|
57
|
-
},
|
|
58
|
-
};
|
|
59
|
-
class XMLHandler {
|
|
60
|
-
constructor() {
|
|
61
|
-
this.parser = new fast_xml_parser_1.XMLParser(options);
|
|
62
|
-
this.builder = new fast_xml_parser_1.XMLBuilder(options);
|
|
63
|
-
}
|
|
64
|
-
load(filepath) {
|
|
65
|
-
console.log(`Loading XML file: ${filepath}`);
|
|
66
|
-
const fileContent = fs.readFileSync(filepath, "utf-8");
|
|
67
|
-
this.xmlData = this.parser.parse(fileContent);
|
|
68
|
-
}
|
|
69
|
-
save(filepath) {
|
|
70
|
-
console.log(`Saving XML file to: ${filepath}`);
|
|
71
|
-
const xmlContent = this.builder.build(this.xmlData);
|
|
72
|
-
// Hack pour ajouter l'en-tête XML standard si manquant (PP aime bien l'encodage UTF-8 explicite)
|
|
73
|
-
const finalXml = `<?xml version="1.0" encoding="UTF-8"?>\n${xmlContent}`;
|
|
74
|
-
fs.writeFileSync(filepath, finalXml, "utf-8");
|
|
75
|
-
}
|
|
76
|
-
getSecurities() {
|
|
77
|
-
if (!this.xmlData?.client?.securities?.security)
|
|
78
|
-
return [];
|
|
79
|
-
return this.xmlData.client.securities.security.map((sec) => {
|
|
80
|
-
const security = {
|
|
81
|
-
uuid: sec.uuid,
|
|
82
|
-
name: sec.name,
|
|
83
|
-
isin: sec.isin,
|
|
84
|
-
note: sec.note,
|
|
85
|
-
isRetired: sec.isRetired,
|
|
86
|
-
isinOverride: undefined,
|
|
87
|
-
ignoreTaxonomies: undefined,
|
|
88
|
-
};
|
|
89
|
-
if (sec.note) {
|
|
90
|
-
const isinMatch = sec.note.match(/#PPC:\[ISIN2=([A-Z0-9]{12})\]/);
|
|
91
|
-
if (isinMatch)
|
|
92
|
-
security.isinOverride = isinMatch[1];
|
|
93
|
-
const ignoreMatch = sec.note.match(/#PPC:\[ignore(?:=([^\]]+))?\]/);
|
|
94
|
-
if (ignoreMatch) {
|
|
95
|
-
security.ignoreTaxonomies = ignoreMatch[1] ? ignoreMatch[1].split(",").map((s) => s.trim()) : true;
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
// console.debug(security);
|
|
99
|
-
return security;
|
|
100
|
-
});
|
|
101
|
-
}
|
|
102
|
-
// Méthode utilitaire pour vérifier si le XML est chargé
|
|
103
|
-
isLoaded() {
|
|
104
|
-
return !!this.xmlData;
|
|
105
|
-
}
|
|
106
|
-
/**
|
|
107
|
-
* Finds or creates a taxonomy by name
|
|
108
|
-
*/
|
|
109
|
-
getTaxonomy(name) {
|
|
110
|
-
if (!this.xmlData.client.taxonomies) {
|
|
111
|
-
this.xmlData.client.taxonomies = { taxonomy: [] };
|
|
112
|
-
}
|
|
113
|
-
let tax = this.xmlData.client.taxonomies.taxonomy.find((t) => t.name === name);
|
|
114
|
-
if (!tax) {
|
|
115
|
-
tax = {
|
|
116
|
-
id: (0, crypto_1.randomUUID)(),
|
|
117
|
-
name: name,
|
|
118
|
-
root: {
|
|
119
|
-
id: (0, crypto_1.randomUUID)(),
|
|
120
|
-
name: name,
|
|
121
|
-
color: "#89afee",
|
|
122
|
-
children: { classification: [] },
|
|
123
|
-
assignments: { assignment: [] },
|
|
124
|
-
weight: 10000,
|
|
125
|
-
rank: 0,
|
|
126
|
-
},
|
|
127
|
-
};
|
|
128
|
-
this.xmlData.client.taxonomies.taxonomy.push(tax);
|
|
129
|
-
}
|
|
130
|
-
return tax;
|
|
131
|
-
}
|
|
132
|
-
/**
|
|
133
|
-
* Assigns a security to a specific classification path within a taxonomy
|
|
134
|
-
*/
|
|
135
|
-
assignSecurityToTaxonomy(taxonomyName, path, securityUuid, weight) {
|
|
136
|
-
const tax = this.getTaxonomy(taxonomyName);
|
|
137
|
-
// 1. Remove existing assignments for this security in this taxonomy to avoid duplicates
|
|
138
|
-
const securityIndex = this.getSecurityIndex(securityUuid);
|
|
139
|
-
if (securityIndex === -1) {
|
|
140
|
-
console.error(` [XML] Error: Security UUID ${securityUuid} not found in XML structure.`);
|
|
141
|
-
return;
|
|
142
|
-
}
|
|
143
|
-
// console.log(` [XML] Updating '${taxonomyName}' -> ${path.join(' > ')}: ${(weight/100).toFixed(2)}%`);
|
|
144
|
-
// 2. Find or create the target classification node
|
|
145
|
-
const targetNode = this.ensureClassificationPath(tax.root, path);
|
|
146
|
-
// 3. Add the new assignment
|
|
147
|
-
if (!targetNode.assignments)
|
|
148
|
-
targetNode.assignments = { assignment: [] };
|
|
149
|
-
if (!targetNode.assignments.assignment)
|
|
150
|
-
targetNode.assignments.assignment = [];
|
|
151
|
-
// Calculate depth for relative reference (Root is depth 0)
|
|
152
|
-
const depth = path.length;
|
|
153
|
-
const securityRef = this.getSecurityReference(securityIndex, depth);
|
|
154
|
-
// Check if assignment already exists to avoid duplicates in the same node
|
|
155
|
-
const existingAssignmentIndex = targetNode.assignments.assignment.findIndex((a) => a.investmentVehicle && a.investmentVehicle["@_reference"] === securityRef);
|
|
156
|
-
const newAssignment = {
|
|
157
|
-
investmentVehicle: {
|
|
158
|
-
"@_class": "security",
|
|
159
|
-
"@_reference": securityRef,
|
|
160
|
-
},
|
|
161
|
-
weight: Math.round(weight),
|
|
162
|
-
rank: 0,
|
|
163
|
-
};
|
|
164
|
-
if (existingAssignmentIndex !== -1) {
|
|
165
|
-
targetNode.assignments.assignment[existingAssignmentIndex] = newAssignment;
|
|
166
|
-
}
|
|
167
|
-
else {
|
|
168
|
-
targetNode.assignments.assignment.push(newAssignment);
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
/**
|
|
172
|
-
* Removes all assignments for a specific security in a given taxonomy.
|
|
173
|
-
*/
|
|
174
|
-
removeSecurityFromTaxonomy(taxonomyName, securityUuid) {
|
|
175
|
-
const tax = this.getTaxonomy(taxonomyName);
|
|
176
|
-
const securityIndex = this.getSecurityIndex(securityUuid);
|
|
177
|
-
if (securityIndex === -1)
|
|
178
|
-
return;
|
|
179
|
-
const removeRecursive = (node, depth) => {
|
|
180
|
-
if (node.assignments && node.assignments.assignment) {
|
|
181
|
-
const securityRef = this.getSecurityReference(securityIndex, depth);
|
|
182
|
-
node.assignments.assignment = node.assignments.assignment.filter((a) => !a.investmentVehicle || a.investmentVehicle["@_reference"] !== securityRef);
|
|
183
|
-
}
|
|
184
|
-
if (node.children && node.children.classification) {
|
|
185
|
-
node.children.classification.forEach((child) => removeRecursive(child, depth + 1));
|
|
186
|
-
}
|
|
187
|
-
};
|
|
188
|
-
removeRecursive(tax.root, 0);
|
|
189
|
-
}
|
|
190
|
-
/**
|
|
191
|
-
* Updates assignments for a security in a taxonomy.
|
|
192
|
-
* If assignments are provided, it clears existing ones first .
|
|
193
|
-
* If no assignments are provided, it leaves existing ones untouched.
|
|
194
|
-
*/
|
|
195
|
-
updateSecurityAssignments(taxonomyName, securityUuid, assignments) {
|
|
196
|
-
if (!assignments || assignments.length === 0)
|
|
197
|
-
return;
|
|
198
|
-
this.removeSecurityFromTaxonomy(taxonomyName, securityUuid);
|
|
199
|
-
for (const assignment of assignments) {
|
|
200
|
-
this.assignSecurityToTaxonomy(taxonomyName, assignment.path, securityUuid, assignment.weight);
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
ensureClassificationPath(root, path) {
|
|
204
|
-
let current = root;
|
|
205
|
-
for (const segment of path) {
|
|
206
|
-
if (!current.children)
|
|
207
|
-
current.children = { classification: [] };
|
|
208
|
-
if (!current.children.classification)
|
|
209
|
-
current.children.classification = [];
|
|
210
|
-
let nextNode = current.children.classification.find((c) => c.name === segment);
|
|
211
|
-
if (!nextNode) {
|
|
212
|
-
nextNode = {
|
|
213
|
-
id: (0, crypto_1.randomUUID)(),
|
|
214
|
-
name: segment,
|
|
215
|
-
color: "#89afee",
|
|
216
|
-
parent: { "@_reference": `../../..` },
|
|
217
|
-
children: { classification: [] },
|
|
218
|
-
assignments: { assignment: [] },
|
|
219
|
-
weight: 0,
|
|
220
|
-
rank: 0,
|
|
221
|
-
};
|
|
222
|
-
current.children.classification.push(nextNode);
|
|
223
|
-
}
|
|
224
|
-
current = nextNode;
|
|
225
|
-
}
|
|
226
|
-
return current;
|
|
227
|
-
}
|
|
228
|
-
getSecurityIndex(uuid) {
|
|
229
|
-
if (!this.xmlData?.client?.securities?.security)
|
|
230
|
-
return -1;
|
|
231
|
-
return this.xmlData.client.securities.security.findIndex((s) => s.uuid === uuid);
|
|
232
|
-
}
|
|
233
|
-
getSecurityReference(index, depth) {
|
|
234
|
-
// Base depth calculation:
|
|
235
|
-
// Taxonomy(Root) -> Children -> Classif -> Assignments -> Assignment -> InvVehicle
|
|
236
|
-
// 6 levels up to Client for depth 0 (direct child of root)
|
|
237
|
-
// For each nesting level, add 2 levels (Children + Classification)
|
|
238
|
-
const levelsUp = 6 + depth * 2;
|
|
239
|
-
const prefix = "../".repeat(levelsUp);
|
|
240
|
-
const suffix = index === 0 ? "securities/security" : `securities/security[${index + 1}]`;
|
|
241
|
-
return `${prefix}${suffix}`;
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
exports.XMLHandler = XMLHandler;
|