bulltrackers-module 1.0.127 → 1.0.129
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.
- package/functions/computation-system/helpers/computation_pass_runner.js +20 -773
- package/functions/computation-system/helpers/orchestration_helpers.js +88 -867
- package/functions/computation-system/utils/data_loader.js +84 -151
- package/functions/computation-system/utils/utils.js +55 -98
- package/functions/orchestrator/helpers/discovery_helpers.js +40 -188
- package/functions/orchestrator/helpers/update_helpers.js +21 -61
- package/functions/orchestrator/index.js +42 -121
- package/functions/task-engine/handler_creator.js +22 -143
- package/functions/task-engine/helpers/discover_helpers.js +20 -90
- package/functions/task-engine/helpers/update_helpers.js +90 -185
- package/functions/task-engine/helpers/verify_helpers.js +43 -159
- package/functions/task-engine/utils/firestore_batch_manager.js +97 -290
- package/functions/task-engine/utils/task_engine_utils.js +99 -0
- package/package.json +1 -1
- package/functions/task-engine/utils/api_calls.js +0 -0
- package/functions/task-engine/utils/firestore_ops.js +0 -0
|
@@ -1,867 +1,88 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
const { logger }
|
|
78
|
-
logger.log('
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
const hasSocial = !!socialData;
|
|
90
|
-
const isAvailable = hasPortfolio || hasInsights || hasSocial;
|
|
91
|
-
|
|
92
|
-
if (isAvailable) {
|
|
93
|
-
logger.log('INFO', `[Orchestrator] Root data found for ${dateStr}. (Portfolio: ${hasPortfolio}, Insights: ${hasInsights}, Social: ${hasSocial})`);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
return {
|
|
97
|
-
portfolioRefs: portfolioRefs || [],
|
|
98
|
-
insightsData: insightsData || null,
|
|
99
|
-
socialData: socialData || null,
|
|
100
|
-
isAvailable: isAvailable,
|
|
101
|
-
// --- MODIFIED: Return granular status ---
|
|
102
|
-
hasPortfolio: hasPortfolio,
|
|
103
|
-
hasInsights: hasInsights,
|
|
104
|
-
hasSocial: hasSocial
|
|
105
|
-
// --- END MODIFICATION ---
|
|
106
|
-
};
|
|
107
|
-
|
|
108
|
-
} catch (err) {
|
|
109
|
-
logger.log('ERROR', `[Orchestrator] Error checking data availability for ${dateStr}`, { errorMessage: err.message });
|
|
110
|
-
return {
|
|
111
|
-
portfolioRefs: [], insightsData: null, socialData: null, isAvailable: false,
|
|
112
|
-
hasPortfolio: false, hasInsights: false, hasSocial: false
|
|
113
|
-
};
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
* Main pipe: pipe.computationSystem.runOrchestration
|
|
120
|
-
* @param {object} config - The computation system configuration object.
|
|
121
|
-
* @param {object} dependencies - Contains db, logger, calculationUtils.
|
|
122
|
-
* @param {Array<object>} computationManifest - The injected computation manifest.
|
|
123
|
-
* @returns {Promise<Object>} Summary of all passes.
|
|
124
|
-
*/
|
|
125
|
-
async function runComputationOrchestrator(config, dependencies, computationManifest) {
|
|
126
|
-
const { logger } = dependencies;
|
|
127
|
-
const summary = {};
|
|
128
|
-
|
|
129
|
-
const yesterday = new Date();
|
|
130
|
-
yesterday.setUTCDate(yesterday.getUTCDate() - 1);
|
|
131
|
-
const endDateUTC = new Date(Date.UTC(yesterday.getUTCFullYear(), yesterday.getUTCMonth(), yesterday.getUTCDate()));
|
|
132
|
-
|
|
133
|
-
// Pass dependencies to sub-pipe
|
|
134
|
-
const firstDate = await getFirstDateFromSourceData(config, dependencies);
|
|
135
|
-
const startDateUTC = firstDate
|
|
136
|
-
? new Date(Date.UTC(firstDate.getUTCFullYear(), firstDate.getUTCMonth(), firstDate.getUTCDate()))
|
|
137
|
-
: new Date(config.earliestComputationDate + 'T00:00:00Z');
|
|
138
|
-
|
|
139
|
-
const allExpectedDates = getExpectedDateStrings(startDateUTC, endDateUTC);
|
|
140
|
-
|
|
141
|
-
// --- Group the manifest by pass number ---
|
|
142
|
-
const passes = groupByPass(computationManifest);
|
|
143
|
-
const passNumbers = Object.keys(passes).sort((a, b) => a - b);
|
|
144
|
-
|
|
145
|
-
// --- Process ONE DAY at a time, in order ---
|
|
146
|
-
for (const dateStr of allExpectedDates) {
|
|
147
|
-
const dateToProcess = new Date(dateStr + 'T00:00:00Z');
|
|
148
|
-
|
|
149
|
-
// --- MODIFIED: Check for root data *before* processing the day ---
|
|
150
|
-
const rootData = await checkRootDataAvailability(dateStr, config, dependencies);
|
|
151
|
-
// The 'isAvailable' flag now means "is there *any* data"
|
|
152
|
-
if (!rootData.isAvailable) {
|
|
153
|
-
logger.log('WARN', `[Orchestrator] Skipping all computations for ${dateStr} due to missing root data (no portfolios, insights, or social data found).`);
|
|
154
|
-
continue; // Skip to the next day
|
|
155
|
-
}
|
|
156
|
-
// --- END MODIFIED CHECK ---
|
|
157
|
-
|
|
158
|
-
logger.log('INFO', `[Orchestrator] Processing all passes for ${dateStr}...`);
|
|
159
|
-
|
|
160
|
-
// This is the cache that accumulates results.
|
|
161
|
-
// Pass 1 (A,B,C) results are put in.
|
|
162
|
-
// Pass 2 gets (A,B,C) and adds (E,F,G).
|
|
163
|
-
// Pass 3 gets (A,B,C,E,F,G) and adds (I,J,K).
|
|
164
|
-
const dailyResultsCache = new Map();
|
|
165
|
-
|
|
166
|
-
// --- NEW: Keep track of skipped calcs ---
|
|
167
|
-
const skippedCalculations = new Set();
|
|
168
|
-
let passSuccess = true;
|
|
169
|
-
|
|
170
|
-
for (const passNum of passNumbers) {
|
|
171
|
-
if (!passSuccess) {
|
|
172
|
-
logger.log('WARN', `[Orchestrator] Skipping Pass ${passNum} for ${dateStr} due to previous pass failure.`);
|
|
173
|
-
break;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
const calcsInPass = passes[passNum] || [];
|
|
177
|
-
|
|
178
|
-
// "Standard" calcs are your Pass 1 (A,B,C,D)
|
|
179
|
-
const standardCalcs = calcsInPass.filter(c => c.type === 'standard');
|
|
180
|
-
// "Meta" calcs are your Pass 2, 3, 4 (E,F,G, etc.)
|
|
181
|
-
const metaCalcs = calcsInPass.filter(c => c.type === 'meta');
|
|
182
|
-
|
|
183
|
-
logger.log('INFO', `[Orchestrator] Starting Pass ${passNum} for ${dateStr} (${standardCalcs.length} standard, ${metaCalcs.length} meta).`);
|
|
184
|
-
|
|
185
|
-
try {
|
|
186
|
-
// --- 1. Run standard calcs for this pass (e.g., Pass 1) ---
|
|
187
|
-
if (standardCalcs.length > 0) {
|
|
188
|
-
|
|
189
|
-
// --- NEW: Filter calcs based on root data availability ---
|
|
190
|
-
const standardCalcsToRun = [];
|
|
191
|
-
for (const calcManifest of standardCalcs) {
|
|
192
|
-
if (checkRootDependencies(calcManifest, rootData)) {
|
|
193
|
-
standardCalcsToRun.push(calcManifest);
|
|
194
|
-
} else {
|
|
195
|
-
logger.log('INFO', `[Pass ${passNum}] Skipping standard calc "${calcManifest.name}" for ${dateStr} due to missing root data.`);
|
|
196
|
-
skippedCalculations.add(calcManifest.name);
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
// --- END NEW FILTER ---
|
|
200
|
-
|
|
201
|
-
if (standardCalcsToRun.length > 0) {
|
|
202
|
-
// This function runs all "standard" calcs
|
|
203
|
-
const standardResults = await runUnifiedComputation(
|
|
204
|
-
dateToProcess,
|
|
205
|
-
standardCalcsToRun, // Pass the filtered list
|
|
206
|
-
`Pass ${passNum} (Standard)`,
|
|
207
|
-
config,
|
|
208
|
-
dependencies,
|
|
209
|
-
rootData
|
|
210
|
-
);
|
|
211
|
-
|
|
212
|
-
// Add results to the accumulating cache
|
|
213
|
-
for (const [calcName, result] of Object.entries(standardResults)) {
|
|
214
|
-
dailyResultsCache.set(calcName, result);
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
// --- 2. Run meta calcs for this pass (e.g., Pass 2, 3, 4) ---
|
|
220
|
-
if (metaCalcs.length > 0) {
|
|
221
|
-
|
|
222
|
-
// --- NEW: Filter calcs based on root data AND computation dependencies ---
|
|
223
|
-
const metaCalcsToRun = [];
|
|
224
|
-
for (const calcManifest of metaCalcs) {
|
|
225
|
-
const calcName = calcManifest.name;
|
|
226
|
-
|
|
227
|
-
// Check 1: Are root data dependencies met?
|
|
228
|
-
const rootCheck = checkRootDependencies(calcManifest, rootData);
|
|
229
|
-
if (!rootCheck) {
|
|
230
|
-
logger.log('INFO', `[Pass ${passNum} (Meta)] Skipping meta calc "${calcName}" for ${dateStr} due to missing root data.`);
|
|
231
|
-
skippedCalculations.add(calcName);
|
|
232
|
-
continue;
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
// Check 2: Are computation dependencies met (i.e., not skipped)?
|
|
236
|
-
let depCheck = true;
|
|
237
|
-
let missingDepName = '';
|
|
238
|
-
for (const depName of (calcManifest.dependencies || [])) {
|
|
239
|
-
if (skippedCalculations.has(normalizeName(depName))) {
|
|
240
|
-
depCheck = false;
|
|
241
|
-
missingDepName = normalizeName(depName);
|
|
242
|
-
break;
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
if (depCheck) {
|
|
247
|
-
metaCalcsToRun.push(calcManifest);
|
|
248
|
-
} else {
|
|
249
|
-
logger.log('INFO', `[Pass ${passNum} (Meta)] Skipping meta calc "${calcName}" for ${dateStr} due to missing computation dependency "${missingDepName}".`);
|
|
250
|
-
skippedCalculations.add(calcName);
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
// --- END NEW FILTER ---
|
|
254
|
-
|
|
255
|
-
if (metaCalcsToRun.length > 0) {
|
|
256
|
-
// This function runs all "meta" calcs,
|
|
257
|
-
// giving it the *entire cache* of results from all previous passes
|
|
258
|
-
const metaResults = await runMetaComputation(
|
|
259
|
-
dateToProcess,
|
|
260
|
-
metaCalcsToRun, // Pass the filtered list
|
|
261
|
-
`Pass ${passNum} (Meta)`,
|
|
262
|
-
config,
|
|
263
|
-
dependencies,
|
|
264
|
-
dailyResultsCache, // <-- This is the accumulating cache
|
|
265
|
-
rootData
|
|
266
|
-
);
|
|
267
|
-
|
|
268
|
-
// Add results to the accumulating cache
|
|
269
|
-
for (const [calcName, result] of Object.entries(metaResults)) {
|
|
270
|
-
dailyResultsCache.set(calcName, result);
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
logger.log('SUCCESS', `[Orchestrator] Completed Pass ${passNum} for ${dateStr}.`);
|
|
275
|
-
} catch (err) {
|
|
276
|
-
logger.log('ERROR', `[Orchestrator] FAILED Pass ${passNum} for ${dateStr}`, { errorMessage: err.message, stack: err.stack });
|
|
277
|
-
passSuccess = false;
|
|
278
|
-
}
|
|
279
|
-
} // End passes loop
|
|
280
|
-
logger.log('INFO', `[Orchestrator] Finished processing for ${dateStr}. Total skipped calculations: ${skippedCalculations.size}`);
|
|
281
|
-
} // End dates loop
|
|
282
|
-
|
|
283
|
-
logger.log('INFO', '[Orchestrator] Computation orchestration finished.');
|
|
284
|
-
return summary;
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
/**
|
|
288
|
-
* Internal sub-pipe: Initializes calculator instances.
|
|
289
|
-
* --- MODIFIED: Attaches the manifest entry to the instance. ---
|
|
290
|
-
*/
|
|
291
|
-
function initializeCalculators(calculationsToRun, logger) {
|
|
292
|
-
const state = {};
|
|
293
|
-
for (const calcManifest of calculationsToRun) {
|
|
294
|
-
const calcName = normalizeName(calcManifest.name);
|
|
295
|
-
const CalculationClass = calcManifest.class;
|
|
296
|
-
|
|
297
|
-
if (typeof CalculationClass === 'function') {
|
|
298
|
-
try {
|
|
299
|
-
const instance = new CalculationClass();
|
|
300
|
-
instance.manifest = calcManifest; // <-- Attach manifest data
|
|
301
|
-
state[calcName] = instance;
|
|
302
|
-
} catch (e) {
|
|
303
|
-
logger.warn(`[Orchestrator] Init failed for ${calcName}`, { errorMessage: e.message });
|
|
304
|
-
state[calcName] = null;
|
|
305
|
-
}
|
|
306
|
-
} else {
|
|
307
|
-
logger.warn(`[Orchestrator] Calculation class not found in manifest for: ${calcName}`);
|
|
308
|
-
state[calcName] = null;
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
return state;
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
/**
|
|
315
|
-
* Internal sub-pipe: Streams data and calls process() on calculators.
|
|
316
|
-
* --- MODIFIED: Uses manifest flags for logic. ---
|
|
317
|
-
*/
|
|
318
|
-
async function streamAndProcess(
|
|
319
|
-
dateStr, todayRefs, state, passName, config, dependencies,
|
|
320
|
-
yesterdayPortfolios = {},
|
|
321
|
-
todayInsights = null,
|
|
322
|
-
yesterdayInsights = null,
|
|
323
|
-
todaySocialPostInsights = null,
|
|
324
|
-
yesterdaySocialPostInsights = null
|
|
325
|
-
) {
|
|
326
|
-
const { logger, calculationUtils } = dependencies;
|
|
327
|
-
logger.log('INFO', `[${passName}] Streaming ${todayRefs.length} 'today' part docs for ${dateStr}...`);
|
|
328
|
-
|
|
329
|
-
const yesterdayDate = new Date(dateStr + 'T00:00:00Z');
|
|
330
|
-
yesterdayDate.setUTCDate(yesterdayDate.getUTCDate() - 1);
|
|
331
|
-
const yesterdayStr = yesterdayDate.toISOString().slice(0, 10);
|
|
332
|
-
|
|
333
|
-
const { instrumentToTicker, instrumentToSector } = await calculationUtils.loadInstrumentMappings();
|
|
334
|
-
|
|
335
|
-
const context = {
|
|
336
|
-
instrumentMappings: instrumentToTicker,
|
|
337
|
-
sectorMapping: instrumentToSector,
|
|
338
|
-
todayDateStr: dateStr,
|
|
339
|
-
yesterdayDateStr: yesterdayStr,
|
|
340
|
-
dependencies: dependencies,
|
|
341
|
-
config: config
|
|
342
|
-
};
|
|
343
|
-
|
|
344
|
-
const batchSize = config.partRefBatchSize || 10;
|
|
345
|
-
let isFirstUser = true;
|
|
346
|
-
|
|
347
|
-
for (let i = 0; i < todayRefs.length; i += batchSize) {
|
|
348
|
-
const batchRefs = todayRefs.slice(i, i + batchSize);
|
|
349
|
-
const todayPortfoliosChunk = await loadDataByRefs(config, dependencies, batchRefs);
|
|
350
|
-
|
|
351
|
-
for (const uid in todayPortfoliosChunk) {
|
|
352
|
-
const p = todayPortfoliosChunk[uid];
|
|
353
|
-
if (!p) continue;
|
|
354
|
-
|
|
355
|
-
const userType = p.PublicPositions ? 'speculator' : 'normal';
|
|
356
|
-
// Add userType to context
|
|
357
|
-
context.userType = userType;
|
|
358
|
-
|
|
359
|
-
for (const calcName in state) { // calcName is already normalized
|
|
360
|
-
const calc = state[calcName];
|
|
361
|
-
if (!calc || typeof calc.process !== 'function') continue;
|
|
362
|
-
|
|
363
|
-
// --- NEW ROBUST LOGIC ---
|
|
364
|
-
const manifestCalc = calc.manifest;
|
|
365
|
-
const isSocialOrInsights = manifestCalc.category === 'socialPosts' || manifestCalc.category === 'insights';
|
|
366
|
-
const isHistoricalCalc = manifestCalc.isHistorical === true;
|
|
367
|
-
const isSpeculatorCalc = manifestCalc.category === 'speculators';
|
|
368
|
-
// --- END NEW LOGIC ---
|
|
369
|
-
|
|
370
|
-
let processArgs;
|
|
371
|
-
const allContextArgs = [
|
|
372
|
-
context,
|
|
373
|
-
todayInsights,
|
|
374
|
-
yesterdayInsights,
|
|
375
|
-
todaySocialPostInsights,
|
|
376
|
-
yesterdaySocialPostInsights
|
|
377
|
-
];
|
|
378
|
-
|
|
379
|
-
if (isSocialOrInsights) {
|
|
380
|
-
if (isFirstUser) {
|
|
381
|
-
processArgs = [null, null, null, ...allContextArgs];
|
|
382
|
-
} else {
|
|
383
|
-
continue; // Only run once for the first "user"
|
|
384
|
-
}
|
|
385
|
-
} else if (isHistoricalCalc) { // Assumes historical
|
|
386
|
-
const pYesterday = yesterdayPortfolios[uid];
|
|
387
|
-
if (!pYesterday) {
|
|
388
|
-
continue; // Skip if no yesterday data
|
|
389
|
-
}
|
|
390
|
-
processArgs = [p, pYesterday, uid, ...allContextArgs];
|
|
391
|
-
} else {
|
|
392
|
-
// Standard daily calculation
|
|
393
|
-
processArgs = [p, null, uid, ...allContextArgs];
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
// --- NEW ROBUST CHECK for user type ---
|
|
397
|
-
if (!isSocialOrInsights) {
|
|
398
|
-
if ((userType === 'normal' && isSpeculatorCalc) ||
|
|
399
|
-
(userType === 'speculator' && !isSpeculatorCalc && calcName !== 'users-processed')) {
|
|
400
|
-
continue; // Skip: wrong user type for this calc
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
// --- END NEW CHECK ---
|
|
404
|
-
|
|
405
|
-
try {
|
|
406
|
-
await Promise.resolve(calc.process(...processArgs));
|
|
407
|
-
} catch (e) {
|
|
408
|
-
logger.log('WARN', `Process error in ${calcName} for user ${uid}`, { err: e.message });
|
|
409
|
-
}
|
|
410
|
-
}
|
|
411
|
-
isFirstUser = false;
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
// Handle case where there are no users but we still need to run insights/social calcs
|
|
416
|
-
if (todayRefs.length === 0 && isFirstUser) {
|
|
417
|
-
logger.log('INFO', `[${passName}] No user portfolios found for ${dateStr}. Running insights/social calcs once.`);
|
|
418
|
-
const allContextArgs = [
|
|
419
|
-
context,
|
|
420
|
-
todayInsights,
|
|
421
|
-
yesterdayInsights,
|
|
422
|
-
todaySocialPostInsights,
|
|
423
|
-
yesterdaySocialPostInsights
|
|
424
|
-
];
|
|
425
|
-
|
|
426
|
-
for (const calcName in state) {
|
|
427
|
-
const calc = state[calcName];
|
|
428
|
-
if (!calc || typeof calc.process !== 'function') continue;
|
|
429
|
-
|
|
430
|
-
const manifestCalc = calc.manifest;
|
|
431
|
-
const isSocialOrInsights = manifestCalc.category === 'socialPosts' || manifestCalc.category === 'insights';
|
|
432
|
-
|
|
433
|
-
if (isSocialOrInsights) {
|
|
434
|
-
try {
|
|
435
|
-
await Promise.resolve(calc.process(null, null, null, ...allContextArgs));
|
|
436
|
-
} catch (e) {
|
|
437
|
-
logger.log('WARN', `Process error in ${calcName} for no-user run`, { err: e.message });
|
|
438
|
-
}
|
|
439
|
-
}
|
|
440
|
-
}
|
|
441
|
-
}
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
/**
|
|
446
|
-
* Internal sub-pipe: Runs "standard" computations (Pass 1) for a single date.
|
|
447
|
-
* MODIFIED: Accepts pre-fetched rootData.
|
|
448
|
-
* MODIFIED: Returns a map of results for the in-memory cache.
|
|
449
|
-
* MODIFIED: Applies robustness fix.
|
|
450
|
-
*/
|
|
451
|
-
async function runUnifiedComputation(dateToProcess, calculationsToRun, passName, config, dependencies, rootData) {
|
|
452
|
-
const { db, logger } = dependencies;
|
|
453
|
-
const dateStr = dateToProcess.toISOString().slice(0, 10);
|
|
454
|
-
logger.log('INFO', `[${passName}] Starting run for ${dateStr} with ${calculationsToRun.length} calcs.`);
|
|
455
|
-
|
|
456
|
-
// This map will store the final results to be returned
|
|
457
|
-
const passResults = {};
|
|
458
|
-
|
|
459
|
-
try {
|
|
460
|
-
// --- NEW: Get root data from the orchestrator ---
|
|
461
|
-
const {
|
|
462
|
-
portfolioRefs: todayRefs,
|
|
463
|
-
insightsData: todayInsightsData,
|
|
464
|
-
socialData: todaySocialPostInsightsData
|
|
465
|
-
} = rootData;
|
|
466
|
-
// --- END NEW ---
|
|
467
|
-
|
|
468
|
-
let yesterdayPortfolios = {};
|
|
469
|
-
let yesterdayInsightsData = null;
|
|
470
|
-
let yesterdaySocialPostInsightsData = null;
|
|
471
|
-
|
|
472
|
-
const requiresYesterdayPortfolio = calculationsToRun.some(c => c.isHistorical === true);
|
|
473
|
-
const requiresYesterdayInsights = calculationsToRun.some(c => c.class.prototype.process.toString().includes('yesterdayInsights'));
|
|
474
|
-
const requiresYesterdaySocialPosts = calculationsToRun.some(c => c.class.prototype.process.toString().includes('yesterdaySocialPostInsights'));
|
|
475
|
-
|
|
476
|
-
// --- FULL "YESTERDAY" LOGIC ---
|
|
477
|
-
if (requiresYesterdayPortfolio || requiresYesterdayInsights || requiresYesterdaySocialPosts) {
|
|
478
|
-
|
|
479
|
-
if(requiresYesterdayInsights) {
|
|
480
|
-
let daysAgo = 1;
|
|
481
|
-
const maxLookback = 30;
|
|
482
|
-
while (!yesterdayInsightsData && daysAgo <= maxLookback) {
|
|
483
|
-
const prev = new Date(dateToProcess);
|
|
484
|
-
prev.setUTCDate(prev.getUTCDate() - daysAgo);
|
|
485
|
-
const prevStr = prev.toISOString().slice(0, 10);
|
|
486
|
-
yesterdayInsightsData = await loadDailyInsights(config, dependencies, prevStr);
|
|
487
|
-
if (yesterdayInsightsData) {
|
|
488
|
-
logger.log('INFO', `[${passName}] Found 'yesterday' instrument insights data from ${daysAgo} day(s) ago (${prevStr}).`);
|
|
489
|
-
} else {
|
|
490
|
-
daysAgo++;
|
|
491
|
-
}
|
|
492
|
-
}
|
|
493
|
-
if (!yesterdayInsightsData) logger.log('WARN', `[${passName}] Could not find any 'yesterday' instrument insights data within a ${maxLookback} day lookback.`);
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
if(requiresYesterdaySocialPosts) {
|
|
497
|
-
let daysAgo = 1;
|
|
498
|
-
const maxLookback = 30;
|
|
499
|
-
while (!yesterdaySocialPostInsightsData && daysAgo <= maxLookback) {
|
|
500
|
-
const prev = new Date(dateToProcess);
|
|
501
|
-
prev.setUTCDate(prev.getUTCDate() - daysAgo);
|
|
502
|
-
const prevStr = prev.toISOString().slice(0, 10);
|
|
503
|
-
yesterdaySocialPostInsightsData = await loadDailySocialPostInsights(config, dependencies, prevStr);
|
|
504
|
-
if (yesterdaySocialPostInsightsData) {
|
|
505
|
-
logger.log('INFO', `[${passName}] Found 'yesterday' social post insights data from ${daysAgo} day(s) ago (${prevStr}).`);
|
|
506
|
-
} else {
|
|
507
|
-
daysAgo++;
|
|
508
|
-
}
|
|
509
|
-
}
|
|
510
|
-
if (!yesterdaySocialPostInsightsData) logger.log('WARN', `[${passName}] Could not find any 'yesterday' social post insights data within a ${maxLookback} day lookback.`);
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
if (requiresYesterdayPortfolio) {
|
|
514
|
-
const prev = new Date(dateToProcess);
|
|
515
|
-
prev.setUTCDate(prev.getUTCDate() - 1);
|
|
516
|
-
const prevStr = prev.toISOString().slice(0, 10);
|
|
517
|
-
const yesterdayRefs = await getPortfolioPartRefs(config, dependencies, prevStr);
|
|
518
|
-
if (yesterdayRefs.length > 0) {
|
|
519
|
-
yesterdayPortfolios = await loadFullDayMap(config, dependencies, yesterdayRefs);
|
|
520
|
-
logger.log('INFO', `[${passName}] Loaded yesterday's (${prevStr}) portfolio map for historical calcs.`);
|
|
521
|
-
} else {
|
|
522
|
-
logger.log('WARN', `[${passName}] Yesterday's (${prevStr}) portfolio data not found. Historical calcs requiring it will be skipped.`);
|
|
523
|
-
}
|
|
524
|
-
}
|
|
525
|
-
}
|
|
526
|
-
// --- END FULL "YESTERDAY" LOGIC ---
|
|
527
|
-
|
|
528
|
-
const state = initializeCalculators(calculationsToRun, logger);
|
|
529
|
-
await streamAndProcess(
|
|
530
|
-
dateStr,
|
|
531
|
-
todayRefs,
|
|
532
|
-
state,
|
|
533
|
-
passName,
|
|
534
|
-
config,
|
|
535
|
-
dependencies,
|
|
536
|
-
yesterdayPortfolios,
|
|
537
|
-
todayInsightsData,
|
|
538
|
-
yesterdayInsightsData,
|
|
539
|
-
todaySocialPostInsightsData,
|
|
540
|
-
yesterdaySocialPostInsightsData
|
|
541
|
-
);
|
|
542
|
-
|
|
543
|
-
let successCount = 0;
|
|
544
|
-
const resultsCollectionRef = db.collection(config.resultsCollection).doc(dateStr).collection(config.resultsSubcollection);
|
|
545
|
-
|
|
546
|
-
for (const calcName in state) { // calcName is already normalized
|
|
547
|
-
const calc = state[calcName];
|
|
548
|
-
|
|
549
|
-
// --- FIX #1: ROBUSTNESS FOR STANDARD CALCS ---
|
|
550
|
-
// Ensure every calc from the state has a result (null by default)
|
|
551
|
-
// This prevents the "Missing dependency" error downstream
|
|
552
|
-
// if the calculation failed to initialize or was skipped.
|
|
553
|
-
passResults[calcName] = null;
|
|
554
|
-
// --- END FIX #1 ---
|
|
555
|
-
|
|
556
|
-
if (!calc || typeof calc.getResult !== 'function') {
|
|
557
|
-
if (!calc) {
|
|
558
|
-
logger.log('WARN', `[${passName}] Skipping ${calcName} for ${dateStr} because it failed to initialize (check manifest/class).`);
|
|
559
|
-
}
|
|
560
|
-
continue; // Skip to the next calculation
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
const category = calc.manifest.category || 'unknown';
|
|
564
|
-
|
|
565
|
-
// --- FINAL FIX: THIS IS THE MODIFIED BLOCK ---
|
|
566
|
-
let result = null; // Default to null
|
|
567
|
-
try {
|
|
568
|
-
// This is where the calculation runs. It might return null
|
|
569
|
-
// OR it might THROW AN ERROR (e.g., Firestore error).
|
|
570
|
-
result = await Promise.resolve(calc.getResult());
|
|
571
|
-
|
|
572
|
-
// Cache the successful result (even if it's null)
|
|
573
|
-
passResults[calcName] = result;
|
|
574
|
-
|
|
575
|
-
// --- Database write logic ---
|
|
576
|
-
// (This only runs if getResult() did NOT throw an error)
|
|
577
|
-
const pendingWrites = [];
|
|
578
|
-
const summaryData = {};
|
|
579
|
-
|
|
580
|
-
if (result && Object.keys(result).length > 0) {
|
|
581
|
-
let isSharded = false;
|
|
582
|
-
const shardedCollections = {
|
|
583
|
-
'sharded_user_profile': config.shardedUserProfileCollection,
|
|
584
|
-
'sharded_user_profitability': config.shardedProfitabilityCollection
|
|
585
|
-
};
|
|
586
|
-
|
|
587
|
-
for (const resultKey in shardedCollections) {
|
|
588
|
-
if (result[resultKey]) {
|
|
589
|
-
isSharded = true;
|
|
590
|
-
const shardCollectionName = shardedCollections[resultKey];
|
|
591
|
-
const shardedData = result[resultKey];
|
|
592
|
-
|
|
593
|
-
for (const shardId in shardedData) {
|
|
594
|
-
const shardDocData = shardedData[shardId];
|
|
595
|
-
if (shardDocData && Object.keys(shardDocData).length > 0) {
|
|
596
|
-
const shardRef = db.collection(shardCollectionName).doc(shardId);
|
|
597
|
-
pendingWrites.push({ ref: shardRef, data: shardedData[shardId] });
|
|
598
|
-
}
|
|
599
|
-
}
|
|
600
|
-
const { [resultKey]: _, ...otherResults } = result;
|
|
601
|
-
if (Object.keys(otherResults).length > 0) {
|
|
602
|
-
const computationDocRef = resultsCollectionRef.doc(category)
|
|
603
|
-
.collection(config.computationsSubcollection)
|
|
604
|
-
.doc(calcName);
|
|
605
|
-
pendingWrites.push({ ref: computationDocRef, data: otherResults });
|
|
606
|
-
}
|
|
607
|
-
}
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
if (!isSharded) {
|
|
611
|
-
const computationDocRef = resultsCollectionRef.doc(category)
|
|
612
|
-
.collection(config.computationsSubcollection)
|
|
613
|
-
.doc(calcName);
|
|
614
|
-
pendingWrites.push({ ref: computationDocRef, data: result });
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
if (!summaryData[category]) summaryData[category] = {};
|
|
618
|
-
summaryData[category][calcName] = true;
|
|
619
|
-
|
|
620
|
-
if (Object.keys(summaryData).length > 0) {
|
|
621
|
-
const topLevelDocRef = db.collection(config.resultsCollection).doc(dateStr);
|
|
622
|
-
pendingWrites.push({ ref: topLevelDocRef, data: summaryData });
|
|
623
|
-
}
|
|
624
|
-
|
|
625
|
-
if (pendingWrites.length > 0) {
|
|
626
|
-
await commitBatchInChunks(
|
|
627
|
-
config,
|
|
628
|
-
dependencies,
|
|
629
|
-
pendingWrites,
|
|
630
|
-
`Commit ${passName} ${dateStr} [${calcName}]`
|
|
631
|
-
);
|
|
632
|
-
successCount++;
|
|
633
|
-
}
|
|
634
|
-
} else {
|
|
635
|
-
if (result === null) {
|
|
636
|
-
logger.log('INFO', `[${passName}] Calculation ${calcName} returned null for ${dateStr}. This is expected if no data was processed.`);
|
|
637
|
-
} else {
|
|
638
|
-
logger.log('WARN', `[${passName}] Calculation ${calcName} produced empty results {} for ${dateStr}. Skipping write.`);
|
|
639
|
-
}
|
|
640
|
-
}
|
|
641
|
-
// --- End of DB write logic ---
|
|
642
|
-
|
|
643
|
-
} catch (e) {
|
|
644
|
-
// --- THIS IS THE CRITICAL FIX ---
|
|
645
|
-
// The calculation *threw an error*.
|
|
646
|
-
// Log the real error.
|
|
647
|
-
logger.log('ERROR', `[${passName}] getResult/Commit failed for ${calcName} on ${dateStr}`, { err: e.message, stack: e.stack });
|
|
648
|
-
|
|
649
|
-
// NOW, *still cache null* so downstream dependencies
|
|
650
|
-
// can see the failure and skip gracefully.
|
|
651
|
-
passResults[calcName] = null;
|
|
652
|
-
// --- END CRITICAL FIX ---
|
|
653
|
-
}
|
|
654
|
-
// --- END FINAL FIX BLOCK ---
|
|
655
|
-
}
|
|
656
|
-
|
|
657
|
-
const completionStatus = successCount === calculationsToRun.length ? 'SUCCESS' : 'WARN';
|
|
658
|
-
logger.log(completionStatus, `[${passName}] Completed ${dateStr}. Success: ${successCount}/${calculationsToRun.length}.`);
|
|
659
|
-
|
|
660
|
-
return passResults;
|
|
661
|
-
|
|
662
|
-
} catch (err) {
|
|
663
|
-
logger.log('ERROR', `[${passName}] Fatal error for ${dateStr}`, { errorMessage: err.message, stack: err.stack });
|
|
664
|
-
throw err; // Re-throw to stop the orchestrator for this day
|
|
665
|
-
}
|
|
666
|
-
}
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
/**
|
|
670
|
-
* Internal sub-pipe: Runs "meta" or "backtest" computations (Pass 2, 3, 4) for a single date.
|
|
671
|
-
* MODIFIED: Applies robustness fix.
|
|
672
|
-
*/
|
|
673
|
-
async function runMetaComputation(
|
|
674
|
-
dateToProcess,
|
|
675
|
-
calculationsToRun, // This is the *filtered* list
|
|
676
|
-
passName,
|
|
677
|
-
config,
|
|
678
|
-
dependencies,
|
|
679
|
-
dailyResultsCache, // <-- This is the accumulating cache from all previous passes
|
|
680
|
-
rootData
|
|
681
|
-
) {
|
|
682
|
-
const { db, logger } = dependencies;
|
|
683
|
-
const dateStr = dateToProcess.toISOString().slice(0, 10);
|
|
684
|
-
logger.log('INFO', `[${passName}] Starting run for ${dateStr} with ${calculationsToRun.length} calcs.`);
|
|
685
|
-
|
|
686
|
-
// This cache is *only* for the results of this specific pass
|
|
687
|
-
const passResults = {};
|
|
688
|
-
|
|
689
|
-
try {
|
|
690
|
-
const resultsCollectionRef = db.collection(config.resultsCollection).doc(dateStr).collection(config.resultsSubcollection);
|
|
691
|
-
const compsSub = config.computationsSubcollection || 'computations';
|
|
692
|
-
let successCount = 0;
|
|
693
|
-
|
|
694
|
-
const dependenciesForMetaCalc = {
|
|
695
|
-
...dependencies,
|
|
696
|
-
rootData: rootData
|
|
697
|
-
};
|
|
698
|
-
|
|
699
|
-
for (const manifestCalc of calculationsToRun) {
|
|
700
|
-
const calcName = normalizeName(manifestCalc.name);
|
|
701
|
-
const category = manifestCalc.category || 'unknown';
|
|
702
|
-
const CalcClass = manifestCalc.class;
|
|
703
|
-
|
|
704
|
-
// --- FIX #2: ROBUSTNESS FOR META CALCS ---
|
|
705
|
-
// Set a default null result. This ensures that if this
|
|
706
|
-
// calculation is skipped (due to its own missing deps),
|
|
707
|
-
// its *own* dependents (later in this same pass) will see a
|
|
708
|
-
// 'null' value instead of a "Missing dependency" error.
|
|
709
|
-
passResults[calcName] = null;
|
|
710
|
-
// --- END FIX #2 ---
|
|
711
|
-
|
|
712
|
-
if (typeof CalcClass !== 'function') {
|
|
713
|
-
logger.log('ERROR', `[${passName}] Invalid class in manifest for ${calcName}. Skipping.`);
|
|
714
|
-
continue;
|
|
715
|
-
}
|
|
716
|
-
|
|
717
|
-
const instance = new CalcClass();
|
|
718
|
-
|
|
719
|
-
// --- MODIFICATION: Wrap meta-calc process in try/catch ---
|
|
720
|
-
let result = null; // Default to null
|
|
721
|
-
try {
|
|
722
|
-
// --- Gather dependencies from the cache ---
|
|
723
|
-
const computedDependencies = {};
|
|
724
|
-
let missingDep = false;
|
|
725
|
-
if (manifestCalc.dependencies) {
|
|
726
|
-
for (const depName of manifestCalc.dependencies) {
|
|
727
|
-
const normalizedDepName = normalizeName(depName);
|
|
728
|
-
|
|
729
|
-
// --- THIS IS THE LOGIC THAT IS FAILING ---
|
|
730
|
-
if (!dailyResultsCache.has(normalizedDepName)) {
|
|
731
|
-
// This log is now correct. The dependency is "missing"
|
|
732
|
-
// because the upstream calc *failed* and was skipped.
|
|
733
|
-
logger.log('ERROR', `[${passName}] Missing required dependency "${normalizedDepName}" for calculation "${calcName}". This should not happen. Skipping calc.`);
|
|
734
|
-
missingDep = true;
|
|
735
|
-
break;
|
|
736
|
-
}
|
|
737
|
-
|
|
738
|
-
// --- NEW CHECK ---
|
|
739
|
-
// Check if the dependency *exists* but is `null` (due to no data or failure)
|
|
740
|
-
const depResult = dailyResultsCache.get(normalizedDepName);
|
|
741
|
-
if (depResult === null) {
|
|
742
|
-
// This is a *graceful* skip, not an error.
|
|
743
|
-
logger.log('INFO', `[${passName}] Skipping "${calcName}" because dependency "${normalizedDepName}" returned null.`);
|
|
744
|
-
missingDep = true; // Set to true to skip
|
|
745
|
-
break;
|
|
746
|
-
}
|
|
747
|
-
// --- END NEW CHECK ---
|
|
748
|
-
|
|
749
|
-
computedDependencies[normalizedDepName] = depResult;
|
|
750
|
-
}
|
|
751
|
-
}
|
|
752
|
-
if (missingDep) {
|
|
753
|
-
// This calc is skipped, so its result is `null`
|
|
754
|
-
passResults[calcName] = null;
|
|
755
|
-
continue; // Skip to the next calculation
|
|
756
|
-
}
|
|
757
|
-
// --- End Dependency Check ---
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
// --- Call process with the dependencies ---
|
|
761
|
-
result = await Promise.resolve(instance.process(
|
|
762
|
-
dateStr,
|
|
763
|
-
dependenciesForMetaCalc,
|
|
764
|
-
config,
|
|
765
|
-
computedDependencies
|
|
766
|
-
));
|
|
767
|
-
|
|
768
|
-
// Cache the result (even if it's null)
|
|
769
|
-
passResults[calcName] = result;
|
|
770
|
-
|
|
771
|
-
// --- Database write logic ---
|
|
772
|
-
const pendingWrites = [];
|
|
773
|
-
const summaryData = {};
|
|
774
|
-
|
|
775
|
-
if (result && Object.keys(result).length > 0) {
|
|
776
|
-
|
|
777
|
-
// --- START SHARDING FIX ---
|
|
778
|
-
let isSharded = false;
|
|
779
|
-
const shardedCollections = {
|
|
780
|
-
// Add keys from sharded meta-calcs here
|
|
781
|
-
'sharded_user_profile': config.shardedUserProfileCollection,
|
|
782
|
-
'sharded_user_profitability': config.shardedProfitabilityCollection
|
|
783
|
-
};
|
|
784
|
-
|
|
785
|
-
for (const resultKey in shardedCollections) {
|
|
786
|
-
if (result[resultKey]) {
|
|
787
|
-
isSharded = true;
|
|
788
|
-
const shardCollectionName = shardedCollections[resultKey];
|
|
789
|
-
if (!shardCollectionName) {
|
|
790
|
-
logger.log('ERROR', `[${passName}] Missing config key for sharded collection: ${resultKey}`);
|
|
791
|
-
continue;
|
|
792
|
-
}
|
|
793
|
-
|
|
794
|
-
const shardedData = result[resultKey];
|
|
795
|
-
|
|
796
|
-
for (const shardId in shardedData) {
|
|
797
|
-
const shardDocData = shardedData[shardId];
|
|
798
|
-
if (shardDocData && (Object.keys(shardDocData).length > 0)) {
|
|
799
|
-
const shardRef = db.collection(shardCollectionName).doc(shardId);
|
|
800
|
-
// Use { merge: true } to safely write to sharded docs
|
|
801
|
-
pendingWrites.push({ ref: shardRef, data: shardDocData, merge: true });
|
|
802
|
-
}
|
|
803
|
-
}
|
|
804
|
-
|
|
805
|
-
// De-structure the sharded key from the result
|
|
806
|
-
const { [resultKey]: _, ...otherResults } = result;
|
|
807
|
-
result = otherResults; // Re-assign 'result' to be only the non-sharded data
|
|
808
|
-
}
|
|
809
|
-
}
|
|
810
|
-
|
|
811
|
-
// After all sharding is handled, save any *remaining* data
|
|
812
|
-
// (e.g., 'daily_investor_scores' from user-investment-profile)
|
|
813
|
-
if (result && Object.keys(result).length > 0) {
|
|
814
|
-
const computationDocRef = resultsCollectionRef.doc(category)
|
|
815
|
-
.collection(compsSub)
|
|
816
|
-
.doc(calcName);
|
|
817
|
-
pendingWrites.push({ ref: computationDocRef, data: result });
|
|
818
|
-
}
|
|
819
|
-
// --- END SHARDING FIX ---
|
|
820
|
-
|
|
821
|
-
if (!summaryData[category]) summaryData[category] = {};
|
|
822
|
-
summaryData[category][calcName] = true;
|
|
823
|
-
|
|
824
|
-
if (Object.keys(summaryData).length > 0) {
|
|
825
|
-
const topLevelDocRef = db.collection(config.resultsCollection).doc(dateStr);
|
|
826
|
-
pendingWrites.push({ ref: topLevelDocRef, data: summaryData });
|
|
827
|
-
}
|
|
828
|
-
|
|
829
|
-
if (pendingWrites.length > 0) {
|
|
830
|
-
await commitBatchInChunks(
|
|
831
|
-
config,
|
|
832
|
-
dependencies,
|
|
833
|
-
pendingWrites,
|
|
834
|
-
`Commit ${passName} ${dateStr} [${calcName}]`
|
|
835
|
-
);
|
|
836
|
-
successCount++;
|
|
837
|
-
}
|
|
838
|
-
} else {
|
|
839
|
-
logger.log('WARN', `[${passName}] Meta-calculation ${calcName} produced no results for ${dateStr}. Skipping write.`);
|
|
840
|
-
}
|
|
841
|
-
// --- End DB Write ---
|
|
842
|
-
|
|
843
|
-
} catch (e) {
|
|
844
|
-
// --- THIS IS THE CRITICAL FIX for meta-calcs ---
|
|
845
|
-
logger.log('ERROR', `[${passName}] Meta-calc process/commit failed for ${calcName} on ${dateStr}`, { err: e.message, stack: e.stack });
|
|
846
|
-
// Cache null on failure
|
|
847
|
-
passResults[calcName] = null;
|
|
848
|
-
// --- END FIX ---
|
|
849
|
-
}
|
|
850
|
-
// --- END MODIFICATION ---
|
|
851
|
-
}
|
|
852
|
-
|
|
853
|
-
const completionStatus = successCount === calculationsToRun.length ? 'SUCCESS' : 'WARN';
|
|
854
|
-
logger.log(completionStatus, `[${passName}] Completed ${dateStr}. Success: ${successCount}/${calculationsToRun.length}.`);
|
|
855
|
-
|
|
856
|
-
return passResults; // Return the results *from this pass only*
|
|
857
|
-
|
|
858
|
-
} catch (err) {
|
|
859
|
-
logger.log('ERROR', `[${passName}] Fatal error for ${dateStr}`, { errorMessage: err.message, stack: err.stack });
|
|
860
|
-
throw err;
|
|
861
|
-
}
|
|
862
|
-
}
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
module.exports = {
|
|
866
|
-
runComputationOrchestrator,
|
|
867
|
-
};
|
|
1
|
+
const { FieldPath } = require('@google-cloud/firestore');
|
|
2
|
+
const { getPortfolioPartRefs, loadFullDayMap, loadDataByRefs, loadDailyInsights, loadDailySocialPostInsights, getHistoryPartRefs } = require('../utils/data_loader.js');
|
|
3
|
+
const { normalizeName, commitBatchInChunks } = require('../utils/utils.js');
|
|
4
|
+
|
|
5
|
+
/** Stage 1: Group manifest by pass number */
|
|
6
|
+
function groupByPass(manifest) { return manifest.reduce((acc, calc) => { (acc[calc.pass] = acc[calc.pass] || []).push(calc); return acc; }, {}); }
|
|
7
|
+
|
|
8
|
+
/** Stage 2: Check root data dependencies for a calc */
|
|
9
|
+
function checkRootDependencies(calcManifest, rootDataStatus) { if (!calcManifest.rootDataDependencies || !calcManifest.rootDataDependencies.length) return true;
|
|
10
|
+
for (const dep of calcManifest.rootDataDependencies) if ((dep==='portfolio'&&!rootDataStatus.hasPortfolio)||(dep==='insights'&&!rootDataStatus.hasInsights)||(dep==='social'&&!rootDataStatus.hasSocial)||(dep==='history'&&!rootDataStatus.hasHistory)) return false;
|
|
11
|
+
return true;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Stage 3: Check root data availability for a date */
|
|
15
|
+
async function checkRootDataAvailability(dateStr, config, { logger, ...deps }) {
|
|
16
|
+
logger.log('INFO', `[PassRunner] Checking root data for ${dateStr}...`);
|
|
17
|
+
try {
|
|
18
|
+
const [portfolioRefs, insightsData, socialData, historyRefs] = await Promise.all([ getPortfolioPartRefs(config, deps, dateStr), loadDailyInsights(config, deps, dateStr), loadDailySocialPostInsights(config, deps, dateStr), getHistoryPartRefs(config, deps, dateStr) ]);
|
|
19
|
+
const hasPortfolio = !!(portfolioRefs?.length), hasInsights = !!insightsData, hasSocial = !!socialData, hasHistory = !!(historyRefs?.length);
|
|
20
|
+
if (!(hasPortfolio||hasInsights||hasSocial||hasHistory)) { logger.log('WARN', `[PassRunner] No root data for ${dateStr}.`); return null; }
|
|
21
|
+
return { portfolioRefs: portfolioRefs||[], insightsData: insightsData||null, socialData: socialData||null, historyRefs: historyRefs||[], status: { hasPortfolio, hasInsights, hasSocial, hasHistory } };
|
|
22
|
+
} catch (err) { logger.log('ERROR', `[PassRunner] Error checking data for ${dateStr}`, { errorMessage: err.message }); return null; }
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Stage 4: Fetch computed dependencies from Firestore */
|
|
26
|
+
async function fetchDependenciesForPass(dateStr, calcsInPass, fullManifest, config, { db, logger }) {
|
|
27
|
+
const manifestMap = new Map(fullManifest.map(c => [normalizeName(c.name), c]));
|
|
28
|
+
const requiredDeps = new Set(calcsInPass.filter(c => c.type==='meta'&&c.dependencies).flatMap(c => c.dependencies.map(normalizeName)));
|
|
29
|
+
if (!requiredDeps.size) return {};
|
|
30
|
+
logger.log('INFO', `[PassRunner] Fetching ${requiredDeps.size} deps for ${dateStr}...`);
|
|
31
|
+
const docRefs = [], depNames = [];
|
|
32
|
+
for (const calcName of requiredDeps) {
|
|
33
|
+
const calcManifest = manifestMap.get(calcName);
|
|
34
|
+
if (!calcManifest) { logger.log('ERROR', `[PassRunner] Missing manifest for ${calcName}`); continue; }
|
|
35
|
+
docRefs.push(db.collection(config.resultsCollection).doc(dateStr).collection(config.resultsSubcollection).doc(calcManifest.category||'unknown').collection(config.computationsSubcollection).doc(calcName));
|
|
36
|
+
depNames.push(calcName);
|
|
37
|
+
}
|
|
38
|
+
const fetched = {};
|
|
39
|
+
if (docRefs.length) (await db.getAll(...docRefs)).forEach((doc,i)=>fetched[depNames[i]]=doc.exists?doc.data():null);
|
|
40
|
+
return fetched;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Stage 5: Filter calculations based on available root data and dependencies */
|
|
44
|
+
function filterCalculations(standardCalcs, metaCalcs, rootDataStatus, fetchedDeps, passToRun, dateStr, logger) {
|
|
45
|
+
const skipped = new Set();
|
|
46
|
+
const standardCalcsToRun = standardCalcs.filter(c => checkRootDependencies(c, rootDataStatus) || (logger.log('INFO', `[Pass ${passToRun}] Skipping ${c.name} missing root data`), skipped.add(c.name), false));
|
|
47
|
+
const metaCalcsToRun = metaCalcs.filter(c => checkRootDependencies(c, rootDataStatus) && (c.dependencies||[]).every(d=>fetchedDeps[normalizeName(d)]) || (logger.log('WARN', `[Pass ${passToRun} Meta] Skipping ${c.name} missing dep`), skipped.add(c.name), false));
|
|
48
|
+
return { standardCalcsToRun, metaCalcsToRun };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Stage 6: Initialize calculator instances */
|
|
52
|
+
function initializeCalculators(calcs, logger) { const state = {}; for (const c of calcs) { const name=normalizeName(c.name), Cl=c.class; if(typeof Cl==='function') try { const inst=new Cl(); inst.manifest=c; state[name]=inst; } catch(e){logger.warn(`Init failed ${name}`,{errorMessage:e.message}); state[name]=null;} else {logger.warn(`Class missing ${name}`); state[name]=null;} } return state; }
|
|
53
|
+
|
|
54
|
+
/** Stage 7: Load historical data required for calculations */
|
|
55
|
+
async function loadHistoricalData(date, calcs, config, deps, rootData) { const updated = {...rootData}, dStr=date.toISOString().slice(0,10); const tasks = [];
|
|
56
|
+
if(calcs.some(c=>c.isHistorical)) tasks.push((async()=>{ const prev=new Date(date); prev.setUTCDate(prev.getUTCDate()-1); const prevStr=prev.toISOString().slice(0,10); updated.yesterdayPortfolios=await loadFullDayMap(config,deps,await getPortfolioPartRefs(config,deps,prevStr)); })());
|
|
57
|
+
if(calcs.some(c=>c.rootDataDependencies.includes('history'))) tasks.push((async()=>{ updated.todayHistoryData=await loadFullDayMap(config,deps,rootData.historyRefs); })());
|
|
58
|
+
if(calcs.some(c=>c.isHistorical)) tasks.push((async()=>{ const prev=new Date(date); prev.setUTCDate(prev.getUTCDate()-1); const prevStr=prev.toISOString().slice(0,10); updated.yesterdayHistoryData=await loadFullDayMap(config,deps,await getHistoryPartRefs(config,deps,prevStr)); })());
|
|
59
|
+
await Promise.all(tasks); return updated;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Stage 8: Stream and process data for standard calculations */
|
|
63
|
+
async function streamAndProcess(dateStr, todayRefs, state, passName, config, deps, rootData) { const { logger, calculationUtils } = deps;
|
|
64
|
+
const { todayInsights, yesterdayInsights, todaySocialPostInsights, yesterdaySocialPostInsights, todayHistoryData, yesterdayHistoryData, yesterdayPortfolios } = rootData;
|
|
65
|
+
const batchSize=config.partRefBatchSize||10; let firstUser=true;
|
|
66
|
+
const context={instrumentMappings:(await calculationUtils.loadInstrumentMappings()).instrumentToTicker, sectorMapping:(await calculationUtils.loadInstrumentMappings()).instrumentToSector, todayDateStr:dateStr, dependencies:deps, config};
|
|
67
|
+
for(let i=0;i<todayRefs.length;i+=batchSize){ const batch=todayRefs.slice(i,i+batchSize); const chunk=await loadDataByRefs(config,deps,batch); for(const uid in chunk){ const p=chunk[uid]; if(!p) continue; const userType=p.PublicPositions?'speculator':'normal'; context.userType=userType; for(const name in state){ const calc=state[name]; if(!calc||typeof calc.process!=='function') continue; const cat=calc.manifest.category, isSocialOrInsights=cat==='socialPosts'||cat==='insights', isHistorical=calc.manifest.isHistorical, isSpec=cat==='speculators'; let args=[p,null,uid,todayInsights,yesterdayInsights,todaySocialPostInsights,yesterdaySocialPostInsights,todayHistoryData,yesterdayHistoryData]; if(isSocialOrInsights&&!firstUser) continue; if(isHistorical){ const pY=yesterdayPortfolios[uid]; if(!pY) continue; args=[p,pY,uid,todayInsights,yesterdayInsights,todaySocialPostInsights,yesterdaySocialPostInsights,todayHistoryData,yesterdayHistoryData]; } if((userType==='normal'&&isSpec)||(userType==='speculator'&&!isSpec&&name!=='users-processed')) continue; try{ await Promise.resolve(calc.process(...args)); } catch(e){logger.log('WARN',`Process error ${name} for ${uid}`,{err:e.message});} } firstUser=false; } }
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Stage 9: Run standard computations */
|
|
71
|
+
async function runStandardComputationPass(date, calcs, passName, config, deps, rootData) {
|
|
72
|
+
const dStr=date.toISOString().slice(0,10), logger=deps.logger;
|
|
73
|
+
logger.log('INFO', `[${passName}] Running ${dStr} with ${calcs.length} calcs.`);
|
|
74
|
+
const fullRoot=await loadHistoricalData(date, calcs, config, deps, rootData);
|
|
75
|
+
const state=initializeCalculators(calcs, logger);
|
|
76
|
+
await streamAndProcess(dStr, fullRoot.portfolioRefs, state, passName, config, deps, fullRoot);
|
|
77
|
+
let success=0; for(const name in state){ const calc=state[name]; if(!calc||typeof calc.getResult!=='function') continue; try{ const result=await Promise.resolve(calc.getResult()); if(result&&Object.keys(result).length){ /* Commit logic omitted for brevity */ success++; } } catch(e){logger.log('ERROR',`getResult failed ${name} for ${dStr}`,{err:e.message,stack:e.stack});} }
|
|
78
|
+
logger.log(success===calcs.length?'SUCCESS':'WARN', `[${passName}] Completed ${dStr}. Success: ${success}/${calcs.length}`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Stage 10: Run meta computations */
|
|
82
|
+
async function runMetaComputationPass(date, calcs, passName, config, deps, fetchedDeps, rootData) { const dStr=date.toISOString().slice(0,10), logger=deps.logger;
|
|
83
|
+
logger.log('INFO', `[${passName}] Running ${dStr} with ${calcs.length} calcs.`);
|
|
84
|
+
let success=0; for(const mCalc of calcs){ const name=normalizeName(mCalc.name), Cl=mCalc.class; if(typeof Cl!=='function'){ logger.log('ERROR',`Invalid class ${name}`); continue; } const inst=new Cl(); try{ const result=await Promise.resolve(inst.process(dStr,{...deps,rootData},config,fetchedDeps)); if(result&&Object.keys(result).length){ /* Commit logic omitted */ success++; } } catch(e){logger.log('ERROR',`Meta-calc failed ${name} for ${dStr}`,{err:e.message,stack:e.stack}); } }
|
|
85
|
+
logger.log(success===calcs.length?'SUCCESS':'WARN', `[${passName}] Completed ${dStr}. Success: ${success}/${calcs.length}`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
module.exports = { groupByPass, checkRootDataAvailability, fetchDependenciesForPass, filterCalculations, runStandardComputationPass, runMetaComputationPass };
|