bulltrackers-module 1.0.203 → 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
+ }));
@@ -1,40 +1,93 @@
1
1
  /**
2
2
  * @fileoverview Computation system sub-pipes and utils.
3
3
  * REFACTORED: Now stateless and receive dependencies where needed.
4
- * DYNAMIC: Categorization logic is removed, replaced by manifest.
5
- * --- MODIFIED: getFirstDateFromSourceData is now getEarliestDataDates
6
- * and queries all data sources to build an availability map. ---
4
+ * FIXED: 'commitBatchInChunks' now respects Firestore 10MB size limit.
7
5
  */
8
- /** --- Computation System Sub-Pipes & Utils (Stateless, Dependency-Injection) --- */
9
6
 
10
7
  const { FieldValue, FieldPath } = require('@google-cloud/firestore');
11
8
 
12
9
  /** Stage 1: Normalize a calculation name to kebab-case */
13
10
  function normalizeName(name) { return name.replace(/_/g, '-'); }
14
11
 
15
- /** Stage 2: Commit a batch of writes in chunks */
12
+ /** * Stage 2: Commit a batch of writes in chunks
13
+ * FIXED: Now splits batches by SIZE (9MB limit) and COUNT (450 docs)
14
+ * to prevent "Request payload size exceeds the limit" errors.
15
+ */
16
16
  async function commitBatchInChunks(config, deps, writes, operationName) {
17
17
  const { db, logger, calculationUtils } = deps;
18
18
  const { withRetry } = calculationUtils;
19
- const batchSizeLimit = config.batchSizeLimit || 450;
20
- if (!writes.length) { logger.log('WARN', `[${operationName}] No writes to commit.`); return; }
21
- for (let i = 0; i < writes.length; i += batchSizeLimit) {
22
- const chunk = writes.slice(i, i + batchSizeLimit);
23
- const batch = db.batch();
24
- chunk.forEach(write => batch.set(write.ref, write.data, { merge: true }));
25
- const chunkNum = Math.floor(i / batchSizeLimit) + 1;
26
- const totalChunks = Math.ceil(writes.length / batchSizeLimit);
27
- await withRetry(() => batch.commit(), `${operationName} (Chunk ${chunkNum}/${totalChunks})`);
28
- logger.log('INFO', `[${operationName}] Committed chunk ${chunkNum}/${totalChunks} (${chunk.length} ops).`); }
19
+
20
+ if (!writes || !writes.length) {
21
+ logger.log('WARN', `[${operationName}] No writes to commit.`);
22
+ return;
23
+ }
24
+
25
+ // Firestore Constraints
26
+ const MAX_BATCH_OPS = 300; // Safety limit (Max 500)
27
+ const MAX_BATCH_BYTES = 9 * 1024 * 1024; // 9MB Safety limit (Max 10MB)
28
+
29
+ let currentBatch = db.batch();
30
+ let currentOpsCount = 0;
31
+ let currentBytesEst = 0;
32
+ let batchIndex = 1;
33
+ let totalChunks = 0; // We don't know total chunks in advance now due to dynamic sizing
34
+
35
+ // Helper to commit the current batch and reset
36
+ const commitAndReset = async () => {
37
+ if (currentOpsCount > 0) {
38
+ try {
39
+ await withRetry(
40
+ () => currentBatch.commit(),
41
+ `${operationName} (Chunk ${batchIndex})`
42
+ );
43
+ logger.log('INFO', `[${operationName}] Committed chunk ${batchIndex} (${currentOpsCount} ops, ~${(currentBytesEst / 1024 / 1024).toFixed(2)} MB).`);
44
+ batchIndex++;
45
+ } catch (err) {
46
+ logger.log('ERROR', `[${operationName}] Failed to commit chunk ${batchIndex}. Size: ${(currentBytesEst / 1024 / 1024).toFixed(2)} MB.`, { error: err.message });
47
+ throw err;
48
+ }
49
+ }
50
+ currentBatch = db.batch();
51
+ currentOpsCount = 0;
52
+ currentBytesEst = 0;
53
+ };
54
+
55
+ for (const write of writes) {
56
+ // 1. Estimate Size: JSON stringify is a decent proxy for Firestore payload size
57
+ // We handle potential circular refs or failures gracefully by assuming a minimum size
58
+ let docSize = 100;
59
+ try {
60
+ if (write.data) docSize = JSON.stringify(write.data).length;
61
+ } catch (e) { /* ignore size check error */ }
62
+
63
+ // 2. Warn if a SINGLE document is approaching the 1MB limit
64
+ if (docSize > 900 * 1024) {
65
+ logger.log('WARN', `[${operationName}] Large document detected (~${(docSize / 1024).toFixed(2)} KB). This allows few ops per batch.`);
66
+ }
67
+
68
+ // 3. Check if adding this write would overflow the batch
69
+ if ((currentOpsCount + 1 > MAX_BATCH_OPS) || (currentBytesEst + docSize > MAX_BATCH_BYTES)) {
70
+ await commitAndReset();
71
+ }
72
+
73
+ // 4. Add to batch
74
+ currentBatch.set(write.ref, write.data, { merge: true });
75
+ currentOpsCount++;
76
+ currentBytesEst += docSize;
77
+ }
78
+
79
+ // 5. Commit remaining
80
+ await commitAndReset();
29
81
  }
