bulltrackers-module 1.0.687 → 1.0.689
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.
|
@@ -63,12 +63,15 @@ async function loadAlertTypesFromManifest(logger) {
|
|
|
63
63
|
severity: metadata.alert.severity || 'medium',
|
|
64
64
|
configKey: metadata.alert.configKey,
|
|
65
65
|
isTest: metadata.isTest === true,
|
|
66
|
-
enabled: true
|
|
66
|
+
enabled: true,
|
|
67
|
+
// [FIX] Extract dynamic alert configuration from metadata
|
|
68
|
+
isDynamic: metadata.alert.isDynamic || false,
|
|
69
|
+
dynamicConfig: metadata.alert.dynamicConfig || null
|
|
67
70
|
};
|
|
68
71
|
|
|
69
72
|
alertTypes.push(alertType);
|
|
70
73
|
|
|
71
|
-
logger?.log('DEBUG', `[AlertManifestLoader] Loaded alert type: ${alertType.id} from ${metadata.name}`);
|
|
74
|
+
logger?.log('DEBUG', `[AlertManifestLoader] Loaded alert type: ${alertType.id} from ${metadata.name} (isDynamic: ${alertType.isDynamic})`);
|
|
72
75
|
}
|
|
73
76
|
|
|
74
77
|
logger?.log('INFO', `[AlertManifestLoader] Successfully loaded ${alertTypes.length} alert types from calculations`);
|
|
@@ -417,11 +417,6 @@ const manageUserWatchlist = async (db, userId, instruction, payload = {}) => {
|
|
|
417
417
|
// 3. Calculate Deltas based on Instruction
|
|
418
418
|
if (instruction === 'create') {
|
|
419
419
|
addedItems = payload.items || [];
|
|
420
|
-
|
|
421
|
-
// DEBUG: Log what we're receiving
|
|
422
|
-
console.log('[manageUserWatchlist DEBUG] payload received:', JSON.stringify(payload));
|
|
423
|
-
console.log('[manageUserWatchlist DEBUG] payload.type:', payload.type);
|
|
424
|
-
|
|
425
420
|
// Create the User Document
|
|
426
421
|
const newDocData = {
|
|
427
422
|
...payload,
|
|
@@ -432,11 +427,6 @@ const manageUserWatchlist = async (db, userId, instruction, payload = {}) => {
|
|
|
432
427
|
copyCount: 0,
|
|
433
428
|
isAutoGenerated: false
|
|
434
429
|
};
|
|
435
|
-
|
|
436
|
-
// DEBUG: Log what we're about to save
|
|
437
|
-
console.log('[manageUserWatchlist DEBUG] newDocData to save:', JSON.stringify(newDocData));
|
|
438
|
-
console.log('[manageUserWatchlist DEBUG] newDocData.type:', newDocData.type);
|
|
439
|
-
|
|
440
430
|
batch.set(userDocRef, newDocData);
|
|
441
431
|
}
|
|
442
432
|
|
|
@@ -3036,6 +3026,161 @@ const getWatchlistTriggerCounts = async (db, userId, watchlistId) => {
|
|
|
3036
3026
|
}
|
|
3037
3027
|
};
|
|
3038
3028
|
|
|
3029
|
+
/**
|
|
3030
|
+
* Query PIs matching dynamic watchlist criteria
|
|
3031
|
+
* @param {Object} db - Firestore instance
|
|
3032
|
+
* @param {string} computationName - Name of the computation to query
|
|
3033
|
+
* @param {Object} conditions - Threshold conditions to filter by
|
|
3034
|
+
* @param {string} timeRange - Time range (today, last_7_days, last_30_days, all_time)
|
|
3035
|
+
* @param {number} limit - Maximum number of results
|
|
3036
|
+
* @returns {Promise<Object>} Matching PIs with their values
|
|
3037
|
+
*/
|
|
3038
|
+
const queryDynamicWatchlistMatches = async (db, computationName, conditions = {}, timeRange = 'last_7_days', limit = 100) => {
|
|
3039
|
+
try {
|
|
3040
|
+
const { readComputationResultsWithShards } = require('../../alert-system/helpers/alert_helpers');
|
|
3041
|
+
|
|
3042
|
+
// Determine date range based on timeRange
|
|
3043
|
+
const endDate = new Date();
|
|
3044
|
+
const startDate = new Date();
|
|
3045
|
+
|
|
3046
|
+
switch (timeRange) {
|
|
3047
|
+
case 'today':
|
|
3048
|
+
// Just today
|
|
3049
|
+
break;
|
|
3050
|
+
case 'last_7_days':
|
|
3051
|
+
startDate.setDate(startDate.getDate() - 7);
|
|
3052
|
+
break;
|
|
3053
|
+
case 'last_30_days':
|
|
3054
|
+
startDate.setDate(startDate.getDate() - 30);
|
|
3055
|
+
break;
|
|
3056
|
+
case 'all_time':
|
|
3057
|
+
startDate.setDate(startDate.getDate() - 365); // Max 1 year lookback
|
|
3058
|
+
break;
|
|
3059
|
+
default:
|
|
3060
|
+
startDate.setDate(startDate.getDate() - 7);
|
|
3061
|
+
}
|
|
3062
|
+
|
|
3063
|
+
// Build list of dates to query
|
|
3064
|
+
const dates = [];
|
|
3065
|
+
for (let d = new Date(endDate); d >= startDate; d.setDate(d.getDate() - 1)) {
|
|
3066
|
+
dates.push(d.toISOString().split('T')[0]);
|
|
3067
|
+
}
|
|
3068
|
+
|
|
3069
|
+
const matchingPIs = new Map(); // cid -> { matchValue, matchedAt, metadata }
|
|
3070
|
+
|
|
3071
|
+
// Query each date until we find results
|
|
3072
|
+
for (const dateStr of dates) {
|
|
3073
|
+
try {
|
|
3074
|
+
const docRef = db.collection('unified_insights')
|
|
3075
|
+
.doc(dateStr)
|
|
3076
|
+
.collection('results')
|
|
3077
|
+
.doc('alerts')
|
|
3078
|
+
.collection('computations')
|
|
3079
|
+
.doc(computationName);
|
|
3080
|
+
|
|
3081
|
+
const docSnapshot = await docRef.get();
|
|
3082
|
+
|
|
3083
|
+
if (!docSnapshot.exists) continue;
|
|
3084
|
+
|
|
3085
|
+
const docData = docSnapshot.data();
|
|
3086
|
+
const results = await readComputationResultsWithShards(db, docData, docRef, null);
|
|
3087
|
+
|
|
3088
|
+
if (!results.cids || results.cids.length === 0) continue;
|
|
3089
|
+
|
|
3090
|
+
// Process each PI
|
|
3091
|
+
for (const piCid of results.cids) {
|
|
3092
|
+
// Skip if already processed
|
|
3093
|
+
if (matchingPIs.has(piCid)) continue;
|
|
3094
|
+
|
|
3095
|
+
const piData = results.perUserData?.[piCid] || results.metadata || {};
|
|
3096
|
+
|
|
3097
|
+
// Apply condition filtering
|
|
3098
|
+
let passesConditions = true;
|
|
3099
|
+
let matchValue = 0;
|
|
3100
|
+
|
|
3101
|
+
// Extract match value based on computation type
|
|
3102
|
+
if (piData.change !== undefined) {
|
|
3103
|
+
matchValue = piData.change;
|
|
3104
|
+
} else if (piData.current !== undefined) {
|
|
3105
|
+
matchValue = piData.current;
|
|
3106
|
+
} else if (piData.riskScore !== undefined) {
|
|
3107
|
+
matchValue = piData.riskScore;
|
|
3108
|
+
} else if (piData.volatility !== undefined) {
|
|
3109
|
+
matchValue = piData.volatility * 100; // Convert to percentage
|
|
3110
|
+
} else if (piData.diff !== undefined) {
|
|
3111
|
+
matchValue = piData.diff;
|
|
3112
|
+
}
|
|
3113
|
+
|
|
3114
|
+
// Apply threshold conditions
|
|
3115
|
+
if (conditions.minValue !== undefined && matchValue < conditions.minValue) {
|
|
3116
|
+
passesConditions = false;
|
|
3117
|
+
}
|
|
3118
|
+
if (conditions.maxValue !== undefined && matchValue > conditions.maxValue) {
|
|
3119
|
+
passesConditions = false;
|
|
3120
|
+
}
|
|
3121
|
+
|
|
3122
|
+
if (passesConditions) {
|
|
3123
|
+
// Get PI username from master list
|
|
3124
|
+
let username = piData.piUsername || piData.username || `PI-${piCid}`;
|
|
3125
|
+
try {
|
|
3126
|
+
const piProfile = await fetchPopularInvestorMasterList(db, String(piCid));
|
|
3127
|
+
if (piProfile && piProfile.username) {
|
|
3128
|
+
username = piProfile.username;
|
|
3129
|
+
}
|
|
3130
|
+
} catch (e) {
|
|
3131
|
+
// Use default username
|
|
3132
|
+
}
|
|
3133
|
+
|
|
3134
|
+
matchingPIs.set(piCid, {
|
|
3135
|
+
cid: Number(piCid),
|
|
3136
|
+
username,
|
|
3137
|
+
matchedAt: dateStr,
|
|
3138
|
+
matchValue,
|
|
3139
|
+
previousValue: piData.previous || piData.prev,
|
|
3140
|
+
trend: piData.previous !== undefined
|
|
3141
|
+
? (matchValue > piData.previous ? 'up' : matchValue < piData.previous ? 'down' : 'stable')
|
|
3142
|
+
: undefined,
|
|
3143
|
+
metadata: {
|
|
3144
|
+
...piData,
|
|
3145
|
+
computationDate: dateStr
|
|
3146
|
+
}
|
|
3147
|
+
});
|
|
3148
|
+
|
|
3149
|
+
// Stop if we've hit the limit
|
|
3150
|
+
if (matchingPIs.size >= limit) break;
|
|
3151
|
+
}
|
|
3152
|
+
}
|
|
3153
|
+
|
|
3154
|
+
// Break out of date loop if we have enough results
|
|
3155
|
+
if (matchingPIs.size >= limit) break;
|
|
3156
|
+
|
|
3157
|
+
} catch (dateErr) {
|
|
3158
|
+
console.warn(`Error processing date ${dateStr} for dynamic watchlist: ${dateErr.message}`);
|
|
3159
|
+
continue;
|
|
3160
|
+
}
|
|
3161
|
+
}
|
|
3162
|
+
|
|
3163
|
+
// Sort by match value (descending) and return
|
|
3164
|
+
const sortedMatches = Array.from(matchingPIs.values())
|
|
3165
|
+
.sort((a, b) => Math.abs(b.matchValue) - Math.abs(a.matchValue))
|
|
3166
|
+
.slice(0, limit);
|
|
3167
|
+
|
|
3168
|
+
return {
|
|
3169
|
+
success: true,
|
|
3170
|
+
matches: sortedMatches,
|
|
3171
|
+
count: sortedMatches.length,
|
|
3172
|
+
dateRange: {
|
|
3173
|
+
start: startDate.toISOString().split('T')[0],
|
|
3174
|
+
end: endDate.toISOString().split('T')[0]
|
|
3175
|
+
},
|
|
3176
|
+
computationName
|
|
3177
|
+
};
|
|
3178
|
+
} catch (error) {
|
|
3179
|
+
console.error(`Error querying dynamic watchlist matches: ${error}`);
|
|
3180
|
+
throw error;
|
|
3181
|
+
}
|
|
3182
|
+
};
|
|
3183
|
+
|
|
3039
3184
|
/**
|
|
3040
3185
|
* Subscribe to all PIs in a watchlist
|
|
3041
3186
|
*/
|
|
@@ -3148,5 +3293,6 @@ module.exports = {
|
|
|
3148
3293
|
updateSubscription,
|
|
3149
3294
|
unsubscribeFromAlerts,
|
|
3150
3295
|
getWatchlistTriggerCounts,
|
|
3151
|
-
subscribeToAllWatchlistPIs
|
|
3296
|
+
subscribeToAllWatchlistPIs,
|
|
3297
|
+
queryDynamicWatchlistMatches
|
|
3152
3298
|
};
|
|
@@ -10,7 +10,8 @@ const {
|
|
|
10
10
|
autoGenerateWatchlist,
|
|
11
11
|
fetchPopularInvestorMasterList,
|
|
12
12
|
getWatchlistTriggerCounts,
|
|
13
|
-
subscribeToAllWatchlistPIs
|
|
13
|
+
subscribeToAllWatchlistPIs,
|
|
14
|
+
queryDynamicWatchlistMatches
|
|
14
15
|
} = require('../helpers/data-fetchers/firestore.js');
|
|
15
16
|
const { sanitizeDocId } = require('../helpers/security_utils.js');
|
|
16
17
|
|
|
@@ -37,6 +38,16 @@ const watchlistSubscribeSchema = z.object({
|
|
|
37
38
|
thresholds: z.record(z.any()).optional()
|
|
38
39
|
});
|
|
39
40
|
|
|
41
|
+
const dynamicMatchesSchema = z.object({
|
|
42
|
+
computationName: z.string().min(1).max(200),
|
|
43
|
+
conditions: z.object({
|
|
44
|
+
minValue: z.number().optional(),
|
|
45
|
+
maxValue: z.number().optional()
|
|
46
|
+
}).optional(),
|
|
47
|
+
timeRange: z.enum(['today', 'last_7_days', 'last_30_days', 'all_time']).optional(),
|
|
48
|
+
limit: z.number().int().min(1).max(500).optional()
|
|
49
|
+
});
|
|
50
|
+
|
|
40
51
|
// GET /watchlists/all
|
|
41
52
|
router.get('/all', async (req, res, next) => {
|
|
42
53
|
try {
|
|
@@ -217,4 +228,77 @@ router.post('/:id/subscribe-all', async (req, res, next) => {
|
|
|
217
228
|
}
|
|
218
229
|
});
|
|
219
230
|
|
|
231
|
+
// POST /watchlists/dynamic/query - Query PIs matching dynamic watchlist criteria
|
|
232
|
+
router.post('/dynamic/query', async (req, res, next) => {
|
|
233
|
+
try {
|
|
234
|
+
// Validate input
|
|
235
|
+
const validatedData = dynamicMatchesSchema.parse(req.body);
|
|
236
|
+
|
|
237
|
+
const { db } = req.dependencies;
|
|
238
|
+
const result = await queryDynamicWatchlistMatches(
|
|
239
|
+
db,
|
|
240
|
+
validatedData.computationName,
|
|
241
|
+
validatedData.conditions || {},
|
|
242
|
+
validatedData.timeRange || 'last_7_days',
|
|
243
|
+
validatedData.limit || 100
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
res.json(result);
|
|
247
|
+
} catch (error) {
|
|
248
|
+
if (error instanceof z.ZodError) {
|
|
249
|
+
return res.status(400).json({
|
|
250
|
+
success: false,
|
|
251
|
+
error: 'Invalid input',
|
|
252
|
+
details: error.errors
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
next(error);
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
// GET /watchlists/:id/dynamic/matches - Get current matches for a dynamic watchlist
|
|
260
|
+
router.get('/:id/dynamic/matches', async (req, res, next) => {
|
|
261
|
+
try {
|
|
262
|
+
const { db } = req.dependencies;
|
|
263
|
+
const id = sanitizeDocId(req.params.id);
|
|
264
|
+
|
|
265
|
+
// Fetch the watchlist to get its dynamic config
|
|
266
|
+
const watchlist = await latestUserCentricSnapshot(db, req.targetUserId, 'watchlists', 'watchlist', 'SignedInUsers', id);
|
|
267
|
+
|
|
268
|
+
if (!watchlist) {
|
|
269
|
+
const error = new Error("Watchlist not found");
|
|
270
|
+
error.status = 404;
|
|
271
|
+
return next(error);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (watchlist.type !== 'dynamic') {
|
|
275
|
+
return res.status(400).json({
|
|
276
|
+
success: false,
|
|
277
|
+
error: 'This endpoint is only for dynamic watchlists'
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const dynamicConfig = watchlist.dynamicConfig || {};
|
|
282
|
+
const result = await queryDynamicWatchlistMatches(
|
|
283
|
+
db,
|
|
284
|
+
dynamicConfig.computationName,
|
|
285
|
+
dynamicConfig.parameters || {},
|
|
286
|
+
dynamicConfig.timeRange || 'last_7_days',
|
|
287
|
+
100
|
|
288
|
+
);
|
|
289
|
+
|
|
290
|
+
res.json({
|
|
291
|
+
success: true,
|
|
292
|
+
watchlistId: id,
|
|
293
|
+
watchlistName: watchlist.name,
|
|
294
|
+
...result
|
|
295
|
+
});
|
|
296
|
+
} catch (error) {
|
|
297
|
+
if (error.message === "Watchlist not found") {
|
|
298
|
+
error.status = 404;
|
|
299
|
+
}
|
|
300
|
+
next(error);
|
|
301
|
+
}
|
|
302
|
+
});
|
|
303
|
+
|
|
220
304
|
module.exports = router;
|