bulltrackers-module 1.0.183 → 1.0.185

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,6 +1,6 @@
1
1
  /**
2
2
  * FIXED: computation_controller.js
3
- * V3.3: Adds Price Loading for Meta Context & Fixes Context Injection
3
+ * V3.4: Adds Price Loading & Context Injection.
4
4
  */
5
5
 
6
6
  const { DataExtractor,
@@ -55,34 +55,25 @@ class DataLoader {
55
55
 
56
56
  try {
57
57
  const snapshot = await db.collection(collection).get();
58
- if (snapshot.empty) return { history: [] };
58
+ if (snapshot.empty) return { history: {} };
59
59
 
60
- // Flatten shards into a single array for the context
61
- // Structure expected by calculation: Array of { instrumentId, prices: {...} }
62
- const allPrices = [];
60
+ const historyMap = {};
63
61
 
64
62
  snapshot.forEach(doc => {
65
63
  const shardData = doc.data();
66
- // Iterate keys in shard (instrumentIds)
67
- for (const [instId, data] of Object.entries(shardData)) {
68
- if (data && data.prices) {
69
- allPrices.push({
70
- instrumentId: instId,
71
- ...data
72
- });
73
- }
74
- }
64
+ // Merge shard keys (instrumentIds) into main map
65
+ if (shardData) Object.assign(historyMap, shardData);
75
66
  });
76
67
 
77
- logger.log('INFO', `[DataLoader] Loaded prices for ${allPrices.length} instruments.`);
68
+ logger.log('INFO', `[DataLoader] Loaded prices for ${Object.keys(historyMap).length} instruments.`);
78
69
 
79
- // Cache as an object with 'history' array to match context expectations
80
- this.cache.prices = { history: allPrices };
70
+ // Cache as an object with 'history' map to match priceExtractor expectations
71
+ this.cache.prices = { history: historyMap };
81
72
  return this.cache.prices;
82
73
 
83
74
  } catch (e) {
84
75
  logger.log('ERROR', `[DataLoader] Failed to load prices: ${e.message}`);
85
- return { history: [] };
76
+ return { history: {} };
86
77
  }
87
78
  }
88
79
  }
@@ -13,7 +13,7 @@ const {
13
13
  checkRootDependencies
14
14
  } = require('./orchestration_helpers.js');
15
15
 
16
- const { getExpectedDateStrings, normalizeName } = require('../utils/utils.js');
16
+ const { getExpectedDateStrings, normalizeName, getEarliestDataDates } = require('../utils/utils.js');
17
17
 
18
18
  const PARALLEL_BATCH_SIZE = 7;
19
19
 
@@ -25,8 +25,7 @@ async function runComputationPass(config, dependencies, computationManifest) {
25
25
 
26
26
  logger.log('INFO', `🚀 Starting PASS ${passToRun} (Targeting /computation_status/{YYYY-MM-DD})...`);
27
27
 
28
- // Hardcoded earliest dates
29
- 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') };
28
+ const earliestDates = await getEarliestDataDates(config, dependencies); // Now not hardcoded.
30
29
  earliestDates.absoluteEarliest = Object.values(earliestDates).reduce((a,b) => a < b ? a : b);
31
30
 
32
31
  const passes = groupByPass(computationManifest);
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * FILENAME: bulltrackers-module/functions/computation-system/helpers/orchestration_helpers.js
3
- * FIXED: Only marks computations as TRUE if they actually store results.
3
+ * FIXED: Explicit Logging + Honest Status Updates
4
4
  */
5
5
 
6
6
  const { ComputationController } = require('../controllers/computation_controller');
@@ -28,8 +28,7 @@ function checkRootDependencies(calcManifest, rootDataStatus) {
28
28
  else if (dep === 'insights' && !rootDataStatus.hasInsights) missing.push('insights');
29
29
  else if (dep === 'social' && !rootDataStatus.hasSocial) missing.push('social');
30
30
  else if (dep === 'history' && !rootDataStatus.hasHistory) missing.push('history');
31
- // Note: 'price' is typically not a blocking root check in this specific function logic unless added,
32
- // but usually prices are treated as auxiliary. If you want to block on prices, add it here.
31
+ else if (dep === 'price' && !rootDataStatus.hasPrices) missing.push('price'); // NEW
33
32
  }
34
33
  return { canRun: missing.length === 0, missing };
35
34
  }