30
82
 
31
83
  /** Stage 3: Generate an array of expected date strings between two dates */
32
84
  function getExpectedDateStrings(startDate, endDate) {
33
85
  const dateStrings = [];
34
86
  if (startDate <= endDate) {
35
- const startUTC = new Date(Date.UTC(startDate.getUTCFullYear(), startDate.getUTCMonth(), startDate.getUTCDate()));
36
- const endUTC = new Date(Date.UTC(endDate.getUTCFullYear(), endDate.getUTCMonth(), endDate.getUTCDate()));
37
- for (let d = startUTC; d <= endUTC; d.setUTCDate(d.getUTCDate() + 1)) { dateStrings.push(new Date(d).toISOString().slice(0, 10)); } }
87
+ const startUTC = new Date(Date.UTC(startDate.getUTCFullYear(), startDate.getUTCMonth(), startDate.getUTCDate()));
88
+ const endUTC = new Date(Date.UTC(endDate.getUTCFullYear(), endDate.getUTCMonth(), endDate.getUTCDate()));
89
+ for (let d = startUTC; d <= endUTC; d.setUTCDate(d.getUTCDate() + 1)) { dateStrings.push(new Date(d).toISOString().slice(0, 10)); }
90
+ }
38
91
  return dateStrings;
39
92
  }
40
93
 
@@ -46,10 +99,10 @@ async function getFirstDateFromSimpleCollection(config, deps, collectionName) {
46
99
  const { db, logger, calculationUtils } = deps;
47
100
  const { withRetry } = calculationUtils;
48
101
  try {
49
- if (!collectionName) { logger.log('WARN', `[Core Utils] Collection name not provided for simple date query.`); return null; }
50
- const query = db.collection(collectionName) .where(FieldPath.documentId(), '>=', '2000-01-01') .orderBy(FieldPath.documentId(), 'asc') .limit(1);
51
- const snapshot = await withRetry(() => query.get(), `GetEarliestDoc(${collectionName})`);
52
- if (!snapshot.empty && /^\d{4}-\d{2}-\d{2}$/.test(snapshot.docs[0].id)) { return new Date(snapshot.docs[0].id + 'T00:00:00Z'); }
102
+ if (!collectionName) { logger.log('WARN', `[Core Utils] Collection name not provided for simple date query.`); return null; }
103
+ const query = db.collection(collectionName).where(FieldPath.documentId(), '>=', '2000-01-01').orderBy(FieldPath.documentId(), 'asc').limit(1);
104
+ const snapshot = await withRetry(() => query.get(), `GetEarliestDoc(${collectionName})`);
105
+ if (!snapshot.empty && /^\d{4}-\d{2}-\d{2}$/.test(snapshot.docs[0].id)) { return new Date(snapshot.docs[0].id + 'T00:00:00Z'); }
53
106
  } catch (e) { logger.log('ERROR', `GetFirstDate failed for ${collectionName}`, { errorMessage: e.message }); }
54
107
  return null;
55
108
  }
