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
|
|
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
|
-
//
|
|
1238
|
-
|
|
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
|
-
//
|
|
1248
|
-
|
|
1249
|
-
|
|
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
|
-
|
|
16
|
-
const {
|
|
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
|
|
233
|
+
let candidateDates = [];
|
|
236
234
|
if (dateInput) {
|
|
237
235
|
// Single Date Mode
|
|
238
|
-
|
|
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
|
-
|
|
242
|
+
candidateDates = getExpectedDateStrings(earliestDates.absoluteEarliest, new Date());
|
|
250
243
|
}
|
|
251
244
|
|
|
252
|
-
|
|
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
|
-
|
|
261
|
-
const
|
|
262
|
-
const
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
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
|
-
//
|
|
268
|
-
|
|
269
|
-
|
|
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
|
-
|
|
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 (
|
|
274
|
-
|
|
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
|
-
|
|
277
|
-
skippedStats[reason] = (skippedStats[reason] || 0) + 1;
|
|
294
|
+
skippedDates.push({ date: dateStr, reason: 'Not runnable (unknown reason)' });
|
|
278
295
|
}
|
|
279
296
|
}
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
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: '
|
|
305
|
+
status: 'NO_RUNNABLE_DATES',
|
|
285
306
|
computation: computationName,
|
|
286
|
-
|
|
287
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
//
|
|
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 ? '
|
|
341
|
-
|
|
359
|
+
mode: dateInput ? 'SINGLE_DATE' : 'ALL_DATES',
|
|
360
|
+
datesChecked: candidateDates.length,
|
|
361
|
+
datesRunnable: runnableDates.length,
|
|
342
362
|
datesTriggered: dispatchedCount,
|
|
343
|
-
|
|
363
|
+
skippedCount: skippedDates.length,
|
|
344
364
|
targetTopic: topic
|
|
345
365
|
};
|
|
346
366
|
}
|