bulltrackers-module 1.0.118 → 1.0.120

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.
@@ -0,0 +1,741 @@
1
+ /**
2
+ * @fileoverview
3
+ * Main "Pass Runner" for the V2 Computation System.
4
+ *
5
+ * This orchestrator is designed to be run by a separate Cloud Function for each "pass".
6
+ * It reads its pass number from the config and executes only those calculations.
7
+ *
8
+ * - Pass 1: Runs "standard" calcs that stream user data.
9
+ * - Pass 2+: Runs "meta" calcs, which it first supplies with dependencies
10
+ * by fetching the results of *previous* passes from Firestore.
11
+ */
12
+
13
+ const { FieldPath } = require('@google-cloud/firestore');
14
+ const {
15
+ getPortfolioPartRefs,
16
+ loadFullDayMap,
17
+ loadDataByRefs,
18
+ loadDailyInsights,
19
+ loadDailySocialPostInsights
20
+ } = require('../utils/data_loader.js');
21
+
22
+ const {
23
+ normalizeName,
24
+ getExpectedDateStrings,
25
+ getFirstDateFromSourceData,
26
+ commitBatchInChunks
27
+ } = require('../utils/utils.js');
28
+
29
+
30
+ /**
31
+ * Groups the manifest by pass number.
32
+ * @param {Array<object>} manifest - The computation manifest.
33
+ * @returns {object} { '1': [...], '2': [...] }
34
+ */
35
+ function groupByPass(manifest) {
36
+ return manifest.reduce((acc, calc) => {
37
+ (acc[calc.pass] = acc[calc.pass] || []).push(calc);
38
+ return acc;
39
+ }, {});
40
+ }
41
+
42
+ /**
43
+ * Checks if a calculation's root data dependencies are met.
44
+ * @param {object} calcManifest - The manifest entry for the calculation.
45
+ * @param {object} rootDataStatus - The status object from checkRootDataAvailability.
46
+ * @returns {boolean} True if dependencies are met, false otherwise.
47
+ */
48
+ function checkRootDependencies(calcManifest, rootDataStatus) {
49
+ if (!calcManifest.rootDataDependencies || calcManifest.rootDataDependencies.length === 0) {
50
+ return true;
51
+ }
52
+ for (const dep of calcManifest.rootDataDependencies) {
53
+ if (dep === 'portfolio' && !rootDataStatus.hasPortfolio) return false;
54
+ if (dep === 'insights' && !rootDataStatus.hasInsights) return false;
55
+ if (dep === 'social' && !rootDataStatus.hasSocial) return false;
56
+ }
57
+ return true; // All dependencies were met
58
+ }
59
+
60
+
61
+ /**
62
+ * Checks if the root data (portfolios, insights, social) exists for a given day.
63
+ * @param {string} dateStr - The date string to check (YYYY-MM-DD).
64
+ * @param {object} config - The computation system configuration object.
65
+ * @param {object} dependencies - Contains db, logger, calculationUtils.
66
+ * @returns {Promise<object>} { portfolioRefs, insightsData, socialData, hasPortfolio, hasInsights, hasSocial }
67
+ */
68
+ async function checkRootDataAvailability(dateStr, config, dependencies) {
69
+ const { logger } = dependencies;
70
+ logger.log('INFO', `[PassRunner] Checking root data availability for ${dateStr}...`);
71
+
72
+ try {
73
+ const [portfolioRefs, insightsData, socialData] = await Promise.all([
74
+ getPortfolioPartRefs(config, dependencies, dateStr),
75
+ loadDailyInsights(config, dependencies, dateStr),
76
+ loadDailySocialPostInsights(config, dependencies, dateStr)
77
+ ]);
78
+
79
+ const hasPortfolio = (portfolioRefs && portfolioRefs.length > 0);
80
+ const hasInsights = !!insightsData;
81
+ const hasSocial = !!socialData;
82
+
83
+ return {
84
+ portfolioRefs: portfolioRefs || [],
85
+ insightsData: insightsData || null,
86
+ socialData: socialData || null,
87
+ hasPortfolio: hasPortfolio,
88
+ hasInsights: hasInsights,
89
+ hasSocial: hasSocial
90
+ };
91
+
92
+ } catch (err) {
93
+ logger.log('ERROR', `[PassRunner] Error checking data availability for ${dateStr}`, { errorMessage: err.message });
94
+ return {
95
+ portfolioRefs: [], insightsData: null, socialData: null,
96
+ hasPortfolio: false, hasInsights: false, hasSocial: false
97
+ };
98
+ }
99
+ }
100
+
101
+ /**
102
+ * (NEW) Fetches all computed dependencies for a given pass and date from Firestore.
103
+ * @param {string} dateStr - The date string (YYYY-MM-DD).
104
+ * @param {Array<object>} calcsInPass - The manifest entries for this pass.
105
+ * @param {Array<object>} fullManifest - The *entire* computation manifest.
106
+ * @param {object} config - The computation system configuration.
107
+ * @param {object} dependencies - Contains db, logger.
108
+ * @returns {Promise<object>} A map of { 'calc-name': result, ... }
109
+ */
110
+ async function fetchDependenciesForPass(dateStr, calcsInPass, fullManifest, config, dependencies) {
111
+ const { db, logger } = dependencies;
112
+ const { resultsCollection, resultsSubcollection, computationsSubcollection } = config;
113
+
114
+ // --- THIS IS THE FIX ---
115
+ // Build the manifestMap from the *fullManifest* so we can find
116
+ // dependencies from *all* passes, not just the current one.
117
+ const manifestMap = new Map();
118
+ for (const calc of fullManifest) {
119
+ manifestMap.set(normalizeName(calc.name), calc);
120
+ }
121
+ // --- END FIX ---
122
+
123
+ const requiredDeps = new Set();
124
+
125
+ // 1. Get all unique dependencies required by calcs in *this* pass
126
+ for (const calc of calcsInPass) {
127
+ if (calc.type === 'meta' && calc.dependencies) {
128
+ calc.dependencies.forEach(depName => requiredDeps.add(normalizeName(depName)));
129
+ }
130
+ }
131
+
132
+ if (requiredDeps.size === 0) {
133
+ logger.log('INFO', `[PassRunner] No Firestore dependencies to fetch for this pass on ${dateStr}.`);
134
+ return {};
135
+ }
136
+
137
+ logger.log('INFO', `[PassRunner] Fetching ${requiredDeps.size} dependencies from Firestore for ${dateStr}...`);
138
+
139
+ const docRefs = [];
140
+ const depNames = [];
141
+
142
+ // 2. Build the list of Firestore document references
143
+ for (const calcName of requiredDeps) {
144
+ const calcManifest = manifestMap.get(calcName); // Now correctly finds deps from other passes
145
+ if (!calcManifest) {
146
+ logger.log('ERROR', `[PassRunner] Cannot find manifest entry for dependency "${calcName}". This is a manifest error. Skipping dependency.`);
147
+ continue;
148
+ }
149
+
150
+ const category = calcManifest.category || 'unknown';
151
+ const docRef = db.collection(resultsCollection).doc(dateStr)
152
+ .collection(resultsSubcollection).doc(category)
153
+ .collection(computationsSubcollection).doc(calcName);
154
+
155
+ docRefs.push(docRef);
156
+ depNames.push(calcName);
157
+ }
158
+
159
+ // 3. Fetch all dependencies in one batch
160
+ const fetchedDependencies = {};
161
+ if (docRefs.length > 0) {
162
+ const snapshots = await db.getAll(...docRefs);
163
+ snapshots.forEach((doc, i) => {
164
+ const calcName = depNames[i];
165
+ if (doc.exists) {
166
+ fetchedDependencies[calcName] = doc.data();
167
+ } else {
168
+ fetchedDependencies[calcName] = null; // Mark as null if not found
169
+ }
170
+ });
171
+ }
172
+
173
+ logger.log('INFO', `[PassRunner] Successfully fetched ${Object.keys(fetchedDependencies).length} dependencies for ${dateStr}.`);
174
+ return fetchedDependencies;
175
+ }
176
+
177
+
178
+ /**
179
+ * Main pipe: pipe.computationSystem.runComputationPass
180
+ * @param {object} config - The computation system configuration object.
181
+ * @param {object} dependencies - Contains db, logger, calculationUtils.
182
+ * @param {Array<object>} computationManifest - The injected computation manifest.
183
+ * @returns {Promise<Object>} Summary of all passes.
184
+ */
185
+ async function runComputationPass(config, dependencies, computationManifest) {
186
+ const { logger } = dependencies;
187
+
188
+ // --- (NEW) Get the pass number this function is responsible for ---
189
+ const passToRun = String(config.COMPUTATION_PASS_TO_RUN);
190
+ if (!passToRun) {
191
+ logger.log('ERROR', '[PassRunner] FATAL: COMPUTATION_PASS_TO_RUN is not defined in config. Aborting.');
192
+ return;
193
+ }
194
+ logger.log('INFO', `🚀 [PassRunner] Starting run for PASS ${passToRun}...`);
195
+ // --- END NEW ---
196
+
197
+ const summary = {};
198
+ const yesterday = new Date();
199
+ yesterday.setUTCDate(yesterday.getUTCDate() - 1);
200
+ const endDateUTC = new Date(Date.UTC(yesterday.getUTCFullYear(), yesterday.getUTCMonth(), yesterday.getUTCDate()));
201
+
202
+ const firstDate = await getFirstDateFromSourceData(config, dependencies);
203
+ const startDateUTC = firstDate
204
+ ? new Date(Date.UTC(firstDate.getUTCFullYear(), firstDate.getUTCMonth(), firstDate.getUTCDate()))
205
+ : new Date(config.earliestComputationDate + 'T00:00:00Z');
206
+
207
+ const allExpectedDates = getExpectedDateStrings(startDateUTC, endDateUTC);
208
+
209
+ const passes = groupByPass(computationManifest);
210
+ const calcsInThisPass = passes[passToRun] || [];
211
+
212
+ if (calcsInThisPass.length === 0) {
213
+ logger.log('WARN', `[PassRunner] No calculations found in manifest for Pass ${passToRun}. Exiting.`);
214
+ return;
215
+ }
216
+
217
+ const standardCalcs = calcsInThisPass.filter(c => c.type === 'standard');
218
+ const metaCalcs = calcsInThisPass.filter(c => c.type === 'meta');
219
+ logger.log('INFO', `[PassRunner] Found ${standardCalcs.length} standard and ${metaCalcs.length} meta calcs for Pass ${passToRun}.`);
220
+
221
+
222
+ // --- Process ONE DAY at a time, in order ---
223
+ for (const dateStr of allExpectedDates) {
224
+ const dateToProcess = new Date(dateStr + 'T00:00:00Z');
225
+
226
+ // 1. Check for root data (portfolios, insights, social)
227
+ const rootData = await checkRootDataAvailability(dateStr, config, dependencies);
228
+ const rootDataStatus = {
229
+ hasPortfolio: rootData.hasPortfolio,
230
+ hasInsights: rootData.hasInsights,
231
+ hasSocial: rootData.hasSocial
232
+ };
233
+
234
+ if (!rootData.hasPortfolio && !rootData.hasInsights && !rootData.hasSocial) {
235
+ logger.log('WARN', `[PassRunner] Skipping Pass ${passToRun} for ${dateStr} due to missing all root data.`);
236
+ continue; // Skip to the next day
237
+ }
238
+
239
+ logger.log('INFO', `[PassRunner] Processing Pass ${passToRun} for ${dateStr}...`);
240
+
241
+ // 2. (NEW) Fetch all dependencies for this pass and date from Firestore
242
+ // This is skipped for Pass 1, as `requiredDeps.size` will be 0
243
+ const fetchedDependencies = await fetchDependenciesForPass(
244
+ dateStr,
245
+ calcsInThisPass,
246
+ computationManifest, // <-- Pass the *full* manifest here
247
+ config,
248
+ dependencies
249
+ );
250
+
251
+ const skippedCalculations = new Set();
252
+
253
+ // 3. Filter calculations based on root data
254
+ const standardCalcsToRun = [];
255
+ for (const calcManifest of standardCalcs) {
256
+ if (checkRootDependencies(calcManifest, rootDataStatus)) {
257
+ standardCalcsToRun.push(calcManifest);
258
+ } else {
259
+ logger.log('INFO', `[Pass ${passToRun}] Skipping standard calc "${calcManifest.name}" for ${dateStr} due to missing root data.`);
260
+ skippedCalculations.add(calcManifest.name);
261
+ }
262
+ }
263
+
264
+ const metaCalcsToRun = [];
265
+ for (const calcManifest of metaCalcs) {
266
+ const calcName = calcManifest.name;
267
+
268
+ // Check 1: Are root data dependencies met?
269
+ if (!checkRootDependencies(calcManifest, rootDataStatus)) {
270
+ logger.log('INFO', `[Pass ${passToRun} (Meta)] Skipping meta calc "${calcName}" for ${dateStr} due to missing root data.`);
271
+ skippedCalculations.add(calcName);
272
+ continue;
273
+ }
274
+
275
+ // Check 2: Are *computed* dependencies (from previous passes) met?
276
+ let depCheck = true;
277
+ let missingDepName = '';
278
+ for (const depName of (calcManifest.dependencies || [])) {
279
+ const normalizedDepName = normalizeName(depName);
280
+ if (!fetchedDependencies[normalizedDepName]) { // Check if null or undefined
281
+ depCheck = false;
282
+ missingDepName = normalizedDepName;
283
+ break;
284
+ }
285
+ }
286
+
287
+ if (depCheck) {
288
+ metaCalcsToRun.push(calcManifest);
289
+ } else {
290
+ logger.log('WARN', `[Pass ${passToRun} (Meta)] Skipping meta calc "${calcName}" for ${dateStr} due to missing computed dependency "${missingDepName}" from Firestore.`);
291
+ skippedCalculations.add(calcName);
292
+ }
293
+ }
294
+
295
+ // --- 4. Run the filtered calculations ---
296
+ try {
297
+ // Run standard calcs for this pass (e.g., Pass 1)
298
+ if (standardCalcsToRun.length > 0) {
299
+ await runUnifiedComputation(
300
+ dateToProcess,
301
+ standardCalcsToRun,
302
+ `Pass ${passToRun} (Standard)`,
303
+ config,
304
+ dependencies,
305
+ rootData
306
+ );
307
+ }
308
+
309
+ // Run meta calcs for this pass (e.g., Pass 2, 3, 4)
310
+ if (metaCalcsToRun.length > 0) {
311
+ await runMetaComputation(
312
+ dateToProcess,
313
+ metaCalcsToRun,
314
+ `Pass ${passToRun} (Meta)`,
315
+ config,
316
+ dependencies,
317
+ fetchedDependencies, // <-- This is the NEW object from Firestore
318
+ rootData
319
+ );
320
+ }
321
+ logger.log('SUCCESS', `[PassRunner] Completed Pass ${passToRun} for ${dateStr}.`);
322
+ } catch (err) {
323
+ logger.log('ERROR', `[PassRunner] FAILED Pass ${passToRun} for ${dateStr}`, { errorMessage: err.message, stack: err.stack });
324
+ }
325
+ } // End dates loop
326
+
327
+ logger.log('INFO', `[PassRunner] Pass ${passToRun} orchestration finished.`);
328
+ return summary;
329
+ }
330
+
331
+ /**
332
+ * Internal sub-pipe: Initializes calculator instances.
333
+ */
334
+ function initializeCalculators(calculationsToRun, logger) {
335
+ const state = {};
336
+ for (const calcManifest of calculationsToRun) {
337
+ const calcName = normalizeName(calcManifest.name);
338
+ const CalculationClass = calcManifest.class;
339
+
340
+ if (typeof CalculationClass === 'function') {
341
+ try {
342
+ const instance = new CalculationClass();
343
+ instance.manifest = calcManifest; // Attach manifest data
344
+ state[calcName] = instance;
345
+ } catch (e) {
346
+ logger.warn(`[PassRunner] Init failed for ${calcName}`, { errorMessage: e.message });
347
+ state[calcName] = null;
348
+ }
349
+ } else {
350
+ logger.warn(`[PassRunner] Calculation class not found in manifest for: ${calcName}`);
351
+ state[calcName] = null;
352
+ }
353
+ }
354
+ return state;
355
+ }
356
+
357
+ /**
358
+ * Internal sub-pipe: Streams data and calls process() on "standard" calculators.
359
+ */
360
+ async function streamAndProcess(
361
+ dateStr, todayRefs, state, passName, config, dependencies,
362
+ yesterdayPortfolios = {},
363
+ todayInsights = null,
364
+ yesterdayInsights = null,
365
+ todaySocialPostInsights = null,
366
+ yesterdaySocialPostInsights = null
367
+ ) {
368
+ const { logger, calculationUtils } = dependencies;
369
+ logger.log('INFO', `[${passName}] Streaming ${todayRefs.length} 'today' part docs for ${dateStr}...`);
370
+
371
+ const yesterdayDate = new Date(dateStr + 'T00:00:00Z');
372
+ yesterdayDate.setUTCDate(yesterdayDate.getUTCDate() - 1);
373
+ const yesterdayStr = yesterdayDate.toISOString().slice(0, 10);
374
+
375
+ const { instrumentToTicker, instrumentToSector } = await calculationUtils.loadInstrumentMappings();
376
+
377
+ const context = {
378
+ instrumentMappings: instrumentToTicker,
379
+ sectorMapping: instrumentToSector,
380
+ todayDateStr: dateStr,
381
+ yesterdayDateStr: yesterdayStr,
382
+ dependencies: dependencies,
383
+ config: config
384
+ };
385
+
386
+ const batchSize = config.partRefBatchSize || 10;
387
+ let isFirstUser = true;
388
+
389
+ for (let i = 0; i < todayRefs.length; i += batchSize) {
390
+ const batchRefs = todayRefs.slice(i, i + batchSize);
391
+ const todayPortfoliosChunk = await loadDataByRefs(config, dependencies, batchRefs);
392
+
393
+ for (const uid in todayPortfoliosChunk) {
394
+ const p = todayPortfoliosChunk[uid];
395
+ if (!p) continue;
396
+ const userType = p.PublicPositions ? 'speculator' : 'normal';
397
+ context.userType = userType;
398
+
399
+ for (const calcName in state) {
400
+ const calc = state[calcName];
401
+ if (!calc || typeof calc.process !== 'function') continue;
402
+
403
+ const manifestCalc = calc.manifest;
404
+ const isSocialOrInsights = manifestCalc.category === 'socialPosts' || manifestCalc.category === 'insights';
405
+ const isHistoricalCalc = manifestCalc.isHistorical === true;
406
+ const isSpeculatorCalc = manifestCalc.category === 'speculators';
407
+ let processArgs;
408
+ const allContextArgs = [
409
+ context,
410
+ todayInsights,
411
+ yesterdayInsights,
412
+ todaySocialPostInsights,
413
+ yesterdaySocialPostInsights
414
+ ];
415
+
416
+ if (isSocialOrInsights) {
417
+ if (isFirstUser) {
418
+ processArgs = [null, null, null, ...allContextArgs];
419
+ } else {
420
+ continue; // Only run once
421
+ }
422
+ } else if (isHistoricalCalc) {
423
+ const pYesterday = yesterdayPortfolios[uid];
424
+ if (!pYesterday) continue;
425
+ processArgs = [p, pYesterday, uid, ...allContextArgs];
426
+ } else {
427
+ processArgs = [p, null, uid, ...allContextArgs];
428
+ }
429
+
430
+ if (!isSocialOrInsights) {
431
+ if ((userType === 'normal' && isSpeculatorCalc) ||
432
+ (userType === 'speculator' && !isSpeculatorCalc && calcName !== 'users-processed')) {
433
+ continue; // Skip: wrong user type
434
+ }
435
+ }
436
+
437
+ try {
438
+ await Promise.resolve(calc.process(...processArgs));
439
+ } catch (e) {
440
+ logger.log('WARN', `Process error in ${calcName} for user ${uid}`, { err: e.message });
441
+ }
442
+ }
443
+ isFirstUser = false;
444
+ }
445
+ }
446
+
447
+ // Handle case where there are no users but we still need to run insights/social calcs
448
+ if (todayRefs.length === 0 && isFirstUser) {
449
+ logger.log('INFO', `[${passName}] No user portfolios found for ${dateStr}. Running insights/social calcs once.`);
450
+ const allContextArgs = [ context, todayInsights, yesterdayInsights, todaySocialPostInsights, yesterdaySocialPostInsights ];
451
+ for (const calcName in state) {
452
+ const calc = state[calcName];
453
+ if (!calc || typeof calc.process !== 'function') continue;
454
+ const manifestCalc = calc.manifest;
455
+ const isSocialOrInsights = manifestCalc.category === 'socialPosts' || manifestCalc.category === 'insights';
456
+ if (isSocialOrInsights) {
457
+ try {
458
+ await Promise.resolve(calc.process(null, null, null, ...allContextArgs));
459
+ } catch (e) {
460
+ logger.log('WARN', `Process error in ${calcName} for no-user run`, { err: e.message });
461
+ }
462
+ }
463
+ }
464
+ }
465
+ }
466
+
467
+
468
+ /**
469
+ * Internal sub-pipe: Runs "standard" computations (Pass 1) for a single date.
470
+ */
471
+ async function runUnifiedComputation(dateToProcess, calculationsToRun, passName, config, dependencies, rootData) {
472
+ const { db, logger } = dependencies;
473
+ const dateStr = dateToProcess.toISOString().slice(0, 10);
474
+ logger.log('INFO', `[${passName}] Starting run for ${dateStr} with ${calculationsToRun.length} calcs.`);
475
+
476
+ try {
477
+ const {
478
+ portfolioRefs: todayRefs,
479
+ insightsData: todayInsightsData,
480
+ socialData: todaySocialPostInsightsData
481
+ } = rootData;
482
+
483
+ let yesterdayPortfolios = {};
484
+ let yesterdayInsightsData = null;
485
+ let yesterdaySocialPostInsightsData = null;
486
+
487
+ const requiresYesterdayPortfolio = calculationsToRun.some(c => c.isHistorical === true);
488
+ const requiresYesterdayInsights = calculationsToRun.some(c => c.class.prototype.process.toString().includes('yesterdayInsights'));
489
+ const requiresYesterdaySocialPosts = calculationsToRun.some(c => c.class.prototype.process.toString().includes('yesterdaySocialPostInsights'));
490
+
491
+ if (requiresYesterdayPortfolio || requiresYesterdayInsights || requiresYesterdaySocialPosts) {
492
+ // ... [Identical logic for fetching yesterday's data as in original file] ...
493
+ // This logic is complex and correct, so it is preserved.
494
+ if(requiresYesterdayInsights) {
495
+ let daysAgo = 1; const maxLookback = 30;
496
+ while (!yesterdayInsightsData && daysAgo <= maxLookback) {
497
+ const prev = new Date(dateToProcess); prev.setUTCDate(prev.getUTCDate() - daysAgo);
498
+ const prevStr = prev.toISOString().slice(0, 10);
499
+ yesterdayInsightsData = await loadDailyInsights(config, dependencies, prevStr);
500
+ if (yesterdayInsightsData) logger.log('INFO', `[${passName}] Found 'yesterday' instrument insights data from ${daysAgo} day(s) ago (${prevStr}).`);
501
+ else daysAgo++;
502
+ }
503
+ if (!yesterdayInsightsData) logger.log('WARN', `[${passName}] Could not find any 'yesterday' instrument insights data within a ${maxLookback} day lookback.`);
504
+ }
505
+ if(requiresYesterdaySocialPosts) {
506
+ let daysAgo = 1; const maxLookback = 30;
507
+ while (!yesterdaySocialPostInsightsData && daysAgo <= maxLookback) {
508
+ const prev = new Date(dateToProcess); prev.setUTCDate(prev.getUTCDate() - daysAgo);
509
+ const prevStr = prev.toISOString().slice(0, 10);
510
+ yesterdaySocialPostInsightsData = await loadDailySocialPostInsights(config, dependencies, prevStr);
511
+ if (yesterdaySocialPostInsightsData) logger.log('INFO', `[${passName}] Found 'yesterday' social post insights data from ${daysAgo} day(s) ago (${prevStr}).`);
512
+ else daysAgo++;
513
+ }
514
+ if (!yesterdaySocialPostInsightsData) logger.log('WARN', `[${passName}] Could not find any 'yesterday' social post insights data within a ${maxLookback} day lookback.`);
515
+ }
516
+ if (requiresYesterdayPortfolio) {
517
+ const prev = new Date(dateToProcess); prev.setUTCDate(prev.getUTCDate() - 1);
518
+ const prevStr = prev.toISOString().slice(0, 10);
519
+ const yesterdayRefs = await getPortfolioPartRefs(config, dependencies, prevStr);
520
+ if (yesterdayRefs.length > 0) {
521
+ yesterdayPortfolios = await loadFullDayMap(config, dependencies, yesterdayRefs);
522
+ logger.log('INFO', `[${passName}] Loaded yesterday's (${prevStr}) portfolio map for historical calcs.`);
523
+ } else {
524
+ logger.log('WARN', `[${passName}] Yesterday's (${prevStr}) portfolio data not found. Historical calcs requiring it will be skipped.`);
525
+ }
526
+ }
527
+ }
528
+
529
+ const state = initializeCalculators(calculationsToRun, logger);
530
+ await streamAndProcess(
531
+ dateStr,
532
+ todayRefs,
533
+ state,
534
+ passName,
535
+ config,
536
+ dependencies,
537
+ yesterdayPortfolios,
538
+ todayInsightsData,
539
+ yesterdayInsightsData,
540
+ todaySocialPostInsightsData,
541
+ yesterdaySocialPostInsightsData
542
+ );
543
+
544
+ let successCount = 0;
545
+ const resultsCollectionRef = db.collection(config.resultsCollection).doc(dateStr).collection(config.resultsSubcollection);
546
+
547
+ for (const calcName in state) {
548
+ const calc = state[calcName];
549
+ if (!calc || typeof calc.getResult !== 'function') {
550
+ if (!calc) logger.log('WARN', `[${passName}] Skipping ${calcName} for ${dateStr} because it failed to initialize.`);
551
+ continue;
552
+ }
553
+
554
+ const category = calc.manifest.category || 'unknown';
555
+ let result = null;
556
+ try {
557
+ result = await Promise.resolve(calc.getResult());
558
+
559
+ // --- Database write logic (Identical to original file) ---
560
+ const pendingWrites = [];
561
+ const summaryData = {};
562
+ if (result && Object.keys(result).length > 0) {
563
+ let isSharded = false;
564
+ const shardedCollections = {
565
+ 'sharded_user_profile': config.shardedUserProfileCollection,
566
+ 'sharded_user_profitability': config.shardedProfitabilityCollection
567
+ };
568
+ for (const resultKey in shardedCollections) {
569
+ if (result[resultKey]) {
570
+ isSharded = true;
571
+ const shardCollectionName = shardedCollections[resultKey];
572
+ const shardedData = result[resultKey];
573
+ for (const shardId in shardedData) {
574
+ const shardDocData = shardedData[shardId];
575
+ if (shardDocData && Object.keys(shardDocData).length > 0) {
576
+ const shardRef = db.collection(shardCollectionName).doc(shardId);
577
+ pendingWrites.push({ ref: shardRef, data: shardedData[shardId] });
578
+ }
579
+ }
580
+ const { [resultKey]: _, ...otherResults } = result;
581
+ if (Object.keys(otherResults).length > 0) {
582
+ const computationDocRef = resultsCollectionRef.doc(category)
583
+ .collection(config.computationsSubcollection)
584
+ .doc(calcName);
585
+ pendingWrites.push({ ref: computationDocRef, data: otherResults });
586
+ }
587
+ }
588
+ }
589
+ if (!isSharded) {
590
+ const computationDocRef = resultsCollectionRef.doc(category)
591
+ .collection(config.computationsSubcollection)
592
+ .doc(calcName);
593
+ pendingWrites.push({ ref: computationDocRef, data: result });
594
+ }
595
+ if (!summaryData[category]) summaryData[category] = {};
596
+ summaryData[category][calcName] = true;
597
+ if (Object.keys(summaryData).length > 0) {
598
+ const topLevelDocRef = db.collection(config.resultsCollection).doc(dateStr);
599
+ pendingWrites.push({ ref: topLevelDocRef, data: summaryData });
600
+ }
601
+ if (pendingWrites.length > 0) {
602
+ await commitBatchInChunks(config, dependencies, pendingWrites, `Commit ${passName} ${dateStr} [${calcName}]`);
603
+ successCount++;
604
+ }
605
+ } else {
606
+ if (result === null) logger.log('INFO', `[${passName}] Calculation ${calcName} returned null for ${dateStr}. This is expected if no data was processed.`);
607
+ else logger.log('WARN', `[${passName}] Calculation ${calcName} produced empty results {} for ${dateStr}. Skipping write.`);
608
+ }
609
+ } catch (e) {
610
+ logger.log('ERROR', `[${passName}] getResult/Commit failed for ${calcName} on ${dateStr}`, { err: e.message, stack: e.stack });
611
+ }
612
+ }
613
+ const completionStatus = successCount === calculationsToRun.length ? 'SUCCESS' : 'WARN';
614
+ logger.log(completionStatus, `[${passName}] Completed ${dateStr}. Success: ${successCount}/${calculationsToRun.length}.`);
615
+
616
+ } catch (err) {
617
+ logger.log('ERROR', `[${passName}] Fatal error for ${dateStr}`, { errorMessage: err.message, stack: err.stack });
618
+ throw err; // Re-throw to stop the orchestrator for this day
619
+ }
620
+ }
621
+
622
+
623
+ /**
624
+ * Internal sub-pipe: Runs "meta" computations (Pass 2, 3, 4) for a single date.
625
+ */
626
+ async function runMetaComputation(
627
+ dateToProcess,
628
+ calculationsToRun, // This is the *filtered* list
629
+ passName,
630
+ config,
631
+ dependencies,
632
+ fetchedDependencies, // <-- This is the NEW object from Firestore
633
+ rootData
634
+ ) {
635
+ const { db, logger } = dependencies;
636
+ const dateStr = dateToProcess.toISOString().slice(0, 10);
637
+ logger.log('INFO', `[${passName}] Starting run for ${dateStr} with ${calculationsToRun.length} calcs.`);
638
+
639
+ try {
640
+ const resultsCollectionRef = db.collection(config.resultsCollection).doc(dateStr).collection(config.resultsSubcollection);
641
+ const compsSub = config.computationsSubcollection || 'computations';
642
+ let successCount = 0;
643
+
644
+ // Add rootData to dependencies for meta-calcs that need to stream users
645
+ const dependenciesForMetaCalc = {
646
+ ...dependencies,
647
+ rootData: rootData
648
+ };
649
+
650
+ for (const manifestCalc of calculationsToRun) {
651
+ const calcName = normalizeName(manifestCalc.name);
652
+ const category = manifestCalc.category || 'unknown';
653
+ const CalcClass = manifestCalc.class;
654
+
655
+ if (typeof CalcClass !== 'function') {
656
+ logger.log('ERROR', `[${passName}] Invalid class in manifest for ${calcName}. Skipping.`);
657
+ continue;
658
+ }
659
+
660
+ const instance = new CalcClass();
661
+ let result = null;
662
+ try {
663
+ // --- Call process with the fetched dependencies ---
664
+ result = await Promise.resolve(instance.process(
665
+ dateStr,
666
+ dependenciesForMetaCalc,
667
+ config,
668
+ fetchedDependencies // <-- Pass the pre-fetched results
669
+ ));
670
+
671
+ // --- Database write logic (Identical to original file) ---
672
+ const pendingWrites = [];
673
+ const summaryData = {};
674
+ if (result && Object.keys(result).length > 0) {
675
+ let isSharded = false;
676
+ const shardedCollections = {
677
+ 'sharded_user_profile': config.shardedUserProfileCollection,
678
+ 'sharded_user_profitability': config.shardedProfitabilityCollection
679
+ };
680
+ for (const resultKey in shardedCollections) {
681
+ if (result[resultKey]) {
682
+ isSharded = true;
683
+ const shardCollectionName = shardedCollections[resultKey];
684
+ if (!shardCollectionName) {
685
+ logger.log('ERROR', `[${passName}] Missing config key for sharded collection: ${resultKey}`);
686
+ continue;
687
+ }
688
+ const shardedData = result[resultKey];
689
+ for (const shardId in shardedData) {
690
+ const shardDocData = shardedData[shardId];
691
+ if (shardDocData && (Object.keys(shardDocData).length > 0)) {
692
+ const shardRef = db.collection(shardCollectionName).doc(shardId);
693
+ pendingWrites.push({ ref: shardRef, data: shardDocData, merge: true });
694
+ }
695
+ }
696
+ const { [resultKey]: _, ...otherResults } = result;
697
+ result = otherResults;
698
+ }
699
+ }
700
+ if (result && Object.keys(result).length > 0) {
701
+ const computationDocRef = resultsCollectionRef.doc(category)
702
+ .collection(compsSub)
703
+ .doc(calcName);
704
+ pendingWrites.push({ ref: computationDocRef, data: result });
705
+ }
706
+ if (!summaryData[category]) summaryData[category] = {};
707
+ summaryData[category][calcName] = true;
708
+ if (Object.keys(summaryData).length > 0) {
709
+ const topLevelDocRef = db.collection(config.resultsCollection).doc(dateStr);
710
+ pendingWrites.push({ ref: topLevelDocRef, data: summaryData });
711
+ }
712
+ if (pendingWrites.length > 0) {
713
+ await commitBatchInChunks(
714
+ config,
715
+ dependencies,
716
+ pendingWrites,
717
+ `Commit ${passName} ${dateStr} [${calcName}]`
718
+ );
719
+ successCount++;
720
+ }
721
+ } else {
722
+ logger.log('WARN', `[${passName}] Meta-calculation ${calcName} produced no results for ${dateStr}. Skipping write.`);
723
+ }
724
+ } catch (e) {
725
+ logger.log('ERROR', `[${passName}] Meta-calc process/commit failed for ${calcName} on ${dateStr}`, { err: e.message, stack: e.stack });
726
+ }
727
+ }
728
+
729
+ const completionStatus = successCount === calculationsToRun.length ? 'SUCCESS' : 'WARN';
730
+ logger.log(completionStatus, `[${passName}] Completed ${dateStr}. Success: ${successCount}/${calculationsToRun.length}.`);
731
+
732
+ } catch (err) {
733
+ logger.log('ERROR', `[${passName}] Fatal error for ${dateStr}`, { errorMessage: err.message, stack: err.stack });
734
+ throw err;
735
+ }
736
+ }
737
+
738
+
739
+ module.exports = {
740
+ runComputationPass,
741
+ };
package/index.js CHANGED
@@ -32,31 +32,27 @@ const orchestrator = {
32
32
  };