@@ -59,13 +112,19 @@ async function getFirstDateFromCollection(config, deps, collectionName) {
59
112
  const { db, logger, calculationUtils } = deps;
60
113
  const { withRetry } = calculationUtils;
61
114
  let earliestDate = null;
62
- try { if (!collectionName) { logger.log('WARN', `[Core Utils] Collection name not provided for sharded date query.`); return null; }
63
- const blockDocRefs = await withRetry(() => db.collection(collectionName).listDocuments(), `GetBlocks(${collectionName})`);
64
- if (!blockDocRefs.length) { logger.log('WARN', `No block documents in collection: ${collectionName}`); return null; }
65
- for (const blockDocRef of blockDocRefs) { const snapshotQuery = blockDocRef.collection(config.snapshotsSubcollection) .where(FieldPath.documentId(), '>=', '2000-01-01') .orderBy(FieldPath.documentId(), 'asc') .limit(1);
66
- const snapshotSnap = await withRetry(() => snapshotQuery.get(), `GetEarliestSnapshot(${blockDocRef.path})`);
67
- if (!snapshotSnap.empty && /^\d{4}-\d{2}-\d{2}$/.test(snapshotSnap.docs[0].id)) { const foundDate = new Date(snapshotSnap.docs[0].id + 'T00:00:00Z');
68
- if (!earliestDate || foundDate < earliestDate) earliestDate = foundDate; } } } catch (e) { logger.log('ERROR', `GetFirstDate failed for ${collectionName}`, { errorMessage: e.message }); }
115
+ try {
116
+ if (!collectionName) { logger.log('WARN', `[Core Utils] Collection name not provided for sharded date query.`); return null; }
117
+ const blockDocRefs = await withRetry(() => db.collection(collectionName).listDocuments(), `GetBlocks(${collectionName})`);
118
+ if (!blockDocRefs.length) { logger.log('WARN', `No block documents in collection: ${collectionName}`); return null; }
119
+ for (const blockDocRef of blockDocRefs) {
120
+ const snapshotQuery = blockDocRef.collection(config.snapshotsSubcollection).where(FieldPath.documentId(), '>=', '2000-01-01').orderBy(FieldPath.documentId(), 'asc').limit(1);
121
+ const snapshotSnap = await withRetry(() => snapshotQuery.get(), `GetEarliestSnapshot(${blockDocRef.path})`);
122
+ if (!snapshotSnap.empty && /^\d{4}-\d{2}-\d{2}$/.test(snapshotSnap.docs[0].id)) {
123
+ const foundDate = new Date(snapshotSnap.docs[0].id + 'T00:00:00Z');
124
+ if (!earliestDate || foundDate < earliestDate) earliestDate = foundDate;
125
+ }
126
+ }
127
+ } catch (e) { logger.log('ERROR', `GetFirstDate failed for ${collectionName}`, { errorMessage: e.message }); }
69
128
  return earliestDate;
70
129
  }
71
130
 
