bulltrackers-module 1.0.204 → 1.0.205

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,27 +1,26 @@
1
1
  /**
2
2
  * FILENAME: bulltrackers-module/functions/computation-system/helpers/orchestration_helpers.js
3
- * FIXED: Context math mapping in runBatchPriceComputation (resolves 'undefined' crash).
4
- * IMPROVED: Explicit logging for every calculation run (Start, Success, Failure).
5
- * OPTIMIZED: Parallel Commits & Pipelined Shards.
3
+ * FIXED: 'commitResults' now isolates commits PER COMPUTATION.
4
+ * A single failure (e.g., size limit) will only fail that specific calculation,
5
+ * allowing others in the same pass/date to succeed and be recorded.
6
6
  */
7
7
 
8
- const { ComputationController } = require('../controllers/computation_controller');
9
- const { batchStoreSchemas } = require('../utils/schema_capture');
8
+ const { ComputationController } = require('../controllers/computation_controller');
9
+ const { batchStoreSchemas } = require('../utils/schema_capture');
10
10
  const { normalizeName, commitBatchInChunks } = require('../utils/utils');
11
- const {
12
- getPortfolioPartRefs, loadDailyInsights, loadDailySocialPostInsights,
11
+ const {
12
+ getPortfolioPartRefs, loadDailyInsights, loadDailySocialPostInsights,
13
13
  getHistoryPartRefs, streamPortfolioData, streamHistoryData,
14
14
  getRelevantShardRefs, loadDataByRefs
15
15
  } = require('../utils/data_loader');
16
16
 
17
- // --- FIX 1: Import Math Layer Primitives for Correct Context Mapping ---
18
- const {
19
- DataExtractor, HistoryExtractor, MathPrimitives, Aggregators,
20
- Validators, SCHEMAS, SignalPrimitives, DistributionAnalytics,
21
- TimeSeries, priceExtractor
17
+ const {
18
+ DataExtractor, HistoryExtractor, MathPrimitives, Aggregators,
19
+ Validators, SCHEMAS, SignalPrimitives, DistributionAnalytics,
20
+ TimeSeries, priceExtractor
22
21
  } = require('../layers/math_primitives.js');
23
22
 
24
- const pLimit = require('p-limit');
23
+ const pLimit = require('p-limit');
25
24
 
26
25
  /**
27
26
  * Groups calculations from a manifest by their 'pass' property.
@@ -30,21 +29,20 @@ function groupByPass(manifest) { return manifest.reduce((acc, calc) => { (acc[ca
30
29
 
31
30
  /**
32
31
  * --- PASSIVE DATA VALIDATION ---
33
- * Scans a result set for suspicious patterns (e.g., a field is NULL for 100% of tickers).
34
32
  */
35
- function validateResultPatterns(logger, calcName, results, category) {
33
+ function validateResultPatterns(logger, calcName, results, category) {
36
34
  if (category === 'speculator' || category === 'speculators') return;
37
35
 
38
36
  const tickers = Object.keys(results);
39
37
  const totalItems = tickers.length;
40
-
41
- if (totalItems < 5) return;
38
+
39
+ if (totalItems < 5) return;
42
40
 
43
41
  const sampleTicker = tickers.find(t => results[t] && typeof results[t] === 'object');
44
42
  if (!sampleTicker) return;
45
-
43
+
46
44
  const keys = Object.keys(results[sampleTicker]);
47
-
45
+
48
46
  keys.forEach(key => {
49
47
  if (key.startsWith('_')) return;
50
48
 
@@ -60,28 +58,25 @@ function validateResultPatterns(logger, calcName, results, category) {
60
58
  }
61
59
 
62
60
  if (nanCount === totalItems) {
63
- logger.log('ERROR', `[DataQuality] Calc '${calcName}' field '${key}' is NaN for 100% of ${totalItems} items. Code bug likely.`);
61
+ logger.log('ERROR', `[DataQuality] Calc '${calcName}' field '${key}' is NaN for 100% of ${totalItems} items.`);
64
62
  } else if (undefinedCount === totalItems) {
65
- logger.log('ERROR', `[DataQuality] Calc '${calcName}' field '${key}' is UNDEFINED for 100% of ${totalItems} items. Code bug likely.`);
66
- }
63
+ logger.log('ERROR', `[DataQuality] Calc '${calcName}' field '${key}' is UNDEFINED for 100% of ${totalItems} items.`);
64
+ }
67
65
  else if (nullCount > (totalItems * 0.9)) {
68
- logger.log('WARN', `[DataQuality] Calc '${calcName}' field '${key}' is NULL for ${nullCount}/${totalItems} items. Check logic if this is unexpected.`);
66
+ logger.log('WARN', `[DataQuality] Calc '${calcName}' field '${key}' is NULL for ${nullCount}/${totalItems} items.`);
69
67
  }
70
68
  });
71
69
  }
72
70
 
73
- /**
74
- * Checks if all root data dependencies for a given calculation are met.
75
- */
76
71
  function checkRootDependencies(calcManifest, rootDataStatus) {
77
72
  const missing = [];
78
73
  if (!calcManifest.rootDataDependencies) return { canRun: true, missing };
79
74
  for (const dep of calcManifest.rootDataDependencies) {
80
- if (dep === 'portfolio' && !rootDataStatus.hasPortfolio) missing.push('portfolio');
81
- else if (dep === 'insights' && !rootDataStatus.hasInsights) missing.push('insights');
82
- else if (dep === 'social' && !rootDataStatus.hasSocial) missing.push('social');
83
- else if (dep === 'history' && !rootDataStatus.hasHistory) missing.push('history');
84
- else if (dep === 'price' && !rootDataStatus.hasPrices) missing.push('price');
75
+ if (dep === 'portfolio' && !rootDataStatus.hasPortfolio) missing.push('portfolio');
76
+ else if (dep === 'insights' && !rootDataStatus.hasInsights) missing.push('insights');
77
+ else if (dep === 'social' && !rootDataStatus.hasSocial) missing.push('social');
78
+ else if (dep === 'history' && !rootDataStatus.hasHistory) missing.push('history');
79
+ else if (dep === 'price' && !rootDataStatus.hasPrices) missing.push('price');
85
80
  }
86
81
  return { canRun: missing.length === 0, missing };
87
82
  }
@@ -89,30 +84,30 @@ function checkRootDependencies(calcManifest, rootDataStatus) {
89
84
  async function checkRootDataAvailability(dateStr, config, dependencies, earliestDates) {
90
85
  const { logger } = dependencies;
91
86
  const dateToProcess = new Date(dateStr + 'T00:00:00Z');
92
- let portfolioRefs = [], historyRefs = [];
93
- let hasPortfolio = false, hasInsights = false, hasSocial = false, hasHistory = false, hasPrices = false;
94
- let insightsData = null, socialData = null;
87
+ let portfolioRefs = [], historyRefs = [];
88
+ let hasPortfolio = false, hasInsights = false, hasSocial = false, hasHistory = false, hasPrices = false;
89
+ let insightsData = null, socialData = null;
95
90
 
96
91
  try {
97
92
  const tasks = [];
98
93
  if (dateToProcess >= earliestDates.portfolio) tasks.push(getPortfolioPartRefs(config, dependencies, dateStr).then(r => { portfolioRefs = r; hasPortfolio = !!r.length; }));
99
- if (dateToProcess >= earliestDates.insights) tasks.push(loadDailyInsights(config, dependencies, dateStr).then(r => { insightsData = r; hasInsights = !!r; }));
100
- if (dateToProcess >= earliestDates.social) tasks.push(loadDailySocialPostInsights(config, dependencies, dateStr).then(r => { socialData = r; hasSocial = !!r; }));
101
- if (dateToProcess >= earliestDates.history) tasks.push(getHistoryPartRefs(config, dependencies, dateStr).then(r => { historyRefs = r; hasHistory = !!r.length; }));
102
-
94
+ if (dateToProcess >= earliestDates.insights) tasks.push(loadDailyInsights(config, dependencies, dateStr).then(r => { insightsData = r; hasInsights = !!r; }));
95
+ if (dateToProcess >= earliestDates.social) tasks.push(loadDailySocialPostInsights(config, dependencies, dateStr).then(r => { socialData = r; hasSocial = !!r; }));
96
+ if (dateToProcess >= earliestDates.history) tasks.push(getHistoryPartRefs(config, dependencies, dateStr).then(r => { historyRefs = r; hasHistory = !!r.length; }));
97
+
103
98
  if (dateToProcess >= earliestDates.price) {
104
99
  tasks.push(checkPriceDataAvailability(config, dependencies).then(r => { hasPrices = r; }));
105
100
  }
106
-
101
+
107
102
  await Promise.all(tasks);
108
-
103
+
109
104
  if (!(hasPortfolio || hasInsights || hasSocial || hasHistory || hasPrices)) return null;
110
-
111
- return {
112
- portfolioRefs,
113
- historyRefs,
114
- todayInsights: insightsData,
115
- todaySocialPostInsights: socialData,
105
+
106
+ return {
107
+ portfolioRefs,
108
+ historyRefs,
109
+ todayInsights: insightsData,
110
+ todaySocialPostInsights: socialData,
116
111
  status: { hasPortfolio, hasInsights, hasSocial, hasHistory, hasPrices }
117
112
  };
118
113
 
@@ -140,8 +135,8 @@ async function checkPriceDataAvailability(config, dependencies) {
140
135
 
141
136
  async function fetchComputationStatus(dateStr, config, { db }) {
142
137
  const collection = config.computationStatusCollection || 'computation_status';
143
- const docRef = db.collection(collection).doc(dateStr);
144
- const snap = await docRef.get();
138
+ const docRef = db.collection(collection).doc(dateStr);
139
+ const snap = await docRef.get();
145
140
  return snap.exists ? snap.data() : {};
146
141
  }
147
142
 
@@ -155,8 +150,8 @@ async function fetchGlobalComputationStatus(config, { db }) {
155
150
  async function updateComputationStatus(dateStr, updates, config, { db }) {
156
151
  if (!updates || Object.keys(updates).length === 0) return;
157
152
  const collection = config.computationStatusCollection || 'computation_status';
158
- const docRef = db.collection(collection).doc(dateStr);
159
- await docRef.set(updates, { merge: true });
153
+ const docRef = db.collection(collection).doc(dateStr);
154
+ await docRef.set(updates, { merge: true });
160
155
  }
161
156
 
162
157
  async function updateGlobalComputationStatus(updatesByDate, config, { db }) {
@@ -172,38 +167,41 @@ async function updateGlobalComputationStatus(updatesByDate, config, { db }) {
172
167
  try {
173
168
  await docRef.update(flattenUpdates);
174
169
  } catch (err) {
175
- if (err.code === 5) {
176
- const deepObj = {};
177
- for (const [date, statuses] of Object.entries(updatesByDate)) {
178
- deepObj[date] = statuses;
179
- }
180
- await docRef.set(deepObj, { merge: true });
170
+ if (err.code === 5) {
171
+ const deepObj = {};
172
+ for (const [date, statuses] of Object.entries(updatesByDate)) {
173
+ deepObj[date] = statuses;
174
+ }
175
+ await docRef.set(deepObj, { merge: true });
181
176
  } else {
182
- throw err;
177
+ throw err;
183
178
  }
184
179
  }
185
180
  }
186
181
 
187
182
  async function fetchExistingResults(dateStr, calcsInPass, fullManifest, config, { db }, includeSelf = false) {
188
- const manifestMap = new Map(fullManifest.map(c => [normalizeName(c.name), c]));
183
+ const manifestMap = new Map(fullManifest.map(c => [normalizeName(c.name), c]));
189
184
  const calcsToFetch = new Set();
190
185
  for (const calc of calcsInPass) {
191
- if (calc.dependencies) { calc.dependencies.forEach(d => calcsToFetch.add(normalizeName(d))); }
186
+ if (calc.dependencies) { calc.dependencies.forEach(d => calcsToFetch.add(normalizeName(d))); }
192
187
  if (includeSelf && calc.isHistorical) { calcsToFetch.add(normalizeName(calc.name)); }
193
188
  }
194
189
  if (!calcsToFetch.size) return {};
195
190
  const fetched = {};
196
191
  const docRefs = [];
197
- const names = [];
192
+ const names = [];
198
193
  for (const name of calcsToFetch) {
199
194
  const m = manifestMap.get(name);
200
- if (m) { docRefs.push(db.collection(config.resultsCollection).doc(dateStr)
201
- .collection(config.resultsSubcollection).doc(m.category || 'unknown')
202
- .collection(config.computationsSubcollection).doc(name));
203
- names.push(name); } }
195
+ if (m) {
196
+ docRefs.push(db.collection(config.resultsCollection).doc(dateStr)
197
+ .collection(config.resultsSubcollection).doc(m.category || 'unknown')
198
+ .collection(config.computationsSubcollection).doc(name));
199
+ names.push(name);
200
+ }
201
+ }
204
202
  if (docRefs.length) {
205
203
  const snaps = await db.getAll(...docRefs);
206
- snaps.forEach((doc, i) => { if(doc.exists && doc.data()._completed) { fetched[names[i]] = doc.data(); } });
204
+ snaps.forEach((doc, i) => { if (doc.exists && doc.data()._completed) { fetched[names[i]] = doc.data(); } });
207
205
  }
208
206
  return fetched;
209
207
  }
@@ -212,8 +210,8 @@ async function streamAndProcess(dateStr, state, passName, config, deps, rootData
212
210
  const { logger } = deps;
213
211
  const controller = new ComputationController(config, deps);
214
212
  const calcs = Object.values(state).filter(c => c && c.manifest);
215
- const streamingCalcs = calcs.filter(c =>
216
- c.manifest.rootDataDependencies.includes('portfolio') ||
213
+ const streamingCalcs = calcs.filter(c =>
214
+ c.manifest.rootDataDependencies.includes('portfolio') ||
217
215
  c.manifest.rootDataDependencies.includes('history')
218
216
  );
219
217
 
@@ -227,7 +225,7 @@ async function streamAndProcess(dateStr, state, passName, config, deps, rootData
227
225
 
228
226
  const tP_iter = streamPortfolioData(config, deps, dateStr, portfolioRefs);
229
227
  const needsYesterdayPortfolio = streamingCalcs.some(c => c.manifest.isHistorical);
230
- const yP_iter = (needsYesterdayPortfolio && rootData.yesterdayPortfolioRefs) ? streamPortfolioData(config, deps, prevDateStr, rootData.yesterdayPortfolioRefs) : null;
228
+ const yP_iter = (needsYesterdayPortfolio && rootData.yesterdayPortfolioRefs) ? streamPortfolioData(config, deps, prevDateStr, rootData.yesterdayPortfolioRefs) : null;
231
229
  const needsTradingHistory = streamingCalcs.some(c => c.manifest.rootDataDependencies.includes('history'));
232
230
  const tH_iter = (needsTradingHistory && historyRefs) ? streamHistoryData(config, deps, dateStr, historyRefs) : null;
233
231
 
@@ -238,14 +236,14 @@ async function streamAndProcess(dateStr, state, passName, config, deps, rootData
238
236
  if (yP_iter) yP_chunk = (await yP_iter.next()).value || {};
239
237
  if (tH_iter) tH_chunk = (await tH_iter.next()).value || {};
240
238
 
241
- const promises = streamingCalcs.map(calc =>
239
+ const promises = streamingCalcs.map(calc =>
242
240
  controller.executor.executePerUser(
243
241
  calc,
244
242
  calc.manifest,
245
243
  dateStr,
246
244
  tP_chunk,
247
- yP_chunk,
248
- tH_chunk,
245
+ yP_chunk,
246
+ tH_chunk,
249
247
  fetchedDeps,
250
248
  previousFetchedDeps
251
249
  )
@@ -260,20 +258,20 @@ async function runStandardComputationPass(date, calcs, passName, config, deps, r
260
258
  const logger = deps.logger;
261
259
  const fullRoot = { ...rootData };
262
260
  if (calcs.some(c => c.isHistorical)) {
263
- const prev = new Date(date); prev.setUTCDate(prev.getUTCDate() - 1);
261
+ const prev = new Date(date); prev.setUTCDate(prev.getUTCDate() - 1);
264
262
  const prevStr = prev.toISOString().slice(0, 10);
265
263
  fullRoot.yesterdayPortfolioRefs = await getPortfolioPartRefs(config, deps, prevStr);
266
264
  }
267
265
 
268
266
  const state = {};
269
267
  for (const c of calcs) {
270
- try {
271
- const inst = new c.class();
272
- inst.manifest = c;
273
- state[normalizeName(c.name)] = inst;
268
+ try {
269
+ const inst = new c.class();
270
+ inst.manifest = c;
271
+ state[normalizeName(c.name)] = inst;
274
272
  logger.log('INFO', `${c.name} calculation running for ${dStr}`);
275
- }
276
- catch(e) { logger.log('WARN', `Failed to init ${c.name}`); }
273
+ }
274
+ catch (e) { logger.log('WARN', `Failed to init ${c.name}`); }
277
275
  }
278
276
 
279
277
  await streamAndProcess(dStr, state, passName, config, deps, fullRoot, rootData.portfolioRefs, rootData.historyRefs, fetchedDeps, previousFetchedDeps);
@@ -282,8 +280,8 @@ async function runStandardComputationPass(date, calcs, passName, config, deps, r
282
280
 
283
281
  async function runMetaComputationPass(date, calcs, passName, config, deps, fetchedDeps, previousFetchedDeps, rootData, skipStatusWrite = false) {
284
282
  const controller = new ComputationController(config, deps);
285
- const dStr = date.toISOString().slice(0, 10);
286
- const state = {};
283
+ const dStr = date.toISOString().slice(0, 10);
284
+ const state = {};
287
285
 
288
286
  for (const mCalc of calcs) {
289
287
  try {
@@ -297,28 +295,46 @@ async function runMetaComputationPass(date, calcs, passName, config, deps, fetch
297
295
  return await commitResults(state, dStr, passName, config, deps, skipStatusWrite);
298
296
  }
299
297
 
298
+ /**
299
+ * --- REFACTORED: commitResults ---
300
+ * Commits results individually per calculation.
301
+ * If one calculation fails (e.g. size limit), others still succeed.
302
+ */
300
303
  async function commitResults(stateObj, dStr, passName, config, deps, skipStatusWrite = false) {
301
- const writes = [], schemas = [], sharded = {};
302
- const successUpdates = {};
304
+ const successUpdates = {};
305
+ const schemas = [];
303
306
 
307
+ // Iterate PER CALCULATION to isolate failures
304
308
  for (const name in stateObj) {
305
309
  const calc = stateObj[name];
310
+ let hasData = false;
311
+
306
312
  try {
307
- const result = await calc.getResult();
313
+ const result = await calc.getResult();
308
314
  if (!result) {
309
- deps.logger.log('INFO', `${name} calculation for ${dStr} ran, result : Failed (Empty Result)`);
315
+ deps.logger.log('INFO', `${name} for ${dStr}: Skipped (Empty Result)`);
310
316
  continue;
311
317
  }
312
-
318
+
313
319
  const standardRes = {};
314
- let hasData = false;
320
+ const shardedWrites = [];
321
+ const calcWrites = []; // Accumulate all writes for THIS specific calculation
315
322
 
323
+ // 1. Separate Standard and Sharded Data
316
324
  for (const key in result) {
317
325
  if (key.startsWith('sharded_')) {
318
326
  const sData = result[key];
319
- for (const c in sData) {
320
- sharded[c] = sharded[c] || {};
321
- Object.assign(sharded[c], sData[c]);
327
+ // sData structure: { CollectionName: { DocId: { ...data } } }
328
+ for (const colName in sData) {
329
+ const docsMap = sData[colName];
330
+ for (const docId in docsMap) {
331
+ // Support both full path or collection-relative path
332
+ const ref = docId.includes('/') ? deps.db.doc(docId) : deps.db.collection(colName).doc(docId);
333
+ shardedWrites.push({
334
+ ref,
335
+ data: { ...docsMap[docId], _completed: true }
336
+ });
337
+ }
322
338
  }
323
339
  if (Object.keys(sData).length > 0) hasData = true;
324
340
  } else {
@@ -326,70 +342,75 @@ async function commitResults(stateObj, dStr, passName, config, deps, skipStatusW
326
342
  }
327
343
  }
328
344
 
345
+ // 2. Prepare Standard Result Write
329
346
  if (Object.keys(standardRes).length) {
330
347
  validateResultPatterns(deps.logger, name, standardRes, calc.manifest.category);
331
-
332
348
  standardRes._completed = true;
333
- writes.push({
334
- ref: deps.db.collection(config.resultsCollection).doc(dStr)
335
- .collection(config.resultsSubcollection).doc(calc.manifest.category)
336
- .collection(config.computationsSubcollection).doc(name),
349
+
350
+ const docRef = deps.db.collection(config.resultsCollection).doc(dStr)
351
+ .collection(config.resultsSubcollection).doc(calc.manifest.category)
352
+ .collection(config.computationsSubcollection).doc(name);
353
+
354
+ calcWrites.push({
355
+ ref: docRef,
337
356
  data: standardRes
338
357
  });
339
358
  hasData = true;
340
359
  }
341
-
360
+
361
+ // 3. Queue Schema (Safe to accumulate)
342
362
  if (calc.manifest.class.getSchema) {
343
363
  const { class: _cls, ...safeMetadata } = calc.manifest;
344
- schemas.push({
345
- name, category: calc.manifest.category, schema: calc.manifest.class.getSchema(), metadata: safeMetadata
364
+ schemas.push({
365
+ name, category: calc.manifest.category, schema: calc.manifest.class.getSchema(), metadata: safeMetadata
346
366
  });
347
367
  }
348
-
368
+
369
+ // 4. ATTEMPT COMMIT FOR THIS CALCULATION ONLY
349
370
  if (hasData) {
350
- successUpdates[name] = true;
351
- deps.logger.log('INFO', `${name} calculation for ${dStr} ran, result : Succeeded`);
371
+ // Combine standard + sharded writes for this unit of work
372
+ const allWritesForCalc = [...calcWrites, ...shardedWrites];
373
+
374
+ if (allWritesForCalc.length > 0) {
375
+ await commitBatchInChunks(config, deps, allWritesForCalc, `${name} Results`);
376
+
377
+ // IF we get here, the commit succeeded.
378
+ successUpdates[name] = true;
379
+ deps.logger.log('INFO', `${name} for ${dStr}: \u2714 Success (Written)`);
380
+ } else {
381
+ deps.logger.log('INFO', `${name} for ${dStr}: - No Data to Write`);
382
+ }
352
383
  } else {
353
- deps.logger.log('INFO', `${name} calculation for ${dStr} ran, result : Unknown (No Data Written)`);
384
+ deps.logger.log('INFO', `${name} for ${dStr}: - Empty`);
354
385
  }
355
386
 
356
- } catch (e) {
357
- deps.logger.log('ERROR', `Commit failed ${name}: ${e.message}`);
358
- deps.logger.log('INFO', `${name} calculation for ${dStr} ran, result : Failed (Exception)`);
387
+ } catch (e) {
388
+ // CRITICAL: Catch errors here so the loop continues for other calculations
389
+ deps.logger.log('ERROR', `${name} for ${dStr}: \u2716 FAILED Commit: ${e.message}`);
390
+ // Do NOT add to successUpdates
359
391
  }
360
392
  }
361
393
 
362
- if (schemas.length) batchStoreSchemas(deps, config, schemas).catch(()=>{});
363
- if (writes.length) await commitBatchInChunks(config, deps, writes, `${passName} Results`);
364
- for (const col in sharded) {
365
- const sWrites = [];
366
- for (const id in sharded[col]) {
367
- const ref = id.includes('/') ? deps.db.doc(id) : deps.db.collection(col).doc(id);
368
- sWrites.push({ ref, data: { ...sharded[col][id], _completed: true } });
369
- }
370
- if (sWrites.length) await commitBatchInChunks(config, deps, sWrites, `${passName} Sharded ${col}`);
371
- }
394
+ // Save Schemas (Best effort, isolated)
395
+ if (schemas.length) batchStoreSchemas(deps, config, schemas).catch(() => { });
372
396
 
397
+ // Update Status Document (Only for the ones that succeeded)
373
398
  if (!skipStatusWrite && Object.keys(successUpdates).length > 0) {
374
399
  await updateComputationStatus(dStr, successUpdates, config, deps);
375
- deps.logger.log('INFO', `[${passName}] Updated status document for ${Object.keys(successUpdates).length} computations.`);
400
+ deps.logger.log('INFO', `[${passName}] Updated status document for ${Object.keys(successUpdates).length} successful computations.`);
376
401
  }
377
402
  return successUpdates;
378
403
  }
379
404
 
380
405
  /**
381
406
  * --- UPDATED: runBatchPriceComputation ---
382
- * Now supports subset/specific ticker execution via 'targetTickers'
383
- * OPTIMIZED: Implements concurrency for both Shard Processing and Write Commits
384
407
  */
385
408
  async function runBatchPriceComputation(config, deps, dateStrings, calcs, targetTickers = []) {
386
- const { logger, db, calculationUtils } = deps;
409
+ const { logger, db, calculationUtils } = deps;
387
410
  const controller = new ComputationController(config, deps);
388
-
389
- // 1. Call loadMappings() correctly and get the result
390
- const mappings = await controller.loader.loadMappings();
391
-
392
- // 2. Resolve Shards (All or Subset)
411
+
412
+ const mappings = await controller.loader.loadMappings();
413
+
393
414
  let targetInstrumentIds = [];
394
415
  if (targetTickers && targetTickers.length > 0) {
395
416
  const tickerToInst = mappings.tickerToInstrument || {};
@@ -399,22 +420,20 @@ async function runBatchPriceComputation(config, deps, dateStrings, calcs, target
399
420
  return;
400
421
  }
401
422
  }
402
-
423
+
403
424
  const allShardRefs = await getRelevantShardRefs(config, deps, targetInstrumentIds);
404
-
425
+
405
426
  if (!allShardRefs.length) {
406
427
  logger.log('WARN', '[BatchPrice] No relevant price shards found. Exiting.');
407
428
  return;
408
429
  }
409
430
 
410
- // 3. Execution Planning
411
- const OUTER_CONCURRENCY_LIMIT = 2;
431
+ const OUTER_CONCURRENCY_LIMIT = 2;
412
432
  const SHARD_BATCH_SIZE = 20;
413
- const WRITE_BATCH_LIMIT = 50;
433
+ const WRITE_BATCH_LIMIT = 50;
414
434
 
415
435
  logger.log('INFO', `[BatchPrice] Execution Plan: ${dateStrings.length} days, ${allShardRefs.length} shards. Concurrency: ${OUTER_CONCURRENCY_LIMIT}.`);
416
436
 
417
- // 4. Create Chunks of Shards
418
437
  const shardChunks = [];
419
438
  for (let i = 0; i < allShardRefs.length; i += SHARD_BATCH_SIZE) {
420
439
  shardChunks.push(allShardRefs.slice(i, i + SHARD_BATCH_SIZE));
@@ -422,114 +441,104 @@ async function runBatchPriceComputation(config, deps, dateStrings, calcs, target
422
441
 
423
442
  const outerLimit = pLimit(OUTER_CONCURRENCY_LIMIT);
424
443
 
425
- // 5. Process Shard Chunks Concurrently
426
- const chunkPromises = shardChunks.map((shardChunkRefs, index) => outerLimit(async () => {
427
- try {
428
- logger.log('INFO', `[BatchPrice] Processing chunk ${index + 1}/${shardChunks.length} (${shardChunkRefs.length} shards)...`);
429
-
430
- const pricesData = await loadDataByRefs(config, deps, shardChunkRefs);
431
-
432
- // Optional Filtering for Subset Mode
433
- if (targetInstrumentIds.length > 0) {
434
- const requestedSet = new Set(targetInstrumentIds);
435
- for (const loadedInstrumentId in pricesData) {
436
- if (!requestedSet.has(loadedInstrumentId)) {
437
- delete pricesData[loadedInstrumentId];
444
+ const chunkPromises = [];
445
+ for (let index = 0; index < shardChunks.length; index++) {
446
+ const shardChunkRefs = shardChunks[index];
447
+ chunkPromises.push(outerLimit(async () => {
448
+ try {
449
+ logger.log('INFO', `[BatchPrice] Processing chunk ${index + 1}/${shardChunks.length} (${shardChunkRefs.length} shards)...`);
450
+
451
+ const pricesData = await loadDataByRefs(config, deps, shardChunkRefs);
452
+
453
+ if (targetInstrumentIds.length > 0) {
454
+ const requestedSet = new Set(targetInstrumentIds);
455
+ for (const loadedInstrumentId in pricesData) {
456
+ if (!requestedSet.has(loadedInstrumentId)) {
457
+ delete pricesData[loadedInstrumentId];
458
+ }
438
459
  }
439
460
  }
440
- }
441
- const writes = [];
442
-
443
- // --- CALCULATION PHASE ---
444
- for (const dateStr of dateStrings) {
445
- // --- FIX 2: Manually map math primitives to their alias names ---
446
- // This matches the ContextBuilder logic in ComputationController
447
- // and fixes the "Cannot read properties of undefined (reading 'standardDeviation')" error.
448
- const context = {
449
- mappings,
450
- prices: { history: pricesData },
451
- date: { today: dateStr },
452
- math: {
453
- extract: DataExtractor,
454
- history: HistoryExtractor,
455
- compute: MathPrimitives,
456
- aggregate: Aggregators,
457
- validate: Validators,
458
- signals: SignalPrimitives,
459
- schemas: SCHEMAS,
460
- distribution : DistributionAnalytics,
461
- TimeSeries: TimeSeries,
462
- priceExtractor : priceExtractor
463
- }
464
- };
465
-
466
- for (const calcManifest of calcs) {
467
- try {
468
- // --- LOGGING FIX: Log start of calculation ---
469
- logger.log('INFO', `[BatchPrice] >> Running ${calcManifest.name} for ${dateStr}...`);
470
-
471
- const instance = new calcManifest.class();
472
- await instance.process(context);
473
- const result = await instance.getResult();
474
-
475
- let hasContent = false;
476
- if (result && Object.keys(result).length > 0) {
477
- let dataToWrite = result;
478
- if (result.by_instrument) dataToWrite = result.by_instrument;
479
-
480
- if (Object.keys(dataToWrite).length > 0) {
481
- hasContent = true;
482
- const docRef = db.collection(config.resultsCollection).doc(dateStr)
483
- .collection(config.resultsSubcollection).doc(calcManifest.category)
484
- .collection(config.computationsSubcollection).doc(normalizeName(calcManifest.name));
485
-
486
- writes.push({
487
- ref: docRef,
488
- data: { ...dataToWrite, _completed: true },
489
- options: { merge: true }
490
- });
491
- }
492
- }
493
461
 
494
- // --- LOGGING FIX: Log success/completion ---
495
- if (hasContent) {
496
- logger.log('INFO', `[BatchPrice] \u2714 Finished ${calcManifest.name} for ${dateStr}. Found data.`);
497
- } else {
498
- logger.log('INFO', `[BatchPrice] - Finished ${calcManifest.name} for ${dateStr}. No result data.`);
462
+ // We now accumulate writes per calc to allow partial success, though batching optimization is tricky here.
463
+ // For safety, let's keep the existing structure but wrap individual calc processing in try/catch
464
+ // inside the write phase if possible.
465
+ // However, runBatchPrice is optimized for BULK throughput.
466
+ // To prevent total failure, we will use a safe array.
467
+ const writes = [];
468
+
469
+ for (const dateStr of dateStrings) {
470
+ const context = {
471
+ mappings,
472
+ prices: { history: pricesData },
473
+ date: { today: dateStr },
474
+ math: {
475
+ extract: DataExtractor,
476
+ history: HistoryExtractor,
477
+ compute: MathPrimitives,
478
+ aggregate: Aggregators,
479
+ validate: Validators,
480
+ signals: SignalPrimitives,
481
+ schemas: SCHEMAS,
482
+ distribution: DistributionAnalytics,
483
+ TimeSeries: TimeSeries,
484
+ priceExtractor: priceExtractor
485
+ }
486
+ };
487
+
488
+ for (const calcManifest of calcs) {
489
+ try {
490
+ // logger.log('INFO', `[BatchPrice] >> Running ${calcManifest.name} for ${dateStr}...`); // Verbose
491
+ const instance = new calcManifest.class();
492
+ await instance.process(context);
493
+ const result = await instance.getResult();
494
+
495
+ if (result && Object.keys(result).length > 0) {
496
+ let dataToWrite = result;
497
+ if (result.by_instrument) dataToWrite = result.by_instrument;
498
+
499
+ if (Object.keys(dataToWrite).length > 0) {
500
+ const docRef = db.collection(config.resultsCollection).doc(dateStr)
501
+ .collection(config.resultsSubcollection).doc(calcManifest.category)
502
+ .collection(config.computationsSubcollection).doc(normalizeName(calcManifest.name));
503
+
504
+ writes.push({
505
+ ref: docRef,
506
+ data: { ...dataToWrite, _completed: true },
507
+ options: { merge: true }
508
+ });
509
+ }
510
+ }
511
+ } catch (err) {
512
+ logger.log('ERROR', `[BatchPrice] \u2716 Failed ${calcManifest.name} for ${dateStr}: ${err.message}`);
499
513
  }
500
-
501
- } catch (err) {
502
- // --- LOGGING FIX: Explicit failure log ---
503
- logger.log('ERROR', `[BatchPrice] \u2716 Failed ${calcManifest.name} for ${dateStr}: ${err.message}`);
504
514
  }
505
515
  }
506
- }
507
-
508
- // --- PARALLEL COMMIT PHASE ---
509
- if (writes.length > 0) {
510
- const commitBatches = [];
511
- for (let i = 0; i < writes.length; i += WRITE_BATCH_LIMIT) {
512
- commitBatches.push(writes.slice(i, i + WRITE_BATCH_LIMIT));
513
- }
514
516
 
515
- const commitLimit = pLimit(10);
516
-
517
- await Promise.all(commitBatches.map((batchWrites, bIndex) => commitLimit(async () => {
518
- const batch = db.batch();
519
- batchWrites.forEach(w => batch.set(w.ref, w.data, w.options));
520
-
521
- try {
522
- await calculationUtils.withRetry(() => batch.commit(), `BatchPrice-C${index}-B${bIndex}`);
523
- } catch (commitErr) {
524
- logger.log('ERROR', `[BatchPrice] Commit failed for Chunk ${index} Batch ${bIndex}.`, { error: commitErr.message });
517
+ if (writes.length > 0) {
518
+ const commitBatches = [];
519
+ for (let i = 0; i < writes.length; i += WRITE_BATCH_LIMIT) {
520
+ commitBatches.push(writes.slice(i, i + WRITE_BATCH_LIMIT));
525
521
  }
526
- })));
527
- }
528
522
 
529
- } catch (chunkErr) {
530
- logger.log('ERROR', `[BatchPrice] Fatal error processing Chunk ${index}.`, { error: chunkErr.message });
531
- }
532
- }));
523
+ const commitLimit = pLimit(10);
524
+
525
+ await Promise.all(commitBatches.map((batchWrites, bIndex) => commitLimit(async () => {
526
+ const batch = db.batch();
527
+ batchWrites.forEach(w => batch.set(w.ref, w.data, w.options));
528
+
529
+ try {
530
+ await calculationUtils.withRetry(() => batch.commit(), `BatchPrice-C${index}-B${bIndex}`);
531
+ } catch (commitErr) {
532
+ logger.log('ERROR', `[BatchPrice] Commit failed for Chunk ${index} Batch ${bIndex}.`, { error: commitErr.message });
533
+ }
534
+ })));
535
+ }
536
+
537
+ } catch (chunkErr) {
538
+ logger.log('ERROR', `[BatchPrice] Fatal error processing Chunk ${index}.`, { error: chunkErr.message });
539
+ }
540
+ }));
541
+ }
533
542
 
534
543
  await Promise.all(chunkPromises);
535
544
  logger.log('INFO', '[BatchPrice] Optimization pass complete.');
@@ -541,9 +550,9 @@ module.exports = {
541
550
  checkRootDataAvailability,
542
551
  fetchExistingResults,
543
552
  fetchComputationStatus,
544
- fetchGlobalComputationStatus,
545
- updateComputationStatus,
546
- updateGlobalComputationStatus,
553
+ fetchGlobalComputationStatus,
554
+ updateComputationStatus,
555
+ updateGlobalComputationStatus,
547
556
  runStandardComputationPass,
548
557
  runMetaComputationPass,
549
558
  runBatchPriceComputation
@@ -0,0 +1,19 @@
1
+
2
+ // Mock types
3
+ namespace Firestore {
4
+ export class DocumentReference { }
5
+ }
6
+
7
+ const pLimit = (concurrency: number) => {
8
+ return (fn: () => Promise<any>) => fn();
9
+ };
10
+
11
+ const OUTER_CONCURRENCY_LIMIT = 2;
12
+ const outerLimit = pLimit(OUTER_CONCURRENCY_LIMIT);
13
+
14
+ const shardChunks: Firestore.DocumentReference[][] = [];
15
+
16
+ // The problematic code
17
+ const chunkPromises = shardChunks.map((shardChunkRefs, index) => outerLimit(async () => {
18
+ console.log(index);
19
+ }));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bulltrackers-module",
3
- "version": "1.0.204",
3
+ "version": "1.0.205",
4
4
  "description": "Helper Functions for Bulltrackers.",
5
5
  "main": "index.js",
6
6
  "files": [