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. STREAM EXECUTION ---
121
- const tP_iter = streamPortfolioData(config, deps, dateStr, effPortRefs, requiredUserTypes);
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 // <--- PASSED DOWN
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 (Pure Logic)
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
- actualType = isRanked ? 'POPULAR_INVESTOR' : (todayPortfolio.PublicPositions ? SCHEMAS.USER_TYPES.SPECULATOR : SCHEMAS.USER_TYPES.NORMAL);
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
- // HELPERS (Flush & Merge)
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 = {}; // Clear buffer
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
- piMasterList: true,
329
- rankings: false,
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), // Always required
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 = {}; // norm -> { days, originalName, category }
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,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bulltrackers-module",
3
- "version": "1.0.701",
3
+ "version": "1.0.702",
4
4
  "description": "Helper Functions for Bulltrackers.",
5
5
  "main": "index.js",
6
6
  "files": [