bulltrackers-module 1.0.777 → 1.0.779
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/functions/alert-system/helpers/alert_helpers.js +114 -90
- package/functions/alert-system/helpers/alert_manifest_loader.js +88 -99
- package/functions/alert-system/index.js +81 -138
- package/functions/alert-system/tests/stage1-alert-manifest.test.js +94 -0
- package/functions/alert-system/tests/stage2-alert-metadata.test.js +93 -0
- package/functions/alert-system/tests/stage3-alert-handler.test.js +79 -0
- package/functions/api-v2/helpers/data-fetchers/firestore.js +613 -478
- package/functions/api-v2/routes/popular_investors.js +7 -7
- package/functions/api-v2/routes/profile.js +2 -1
- package/functions/api-v2/tests/stage4-profile-paths.test.js +52 -0
- package/functions/api-v2/tests/stage5-aum-bigquery.test.js +81 -0
- package/functions/api-v2/tests/stage7-pi-page-views.test.js +55 -0
- package/functions/api-v2/tests/stage8-watchlist-membership.test.js +49 -0
- package/functions/api-v2/tests/stage9-user-alert-settings.test.js +81 -0
- package/functions/computation-system-v2/computations/BehavioralAnomaly.js +104 -81
- package/functions/computation-system-v2/computations/NewSectorExposure.js +7 -7
- package/functions/computation-system-v2/computations/NewSocialPost.js +6 -6
- package/functions/computation-system-v2/computations/PositionInvestedIncrease.js +11 -11
- package/functions/computation-system-v2/computations/SignedInUserPIProfileMetrics.js +1 -1
- package/functions/computation-system-v2/config/bulltrackers.config.js +8 -0
- package/functions/computation-system-v2/framework/core/Manifest.js +1 -0
- package/functions/computation-system-v2/handlers/scheduler.js +15 -24
- package/functions/core/utils/bigquery_utils.js +32 -0
- package/package.json +1 -1
|
@@ -1,81 +1,63 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @fileoverview Dynamic Alert Manifest Loader
|
|
3
|
-
* Loads alert types from computation
|
|
3
|
+
* Loads alert types from V2 computation config (getConfig()) instead of legacy package.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
const path = require('path');
|
|
7
|
+
|
|
6
8
|
/**
|
|
7
|
-
*
|
|
8
|
-
* @
|
|
9
|
-
* @returns {Array<Function>} Flat array of computation classes
|
|
9
|
+
* Load V2 computation config (same source as scheduler).
|
|
10
|
+
* @returns {Object} Config with computations array
|
|
10
11
|
*/
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
result.push(obj[key]);
|
|
16
|
-
} else if (typeof obj[key] === 'object' && obj[key] !== null) {
|
|
17
|
-
result = result.concat(flattenCalculations(obj[key]));
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
return result;
|
|
21
|
-
};
|
|
12
|
+
function getV2Config() {
|
|
13
|
+
const configPath = path.join(__dirname, '../../computation-system-v2/config/bulltrackers.config.js');
|
|
14
|
+
return require(configPath);
|
|
15
|
+
}
|
|
22
16
|
|
|
23
17
|
/**
|
|
24
|
-
* Load all alert computations from
|
|
18
|
+
* Load all alert computations from V2 config
|
|
25
19
|
* @param {Object} logger - Logger instance
|
|
20
|
+
* @param {Object} [injectedConfig] - Optional config with computations array (for tests)
|
|
26
21
|
* @returns {Promise<Array>} Array of alert type objects with metadata
|
|
27
22
|
*/
|
|
28
|
-
async function loadAlertTypesFromManifest(logger) {
|
|
23
|
+
async function loadAlertTypesFromManifest(logger, injectedConfig = null) {
|
|
29
24
|
try {
|
|
30
|
-
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
25
|
+
const v2Config = injectedConfig || getV2Config();
|
|
26
|
+
const computations = v2Config.computations || [];
|
|
27
|
+
|
|
34
28
|
const alertTypes = [];
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
if (!CalcClass || typeof CalcClass.getMetadata !== 'function') {
|
|
39
|
-
continue;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
const metadata = CalcClass.getMetadata();
|
|
43
|
-
|
|
44
|
-
// Check if this is an alert computation
|
|
45
|
-
if (metadata.isAlertComputation !== true) {
|
|
29
|
+
|
|
30
|
+
for (const ComputationClass of computations) {
|
|
31
|
+
if (!ComputationClass || typeof ComputationClass.getConfig !== 'function') {
|
|
46
32
|
continue;
|
|
47
33
|
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
if (!
|
|
51
|
-
logger?.log('WARN', `[AlertManifestLoader] Computation ${metadata.name} has isAlertComputation=true but no alert metadata`);
|
|
34
|
+
|
|
35
|
+
const config = ComputationClass.getConfig();
|
|
36
|
+
if (!config || !config.alert) {
|
|
52
37
|
continue;
|
|
53
38
|
}
|
|
54
|
-
|
|
55
|
-
|
|
39
|
+
|
|
40
|
+
const alert = config.alert;
|
|
56
41
|
const alertType = {
|
|
57
|
-
id:
|
|
58
|
-
name:
|
|
59
|
-
description:
|
|
60
|
-
computationName:
|
|
61
|
-
category:
|
|
62
|
-
messageTemplate:
|
|
63
|
-
severity:
|
|
64
|
-
configKey:
|
|
65
|
-
isTest:
|
|
42
|
+
id: alert.id,
|
|
43
|
+
name: alert.frontendName,
|
|
44
|
+
description: alert.description || '',
|
|
45
|
+
computationName: config.name,
|
|
46
|
+
category: config.category || 'alerts',
|
|
47
|
+
messageTemplate: alert.messageTemplate || '',
|
|
48
|
+
severity: alert.severity || 'medium',
|
|
49
|
+
configKey: alert.configKey,
|
|
50
|
+
isTest: config.isTest === true,
|
|
66
51
|
enabled: true,
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
dynamicConfig: metadata.alert.dynamicConfig || null
|
|
52
|
+
isDynamic: alert.isDynamic || false,
|
|
53
|
+
dynamicConfig: alert.dynamicConfig || null
|
|
70
54
|
};
|
|
71
|
-
|
|
55
|
+
|
|
72
56
|
alertTypes.push(alertType);
|
|
73
|
-
|
|
74
|
-
logger?.log('DEBUG', `[AlertManifestLoader] Loaded alert type: ${alertType.id} from ${metadata.name} (isDynamic: ${alertType.isDynamic})`);
|
|
57
|
+
logger?.log('DEBUG', `[AlertManifestLoader] Loaded alert type: ${alertType.id} from ${config.name} (isDynamic: ${alertType.isDynamic})`);
|
|
75
58
|
}
|
|
76
|
-
|
|
77
|
-
logger?.log('INFO', `[AlertManifestLoader] Successfully loaded ${alertTypes.length} alert types from
|
|
78
|
-
|
|
59
|
+
|
|
60
|
+
logger?.log('INFO', `[AlertManifestLoader] Successfully loaded ${alertTypes.length} alert types from V2 config`);
|
|
79
61
|
return alertTypes;
|
|
80
62
|
} catch (error) {
|
|
81
63
|
logger?.log('ERROR', `[AlertManifestLoader] Failed to load alert types: ${error.message}`);
|
|
@@ -104,59 +86,66 @@ function isAlertComputation(alertTypes, computationName) {
|
|
|
104
86
|
}
|
|
105
87
|
|
|
106
88
|
/**
|
|
107
|
-
*
|
|
89
|
+
* Build resolved values for message placeholders from computation result using alert metadata.
|
|
90
|
+
* Uses alert.dynamicConfig.resultFields to map placeholder names to result keys (metadata-only; no hardcoded values).
|
|
91
|
+
* @param {Object} alertType - Alert type with messageTemplate and optional dynamicConfig.resultFields
|
|
92
|
+
* @param {Object} metadata - Computation result (e.g. per-PI result)
|
|
93
|
+
* @returns {Object} Resolved key-value map for placeholder substitution
|
|
94
|
+
*/
|
|
95
|
+
function resolvePlaceholdersFromMetadata(alertType, metadata = {}) {
|
|
96
|
+
const resolved = { ...metadata };
|
|
97
|
+
const resultFields = alertType.dynamicConfig?.resultFields;
|
|
98
|
+
if (resultFields && typeof resultFields === 'object') {
|
|
99
|
+
for (const [placeholderName, resultKey] of Object.entries(resultFields)) {
|
|
100
|
+
resolved[placeholderName] = metadata[resultKey];
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return resolved;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Generate alert message from template and metadata only (no hardcoded message strings).
|
|
108
|
+
* Uses alertType.messageTemplate and injects values from computation result; resultFields maps placeholders to result keys.
|
|
108
109
|
* @param {Object} alertType - Alert type object
|
|
109
110
|
* @param {string} piUsername - PI username
|
|
110
111
|
* @param {Object} metadata - Metadata from computation results
|
|
111
112
|
* @returns {string} Generated alert message
|
|
112
113
|
*/
|
|
113
114
|
function generateAlertMessage(alertType, piUsername, metadata = {}) {
|
|
114
|
-
let message = alertType.messageTemplate;
|
|
115
|
-
|
|
116
|
-
|
|
115
|
+
let message = alertType.messageTemplate || '';
|
|
116
|
+
const r = resolvePlaceholdersFromMetadata(alertType, metadata);
|
|
117
|
+
|
|
117
118
|
message = message.replace(/{piUsername}/g, piUsername || 'Unknown');
|
|
118
|
-
message = message.replace(/{count}/g,
|
|
119
|
-
message = message.replace(/{change}/g,
|
|
120
|
-
message = message.replace(/{previous}/g,
|
|
121
|
-
message = message.replace(/{current}/g,
|
|
122
|
-
message = message.replace(/{sectorName}/g,
|
|
123
|
-
message = message.replace(/{ticker}/g,
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
message = message.replace(/{
|
|
127
|
-
message = message.replace(/{
|
|
128
|
-
message = message.replace(/{
|
|
129
|
-
message = message.replace(/{
|
|
130
|
-
message = message.replace(/{
|
|
131
|
-
message = message.replace(/{
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
message = message.replace(/{
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
message = message.replace(/{primaryDriver}/g, metadata.primaryDriver || 'Unknown Factor');
|
|
139
|
-
message = message.replace(/{driverSignificance}/g, metadata.driverSignificance || 'N/A');
|
|
140
|
-
message = message.replace(/{anomalyScore}/g, metadata.anomalyScore || 'N/A');
|
|
141
|
-
|
|
142
|
-
// Handle positions list if available
|
|
143
|
-
if (metadata.positions && Array.isArray(metadata.positions) && metadata.positions.length > 0) {
|
|
144
|
-
const positionsList = metadata.positions
|
|
145
|
-
.slice(0, 3)
|
|
146
|
-
.map(p => p.ticker || p.instrumentId)
|
|
147
|
-
.join(', ');
|
|
119
|
+
message = message.replace(/{count}/g, r.count ?? r.positions?.length ?? r.moveCount ?? 0);
|
|
120
|
+
message = message.replace(/{change}/g, r.change ?? r.changePercent ?? r.diff ?? 'N/A');
|
|
121
|
+
message = message.replace(/{previous}/g, r.previous ?? r.previousValue ?? r.prev ?? r.previousRisk ?? 'N/A');
|
|
122
|
+
message = message.replace(/{current}/g, r.current ?? r.currentValue ?? r.curr ?? r.currentRisk ?? 'N/A');
|
|
123
|
+
message = message.replace(/{sectorName}/g, r.sectorName ?? (r.newExposures?.length ? r.newExposures.join(', ') : 'Unknown'));
|
|
124
|
+
message = message.replace(/{ticker}/g, r.ticker ?? r.symbol ?? 'Unknown');
|
|
125
|
+
message = message.replace(/{volatility}/g, r.volatility != null ? `${(r.volatility * 100).toFixed(1)}` : 'N/A');
|
|
126
|
+
message = message.replace(/{threshold}/g, r.threshold != null ? `${(r.threshold * 100).toFixed(0)}` : 'N/A');
|
|
127
|
+
message = message.replace(/{diff}/g, r.diff != null ? `${Number(r.diff).toFixed(1)}` : 'N/A');
|
|
128
|
+
message = message.replace(/{prev}/g, r.prev != null ? `${Number(r.prev).toFixed(1)}` : 'N/A');
|
|
129
|
+
message = message.replace(/{curr}/g, r.curr != null ? `${Number(r.curr).toFixed(1)}` : 'N/A');
|
|
130
|
+
message = message.replace(/{title}/g, r.title ?? 'New Update');
|
|
131
|
+
message = message.replace(/{status}/g, r.status ?? 'Unknown Status');
|
|
132
|
+
message = message.replace(/{timestamp}/g, r.timestamp ?? new Date().toISOString());
|
|
133
|
+
message = message.replace(/{primaryDriver}/g, r.primaryDriver ?? r.driver ?? 'Unknown Factor');
|
|
134
|
+
message = message.replace(/{driverSignificance}/g, r.driverSignificance ?? r.driverValue ?? 'N/A');
|
|
135
|
+
message = message.replace(/{anomalyScore}/g, r.anomalyScore ?? r.score ?? 'N/A');
|
|
136
|
+
|
|
137
|
+
if (r.positions?.length) {
|
|
138
|
+
const positionsList = r.positions.slice(0, 3).map(p => p.ticker || p.instrumentId).join(', ');
|
|
148
139
|
message = message.replace(/{positions}/g, positionsList);
|
|
149
140
|
}
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
message = message.replace(/{
|
|
155
|
-
message = message.replace(/{
|
|
156
|
-
message = message.replace(/{prev}/g, firstMove.prev ? `${firstMove.prev.toFixed(1)}` : 'N/A');
|
|
157
|
-
message = message.replace(/{curr}/g, firstMove.curr ? `${firstMove.curr.toFixed(1)}` : 'N/A');
|
|
141
|
+
if (r.moves?.length) {
|
|
142
|
+
const firstMove = r.moves[0];
|
|
143
|
+
message = message.replace(/{symbol}/g, firstMove.symbol ?? 'Unknown');
|
|
144
|
+
message = message.replace(/{diff}/g, firstMove.diff != null ? `${Number(firstMove.diff).toFixed(1)}` : 'N/A');
|
|
145
|
+
message = message.replace(/{prev}/g, firstMove.prev != null ? `${Number(firstMove.prev).toFixed(1)}` : 'N/A');
|
|
146
|
+
message = message.replace(/{curr}/g, firstMove.curr != null ? `${Number(firstMove.curr).toFixed(1)}` : 'N/A');
|
|
158
147
|
}
|
|
159
|
-
|
|
148
|
+
|
|
160
149
|
return message;
|
|
161
150
|
}
|
|
162
151
|
|