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;
|