bulltrackers-module 1.0.185 → 1.0.187

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.4: Adds Price Loading & Context Injection.
3
+ * V4.0: Implements Batch/Sharded Execution for Price Dependencies to prevent OOM.
4
4
  */
5
5
 
6
6
  const { DataExtractor,
@@ -43,36 +43,54 @@ class DataLoader {
43
43
  this.cache.social.set(dateStr, social);
44
44
  return social;
45
45
  }
46
+
46
47
  /**
47
- * NEW: Loads sharded price data for Meta calculations
48
+ * NEW: Get references to all price shards without loading data.
49
+ */
50
+ async getPriceShardReferences() {
51
+ const { db, logger } = this.deps;
52
+ const collection = this.config.priceCollection || 'asset_prices';
53
+ try {
54
+ const refs = await db.collection(collection).listDocuments();
55
+ logger.log('INFO', `[DataLoader] Found ${refs.length} price shards to process.`);
56
+ return refs;
57
+ } catch (e) {
58
+ logger.log('ERROR', `[DataLoader] Failed to list price shards: ${e.message}`);
59
+ return [];
60
+ }
61
+ }
62
+
63
+ /**
64
+ * NEW: Load a single price shard.
65
+ */
66
+ async loadPriceShard(docRef) {
67
+ try {
68
+ const snap = await docRef.get();
69
+ if (!snap.exists) return {};
70
+ return snap.data();
71
+ } catch (e) {
72
+ console.error(`Error loading shard ${docRef.path}:`, e);
73
+ return {};
74
+ }
75
+ }
76
+
77
+ /**
78
+ * LEGACY: Kept for non-batched calls if ever needed, but effectively replaced by batching.
48
79
  */
49
80
  async loadPrices() {
50
- if (this.cache.prices) return this.cache.prices;
51
81
  const { db, logger } = this.deps;
52
82
  const collection = this.config.priceCollection || 'asset_prices';
53
-
54
- logger.log('INFO', `[DataLoader] Loading all price shards from ${collection}...`);
55
-
83
+ logger.log('WARN', `[DataLoader] loadPrices() called. This is memory intensive!`);
56
84
  try {
57
85
  const snapshot = await db.collection(collection).get();
58
86
  if (snapshot.empty) return { history: {} };
59
-
60
87
  const historyMap = {};
61
-
62
88
  snapshot.forEach(doc => {
63
89
  const shardData = doc.data();
64
- // Merge shard keys (instrumentIds) into main map
65
90
  if (shardData) Object.assign(historyMap, shardData);
66
91
  });
67
-
68
- logger.log('INFO', `[DataLoader] Loaded prices for ${Object.keys(historyMap).length} instruments.`);
69
-
70
- // Cache as an object with 'history' map to match priceExtractor expectations
71
- this.cache.prices = { history: historyMap };
72
- return this.cache.prices;
73
-
92
+ return { history: historyMap };
74
93
  } catch (e) {
75
- logger.log('ERROR', `[DataLoader] Failed to load prices: ${e.message}`);
76
94
  return { history: {} };
77
95
  }
78
96
  }
@@ -81,21 +99,9 @@ class DataLoader {
81
99
  class ContextBuilder {
82
100
  static buildPerUserContext(options) {
83
101
  const {
84
- todayPortfolio,
85
- yesterdayPortfolio,
86
- todayHistory,
87
- yesterdayHistory,
88
- userId,
89
- userType,
90
- dateStr,
91
- metadata,
92
- mappings,
93
- insights,
94
- socialData,
95
- computedDependencies,
96
- previousComputedDependencies,
97
- config,
98
- deps
102
+ todayPortfolio, yesterdayPortfolio, todayHistory, yesterdayHistory,
103
+ userId, userType, dateStr, metadata, mappings, insights, socialData,
104
+ computedDependencies, previousComputedDependencies, config, deps
99
105
  } = options;
100
106
 
101
107
  return {
@@ -131,23 +137,16 @@ class ContextBuilder {
131
137
 
132
138
  static buildMetaContext(options) {
133
139
  const {
134
- dateStr,
135
- metadata,
136
- mappings,
137
- insights,
138
- socialData,
139
- prices, // <--- ADDED THIS
140
- computedDependencies,
141
- previousComputedDependencies,
142
- config,
143
- deps
140
+ dateStr, metadata, mappings, insights, socialData,
141
+ prices,
142
+ computedDependencies, previousComputedDependencies, config, deps
144
143
  } = options;
145
144
 
146
145
  return {
147
146
  date: { today: dateStr },
148
147
  insights: { today: insights?.today, yesterday: insights?.yesterday },
149
148
  social: { today: socialData?.today, yesterday: socialData?.yesterday },
150
- prices: prices || {}, // <--- INJECTED HERE
149
+ prices: prices || {},
151
150
  mappings: mappings || {},
152
151
  math: {
153
152
  extract: DataExtractor,
@@ -207,27 +206,70 @@ class ComputationExecutor {
207
206
  }
208
207
  }
209
208
 
209
+ /**
210
+ * REFACTORED: Batched execution for Price dependencies
211
+ */
210
212
  async executeOncePerDay(calcInstance, metadata, dateStr, computedDeps, prevDeps) {
211
213
  const mappings = await this.loader.loadMappings();
214
+ const { logger } = this.deps;
212
215
 
213
- // Load standard dependencies
214
216
  const insights = metadata.rootDataDependencies?.includes('insights') ? { today: await this.loader.loadInsights(dateStr) } : null;
215
217
  const social = metadata.rootDataDependencies?.includes('social') ? { today: await this.loader.loadSocial(dateStr) } : null;
216
218
 
217
- // NEW: Load Price dependencies if required
218
- let prices = null;
219
+ // CHECK: Does this calculation require price history?
219
220
  if (metadata.rootDataDependencies?.includes('price')) {
220
- prices = await this.loader.loadPrices();
221
- }
221
+ logger.log('INFO', `[Executor] Running Batched/Sharded Execution for ${metadata.name}`);
222
+
223
+ // 1. Get Shard References (Low Memory)
224
+ const shardRefs = await this.loader.getPriceShardReferences();
225
+
226
+ if (shardRefs.length === 0) {
227
+ logger.log('WARN', '[Executor] No price shards found.');
228
+ return {};
229
+ }
230
+
231
+ // 2. Iterate Shards (One at a time in memory)
232
+ let processedCount = 0;
233
+ for (const ref of shardRefs) {
234
+ // Load ONE shard
235
+ const shardData = await this.loader.loadPriceShard(ref);
236
+
237
+ // Construct a "Partial Context" containing only this shard's prices
238
+ const partialContext = ContextBuilder.buildMetaContext({
239
+ dateStr, metadata, mappings, insights, socialData: social,
240
+ prices: { history: shardData }, // Inject partial history
241
+ computedDependencies: computedDeps,
242
+ previousComputedDependencies: prevDeps,
243
+ config: this.config, deps: this.deps
244
+ });
245
+
246
+ // Process the chunk
247
+ // IMPORTANT: The calc instance must ACCUMULATE results, not overwrite.
248
+ await calcInstance.process(partialContext);
249
+
250
+ // Force dereference for GC
251
+ partialContext.prices = null;
252
+ processedCount++;
253
+
254
+ if (processedCount % 10 === 0) {
255
+ if (global.gc) { global.gc(); } // Optional: Hint GC if exposed
256
+ }
257
+ }
258
+
259
+ logger.log('INFO', `[Executor] Finished Batched Execution for ${metadata.name} (${processedCount} shards).`);
260
+ return calcInstance.getResult ? await calcInstance.getResult() : {};
222
261
 
223
- const context = ContextBuilder.buildMetaContext({
224
- dateStr, metadata, mappings, insights, socialData: social,
225
- prices, // Pass prices to builder
226
- computedDependencies: computedDeps,
227
- previousComputedDependencies: prevDeps,
228
- config: this.config, deps: this.deps
229
- });
230
- return await calcInstance.process(context);
262
+ } else {
263
+ // Standard execution for non-price calculations
264
+ const context = ContextBuilder.buildMetaContext({
265
+ dateStr, metadata, mappings, insights, socialData: social,
266
+ prices: {},
267
+ computedDependencies: computedDeps,
268
+ previousComputedDependencies: prevDeps,
269
+ config: this.config, deps: this.deps
270
+ });
271
+ return await calcInstance.process(context);
272
+ }
231
273
  }
232
274
  }
233
275
 
@@ -25,9 +25,18 @@ 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
- const earliestDates = await getEarliestDataDates(config, dependencies); // Now not hardcoded.
28
+ // Hardcoded earliest dates
29
+ const earliestDates = {
30
+ portfolio: new Date('2025-09-25T00:00:00Z'),
31
+ history: new Date('2025-11-05T00:00:00Z'),
32
+ social: new Date('2025-10-30T00:00:00Z'),
33
+ insights: new Date('2025-08-26T00:00:00Z'),
34
+ price: new Date('2025-08-01T00:00:00Z') // A few weeks before insights (earliest other data)
35
+
36
+ };
29
37
  earliestDates.absoluteEarliest = Object.values(earliestDates).reduce((a,b) => a < b ? a : b);
30
38
 
39
+
31
40
  const passes = groupByPass(computationManifest);
32
41
  const calcsInThisPass = passes[passToRun] || [];
33
42
 
@@ -37,7 +37,7 @@ function checkRootDependencies(calcManifest, rootDataStatus) {
37
37
  * Checks for the availability of all required root data for a specific date.
38
38
  */
39
39
  async function checkRootDataAvailability(dateStr, config, dependencies, earliestDates) {
40
- const { logger, db } = dependencies;
40
+ const { logger } = dependencies;
41
41
  const dateToProcess = new Date(dateStr + 'T00:00:00Z');
42
42
  let portfolioRefs = [], historyRefs = [];
43
43
  let hasPortfolio = false, hasInsights = false, hasSocial = false, hasHistory = false, hasPrices = false;
@@ -50,8 +50,8 @@ async function checkRootDataAvailability(dateStr, config, dependencies, earliest
50
50
  if (dateToProcess >= earliestDates.social) tasks.push(loadDailySocialPostInsights(config, dependencies, dateStr).then(r => { socialData = r; hasSocial = !!r; }));
51
51
  if (dateToProcess >= earliestDates.history) tasks.push(getHistoryPartRefs(config, dependencies, dateStr).then(r => { historyRefs = r; hasHistory = !!r.length; }));
52
52
 
53
- // NEW: Check if price data exists (proper validation)
54
- if (dateToProcess >= (earliestDates.price || earliestDates.absoluteEarliest)) {
53
+ // NEW: Check if price data exists - simple validation
54
+ if (dateToProcess >= earliestDates.price) {
55
55
  tasks.push(checkPriceDataAvailability(config, dependencies).then(r => { hasPrices = r; }));
56
56
  }
57
57
 
@@ -74,14 +74,14 @@ async function checkRootDataAvailability(dateStr, config, dependencies, earliest
74
74
  }
75
75
 
76
76
  /**
77
- * NEW HELPER: Check if price data collection has any data
77
+ * NEW HELPER: Simple check if price collection has any data
78
78
  */
79
79
  async function checkPriceDataAvailability(config, dependencies) {
80
80
  const { db, logger } = dependencies;
81
81
  const collection = config.priceCollection || 'asset_prices';
82
82
 
83
83
  try {
84
- // Check if the collection has at least one shard document
84
+ // Just check if the collection has at least one document
85
85
  const snapshot = await db.collection(collection).limit(1).get();
86
86
 
87
87
  if (snapshot.empty) {
@@ -89,16 +89,6 @@ async function checkPriceDataAvailability(config, dependencies) {
89
89
  return false;
90
90
  }
91
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
92
  return true;
103
93
 
104
94
  } catch (e) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bulltrackers-module",
3
- "version": "1.0.185",
3
+ "version": "1.0.187",
4
4
  "description": "Helper Functions for Bulltrackers.",
5
5
  "main": "index.js",
6
6
  "files": [