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: Fetches Previous Day's Results to enable State Persistence.
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 { groupByPass, checkRootDataAvailability, fetchExistingResults, runStandardComputationPass, runMetaComputationPass } = require('./orchestration_helpers.js');
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
- const calcEarliestDates = new Map();
28
-
29
- for (const calc of calcsInThisPass) {
30
- const deps = calc.rootDataDependencies || [];
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
- const checkDeps = (calc, rootData, existingResults, dateToProcess) => {
50
- if (existingResults[calc.name])
51
- return false;
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
- if (missingRoot.length)
61
- return false;
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
- if (calc.type === 'meta')
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
- const rootData = await checkRootDataAvailability(dateStr, config, dependencies, earliestDates);
74
- if (!rootData) return logger.log('WARN', `[PassRunner] Skipping ${dateStr}: No root data.`);
75
-
76
- // 1. Fetch TODAY'S results (Check what is already done)
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
- // 2. Fetch YESTERDAY'S results (For State Persistence) We calculate T-1 date string
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
- // We pass 'true' to includeSelf, because we need the calc's OWN history (e.g. mimetic-latency needs previous mimetic-latency)
84
- const previousResults = await fetchExistingResults(prevDateStr, calcsInThisPass, computationManifest, config, dependencies, true);
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
- logger.log('INFO', `[PassRunner] Running ${dateStr}: ${standardToRun.length} standard, ${metaToRun.length} meta`);
116
+ if (finalMetaToRun.length)
117
+ await runMetaComputationPass(dateToProcess, finalMetaToRun, `Pass ${passToRun} (Meta)`, config, dependencies, existingResults, previousResults, rootData);
92
118
 
93
- // Pass 'previousResults' to the runners
94
- if (standardToRun.length) await runStandardComputationPass(dateToProcess, standardToRun, `Pass ${passToRun} (Standard)`, config, dependencies, rootData, existingResults, previousResults);
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
- } catch (err) { logger.log('ERROR', `[PassRunner] FAILED Pass ${passToRun} for ${dateStr}`, { errorMessage: err.message, stack: err.stack }); }
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.2: Enabled streaming of Trading History data (tH_iter) for computations
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) { // TODO passtorun is unused, why?
109
- const filter = (c) => { // Since we are using toplogical sorting for deps, surely the pass being run is implicit
110
- if (existingResults[c.name]) return false; // Am a bit confused here. Feel this may be a bug.
111
- let earliest = new Date('1970-01-01');
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 (NEW)
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, // Yesterday Portfolio
171
- tH_chunk, // Today History (NEW ARGUMENT)
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_')) { // TODO - This should iedally become redundant, computations themselves should NEVER return an object so large it requires sharding...
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
- filterCalculations,
302
+ fetchComputationStatus, // Exported for Pass Runner
303
+ updateComputationStatus, // Exported for Pass Runner (error handling)
279
304
  runStandardComputationPass,
280
305
  runMetaComputationPass
281
- };
306
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bulltrackers-module",
3
- "version": "1.0.174",
3
+ "version": "1.0.175",
4
4
  "description": "Helper Functions for Bulltrackers.",
5
5
  "main": "index.js",
6
6
  "files": [