bulltrackers-module 1.0.683 → 1.0.685

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.
@@ -7,7 +7,8 @@ const { FieldValue } = require('@google-cloud/firestore');
7
7
  const zlib = require('zlib');
8
8
  const { Storage } = require('@google-cloud/storage');
9
9
  const { generateAlertMessage } = require('./alert_manifest_loader');
10
- // [UPDATED] Now uses dynamic manifest loading instead of hardcoded registry
10
+ const { evaluateDynamicConditions } = require('./dynamic_evaluator');
11
+ // [UPDATED] Now uses dynamic manifest loading and condition evaluation
11
12
 
12
13
  const storage = new Storage(); // Singleton GCS Client
13
14
 
@@ -97,6 +98,36 @@ async function processAlertForPI(db, logger, piCid, alertType, computationMetada
97
98
  // Continue anyway - fail open for important alerts
98
99
  }
99
100
 
101
+ // [NEW] Evaluate dynamic conditions based on user's subscription mode
102
+ // Check if user is a developer for bypass
103
+ const { isDeveloper } = require('../../api-v2/helpers/data-fetchers/firestore.js');
104
+ const isDev = await isDeveloper(db, String(userCid));
105
+
106
+ // Get user's configuration from subscription
107
+ const userDynamicConfig = subscription.dynamicConfig?.[alertType.configKey] || {};
108
+ const userUseDynamic = subscription.useDynamic?.[alertType.configKey] === true;
109
+
110
+ // Evaluate conditions (will pass if static mode or no conditions)
111
+ const evaluation = evaluateDynamicConditions(
112
+ alertType,
113
+ computationMetadata,
114
+ userDynamicConfig,
115
+ userUseDynamic,
116
+ isDev,
117
+ logger
118
+ );
119
+
120
+ if (!evaluation.passes) {
121
+ logger.log('DEBUG', `[processAlertForPI] User ${userCid} dynamic conditions not met for ${alertType.id}: ${evaluation.reason}`);
122
+ continue; // Skip this user
123
+ }
124
+
125
+ if (evaluation.developerBypass) {
126
+ logger.log('INFO', `[processAlertForPI] Developer ${userCid} bypass: ${evaluation.reason}`);
127
+ } else {
128
+ logger.log('DEBUG', `[processAlertForPI] User ${userCid} alert ${alertType.id} in ${evaluation.mode} mode`);
129
+ }
130
+
100
131
  const notificationId = `alert_${Date.now()}_${userCid}_${piCid}_${Math.random().toString(36).substring(2, 9)}`;
101
132
 
