bulltrackers-module 1.0.654 → 1.0.656

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,98 @@ const getComputationResults = async (db, computationName, dateStr, userId = null
1273
1277
  }
1274
1278
  };
1275
1279
 
1280
+ // 10.4. Find most recent available GlobalAumPerAsset30D data
1281
+ // Looks back up to 7 days to find the most recent available data
1282
+ const findMostRecentGlobalAumData = async (db, startDateStr, maxLookbackDays = 7) => {
1283
+ try {
1284
+ const startDate = new Date(startDateStr);
1285
+
1286
+ // Look back through dates to find the most recent available data
1287
+ for (let i = 0; i < maxLookbackDays; i++) {
1288
+ const checkDate = new Date(startDate);
1289
+ checkDate.setDate(startDate.getDate() - i);
1290
+ const dateKey = checkDate.toISOString().split('T')[0];
1291
+
1292
+ try {
1293
+ const computationData = await getComputationResults(db, 'GlobalAumPerAsset30D', dateKey);
1294
+
1295
+ // If we got valid data (not null/empty), return it
1296
+ if (computationData && typeof computationData === 'object' && Object.keys(computationData).length > 0) {
1297
+ return {
1298
+ date: dateKey,
1299
+ data: computationData
1300
+ };
1301
+ }
1302
+ } catch (error) {
1303
+ // Missing data for this date - continue looking back
1304
+ console.log(`[GlobalAum] No data found for ${dateKey}, checking previous date...`);
1305
+ }
1306
+ }
1307
+
1308
+ // No data found in the lookback period
1309
+ return null;
1310
+ } catch (error) {
1311
+ console.error(`[GlobalAum] Error finding most recent data:`, error);
1312
+ throw error;
1313
+ }
1314
+ };
1315
+
1316
+ // 10.5. Fetch GlobalAumPerAsset30D with 7-day lookback
1317
+ // Returns the last 7 days of successfully stored data for a specific ticker or all assets
1318
+ const fetchGlobalAumPerAssetWithLookback = async (db, dateStr, ticker = null, lookbackDays = 7) => {
1319
+ try {
1320
+ const endDate = new Date(dateStr);
1321
+ const results = [];
1322
+
1323
+ // Fetch data for the last 7 days, handling missing data gracefully
1324
+ for (let i = 0; i < lookbackDays; i++) {
1325
+ const checkDate = new Date(endDate);
1326
+ checkDate.setDate(endDate.getDate() - i);
1327
+ const dateKey = checkDate.toISOString().split('T')[0];
1328
+
1329
+ try {
1330
+ const computationData = await getComputationResults(db, 'GlobalAumPerAsset30D', dateKey);
1331
+
1332
+ // If we got valid data (not null/empty), add it to results
1333
+ if (computationData && typeof computationData === 'object' && Object.keys(computationData).length > 0) {
1334
+ if (ticker) {
1335
+ // If searching for a specific ticker, only include if it exists
1336
+ const tickerUpper = ticker.toUpperCase();
1337
+ if (computationData[tickerUpper] !== undefined) {
1338
+ results.push({
1339
+ date: dateKey,
1340
+ aum: computationData[tickerUpper],
1341
+ ticker: tickerUpper
1342
+ });
1343
+ }
1344
+ } else {
1345
+ // Return all assets for this date
1346
+ results.push({
1347
+ date: dateKey,
1348
+ data: computationData
1349
+ });
1350
+ }
1351
+ }
1352
+ } catch (error) {
1353
+ // Missing data for this date - skip it (graceful handling)
1354
+ console.log(`[GlobalAum] No data found for ${dateKey}, skipping`);
1355
+ }
1356
+ }
1357
+
1358
+ // Sort by date descending (newest first)
1359
+ results.sort((a, b) => {
1360
+ const dateA = new Date(a.date);
1361
+ const dateB = new Date(b.date);
1362
+ return dateB.getTime() - dateA.getTime();
1363
+ });
1364
+
1365
+ return results;
1366
+ } catch (error) {
1367
+ console.error(`[GlobalAum] Error fetching with lookback:`, error);
1368
+ throw error;
1369
+ }
1370
+ };
1371
+
1276
1372
  // 11. Fetch User Notifications
