bulltrackers-module 1.0.653 → 1.0.655

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.
@@ -1222,7 +1222,7 @@ const getComputationResults = async (db, computationName, dateStr, userId = null
1222
1222
  const shardCount = pointerData._shardCount || 0;
1223
1223
  console.log(`[Computation] Reassembling ${shardCount} shards for ${computationName}`);
1224
1224
 
1225
- if (shardCount === 0) return []; // Or empty object
1225
+ if (shardCount === 0) return {}; // Return empty object for object-based results
1226
1226
 
1227
1227
  // Create an array of promises to fetch all shards in parallel
1228
1228
  const shardPromises = [];
@@ -1233,10 +1233,9 @@ const getComputationResults = async (db, computationName, dateStr, userId = null
1233
1233
 
1234
1234
  const shardSnaps = await Promise.all(shardPromises);
1235
1235
 
1236
- // Reassemble data.
1237
- // Assuming shards contain lists of data that need concatenation.
1238
- // Adjust logic if shards contain maps that need merging.
1239
- let reassembledData = [];
1236
+ // Reassemble data by merging objects (for object-based results like GlobalAumPerAsset30D)
1237
+ // Shards contain partial objects that need to be merged together
1238
+ let reassembledData = {};
1240
1239
 
1241
1240
  shardSnaps.forEach((snap, index) => {
1242
1241
  if (snap.exists) {
@@ -1244,9 +1243,14 @@ const getComputationResults = async (db, computationName, dateStr, userId = null
1244
1243
  // If the shard itself is compressed (common in big data), decompress it
1245
1244
  const content = (data._compressed) ? tryDecompress(data) : data;
1246
1245
 
1247
- // Push to master list. Assuming standard structure { items: [...] } or Array
1248
- // If specific structure isn't known, we push the whole object.
1249
- reassembledData.push(content);
1246
+ // Merge shard contents, ignoring internal metadata fields
1247
+ if (content && typeof content === 'object') {
1248
+ Object.entries(content).forEach(([k, v]) => {
1249
+ if (!k.startsWith('_')) {
1250
+ reassembledData[k] = v;
1251
+ }
1252
+ });
1253
+ }
1250
1254
  } else {
1251
1255
  console.warn(`[Computation] Missing shard_${index} for ${computationName}`);
1252
1256
  }
@@ -1273,6 +1277,62 @@ const getComputationResults = async (db, computationName, dateStr, userId = null
1273
1277
  }
1274
1278
  };
1275
1279
 
1280
+ // 10.5. Fetch GlobalAumPerAsset30D with 7-day lookback
1281
+ // Returns the last 7 days of successfully stored data for a specific ticker or all assets
1282
+ const fetchGlobalAumPerAssetWithLookback = async (db, dateStr, ticker = null, lookbackDays = 7) => {
1283
+ try {
1284
+ const endDate = new Date(dateStr);
1285
+ const results = [];
1286
+
1287
+ // Fetch data for the last 7 days, handling missing data gracefully
1288
+ for (let i = 0; i < lookbackDays; i++) {
1289
+ const checkDate = new Date(endDate);
1290
+ checkDate.setDate(endDate.getDate() - i);
1291
+ const dateKey = checkDate.toISOString().split('T')[0];
1292
+
1293
+ try {
1294
+ const computationData = await getComputationResults(db, 'GlobalAumPerAsset30D', dateKey);
1295
+
1296
+ // If we got valid data (not null/empty), add it to results
1297
+ if (computationData && typeof computationData === 'object' && Object.keys(computationData).length > 0) {
1298
+ if (ticker) {
1299
+ // If searching for a specific ticker, only include if it exists
1300
+ const tickerUpper = ticker.toUpperCase();
1301
+ if (computationData[tickerUpper] !== undefined) {
1302
+ results.push({
1303
+ date: dateKey,
1304
+ aum: computationData[tickerUpper],
1305
+ ticker: tickerUpper
1306
+ });
1307
+ }
1308
+ } else {
1309
+ // Return all assets for this date
1310
+ results.push({
1311
+ date: dateKey,
1312
+ data: computationData
1313
+ });
1314
+ }
1315
+ }
1316
+ } catch (error) {
1317
+ // Missing data for this date - skip it (graceful handling)
1318
+ console.log(`[GlobalAum] No data found for ${dateKey}, skipping`);
1319
+ }
1320
+ }
1321
+
1322
+ // Sort by date descending (newest first)
1323
+ results.sort((a, b) => {
1324
+ const dateA = new Date(a.date);
1325
+ const dateB = new Date(b.date);
1326
+ return dateB.getTime() - dateA.getTime();
1327
+ });
1328
+
1329
+ return results;
1330
+ } catch (error) {
1331
+ console.error(`[GlobalAum] Error fetching with lookback:`, error);
1332
+ throw error;
1333
+ }
1334
+ };
1335
+
1276
1336
  // 11. Fetch User Notifications
1277
1337
  const fetchNotifications = async (firestore, userId, options = {}) => {
1278
1338
  const { limit = 20, unreadOnly = false, excludeTypes = ['watchlistAlerts'] } = options;
@@ -2913,6 +2973,8 @@ const subscribeToAllWatchlistPIs = async (db, userId, watchlistId, alertTypes =
2913
2973
  module.exports = {
2914
2974
  latestUserCentricSnapshot,
2915
2975
  pageCollection,
2976
+ fetchGlobalAumPerAssetWithLookback,
2977
+ getComputationResults,
2916
2978
  fetchPopularInvestorMasterList,
2917
2979
  isDeveloper,
2918
2980
  lookupCidByEmail,
@@ -8,10 +8,12 @@ const {
8
8
  getComputationResults,
9
9
  requestPopularInvestorAddition,
10
10
  trackPopularInvestorView,
11
- pageCollection
11
+ pageCollection,
12
+ fetchGlobalAumPerAssetWithLookback
12
13
  } = require('../helpers/data-fetchers/firestore.js');
13
14
  const { sanitizeCid } = require('../helpers/security_utils.js');
14
15
  const { timeouts } = require('../helpers/timeout_utils.js');
16
+ const { requireFirebaseAuth } = require('../middleware/firebase_auth_middleware.js');
15
17
 
16
18
  const router = express.Router();
17
19
 
@@ -255,5 +257,93 @@ router.get('/:piId/analytics', async (req, res) => {
255
257
  }
256
258
  });
257
259
 
260
+ /**
261
+ * GLOBAL AUM PER ASSET - Public for all signed-in users
262
+ *
263
+ * These routes show how much $ is invested in each asset based on AUM totals of all Popular Investors.
264
+ * - Uses GlobalAumPerAsset30D computation
265
+ * - Access: Public (any signed-in user can view)
266
+ * - Requires Firebase authentication
267
+ */
268
+
269
+ // GET /popular-investors/global-aum - Get top assets by AUM (default: top 10)
270
+ router.get('/global-aum', requireFirebaseAuth, async (req, res, next) => {
271
+ try {
272
+ const { db } = req.dependencies;
273
+ const { date, limit = 10 } = req.query;
274
+
275
+ // Default to today if no date provided
276
+ const targetDate = date || new Date().toISOString().split('T')[0];
277
+
278
+ // Fetch the latest computation result
279
+ const computationData = await getComputationResults(db, 'GlobalAumPerAsset30D', targetDate);
280
+
281
+ if (!computationData || typeof computationData !== 'object' || Object.keys(computationData).length === 0) {
282
+ return res.status(404).json({
283
+ success: false,
284
+ error: 'No AUM data available for the specified date'
285
+ });
286
+ }
287
+
288
+ // Convert to array and sort by AUM (descending)
289
+ const assetsArray = Object.entries(computationData)
290
+ .map(([ticker, aum]) => ({ ticker, aum: Number(aum) }))
291
+ .sort((a, b) => b.aum - a.aum)
292
+ .slice(0, parseInt(limit) || 10);
293
+
294
+ res.json({
295
+ success: true,
296
+ date: targetDate,
297
+ count: assetsArray.length,
298
+ data: assetsArray
299
+ });
300
+ } catch (error) {
301
+ next(error);
302
+ }
303
+ });
304
+
305
+ // GET /popular-investors/global-aum/:ticker - Get 7-day AUM history for a specific ticker
306
+ router.get('/global-aum/:ticker', requireFirebaseAuth, async (req, res, next) => {
307
+ try {
308
+ const { db } = req.dependencies;
309
+ const { ticker } = req.params;
310
+ const { date, lookback = 7 } = req.query;
311
+
312
+ // Validate ticker
313
+ if (!ticker || typeof ticker !== 'string' || ticker.length === 0) {
314
+ return res.status(400).json({
315
+ success: false,
316
+ error: 'Invalid ticker symbol'
317
+ });
318
+ }
319
+
320
+ // Default to today if no date provided
321
+ const targetDate = date || new Date().toISOString().split('T')[0];
322
+
323
+ // Validate lookback parameter
324
+ const lookbackDays = Math.min(Math.max(parseInt(lookback) || 7, 1), 30); // Between 1 and 30 days
325
+
326
+ // Fetch 7-day lookback data for the ticker
327
+ const results = await fetchGlobalAumPerAssetWithLookback(db, targetDate, ticker, lookbackDays);
328
+
329
+ if (results.length === 0) {
330
+ return res.status(404).json({
331
+ success: false,
332
+ error: `No AUM data found for ticker ${ticker.toUpperCase()} in the last ${lookbackDays} days`
333
+ });
334
+ }
335
+
336
+ res.json({
337
+ success: true,
338
+ ticker: ticker.toUpperCase(),
339
+ lookbackDays: lookbackDays,
340
+ count: results.length,
341
+ data: results
342
+ });
343
+ } catch (error) {
344
+ next(error);
345
+ }
346
+ });
347
+
258
348
 
259
349
  module.exports = router;
@@ -5,16 +5,14 @@
5
5
  * UPDATED: Enforces Strict One-Shot Policy (Standard -> HighMem -> Dead Letter).
6
6
  * UPDATED: Generates Google Cloud Trace Context (traceId/spanId) for end-to-end monitoring.
7
7
  * UPDATED: Added Schedule Awareness (Daily, Weekly, Monthly) to filter tasks by date.
8
- * UPDATED: Force Run now validates Root Data Availability before dispatching.
9
8
  */
10
9
 
11
10
  const { getExpectedDateStrings, getEarliestDataDates, normalizeName, DEFINITIVE_EARLIEST_DATES } = require('../utils/utils.js');
12
11
  const { groupByPass, analyzeDateExecution } = require('../WorkflowOrchestrator.js');
13
12
  const { PubSubUtils } = require('../../core/utils/pubsub_utils');
14
13
  const { fetchComputationStatus } = require('../persistence/StatusRepository');
15
- // [UPDATED] Imported getAvailabilityWindow and checkRootDependencies
16
- const { checkRootDataAvailability, getAvailabilityWindow, checkRootDependencies } = require('../data/AvailabilityChecker');
17
- const { runFinalSweepCheck } = require('../tools/FinalSweepReporter');
14
+ const { checkRootDataAvailability } = require('../data/AvailabilityChecker');
15
+ const { runFinalSweepCheck } = require('../tools/FinalSweepReporter'); // [NEW]
18
16
  const crypto = require('crypto');
19
17
 
20
18
  const OOM_THRESHOLD_MB = 1500; // Unused
@@ -232,71 +230,92 @@ async function handleForceRun(config, dependencies, computationManifest, reqBody
232
230
  }
233
231
 
234
232
  // 2. Determine Target Dates
235
- let targetDates = [];
233
+ let candidateDates = [];
236
234
  if (dateInput) {
237
235
  // Single Date Mode
238
- // We still perform the check, but for a single item array
239
- if (Array.isArray(dateInput)) {
240
- targetDates = dateInput;
241
- } else {
242
- targetDates = [dateInput];
243
- }
236
+ candidateDates = [dateInput];
244
237
  } else {
245
238
  // All Dates Mode (Backfill)
246
239
  logger.log('INFO', `[ForceRun] No date provided. Calculating date range for ${computationName}...`);
247
240
  const earliestDates = await getEarliestDataDates(config, dependencies);
248
241
  // Calculate from system start until today
249
- targetDates = getExpectedDateStrings(earliestDates.absoluteEarliest, new Date());
242
+ candidateDates = getExpectedDateStrings(earliestDates.absoluteEarliest, new Date());
250
243
  }
251
244
 
252
- // [NEW] 3. Filter Impossible Dates (Availability Check)
253
- logger.log('INFO', `[ForceRun] 🔍 Validating data availability for ${targetDates.length} candidate dates...`);
254
-
255
- // Sort dates to get efficient min/max for range query
256
- targetDates.sort();
257
- const startDate = targetDates[0];
258
- const endDate = targetDates[targetDates.length - 1];
245
+ logger.log('INFO', `[ForceRun] Checking ${candidateDates.length} candidate dates for runnability...`);
259
246
 
260
- const availabilityMap = await getAvailabilityWindow(dependencies, startDate, endDate);
261
- const validDates = [];
262
- const skippedStats = {};
263
-
264
- for (const date of targetDates) {
265
- const status = availabilityMap.get(date);
247
+ // 3. Filter to only runnable dates using analyzeDateExecution
248
+ const manifestMap = new Map(computationManifest.map(c => [normalizeName(c.name), c]));
249
+ const calcsInPass = groupByPass(computationManifest, manifestItem.pass || "1");
250
+ const targetComputationNormalized = normalizeName(computationName);
251
+
252
+ // Filter to only the target computation
253
+ const targetCalcs = calcsInPass.filter(c => normalizeName(c.name) === targetComputationNormalized);
254
+
255
+ if (targetCalcs.length === 0) {
256
+ throw new Error(`Computation '${computationName}' not found in pass ${manifestItem.pass || "1"}`);
257
+ }
258
+
259
+ const runnableDates = [];
260
+ const skippedDates = [];
261
+
262
+ for (const dateStr of candidateDates) {
263
+ // Check root data availability
264
+ const rootDataStatus = await checkRootDataAvailability(dateStr, config, dependencies, DEFINITIVE_EARLIEST_DATES);
265
+
266
+ // Get computation status for this date
267
+ const dailyStatus = await fetchComputationStatus(dateStr, config, dependencies);
266
268
 
267
- // If no index exists, we pass an empty object (AvailabilityChecker will fail all checks)
268
- // This effectively filters out dates where we have NO knowledge of data
269
- const effectiveStatus = status || {};
269
+ // Check previous day status if needed
270
+ let prevDailyStatus = null;
271
+ if (targetCalcs.some(c => c.isHistorical)) {
272
+ const prevDate = new Date(dateStr + 'T00:00:00Z');
273
+ prevDate.setUTCDate(prevDate.getUTCDate() - 1);
274
+ prevDailyStatus = await fetchComputationStatus(prevDate.toISOString().slice(0, 10), config, dependencies);
275
+ }
276
+
277
+ // Analyze if this computation can run on this date
278
+ const report = analyzeDateExecution(dateStr, targetCalcs, rootDataStatus, dailyStatus, manifestMap, prevDailyStatus);
270
279
 
271
- const check = checkRootDependencies(manifestItem, effectiveStatus);
280
+ // Check if the target computation is runnable, needs re-run, or has failed dependencies
281
+ const isRunnable = report.runnable.some(t => normalizeName(t.name) === targetComputationNormalized);
282
+ const needsReRun = report.reRuns.some(t => normalizeName(t.name) === targetComputationNormalized);
283
+ const hasFailedDep = report.failedDependency.some(t => normalizeName(t.name) === targetComputationNormalized);
284
+ const isImpossible = report.impossible.some(t => normalizeName(t.name) === targetComputationNormalized);
285
+ const isBlocked = report.blocked.some(t => normalizeName(t.name) === targetComputationNormalized);
272
286
 
273
- if (check.canRun) {
274
- validDates.push(date);
287
+ if (isRunnable || needsReRun || hasFailedDep) {
288
+ runnableDates.push(dateStr);
289
+ } else if (isImpossible) {
290
+ skippedDates.push({ date: dateStr, reason: report.impossible.find(t => normalizeName(t.name) === targetComputationNormalized)?.reason || 'Impossible' });
291
+ } else if (isBlocked) {
292
+ skippedDates.push({ date: dateStr, reason: report.blocked.find(t => normalizeName(t.name) === targetComputationNormalized)?.reason || 'Blocked' });
275
293
  } else {
276
- const reason = check.missing.length > 0 ? `Missing: ${check.missing.join(', ')}` : 'Dependencies not met';
277
- skippedStats[reason] = (skippedStats[reason] || 0) + 1;
294
+ skippedDates.push({ date: dateStr, reason: 'Not runnable (unknown reason)' });
278
295
  }
279
296
  }
280
-
281
- if (validDates.length === 0) {
282
- logger.log('WARN', `[ForceRun] 🛑 ABORTING: No valid dates found for ${computationName} out of ${targetDates.length} requested.`);
297
+
298
+ logger.log('INFO', `[ForceRun] ✅ Found ${runnableDates.length} runnable dates out of ${candidateDates.length} candidates`);
299
+ if (skippedDates.length > 0) {
300
+ logger.log('INFO', `[ForceRun] ⏭️ Skipped ${skippedDates.length} dates: ${skippedDates.slice(0, 5).map(s => `${s.date} (${s.reason})`).join(', ')}${skippedDates.length > 5 ? '...' : ''}`);
301
+ }
302
+
303
+ if (runnableDates.length === 0) {
283
304
  return {
284
- status: 'ABORTED',
305
+ status: 'NO_RUNNABLE_DATES',
285
306
  computation: computationName,
286
- reason: 'NO_DATA_AVAILABLE',
287
- skippedDetails: skippedStats
307
+ mode: dateInput ? 'SINGLE_DATE' : 'ALL_DATES',
308
+ datesChecked: candidateDates.length,
309
+ datesRunnable: 0,
310
+ skippedReasons: skippedDates.slice(0, 10)
288
311
  };
289
312
  }
313
+
314
+ logger.log('WARN', `[ForceRun] 🚨 MANUALLY Triggering ${computationName} for ${runnableDates.length} runnable dates. Pass: ${manifestItem.pass}`);
290
315
 
291
- if (validDates.length < targetDates.length) {
292
- logger.log('INFO', `[ForceRun] ⚠️ Filtered impossible dates: ${targetDates.length} requested -> ${validDates.length} valid.`, { skippedStats });
293
- }
294
-
295
- logger.log('WARN', `[ForceRun] 🚨 MANUALLY Triggering ${computationName} for ${validDates.length} VALID dates. Pass: ${manifestItem.pass}`);
296
-
297
- // 4. Construct Tasks
316
+ // 4. Construct Tasks (only for runnable dates)
298
317
  const dispatchId = crypto.randomUUID();
299
- const tasks = validDates.map(date => {
318
+ const tasks = runnableDates.map(date => {
300
319
  const traceId = crypto.randomBytes(16).toString('hex');
301
320
  const spanId = crypto.randomBytes(8).toString('hex');
302
321
  return {
@@ -312,7 +331,7 @@ async function handleForceRun(config, dependencies, computationManifest, reqBody
312
331
  };
313
332
  });
314
333
 
315
- // 5. Batch Publish (Chunked to stay under Pub/Sub limits)
334
+ // 4. Batch Publish (Chunked to stay under Pub/Sub limits)
316
335
  const CHUNK_SIZE = 250; // Safe batch size
317
336
  const topic = (reqBody.resources === 'high-mem')
318
337
  ? (config.computationTopicHighMem || 'computation-tasks-highmem')
@@ -337,10 +356,11 @@ async function handleForceRun(config, dependencies, computationManifest, reqBody
337
356
  return {
338
357
  status: 'FORCED',
339
358
  computation: computationName,
340
- mode: dateInput ? 'SINGLE/ARRAY_DATE' : 'ALL_DATES',
341
- datesRequested: targetDates.length,
359
+ mode: dateInput ? 'SINGLE_DATE' : 'ALL_DATES',
360
+ datesChecked: candidateDates.length,
361
+ datesRunnable: runnableDates.length,
342
362
  datesTriggered: dispatchedCount,
343
- skippedImpossible: targetDates.length - dispatchedCount,
363
+ skippedCount: skippedDates.length,
344
364
  targetTopic: topic
345
365
  };
346
366
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bulltrackers-module",
3
- "version": "1.0.653",
3
+ "version": "1.0.655",
4
4
  "description": "Helper Functions for Bulltrackers.",
5
5
  "main": "index.js",
6
6
  "files": [