bulltrackers-module 1.0.174 → 1.0.175
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.
|
@@ -1,9 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* FIXED: computation_pass_runner.js
|
|
3
|
-
* V3:
|
|
3
|
+
* V3.4: Optimized Orchestration using Status Document (Single Source of Truth).
|
|
4
|
+
* - Reads one 'computation_status' doc per day to decide what to run.
|
|
5
|
+
* - Reduces Firestore reads significantly.
|
|
6
|
+
* - explicitly marks failures as false in the status doc.
|
|
4
7
|
*/
|
|
5
8
|
|
|
6
|
-
const {
|
|
9
|
+
const {
|
|
10
|
+
groupByPass,
|
|
11
|
+
checkRootDataAvailability,
|
|
12
|
+
fetchExistingResults,
|
|
13
|
+
fetchComputationStatus,
|
|
14
|
+
updateComputationStatus,
|
|
15
|
+
runStandardComputationPass,
|
|
16
|
+
runMetaComputationPass,
|
|
17
|
+
checkRootDependencies
|
|
18
|
+
} = require('./orchestration_helpers.js');
|
|
7
19
|
const { getExpectedDateStrings } = require('../utils/utils.js');
|
|
8
20
|
const PARALLEL_BATCH_SIZE = 7;
|
|
9
21
|
|
|
@@ -13,8 +25,9 @@ async function runComputationPass(config, dependencies, computationManifest) {
|
|
|
13
25
|
if (!passToRun)
|
|
14
26
|
return logger.log('ERROR', '[PassRunner] No pass defined. Aborting.');
|
|
15
27
|
|
|
16
|
-
logger.log('INFO', `🚀 Starting PASS ${passToRun}...`);
|
|
28
|
+
logger.log('INFO', `🚀 Starting PASS ${passToRun} with Optimized Status Check...`);
|
|
17
29
|
|
|
30
|
+
// Hardcoded earliest dates for global availability checks
|
|
18
31
|
const earliestDates = { portfolio: new Date('2025-09-25T00:00:00Z'), history: new Date('2025-11-05T00:00:00Z'), social: new Date('2025-10-30T00:00:00Z'), insights: new Date('2025-08-26T00:00:00Z') };
|
|
19
32
|
earliestDates.absoluteEarliest = Object.values(earliestDates).reduce((a,b) => a < b ? a : b);
|
|
20
33
|
|
|
@@ -24,79 +37,96 @@ async function runComputationPass(config, dependencies, computationManifest) {
|
|
|
24
37
|
if (!calcsInThisPass.length)
|
|
25
38
|
return logger.log('WARN', `[PassRunner] No calcs for Pass ${passToRun}. Exiting.`);
|
|
26
39
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
if (!deps.length)
|
|
33
|
-
{ calcEarliestDates.set(calc.name, earliestDates.absoluteEarliest); continue; }
|
|
34
|
-
|
|
35
|
-
const latestDep = new Date(Math.max(...deps.map(d => earliestDates[d]?.getTime() || 0)));
|
|
36
|
-
const calcDate = calc.isHistorical
|
|
37
|
-
? new Date(latestDep.getTime() + 86400000)
|
|
38
|
-
: latestDep;
|
|
39
|
-
|
|
40
|
-
calcEarliestDates.set(calc.name, calcDate);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
const passEarliestDate = new Date(Math.min(...Array.from(calcEarliestDates.values()).map(d => d.getTime())));
|
|
40
|
+
// Determine date range to process
|
|
41
|
+
// We no longer need complex per-calc date derivation since the status doc handles "done-ness".
|
|
42
|
+
// We just iterate from absolute earliest to yesterday.
|
|
43
|
+
const passEarliestDate = earliestDates.absoluteEarliest;
|
|
44
44
|
const endDateUTC = new Date(Date.UTC(new Date().getUTCFullYear(), new Date().getUTCMonth(), new Date().getUTCDate() - 1));
|
|
45
45
|
const allExpectedDates = getExpectedDateStrings(passEarliestDate, endDateUTC);
|
|
46
|
+
|
|
46
47
|
const standardCalcs = calcsInThisPass.filter(c => c.type === 'standard');
|
|
47
48
|
const metaCalcs = calcsInThisPass.filter(c => c.type === 'meta');
|
|
48
49
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
const earliest = calcEarliestDates.get(calc.name);
|
|
54
|
-
|
|
55
|
-
if (earliest && dateToProcess < earliest)
|
|
56
|
-
return false;
|
|
57
|
-
|
|
58
|
-
const missingRoot = (calc.rootDataDependencies || []).filter(dep => !rootData.status[`has${dep[0].toUpperCase() + dep.slice(1)}`]);
|
|
50
|
+
// Helper: Decide if a calculation should run based on the status document
|
|
51
|
+
const shouldRun = (calc, statusData) => {
|
|
52
|
+
// 1. If explicitly TRUE, it ran fine. Ignore.
|
|
53
|
+
if (statusData[calc.name] === true) return false;
|
|
59
54
|
|
|
60
|
-
|
|
61
|
-
|
|
55
|
+
// 2. If missing or FALSE, it needs to run.
|
|
56
|
+
// But first, check if its dependencies are ready (if any).
|
|
57
|
+
// Note: Dependencies from previous passes must be TRUE.
|
|
58
|
+
if (calc.dependencies && calc.dependencies.length > 0) {
|
|
59
|
+
const depsMet = calc.dependencies.every(depName => statusData[depName] === true);
|
|
60
|
+
if (!depsMet) return false; // Dependency not ready yet
|
|
61
|
+
}
|
|
62
62
|
|
|
63
|
-
|
|
64
|
-
{ const missingComputed = (calc.dependencies || []).filter(d => !existingResults[d]);
|
|
65
|
-
if (missingComputed.length)
|
|
66
|
-
return false; }
|
|
67
|
-
return true;
|
|
63
|
+
return true;
|
|
68
64
|
};
|
|
69
65
|
|
|
70
66
|
const processDate = async (dateStr) => {
|
|
67
|
+
// 1. Fetch the Single Status Document (Cheap Read)
|
|
68
|
+
const statusData = await fetchComputationStatus(dateStr, config, dependencies);
|
|
71
69
|
const dateToProcess = new Date(dateStr + 'T00:00:00Z');
|
|
70
|
+
|
|
71
|
+
// 2. Filter calculations based on Status Doc
|
|
72
|
+
const standardToRun = standardCalcs.filter(c => shouldRun(c, statusData));
|
|
73
|
+
const metaToRun = metaCalcs.filter(c => shouldRun(c, statusData));
|
|
74
|
+
|
|
75
|
+
// Optimization: If nothing needs to run, stop here.
|
|
76
|
+
// No checking root data, no loading results.
|
|
77
|
+
if (!standardToRun.length && !metaToRun.length) {
|
|
78
|
+
return; // logger.log('INFO', `[PassRunner] ${dateStr} complete or waiting for deps.`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// 3. Check Root Data Availability (Only done if we have work to do)
|
|
82
|
+
// We filter standardToRun further based on root data requirements
|
|
83
|
+
// (e.g. if calc needs history but history isn't ready for this date)
|
|
84
|
+
const rootData = await checkRootDataAvailability(dateStr, config, dependencies, earliestDates);
|
|
85
|
+
|
|
86
|
+
if (!rootData) {
|
|
87
|
+
// If root data is completely missing for the day, we can't run anything.
|
|
88
|
+
// We do NOT mark as false, because it's not a failure, just data unavailability.
|
|
89
|
+
// We just skip.
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Further filter based on specific root data needs (e.g. checks "hasHistory")
|
|
94
|
+
const finalStandardToRun = standardToRun.filter(c => checkRootDependencies(c, rootData.status).canRun);
|
|
95
|
+
const finalMetaToRun = metaToRun.filter(c => checkRootDependencies(c, rootData.status).canRun);
|
|
96
|
+
|
|
97
|
+
if (!finalStandardToRun.length && !finalMetaToRun.length) return;
|
|
98
|
+
|
|
99
|
+
logger.log('INFO', `[PassRunner] Running ${dateStr}: ${finalStandardToRun.length} standard, ${finalMetaToRun.length} meta`);
|
|
100
|
+
|
|
72
101
|
try {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
const existingResults = await fetchExistingResults(dateStr, calcsInThisPass, computationManifest, config, dependencies, false);
|
|
102
|
+
// 4. Fetch Data for Execution (Dependencies)
|
|
103
|
+
// We only fetch data required by the calcs that are actually running.
|
|
104
|
+
const calcsRunning = [...finalStandardToRun, ...finalMetaToRun];
|
|
105
|
+
const existingResults = await fetchExistingResults(dateStr, calcsRunning, computationManifest, config, dependencies, false);
|
|
78
106
|
|
|
79
|
-
//
|
|
107
|
+
// Fetch Previous Day's Results (for State Persistence)
|
|
80
108
|
const prevDate = new Date(dateToProcess); prevDate.setUTCDate(prevDate.getUTCDate() - 1);
|
|
81
109
|
const prevDateStr = prevDate.toISOString().slice(0, 10);
|
|
110
|
+
const previousResults = await fetchExistingResults(prevDateStr, calcsRunning, computationManifest, config, dependencies, true);
|
|
82
111
|
|
|
83
|
-
//
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
const standardToRun = standardCalcs.filter(c => checkDeps(c, rootData, existingResults, dateToProcess));
|
|
87
|
-
const metaToRun = metaCalcs.filter(c => checkDeps(c, rootData, existingResults, dateToProcess));
|
|
88
|
-
|
|
89
|
-
if (!standardToRun.length && !metaToRun.length) return logger.log('INFO', `[PassRunner] All calcs complete for ${dateStr}. Skipping.`);
|
|
112
|
+
// 5. Execute
|
|
113
|
+
if (finalStandardToRun.length)
|
|
114
|
+
await runStandardComputationPass(dateToProcess, finalStandardToRun, `Pass ${passToRun} (Standard)`, config, dependencies, rootData, existingResults, previousResults);
|
|
90
115
|
|
|
91
|
-
|
|
116
|
+
if (finalMetaToRun.length)
|
|
117
|
+
await runMetaComputationPass(dateToProcess, finalMetaToRun, `Pass ${passToRun} (Meta)`, config, dependencies, existingResults, previousResults, rootData);
|
|
92
118
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
if (metaToRun.length) await runMetaComputationPass(dateToProcess, metaToRun, `Pass ${passToRun} (Meta)`, config, dependencies, existingResults, previousResults, rootData);
|
|
119
|
+
} catch (err) {
|
|
120
|
+
logger.log('ERROR', `[PassRunner] FAILED Pass ${passToRun} for ${dateStr}`, { errorMessage: err.message });
|
|
96
121
|
|
|
97
|
-
|
|
122
|
+
// 6. Explicitly Mark Failures as FALSE
|
|
123
|
+
const failedUpdates = {};
|
|
124
|
+
[...finalStandardToRun, ...finalMetaToRun].forEach(c => failedUpdates[c.name] = false);
|
|
125
|
+
await updateComputationStatus(dateStr, failedUpdates, config, dependencies);
|
|
126
|
+
}
|
|
98
127
|
};
|
|
99
128
|
|
|
129
|
+
// Batch process dates
|
|
100
130
|
for (let i = 0; i < allExpectedDates.length; i += PARALLEL_BATCH_SIZE) {
|
|
101
131
|
const batch = allExpectedDates.slice(i, i + PARALLEL_BATCH_SIZE);
|
|
102
132
|
await Promise.all(batch.map(processDate));
|
|
@@ -104,4 +134,4 @@ async function runComputationPass(config, dependencies, computationManifest) {
|
|
|
104
134
|
logger.log('INFO', `[PassRunner] Pass ${passToRun} orchestration finished.`);
|
|
105
135
|
}
|
|
106
136
|
|
|
107
|
-
module.exports = { runComputationPass };
|
|
137
|
+
module.exports = { runComputationPass };
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* FIXED: orchestration_helpers.js
|
|
3
|
-
* V3.
|
|
4
|
-
* that require it, alongside Portfolio data.
|
|
3
|
+
* V3.3: Added Status Document logic (Single Source of Truth for Run Status).
|
|
5
4
|
*/
|
|
6
5
|
|
|
7
6
|
const { ComputationController } = require('../controllers/computation_controller');
|
|
@@ -64,7 +63,25 @@ async function checkRootDataAvailability(dateStr, config, dependencies, earliest
|
|
|
64
63
|
}
|
|
65
64
|
}
|
|
66
65
|
|
|
66
|
+
// --- NEW: Status Document Helpers ---
|
|
67
|
+
|
|
68
|
+
async function fetchComputationStatus(dateStr, config, { db }) {
|
|
69
|
+
const collection = config.computationStatusCollection || 'computation_status';
|
|
70
|
+
const docRef = db.collection(collection).doc(dateStr);
|
|
71
|
+
const snap = await docRef.get();
|
|
72
|
+
return snap.exists ? snap.data() : {};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function updateComputationStatus(dateStr, updates, config, { db }) {
|
|
76
|
+
if (!updates || Object.keys(updates).length === 0) return;
|
|
77
|
+
const collection = config.computationStatusCollection || 'computation_status';
|
|
78
|
+
const docRef = db.collection(collection).doc(dateStr);
|
|
79
|
+
// Merge the new statuses (true/false) into the daily tracking document
|
|
80
|
+
await docRef.set(updates, { merge: true });
|
|
81
|
+
}
|
|
82
|
+
|
|
67
83
|
// --- OPTIMIZED FETCH ---
|
|
84
|
+
// Now strictly used for fetching DATA (results), not for checking if something ran.
|
|
68
85
|
async function fetchExistingResults(dateStr, calcsInPass, fullManifest, config, { db }, includeSelf = false) {
|
|
69
86
|
const manifestMap = new Map(fullManifest.map(c => [normalizeName(c.name), c]));
|
|
70
87
|
const calcsToFetch = new Set();
|
|
@@ -105,18 +122,10 @@ async function fetchExistingResults(dateStr, calcsInPass, fullManifest, config,
|
|
|
105
122
|
return fetched;
|
|
106
123
|
}
|
|
107
124
|
|
|
108
|
-
function filterCalculations(standardCalcs, metaCalcs, rootDataStatus, existingResults, passToRun, dateStr, earliestDates) {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
(c.rootDataDependencies || []).forEach(d => { if(earliestDates[d] > earliest) earliest = earliestDates[d]; });
|
|
113
|
-
if (c.isHistorical) earliest.setUTCDate(earliest.getUTCDate() + 1);
|
|
114
|
-
if (new Date(dateStr) < earliest) return false;
|
|
115
|
-
if (!checkRootDependencies(c, rootDataStatus).canRun) return false;
|
|
116
|
-
if (c.type === 'meta' && c.dependencies && c.dependencies.some(d => !existingResults[normalizeName(d)])) return false;
|
|
117
|
-
return true;
|
|
118
|
-
};
|
|
119
|
-
return { standardCalcsToRun: standardCalcs.filter(filter), metaCalcsToRun: metaCalcs.filter(filter) };
|
|
125
|
+
function filterCalculations(standardCalcs, metaCalcs, rootDataStatus, existingResults, passToRun, dateStr, earliestDates) {
|
|
126
|
+
// DEPRECATED in favor of Status Document logic in Pass Runner.
|
|
127
|
+
// Kept for backward compatibility if needed, but effectively replaced.
|
|
128
|
+
return { standardCalcsToRun: standardCalcs, metaCalcsToRun: metaCalcs };
|
|
120
129
|
}
|
|
121
130
|
|
|
122
131
|
// --- EXECUTION DELEGATES ---
|
|
@@ -148,7 +157,7 @@ async function streamAndProcess(dateStr, state, passName, config, deps, rootData
|
|
|
148
157
|
? streamPortfolioData(config, deps, prevDateStr, rootData.yesterdayPortfolioRefs)
|
|
149
158
|
: null;
|
|
150
159
|
|
|
151
|
-
// 3. Today's History Stream
|
|
160
|
+
// 3. Today's History Stream
|
|
152
161
|
const needsTradingHistory = streamingCalcs.some(c => c.manifest.rootDataDependencies.includes('history'));
|
|
153
162
|
const tH_iter = (needsTradingHistory && historyRefs)
|
|
154
163
|
? streamHistoryData(config, deps, dateStr, historyRefs)
|
|
@@ -167,8 +176,8 @@ async function streamAndProcess(dateStr, state, passName, config, deps, rootData
|
|
|
167
176
|
calc.manifest,
|
|
168
177
|
dateStr,
|
|
169
178
|
tP_chunk,
|
|
170
|
-
yP_chunk,
|
|
171
|
-
tH_chunk,
|
|
179
|
+
yP_chunk,
|
|
180
|
+
tH_chunk,
|
|
172
181
|
fetchedDeps,
|
|
173
182
|
previousFetchedDeps
|
|
174
183
|
)
|
|
@@ -223,6 +232,8 @@ async function runMetaComputationPass(date, calcs, passName, config, deps, fetch
|
|
|
223
232
|
|
|
224
233
|
async function commitResults(stateObj, dStr, passName, config, deps) {
|
|
225
234
|
const writes = [], schemas = [], sharded = {};
|
|
235
|
+
const successUpdates = {}; // Track which calcs finished successfully
|
|
236
|
+
|
|
226
237
|
for (const name in stateObj) {
|
|
227
238
|
const calc = stateObj[name];
|
|
228
239
|
try {
|
|
@@ -230,7 +241,7 @@ async function commitResults(stateObj, dStr, passName, config, deps) {
|
|
|
230
241
|
if (!result) continue;
|
|
231
242
|
const standardRes = {};
|
|
232
243
|
for (const key in result) {
|
|
233
|
-
if (key.startsWith('sharded_')) {
|
|
244
|
+
if (key.startsWith('sharded_')) {
|
|
234
245
|
const sData = result[key];
|
|
235
246
|
for (const c in sData) { sharded[c] = sharded[c] || {}; Object.assign(sharded[c], sData[c]); }
|
|
236
247
|
} else standardRes[key] = result[key];
|
|
@@ -245,9 +256,7 @@ async function commitResults(stateObj, dStr, passName, config, deps) {
|
|
|
245
256
|
});
|
|
246
257
|
}
|
|
247
258
|
if (calc.manifest.class.getSchema) {
|
|
248
|
-
// FIX: Remove the 'class' property (function) because Firestore cannot store it. (We were literally submitting the entire JS class to firestore...)
|
|
249
259
|
const { class: _cls, ...safeMetadata } = calc.manifest;
|
|
250
|
-
|
|
251
260
|
schemas.push({
|
|
252
261
|
name,
|
|
253
262
|
category: calc.manifest.category,
|
|
@@ -255,12 +264,20 @@ async function commitResults(stateObj, dStr, passName, config, deps) {
|
|
|
255
264
|
metadata: safeMetadata
|
|
256
265
|
});
|
|
257
266
|
}
|
|
267
|
+
|
|
268
|
+
// Mark as successful in our local tracker
|
|
269
|
+
successUpdates[name] = true;
|
|
270
|
+
|
|
258
271
|
} catch (e) { deps.logger.log('ERROR', `Commit failed ${name}: ${e.message}`); }
|
|
259
272
|
}
|
|
260
273
|
|
|
274
|
+
// 1. Store Schemas
|
|
261
275
|
if (schemas.length) batchStoreSchemas(deps, config, schemas).catch(()=>{});
|
|
276
|
+
|
|
277
|
+
// 2. Write Results
|
|
262
278
|
if (writes.length) await commitBatchInChunks(config, deps, writes, `${passName} Results`);
|
|
263
279
|
|
|
280
|
+
// 3. Write Sharded Results
|
|
264
281
|
for (const col in sharded) {
|
|
265
282
|
const sWrites = [];
|
|
266
283
|
for (const id in sharded[col]) {
|
|
@@ -269,13 +286,21 @@ async function commitResults(stateObj, dStr, passName, config, deps) {
|
|
|
269
286
|
}
|
|
270
287
|
if (sWrites.length) await commitBatchInChunks(config, deps, sWrites, `${passName} Sharded ${col}`);
|
|
271
288
|
}
|
|
289
|
+
|
|
290
|
+
// 4. Update Status Document (Single Source of Truth)
|
|
291
|
+
if (Object.keys(successUpdates).length > 0) {
|
|
292
|
+
await updateComputationStatus(dStr, successUpdates, config, deps);
|
|
293
|
+
deps.logger.log('INFO', `[${passName}] Updated status document for ${Object.keys(successUpdates).length} computations.`);
|
|
294
|
+
}
|
|
272
295
|
}
|
|
273
296
|
|
|
274
297
|
module.exports = {
|
|
275
298
|
groupByPass,
|
|
299
|
+
checkRootDependencies,
|
|
276
300
|
checkRootDataAvailability,
|
|
277
301
|
fetchExistingResults,
|
|
278
|
-
|
|
302
|
+
fetchComputationStatus, // Exported for Pass Runner
|
|
303
|
+
updateComputationStatus, // Exported for Pass Runner (error handling)
|
|
279
304
|
runStandardComputationPass,
|
|
280
305
|
runMetaComputationPass
|
|
281
|
-
};
|
|
306
|
+
};
|