bulltrackers-module 1.0.688 → 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.
@@ -3026,6 +3026,161 @@ const getWatchlistTriggerCounts = async (db, userId, watchlistId) => {
3026
3026
  }
3027
3027
  };
3028
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
+
3029
3184
  /**
3030
3185
  * Subscribe to all PIs in a watchlist
3031
3186
  */
@@ -3138,5 +3293,6 @@ module.exports = {
3138
3293
  updateSubscription,
3139
3294
  unsubscribeFromAlerts,
3140
3295
  getWatchlistTriggerCounts,
3141
- subscribeToAllWatchlistPIs
3296
+ subscribeToAllWatchlistPIs,
3297
+ queryDynamicWatchlistMatches
3142
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bulltrackers-module",
3
- "version": "1.0.688",
3
+ "version": "1.0.689",
4
4
  "description": "Helper Functions for Bulltrackers.",
5
5
  "main": "index.js",
6
6
  "files": [