@@ -38,27 +37,34 @@ function checkRootDependencies(calcManifest, rootDataStatus) {
38
37
  * Checks for the availability of all required root data for a specific date.
39
38
  */
40
39
  async function checkRootDataAvailability(dateStr, config, dependencies, earliestDates) {
41
- const { logger } = dependencies;
40
+ const { logger, db } = dependencies;
42
41
  const dateToProcess = new Date(dateStr + 'T00:00:00Z');
43
- let portfolioRefs = [], historyRefs = [];
44
- let hasPortfolio = false, hasInsights = false, hasSocial = false, hasHistory = false, insightsData = null , socialData = null;
42
+ let portfolioRefs = [], historyRefs = [];
43
+ let hasPortfolio = false, hasInsights = false, hasSocial = false, hasHistory = false, hasPrices = false;
44
+ let insightsData = null, socialData = null;
45
45
 
46
46
  try {
47
47
  const tasks = [];
48
- if (dateToProcess >= earliestDates.portfolio) tasks.push(getPortfolioPartRefs(config, dependencies, dateStr).then(r => { portfolioRefs = r; hasPortfolio = !!r.length; }));
49
- if (dateToProcess >= earliestDates.insights) tasks.push(loadDailyInsights(config, dependencies, dateStr).then(r => { insightsData = r; hasInsights = !!r; }));
50
- if (dateToProcess >= earliestDates.social) tasks.push(loadDailySocialPostInsights(config, dependencies, dateStr).then(r => { socialData = r; hasSocial = !!r; }));
51
- if (dateToProcess >= earliestDates.history) tasks.push(getHistoryPartRefs(config, dependencies, dateStr).then(r => { historyRefs = r; hasHistory = !!r.length; }));
48
+ if (dateToProcess >= earliestDates.portfolio) tasks.push(getPortfolioPartRefs(config, dependencies, dateStr).then(r => { portfolioRefs = r; hasPortfolio = !!r.length; }));
49
+ if (dateToProcess >= earliestDates.insights) tasks.push(loadDailyInsights(config, dependencies, dateStr).then(r => { insightsData = r; hasInsights = !!r; }));
50
+ if (dateToProcess >= earliestDates.social) tasks.push(loadDailySocialPostInsights(config, dependencies, dateStr).then(r => { socialData = r; hasSocial = !!r; }));
51
+ if (dateToProcess >= earliestDates.history) tasks.push(getHistoryPartRefs(config, dependencies, dateStr).then(r => { historyRefs = r; hasHistory = !!r.length; }));
52
+
53
+ // NEW: Check if price data exists (proper validation)
54
+ if (dateToProcess >= (earliestDates.price || earliestDates.absoluteEarliest)) {
55
+ tasks.push(checkPriceDataAvailability(config, dependencies).then(r => { hasPrices = r; }));
56
+ }
52
57
 
53
58
  await Promise.all(tasks);
54
59
 
55
- // We allow running if ANY data is present. Specific calcs filter themselves using checkRootDependencies.
56
- if (!(hasPortfolio || hasInsights || hasSocial || hasHistory)) return null;
60
+ if (!(hasPortfolio || hasInsights || hasSocial || hasHistory || hasPrices)) return null;
57
61
 
58
62
  return {
59
- portfolioRefs, historyRefs,
60
- todayInsights: insightsData, todaySocialPostInsights: socialData,
61
- status: { hasPortfolio, hasInsights, hasSocial, hasHistory }
63
+ portfolioRefs,
64
+ historyRefs,
65
+ todayInsights: insightsData,
66
+ todaySocialPostInsights: socialData,
67
+ status: { hasPortfolio, hasInsights, hasSocial, hasHistory, hasPrices }
62
68
  };
63
69
 
64
70
  } catch (err) {
@@ -67,6 +73,40 @@ async function checkRootDataAvailability(dateStr, config, dependencies, earliest
67
73
  }
68
74
  }
69
75
 
76
+ /**
77
+ * NEW HELPER: Check if price data collection has any data
78
+ */
79
+ async function checkPriceDataAvailability(config, dependencies) {
80
+ const { db, logger } = dependencies;
81
+ const collection = config.priceCollection || 'asset_prices';
82
+
83
+ try {
84
+ // Check if the collection has at least one shard document
85
+ const snapshot = await db.collection(collection).limit(1).get();
86
+
87
+ if (snapshot.empty) {
88
+ logger.log('WARN', `[checkPriceData] No price shards found in ${collection}`);
89
+ return false;
90
+ }
91
+
92
+ // Check if the first shard actually has price data
93
+ const firstDoc = snapshot.docs[0];
94
+ const data = firstDoc.data();
95
+
96
+ if (!data || Object.keys(data).length === 0) {
97
+ logger.log('WARN', `[checkPriceData] Price shard exists but is empty`);
98
+ return false;
99
+ }
100
+
101
+ logger.log('TRACE', `[checkPriceData] Price data available in ${collection}`);
102
+ return true;
103
+
104
+ } catch (e) {
105
+ logger.log('ERROR', `[checkPriceData] Failed to check price availability: ${e.message}`);
106
+ return false;
107
+ }
108
+ }
109
+
70
110
  async function fetchComputationStatus(dateStr, config, { db }) {
71
111
  const collection = config.computationStatusCollection || 'computation_status';
72
112
  const docRef = db.collection(collection).doc(dateStr);
@@ -205,6 +245,9 @@ async function runStandardComputationPass(date, calcs, passName, config, deps, r
205
245
  const inst = new c.class();
206
246
  inst.manifest = c;
207
247
  state[normalizeName(c.name)] = inst;
248
+
249
+ // LOG: Explicitly say what calculation is being processed (Initialized)
250
+ logger.log('INFO', `${c.name} calculation running for ${dStr}`);
208
251
  }
209
252
  catch(e) {
210
253
  logger.log('WARN', `Failed to init ${c.name}`);
@@ -223,6 +266,9 @@ async function runMetaComputationPass(date, calcs, passName, config, deps, fetch
223
266
 
224
267
  for (const mCalc of calcs) {
225
268
  try {
269
+ // LOG: Explicitly say what calculation is being processed
270
+ deps.logger.log('INFO', `${mCalc.name} calculation running for ${dStr}`);
271
+
226
272
  const inst = new mCalc.class();
227
273
  inst.manifest = mCalc;
228
274
  await controller.executor.executeOncePerDay(inst, mCalc, dStr, fetchedDeps, previousFetchedDeps);
@@ -234,8 +280,8 @@ async function runMetaComputationPass(date, calcs, passName, config, deps, fetch
234
280
  }
235
281
 
236
282
  /**
237
- * --- FIXED: commitResults ---
238
- * Only marks 'successUpdates' if data is actually written.
283
+ * --- UPDATED: commitResults ---
284
+ * Includes Explicit Result Logging and Honest Status Reporting.
239
285
  */
240
286
  async function commitResults(stateObj, dStr, passName, config, deps, skipStatusWrite = false) {
241
287
  const writes = [], schemas = [], sharded = {};
@@ -245,10 +291,15 @@ async function commitResults(stateObj, dStr, passName, config, deps, skipStatusW
245
291
  const calc = stateObj[name];
246
292
  try {
247
293
  const result = await calc.getResult();
248
- if (!result) continue;
294
+
295
+ // If null/undefined, log as Failed/Unknown immediately
296
+ if (!result) {
297
+ deps.logger.log('INFO', `${name} calculation for ${dStr} ran, result : Failed (Empty Result)`);
298
+ continue;
299
+ }
249
300
 
250
301
  const standardRes = {};
251
- let hasData = false; // Track if this calc produced data
302
+ let hasData = false;
252
303
 
253
304
  for (const key in result) {
254
305
  if (key.startsWith('sharded_')) {
@@ -284,15 +335,19 @@ async function commitResults(stateObj, dStr, passName, config, deps, skipStatusW
284
335
  });
285
336
  }
286
337
 
287
- // FIX: Only mark as successful if we actually had data to write.
338
+ // --- EXPLICIT LOGGING & STATUS UPDATE ---
288
339
  if (hasData) {
289
340
  successUpdates[name] = true;
341
+ deps.logger.log('INFO', `${name} calculation for ${dStr} ran, result : Succeeded`);
290
342
  } else {
291
- // Optional: Log that it produced no data
292
- // deps.logger.log('INFO', `Calc ${name} produced no data. Skipping status update.`);
343
+ // It ran without error, but produced no content (e.g. no data met criteria)
344
+ deps.logger.log('INFO', `${name} calculation for ${dStr} ran, result : Unknown (No Data Written)`);
293
345
  }
294
346
 
295
- } catch (e) { deps.logger.log('ERROR', `Commit failed ${name}: ${e.message}`); }
347
+ } catch (e) {
348
+ deps.logger.log('ERROR', `Commit failed ${name}: ${e.message}`);
349
+ deps.logger.log('INFO', `${name} calculation for ${dStr} ran, result : Failed (Exception)`);
350
+ }
296
351
  }
297
352
 
298
353
  if (schemas.length) batchStoreSchemas(deps, config, schemas).catch(()=>{});
@@ -75,25 +75,125 @@ async function getFirstDateFromCollection(config, deps, collectionName) {
75
75
  async function getEarliestDataDates(config, deps) {
76
76
  const { logger } = deps;
77
77
  logger.log('INFO', 'Querying for earliest date from ALL source data collections...');
78
- const [ investorDate, speculatorDate, investorHistoryDate, speculatorHistoryDate, insightsDate, socialDate ] = await Promise.all([
79
- getFirstDateFromCollection (config, deps, config.normalUserPortfolioCollection),
80
- getFirstDateFromCollection (config, deps, config.speculatorPortfolioCollection),
81
- getFirstDateFromCollection (config, deps, config.normalUserHistoryCollection),
82
- getFirstDateFromCollection (config, deps, config.speculatorHistoryCollection),
78
+
79
+ const [
80
+ investorDate,
81
+ speculatorDate,
82
+ investorHistoryDate,
83
+ speculatorHistoryDate,
84
+ insightsDate,
85
+ socialDate,
86
+ priceDate // NEW
87
+ ] = await Promise.all([
88
+ getFirstDateFromCollection(config, deps, config.normalUserPortfolioCollection),
89
+ getFirstDateFromCollection(config, deps, config.speculatorPortfolioCollection),
90
+ getFirstDateFromCollection(config, deps, config.normalUserHistoryCollection),
91
+ getFirstDateFromCollection(config, deps, config.speculatorHistoryCollection),
83
92
  getFirstDateFromSimpleCollection(config, deps, config.insightsCollectionName),
84
- getFirstDateFromSimpleCollection(config, deps, config.socialInsightsCollectionName)
93
+ getFirstDateFromSimpleCollection(config, deps, config.socialInsightsCollectionName),
94
+ getFirstDateFromPriceCollection(config, deps) // NEW
85
95
  ]);
86
96
 
87
- const getMinDate = (...dates) => { const validDates = dates.filter(Boolean); if (validDates.length === 0) return null; return new Date(Math.min(...validDates)); };
97
+ const getMinDate = (...dates) => {
98
+ const validDates = dates.filter(Boolean);
99
+ if (validDates.length === 0) return null;
100
+ return new Date(Math.min(...validDates));
101
+ };
102
+
88
103
  const earliestPortfolioDate = getMinDate(investorDate, speculatorDate);
89
- const earliestHistoryDate = getMinDate(investorHistoryDate, speculatorHistoryDate);
90
- const earliestInsightsDate = getMinDate(insightsDate);
91
- const earliestSocialDate = getMinDate(socialDate);
92
- const absoluteEarliest = getMinDate(earliestPortfolioDate, earliestHistoryDate, earliestInsightsDate, earliestSocialDate );
93
- const fallbackDate = new Date(config.earliestComputationDate + 'T00:00:00Z' || '2023-01-01T00:00:00Z');
94
- const result = { portfolio: earliestPortfolioDate || new Date('2999-12-31'), history: earliestHistoryDate || new Date('2999-12-31'), insights: earliestInsightsDate || new Date('2999-12-31'), social: earliestSocialDate || new Date('2999-12-31'), absoluteEarliest: absoluteEarliest || fallbackDate };
95
- logger.log('INFO', 'Earliest data availability map built:', { portfolio: result.portfolio.toISOString().slice(0, 10), history: result.history.toISOString().slice(0, 10), insights: result.insights.toISOString().slice(0, 10), social: result.social.toISOString().slice(0, 10), absoluteEarliest: result.absoluteEarliest.toISOString().slice(0, 10) });
104
+ const earliestHistoryDate = getMinDate(investorHistoryDate, speculatorHistoryDate);
105
+ const earliestInsightsDate = getMinDate(insightsDate);
106
+ const earliestSocialDate = getMinDate(socialDate);
107
+ const earliestPriceDate = getMinDate(priceDate); // NEW
108
+ const absoluteEarliest = getMinDate(
109
+ earliestPortfolioDate,
110
+ earliestHistoryDate,
111
+ earliestInsightsDate,
112
+ earliestSocialDate,
113
+ earliestPriceDate // NEW
114
+ );
115
+
116
+ 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-31'),
120
+ history: earliestHistoryDate || new Date('2999-12-31'),
121
+ insights: earliestInsightsDate || new Date('2999-12-31'),
122
+ social: earliestSocialDate || new Date('2999-12-31'),
123
+ price: earliestPriceDate || new Date('2999-12-31'), // NEW
124
+ absoluteEarliest: absoluteEarliest || fallbackDate
125
+ };
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), // NEW
133
+ absoluteEarliest: result.absoluteEarliest.toISOString().slice(0, 10)
134
+ });
135
+
96
136
  return result;
97
137
  }
98
138
 
139
+ /**
140
+ * NEW HELPER: Get the earliest date from price collection
141
+ * Price data is sharded differently - each shard contains instrumentId -> {prices: {date: price}}
142
+ */
143
+ async function getFirstDateFromPriceCollection(config, deps) {
144
+ const { db, logger, calculationUtils } = deps;
145
+ const { withRetry } = calculationUtils;
146
+ const collection = config.priceCollection || 'asset_prices';
147
+
148
+ try {
149
+ logger.log('TRACE', `[getFirstDateFromPriceCollection] Querying ${collection}...`);
150
+
151
+ // Get all shards (limit to first few for performance)
152
+ const snapshot = await withRetry(
153
+ () => db.collection(collection).limit(10).get(),
154
+ `GetPriceShards(${collection})`
155
+ );
156
+
157
+ if (snapshot.empty) {
158
+ logger.log('WARN', `No price shards found in ${collection}`);
159
+ return null;
160
+ }
161
+
162
+ let earliestDate = null;
163
+
164
+ // Iterate through shards to find the earliest date across all instruments
165
+ snapshot.forEach(doc => {
166
+ const shardData = doc.data();
167
+
168
+ // Each shard has structure: { instrumentId: { ticker, prices: { "YYYY-MM-DD": price } } }
169
+ for (const instrumentId in shardData) {
170
+ const instrumentData = shardData[instrumentId];
171
+ if (!instrumentData.prices) continue;
172
+
173
+ // Get all dates for this instrument
174
+ const dates = Object.keys(instrumentData.prices)
175
+ .filter(d => /^\d{4}-\d{2}-\d{2}$/.test(d))
176
+ .sort();
177
+
178
+ if (dates.length > 0) {
179
+ const firstDate = new Date(dates[0] + 'T00:00:00Z');
180
+ if (!earliestDate || firstDate < earliestDate) {
181
+ earliestDate = firstDate;
182
+ }
183
+ }
184
+ }
185
+ });
186
+
187
+ if (earliestDate) {
188
+ logger.log('TRACE', `[getFirstDateFromPriceCollection] Earliest price date: ${earliestDate.toISOString().slice(0, 10)}`);
189
+ }
190
+
191
+ return earliestDate;
192
+
193
+ } catch (e) {
194
+ logger.log('ERROR', `Failed to get earliest price date from ${collection}`, { errorMessage: e.message });
195
+ return null;
196
+ }
197
+ }
198
+
99
199
  module.exports = { FieldValue, FieldPath, normalizeName, commitBatchInChunks, getExpectedDateStrings, getEarliestDataDates };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bulltrackers-module",
3
- "version": "1.0.183",
3
+ "version": "1.0.185",
4
4
  "description": "Helper Functions for Bulltrackers.",
5
5
  "main": "index.js",
6
6
  "files": [