bulltrackers-module 1.0.105 → 1.0.106

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.
Files changed (33) hide show
  1. package/README.MD +222 -222
  2. package/functions/appscript-api/helpers/errors.js +19 -19
  3. package/functions/appscript-api/index.js +58 -58
  4. package/functions/computation-system/helpers/orchestration_helpers.js +647 -113
  5. package/functions/computation-system/utils/data_loader.js +191 -191
  6. package/functions/computation-system/utils/utils.js +149 -254
  7. package/functions/core/utils/firestore_utils.js +433 -433
  8. package/functions/core/utils/pubsub_utils.js +53 -53
  9. package/functions/dispatcher/helpers/dispatch_helpers.js +47 -47
  10. package/functions/dispatcher/index.js +52 -52
  11. package/functions/etoro-price-fetcher/helpers/handler_helpers.js +124 -124
  12. package/functions/fetch-insights/helpers/handler_helpers.js +91 -91
  13. package/functions/generic-api/helpers/api_helpers.js +379 -379
  14. package/functions/generic-api/index.js +150 -150
  15. package/functions/invalid-speculator-handler/helpers/handler_helpers.js +75 -75
  16. package/functions/orchestrator/helpers/discovery_helpers.js +226 -226
  17. package/functions/orchestrator/helpers/update_helpers.js +92 -92
  18. package/functions/orchestrator/index.js +147 -147
  19. package/functions/price-backfill/helpers/handler_helpers.js +116 -123
  20. package/functions/social-orchestrator/helpers/orchestrator_helpers.js +61 -61
  21. package/functions/social-task-handler/helpers/handler_helpers.js +288 -288
  22. package/functions/task-engine/handler_creator.js +78 -78
  23. package/functions/task-engine/helpers/discover_helpers.js +125 -125
  24. package/functions/task-engine/helpers/update_helpers.js +118 -118
  25. package/functions/task-engine/helpers/verify_helpers.js +162 -162
  26. package/functions/task-engine/utils/firestore_batch_manager.js +258 -258
  27. package/index.js +105 -113
  28. package/package.json +45 -45
  29. package/functions/computation-system/computation_dependencies.json +0 -120
  30. package/functions/computation-system/helpers/worker_helpers.js +0 -340
  31. package/functions/computation-system/utils/computation_state_manager.js +0 -178
  32. package/functions/computation-system/utils/dependency_graph.js +0 -191
  33. package/functions/speculator-cleanup-orchestrator/helpers/cleanup_helpers.js +0 -160
