bulltrackers-module 1.0.689 → 1.0.691
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.
|
@@ -3028,16 +3028,18 @@ const getWatchlistTriggerCounts = async (db, userId, watchlistId) => {
|
|
|
3028
3028
|
|
|
3029
3029
|
/**
|
|
3030
3030
|
* Query PIs matching dynamic watchlist criteria
|
|
3031
|
+
* Reads computation results and filters ALL per-user data (not just triggered CIDs)
|
|
3032
|
+
*
|
|
3031
3033
|
* @param {Object} db - Firestore instance
|
|
3032
3034
|
* @param {string} computationName - Name of the computation to query
|
|
3033
|
-
* @param {Object}
|
|
3034
|
-
* @param {string} timeRange - Time range (today, last_7_days, last_30_days
|
|
3035
|
+
* @param {Object} parameters - Threshold parameters from watchlist config (e.g., {minChange: 1, minRiskLevel: 5})
|
|
3036
|
+
* @param {string} timeRange - Time range (today, last_7_days, last_30_days)
|
|
3035
3037
|
* @param {number} limit - Maximum number of results
|
|
3036
3038
|
* @returns {Promise<Object>} Matching PIs with their values
|
|
3037
3039
|
*/
|
|
3038
|
-
const queryDynamicWatchlistMatches = async (db, computationName,
|
|
3040
|
+
const queryDynamicWatchlistMatches = async (db, computationName, parameters = {}, timeRange = 'last_7_days', limit = 100) => {
|
|
3039
3041
|
try {
|
|
3040
|
-
|
|
3042
|
+
console.log(`[queryDynamicWatchlistMatches] Querying ${computationName} with params:`, parameters, `timeRange: ${timeRange}`);
|
|
3041
3043
|
|
|
3042
3044
|
// Determine date range based on timeRange
|
|
3043
3045
|
const endDate = new Date();
|
|
@@ -3053,75 +3055,108 @@ const queryDynamicWatchlistMatches = async (db, computationName, conditions = {}
|
|
|
3053
3055
|
case 'last_30_days':
|
|
3054
3056
|
startDate.setDate(startDate.getDate() - 30);
|
|
3055
3057
|
break;
|
|
3056
|
-
case 'all_time':
|
|
3057
|
-
startDate.setDate(startDate.getDate() - 365); // Max 1 year lookback
|
|
3058
|
-
break;
|
|
3059
3058
|
default:
|
|
3060
3059
|
startDate.setDate(startDate.getDate() - 7);
|
|
3061
3060
|
}
|
|
3062
3061
|
|
|
3063
|
-
// Build list of dates to query
|
|
3062
|
+
// Build list of dates to query (most recent first)
|
|
3064
3063
|
const dates = [];
|
|
3065
3064
|
for (let d = new Date(endDate); d >= startDate; d.setDate(d.getDate() - 1)) {
|
|
3066
3065
|
dates.push(d.toISOString().split('T')[0]);
|
|
3067
3066
|
}
|
|
3068
3067
|
|
|
3069
|
-
|
|
3068
|
+
console.log(`[queryDynamicWatchlistMatches] Checking dates:`, dates);
|
|
3069
|
+
|
|
3070
|
+
const matchingPIs = new Map(); // cid -> match data
|
|
3070
3071
|
|
|
3071
|
-
// Query each date
|
|
3072
|
+
// Query each date
|
|
3072
3073
|
for (const dateStr of dates) {
|
|
3073
3074
|
try {
|
|
3074
|
-
|
|
3075
|
+
// Try both 'alerts' and 'popular-investor' result paths
|
|
3076
|
+
let docSnapshot = null;
|
|
3077
|
+
let docRef = null;
|
|
3078
|
+
|
|
3079
|
+
// First try alerts path
|
|
3080
|
+
docRef = db.collection('unified_insights')
|
|
3075
3081
|
.doc(dateStr)
|
|
3076
3082
|
.collection('results')
|
|
3077
3083
|
.doc('alerts')
|
|
3078
3084
|
.collection('computations')
|
|
3079
3085
|
.doc(computationName);
|
|
3080
3086
|
|
|
3081
|
-
|
|
3087
|
+
docSnapshot = await docRef.get();
|
|
3088
|
+
|
|
3089
|
+
// If not found, try popular-investor path
|
|
3090
|
+
if (!docSnapshot.exists) {
|
|
3091
|
+
docRef = db.collection('unified_insights')
|
|
3092
|
+
.doc(dateStr)
|
|
3093
|
+
.collection('results')
|
|
3094
|
+
.doc('popular-investor')
|
|
3095
|
+
.collection('computations')
|
|
3096
|
+
.doc(computationName);
|
|
3097
|
+
|
|
3098
|
+
docSnapshot = await docRef.get();
|
|
3099
|
+
}
|
|
3100
|
+
|
|
3101
|
+
if (!docSnapshot.exists) {
|
|
3102
|
+
console.log(`[queryDynamicWatchlistMatches] No data for ${dateStr}`);
|
|
3103
|
+
continue;
|
|
3104
|
+
}
|
|
3082
3105
|
|
|
3083
|
-
|
|
3106
|
+
console.log(`[queryDynamicWatchlistMatches] Found data for ${dateStr}`);
|
|
3084
3107
|
|
|
3085
3108
|
const docData = docSnapshot.data();
|
|
3086
|
-
const results = await readComputationResultsWithShards(db, docData, docRef, null);
|
|
3087
3109
|
|
|
3088
|
-
|
|
3110
|
+
// Check for sharded data
|
|
3111
|
+
let allUserData = {};
|
|
3089
3112
|
|
|
3090
|
-
|
|
3091
|
-
|
|
3092
|
-
|
|
3093
|
-
|
|
3113
|
+
if (docData._sharded === true) {
|
|
3114
|
+
// Read from shards
|
|
3115
|
+
const shardsSnapshot = await docRef.collection('_shards').get();
|
|
3116
|
+
console.log(`[queryDynamicWatchlistMatches] Found ${shardsSnapshot.size} shards`);
|
|
3094
3117
|
|
|
3095
|
-
const
|
|
3118
|
+
for (const shardDoc of shardsSnapshot.docs) {
|
|
3119
|
+
const shardData = shardDoc.data();
|
|
3120
|
+
// Each shard contains CID keys with their data
|
|
3121
|
+
Object.entries(shardData).forEach(([key, value]) => {
|
|
3122
|
+
// Skip metadata keys
|
|
3123
|
+
if (key.startsWith('_') || key === 'cids' || key === 'metadata') return;
|
|
3124
|
+
// Check if key is a CID (numeric string)
|
|
3125
|
+
if (/^\d+$/.test(key)) {
|
|
3126
|
+
allUserData[key] = value;
|
|
3127
|
+
}
|
|
3128
|
+
});
|
|
3129
|
+
}
|
|
3130
|
+
} else {
|
|
3131
|
+
// Data is in the document itself
|
|
3132
|
+
Object.entries(docData).forEach(([key, value]) => {
|
|
3133
|
+
// Skip metadata keys
|
|
3134
|
+
if (key.startsWith('_') || key === 'cids' || key === 'metadata' || key === 'globalMetadata') return;
|
|
3135
|
+
// Check if key is a CID (numeric string)
|
|
3136
|
+
if (/^\d+$/.test(key)) {
|
|
3137
|
+
allUserData[key] = value;
|
|
3138
|
+
}
|
|
3139
|
+
});
|
|
3140
|
+
}
|
|
3141
|
+
|
|
3142
|
+
console.log(`[queryDynamicWatchlistMatches] Found ${Object.keys(allUserData).length} PIs in computation data`);
|
|
3143
|
+
|
|
3144
|
+
// Process ALL PIs (not just those in cids array)
|
|
3145
|
+
for (const [piCidStr, piData] of Object.entries(allUserData)) {
|
|
3146
|
+
const piCid = Number(piCidStr);
|
|
3096
3147
|
|
|
3097
|
-
//
|
|
3098
|
-
|
|
3099
|
-
let matchValue = 0;
|
|
3148
|
+
// Skip if already matched from a more recent date
|
|
3149
|
+
if (matchingPIs.has(piCid)) continue;
|
|
3100
3150
|
|
|
3101
|
-
//
|
|
3102
|
-
if (piData.
|
|
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
|
-
}
|
|
3151
|
+
// Skip if error data
|
|
3152
|
+
if (piData.error) continue;
|
|
3113
3153
|
|
|
3114
|
-
// Apply
|
|
3115
|
-
|
|
3116
|
-
passesConditions = false;
|
|
3117
|
-
}
|
|
3118
|
-
if (conditions.maxValue !== undefined && matchValue > conditions.maxValue) {
|
|
3119
|
-
passesConditions = false;
|
|
3120
|
-
}
|
|
3154
|
+
// Apply computation-specific filtering based on parameters
|
|
3155
|
+
const passesFilter = checkPIMatchesCriteria(computationName, piData, parameters);
|
|
3121
3156
|
|
|
3122
|
-
if (
|
|
3157
|
+
if (passesFilter.passes) {
|
|
3123
3158
|
// Get PI username from master list
|
|
3124
|
-
let username =
|
|
3159
|
+
let username = `PI-${piCid}`;
|
|
3125
3160
|
try {
|
|
3126
3161
|
const piProfile = await fetchPopularInvestorMasterList(db, String(piCid));
|
|
3127
3162
|
if (piProfile && piProfile.username) {
|
|
@@ -3132,14 +3167,13 @@ const queryDynamicWatchlistMatches = async (db, computationName, conditions = {}
|
|
|
3132
3167
|
}
|
|
3133
3168
|
|
|
3134
3169
|
matchingPIs.set(piCid, {
|
|
3135
|
-
cid:
|
|
3170
|
+
cid: piCid,
|
|
3136
3171
|
username,
|
|
3137
3172
|
matchedAt: dateStr,
|
|
3138
|
-
matchValue,
|
|
3139
|
-
|
|
3140
|
-
|
|
3141
|
-
|
|
3142
|
-
: undefined,
|
|
3173
|
+
matchValue: passesFilter.matchValue,
|
|
3174
|
+
currentValue: passesFilter.currentValue,
|
|
3175
|
+
previousValue: passesFilter.previousValue,
|
|
3176
|
+
change: passesFilter.change,
|
|
3143
3177
|
metadata: {
|
|
3144
3178
|
...piData,
|
|
3145
3179
|
computationDate: dateStr
|
|
@@ -3151,11 +3185,11 @@ const queryDynamicWatchlistMatches = async (db, computationName, conditions = {}
|
|
|
3151
3185
|
}
|
|
3152
3186
|
}
|
|
3153
3187
|
|
|
3154
|
-
//
|
|
3188
|
+
// If we found enough matches, stop looking at older dates
|
|
3155
3189
|
if (matchingPIs.size >= limit) break;
|
|
3156
3190
|
|
|
3157
3191
|
} catch (dateErr) {
|
|
3158
|
-
console.warn(`Error processing date ${dateStr}
|
|
3192
|
+
console.warn(`[queryDynamicWatchlistMatches] Error processing date ${dateStr}: ${dateErr.message}`);
|
|
3159
3193
|
continue;
|
|
3160
3194
|
}
|
|
3161
3195
|
}
|
|
@@ -3165,22 +3199,135 @@ const queryDynamicWatchlistMatches = async (db, computationName, conditions = {}
|
|
|
3165
3199
|
.sort((a, b) => Math.abs(b.matchValue) - Math.abs(a.matchValue))
|
|
3166
3200
|
.slice(0, limit);
|
|
3167
3201
|
|
|
3202
|
+
console.log(`[queryDynamicWatchlistMatches] Returning ${sortedMatches.length} matches`);
|
|
3203
|
+
|
|
3168
3204
|
return {
|
|
3169
3205
|
success: true,
|
|
3170
3206
|
matches: sortedMatches,
|
|
3171
3207
|
count: sortedMatches.length,
|
|
3208
|
+
totalScanned: Object.keys(matchingPIs).length,
|
|
3172
3209
|
dateRange: {
|
|
3173
3210
|
start: startDate.toISOString().split('T')[0],
|
|
3174
3211
|
end: endDate.toISOString().split('T')[0]
|
|
3175
3212
|
},
|
|
3176
|
-
computationName
|
|
3213
|
+
computationName,
|
|
3214
|
+
parameters
|
|
3177
3215
|
};
|
|
3178
3216
|
} catch (error) {
|
|
3179
|
-
console.error(`Error
|
|
3217
|
+
console.error(`[queryDynamicWatchlistMatches] Error: ${error.message}`, error);
|
|
3180
3218
|
throw error;
|
|
3181
3219
|
}
|
|
3182
3220
|
};
|
|
3183
3221
|
|
|
3222
|
+
/**
|
|
3223
|
+
* Check if a PI's data matches the watchlist criteria
|
|
3224
|
+
* Handles different computation types with their specific data formats
|
|
3225
|
+
*/
|
|
3226
|
+
function checkPIMatchesCriteria(computationName, piData, parameters) {
|
|
3227
|
+
const result = { passes: false, matchValue: 0, currentValue: null, previousValue: null, change: null };
|
|
3228
|
+
|
|
3229
|
+
switch (computationName) {
|
|
3230
|
+
case 'RiskScoreIncrease': {
|
|
3231
|
+
// Data format: { change, currentRisk, previousRisk, isBaselineReset }
|
|
3232
|
+
const change = piData.change || 0;
|
|
3233
|
+
const currentRisk = piData.currentRisk;
|
|
3234
|
+
const previousRisk = piData.previousRisk;
|
|
3235
|
+
const minChange = parameters.minChange ?? 0;
|
|
3236
|
+
const minRiskLevel = parameters.minRiskLevel ?? 0;
|
|
3237
|
+
|
|
3238
|
+
result.currentValue = currentRisk;
|
|
3239
|
+
result.previousValue = previousRisk;
|
|
3240
|
+
result.change = change;
|
|
3241
|
+
result.matchValue = currentRisk; // Sort by current risk level
|
|
3242
|
+
|
|
3243
|
+
// Pass if change >= minChange AND currentRisk >= minRiskLevel
|
|
3244
|
+
if (change >= minChange && currentRisk >= minRiskLevel) {
|
|
3245
|
+
result.passes = true;
|
|
3246
|
+
}
|
|
3247
|
+
break;
|
|
3248
|
+
}
|
|
3249
|
+
|
|
3250
|
+
case 'SignificantVolatility': {
|
|
3251
|
+
// Data format: { volatility, threshold, samples }
|
|
3252
|
+
const volatility = piData.volatility || 0;
|
|
3253
|
+
const minVolatility = parameters.volatilityThreshold ?? 50;
|
|
3254
|
+
|
|
3255
|
+
result.currentValue = volatility;
|
|
3256
|
+
result.matchValue = volatility;
|
|
3257
|
+
|
|
3258
|
+
if (volatility >= minVolatility) {
|
|
3259
|
+
result.passes = true;
|
|
3260
|
+
}
|
|
3261
|
+
break;
|
|
3262
|
+
}
|
|
3263
|
+
|
|
3264
|
+
case 'PositionInvestedIncrease': {
|
|
3265
|
+
// Data format: { moveCount, moves: [{symbol, prev, curr, diff}], isBaselineReset }
|
|
3266
|
+
const moveCount = piData.moveCount || 0;
|
|
3267
|
+
const moves = piData.moves || [];
|
|
3268
|
+
const minIncrease = parameters.minIncrease ?? 5;
|
|
3269
|
+
|
|
3270
|
+
// Find the largest position increase
|
|
3271
|
+
const maxMove = moves.reduce((max, m) => m.diff > max ? m.diff : max, 0);
|
|
3272
|
+
|
|
3273
|
+
result.currentValue = moveCount;
|
|
3274
|
+
result.matchValue = maxMove;
|
|
3275
|
+
|
|
3276
|
+
if (maxMove >= minIncrease) {
|
|
3277
|
+
result.passes = true;
|
|
3278
|
+
}
|
|
3279
|
+
break;
|
|
3280
|
+
}
|
|
3281
|
+
|
|
3282
|
+
case 'NewSectorExposure': {
|
|
3283
|
+
// Data format: { currentSectors, previousSectors, newExposures, isBaselineReset }
|
|
3284
|
+
const newExposures = piData.newExposures || [];
|
|
3285
|
+
|
|
3286
|
+
result.currentValue = newExposures.length;
|
|
3287
|
+
result.matchValue = newExposures.length;
|
|
3288
|
+
|
|
3289
|
+
if (newExposures.length > 0) {
|
|
3290
|
+
result.passes = true;
|
|
3291
|
+
}
|
|
3292
|
+
break;
|
|
3293
|
+
}
|
|
3294
|
+
|
|
3295
|
+
case 'NewSocialPost': {
|
|
3296
|
+
// Data format: { hasNewPost, latestPostDate, postCount, title }
|
|
3297
|
+
const hasNewPost = piData.hasNewPost || false;
|
|
3298
|
+
|
|
3299
|
+
result.currentValue = hasNewPost ? 1 : 0;
|
|
3300
|
+
result.matchValue = hasNewPost ? 1 : 0;
|
|
3301
|
+
|
|
3302
|
+
if (hasNewPost) {
|
|
3303
|
+
result.passes = true;
|
|
3304
|
+
}
|
|
3305
|
+
break;
|
|
3306
|
+
}
|
|
3307
|
+
|
|
3308
|
+
case 'BehavioralAnomaly': {
|
|
3309
|
+
// Data format: { anomalyScore, primaryDriver, driverSignificance }
|
|
3310
|
+
const anomalyScore = piData.anomalyScore || 0;
|
|
3311
|
+
const minScore = parameters.minAnomalyScore ?? 0;
|
|
3312
|
+
|
|
3313
|
+
result.currentValue = anomalyScore;
|
|
3314
|
+
result.matchValue = anomalyScore;
|
|
3315
|
+
|
|
3316
|
+
if (anomalyScore >= minScore) {
|
|
3317
|
+
result.passes = true;
|
|
3318
|
+
}
|
|
3319
|
+
break;
|
|
3320
|
+
}
|
|
3321
|
+
|
|
3322
|
+
default:
|
|
3323
|
+
// Generic fallback - pass if there's any data
|
|
3324
|
+
result.passes = true;
|
|
3325
|
+
result.matchValue = 0;
|
|
3326
|
+
}
|
|
3327
|
+
|
|
3328
|
+
return result;
|
|
3329
|
+
}
|
|
3330
|
+
|
|
3184
3331
|
/**
|
|
3185
3332
|
* Subscribe to all PIs in a watchlist
|
|
3186
3333
|
*/
|
|
@@ -3261,7 +3408,6 @@ module.exports = {
|
|
|
3261
3408
|
requestPopularInvestorAddition,
|
|
3262
3409
|
hasUserCopiedPopularInvestor,
|
|
3263
3410
|
manageReviews,
|
|
3264
|
-
getComputationResults,
|
|
3265
3411
|
fetchNotifications,
|
|
3266
3412
|
markNotificationRead,
|
|
3267
3413
|
trackPopularInvestorView,
|
|
@@ -40,11 +40,8 @@ const watchlistSubscribeSchema = z.object({
|
|
|
40
40
|
|
|
41
41
|
const dynamicMatchesSchema = z.object({
|
|
42
42
|
computationName: z.string().min(1).max(200),
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
maxValue: z.number().optional()
|
|
46
|
-
}).optional(),
|
|
47
|
-
timeRange: z.enum(['today', 'last_7_days', 'last_30_days', 'all_time']).optional(),
|
|
43
|
+
parameters: z.record(z.any()).optional(), // Flexible parameters object for thresholds
|
|
44
|
+
timeRange: z.enum(['today', 'last_7_days', 'last_30_days']).optional(),
|
|
48
45
|
limit: z.number().int().min(1).max(500).optional()
|
|
49
46
|
});
|
|
50
47
|
|
|
@@ -234,15 +231,20 @@ router.post('/dynamic/query', async (req, res, next) => {
|
|
|
234
231
|
// Validate input
|
|
235
232
|
const validatedData = dynamicMatchesSchema.parse(req.body);
|
|
236
233
|
|
|
237
|
-
const { db } = req.dependencies;
|
|
234
|
+
const { db, logger } = req.dependencies;
|
|
235
|
+
|
|
236
|
+
logger?.log('INFO', `[POST /watchlists/dynamic/query] Querying ${validatedData.computationName} with params:`, validatedData.parameters);
|
|
237
|
+
|
|
238
238
|
const result = await queryDynamicWatchlistMatches(
|
|
239
239
|
db,
|
|
240
240
|
validatedData.computationName,
|
|
241
|
-
validatedData.
|
|
241
|
+
validatedData.parameters || {},
|
|
242
242
|
validatedData.timeRange || 'last_7_days',
|
|
243
243
|
validatedData.limit || 100
|
|
244
244
|
);
|
|
245
245
|
|
|
246
|
+
logger?.log('INFO', `[POST /watchlists/dynamic/query] Found ${result.count} matches`);
|
|
247
|
+
|
|
246
248
|
res.json(result);
|
|
247
249
|
} catch (error) {
|
|
248
250
|
if (error instanceof z.ZodError) {
|