bulltrackers-module 1.0.701 → 1.0.702
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.
|
@@ -17,7 +17,8 @@ const {
|
|
|
17
17
|
loadWatchlistMembership: loadWatchlistMembershipData,
|
|
18
18
|
loadPIAlertHistory,
|
|
19
19
|
loadPIWatchlistData,
|
|
20
|
-
loadPopularInvestorMasterList
|
|
20
|
+
loadPopularInvestorMasterList,
|
|
21
|
+
loadDailyPortfolios // <--- IMPORTED
|
|
21
22
|
} = require('../utils/data_loader');
|
|
22
23
|
const { getAvailabilityWindow } = require('./AvailabilityChecker');
|
|
23
24
|
const zlib = require('zlib');
|
|
@@ -81,6 +82,12 @@ const LOADER_DEFINITIONS = {
|
|
|
81
82
|
// No collection key needed for direct implementation, but handled by fn
|
|
82
83
|
fn: loadPIWatchlistData,
|
|
83
84
|
isIdBased: true // Uses ID instead of Date
|
|
85
|
+
},
|
|
86
|
+
// <--- ADDED SUPPORT FOR PORTFOLIO SERIES
|
|
87
|
+
loadPortfolios: {
|
|
88
|
+
cache: 'portfolios',
|
|
89
|
+
fn: loadDailyPortfolios
|
|
90
|
+
// No configKey/flag ensures legacy loop (required for complex multi-collection fetch)
|
|
84
91
|
}
|
|
85
92
|
};
|
|
86
93
|
|
|
@@ -144,6 +151,8 @@ class CachedDataLoader {
|
|
|
144
151
|
async loadWatchlistMembership(dateStr) { return this._loadGeneric('loadWatchlistMembership', dateStr); }
|
|
145
152
|
async loadAlertHistory(dateStr) { return this._loadGeneric('loadAlertHistory' , dateStr); }
|
|
146
153
|
async loadPIWatchlistData(piCid) { return this._loadGeneric('loadPIWatchlistData' , String(piCid)); }
|
|
154
|
+
// <--- ADDED PORTFOLIOS ACCESSOR
|
|
155
|
+
async loadPortfolios(dateStr) { return this._loadGeneric('loadPortfolios' , dateStr); }
|
|
147
156
|
|
|
148
157
|
// =========================================================================
|
|
149
158
|
// SPECIALIZED LOADERS (Non-Standard Patterns)
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @fileoverview Executor for "Standard" (User-Level) calculations.
|
|
3
3
|
* REFACTORED: Hoisted data loading, centralized Series/Root logic.
|
|
4
|
+
* UPDATED: Added Master List Driver for PI calculations to ensure full coverage.
|
|
4
5
|
*/
|
|
5
6
|
const { normalizeName, getEarliestDataDates } = require('../utils/utils');
|
|
6
|
-
const { streamPortfolioData, streamHistoryData, getPortfolioPartRefs, getHistoryPartRefs } = require('../utils/data_loader');
|
|
7
|
+
const { streamPortfolioData, streamHistoryData, getPortfolioPartRefs, getHistoryPartRefs, loadFullDayMap } = require('../utils/data_loader');
|
|
7
8
|
const { CachedDataLoader } = require('../data/CachedDataLoader');
|
|
8
9
|
const { ContextFactory } = require('../context/ContextFactory');
|
|
9
10
|
const { commitResults } = require('../persistence/ResultCommitter');
|
|
@@ -80,8 +81,6 @@ class StandardExecutor {
|
|
|
80
81
|
const loader = new CachedDataLoader(config, deps);
|
|
81
82
|
|
|
82
83
|
// --- 1. PRE-LOAD GLOBAL DATA (Hoisted) ---
|
|
83
|
-
// Load all "Singleton" datasets once (Ratings, Rankings, Series, Mappings)
|
|
84
|
-
// instead of checking/loading per-user inside the loop.
|
|
85
84
|
const startSetup = performance.now();
|
|
86
85
|
|
|
87
86
|
const [
|
|
@@ -117,8 +116,37 @@ class StandardExecutor {
|
|
|
117
116
|
shardMap[normalizeName(c.manifest.name)] = 0;
|
|
118
117
|
});
|
|
119
118
|
|
|
120
|
-
// --- 3.
|
|
121
|
-
|
|
119
|
+
// --- 3. DETERMINE ITERATION STRATEGY ---
|
|
120
|
+
let tP_iter;
|
|
121
|
+
const isPiOnly = requiredUserTypes && requiredUserTypes.length === 1 && requiredUserTypes[0] === 'POPULAR_INVESTOR';
|
|
122
|
+
|
|
123
|
+
// STRATEGY A: MASTER LIST DRIVER (For PIs, ensuring we hit every ID in the list, even if missing today)
|
|
124
|
+
if (isPiOnly && globalRoots.piMasterList && !targetCid) {
|
|
125
|
+
logger.log('INFO', `[StandardExecutor] 🚀 Driving execution via Master List (${Object.keys(globalRoots.piMasterList).length} PIs)`);
|
|
126
|
+
|
|
127
|
+
// Load Today's Map Fully (Small enough for PIs)
|
|
128
|
+
const todayMap = await loadFullDayMap(config, deps, effPortRefs, dateStr);
|
|
129
|
+
|
|
130
|
+
// Create a generator that yields chunks from the Master List keys
|
|
131
|
+
tP_iter = (async function* () {
|
|
132
|
+
const allCids = Object.keys(globalRoots.piMasterList);
|
|
133
|
+
const batchSize = config.partRefBatchSize || 50;
|
|
134
|
+
for (let i = 0; i < allCids.length; i += batchSize) {
|
|
135
|
+
const batchCids = allCids.slice(i, i + batchSize);
|
|
136
|
+
const chunk = {};
|
|
137
|
+
batchCids.forEach(cid => {
|
|
138
|
+
// If missing today, provide a stub so the Calc can look back in History
|
|
139
|
+
chunk[cid] = todayMap[cid] || { _userType: 'POPULAR_INVESTOR', _isMissingToday: true };
|
|
140
|
+
});
|
|
141
|
+
yield chunk;
|
|
142
|
+
}
|
|
143
|
+
})();
|
|
144
|
+
}
|
|
145
|
+
// STRATEGY B: STANDARD STREAM (Iterate whatever is in Firestore/GCS for today)
|
|
146
|
+
else {
|
|
147
|
+
tP_iter = streamPortfolioData(config, deps, dateStr, effPortRefs, requiredUserTypes);
|
|
148
|
+
}
|
|
149
|
+
|
|
122
150
|
const yP_iter = (streamingCalcs.some(c => c.manifest.isHistorical) && rootData.yesterdayPortfolioRefs)
|
|
123
151
|
? streamPortfolioData(config, deps, new Date(new Date(dateStr).getTime() - 86400000).toISOString().slice(0, 10), rootData.yesterdayPortfolioRefs)
|
|
124
152
|
: null;
|
|
@@ -147,7 +175,7 @@ class StandardExecutor {
|
|
|
147
175
|
stats[normalizeName(calc.manifest.name)],
|
|
148
176
|
earliestDates,
|
|
149
177
|
seriesData,
|
|
150
|
-
globalRoots
|
|
178
|
+
globalRoots
|
|
151
179
|
)
|
|
152
180
|
));
|
|
153
181
|
|
|
@@ -168,8 +196,8 @@ class StandardExecutor {
|
|
|
168
196
|
}
|
|
169
197
|
}
|
|
170
198
|
} finally {
|
|
171
|
-
if (yP_iter?.return) await yP_iter.return();
|
|
172
|
-
if (tH_iter?.return) await tH_iter.return();
|
|
199
|
+
if (yP_iter?.return && typeof yP_iter.return === 'function') await yP_iter.return();
|
|
200
|
+
if (tH_iter?.return && typeof tH_iter.return === 'function') await tH_iter.return();
|
|
173
201
|
}
|
|
174
202
|
|
|
175
203
|
const finalRes = await StandardExecutor.flushBuffer(state, dateStr, passName, config, deps, shardMap, stats, 'FINAL', skipStatusWrite, !hasFlushed);
|
|
@@ -179,7 +207,7 @@ class StandardExecutor {
|
|
|
179
207
|
}
|
|
180
208
|
|
|
181
209
|
// =========================================================================
|
|
182
|
-
// PER-USER EXECUTION
|
|
210
|
+
// PER-USER EXECUTION
|
|
183
211
|
// =========================================================================
|
|
184
212
|
static async executePerUser(calcInstance, metadata, dateStr, portfolioData, yesterdayPortfolioData, historyData, computedDeps, prevDeps, config, deps, stats, earliestDates, seriesData, globalRoots) {
|
|
185
213
|
const { logger } = deps;
|
|
@@ -189,18 +217,21 @@ class StandardExecutor {
|
|
|
189
217
|
let success = 0, failures = 0;
|
|
190
218
|
|
|
191
219
|
for (const [userId, todayPortfolio] of Object.entries(portfolioData)) {
|
|
192
|
-
// 1. Filter User
|
|
193
220
|
if (metadata.targetCid && String(userId) !== String(metadata.targetCid)) { stats.skippedUsers++; continue; }
|
|
194
221
|
|
|
195
|
-
// 2. Determine Type
|
|
222
|
+
// 2. Determine Type (Handle Stub for Missing Users)
|
|
196
223
|
let actualType = todayPortfolio._userType;
|
|
197
224
|
if (!actualType) {
|
|
225
|
+
// If driving from Master List, we know they are PIs even if missing today
|
|
198
226
|
const isRanked = globalRoots.rankings && globalRoots.rankings.some(r => String(r.CustomerId) === String(userId));
|
|
199
|
-
|
|
227
|
+
if (todayPortfolio._isMissingToday && isRanked) {
|
|
228
|
+
actualType = 'POPULAR_INVESTOR';
|
|
229
|
+
} else {
|
|
230
|
+
actualType = isRanked ? 'POPULAR_INVESTOR' : (todayPortfolio.PublicPositions ? SCHEMAS.USER_TYPES.SPECULATOR : SCHEMAS.USER_TYPES.NORMAL);
|
|
231
|
+
}
|
|
200
232
|
}
|
|
201
233
|
if (targetUserType && targetUserType !== 'all' && targetUserType !== actualType) { stats.skippedUsers++; continue; }
|
|
202
234
|
|
|
203
|
-
// 3. Resolve User Specifics from Global Data
|
|
204
235
|
const userRank = globalRoots.rankings?.find(r => String(r.CustomerId) === String(userId)) || null;
|
|
205
236
|
const userRankYest = globalRoots.rankingsYesterday?.find(r => String(r.CustomerId) === String(userId)) || null;
|
|
206
237
|
const userVerify = globalRoots.verifications?.[userId] || null;
|
|
@@ -211,30 +242,21 @@ class StandardExecutor {
|
|
|
211
242
|
(actualType === 'SIGNED_IN_USER' ? globalRoots.social.signedIn[userId] : globalRoots.social.generic)) || {};
|
|
212
243
|
}
|
|
213
244
|
|
|
214
|
-
// 4. Build Context
|
|
215
245
|
const context = ContextFactory.buildPerUserContext({
|
|
216
|
-
todayPortfolio,
|
|
246
|
+
todayPortfolio: todayPortfolio._isMissingToday ? null : todayPortfolio,
|
|
217
247
|
yesterdayPortfolio: yesterdayPortfolioData?.[userId] || null,
|
|
218
248
|
todayHistory: historyData?.[userId] || null,
|
|
219
249
|
userId, userType: actualType, dateStr, metadata,
|
|
220
|
-
|
|
221
|
-
// Injected Global Data
|
|
222
250
|
mappings: globalRoots.mappings,
|
|
223
251
|
piMasterList: globalRoots.piMasterList,
|
|
224
252
|
insights: globalRoots.insights,
|
|
225
253
|
socialData: social ? { today: social } : null,
|
|
226
|
-
|
|
227
|
-
// Dependency Data
|
|
228
254
|
computedDependencies: computedDeps,
|
|
229
255
|
previousComputedDependencies: prevDeps,
|
|
230
256
|
config, deps,
|
|
231
|
-
|
|
232
|
-
// Specific Lookups
|
|
233
257
|
verification: userVerify,
|
|
234
258
|
rankings: userRank,
|
|
235
259
|
yesterdayRankings: userRankYest,
|
|
236
|
-
|
|
237
|
-
// Full Access (if needed by calc)
|
|
238
260
|
allRankings: globalRoots.rankings,
|
|
239
261
|
allRankingsYesterday: globalRoots.rankingsYesterday,
|
|
240
262
|
allVerifications: globalRoots.verifications,
|
|
@@ -242,7 +264,6 @@ class StandardExecutor {
|
|
|
242
264
|
pageViews: globalRoots.pageViews || {},
|
|
243
265
|
watchlistMembership: globalRoots.watchlistMembership || {},
|
|
244
266
|
alertHistory: globalRoots.alertHistory || {},
|
|
245
|
-
|
|
246
267
|
seriesData
|
|
247
268
|
});
|
|
248
269
|
|
|
@@ -264,13 +285,12 @@ class StandardExecutor {
|
|
|
264
285
|
}
|
|
265
286
|
|
|
266
287
|
// =========================================================================
|
|
267
|
-
//
|
|
288
|
+
// FLUSH & MERGE
|
|
268
289
|
// =========================================================================
|
|
269
290
|
static async flushBuffer(state, dateStr, passName, config, deps, shardMap, stats, mode, skipStatus, isInitial) {
|
|
270
291
|
const transformedState = {};
|
|
271
292
|
for (const [name, inst] of Object.entries(state)) {
|
|
272
293
|
let data = inst.results || {};
|
|
273
|
-
// Pivot user-date structure if needed
|
|
274
294
|
const first = Object.keys(data)[0];
|
|
275
295
|
if (first && data[first] && typeof data[first] === 'object' && /^\d{4}-\d{2}-\d{2}$/.test(Object.keys(data[first])[0])) {
|
|
276
296
|
const pivoted = {};
|
|
@@ -283,7 +303,7 @@ class StandardExecutor {
|
|
|
283
303
|
data = pivoted;
|
|
284
304
|
}
|
|
285
305
|
transformedState[name] = { manifest: inst.manifest, getResult: async () => data, _executionStats: stats[name] };
|
|
286
|
-
inst.results = {};
|
|
306
|
+
inst.results = {};
|
|
287
307
|
}
|
|
288
308
|
const res = await commitResults(transformedState, dateStr, passName, config, deps, skipStatus, { flushMode: mode, shardIndexes: shardMap, isInitialWrite: isInitial });
|
|
289
309
|
if (res.shardIndexes) Object.assign(shardMap, res.shardIndexes);
|
|
@@ -310,31 +330,13 @@ class StandardExecutor {
|
|
|
310
330
|
}
|
|
311
331
|
}
|
|
312
332
|
|
|
313
|
-
// =============================================================================
|
|
314
|
-
// SHARED LOADING HELPERS
|
|
315
|
-
// =============================================================================
|
|
316
|
-
|
|
317
|
-
/**
|
|
318
|
-
* Pre-loads all shared global datasets required by the active calculations.
|
|
319
|
-
* Returns a consolidated object of { ratings, rankings, insights, ... }
|
|
320
|
-
*/
|
|
321
333
|
async function loadGlobalRoots(loader, dateStr, calcs, deps) {
|
|
322
334
|
const { logger } = deps;
|
|
323
335
|
const roots = {};
|
|
324
|
-
|
|
325
|
-
// 1. Identify Requirements
|
|
326
336
|
const reqs = {
|
|
327
|
-
mappings: true,
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
rankingsYesterday: false,
|
|
331
|
-
verifications: false,
|
|
332
|
-
insights: false,
|
|
333
|
-
social: false,
|
|
334
|
-
ratings: false,
|
|
335
|
-
pageViews: false,
|
|
336
|
-
watchlist: false,
|
|
337
|
-
alerts: false
|
|
337
|
+
mappings: true, piMasterList: true, rankings: false, rankingsYesterday: false,
|
|
338
|
+
verifications: false, insights: false, social: false, ratings: false,
|
|
339
|
+
pageViews: false, watchlist: false, alerts: false
|
|
338
340
|
};
|
|
339
341
|
|
|
340
342
|
for (const c of calcs) {
|
|
@@ -350,7 +352,6 @@ async function loadGlobalRoots(loader, dateStr, calcs, deps) {
|
|
|
350
352
|
if (deps.includes('alerts')) reqs.alerts = true;
|
|
351
353
|
}
|
|
352
354
|
|
|
353
|
-
// 2. Fetch Helper
|
|
354
355
|
const fetch = async (key, method, dateArg, optional = true) => {
|
|
355
356
|
if (!reqs[key]) return;
|
|
356
357
|
try {
|
|
@@ -362,9 +363,8 @@ async function loadGlobalRoots(loader, dateStr, calcs, deps) {
|
|
|
362
363
|
}
|
|
363
364
|
};
|
|
364
365
|
|
|
365
|
-
// 3. Execute Loads
|
|
366
366
|
await Promise.all([
|
|
367
|
-
fetch('mappings', 'loadMappings', null, false),
|
|
367
|
+
fetch('mappings', 'loadMappings', null, false),
|
|
368
368
|
fetch('piMasterList', 'loadPIMasterList', null, false),
|
|
369
369
|
fetch('rankings', 'loadRankings', dateStr),
|
|
370
370
|
fetch('verifications', 'loadVerifications', dateStr),
|
|
@@ -381,16 +381,14 @@ async function loadGlobalRoots(loader, dateStr, calcs, deps) {
|
|
|
381
381
|
await fetch('rankingsYesterday', 'loadRankings', prev);
|
|
382
382
|
}
|
|
383
383
|
|
|
384
|
-
// Map internal names to match loadGlobalRoots structure if needed
|
|
385
384
|
roots.watchlistMembership = roots.watchlist;
|
|
386
385
|
roots.alertHistory = roots.alerts;
|
|
387
|
-
|
|
388
386
|
return roots;
|
|
389
387
|
}
|
|
390
388
|
|
|
391
389
|
async function loadSeriesData(loader, dateStr, calcs, config, deps) {
|
|
392
390
|
const rootReqs = {};
|
|
393
|
-
const depReqs = {};
|
|
391
|
+
const depReqs = {};
|
|
394
392
|
|
|
395
393
|
calcs.forEach(c => {
|
|
396
394
|
if (c.manifest.rootDataSeries) {
|
|
@@ -410,25 +408,22 @@ async function loadSeriesData(loader, dateStr, calcs, config, deps) {
|
|
|
410
408
|
const series = { root: {}, results: {} };
|
|
411
409
|
const rootMap = {
|
|
412
410
|
alerts: 'loadAlertHistory', insights: 'loadInsights', ratings: 'loadRatings',
|
|
413
|
-
watchlist: 'loadWatchlistMembership', rankings: 'loadRankings'
|
|
411
|
+
watchlist: 'loadWatchlistMembership', rankings: 'loadRankings',
|
|
412
|
+
portfolios: 'loadPortfolios'
|
|
414
413
|
};
|
|
415
414
|
|
|
416
415
|
await Promise.all(Object.entries(rootReqs).map(async ([key, days]) => {
|
|
417
416
|
if (rootMap[key]) series.root[key] = (await loader.loadSeries(rootMap[key], dateStr, days)).data;
|
|
418
417
|
}));
|
|
419
418
|
|
|
420
|
-
// Build lookup from ALL computations using config.calculations
|
|
421
419
|
const depEntries = Object.values(depReqs);
|
|
422
420
|
if (depEntries.length) {
|
|
423
421
|
const depOriginalNames = depEntries.map(e => e.originalName);
|
|
424
422
|
const maxDays = Math.max(...depEntries.map(e => e.days));
|
|
425
|
-
|
|
426
423
|
const allManifests = getManifest(config.activeProductLines || [], getCalculations(config), deps);
|
|
427
424
|
const lookup = Object.fromEntries(allManifests.map(m => [normalizeName(m.name), m.category]));
|
|
428
|
-
|
|
429
425
|
series.results = await fetchResultSeries(dateStr, depOriginalNames, lookup, config, deps, maxDays);
|
|
430
426
|
}
|
|
431
|
-
|
|
432
427
|
return series;
|
|
433
428
|
}
|
|
434
429
|
|
|
@@ -228,6 +228,18 @@ async function loadFullDayMap(config, deps, partRefs, dateString) {
|
|
|
228
228
|
return fullMap;
|
|
229
229
|
}
|
|
230
230
|
|
|
231
|
+
/** Stage 3.5: Load Daily Portfolios (Wrapper for Series Loading) */
|
|
232
|
+
async function loadDailyPortfolios(config, deps, dateString) {
|
|
233
|
+
// 1. GCS FAST PATH
|
|
234
|
+
const cached = await tryLoadFromGCS(config, dateString, 'portfolios', deps.logger);
|
|
235
|
+
if (cached) return cached;
|
|
236
|
+
|
|
237
|
+
// 2. FIRESTORE FALLBACK
|
|
238
|
+
const partRefs = await getPortfolioPartRefs(config, deps, dateString);
|
|
239
|
+
if (partRefs.length === 0) return {};
|
|
240
|
+
return loadDataByRefs(config, deps, partRefs);
|
|
241
|
+
}
|
|
242
|
+
|
|
231
243
|
/** Stage 4: Load daily instrument insights */
|
|
232
244
|
async function loadDailyInsights(config, deps, dateString) {
|
|
233
245
|
const { db, logger, calculationUtils } = deps;
|
|
@@ -800,7 +812,8 @@ async function loadPopularInvestorMasterList(config, deps, dateString = null) {
|
|
|
800
812
|
module.exports = {
|
|
801
813
|
getPortfolioPartRefs,
|
|
802
814
|
loadDataByRefs,
|
|
803
|
-
loadFullDayMap,
|
|
815
|
+
loadFullDayMap,
|
|
816
|
+
loadDailyPortfolios, // <--- EXPORTED NEW WRAPPER
|
|
804
817
|
loadDailyInsights,
|
|
805
818
|
loadDailySocialPostInsights,
|
|
806
819
|
getHistoryPartRefs,
|