@@ -1,114 +1,648 @@
1
- /**
2
- * @fileoverview Main pipe: pipe.computationSystem.runOrchestration
3
- *
4
- * This function is the "Scheduler" for the event-driven DAG.
5
- * It is triggered by a Cloud Scheduler (e.g., at 01:00 UTC).
6
- *
7
- * Its ONLY responsibility is to:
8
- * 1. Find all dates that are new or incomplete (using the StateManager).
9
- * 2. For each of those dates, initialize a new state document.
10
- * 3. Publish a "node task" for each "root node" (nodes with no dependencies).
11
- */
12
-
13
- const { FieldPath } = require('@google-cloud/firestore');
14
- // Import the new graph and state manager
15
- const { ComputationGraph } = require('../utils/dependency_graph.js');
16
- const stateManager = require('../utils/computation_state_manager.js');
17
- // Import existing utils for finding dates
18
- const { getExpectedDateStrings, getFirstDateFromSourceData } = require('../utils/utils.js');
19
-
20
- /**
21
- * Main pipe: pipe.computationSystem.runOrchestration
22
- * @param {object} config - The computation system configuration object.
23
- * @param {object} dependencies - Contains db, logger, calculationUtils, pubsubUtils.
24
- * @param {object} calculations - The injected calculations object from 'aiden-shared-calculations-unified'.
25
- * @param {object} dependencyConfig - The loaded computation_dependencies.json object.
26
- * @returns {Promise<Object>} Summary of the orchestration kickoff.
27
- */
28
- async function runComputationOrchestrator(config, dependencies, calculations, dependencyConfig) {
29
- const { logger, db, calculationUtils, pubsubUtils } = dependencies;
30
-
31
- logger.log('INFO', '🚀 [DAG Orchestrator] Starting scheduled run...');
32
-
33
- // 1. Build the computation dependency graph
34
- // The graph is now passed in dependencies, initialized in index.js
35
- const graph = dependencies.computationGraph;
36
- if (!graph || !graph.isBuilt) {
37
- logger.log('ERROR', '[DAG Orchestrator] ComputationGraph not built or provided. Halting.');
38
- return { success: false, error: "ComputationGraph not available." };
39
- }
40
-
41
- const allNodes = graph.getAllNodes();
42
- const rootNodes = graph.getRootNodes();
43
-
44
- if (rootNodes.length === 0) {
45
- logger.log('ERROR', '[DAG Orchestrator] No root nodes found in graph. Halting.');
46
- return { success: false, error: "No root nodes (Batch 1) found." };
47
- }
48
-
49
- // 2. Find date range to check
50
- const yesterday = new Date();
51
- yesterday.setUTCDate(yesterday.getUTCDate() - 1);
52
- const endDateUTC = new Date(Date.UTC(yesterday.getUTCFullYear(), yesterday.getUTCMonth(), yesterday.getUTCDate()));
53
-
54
- const firstDate = await getFirstDateFromSourceData(config, dependencies);
55
- const startDateUTC = firstDate
56
- ? new Date(Date.UTC(firstDate.getUTCFullYear(), firstDate.getUTCMonth(), firstDate.getUTCDate()))
57
- : new Date(config.earliestComputationDate + 'T00:00:00Z');
58
-
59
- const allExpectedDates = getExpectedDateStrings(startDateUTC, endDateUTC);
60
- if (allExpectedDates.length === 0) {
61
- logger.log('WARN', '[DAG Orchestrator] No dates found in range to process.');
62
- return { success: true, message: "No dates in range." };
63
- }
64
-
65
- // 3. Find all new or incomplete dates using the StateManager
66
- const datesToProcess = await stateManager.getIncompleteDates(dependencies, allExpectedDates);
67
-
68
- if (datesToProcess.length === 0) {
69
- logger.log('SUCCESS', '[DAG Orchestrator] All dates are up-to-date. Nothing to do.');
70
- return { success: true, datesKickedOff: 0, message: "All dates up-to-date." };
71
- }
72
-
73
- logger.log('INFO', `[DAG Orchestrator] Found ${datesToProcess.length} dates needing processing.`);
74
-
75
- // 4. Create and publish root node tasks for each date
76
- const tasks = [];
77
- for (const date of datesToProcess) {
78
- // 4a. Initialize the state document for this date
79
- await stateManager.initializeState(dependencies, date, allNodes);
80
-
81
- // 4b. Create tasks for all root nodes for this date
82
- for (const rootNode of rootNodes) {
83
- tasks.push({
84
- date: date,
85
- nodeName: rootNode.name,
86
- isRootNode: true // Add flag for clarity
87
- });
88
- }
89
- }
90
-
91
- const topicName = config.computeNodeTopicName; // Use a new topic name
92
- if (!topicName) {
93
- logger.log('ERROR', '[DAG Orchestrator] `computeNodeTopicName` is not defined in config. Cannot publish tasks.');
94
- return { success: false, error: "Config missing 'computeNodeTopicName'." };
95
- }
96
-
97
- await pubsubUtils.batchPublishTasks(dependencies, {
98
- topicName: topicName,
99
- tasks: tasks,
100
- taskType: 'Compute Root Node Kickoff'
101
- });
102
-
103
- logger.log('SUCCESS', `[DAG Orchestrator] Published ${tasks.length} root node tasks to ${topicName} (for ${datesToProcess.length} dates).`);
104
-
105
- return {
106
- orchestrator: "Event-Driven DAG",
107
- datesKickedOff: datesToProcess.length,
108
- rootNodeTasks: tasks.length
109
- };
110
- }
111
-
112
- module.exports = {
113
- runComputationOrchestrator,
1
+ /**
2
+ * @fileoverview Main pipe: pipe.computationSystem.runOrchestration
3
+ * REFACTORED: Now stateless and receives dependencies.
4
+ * All internal helpers now receive (config, dependencies) as well.
5
+ * UPDATED: Reads a static manifest to determine execution order and
6
+ * passes computed dependencies in-memory.
7
+ * NEW: Checks for root data (portfolios, insights, social)
8
+ * before processing any calculations for a given day.
9
+ */
10
+
11
+ const { FieldPath } = require('@google-cloud/firestore');
12
+ // Import sub-pipes/utils from their new locations
13
+ const {
14
+ getPortfolioPartRefs,
15
+ loadFullDayMap,
16
+ loadDataByRefs,
17
+ loadDailyInsights,
18
+ loadDailySocialPostInsights
19
+ } = require('../utils/data_loader.js');
20
+
21
+ const {
22
+ normalizeName,
23
+ getExpectedDateStrings,
24
+ getFirstDateFromSourceData,
25
+ commitBatchInChunks
26
+ } = require('../utils/utils.js');
27
+
28
+
29
+ /**
30
+ * Groups the manifest by pass number.
31
+ * @param {Array<object>} manifest - The computation manifest.
32
+ * @returns {object} { '1': [...], '2': [...] }
33
+ */
34
+ function groupByPass(manifest) {
35
+ return manifest.reduce((acc, calc) => {
36
+ (acc[calc.pass] = acc[calc.pass] || []).push(calc);
37
+ return acc;
38
+ }, {});
39
+ }
40
+
41
+ /**
42
+ * --- NEW HELPER ---
43
+ * Checks if the root data (portfolios, insights, social) exists for a given day.
44
+ * @param {string} dateStr - The date string to check (YYYY-MM-DD).
45
+ * @param {object} config - The computation system configuration object.
46
+ * @param {object} dependencies - Contains db, logger, calculationUtils.
47
+ * @returns {Promise<object>} { portfolioRefs, insightsData, socialData, isAvailable }
48
+ */
49
+ async function checkRootDataAvailability(dateStr, config, dependencies) {
50
+ const { logger } = dependencies;
51
+ logger.log('INFO', `[Orchestrator] Checking root data availability for ${dateStr}...`);
52
+
53
+ try {
54
+ const [portfolioRefs, insightsData, socialData] = await Promise.all([
55
+ getPortfolioPartRefs(config, dependencies, dateStr),
56
+ loadDailyInsights(config, dependencies, dateStr),
57
+ loadDailySocialPostInsights(config, dependencies, dateStr)
58
+ ]);
59
+
60
+ const isAvailable = (portfolioRefs && portfolioRefs.length > 0) || !!insightsData || !!socialData;
61
+
62
+ if (isAvailable) {
63
+ logger.log('INFO', `[Orchestrator] Root data found for ${dateStr}. (Portfolio parts: ${portfolioRefs.length}, Insights: ${!!insightsData}, Social: ${!!socialData})`);
64
+ }
65
+
66
+ return {
67
+ portfolioRefs: portfolioRefs || [],
68
+ insightsData: insightsData || null,
69
+ socialData: socialData || null,
70
+ isAvailable: isAvailable
71
+ };
72
+
73
+ } catch (err) {
74
+ logger.log('ERROR', `[Orchestrator] Error checking data availability for ${dateStr}`, { errorMessage: err.message });
75
+ return { portfolioRefs: [], insightsData: null, socialData: null, isAvailable: false };
76
+ }
77
+ }
78
+
79
+
80
+ /**
81
+ * Main pipe: pipe.computationSystem.runOrchestration
82
+ * @param {object} config - The computation system configuration object.
83
+ * @param {object} dependencies - Contains db, logger, calculationUtils.
84
+ * @param {Array<object>} computationManifest - The injected computation manifest.
85
+ * @returns {Promise<Object>} Summary of all passes.
86
+ */
87
+ async function runComputationOrchestrator(config, dependencies, computationManifest) {
88
+ const { logger } = dependencies;
89
+ const summary = {};
90
+
91
+ const yesterday = new Date();
92
+ yesterday.setUTCDate(yesterday.getUTCDate() - 1);
93
+ const endDateUTC = new Date(Date.UTC(yesterday.getUTCFullYear(), yesterday.getUTCMonth(), yesterday.getUTCDate()));
94
+
95
+ // Pass dependencies to sub-pipe
96
+ const firstDate = await getFirstDateFromSourceData(config, dependencies);
97
+ const startDateUTC = firstDate
98
+ ? new Date(Date.UTC(firstDate.getUTCFullYear(), firstDate.getUTCMonth(), firstDate.getUTCDate()))
99
+ : new Date(config.earliestComputationDate + 'T00:00:00Z');
100
+
101
+ const allExpectedDates = getExpectedDateStrings(startDateUTC, endDateUTC);
102
+
103
+ // --- Group the manifest by pass number ---
104
+ const passes = groupByPass(computationManifest);
105
+ const passNumbers = Object.keys(passes).sort((a, b) => a - b);
106
+
107
+ // --- Process ONE DAY at a time, in order ---
108
+ for (const dateStr of allExpectedDates) {
109
+ const dateToProcess = new Date(dateStr + 'T00:00:00Z');
110
+
111
+ // --- NEW: Check for root data *before* processing the day ---
112
+ const rootData = await checkRootDataAvailability(dateStr, config, dependencies);
113
+ if (!rootData.isAvailable) {
114
+ logger.log('WARN', `[Orchestrator] Skipping all computations for ${dateStr} due to missing root data (no portfolios, insights, or social data found).`);
115
+ continue; // Skip to the next day
116
+ }
117
+ // --- END NEW CHECK ---
118
+
119
+ logger.log('INFO', `[Orchestrator] Processing all passes for ${dateStr}...`);
120
+
121
+ // This cache will hold results *for this day only*
122
+ const dailyResultsCache = new Map();
123
+ let passSuccess = true;
124
+
125
+ for (const passNum of passNumbers) {
126
+ if (!passSuccess) {
127
+ logger.log('WARN', `[Orchestrator] Skipping Pass ${passNum} for ${dateStr} due to previous pass failure.`);
128
+ break; // Skip subsequent passes if a previous one failed
129
+ }
130
+
131
+ const calcsInPass = passes[passNum] || [];
132
+
133
+ const standardCalcs = calcsInPass.filter(c => c.type === 'standard');
134
+ const metaCalcs = calcsInPass.filter(c => c.type === 'meta');
135
+
136
+ logger.log('INFO', `[Orchestrator] Starting Pass ${passNum} for ${dateStr} (${standardCalcs.length} standard, ${metaCalcs.length} meta).`);
137
+
138
+ try {
139
+ // 1. Run standard calcs for this pass
140
+ if (standardCalcs.length > 0) {
141
+ const standardResults = await runUnifiedComputation(
142
+ dateToProcess,
143
+ standardCalcs, // Pass the manifest objects
144
+ `Pass ${passNum} (Standard)`,
145
+ config,
146
+ dependencies,
147
+ rootData // <-- Pass pre-fetched root data
148
+ );
149
+
150
+ // Add results to cache
151
+ for (const [calcName, result] of Object.entries(standardResults)) {
152
+ dailyResultsCache.set(calcName, result);
153
+ }
154
+ }
155
+
156
+ // 2. Run meta calcs for this pass
157
+ if (metaCalcs.length > 0) {
158
+ const metaResults = await runMetaComputation(
159
+ dateToProcess,
160
+ metaCalcs, // Pass the manifest objects
161
+ `Pass ${passNum} (Meta)`,
162
+ config,
163
+ dependencies,
164
+ dailyResultsCache // <-- PASS THE CACHE
165
+ );
166
+
167
+ // Add results to cache
168
+ for (const [calcName, result] of Object.entries(metaResults)) {
169
+ dailyResultsCache.set(calcName, result);
170
+ }
171
+ }
172
+ logger.log('SUCCESS', `[Orchestrator] Completed Pass ${passNum} for ${dateStr}.`);
173
+ } catch (err) {
174
+ logger.log('ERROR', `[Orchestrator] FAILED Pass ${passNum} for ${dateStr}`, { errorMessage: err.message, stack: err.stack });
175
+ passSuccess = false;
176
+ }
177
+ } // End passes loop
178
+ logger.log('INFO', `[Orchestrator] Finished processing for ${dateStr}.`);
179
+ } // End dates loop
180
+
181
+ logger.log('INFO', '[Orchestrator] Computation orchestration finished.');
182
+ return summary;
183
+ }
184
+
185
+ /**
186
+ * Internal sub-pipe: Initializes calculator instances.
187
+ * --- MODIFIED: Attaches the manifest entry to the instance. ---
188
+ */
189
+ function initializeCalculators(calculationsToRun, logger) {
190
+ const state = {};
191
+ for (const calcManifest of calculationsToRun) {
192
+ const calcName = normalizeName(calcManifest.name);
193
+ const CalculationClass = calcManifest.class;
194
+
195
+ if (typeof CalculationClass === 'function') {
196
+ try {
197
+ const instance = new CalculationClass();
198
+ instance.manifest = calcManifest; // <-- Attach manifest data
199
+ state[calcName] = instance;
200
+ } catch (e) {
201
+ logger.warn(`[Orchestrator] Init failed for ${calcName}`, { errorMessage: e.message });
202
+ state[calcName] = null;
203
+ }
204
+ } else {
205
+ logger.warn(`[Orchestrator] Calculation class not found in manifest for: ${calcName}`);
206
+ state[calcName] = null;
207
+ }
208
+ }
209
+ return state;
210
+ }
211
+
212
+ /**
213
+ * Internal sub-pipe: Streams data and calls process() on calculators.
214
+ * --- MODIFIED: Uses manifest flags for logic. ---
215
+ */
216
+ async function streamAndProcess(
217
+ dateStr, todayRefs, state, passName, config, dependencies,
218
+ yesterdayPortfolios = {},
219
+ todayInsights = null,
220
+ yesterdayInsights = null,
221
+ todaySocialPostInsights = null,
222
+ yesterdaySocialPostInsights = null
223
+ ) {
224
+ const { logger, calculationUtils } = dependencies;
225
+ logger.log('INFO', `[${passName}] Streaming ${todayRefs.length} 'today' part docs for ${dateStr}...`);
226
+
227
+ const yesterdayDate = new Date(dateStr + 'T00:00:00Z');
228
+ yesterdayDate.setUTCDate(yesterdayDate.getUTCDate() - 1);
229
+ const yesterdayStr = yesterdayDate.toISOString().slice(0, 10);
230
+
231
+ const { instrumentToTicker, instrumentToSector } = await calculationUtils.loadInstrumentMappings();
232
+
233
+ const context = {
234
+ instrumentMappings: instrumentToTicker,
235
+ sectorMapping: instrumentToSector,
236
+ todayDateStr: dateStr,
237
+ yesterdayDateStr: yesterdayStr,
238
+ dependencies: dependencies,
239
+ config: config
240
+ };
241
+
242
+ const batchSize = config.partRefBatchSize || 10;
243
+ let isFirstUser = true;
244
+
245
+ for (let i = 0; i < todayRefs.length; i += batchSize) {
246
+ const batchRefs = todayRefs.slice(i, i + batchSize);
247
+ const todayPortfoliosChunk = await loadDataByRefs(config, dependencies, batchRefs);
248
+
249
+ for (const uid in todayPortfoliosChunk) {
250
+ const p = todayPortfoliosChunk[uid];
251
+ if (!p) continue;
252
+
253
+ const userType = p.PublicPositions ? 'speculator' : 'normal';
254
+
255
+ for (const calcName in state) { // calcName is already normalized
256
+ const calc = state[calcName];
257
+ if (!calc || typeof calc.process !== 'function') continue;
258
+
259
+ // --- NEW ROBUST LOGIC ---
260
+ const manifestCalc = calc.manifest;
261
+ const isSocialOrInsights = manifestCalc.category === 'socialPosts' || manifestCalc.category === 'insights';
262
+ const isHistoricalCalc = manifestCalc.isHistorical === true;
263
+ const isSpeculatorCalc = manifestCalc.category === 'speculators';
264
+ // --- END NEW LOGIC ---
265
+
266
+ let processArgs;
267
+ const allContextArgs = [
268
+ context,
269
+ todayInsights,
270
+ yesterdayInsights,
271
+ todaySocialPostInsights,
272
+ yesterdaySocialPostInsights
273
+ ];
274
+
275
+ if (isSocialOrInsights) {
276
+ if (isFirstUser) {
277
+ processArgs = [null, null, null, ...allContextArgs];
278
+ } else {
279
+ continue; // Only run once for the first "user"
280
+ }
281
+ } else if (isHistoricalCalc) { // Assumes historical
282
+ const pYesterday = yesterdayPortfolios[uid];
283
+ if (!pYesterday) {
284
+ continue; // Skip if no yesterday data
285
+ }
286
+ processArgs = [p, pYesterday, uid, ...allContextArgs];
287
+ } else {
288
+ // Standard daily calculation
289
+ processArgs = [p, null, uid, ...allContextArgs];
290
+ }
291
+
292
+ // --- NEW ROBUST CHECK for user type ---
293
+ if (!isSocialOrInsights) {
294
+ if ((userType === 'normal' && isSpeculatorCalc) ||
295
+ (userType === 'speculator' && !isSpeculatorCalc && calcName !== 'users-processed')) {
296
+ continue; // Skip: wrong user type for this calc
297
+ }
298
+ }
299
+ // --- END NEW CHECK ---
300
+
301
+ try {
302
+ await Promise.resolve(calc.process(...processArgs));
303
+ } catch (e) {
304
+ logger.log('WARN', `Process error in ${calcName} for user ${uid}`, { err: e.message });
305
+ }
306
+ }
307
+ isFirstUser = false;
308
+ }
309
+ }
310
+
311
+ // Handle case where there are no users but we still need to run insights/social calcs
312
+ if (todayRefs.length === 0 && isFirstUser) {
313
+ logger.log('INFO', `[${passName}] No user portfolios found for ${dateStr}. Running insights/social calcs once.`);
314
+ const allContextArgs = [
315
+ context,
316
+ todayInsights,
317
+ yesterdayInsights,
318
+ todaySocialPostInsights,
319
+ yesterdaySocialPostInsights
320
+ ];
321
+
322
+ for (const calcName in state) {
323
+ const calc = state[calcName];
324
+ if (!calc || typeof calc.process !== 'function') continue;
325
+
326
+ const manifestCalc = calc.manifest;
327
+ const isSocialOrInsights = manifestCalc.category === 'socialPosts' || manifestCalc.category === 'insights';
328
+
329
+ if (isSocialOrInsights) {
330
+ try {
331
+ await Promise.resolve(calc.process(null, null, null, ...allContextArgs));
332
+ } catch (e) {
333
+ logger.log('WARN', `Process error in ${calcName} for no-user run`, { err: e.message });
334
+ }
335
+ }
336
+ }
337
+ }
338
+ }
339
+
340
+
341
+ /**
342
+ * Internal sub-pipe: Runs "standard" computations for a single date.
343
+ * MODIFIED: Accepts pre-fetched rootData.
344
+ * MODIFIED: Returns a map of results for the in-memory cache.
345
+ */
346
+ async function runUnifiedComputation(dateToProcess, calculationsToRun, passName, config, dependencies, rootData) {
347
+ const { db, logger } = dependencies;
348
+ const dateStr = dateToProcess.toISOString().slice(0, 10);
349
+ logger.log('INFO', `[${passName}] Starting run for ${dateStr} with ${calculationsToRun.length} calcs.`);
350
+
351
+ // This map will store the final results to be returned
352
+ const passResults = {};
353
+
354
+ try {
355
+ // --- NEW: Get root data from the orchestrator ---
356
+ const {
357
+ portfolioRefs: todayRefs,
358
+ insightsData: todayInsightsData,
359
+ socialData: todaySocialPostInsightsData
360
+ } = rootData;
361
+ // --- END NEW ---
362
+
363
+ // (The check for data availability is now done in the orchestrator)
364
+
365
+ let yesterdayPortfolios = {};
366
+ let yesterdayInsightsData = null;
367
+ let yesterdaySocialPostInsightsData = null;
368
+
369
+ // Check if any calc needs yesterday's data
370
+ // --- MODIFIED: Use the manifest entry ---
371
+ const requiresYesterdayPortfolio = calculationsToRun.some(c => c.isHistorical === true);
372
+ const requiresYesterdayInsights = calculationsToRun.some(c => c.class.prototype.process.toString().includes('yesterdayInsights'));
373
+ const requiresYesterdaySocialPosts = calculationsToRun.some(c => c.class.prototype.process.toString().includes('yesterdaySocialPostInsights'));
374
+
375
+ // --- FULL "YESTERDAY" LOGIC ---
376
+ if (requiresYesterdayPortfolio || requiresYesterdayInsights || requiresYesterdaySocialPosts) {
377
+
378
+ if(requiresYesterdayInsights) {
379
+ let daysAgo = 1;
380
+ const maxLookback = 30;
381
+ while (!yesterdayInsightsData && daysAgo <= maxLookback) {
382
+ const prev = new Date(dateToProcess);
383
+ prev.setUTCDate(prev.getUTCDate() - daysAgo);
384
+ const prevStr = prev.toISOString().slice(0, 10);
385
+ yesterdayInsightsData = await loadDailyInsights(config, dependencies, prevStr);
386
+ if (yesterdayInsightsData) {
387
+ logger.log('INFO', `[${passName}] Found 'yesterday' instrument insights data from ${daysAgo} day(s) ago (${prevStr}).`);
388
+ } else {
389
+ daysAgo++;
390
+ }
391
+ }
392
+ if (!yesterdayInsightsData) logger.log('WARN', `[${passName}] Could not find any 'yesterday' instrument insights data within a ${maxLookback} day lookback.`);
393
+ }
394
+
395
+ if(requiresYesterdaySocialPosts) {
396
+ let daysAgo = 1;
397
+ const maxLookback = 30;
398
+ while (!yesterdaySocialPostInsightsData && daysAgo <= maxLookback) {
399
+ const prev = new Date(dateToProcess);
400
+ prev.setUTCDate(prev.getUTCDate() - daysAgo);
401
+ const prevStr = prev.toISOString().slice(0, 10);
402
+ yesterdaySocialPostInsightsData = await loadDailySocialPostInsights(config, dependencies, prevStr);
403
+ if (yesterdaySocialPostInsightsData) {
404
+ logger.log('INFO', `[${passName}] Found 'yesterday' social post insights data from ${daysAgo} day(s) ago (${prevStr}).`);
405
+ } else {
406
+ daysAgo++;
407
+ }
408
+ }
409
+ if (!yesterdaySocialPostInsightsData) logger.log('WARN', `[${passName}] Could not find any 'yesterday' social post insights data within a ${maxLookback} day lookback.`);
410
+ }
411
+
412
+ if (requiresYesterdayPortfolio) {
413
+ const prev = new Date(dateToProcess);
414
+ prev.setUTCDate(prev.getUTCDate() - 1);
415
+ const prevStr = prev.toISOString().slice(0, 10);
416
+ const yesterdayRefs = await getPortfolioPartRefs(config, dependencies, prevStr);
417
+ if (yesterdayRefs.length > 0) {
418
+ yesterdayPortfolios = await loadFullDayMap(config, dependencies, yesterdayRefs);
419
+ logger.log('INFO', `[${passName}] Loaded yesterday's (${prevStr}) portfolio map for historical calcs.`);
420
+ } else {
421
+ logger.log('WARN', `[${passName}] Yesterday's (${prevStr}) portfolio data not found. Historical calcs requiring it will be skipped.`);
422
+ }
423
+ }
424
+ }
425
+ // --- END FULL "YESTERDAY" LOGIC ---
426
+
427
+ const state = initializeCalculators(calculationsToRun, logger);
428
+ await streamAndProcess(
429
+ dateStr,
430
+ todayRefs,
431
+ state,
432
+ passName,
433
+ config,
434
+ dependencies,
435
+ yesterdayPortfolios,
436
+ todayInsightsData,
437
+ yesterdayInsightsData,
438
+ todaySocialPostInsightsData,
439
+ yesterdaySocialPostInsightsData
440
+ );
441
+
442
+ let successCount = 0;
443
+ const resultsCollectionRef = db.collection(config.resultsCollection).doc(dateStr).collection(config.resultsSubcollection);
444
+
445
+ for (const calcName in state) { // calcName is already normalized
446
+ const calc = state[calcName];
447
+ if (!calc || typeof calc.getResult !== 'function') continue;
448
+
449
+ // --- MODIFIED: Get category from the attached manifest ---
450
+ const category = calc.manifest.category || 'unknown';
451
+
452
+ try {
453
+ const result = await Promise.resolve(calc.getResult());
454
+
455
+ // Add to results map for in-memory cache
456
+ if (result) {
457
+ passResults[calcName] = result;
458
+ }
459
+
460
+ const pendingWrites = [];
461
+ const summaryData = {};
462
+
463
+ if (result && Object.keys(result).length > 0) {
464
+ // (Special handling for sharded calcs remains the same)
465
+ let isSharded = false;
466
+ const shardedCollections = {
467
+ 'sharded_user_profile': config.shardedUserProfileCollection,
468
+ 'sharded_user_profitability': config.shardedProfitabilityCollection
469
+ };
470
+
471
+ for (const resultKey in shardedCollections) {
472
+ if (result[resultKey]) {
473
+ isSharded = true;
474
+ const shardCollectionName = shardedCollections[resultKey];
475
+ const shardedData = result[resultKey];
476
+
477
+ for (const shardId in shardedData) {
478
+ const shardDocData = shardedData[shardId];
479
+ if (shardDocData && Object.keys(shardDocData).length > 0) {
480
+ const shardRef = db.collection(shardCollectionName).doc(shardId);
481
+ pendingWrites.push({ ref: shardRef, data: shardedData[shardId] });
482
+ }
483
+ }
484
+ // Get results *not* in the shard key
485
+ const { [resultKey]: _, ...otherResults } = result;
486
+ if (Object.keys(otherResults).length > 0) {
487
+ const computationDocRef = resultsCollectionRef.doc(category)
488
+ .collection(config.computationsSubcollection)
489
+ .doc(calcName);
490
+ pendingWrites.push({ ref: computationDocRef, data: otherResults });
491
+ }
492
+ }
493
+ }
494
+
495
+ if (!isSharded) {
496
+ const computationDocRef = resultsCollectionRef.doc(category)
497
+ .collection(config.computationsSubcollection)
498
+ .doc(calcName);
499
+ pendingWrites.push({ ref: computationDocRef, data: result });
500
+ }
501
+ // --- END SHARDED HANDLING ---
502
+
503
+ if (!summaryData[category]) summaryData[category] = {};
504
+ summaryData[category][calcName] = true;
505
+
506
+ if (Object.keys(summaryData).length > 0) {
507
+ const topLevelDocRef = db.collection(config.resultsCollection).doc(dateStr);
508
+ pendingWrites.push({ ref: topLevelDocRef, data: summaryData });
509
+ }
510
+
511
+ if (pendingWrites.length > 0) {
512
+ await commitBatchInChunks(
513
+ config,
514
+ dependencies,
515
+ pendingWrites,
516
+ `Commit ${passName} ${dateStr} [${calcName}]`
517
+ );
518
+ successCount++;
519
+ }
520
+ } else {
521
+ logger.log('WARN', `[${passName}] Calculation ${calcName} produced no results for ${dateStr}. Skipping write to allow backfill.`);
522
+ }
523
+ } catch (e) {
524
+ logger.log('ERROR', `[${passName}] getResult/Commit failed for ${calcName} on ${dateStr}`, { err: e.message });
525
+ }
526
+ }
527
+
528
+ const completionStatus = successCount === calculationsToRun.length ? 'SUCCESS' : 'WARN';
529
+ logger.log(completionStatus, `[${passName}] Completed ${dateStr}. Success: ${successCount}/${calculationsToRun.length}.`);
530
+
531
+ return passResults; // --- RETURN THE RESULTS ---
532
+
533
+ } catch (err) {
534
+ logger.log('ERROR', `[${passName}] Fatal error for ${dateStr}`, { errorMessage: err.message, stack: err.stack });
535
+ throw err; // Re-throw to stop the orchestrator for this day
536
+ }
537
+ }
538
+
539
+
540
+ /**
541
+ * Internal sub-pipe: Runs "meta" or "backtest" computations for a single date.
542
+ * MODIFIED: Accepts in-memory cache and passes dependencies to calcs.
543
+ * Returns a map of results.
544
+ */
545
+ async function runMetaComputation(dateToProcess, calculationsToRun, passName, config, dependencies, dailyResultsCache) {
546
+ const { db, logger } = dependencies;
547
+ const dateStr = dateToProcess.toISOString().slice(0, 10);
548
+ logger.log('INFO', `[${passName}] Starting run for ${dateStr} with ${calculationsToRun.length} calcs.`);
549
+
550
+ const passResults = {}; // Map to store and return results
551
+
552
+ try {
553
+ const resultsCollectionRef = db.collection(config.resultsCollection).doc(dateStr).collection(config.resultsSubcollection);
554
+ let successCount = 0;
555
+
556
+ for (const manifestCalc of calculationsToRun) {
557
+ const calcName = normalizeName(manifestCalc.name);
558
+ // --- MODIFIED: Get category from manifest ---
559
+ const category = manifestCalc.category || 'unknown';
560
+ const CalcClass = manifestCalc.class;
561
+
562
+ if (typeof CalcClass !== 'function') {
563
+ logger.log('ERROR', `[${passName}] Invalid class in manifest for ${calcName}. Skipping.`);
564
+ continue;
565
+ }
566
+
567
+ const instance = new CalcClass();
568
+
569
+ try {
570
+ // --- Gather dependencies from the cache ---
571
+ const computedDependencies = {};
572
+ let missingDep = false;
573
+ if (manifestCalc.dependencies) {
574
+ for (const depName of manifestCalc.dependencies) {
575
+ const normalizedDepName = normalizeName(depName);
576
+ if (!dailyResultsCache.has(normalizedDepName)) {
577
+ logger.log('ERROR', `[${passName}] Missing required dependency "${normalizedDepName}" for calculation "${calcName}". This should not happen. Skipping calc.`);
578
+ missingDep = true;
579
+ break;
580
+ }
581
+ computedDependencies[normalizedDepName] = dailyResultsCache.get(normalizedDepName);
582
+ }
583
+ }
584
+ if (missingDep) continue;
585
+
586
+ // --- Call process with the dependencies ---
587
+ const result = await Promise.resolve(instance.process(
588
+ dateStr,
589
+ dependencies,
590
+ config,
591
+ computedDependencies // <-- PASS IN-MEMORY DEPS
592
+ ));
593
+
594
+ // Add to results map
595
+ if (result) {
596
+ passResults[calcName] = result;
597
+ }
598
+
599
+ const pendingWrites = [];
600
+ const summaryData = {};
601
+
602
+ if (result && Object.keys(result).length > 0) {
603
+ const computationDocRef = resultsCollectionRef.doc(category)
604
+ .collection(config.computationsSubcollection)
605
+ .doc(calcName);
606
+ pendingWrites.push({ ref: computationDocRef, data: result });
607
+
608
+ if (!summaryData[category]) summaryData[category] = {};
609
+ summaryData[category][calcName] = true;
610
+
611
+ if (Object.keys(summaryData).length > 0) {
612
+ const topLevelDocRef = db.collection(config.resultsCollection).doc(dateStr);
613
+ pendingWrites.push({ ref: topLevelDocRef, data: summaryData });
614
+ }
615
+
616
+ if (pendingWrites.length > 0) {
617
+ await commitBatchInChunks(
618
+ config,
619
+ dependencies,
620
+ pendingWrites,
621
+ `Commit ${passName} ${dateStr} [${calcName}]`
622
+ );
623
+ successCount++;
624
+ }
625
+ } else {
626
+ logger.log('WARN', `[${passName}] Meta-calculation ${calcName} produced no results for ${dateStr}. Skipping write.`);
627
+ }
628
+ } catch (e) {
629
+ logger.log('ERROR', `[${passName}] Meta-calc process/commit failed for ${calcName} on ${dateStr}`, { err: e.message, stack: e.stack });
630
+ // Don't re-throw, allow other meta-calcs in the pass to run
631
+ }
632
+ }
633
+
634
+ const completionStatus = successCount === calculationsToRun.length ? 'SUCCESS' : 'WARN';
635
+ logger.log(completionStatus, `[${passName}] Completed ${dateStr}. Success: ${successCount}/${calculationsToRun.length}.`);
636
+
637
+ return passResults; // --- RETURN THE RESULTS ---
638
+
639
+ } catch (err) {
640
+ logger.log('ERROR', `[${passName}] Fatal error for ${dateStr}`, { errorMessage: err.message, stack: err.stack });
641
+ throw err; // Re-throw to stop the orchestrator for this day
642
+ }
643
+ }
644
+
645
+
646
+ module.exports = {
647
+ runComputationOrchestrator,
114
648
  };