bulltrackers-module 1.0.700 → 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)
@@ -226,6 +226,7 @@ async function fetchDependencies(date, calcs, config, deps, manifestLookup = {},
226
226
  async function fetchResultSeries(endDateStr, calcNames, manifestLookup, config, deps, lookbackDays) {
227
227
  const { db, logger } = deps;
228
228
  const results = {}; // normalizedName -> { date -> data }
229
+ const { resultsCollection = 'computation_results', resultsSubcollection = 'results', computationsSubcollection = 'computations' } = config;
229
230
 
230
231
  // Initialize results structure
231
232
  calcNames.forEach(n => results[normalizeName(n)] = {});
@@ -238,6 +239,15 @@ async function fetchResultSeries(endDateStr, calcNames, manifestLookup, config,
238
239
  dates.push(d.toISOString().slice(0, 10));
239
240
  }
240
241
 
242
+ // [DEBUG] Log the manifest lookup and resolved categories
243
+ logger.log('INFO', `[DependencyFetcher] 🔍 ManifestLookup has ${Object.keys(manifestLookup).length} entries`);
244
+ for (const rawName of calcNames) {
245
+ const norm = normalizeName(rawName);
246
+ const category = manifestLookup[norm] || 'analytics';
247
+ const samplePath = `${resultsCollection}/${dates[0]}/${resultsSubcollection}/${category}/${computationsSubcollection}/${rawName}`;
248
+ logger.log('INFO', `[DependencyFetcher] 📍 '${rawName}' -> category='${category}' -> Path: ${samplePath}`);
249
+ }
250
+
241
251
  // Build Fetch Operations
242
252
  const ops = [];
243
253
  for (const dateStr of dates) {
@@ -260,6 +270,13 @@ async function fetchResultSeries(endDateStr, calcNames, manifestLookup, config,
260
270
  for (let i = 0; i < ops.length; i += BATCH_SIZE) {
261
271
  await Promise.all(ops.slice(i, i + BATCH_SIZE).map(fn => fn()));
262
272
  }
273
+
274
+ // [DEBUG] Log results summary
275
+ for (const rawName of calcNames) {
276
+ const norm = normalizeName(rawName);
277
+ const foundDates = Object.keys(results[norm] || {});
278
+ logger.log('INFO', `[DependencyFetcher] ✅ '${rawName}' found data for ${foundDates.length}/${lookbackDays} days`);
279
+ }
263
280
 
264
281
  return results;
265
282
  }
@@ -9,14 +9,11 @@ const { commitResults } = require('../persistence/ResultCommitter');
9
9
  const { fetchResultSeries } = require('../data/DependencyFetcher');
10
10
  const { getManifest } = require('../topology/ManifestLoader');
11
11
 
12
- // [FIX] Load calculations package directly for manifest building during force runs
13
- let _calculations = null;
14
- function getCalculations() {
15
- if (!_calculations) {
16
- try { _calculations = require('aiden-shared-calculations-unified').calculations; }
17
- catch (e) { _calculations = {}; }
18
- }
19
- return _calculations;
12
+ // Helper to get calculations - prefer config.calculations, fallback to direct require
13
+ function getCalculations(config) {
14
+ if (config && config.calculations) return config.calculations;
15
+ try { return require('aiden-shared-calculations-unified').calculations; }
16
+ catch (e) { return {}; }
20
17
  }
21
18
 
22
19
  class MetaExecutor {
@@ -29,8 +26,8 @@ class MetaExecutor {
29
26
  const dStr = date.toISOString().slice(0, 10);
30
27
  const loader = new CachedDataLoader(config, deps);
31
28
 
32
- // 1. Setup Manifest Lookup
33
- const allManifests = getManifest(config.productLines, config.calculationsDirectory, deps);
29
+ // 1. Setup Manifest Lookup (use activeProductLines and calculations from config)
30
+ const allManifests = getManifest(config.activeProductLines || [], getCalculations(config), deps);
34
31
  const manifestLookup = Object.fromEntries(allManifests.map(m => [normalizeName(m.name), m.category]));
35
32
 
36
33
  // 2. Load Base Data (Always Required)
@@ -104,8 +101,8 @@ class MetaExecutor {
104
101
  const { logger } = deps;
105
102
  const calcs = [metadata]; // Treat single as list for helpers
106
103
 
107
- // [FIX] Build manifestLookup using the actual calculations package (not config.calculationsDirectory)
108
- const allManifests = getManifest([], getCalculations(), deps);
104
+ // Build manifestLookup using calculations from config (set in index.js)
105
+ const allManifests = getManifest(config.activeProductLines || [], getCalculations(config), deps);
109
106
  const manifestLookup = Object.fromEntries(allManifests.map(m => [normalizeName(m.name), m.category]));
110
107
 
111
108
  // 1. Load Data using Shared Helpers
@@ -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');
@@ -13,14 +14,11 @@ const mathLayer = require('../layers/index');
13
14
  const { performance } = require('perf_hooks');
14
15
  const v8 = require('v8');
15
16
 
16
- // [FIX] Load calculations package directly for manifest building
17
- let _calculations = null;
18
- function getCalculations() {
19
- if (!_calculations) {
20
- try { _calculations = require('aiden-shared-calculations-unified').calculations; }
21
- catch (e) { _calculations = {}; }
22
- }
23
- return _calculations;
17
+ // Helper to get calculations - prefer config.calculations, fallback to direct require
18
+ function getCalculations(config) {
19
+ if (config && config.calculations) return config.calculations;
20
+ try { return require('aiden-shared-calculations-unified').calculations; }
21
+ catch (e) { return {}; }
24
22
  }
25
23
 
26
24
  class StandardExecutor {
@@ -83,8 +81,6 @@ class StandardExecutor {
83
81
  const loader = new CachedDataLoader(config, deps);
84
82
 
85
83
  // --- 1. PRE-LOAD GLOBAL DATA (Hoisted) ---
86
- // Load all "Singleton" datasets once (Ratings, Rankings, Series, Mappings)
87
- // instead of checking/loading per-user inside the loop.
88
84
  const startSetup = performance.now();
89
85
 
90
86
  const [
@@ -120,8 +116,37 @@ class StandardExecutor {
120
116
  shardMap[normalizeName(c.manifest.name)] = 0;
121
117
  });
122
118
 
123
- // --- 3. STREAM EXECUTION ---
124
- 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
+
125
150
  const yP_iter = (streamingCalcs.some(c => c.manifest.isHistorical) && rootData.yesterdayPortfolioRefs)
126
151
  ? streamPortfolioData(config, deps, new Date(new Date(dateStr).getTime() - 86400000).toISOString().slice(0, 10), rootData.yesterdayPortfolioRefs)
127
152
  : null;
@@ -150,7 +175,7 @@ class StandardExecutor {
150
175
  stats[normalizeName(calc.manifest.name)],
151
176
  earliestDates,
152
177
  seriesData,
153
- globalRoots // <--- PASSED DOWN
178
+ globalRoots
154
179
  )
155
180
  ));
156
181
 
@@ -171,8 +196,8 @@ class StandardExecutor {
171
196
  }
172
197
  }
173
198
  } finally {
174
- if (yP_iter?.return) await yP_iter.return();
175
- 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();
176
201
  }
177
202
 
178
203
  const finalRes = await StandardExecutor.flushBuffer(state, dateStr, passName, config, deps, shardMap, stats, 'FINAL', skipStatusWrite, !hasFlushed);
@@ -182,7 +207,7 @@ class StandardExecutor {
182
207
  }
183
208
 
184
209
  // =========================================================================
185
- // PER-USER EXECUTION (Pure Logic)
210
+ // PER-USER EXECUTION
186
211
  // =========================================================================
187
212
  static async executePerUser(calcInstance, metadata, dateStr, portfolioData, yesterdayPortfolioData, historyData, computedDeps, prevDeps, config, deps, stats, earliestDates, seriesData, globalRoots) {
188
213
  const { logger } = deps;
@@ -192,18 +217,21 @@ class StandardExecutor {
192
217
  let success = 0, failures = 0;
193
218
 
194
219
  for (const [userId, todayPortfolio] of Object.entries(portfolioData)) {
195
- // 1. Filter User
196
220
  if (metadata.targetCid && String(userId) !== String(metadata.targetCid)) { stats.skippedUsers++; continue; }
197
221
 
198
- // 2. Determine Type
222
+ // 2. Determine Type (Handle Stub for Missing Users)
199
223
  let actualType = todayPortfolio._userType;
200
224
  if (!actualType) {
225
+ // If driving from Master List, we know they are PIs even if missing today
201
226
  const isRanked = globalRoots.rankings && globalRoots.rankings.some(r => String(r.CustomerId) === String(userId));
202
- 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
+ }
203
232
  }
204
233
  if (targetUserType && targetUserType !== 'all' && targetUserType !== actualType) { stats.skippedUsers++; continue; }
205
234
 
206
- // 3. Resolve User Specifics from Global Data
207
235
  const userRank = globalRoots.rankings?.find(r => String(r.CustomerId) === String(userId)) || null;
208
236
  const userRankYest = globalRoots.rankingsYesterday?.find(r => String(r.CustomerId) === String(userId)) || null;
209
237
  const userVerify = globalRoots.verifications?.[userId] || null;
@@ -214,30 +242,21 @@ class StandardExecutor {
214
242
  (actualType === 'SIGNED_IN_USER' ? globalRoots.social.signedIn[userId] : globalRoots.social.generic)) || {};
215
243
  }
216
244
 
217
- // 4. Build Context
218
245
  const context = ContextFactory.buildPerUserContext({
219
- todayPortfolio,
246
+ todayPortfolio: todayPortfolio._isMissingToday ? null : todayPortfolio,
220
247
  yesterdayPortfolio: yesterdayPortfolioData?.[userId] || null,
221
248
  todayHistory: historyData?.[userId] || null,
222
249
  userId, userType: actualType, dateStr, metadata,
223
-
224
- // Injected Global Data
225
250
  mappings: globalRoots.mappings,
226
251
  piMasterList: globalRoots.piMasterList,
227
252
  insights: globalRoots.insights,
228
253
  socialData: social ? { today: social } : null,
229
-
230
- // Dependency Data
231
254
  computedDependencies: computedDeps,
232
255
  previousComputedDependencies: prevDeps,
233
256
  config, deps,
234
-
235
- // Specific Lookups
236
257
  verification: userVerify,
237
258
  rankings: userRank,
238
259
  yesterdayRankings: userRankYest,
239
-
240
- // Full Access (if needed by calc)
241
260
  allRankings: globalRoots.rankings,
242
261
  allRankingsYesterday: globalRoots.rankingsYesterday,
243
262
  allVerifications: globalRoots.verifications,
@@ -245,7 +264,6 @@ class StandardExecutor {
245
264
  pageViews: globalRoots.pageViews || {},
246
265
  watchlistMembership: globalRoots.watchlistMembership || {},
247
266
  alertHistory: globalRoots.alertHistory || {},
248
-
249
267
  seriesData
250
268
  });
251
269
 
@@ -267,13 +285,12 @@ class StandardExecutor {
267
285
  }
268
286
 
269
287
  // =========================================================================
270
- // HELPERS (Flush & Merge)
288
+ // FLUSH & MERGE
271
289
  // =========================================================================
272
290
  static async flushBuffer(state, dateStr, passName, config, deps, shardMap, stats, mode, skipStatus, isInitial) {
273
291
  const transformedState = {};
274
292
  for (const [name, inst] of Object.entries(state)) {
275
293
  let data = inst.results || {};
276
- // Pivot user-date structure if needed
277
294
  const first = Object.keys(data)[0];
278
295
  if (first && data[first] && typeof data[first] === 'object' && /^\d{4}-\d{2}-\d{2}$/.test(Object.keys(data[first])[0])) {
279
296
  const pivoted = {};
@@ -286,7 +303,7 @@ class StandardExecutor {
286
303
  data = pivoted;
287
304
  }
288
305
  transformedState[name] = { manifest: inst.manifest, getResult: async () => data, _executionStats: stats[name] };
289
- inst.results = {}; // Clear buffer
306
+ inst.results = {};
290
307
  }
291
308
  const res = await commitResults(transformedState, dateStr, passName, config, deps, skipStatus, { flushMode: mode, shardIndexes: shardMap, isInitialWrite: isInitial });
292
309
  if (res.shardIndexes) Object.assign(shardMap, res.shardIndexes);
@@ -313,31 +330,13 @@ class StandardExecutor {
313
330
  }
314
331
  }
315
332
 
316
- // =============================================================================
317
- // SHARED LOADING HELPERS
318
- // =============================================================================
319
-
320
- /**
321
- * Pre-loads all shared global datasets required by the active calculations.
322
- * Returns a consolidated object of { ratings, rankings, insights, ... }
323
- */
324
333
  async function loadGlobalRoots(loader, dateStr, calcs, deps) {
325
334
  const { logger } = deps;
326
335
  const roots = {};
327
-
328
- // 1. Identify Requirements
329
336
  const reqs = {
330
- mappings: true,
331
- piMasterList: true,
332
- rankings: false,
333
- rankingsYesterday: false,
334
- verifications: false,
335
- insights: false,
336
- social: false,
337
- ratings: false,
338
- pageViews: false,
339
- watchlist: false,
340
- 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
341
340
  };
342
341
 
343
342
  for (const c of calcs) {
@@ -353,7 +352,6 @@ async function loadGlobalRoots(loader, dateStr, calcs, deps) {
353
352
  if (deps.includes('alerts')) reqs.alerts = true;
354
353
  }
355
354
 
356
- // 2. Fetch Helper
357
355
  const fetch = async (key, method, dateArg, optional = true) => {
358
356
  if (!reqs[key]) return;
359
357
  try {
@@ -365,9 +363,8 @@ async function loadGlobalRoots(loader, dateStr, calcs, deps) {
365
363
  }
366
364
  };
367
365
 
368
- // 3. Execute Loads
369
366
  await Promise.all([
370
- fetch('mappings', 'loadMappings', null, false), // Always required
367
+ fetch('mappings', 'loadMappings', null, false),
371
368
  fetch('piMasterList', 'loadPIMasterList', null, false),
372
369
  fetch('rankings', 'loadRankings', dateStr),
373
370
  fetch('verifications', 'loadVerifications', dateStr),
@@ -384,16 +381,14 @@ async function loadGlobalRoots(loader, dateStr, calcs, deps) {
384
381
  await fetch('rankingsYesterday', 'loadRankings', prev);
385
382
  }
386
383
 
387
- // Map internal names to match loadGlobalRoots structure if needed
388
384
  roots.watchlistMembership = roots.watchlist;
389
385
  roots.alertHistory = roots.alerts;
390
-
391
386
  return roots;
392
387
  }
393
388
 
394
389
  async function loadSeriesData(loader, dateStr, calcs, config, deps) {
395
390
  const rootReqs = {};
396
- const depReqs = {}; // norm -> { days, originalName, category }
391
+ const depReqs = {};
397
392
 
398
393
  calcs.forEach(c => {
399
394
  if (c.manifest.rootDataSeries) {
@@ -413,26 +408,22 @@ async function loadSeriesData(loader, dateStr, calcs, config, deps) {
413
408
  const series = { root: {}, results: {} };
414
409
  const rootMap = {
415
410
  alerts: 'loadAlertHistory', insights: 'loadInsights', ratings: 'loadRatings',
416
- watchlist: 'loadWatchlistMembership', rankings: 'loadRankings'
411
+ watchlist: 'loadWatchlistMembership', rankings: 'loadRankings',
412
+ portfolios: 'loadPortfolios'
417
413
  };
418
414
 
419
415
  await Promise.all(Object.entries(rootReqs).map(async ([key, days]) => {
420
416
  if (rootMap[key]) series.root[key] = (await loader.loadSeries(rootMap[key], dateStr, days)).data;
421
417
  }));
422
418
 
423
- // [FIX] Use getCalculations() to load ALL computations for proper category lookup
424
419
  const depEntries = Object.values(depReqs);
425
420
  if (depEntries.length) {
426
421
  const depOriginalNames = depEntries.map(e => e.originalName);
427
422
  const maxDays = Math.max(...depEntries.map(e => e.days));
428
-
429
- // Build lookup from ALL computations (empty productLines = load all)
430
- const allManifests = getManifest([], getCalculations(), deps);
423
+ const allManifests = getManifest(config.activeProductLines || [], getCalculations(config), deps);
431
424
  const lookup = Object.fromEntries(allManifests.map(m => [normalizeName(m.name), m.category]));
432
-
433
425
  series.results = await fetchResultSeries(dateStr, depOriginalNames, lookup, config, deps, maxDays);
434
426
  }
435
-
436
427
  return series;
437
428
  }
438
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.700",
3
+ "version": "1.0.702",
4
4
  "description": "Helper Functions for Bulltrackers.",
5
5
  "main": "index.js",
6
6
  "files": [