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