33
33
 
34
34
  // --- Pipe 2: Dispatcher ---
35
-
35
+ // ... (identical to original file)
36
36
  const dispatcher = {
37
- // Main Pipe
38
37
  handleRequest: require('./functions/dispatcher/index').handleRequest,
39
-
40
- // Sub-Pipe
41
38
  dispatchTasksInBatches: require('./functions/dispatcher/helpers/dispatch_helpers').dispatchTasksInBatches,
42
39
  };
43
40
 
44
41
  // --- Pipe 3: Task Engine ---
45
-
42
+ // ... (identical to original file)
46
43
  const taskEngine = {
47
- // Main Pipe
48
44
  handleRequest: require('./functions/task-engine/handler_creator').handleRequest,
49
-
50
- // Sub-Pipes
51
45
  handleDiscover: require('./functions/task-engine/helpers/discover_helpers').handleDiscover,
52
46
  handleVerify: require('./functions/task-engine/helpers/verify_helpers').handleVerify,
53
- handleUpdate: require('./functions/task-engine/helpers/update_helpers').handleUpdate,
47
+ handleUpdate: require('./functions/task-engine/helpers/update_helpers').handleUpdate,pnl
54
48
  };
55
49
 
56
50
  // --- Pipe 4: Computation System ---
57
51
  const computationSystem = {
58
- // Main Pipe
59
- runOrchestration: require('./functions/computation-system/helpers/orchestration_helpers').runComputationOrchestrator,
52
+ // --- (MODIFICATION) ---
53
+ // The main pipe is now the new pass runner
54
+ runComputationPass: require('./functions/computation-system/helpers/computation_pass_runner').runComputationPass,
55
+ // --- (END MODIFICATION) ---
60
56
 
61
57
  // Sub-Pipes (Exposing utils for potential external use)
62
58
  dataLoader: require('./functions/computation-system/utils/data_loader'),
@@ -64,17 +60,14 @@ const computationSystem = {
64
60
  };
65
61
 
66
62
  // --- Pipe 5: API ---
67
-
63
+ // ... (identical to original file)
68
64
  const api = {
69
- // Main Pipe
70
65
  createApiApp: require('./functions/generic-api/index').createApiApp,
71
-
72
- // Sub-Pipes (Helpers exposed for documentation/testing)
73
66
  helpers: require('./functions/generic-api/helpers/api_helpers'),
74
67
  };
75
68
 
76
69
  // --- Pipe 6: Maintenance ---
77
- // Standalone cleanup and utility functions TODO -- Some of these are not really maintenance pipes, socials could do with its own pipe..?
70
+ // ... (identical to original file)
78
71
  const maintenance = {
79
72
  runSpeculatorCleanup: require('./functions/speculator-cleanup-orchestrator/helpers/cleanup_helpers').runCleanup,
80
73
  handleInvalidSpeculator: require('./functions/invalid-speculator-handler/helpers/handler_helpers').handleInvalidSpeculator,
@@ -86,7 +79,7 @@ const maintenance = {
86
79
  };
87
80
 
88
81
  // --- Pipe 7: Proxy ---
89
-
82
+ // ... (identical to original file)
90
83
  const proxy = {
91
84
  handlePost: require('./functions/appscript-api/index').handlePost,
92
85
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bulltrackers-module",
3
- "version": "1.0.118",
3
+ "version": "1.0.120",
4
4
  "description": "Helper Functions for Bulltrackers.",
5
5
  "main": "index.js",
6
6
  "files": [