@@ -75,15 +134,15 @@ async function getFirstDateFromCollection(config, deps, collectionName) {
75
134
  async function getEarliestDataDates(config, deps) {
76
135
  const { logger } = deps;
77
136
  logger.log('INFO', 'Querying for earliest date from ALL source data collections...');
78
-
79
- const [
80
- investorDate,
81
- speculatorDate,
82
- investorHistoryDate,
83
- speculatorHistoryDate,
84
- insightsDate,
137
+
138
+ const [
139
+ investorDate,
140
+ speculatorDate,
141
+ investorHistoryDate,
142
+ speculatorHistoryDate,
143
+ insightsDate,
85
144
  socialDate,
86
- priceDate
145
+ priceDate
87
146
  ] = await Promise.all([
88
147
  getFirstDateFromCollection(config, deps, config.normalUserPortfolioCollection),
89
148
  getFirstDateFromCollection(config, deps, config.speculatorPortfolioCollection),
@@ -91,90 +150,84 @@ async function getEarliestDataDates(config, deps) {
91
150
  getFirstDateFromCollection(config, deps, config.speculatorHistoryCollection),
92
151
  getFirstDateFromSimpleCollection(config, deps, config.insightsCollectionName),
93
152
  getFirstDateFromSimpleCollection(config, deps, config.socialInsightsCollectionName),
94
- getFirstDateFromPriceCollection(config, deps) //TODO, Why no config.pricecollectionname here, looks ugly.
153
+ getFirstDateFromPriceCollection(config, deps)
95
154
  ]);
96
-
97
- const getMinDate = (...dates) => {
98
- const validDates = dates.filter(Boolean);
99
- if (validDates.length === 0) return null;
100
- return new Date(Math.min(...validDates));
155
+
156
+ const getMinDate = (...dates) => {
157
+ const validDates = dates.filter(Boolean);
158
+ if (validDates.length === 0) return null;
159
+ return new Date(Math.min(...validDates));
101
160
  };
102
-
161
+
103
162
  const earliestPortfolioDate = getMinDate(investorDate, speculatorDate);
104
- const earliestHistoryDate = getMinDate(investorHistoryDate, speculatorHistoryDate);
105
- const earliestInsightsDate = getMinDate(insightsDate);
106
- const earliestSocialDate = getMinDate(socialDate);
107
- const earliestPriceDate = getMinDate(priceDate);
108
- const absoluteEarliest = getMinDate(
109
- earliestPortfolioDate,
110
- earliestHistoryDate,
111
- earliestInsightsDate,
163
+ const earliestHistoryDate = getMinDate(investorHistoryDate, speculatorHistoryDate);
164
+ const earliestInsightsDate = getMinDate(insightsDate);
165
+ const earliestSocialDate = getMinDate(socialDate);
166
+ const earliestPriceDate = getMinDate(priceDate);
167
+ const absoluteEarliest = getMinDate(
168
+ earliestPortfolioDate,
169
+ earliestHistoryDate,
170
+ earliestInsightsDate,
112
171
  earliestSocialDate,
113
- earliestPriceDate
172
+ earliestPriceDate
114
173
  );
115
-
174
+
116
175
  const fallbackDate = new Date(config.earliestComputationDate + 'T00:00:00Z' || '2023-01-01T00:00:00Z');
117
-
118
- const result = {
119
- portfolio: earliestPortfolioDate || new Date('2999-12-31T00:00:00Z'),
120
- history: earliestHistoryDate || new Date('2999-12-31T00:00:00Z'),
121
- insights: earliestInsightsDate || new Date('2999-12-31T00:00:00Z'),
122
- social: earliestSocialDate || new Date('2999-12-31T00:00:00Z'),
123
- price: earliestPriceDate || new Date('2999-12-31T00:00:00Z'),
176
+
177
+ const result = {
178
+ portfolio: earliestPortfolioDate || new Date('2999-12-31T00:00:00Z'),
179
+ history: earliestHistoryDate || new Date('2999-12-31T00:00:00Z'),
180
+ insights: earliestInsightsDate || new Date('2999-12-31T00:00:00Z'),
181
+ social: earliestSocialDate || new Date('2999-12-31T00:00:00Z'),
182
+ price: earliestPriceDate || new Date('2999-12-31T00:00:00Z'),
124
183
  absoluteEarliest: absoluteEarliest || fallbackDate
125
184
  };
126
-
127
- logger.log('INFO', 'Earliest data availability map built:', {
128
- portfolio: result.portfolio.toISOString().slice(0, 10),
129
- history: result.history.toISOString().slice(0, 10),
130
- insights: result.insights.toISOString().slice(0, 10),
131
- social: result.social.toISOString().slice(0, 10),
132
- price: result.price.toISOString().slice(0, 10),
133
- absoluteEarliest: result.absoluteEarliest.toISOString().slice(0, 10)
185
+
186
+ logger.log('INFO', 'Earliest data availability map built:', {
187
+ portfolio: result.portfolio.toISOString().slice(0, 10),
188
+ history: result.history.toISOString().slice(0, 10),
189
+ insights: result.insights.toISOString().slice(0, 10),
190
+ social: result.social.toISOString().slice(0, 10),
191
+ price: result.price.toISOString().slice(0, 10),
192
+ absoluteEarliest: result.absoluteEarliest.toISOString().slice(0, 10)
134
193
  });
135
-
194
+
136
195
  return result;
137
196
  }
138
197
 
139
198
  /**
140
199
  * NEW HELPER: Get the earliest date from price collection
141
- * Price data is sharded differently - each shard contains instrumentId -> {prices: {date: price}}
142
200
  */
143
201
  async function getFirstDateFromPriceCollection(config, deps) {
144
202
  const { db, logger, calculationUtils } = deps;
145
203
  const { withRetry } = calculationUtils;
146
- const collection = config.priceCollection || 'asset_prices'; // TODO This hardcode is right, but we should really be passing the config directly as other data sources do.
147
-
204
+ const collection = config.priceCollection || 'asset_prices';
205
+
148
206
  try {
149
207
  logger.log('TRACE', `[getFirstDateFromPriceCollection] Querying ${collection}...`);
150
-
151
- // Get all shards (limit to first few for performance)
208
+
152
209
  const snapshot = await withRetry(
153
- () => db.collection(collection).limit(10).get(),
210
+ () => db.collection(collection).limit(10).get(),
154
211
  `GetPriceShards(${collection})`
155
212
  );
156
-
213
+
157
214
  if (snapshot.empty) {
158
215
  logger.log('WARN', `No price shards found in ${collection}`);
159
216
  return null;
160
217
  }
161
-
218
+
162
219
  let earliestDate = null;
163
-
164
- // Iterate through shards to find the earliest date across all instruments
220
+
165
221
  snapshot.forEach(doc => {
166
222
  const shardData = doc.data();
167
-
168
- // Each shard has structure: { instrumentId: { ticker, prices: { "YYYY-MM-DD": price } } }
169
223
  for (const instrumentId in shardData) {
170
224
  const instrumentData = shardData[instrumentId];
171
225
  if (!instrumentData.prices) continue;
172
-
173
- // Get all dates for this instrument
226
+
174
227
  const dates = Object.keys(instrumentData.prices)
175
228
  .filter(d => /^\d{4}-\d{2}-\d{2}$/.test(d))
176
229
  .sort();
177
-
230
+
178
231
  if (dates.length > 0) {
179
232
  const firstDate = new Date(dates[0] + 'T00:00:00Z');
180
233
  if (!earliestDate || firstDate < earliestDate) {
@@ -183,13 +236,13 @@ async function getFirstDateFromPriceCollection(config, deps) {
183
236
  }
184
237
  }
185
238
  });
186
-
239
+
187
240
  if (earliestDate) {
188
- logger.log('TRACE', `[getFirstDateFromPriceCollection] Earliest price date: ${earliestDate.toISOString().slice(0, 10)}`); // TODO, WTF IS THIS TS ERROR. Property 'toISOString' does not exist on type 'never'.ts(2339)
241
+ logger.log('TRACE', `[getFirstDateFromPriceCollection] Earliest price date: ${earliestDate.toISOString().slice(0, 10)}`);
189
242
  }
190
-
243
+
191
244
  return earliestDate;
192
-
245
+
193
246
  } catch (e) {
194
247
  logger.log('ERROR', `Failed to get earliest price date from ${collection}`, { errorMessage: e.message });
195
248
  return null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bulltrackers-module",
3
- "version": "1.0.203",
3
+ "version": "1.0.205",
4
4
  "description": "Helper Functions for Bulltrackers.",
5
5
  "main": "index.js",
6
6
  "files": [