bulltrackers-module 1.0.535 → 1.0.537
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.
- package/functions/computation-system/context/ContextFactory.js +4 -2
- package/functions/computation-system/data/AvailabilityChecker.js +4 -1
- package/functions/computation-system/data/CachedDataLoader.js +11 -2
- package/functions/computation-system/executors/StandardExecutor.js +72 -32
- package/functions/computation-system/layers/extractors.js +35 -1
- package/functions/computation-system/utils/data_loader.js +33 -1
- package/functions/root-data-indexer/index.js +13 -2
- package/package.json +1 -1
|
@@ -27,7 +27,8 @@ class ContextFactory {
|
|
|
27
27
|
allRankings, allRankingsYesterday, // Global rank lists
|
|
28
28
|
allVerifications,
|
|
29
29
|
// [NEW] New Root Data Types for Profile Metrics
|
|
30
|
-
ratings, pageViews, watchlistMembership, alertHistory
|
|
30
|
+
ratings, pageViews, watchlistMembership, alertHistory,
|
|
31
|
+
piMasterList, // [NEW]
|
|
31
32
|
} = options;
|
|
32
33
|
|
|
33
34
|
return {
|
|
@@ -56,7 +57,8 @@ class ContextFactory {
|
|
|
56
57
|
ratings: ratings || {},
|
|
57
58
|
pageViews: pageViews || {},
|
|
58
59
|
watchlistMembership: watchlistMembership || {},
|
|
59
|
-
alertHistory: alertHistory || {}
|
|
60
|
+
alertHistory: alertHistory || {},
|
|
61
|
+
piMasterList: piMasterList || {}, // [NEW]
|
|
60
62
|
}
|
|
61
63
|
};
|
|
62
64
|
}
|
|
@@ -229,7 +229,10 @@ async function checkRootDataAvailability(dateStr, config, dependencies, earliest
|
|
|
229
229
|
piRatings: !!details.piRatings,
|
|
230
230
|
piPageViews: !!details.piPageViews,
|
|
231
231
|
watchlistMembership: !!details.watchlistMembership,
|
|
232
|
-
piAlertHistory: !!details.piAlertHistory
|
|
232
|
+
piAlertHistory: !!details.piAlertHistory,
|
|
233
|
+
|
|
234
|
+
// [NEW] Global Helper Data
|
|
235
|
+
piMasterList: !!details.piMasterList
|
|
233
236
|
},
|
|
234
237
|
portfolioRefs: null,
|
|
235
238
|
historyRefs: null,
|
|
@@ -14,7 +14,8 @@ const {
|
|
|
14
14
|
loadPIRatings, // [NEW]
|
|
15
15
|
loadPIPageViews, // [NEW]
|
|
16
16
|
loadWatchlistMembership: loadWatchlistMembershipData, // [NEW] Renamed to avoid conflict
|
|
17
|
-
loadPIAlertHistory // [NEW]
|
|
17
|
+
loadPIAlertHistory, // [NEW]
|
|
18
|
+
loadPopularInvestorMasterList // [NEW]
|
|
18
19
|
} = require('../utils/data_loader');
|
|
19
20
|
const zlib = require('zlib');
|
|
20
21
|
|
|
@@ -31,7 +32,8 @@ class CachedDataLoader {
|
|
|
31
32
|
ratings: new Map(), // [NEW]
|
|
32
33
|
pageViews: new Map(), // [NEW]
|
|
33
34
|
watchlistMembership: new Map(), // [NEW]
|
|
34
|
-
alertHistory: new Map()
|
|
35
|
+
alertHistory: new Map(),// [NEW]
|
|
36
|
+
piMasterList: null // [NEW] Singleton cache (not date dependent)
|
|
35
37
|
};
|
|
36
38
|
}
|
|
37
39
|
|
|
@@ -141,6 +143,13 @@ class CachedDataLoader {
|
|
|
141
143
|
return {};
|
|
142
144
|
}
|
|
143
145
|
}
|
|
146
|
+
// [NEW] Load PI Master List (Global, Cached)
|
|
147
|
+
async loadPIMasterList() {
|
|
148
|
+
if (this.cache.piMasterList) return this.cache.piMasterList;
|
|
149
|
+
const data = await loadPopularInvestorMasterList(this.config, this.deps);
|
|
150
|
+
this.cache.piMasterList = data;
|
|
151
|
+
return data;
|
|
152
|
+
}
|
|
144
153
|
}
|
|
145
154
|
|
|
146
155
|
module.exports = { CachedDataLoader };
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* }
|
|
6
6
|
*/
|
|
7
7
|
const { normalizeName, getEarliestDataDates } = require('../utils/utils');
|
|
8
|
-
const { streamPortfolioData, streamHistoryData, getPortfolioPartRefs } = require('../utils/data_loader');
|
|
8
|
+
const { streamPortfolioData, streamHistoryData, getPortfolioPartRefs, getHistoryPartRefs } = require('../utils/data_loader');
|
|
9
9
|
const { CachedDataLoader } = require('../data/CachedDataLoader');
|
|
10
10
|
const { ContextFactory } = require('../context/ContextFactory');
|
|
11
11
|
const { commitResults } = require('../persistence/ResultCommitter');
|
|
@@ -24,15 +24,31 @@ class StandardExecutor {
|
|
|
24
24
|
const type = (c.userType || 'ALL').toUpperCase();
|
|
25
25
|
requiredUserTypes.add(type);
|
|
26
26
|
});
|
|
27
|
-
// If any calc requires 'ALL' (or has no userType), we fetch everything
|
|
28
27
|
const userTypeArray = requiredUserTypes.has('ALL') ? null : Array.from(requiredUserTypes);
|
|
29
28
|
|
|
29
|
+
// [OPTIMIZATION] Check for Target CID in manifests (On-Demand Optimization)
|
|
30
|
+
// If present, we will filter all data streams to strictly this user
|
|
31
|
+
const targetCid = calcs.find(c => c.targetCid)?.targetCid || calcs.find(c => c.manifest?.targetCid)?.manifest?.targetCid;
|
|
32
|
+
if (targetCid) {
|
|
33
|
+
logger.log('INFO', `[StandardExecutor] Running in Targeted Mode for CID: ${targetCid}`);
|
|
34
|
+
}
|
|
35
|
+
|
|
30
36
|
const fullRoot = { ...rootData };
|
|
31
37
|
if (calcs.some(c => c.isHistorical)) {
|
|
32
38
|
const prev = new Date(date); prev.setUTCDate(prev.getUTCDate() - 1);
|
|
33
39
|
const prevStr = prev.toISOString().slice(0, 10);
|
|
34
|
-
|
|
35
|
-
|
|
40
|
+
|
|
41
|
+
// Fetch yesterday's refs
|
|
42
|
+
let yRefs = await getPortfolioPartRefs(config, deps, prevStr, userTypeArray);
|
|
43
|
+
|
|
44
|
+
// [OPTIMIZATION] Filter Yesterday's Refs if targetCid is set
|
|
45
|
+
if (targetCid && yRefs) {
|
|
46
|
+
const originalCount = yRefs.length;
|
|
47
|
+
yRefs = yRefs.filter(r => !r.cid || String(r.cid) === String(targetCid));
|
|
48
|
+
logger.log('INFO', `[StandardExecutor] Filtered Yesterday's Refs: ${originalCount} -> ${yRefs.length}`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
fullRoot.yesterdayPortfolioRefs = yRefs;
|
|
36
52
|
}
|
|
37
53
|
|
|
38
54
|
const state = {};
|
|
@@ -46,18 +62,48 @@ class StandardExecutor {
|
|
|
46
62
|
} catch (e) { logger.log('WARN', `Failed to init ${c.name}`); }
|
|
47
63
|
}
|
|
48
64
|
|
|
49
|
-
|
|
65
|
+
// Pass targetCid to streamAndProcess
|
|
66
|
+
return await StandardExecutor.streamAndProcess(
|
|
67
|
+
dStr, state, passName, config, deps, fullRoot,
|
|
68
|
+
rootData.portfolioRefs, rootData.historyRefs,
|
|
69
|
+
fetchedDeps, previousFetchedDeps, skipStatusWrite,
|
|
70
|
+
userTypeArray, targetCid
|
|
71
|
+
);
|
|
50
72
|
}
|
|
51
73
|
|
|
52
|
-
// [UPDATED]
|
|
53
|
-
static async streamAndProcess(dateStr, state, passName, config, deps, rootData, portfolioRefs, historyRefs, fetchedDeps, previousFetchedDeps, skipStatusWrite, requiredUserTypes = null) {
|
|
74
|
+
// [UPDATED] Added targetCid param
|
|
75
|
+
static async streamAndProcess(dateStr, state, passName, config, deps, rootData, portfolioRefs, historyRefs, fetchedDeps, previousFetchedDeps, skipStatusWrite, requiredUserTypes = null, targetCid = null) {
|
|
54
76
|
const { logger } = deps;
|
|
55
77
|
const calcs = Object.values(state).filter(c => c && c.manifest);
|
|
56
78
|
const streamingCalcs = calcs.filter(c => c.manifest.rootDataDependencies.includes('portfolio') || c.manifest.rootDataDependencies.includes('history'));
|
|
57
79
|
|
|
58
80
|
if (streamingCalcs.length === 0) return { successUpdates: {}, failureReport: [] };
|
|
81
|
+
|
|
82
|
+
// --- 1. Resolve and Filter Portfolio Refs (Today) ---
|
|
83
|
+
let effectivePortfolioRefs = portfolioRefs;
|
|
84
|
+
if (!effectivePortfolioRefs) {
|
|
85
|
+
// If refs weren't provided by AvailabilityChecker, fetch them now
|
|
86
|
+
effectivePortfolioRefs = await getPortfolioPartRefs(config, deps, dateStr, requiredUserTypes);
|
|
87
|
+
}
|
|
88
|
+
if (targetCid && effectivePortfolioRefs) {
|
|
89
|
+
// Filter: Keep only refs that match the CID (or Legacy refs without CID)
|
|
90
|
+
effectivePortfolioRefs = effectivePortfolioRefs.filter(r => !r.cid || String(r.cid) === String(targetCid));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// --- 2. Resolve and Filter History Refs ---
|
|
94
|
+
let effectiveHistoryRefs = historyRefs;
|
|
95
|
+
const needsTradingHistory = streamingCalcs.some(c => c.manifest.rootDataDependencies.includes('history'));
|
|
59
96
|
|
|
60
|
-
|
|
97
|
+
if (needsTradingHistory) {
|
|
98
|
+
if (!effectiveHistoryRefs) {
|
|
99
|
+
effectiveHistoryRefs = await getHistoryPartRefs(config, deps, dateStr, requiredUserTypes);
|
|
100
|
+
}
|
|
101
|
+
if (targetCid && effectiveHistoryRefs) {
|
|
102
|
+
effectiveHistoryRefs = effectiveHistoryRefs.filter(r => !r.cid || String(r.cid) === String(targetCid));
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
let totalReadOps = (effectivePortfolioRefs?.length || 0) + (effectiveHistoryRefs?.length || 0);
|
|
61
107
|
if (rootData.yesterdayPortfolioRefs) totalReadOps += rootData.yesterdayPortfolioRefs.length;
|
|
62
108
|
totalReadOps += 2;
|
|
63
109
|
|
|
@@ -83,6 +129,7 @@ class StandardExecutor {
|
|
|
83
129
|
const setupDuration = performance.now() - startSetup;
|
|
84
130
|
Object.keys(executionStats).forEach(name => executionStats[name].timings.setup += setupDuration);
|
|
85
131
|
|
|
132
|
+
// Yesterday's Refs are already filtered in run()
|
|
86
133
|
const prevDate = new Date(dateStr + 'T00:00:00Z'); prevDate.setUTCDate(prevDate.getUTCDate() - 1);
|
|
87
134
|
const prevDateStr = prevDate.toISOString().slice(0, 10);
|
|
88
135
|
|
|
@@ -91,17 +138,13 @@ class StandardExecutor {
|
|
|
91
138
|
earliestDates = await getEarliestDataDates(config, deps);
|
|
92
139
|
}
|
|
93
140
|
|
|
94
|
-
// [FIX]
|
|
95
|
-
|
|
96
|
-
const tP_iter = streamPortfolioData(config, deps, dateStr, portfolioRefs, requiredUserTypes);
|
|
141
|
+
// [FIX] Use effective/filtered refs
|
|
142
|
+
const tP_iter = streamPortfolioData(config, deps, dateStr, effectivePortfolioRefs, requiredUserTypes);
|
|
97
143
|
|
|
98
144
|
const needsYesterdayPortfolio = streamingCalcs.some(c => c.manifest.isHistorical);
|
|
99
|
-
// Yesterday's refs were already filtered in run(), so we pass them directly
|
|
100
145
|
const yP_iter = (needsYesterdayPortfolio && rootData.yesterdayPortfolioRefs) ? streamPortfolioData(config, deps, prevDateStr, rootData.yesterdayPortfolioRefs) : null;
|
|
101
146
|
|
|
102
|
-
const
|
|
103
|
-
// [FIX] Pass requiredUserTypes to streamHistoryData
|
|
104
|
-
const tH_iter = (needsTradingHistory) ? streamHistoryData(config, deps, dateStr, historyRefs, requiredUserTypes) : null;
|
|
147
|
+
const tH_iter = (needsTradingHistory) ? streamHistoryData(config, deps, dateStr, effectiveHistoryRefs, requiredUserTypes) : null;
|
|
105
148
|
|
|
106
149
|
let yP_chunk = {}, tH_chunk = {};
|
|
107
150
|
let usersSinceLastFlush = 0;
|
|
@@ -159,7 +202,7 @@ class StandardExecutor {
|
|
|
159
202
|
|
|
160
203
|
// ... rest of the file (flushBuffer, mergeReports, executePerUser) ...
|
|
161
204
|
static async flushBuffer(state, dateStr, passName, config, deps, shardIndexMap, executionStats, mode, skipStatusWrite, isInitialWrite = false) {
|
|
162
|
-
|
|
205
|
+
const { logger } = deps;
|
|
163
206
|
const transformedState = {};
|
|
164
207
|
for (const [name, inst] of Object.entries(state)) {
|
|
165
208
|
const rawResult = inst.results || {};
|
|
@@ -197,7 +240,6 @@ class StandardExecutor {
|
|
|
197
240
|
}
|
|
198
241
|
|
|
199
242
|
static mergeReports(successAcc, failureAcc, newResult) {
|
|
200
|
-
// ... (No changes to mergeReports)
|
|
201
243
|
if (!newResult) return;
|
|
202
244
|
for (const [name, update] of Object.entries(newResult.successUpdates)) {
|
|
203
245
|
if (!successAcc[name]) {
|
|
@@ -230,24 +272,25 @@ class StandardExecutor {
|
|
|
230
272
|
static async executePerUser(calcInstance, metadata, dateStr, portfolioData, yesterdayPortfolioData, historyData, computedDeps, prevDeps, config, deps, loader, stats, earliestDates) {
|
|
231
273
|
const { logger } = deps;
|
|
232
274
|
const targetUserType = metadata.userType;
|
|
275
|
+
// [NEW] Always load Global Helpers
|
|
233
276
|
const mappings = await loader.loadMappings();
|
|
277
|
+
const piMasterList = await loader.loadPopularInvestorMasterList(config, deps); // [NEW] Loaded globally
|
|
234
278
|
const SCHEMAS = mathLayer.SCHEMAS;
|
|
235
279
|
|
|
236
280
|
// 1. Load Root Data
|
|
237
281
|
const insights = metadata.rootDataDependencies?.includes('insights') ? { today: await loader.loadInsights(dateStr) } : null;
|
|
238
|
-
const verifications = metadata.rootDataDependencies?.includes('verification') ? await loader.
|
|
239
|
-
const rankings = metadata.rootDataDependencies?.includes('rankings') ? await loader.
|
|
282
|
+
const verifications = metadata.rootDataDependencies?.includes('verification') ? await loader.loadVerificationProfiles(config, deps) : null;
|
|
283
|
+
const rankings = metadata.rootDataDependencies?.includes('rankings') ? await loader.loadPopularInvestorRankings(config, deps, dateStr) : null;
|
|
240
284
|
|
|
241
285
|
// [FIX] Load Yesterday's Rankings if isHistorical is true
|
|
242
286
|
let yesterdayRankings = null;
|
|
243
287
|
if (metadata.rootDataDependencies?.includes('rankings') && metadata.isHistorical) {
|
|
244
288
|
const prevDate = new Date(dateStr); prevDate.setUTCDate(prevDate.getUTCDate() - 1);
|
|
245
289
|
const prevStr = prevDate.toISOString().slice(0, 10);
|
|
246
|
-
|
|
247
|
-
yesterdayRankings = await loader.loadRankings(prevStr);
|
|
290
|
+
yesterdayRankings = await loader.loadPopularInvestorRankings(config, deps, prevStr);
|
|
248
291
|
}
|
|
249
292
|
|
|
250
|
-
const socialContainer = metadata.rootDataDependencies?.includes('social') ? await loader.
|
|
293
|
+
const socialContainer = metadata.rootDataDependencies?.includes('social') ? await loader.loadDailySocialPostInsights(config, deps, dateStr) : null;
|
|
251
294
|
|
|
252
295
|
// [NEW] Load New Root Data Types for Profile Metrics
|
|
253
296
|
// [FIX] Enforce canHaveMissingRoots
|
|
@@ -256,14 +299,13 @@ class StandardExecutor {
|
|
|
256
299
|
let ratings = null;
|
|
257
300
|
if (metadata.rootDataDependencies?.includes('ratings')) {
|
|
258
301
|
try {
|
|
259
|
-
ratings = await loader.
|
|
302
|
+
ratings = await loader.loadPIRatings(config, deps, dateStr);
|
|
260
303
|
} catch (e) {
|
|
261
304
|
if (!allowMissing) {
|
|
262
305
|
throw new Error(`[StandardExecutor] Required root 'ratings' failed to load for ${metadata.name}: ${e.message}`);
|
|
263
306
|
}
|
|
264
307
|
ratings = null;
|
|
265
308
|
}
|
|
266
|
-
// Check if result is null (clean fetch but empty/missing doc)
|
|
267
309
|
if (!ratings && !allowMissing) {
|
|
268
310
|
throw new Error(`[StandardExecutor] Required root 'ratings' is missing for ${metadata.name}`);
|
|
269
311
|
}
|
|
@@ -272,7 +314,7 @@ class StandardExecutor {
|
|
|
272
314
|
let pageViews = null;
|
|
273
315
|
if (metadata.rootDataDependencies?.includes('pageViews')) {
|
|
274
316
|
try {
|
|
275
|
-
pageViews = await loader.
|
|
317
|
+
pageViews = await loader.loadPIPageViews(config, deps, dateStr);
|
|
276
318
|
} catch (e) {
|
|
277
319
|
if (!allowMissing) {
|
|
278
320
|
throw new Error(`[StandardExecutor] Required root 'pageViews' failed to load for ${metadata.name}: ${e.message}`);
|
|
@@ -287,7 +329,7 @@ class StandardExecutor {
|
|
|
287
329
|
let watchlistMembership = null;
|
|
288
330
|
if (metadata.rootDataDependencies?.includes('watchlist')) {
|
|
289
331
|
try {
|
|
290
|
-
watchlistMembership = await loader.loadWatchlistMembership(dateStr);
|
|
332
|
+
watchlistMembership = await loader.loadWatchlistMembership(config, deps, dateStr);
|
|
291
333
|
} catch (e) {
|
|
292
334
|
if (!allowMissing) {
|
|
293
335
|
throw new Error(`[StandardExecutor] Required root 'watchlist' failed to load for ${metadata.name}: ${e.message}`);
|
|
@@ -302,7 +344,7 @@ class StandardExecutor {
|
|
|
302
344
|
let alertHistory = null;
|
|
303
345
|
if (metadata.rootDataDependencies?.includes('alerts')) {
|
|
304
346
|
try {
|
|
305
|
-
alertHistory = await loader.
|
|
347
|
+
alertHistory = await loader.loadPIAlertHistory(config, deps, dateStr);
|
|
306
348
|
} catch (e) {
|
|
307
349
|
if (!allowMissing) {
|
|
308
350
|
throw new Error(`[StandardExecutor] Required root 'alerts' failed to load for ${metadata.name}: ${e.message}`);
|
|
@@ -348,7 +390,6 @@ class StandardExecutor {
|
|
|
348
390
|
|
|
349
391
|
const userVerification = verifications ? verifications[userId] : null;
|
|
350
392
|
|
|
351
|
-
// [FIX] Extract current AND yesterday's rank entry for this user
|
|
352
393
|
const userRanking = rankings ? (rankings.find(r => String(r.CustomerId) === String(userId)) || null) : null;
|
|
353
394
|
const userRankingYesterday = yesterdayRankings ? (yesterdayRankings.find(r => String(r.CustomerId) === String(userId)) || null) : null;
|
|
354
395
|
|
|
@@ -371,21 +412,20 @@ class StandardExecutor {
|
|
|
371
412
|
config, deps,
|
|
372
413
|
verification: userVerification,
|
|
373
414
|
|
|
374
|
-
// [FIX] Pass both ranking entries
|
|
375
415
|
rankings: userRanking,
|
|
376
416
|
yesterdayRankings: userRankingYesterday,
|
|
377
417
|
|
|
378
|
-
// [FIX] Pass both global lists
|
|
379
418
|
allRankings: rankings,
|
|
380
419
|
allRankingsYesterday: yesterdayRankings,
|
|
381
420
|
|
|
382
421
|
allVerifications: verifications,
|
|
383
422
|
|
|
384
|
-
// [NEW] Pass New Root Data Types for Profile Metrics
|
|
385
423
|
ratings: ratings || {},
|
|
386
424
|
pageViews: pageViews || {},
|
|
387
425
|
watchlistMembership: watchlistMembership || {},
|
|
388
|
-
alertHistory: alertHistory || {}
|
|
426
|
+
alertHistory: alertHistory || {},
|
|
427
|
+
|
|
428
|
+
piMasterList,
|
|
389
429
|
});
|
|
390
430
|
|
|
391
431
|
if (metadata.requiresEarliestDataDate && earliestDates) {
|
|
@@ -947,6 +947,39 @@ class AlertHistoryExtractor {
|
|
|
947
947
|
}
|
|
948
948
|
}
|
|
949
949
|
|
|
950
|
+
/**
|
|
951
|
+
* [NEW] Extractor for Popular Investor Master List (Global Backup)
|
|
952
|
+
* Access via: context.globalData.piMasterList
|
|
953
|
+
*/
|
|
954
|
+
class PIMasterListExtractor {
|
|
955
|
+
/**
|
|
956
|
+
* Get the master record for a PI
|
|
957
|
+
* @param {Object} masterList - context.globalData.piMasterList
|
|
958
|
+
* @param {string} cid - The User CID
|
|
959
|
+
*/
|
|
960
|
+
static getRecord(masterList, cid) {
|
|
961
|
+
if (!masterList || !cid) return null;
|
|
962
|
+
return masterList[String(cid)] || null;
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
/**
|
|
966
|
+
* Resolve Username from Master List
|
|
967
|
+
* @param {Object} masterList
|
|
968
|
+
* @param {string} cid
|
|
969
|
+
*/
|
|
970
|
+
static getUsername(masterList, cid) {
|
|
971
|
+
const record = this.getRecord(masterList, cid);
|
|
972
|
+
return record ? record.username : null;
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
/**
|
|
976
|
+
* Check if CID exists in master list
|
|
977
|
+
*/
|
|
978
|
+
static exists(masterList, cid) {
|
|
979
|
+
return !!this.getRecord(masterList, cid);
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
|
|
950
983
|
module.exports = {
|
|
951
984
|
DataExtractor,
|
|
952
985
|
priceExtractor,
|
|
@@ -962,5 +995,6 @@ module.exports = {
|
|
|
962
995
|
RatingsExtractor,
|
|
963
996
|
PageViewsExtractor,
|
|
964
997
|
WatchlistMembershipExtractor,
|
|
965
|
-
AlertHistoryExtractor
|
|
998
|
+
AlertHistoryExtractor,
|
|
999
|
+
PIMasterListExtractor
|
|
966
1000
|
};
|
|
@@ -774,6 +774,37 @@ async function loadPIAlertHistory(config, deps, dateString) {
|
|
|
774
774
|
}
|
|
775
775
|
}
|
|
776
776
|
|
|
777
|
+
// [NEW] Load Popular Investor Master List
|
|
778
|
+
async function loadPopularInvestorMasterList(config, deps) {
|
|
779
|
+
const { db, logger, calculationUtils } = deps;
|
|
780
|
+
const { withRetry } = calculationUtils;
|
|
781
|
+
|
|
782
|
+
// Default to 'system_state' collection, 'popular_investor_master_list' doc
|
|
783
|
+
const collectionName = config.piMasterListCollection || 'system_state';
|
|
784
|
+
const docId = config.piMasterListDocId || 'popular_investor_master_list';
|
|
785
|
+
|
|
786
|
+
logger.log('INFO', `Loading Popular Investor Master List from ${collectionName}/${docId}`);
|
|
787
|
+
|
|
788
|
+
try {
|
|
789
|
+
const docRef = db.collection(collectionName).doc(docId);
|
|
790
|
+
const docSnap = await withRetry(() => docRef.get(), 'getPIMasterList');
|
|
791
|
+
|
|
792
|
+
if (!docSnap.exists) {
|
|
793
|
+
logger.log('WARN', 'Popular Investor Master List not found.');
|
|
794
|
+
return {};
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
const data = tryDecompress(docSnap.data());
|
|
798
|
+
// Structure is { investors: { cid: { username, ... } } } or direct map { cid: { ... } }
|
|
799
|
+
// Based on user input, it looks like a direct map of CIDs or a field holding the map.
|
|
800
|
+
// We return the raw object which acts as the map.
|
|
801
|
+
return data.investors || data;
|
|
802
|
+
} catch (error) {
|
|
803
|
+
logger.log('ERROR', `Failed to load PI Master List: ${error.message}`);
|
|
804
|
+
return {};
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
|
|
777
808
|
module.exports = {
|
|
778
809
|
getPortfolioPartRefs,
|
|
779
810
|
loadDataByRefs,
|
|
@@ -791,5 +822,6 @@ module.exports = {
|
|
|
791
822
|
loadPIRatings,
|
|
792
823
|
loadPIPageViews,
|
|
793
824
|
loadWatchlistMembership,
|
|
794
|
-
loadPIAlertHistory
|
|
825
|
+
loadPIAlertHistory,
|
|
826
|
+
loadPopularInvestorMasterList // [NEW]
|
|
795
827
|
};
|
|
@@ -93,6 +93,7 @@ exports.runRootDataIndexer = async (config, dependencies) => {
|
|
|
93
93
|
piPageViews: collections.piPageViews || 'PIPageViewsData',
|
|
94
94
|
watchlistMembership: collections.watchlistMembership || 'WatchlistMembershipData',
|
|
95
95
|
piAlertHistory: collections.piAlertHistory || 'PIAlertHistoryData',
|
|
96
|
+
piMasterList: collections.piMasterList || 'system_state', // [NEW] Collection for master list
|
|
96
97
|
...collections // Allow overrides
|
|
97
98
|
};
|
|
98
99
|
|
|
@@ -237,7 +238,8 @@ exports.runRootDataIndexer = async (config, dependencies) => {
|
|
|
237
238
|
piRatings: false,
|
|
238
239
|
piPageViews: false,
|
|
239
240
|
watchlistMembership: false,
|
|
240
|
-
piAlertHistory: false
|
|
241
|
+
piAlertHistory: false,
|
|
242
|
+
piMasterList: false // [NEW] Flag for Master List
|
|
241
243
|
}
|
|
242
244
|
};
|
|
243
245
|
|
|
@@ -312,6 +314,10 @@ exports.runRootDataIndexer = async (config, dependencies) => {
|
|
|
312
314
|
// Path: PIAlertHistoryData/{YYYY-MM-DD}
|
|
313
315
|
const piAlertHistoryRef = db.collection(safeCollections.piAlertHistory).doc(dateStr);
|
|
314
316
|
|
|
317
|
+
// [NEW] Master List Ref (Single Global Document)
|
|
318
|
+
// Path: system_state/popular_investor_master_list
|
|
319
|
+
const piMasterListRef = db.collection(safeCollections.piMasterList).doc('popular_investor_master_list');
|
|
320
|
+
|
|
315
321
|
// 4. Social Data Checks - Use date tracking documents (NEW STRUCTURE)
|
|
316
322
|
// Single tracking documents at root level:
|
|
317
323
|
// - PopularInvestorSocialPostData/_dates -> fetchedDates.{date}
|
|
@@ -387,7 +393,8 @@ exports.runRootDataIndexer = async (config, dependencies) => {
|
|
|
387
393
|
piRatingsSnap,
|
|
388
394
|
piPageViewsSnap,
|
|
389
395
|
watchlistMembershipSnap,
|
|
390
|
-
piAlertHistorySnap
|
|
396
|
+
piAlertHistorySnap,
|
|
397
|
+
piMasterListSnap // [NEW]
|
|
391
398
|
] = await Promise.all([
|
|
392
399
|
checkAnyPartExists(normPortPartsRef),
|
|
393
400
|
checkAnyPartExists(specPortPartsRef),
|
|
@@ -396,6 +403,7 @@ exports.runRootDataIndexer = async (config, dependencies) => {
|
|
|
396
403
|
insightsRef.get(),
|
|
397
404
|
Promise.resolve(!genericSocialSnap.empty),
|
|
398
405
|
piRankingsRef.get(),
|
|
406
|
+
piMasterListRef.get(), // [NEW]
|
|
399
407
|
// Check new structure first, fallback to legacy
|
|
400
408
|
checkDateCollectionHasDocs(piPortfoliosCollectionRef).then(exists => exists || checkAnyPartExists(piPortfoliosPartsRef)),
|
|
401
409
|
checkAnyPartExists(piDeepPartsRef), // Legacy only
|
|
@@ -459,6 +467,9 @@ exports.runRootDataIndexer = async (config, dependencies) => {
|
|
|
459
467
|
availability.hasHistory = normHistExists || specHistExists || piHistExists || signedInHistExists;
|
|
460
468
|
availability.hasInsights = insightsSnap.exists;
|
|
461
469
|
availability.hasSocial = foundPISocial || foundSignedInSocial || genericSocialExists;
|
|
470
|
+
|
|
471
|
+
// [NEW] Assign Master List Availability
|
|
472
|
+
availability.details.piMasterList = piMasterListSnap.exists;
|
|
462
473
|
|
|
463
474
|
// Price Check
|
|
464
475
|
// Check if the target date exists in the price availability set
|