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.
Files changed (24) hide show
  1. package/functions/alert-system/helpers/alert_helpers.js +114 -90
  2. package/functions/alert-system/helpers/alert_manifest_loader.js +88 -99
  3. package/functions/alert-system/index.js +81 -138
  4. package/functions/alert-system/tests/stage1-alert-manifest.test.js +94 -0
  5. package/functions/alert-system/tests/stage2-alert-metadata.test.js +93 -0
  6. package/functions/alert-system/tests/stage3-alert-handler.test.js +79 -0
  7. package/functions/api-v2/helpers/data-fetchers/firestore.js +613 -478
  8. package/functions/api-v2/routes/popular_investors.js +7 -7
  9. package/functions/api-v2/routes/profile.js +2 -1
  10. package/functions/api-v2/tests/stage4-profile-paths.test.js +52 -0
  11. package/functions/api-v2/tests/stage5-aum-bigquery.test.js +81 -0
  12. package/functions/api-v2/tests/stage7-pi-page-views.test.js +55 -0
  13. package/functions/api-v2/tests/stage8-watchlist-membership.test.js +49 -0
  14. package/functions/api-v2/tests/stage9-user-alert-settings.test.js +81 -0
  15. package/functions/computation-system-v2/computations/BehavioralAnomaly.js +104 -81
  16. package/functions/computation-system-v2/computations/NewSectorExposure.js +7 -7
  17. package/functions/computation-system-v2/computations/NewSocialPost.js +6 -6
  18. package/functions/computation-system-v2/computations/PositionInvestedIncrease.js +11 -11
  19. package/functions/computation-system-v2/computations/SignedInUserPIProfileMetrics.js +1 -1
  20. package/functions/computation-system-v2/config/bulltrackers.config.js +8 -0
  21. package/functions/computation-system-v2/framework/core/Manifest.js +1 -0
  22. package/functions/computation-system-v2/handlers/scheduler.js +15 -24
  23. package/functions/core/utils/bigquery_utils.js +32 -0
  24. package/package.json +1 -1
@@ -1,81 +1,63 @@
1
1
  /**
2
2
  * @fileoverview Dynamic Alert Manifest Loader
3
- * Loads alert types from computation classes instead of hardcoded registry
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
- * Flatten nested calculations object (same logic as index.js)
8
- * @param {Object} obj - Nested calculations object
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
- const flattenCalculations = (obj) => {
12
- let result = [];
13
- for (const key in obj) {
14
- if (typeof obj[key] === 'function') {
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 calculations package
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
- // Import calculations the same way index.js does
31
- const { calculations } = require('aiden-shared-calculations-unified');
32
- const calculationsArray = flattenCalculations(calculations);
33
-
25
+ const v2Config = injectedConfig || getV2Config();
26
+ const computations = v2Config.computations || [];
27
+
34
28
  const alertTypes = [];
35
-
36
- // Iterate through all computation classes (same logic as index.js alert trigger registration)
37
- for (const CalcClass of calculationsArray) {
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
- // Check if alert metadata exists
50
- if (!metadata.alert) {
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
- // Build alert type object
39
+
40
+ const alert = config.alert;
56
41
  const alertType = {
57
- id: metadata.alert.id,
58
- name: metadata.alert.frontendName,
59
- description: metadata.alert.description,
60
- computationName: metadata.name,
61
- category: metadata.category || 'alerts',
62
- messageTemplate: metadata.alert.messageTemplate,
63
- severity: metadata.alert.severity || 'medium',
64
- configKey: metadata.alert.configKey,
65
- isTest: metadata.isTest === true,
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
- // [FIX] Extract dynamic alert configuration from metadata
68
- isDynamic: metadata.alert.isDynamic || false,
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 calculations`);
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
- * Generate alert message from template and metadata
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
- // Replace placeholders
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, metadata.count || metadata.positions?.length || metadata.moveCount || 0);
119
- message = message.replace(/{change}/g, metadata.change || metadata.changePercent || metadata.diff || 'N/A');
120
- message = message.replace(/{previous}/g, metadata.previous || metadata.previousValue || metadata.prev || metadata.previousRisk || 'N/A');
121
- message = message.replace(/{current}/g, metadata.current || metadata.currentValue || metadata.curr || metadata.currentRisk || 'N/A');
122
- message = message.replace(/{sectorName}/g, metadata.sectorName || (metadata.newExposures && metadata.newExposures.length > 0 ? metadata.newExposures.join(', ') : 'Unknown'));
123
- message = message.replace(/{ticker}/g, metadata.ticker || metadata.symbol || 'Unknown');
124
-
125
- // Format numeric values
126
- message = message.replace(/{volatility}/g, metadata.volatility ? `${(metadata.volatility * 100).toFixed(1)}` : 'N/A');
127
- message = message.replace(/{threshold}/g, metadata.threshold ? `${(metadata.threshold * 100).toFixed(0)}` : 'N/A');
128
- message = message.replace(/{diff}/g, metadata.diff ? `${metadata.diff.toFixed(1)}` : 'N/A');
129
- message = message.replace(/{prev}/g, metadata.prev ? `${metadata.prev.toFixed(1)}` : 'N/A');
130
- message = message.replace(/{curr}/g, metadata.curr ? `${metadata.curr.toFixed(1)}` : 'N/A');
131
- message = message.replace(/{title}/g, metadata.title || 'New Update');
132
-
133
- // Probe placeholders
134
- message = message.replace(/{status}/g, metadata.status || 'Unknown Status');
135
- message = message.replace(/{timestamp}/g, metadata.timestamp || new Date().toISOString());
136
-
137
- // Behavioral Anomaly placeholders
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
- // Handle moves array for PositionInvestedIncrease
152
- if (metadata.moves && Array.isArray(metadata.moves) && metadata.moves.length > 0) {
153
- const firstMove = metadata.moves[0];
154
- message = message.replace(/{symbol}/g, firstMove.symbol || 'Unknown');
155
- message = message.replace(/{diff}/g, firstMove.diff ? `${firstMove.diff.toFixed(1)}` : 'N/A');
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