102
133
  const notificationData = {
@@ -0,0 +1,279 @@
1
+ /**
2
+ * @fileoverview Dynamic Alert Condition Evaluator
3
+ * Evaluates user-specific thresholds and conditions for dynamic alerts
4
+ */
5
+
6
+ /**
7
+ * Evaluate if an alert result meets user's dynamic thresholds and conditions
8
+ * @param {Object} alertType - Alert type metadata (with isDynamic, dynamicConfig)
9
+ * @param {Object} alertResult - Result from computation for specific PI
10
+ * @param {Object} userDynamicConfig - User's threshold/condition overrides from subscription
11
+ * @param {boolean} userUseDynamic - Whether user is using dynamic mode for THIS alert
12
+ * @param {boolean} isDeveloper - Whether user is a developer (bypass conditions)
13
+ * @param {Object} logger - Logger instance
14
+ * @returns {Object} { passes: boolean, reason: string, effectiveConfig: Object }
15
+ */
16
+ function evaluateDynamicConditions(alertType, alertResult, userDynamicConfig, userUseDynamic, isDeveloper, logger) {
17
+ // Check if alert type supports dynamic configs
18
+ const supportsDynamic = alertType.isDynamic === true;
19
+
20
+ // If alert doesn't support dynamic OR user chose static mode, always pass
21
+ if (!supportsDynamic || !userUseDynamic) {
22
+ return {
23
+ passes: true,
24
+ reason: !supportsDynamic
25
+ ? 'Alert type does not support dynamic conditions'
26
+ : 'User using static mode (no conditions)',
27
+ effectiveConfig: null,
28
+ mode: 'static'
29
+ };
30
+ }
31
+
32
+ // Developer bypass: always pass but log what conditions were met
33
+ if (isDeveloper) {
34
+ const metConditions = evaluateConditionsInternal(alertType, alertResult, userDynamicConfig, logger);
35
+ return {
36
+ passes: true,
37
+ reason: `Developer bypass - would have ${metConditions.passes ? 'passed' : 'failed'}: ${metConditions.reason}`,
38
+ effectiveConfig: metConditions.effectiveConfig,
39
+ developerBypass: true,
40
+ mode: 'dynamic-dev-bypass'
41
+ };
42
+ }
43
+
44
+ // Normal dynamic evaluation
45
+ const result = evaluateConditionsInternal(alertType, alertResult, userDynamicConfig, logger);
46
+ return {
47
+ ...result,
48
+ mode: 'dynamic'
49
+ };
50
+ }
51
+
52
+ /**
53
+ * Internal condition evaluation logic
54
+ */
55
+ function evaluateConditionsInternal(alertType, alertResult, userDynamicConfig, logger) {
56
+ const dynamicConfig = alertType.dynamicConfig || {};
57
+ const thresholds = dynamicConfig.thresholds || [];
58
+ const conditions = dynamicConfig.conditions || [];
59
+ const resultFields = dynamicConfig.resultFields || {};
60
+
61
+ // Build effective configuration (user overrides + defaults)
62
+ const effectiveConfig = {
63
+ thresholds: {},
64
+ conditions: {}
65
+ };
66
+
67
+ // Apply threshold defaults and user overrides
68
+ thresholds.forEach(threshold => {
69
+ const userValue = userDynamicConfig?.thresholds?.[threshold.key];
70
+ effectiveConfig.thresholds[threshold.key] = userValue !== undefined ? userValue : threshold.default;
71
+ });
72
+
73
+ // Apply condition defaults and user overrides
74
+ conditions.forEach(condition => {
75
+ const userValue = userDynamicConfig?.conditions?.[condition.key];
76
+ effectiveConfig.conditions[condition.key] = userValue !== undefined ? userValue : condition.default;
77
+ });
78
+
79
+ // Evaluate each threshold
80
+ for (const threshold of thresholds) {
81
+ const thresholdValue = effectiveConfig.thresholds[threshold.key];
82
+ const result = evaluateThreshold(threshold, thresholdValue, alertResult, resultFields, logger);
83
+
84
+ if (!result.passes) {
85
+ return {
86
+ passes: false,
87
+ reason: result.reason,
88
+ effectiveConfig
89
+ };
90
+ }
91
+ }
92
+
93
+ // Evaluate each condition
94
+ for (const condition of conditions) {
95
+ const conditionValue = effectiveConfig.conditions[condition.key];
96
+ const result = evaluateCondition(condition, conditionValue, alertResult, resultFields, logger);
97
+
98
+ if (!result.passes) {
99
+ return {
100
+ passes: false,
101
+ reason: result.reason,
102
+ effectiveConfig
103
+ };
104
+ }
105
+ }
106
+
107
+ // All conditions passed
108
+ return {
109
+ passes: true,
110
+ reason: 'All dynamic conditions met',
111
+ effectiveConfig
112
+ };
113
+ }
114
+
115
+ /**
116
+ * Evaluate a single threshold
117
+ */
118
+ function evaluateThreshold(threshold, thresholdValue, alertResult, resultFields, logger) {
119
+ const key = threshold.key;
120
+
121
+ // Special handling for specific threshold types
122
+ switch (key) {
123
+ case 'minChange':
124
+ case 'minChangeAmount': {
125
+ const changeField = resultFields.change || 'change';
126
+ const actualChange = Math.abs(alertResult[changeField] || 0);
127
+
128
+ if (actualChange < thresholdValue) {
129
+ return {
130
+ passes: false,
131
+ reason: `Change ${actualChange.toFixed(2)} is below threshold ${thresholdValue}`
132
+ };
133
+ }
134
+ return { passes: true };
135
+ }
136
+
137
+ case 'minRiskLevel': {
138
+ const newValueField = resultFields.newValue || 'currentRisk';
139
+ const actualValue = alertResult[newValueField] || 0;
140
+
141
+ if (actualValue < thresholdValue) {
142
+ return {
143
+ passes: false,
144
+ reason: `Risk level ${actualValue.toFixed(2)} is below threshold ${thresholdValue}`
145
+ };
146
+ }
147
+ return { passes: true };
148
+ }
149
+
150
+ case 'volatilityThreshold': {
151
+ const volatilityField = resultFields.volatility || 'volatility';
152
+ const actualVolatility = alertResult[volatilityField] || 0;
153
+
154
+ if (actualVolatility < thresholdValue) {
155
+ return {
156
+ passes: false,
157
+ reason: `Volatility ${actualVolatility.toFixed(1)}% is below threshold ${thresholdValue}%`
158
+ };
159
+ }
160
+ return { passes: true };
161
+ }
162
+
163
+ case 'minIncrease':
164
+ case 'minIncreasePercentage': {
165
+ // For position increases, check if ANY move exceeds threshold
166
+ const moves = alertResult.moves || [];
167
+ const hasSignificantMove = moves.some(move => Math.abs(move.diff || 0) >= thresholdValue);
168
+
169
+ if (!hasSignificantMove) {
170
+ return {
171
+ passes: false,
172
+ reason: `No position increase exceeds threshold ${thresholdValue}pp`
173
+ };
174
+ }
175
+ return { passes: true };
176
+ }
177
+
178
+ case 'anomalyScoreThreshold': {
179
+ const scoreField = resultFields.score || 'anomalyScore';
180
+ const actualScore = alertResult[scoreField] || 0;
181
+
182
+ if (actualScore < thresholdValue) {
183
+ return {
184
+ passes: false,
185
+ reason: `Anomaly score ${actualScore.toFixed(2)} is below threshold ${thresholdValue}`
186
+ };
187
+ }
188
+ return { passes: true };
189
+ }
190
+
191
+ default:
192
+ logger?.log('WARN', `[DynamicEvaluator] Unknown threshold type: ${key}`);
193
+ return { passes: true }; // Unknown thresholds pass by default
194
+ }
195
+ }
196
+
197
+ /**
198
+ * Evaluate a single condition
199
+ */
200
+ function evaluateCondition(condition, conditionValue, alertResult, resultFields, logger) {
201
+ const key = condition.key;
202
+
203
+ // If condition array is empty, pass (means "all")
204
+ if (Array.isArray(conditionValue) && conditionValue.length === 0) {
205
+ return { passes: true };
206
+ }
207
+
208
+ switch (key) {
209
+ case 'watchedSectors': {
210
+ // Check if any new sector is in the watched list
211
+ const newSectors = alertResult.newExposures || [];
212
+ const watchedSectors = Array.isArray(conditionValue) ? conditionValue : [];
213
+
214
+ if (watchedSectors.length === 0) {
215
+ return { passes: true }; // No filter = all sectors
216
+ }
217
+
218
+ const hasWatchedSector = newSectors.some(sector =>
219
+ watchedSectors.includes(sector)
220
+ );
221
+
222
+ if (!hasWatchedSector) {
223
+ return {
224
+ passes: false,
225
+ reason: `New sectors [${newSectors.join(', ')}] not in watched list`
226
+ };
227
+ }
228
+ return { passes: true };
229
+ }
230
+
231
+ case 'watchedSymbols': {
232
+ // Check if any position move involves a watched symbol
233
+ const moves = alertResult.moves || [];
234
+ const watchedSymbols = Array.isArray(conditionValue) ? conditionValue : [];
235
+
236
+ if (watchedSymbols.length === 0) {
237
+ return { passes: true }; // No filter = all symbols
238
+ }
239
+
240
+ const hasWatchedSymbol = moves.some(move =>
241
+ watchedSymbols.includes(move.symbol)
242
+ );
243
+
244
+ if (!hasWatchedSymbol) {
245
+ return {
246
+ passes: false,
247
+ reason: `Position changes not in watched symbols list`
248
+ };
249
+ }
250
+ return { passes: true };
251
+ }
252
+
253
+ case 'watchedDrivers': {
254
+ // Check if primary driver is in watched list
255
+ const primaryDriver = alertResult.primaryDriver || '';
256
+ const watchedDrivers = Array.isArray(conditionValue) ? conditionValue : [];
257
+
258
+ if (watchedDrivers.length === 0) {
259
+ return { passes: true }; // No filter = all drivers
260
+ }
261
+
262
+ if (!watchedDrivers.includes(primaryDriver)) {
263
+ return {
264
+ passes: false,
265
+ reason: `Primary driver '${primaryDriver}' not in watched list`
266
+ };
267
+ }
268
+ return { passes: true };
269
+ }
270
+
271
+ default:
272
+ logger?.log('WARN', `[DynamicEvaluator] Unknown condition type: ${key}`);
273
+ return { passes: true }; // Unknown conditions pass by default
274
+ }
275
+ }
276
+
277
+ module.exports = {
278
+ evaluateDynamicConditions
279
+ };
@@ -56,14 +56,22 @@ router.get('/types', async (req, res, next) => {
56
56
 
57
57
  logger?.log('INFO', `[GET /alerts/types] After filtering: ${filteredAlertTypes.length} alert types (removed ${alertTypes.length - filteredAlertTypes.length} test alerts)`);
58
58
 
59
- // Format response for frontend
59
+ // Format response for frontend (include dynamic configuration)
60
60
  const formattedTypes = filteredAlertTypes.map(type => ({
61
61
  id: type.id,
62
62
  name: type.name,
63
63
  description: type.description,
64
64
  severity: type.severity,
65
65
  configKey: type.configKey,
66
- isTest: type.isTest || false
66
+ isTest: type.isTest || false,
67
+
68
+ // [NEW] Include dynamic alert configuration
69
+ isDynamic: type.isDynamic || false,
70
+ dynamicConfig: type.isDynamic ? {
71
+ thresholds: type.dynamicConfig?.thresholds || [],
72
+ conditions: type.dynamicConfig?.conditions || [],
73
+ resultFields: type.dynamicConfig?.resultFields || {}
74
+ } : null
67
75
  }));
68
76
 
69
77
  res.json({
@@ -189,25 +197,31 @@ router.get('/types', async (req, res, next) => {
189
197
  // GET /alerts/dynamic-watchlist-computations - Get available computations for dynamic watchlists
190
198
  router.get('/dynamic-watchlist-computations', async (req, res, next) => {
191
199
  try {
192
- // Import getAllAlertTypes from alert system
193
- const { getAllAlertTypes } = require('../../alert-system/helpers/alert_type_registry.js');
194
- const alertTypes = getAllAlertTypes();
200
+ const { logger } = req.dependencies;
195
201
 
196
- // Extract unique computations from alert types
197
- const computations = alertTypes.map(type => ({
198
- computationName: type.computationName,
199
- alertTypeName: type.name,
200
- description: type.description,
201
- severity: type.severity
202
- }));
202
+ // [UPDATED] Load alert types from manifest instead of hardcoded registry
203
+ const alertTypes = await loadAlertTypesFromManifest(logger);
203
204
 
204
- // Cache in browser/CDN for 1 hour (static data)
205
+ // Extract unique computations from alert types (exclude test alerts)
206
+ const computations = alertTypes
207
+ .filter(type => !type.isTest)
208
+ .map(type => ({
209
+ computationName: type.computationName,
210
+ alertTypeName: type.name,
211
+ description: type.description,
212
+ severity: type.severity,
213
+ configKey: type.configKey
214
+ }));
215
+
216
+ // Cache in browser/CDN for 1 hour (data changes infrequently)
205
217
  res.set('Cache-Control', 'public, max-age=3600');
206
218
  res.json({
207
219
  success: true,
208
- computations
220
+ computations,
221
+ count: computations.length
209
222
  });
210
223
  } catch (error) {
224
+ logger?.log('ERROR', `[GET /alerts/dynamic-watchlist-computations] Error: ${error.message}`);
211
225
  next(error);
212
226
  }
213
227
  });
@@ -680,13 +680,12 @@ async function resolveRoutes(db, date, pass, tasks, logger) {
680
680
 
681
681
  async function dispatchComputationPass(config, dependencies, computationManifest, reqBody = {}) {
682
682
  switch (reqBody.action) {
683
- case 'VERIFY': return handlePassVerification(config, dependencies, computationManifest, reqBody);
684
- case 'SWEEP': return handleSweepDispatch(config, dependencies, computationManifest, reqBody);
685
- case 'REPORT': return handleFinalSweepReporting(config, dependencies, computationManifest, reqBody);
683
+ case 'VERIFY': return handlePassVerification(config, dependencies, computationManifest, reqBody);
684
+ case 'SWEEP': return handleSweepDispatch(config, dependencies, computationManifest, reqBody);
685
+ case 'REPORT': return handleFinalSweepReporting(config, dependencies, computationManifest, reqBody);
686
686
  case 'FORCE_RUN': return handleForceRun(config, dependencies, computationManifest, reqBody);
687
- // 3. REGISTER SNAPSHOT ACTION
688
- case 'SNAPSHOT': return handleSnapshot(config, dependencies, reqBody);
689
- default: return handleStandardDispatch(config, dependencies, computationManifest, reqBody);
687
+ case 'SNAPSHOT': return handleSnapshot(config, dependencies, reqBody);
688
+ default: return handleStandardDispatch(config, dependencies, computationManifest, reqBody);
690
689
  }
691
690
  }
692
691
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bulltrackers-module",
3
- "version": "1.0.683",
3
+ "version": "1.0.685",
4
4
  "description": "Helper Functions for Bulltrackers.",
5
5
  "main": "index.js",
6
6
  "files": [