bulltrackers-module 1.0.145 → 1.0.146

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
  const { FieldPath } = require('@google-cloud/firestore');
2
2
  // --- MODIFIED: Import streamPortfolioData ---
3
- const { getPortfolioPartRefs, loadFullDayMap, loadDataByRefs, loadDailyInsights, loadDailySocialPostInsights, getHistoryPartRefs, streamPortfolioData } = require('../utils/data_loader.js');
3
+ const { getPortfolioPartRefs, loadFullDayMap, loadDataByRefs, loadDailyInsights, loadDailySocialPostInsights, getHistoryPartRefs, streamPortfolioData, streamHistoryData } = require('../utils/data_loader.js');
4
4
  const { normalizeName, commitBatchInChunks } = require('../utils/utils.js');
5
5
 
6
6
  /** Stage 1: Group manifest by pass number */
@@ -116,26 +116,20 @@ function initializeCalculators(calcs, logger) { const state = {}; for (const c o
116
116
 
117
117
  /** * Stage 7: Load historical data required for calculations
118
118
  */
119
+ // --- MODIFIED: Stage 7: Load ONLY non-streaming historical data ---
119
120
  async function loadHistoricalData(date, calcs, config, deps, rootData) {
120
121
  const { logger } = deps;
121
- const updated = {...rootData}, dStr=date.toISOString().slice(0,10); const tasks = [];
122
- const needsYesterdayPortfolio = calcs.some(c => c.isHistorical && c.rootDataDependencies.includes('portfolio'));
123
- const needsTodayHistory = calcs.some(c => c.rootDataDependencies.includes('history'));
124
- const needsYesterdayHistory = calcs.some(c => c.isHistorical && c.rootDataDependencies.includes('history'));
122
+ const updated = {...rootData};
123
+ const tasks = [];
124
+
125
+ // --- REMOVED: needsYesterdayPortfolio ---
126
+ // --- REMOVED: needsTodayHistory ---
127
+ // --- REMOVED: needsYesterdayHistory ---
125
128
  const needsYesterdayInsights = calcs.some(c => c.isHistorical && c.rootDataDependencies.includes('insights'));
126
129
  const needsYesterdaySocial = calcs.some(c => c.isHistorical && c.rootDataDependencies.includes('social'));
127
- if(needsYesterdayPortfolio) {
128
- tasks.push((async()=>{ const prev=new Date(date); prev.setUTCDate(prev.getUTCDate()-1); const prevStr=prev.toISOString().slice(0,10);
129
- logger.log('INFO', `[PassRunner] Loading YESTERDAY portfolio data for ${prevStr}`);
130
- updated.yesterdayPortfolios=await loadFullDayMap(config,deps,await getPortfolioPartRefs(config,deps,prevStr)); })());}
131
- if(needsTodayHistory) {
132
- tasks.push((async()=>{
133
- logger.log('INFO', `[PassRunner] Loading TODAY history data for ${dStr}`);
134
- updated.todayHistoryData=await loadFullDayMap(config,deps,rootData.historyRefs); })());}
135
- if(needsYesterdayHistory) {
136
- tasks.push((async()=>{ const prev=new Date(date); prev.setUTCDate(prev.getUTCDate()-1); const prevStr=prev.toISOString().slice(0,10);
137
- logger.log('INFO', `[PassRunner] Loading YESTERDAY history data for ${prevStr}`);
138
- updated.yesterdayHistoryData=await loadFullDayMap(config,deps,await getHistoryPartRefs(config,deps,prevStr)); })());}
130
+
131
+ // --- REMOVED: All async tasks for portfolio and history data ---
132
+
139
133
  if(needsYesterdayInsights) {
140
134
  tasks.push((async()=>{ const prev=new Date(date); prev.setUTCDate(prev.getUTCDate()-1); const prevStr=prev.toISOString().slice(0,10);
141
135
  logger.log('INFO', `[PassRunner] Loading YESTERDAY insights data for ${prevStr}`);
@@ -144,56 +138,129 @@ async function loadHistoricalData(date, calcs, config, deps, rootData) {
144
138
  tasks.push((async()=>{ const prev=new Date(date); prev.setUTCDate(prev.getUTCDate()-1); const prevStr=prev.toISOString().slice(0,10);
145
139
  logger.log('INFO', `[PassRunner] Loading YESTERDAY social data for ${prevStr}`);
146
140
  updated.yesterdaySocialPostInsights=await loadDailySocialPostInsights(config,deps,prevStr); })());}
147
- await Promise.all(tasks); return updated;
141
+
142
+ await Promise.all(tasks);
143
+ return updated; // This no longer contains the large data maps
148
144
  }
149
145
 
150
-
151
- /** * --- MODIFIED: Stage 8: Stream and process data for standard calculations ---
152
- * This function now uses an async generator to stream portfolio data
153
- * instead of loading it all into memory.
146
+ /**
147
+ * --- REFACTORED: Stage 8: Stream and process data for standard calculations ---
148
+ * This function now streams today's portfolios, yesterday's portfolios,
149
+ * and today's history data in parallel to avoid OOM errors.
150
+ * It loads chunks of all three streams, processes UIDs found in the
151
+ * main (today's portfolio) stream, and then deletes processed users
152
+ * from the historical maps to free memory.
154
153
  */
155
154
  async function streamAndProcess(dateStr, state, passName, config, deps, rootData) {
156
155
  const { logger, calculationUtils } = deps;
157
- const { todayInsights, yesterdayInsights, todaySocialPostInsights, yesterdaySocialPostInsights, todayHistoryData, yesterdayHistoryData, yesterdayPortfolios } = rootData;
156
+
157
+ // --- MODIFIED: yesterdayPortfolios & todayHistoryData are no longer in rootData ---
158
+ const { todayInsights, yesterdayInsights, todaySocialPostInsights, yesterdaySocialPostInsights } = rootData;
159
+
158
160
  const calcsThatStreamPortfolio = Object.values(state).filter(calc => calc && calc.manifest && (calc.manifest.rootDataDependencies.includes('portfolio') || calc.manifest.category === 'speculators'));
159
161
  const context={instrumentMappings:(await calculationUtils.loadInstrumentMappings()).instrumentToTicker, sectorMapping:(await calculationUtils.loadInstrumentMappings()).instrumentToSector, todayDateStr:dateStr, dependencies:deps, config};
160
162
  let firstUser=true;
163
+
164
+ // --- (Non-streaming (insights/social) calculation logic remains unchanged) ---
161
165
  for(const name in state){
162
166
  const calc=state[name]; if(!calc||typeof calc.process!=='function') continue;
163
167
  const cat=calc.manifest.category;
164
168
  if(cat==='socialPosts'||cat==='insights') {
165
169
  if (firstUser) {
166
170
  logger.log('INFO', `[${passName}] Running non-streaming calc: ${name}`);
167
- let args=[null,null,null,{...context, userType: 'n/a'},todayInsights,yesterdayInsights,todaySocialPostInsights,yesterdaySocialPostInsights,todayHistoryData,yesterdayHistoryData];
171
+ // (Using 'null' for hT and hY as they aren't relevant for these calcs)
172
+ let args=[null,null,null,{...context, userType: 'n/a'},todayInsights,yesterdayInsights,todaySocialPostInsights,yesterdaySocialPostInsights,null,null];
168
173
  if(calc.manifest.isHistorical) {
169
- args=[null,null,null,{...context, userType: 'n/a'},todayInsights,yesterdayInsights,todaySocialPostInsights,yesterdaySocialPostInsights,todayHistoryData,yesterdayHistoryData];
174
+ args=[null,null,null,{...context, userType: 'n/a'},todayInsights,yesterdayInsights,todaySocialPostInsights,yesterdaySocialPostInsights,null,null];
170
175
  }
171
176
  try{ await Promise.resolve(calc.process(...args)); } catch(e){logger.log('WARN',`Process error ${name} (non-stream)`,{err:e.message});}
172
177
  }
173
178
  }
174
179
  }
180
+ // --- (End of non-streaming calc logic) ---
181
+
175
182
  if (calcsThatStreamPortfolio.length === 0) {
176
183
  logger.log('INFO', `[${passName}] No portfolio-streaming calcs to run for ${dateStr}. Skipping stream.`);
177
- return; }
178
- logger.log('INFO', `[${passName}] Streaming portfolio data for ${calcsThatStreamPortfolio.length} calcs...`);
184
+ return;
185
+ }
186
+
187
+ logger.log('INFO', `[${passName}] Streaming portfolio & historical data for ${calcsThatStreamPortfolio.length} calcs...`);
188
+
189
+ // --- NEW: Prepare iterators and maps for parallel streaming ---
190
+ const prevDate = new Date(dateStr + 'T00:00:00Z');
191
+ prevDate.setUTCDate(prevDate.getUTCDate() - 1);
192
+ const prevDateStr = prevDate.toISOString().slice(0, 10);
193
+
194
+ const needsYesterdayPortfolio = Object.values(state).some(c => c && c.manifest.isHistorical && c.manifest.rootDataDependencies.includes('portfolio'));
195
+ const needsTodayHistory = Object.values(state).some(c => c && c.manifest.rootDataDependencies.includes('history'));
196
+
197
+ // Get the async iterators
198
+ const yP_iterator = needsYesterdayPortfolio ? streamPortfolioData(config, deps, prevDateStr) : null;
199
+ const hT_iterator = needsTodayHistory ? streamHistoryData(config, deps, dateStr) : null;
200
+
201
+ // These maps will accumulate data chunk-by-chunk
202
+ let yesterdayPortfolios = {};
203
+ let todayHistoryData = {};
204
+
205
+ // Load the FIRST chunk of historical data before the loop starts
206
+ if (yP_iterator) {
207
+ Object.assign(yesterdayPortfolios, (await yP_iterator.next()).value || {});
208
+ logger.log('INFO', `[${passName}] Loaded first chunk of yesterday's portfolios.`);
209
+ }
210
+ if (hT_iterator) {
211
+ Object.assign(todayHistoryData, (await hT_iterator.next()).value || {});
212
+ logger.log('INFO', `[${passName}] Loaded first chunk of today's history.`);
213
+ }
214
+
215
+ // --- MODIFIED: Main streaming loop (driven by TODAY's portfolio stream) ---
179
216
  for await (const chunk of streamPortfolioData(config, deps, dateStr)) {
180
- for(const uid in chunk){ const p=chunk[uid]; if(!p) continue;
217
+
218
+ // --- NEW: Load the NEXT chunk of historical data ---
219
+ // This keeps the historical maps populated as the main stream progresses
220
+ if (yP_iterator) {
221
+ Object.assign(yesterdayPortfolios, (await yP_iterator.next()).value || {});
222
+ }
223
+ if (hT_iterator) {
224
+ Object.assign(todayHistoryData, (await hT_iterator.next()).value || {});
225
+ }
226
+
227
+ for(const uid in chunk){ // Iterate through today's portfolio chunk
228
+ const p = chunk[uid]; if(!p) continue;
181
229
  const userType=p.PublicPositions?'speculator':'normal';
182
230
  context.userType=userType;
231
+
232
+ // --- NEW: Look up corresponding historical data for THIS user ---
233
+ const pY = yesterdayPortfolios[uid] || null; // Yesterday's Portfolio
234
+ const hT = todayHistoryData[uid] || null; // Today's History
235
+ // (Note: yesterdayHistoryData (hY) would require another stream if needed)
236
+
183
237
  for(const name in state){
184
238
  const calc=state[name]; if(!calc||typeof calc.process!=='function') continue;
185
239
  const cat=calc.manifest.category, isSocialOrInsights=cat==='socialPosts'||cat==='insights', isHistorical=calc.manifest.isHistorical, isSpec=cat==='speculators';
186
- if(isSocialOrInsights) continue;
187
- let args=[p,null,uid,context,todayInsights,yesterdayInsights,todaySocialPostInsights,yesterdaySocialPostInsights,todayHistoryData,yesterdayHistoryData];
240
+
241
+ if(isSocialOrInsights) continue; // Skip non-streaming calcs
242
+
243
+ // --- MODIFIED: Arguments now use streamed historical data (hT) ---
244
+ let args=[p,null,uid,context,todayInsights,yesterdayInsights,todaySocialPostInsights,yesterdaySocialPostInsights,hT,null];
245
+
188
246
  if(isHistorical){
189
- const pY=yesterdayPortfolios ? yesterdayPortfolios[uid] : null;
247
+ // pY is now the streamed yesterday's portfolio for this user
190
248
  if(!pY && (cat !== 'behavioural' && name !== 'historical-performance-aggregator')) continue;
191
- args=[p,pY,uid,context,todayInsights,yesterdayInsights,todaySocialPostInsights,yesterdaySocialPostInsights,todayHistoryData,yesterdayHistoryData]; }
249
+ args=[p,pY,uid,context,todayInsights,yesterdayInsights,todaySocialPostInsights,yesterdaySocialPostInsights,hT,null];
250
+ }
251
+
192
252
  if((userType==='normal'&&isSpec)||(userType==='speculator'&&!isSpec&&name!=='users-processed')) continue;
193
- try{ await Promise.resolve(calc.process(...args)); } catch(e){logger.log('WARN',`Process error ${name} for ${uid}`,{err:e.message});} }
253
+
254
+ try{ await Promise.resolve(calc.process(...args)); } catch(e){logger.log('WARN',`Process error ${name} for ${uid}`,{err:e.message});}
255
+ }
194
256
  firstUser=false;
257
+
258
+ // --- NEW: Clear this user from historical maps to free memory ---
259
+ if (pY) { delete yesterdayPortfolios[uid]; }
260
+ if (hT) { delete todayHistoryData[uid]; }
195
261
  }
196
262
  }
263
+ logger.log('INFO', `[${passName}] Finished streaming data for ${dateStr}.`);
197
264
  }
198
265
 
199
266
  /** Stage 9: Run standard computations */
@@ -198,6 +198,28 @@ async function* streamPortfolioData(config, deps, dateString) {
198
198
  }
199
199
  // --- END: Stage 7 ---
200
200
 
201
+ /**
202
+ * --- NEW: Stage 8: Stream history data in chunks ---
203
+ * Streams history data in chunks for a given date.
204
+ */
205
+ async function* streamHistoryData(config, deps, dateString) {
206
+ const { logger } = deps;
207
+ const refs = await getHistoryPartRefs(config, deps, dateString); // <-- Uses history refs
208
+ if (refs.length === 0) {
209
+ logger.log('WARN', `[streamHistoryData] No history refs found for ${dateString}. Stream is empty.`);
210
+ return;
211
+ }
212
+
213
+ const batchSize = config.partRefBatchSize || 50;
214
+ logger.log('INFO', `[streamHistoryData] Streaming ${refs.length} history parts in chunks of ${batchSize}...`);
215
+
216
+ for (let i = 0; i < refs.length; i += batchSize) {
217
+ const batchRefs = refs.slice(i, i + batchSize);
218
+ const data = await loadDataByRefs(config, deps, batchRefs);
219
+ yield data;
220
+ }
221
+ logger.log('INFO', `[streamHistoryData] Finished streaming for ${dateString}.`);
222
+ }
201
223
 
202
224
  module.exports = {
203
225
  getPortfolioPartRefs,
@@ -207,4 +229,5 @@ module.exports = {
207
229
  loadDailySocialPostInsights,
208
230
  getHistoryPartRefs,
209
231
  streamPortfolioData, // <-- EXPORT NEW FUNCTION
232
+ streamHistoryData // <-- EXPORT NEW FUNCTION
210
233
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bulltrackers-module",
3
- "version": "1.0.145",
3
+ "version": "1.0.146",
4
4
  "description": "Helper Functions for Bulltrackers.",
5
5
  "main": "index.js",
6
6
  "files": [