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
|
-
|
|
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
|
-
|
|
193
|
-
const { getAllAlertTypes } = require('../../alert-system/helpers/alert_type_registry.js');
|
|
194
|
-
const alertTypes = getAllAlertTypes();
|
|
200
|
+
const { logger } = req.dependencies;
|
|
195
201
|
|
|
196
|
-
//
|
|
197
|
-
const
|
|
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
|
-
//
|
|
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':
|
|
684
|
-
case 'SWEEP':
|
|
685
|
-
case 'REPORT':
|
|
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
|
-
|
|
688
|
-
|
|
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
|
|