1277
1373
  const fetchNotifications = async (firestore, userId, options = {}) => {
1278
1374
  const { limit = 20, unreadOnly = false, excludeTypes = ['watchlistAlerts'] } = options;
@@ -2913,6 +3009,9 @@ const subscribeToAllWatchlistPIs = async (db, userId, watchlistId, alertTypes =
2913
3009
  module.exports = {
2914
3010
  latestUserCentricSnapshot,
2915
3011
  pageCollection,
3012
+ findMostRecentGlobalAumData,
3013
+ fetchGlobalAumPerAssetWithLookback,
3014
+ getComputationResults,
2916
3015
  fetchPopularInvestorMasterList,
2917
3016
  isDeveloper,
2918
3017
  lookupCidByEmail,
@@ -8,10 +8,13 @@ const {
8
8
  getComputationResults,
9
9
  requestPopularInvestorAddition,
10
10
  trackPopularInvestorView,
11
- pageCollection
11
+ pageCollection,
12
+ findMostRecentGlobalAumData,
13
+ fetchGlobalAumPerAssetWithLookback
12
14
  } = require('../helpers/data-fetchers/firestore.js');
13
15
  const { sanitizeCid } = require('../helpers/security_utils.js');
14
16
  const { timeouts } = require('../helpers/timeout_utils.js');
17
+ const { requireFirebaseAuth } = require('../middleware/firebase_auth_middleware.js');
15
18
 
16
19
  const router = express.Router();
17
20
 
@@ -255,5 +258,93 @@ router.get('/:piId/analytics', async (req, res) => {
255
258
  }
256
259
  });
257
260
 
261
+ /**
262
+ * GLOBAL AUM PER ASSET - Public for all signed-in users
263
+ *
264
+ * These routes show how much $ is invested in each asset based on AUM totals of all Popular Investors.
265
+ * - Uses GlobalAumPerAsset30D computation
266
+ * - Access: Public (any signed-in user can view)
267
+ * - Requires Firebase authentication
268
+ */
269
+
270
+ // GET /popular-investors/global-aum - Get top assets by AUM (default: top 10)
271
+ router.get('/global-aum', requireFirebaseAuth, async (req, res, next) => {
272
+ try {
273
+ const { db } = req.dependencies;
274
+ const { date, limit = 10 } = req.query;
275
+
276
+ // Default to today if no date provided
277
+ const targetDate = date || new Date().toISOString().split('T')[0];
278
+
279
+ // Find the most recent available data (looks back up to 7 days if today's data doesn't exist)
280
+ const result = await findMostRecentGlobalAumData(db, targetDate, 7);
281
+
282
+ if (!result || !result.data || Object.keys(result.data).length === 0) {
283
+ return res.status(404).json({
284
+ success: false,
285
+ error: 'No AUM data available in the last 7 days'
286
+ });
287
+ }
288
+
289
+ // Convert to array and sort by AUM (descending)
290
+ const assetsArray = Object.entries(result.data)
291
+ .map(([ticker, aum]) => ({ ticker, aum: Number(aum) }))
292
+ .sort((a, b) => b.aum - a.aum)
293
+ .slice(0, parseInt(limit) || 10);
294
+
295
+ res.json({
296
+ success: true,
297
+ date: result.date, // Return the actual date of the data found
298
+ count: assetsArray.length,
299
+ data: assetsArray
300
+ });
301
+ } catch (error) {
302
+ next(error);
303
+ }
304
+ });
305
+
306
+ // GET /popular-investors/global-aum/:ticker - Get 7-day AUM history for a specific ticker
307
+ router.get('/global-aum/:ticker', requireFirebaseAuth, async (req, res, next) => {
308
+ try {
309
+ const { db } = req.dependencies;
310
+ const { ticker } = req.params;
311
+ const { date, lookback = 7 } = req.query;
312
+
313
+ // Validate ticker
314
+ if (!ticker || typeof ticker !== 'string' || ticker.length === 0) {
315
+ return res.status(400).json({
316
+ success: false,
317
+ error: 'Invalid ticker symbol'
318
+ });
319
+ }
320
+
321
+ // Default to today if no date provided
322
+ const targetDate = date || new Date().toISOString().split('T')[0];
323
+
324
+ // Validate lookback parameter
325
+ const lookbackDays = Math.min(Math.max(parseInt(lookback) || 7, 1), 30); // Between 1 and 30 days
326
+
327
+ // Fetch 7-day lookback data for the ticker
328
+ const results = await fetchGlobalAumPerAssetWithLookback(db, targetDate, ticker, lookbackDays);
329
+
330
+ if (results.length === 0) {
331
+ return res.status(404).json({
332
+ success: false,
333
+ error: `No AUM data found for ticker ${ticker.toUpperCase()} in the last ${lookbackDays} days`
334
+ });
335
+ }
336
+
337
+ res.json({
338
+ success: true,
339
+ ticker: ticker.toUpperCase(),
340
+ lookbackDays: lookbackDays,
341
+ count: results.length,
342
+ data: results
343
+ });
344
+ } catch (error) {
345
+ next(error);
346
+ }
347
+ });
348
+
258
349
 
259
350
  module.exports = router;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bulltrackers-module",
3
- "version": "1.0.654",
3
+ "version": "1.0.656",
4
4
  "description": "Helper Functions for Bulltrackers.",
5
5
  "main": "index.js",
6
6
  "files": [