bulltrackers-module 1.0.104 → 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.
- package/README.MD +222 -222
- package/functions/appscript-api/helpers/errors.js +19 -19
- package/functions/appscript-api/index.js +58 -58
- package/functions/computation-system/helpers/orchestration_helpers.js +647 -113
- package/functions/computation-system/utils/data_loader.js +191 -191
- package/functions/computation-system/utils/utils.js +149 -254
- package/functions/core/utils/firestore_utils.js +433 -433
- package/functions/core/utils/pubsub_utils.js +53 -53
- package/functions/dispatcher/helpers/dispatch_helpers.js +47 -47
- package/functions/dispatcher/index.js +52 -52
- package/functions/etoro-price-fetcher/helpers/handler_helpers.js +124 -124
- package/functions/fetch-insights/helpers/handler_helpers.js +91 -91
- package/functions/generic-api/helpers/api_helpers.js +379 -379
- package/functions/generic-api/index.js +150 -150
- package/functions/invalid-speculator-handler/helpers/handler_helpers.js +75 -75
- package/functions/orchestrator/helpers/discovery_helpers.js +226 -226
- package/functions/orchestrator/helpers/update_helpers.js +92 -92
- package/functions/orchestrator/index.js +147 -147
- package/functions/price-backfill/helpers/handler_helpers.js +116 -123
- package/functions/social-orchestrator/helpers/orchestrator_helpers.js +61 -61
- package/functions/social-task-handler/helpers/handler_helpers.js +288 -288
- package/functions/task-engine/handler_creator.js +78 -78
- package/functions/task-engine/helpers/discover_helpers.js +125 -125
- package/functions/task-engine/helpers/update_helpers.js +118 -118
- package/functions/task-engine/helpers/verify_helpers.js +162 -162
- package/functions/task-engine/utils/firestore_batch_manager.js +258 -258
- package/index.js +105 -113
- package/package.json +45 -45
- package/functions/computation-system/computation_dependencies.json +0 -120
- package/functions/computation-system/helpers/worker_helpers.js +0 -340
- package/functions/computation-system/utils/computation_state_manager.js +0 -178
- package/functions/computation-system/utils/dependency_graph.js +0 -191
- 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
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
const {
|
|
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
|
-
const
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
};
|