bulltrackers-module 1.0.144 → 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 */
@@ -12,8 +12,7 @@ function groupByPass(manifest) { return manifest.reduce((acc, calc) => { (acc[ca
12
12
  */
13
13
  function checkRootDependencies(calcManifest, rootDataStatus) {
14
14
  const missing = [];
15
- if (!calcManifest.rootDataDependencies || !calcManifest.rootDataDependencies.length) {
16
- return { canRun: true, missing };}
15
+ if (!calcManifest.rootDataDependencies || !calcManifest.rootDataDependencies.length) { return { canRun: true, missing };}
17
16
  for (const dep of calcManifest.rootDataDependencies) {
18
17
  if (dep === 'portfolio' && !rootDataStatus.hasPortfolio) missing.push('portfolio');
19
18
  else if (dep === 'insights' && !rootDataStatus.hasInsights) missing.push('insights');
@@ -34,8 +33,6 @@ async function checkRootDataAvailability(dateStr, config, dependencies, earliest
34
33
  let hasPortfolio = false, hasInsights = false, hasSocial = false, hasHistory = false;
35
34
  try {
36
35
  const tasks = [];
37
- // This logic is correct. It *avoids* calling get...Refs
38
- // if the dateToProcess is before the earliest known data.
39
36
  if (dateToProcess >= earliestDates.portfolio)
40
37
  {tasks.push(getPortfolioPartRefs(config, dependencies, dateStr).then(res => {portfolioRefs = res;hasPortfolio = !!(res?.length);}));}
41
38
  if (dateToProcess >= earliestDates.insights) {
@@ -44,32 +41,11 @@ async function checkRootDataAvailability(dateStr, config, dependencies, earliest
44
41
  tasks.push(loadDailySocialPostInsights(config, dependencies, dateStr).then(res => {socialData = res;hasSocial = !!res;}));}
45
42
  if (dateToProcess >= earliestDates.history) {
46
43
  tasks.push(getHistoryPartRefs(config, dependencies, dateStr).then(res => {historyRefs = res;hasHistory = !!(res?.length);}));}
47
-
48
44
  await Promise.all(tasks);
49
-
50
- // --- NEW: Log what was *actually* found ---
51
45
  logger.log('INFO', `[PassRunner] Data availability for ${dateStr}: P:${hasPortfolio}, I:${hasInsights}, S:${hasSocial}, H:${hasHistory}`);
52
46
 
53
- if (!(hasPortfolio || hasInsights || hasSocial || hasHistory)) {
54
- logger.log('WARN', `[PassRunner] No root data at all for ${dateStr}.`);
55
- // We return null to skip the entire day
56
- return null;
57
- }
58
-
59
- /**
60
- * --- THIS IS THE FIX ---
61
- * Rename keys to match what streamAndProcess (Stage 8) expects.
62
- * 'insightsData' is renamed to 'todayInsights'.
63
- * 'socialData' is renamed to 'todaySocialPostInsights'.
64
- */
65
- return {
66
- portfolioRefs,
67
- todayInsights: insightsData, // <-- FIX
68
- todaySocialPostInsights: socialData, // <-- FIX
69
- historyRefs,
70
- status: { hasPortfolio, hasInsights, hasSocial, hasHistory }
71
- };
72
-
47
+ if (!(hasPortfolio || hasInsights || hasSocial || hasHistory)) { logger.log('WARN', `[PassRunner] No root data at all for ${dateStr}.`); return null; }
48
+ return { portfolioRefs, todayInsights: insightsData, todaySocialPostInsights: socialData, historyRefs, status: { hasPortfolio, hasInsights, hasSocial, hasHistory } };
73
49
  } catch (err) { logger.log('ERROR', `[PassRunner] Error checking data for ${dateStr}`, { errorMessage: err.message }); return null; }
74
50
  }
75
51
 
@@ -105,76 +81,32 @@ async function fetchExistingResults(dateStr, calcsInPass, fullManifest, config,
105
81
  function filterCalculations(standardCalcs, metaCalcs, rootDataStatus, existingResults, passToRun, dateStr, earliestDates, logger) {
106
82
  const skipped = new Set();
107
83
  const dateToProcess = new Date(dateStr + 'T00:00:00Z');
108
-
109
- /**
110
- * --- CORRECTED LOGIC ---
111
- * Helper to get the true earliest date a calc can run.
112
- */
113
84
  const getTrueEarliestRunDate = (calc) => {
114
- let earliestRunDate = new Date('1970-01-01T00:00:00Z'); // Start at zero
85
+ let earliestRunDate = new Date('1970-01-01T00:00:00Z');
115
86
  const dependencies = calc.rootDataDependencies || [];
116
-
117
- // 1. Find the LATEST "today" dependency
118
- // This is the date where all *today* data is first available
119
87
  for (const dep of dependencies) {
120
88
  if (dep === 'portfolio' && earliestDates.portfolio > earliestRunDate) earliestRunDate = earliestDates.portfolio;
121
89
  if (dep === 'history' && earliestDates.history > earliestRunDate) earliestRunDate = earliestDates.history;
122
90
  if (dep === 'social' && earliestDates.social > earliestRunDate) earliestRunDate = earliestDates.social;
123
91
  if (dep === 'insights' && earliestDates.insights > earliestRunDate) earliestRunDate = earliestDates.insights;
124
92
  }
125
-
126
- // 2. If the calc is historical, shift the *final* date by +1
127
- // This universally applies the "+1" rule if *any* yesterday data is needed,
128
- // (as long as we found a dependency in step 1).
129
- if (calc.isHistorical && earliestRunDate.getTime() > 0) {
130
- earliestRunDate.setUTCDate(earliestRunDate.getUTCDate() + 1);
131
- }
132
-
93
+ if (calc.isHistorical && earliestRunDate.getTime() > 0) { earliestRunDate.setUTCDate(earliestRunDate.getUTCDate() + 1); }
133
94
  return earliestRunDate;
134
95
  };
135
-
136
96
  const filterCalc = (calc) => {
137
- // 1. Skip if result already exists
138
- if (existingResults[calc.name]) {
139
- logger.log('TRACE', `[Pass ${passToRun}] Skipping ${calc.name} for ${dateStr}. Result already exists.`);
140
- skipped.add(calc.name);
141
- return false;
142
- }
97
+ if (existingResults[calc.name]) {logger.log('TRACE', `[Pass ${passToRun}] Skipping ${calc.name} for ${dateStr}. Result already exists.`); skipped.add(calc.name); return false;}
143
98
 
144
- // 2. Check *true* earliest run date
145
99
  const earliestRunDate = getTrueEarliestRunDate(calc);
146
- if (dateToProcess < earliestRunDate) {
147
- logger.log('TRACE', `[Pass ${passToRun}] Skipping ${calc.name} for ${dateStr}. Date is before true earliest run date (${earliestRunDate.toISOString().slice(0, 10)}).`);
148
- skipped.add(calc.name);
149
- return false;
150
- }
151
-
152
- // 3. Check if *today's* root data was *actually* found
153
- // This handles gaps *after* the earliest date
100
+ if (dateToProcess < earliestRunDate) {logger.log('TRACE', `[Pass ${passToRun}] Skipping ${calc.name} for ${dateStr}. Date is before true earliest run date (${earliestRunDate.toISOString().slice(0, 10)}).`); skipped.add(calc.name); return false; }
101
+
154
102
  const { canRun, missing: missingRoot } = checkRootDependencies(calc, rootDataStatus);
155
- if (!canRun) {
156
- logger.log('INFO', `[Pass ${passToRun}] Skipping ${calc.name} for ${dateStr}. Data missing for this date: [${missingRoot.join(', ')}]`);
157
- skipped.add(calc.name);
158
- return false;
159
- }
160
-
161
- // 4. (For Meta Calcs) Check computed dependencies
162
- if (calc.type === 'meta') {
163
- const missingDeps = (calc.dependencies || []).map(normalizeName).filter(d => !existingResults[d]);
164
- if (missingDeps.length > 0) {
165
- logger.log('WARN', `[Pass ${passToRun} Meta] Skipping ${calc.name} for ${dateStr}. Missing computed deps: [${missingDeps.join(', ')}]`);
166
- skipped.add(calc.name);
167
- return false;
168
- }
169
- }
103
+ if (!canRun) {logger.log('INFO', `[Pass ${passToRun}] Skipping ${calc.name} for ${dateStr}. Data missing for this date: [${missingRoot.join(', ')}]`);skipped.add(calc.name); return false;}
170
104
 
171
- // If it passed all checks, run it.
105
+ if (calc.type === 'meta') { const missingDeps = (calc.dependencies || []).map(normalizeName).filter(d => !existingResults[d]); if (missingDeps.length > 0) { logger.log('WARN', `[Pass ${passToRun} Meta] Skipping ${calc.name} for ${dateStr}. Missing computed deps: [${missingDeps.join(', ')}]`); skipped.add(calc.name); return false;} }
172
106
  return true;
173
107
  };
174
-
175
108
  const standardCalcsToRun = standardCalcs.filter(filterCalc);
176
109
  const metaCalcsToRun = metaCalcs.filter(filterCalc);
177
-
178
110
  return { standardCalcsToRun, metaCalcsToRun };
179
111
  }
180
112
 
@@ -183,141 +115,164 @@ function filterCalculations(standardCalcs, metaCalcs, rootDataStatus, existingRe
183
115
  function initializeCalculators(calcs, logger) { const state = {}; for (const c of calcs) { const name=normalizeName(c.name), Cl=c.class; if(typeof Cl==='function') try { const inst=new Cl(); inst.manifest=c; state[name]=inst; } catch(e){logger.warn(`Init failed ${name}`,{errorMessage:e.message}); state[name]=null;} else {logger.warn(`Class missing ${name}`); state[name]=null;} } return state; }
184
116
 
185
117
  /** * Stage 7: Load historical data required for calculations
186
- * --- THIS FUNCTION IS NOW FIXED ---
187
118
  */
119
+ // --- MODIFIED: Stage 7: Load ONLY non-streaming historical data ---
188
120
  async function loadHistoricalData(date, calcs, config, deps, rootData) {
189
- const { logger } = deps; // <--- THIS WAS THE MISSING LINE
190
- const updated = {...rootData}, dStr=date.toISOString().slice(0,10); const tasks = [];
191
-
192
- const needsYesterdayPortfolio = calcs.some(c => c.isHistorical && c.rootDataDependencies.includes('portfolio'));
193
- const needsTodayHistory = calcs.some(c => c.rootDataDependencies.includes('history'));
194
- const needsYesterdayHistory = calcs.some(c => c.isHistorical && c.rootDataDependencies.includes('history'));
195
- const needsYesterdayInsights = calcs.some(c => c.isHistorical && c.rootDataDependencies.includes('insights'));
196
- const needsYesterdaySocial = calcs.some(c => c.isHistorical && c.rootDataDependencies.includes('social'));
121
+ const { logger } = deps;
122
+ const updated = {...rootData};
123
+ const tasks = [];
197
124
 
198
- // --- MODIFIED: Be smarter about loading data ---
199
- if(needsYesterdayPortfolio) {
200
- tasks.push((async()=>{ const prev=new Date(date); prev.setUTCDate(prev.getUTCDate()-1); const prevStr=prev.toISOString().slice(0,10);
201
- logger.log('INFO', `[PassRunner] Loading YESTERDAY portfolio data for ${prevStr}`);
202
- updated.yesterdayPortfolios=await loadFullDayMap(config,deps,await getPortfolioPartRefs(config,deps,prevStr));
203
- })());
204
- }
205
- if(needsTodayHistory) {
206
- tasks.push((async()=>{
207
- logger.log('INFO', `[PassRunner] Loading TODAY history data for ${dStr}`);
208
- updated.todayHistoryData=await loadFullDayMap(config,deps,rootData.historyRefs);
209
- })());
210
- }
211
- if(needsYesterdayHistory) {
212
- tasks.push((async()=>{ const prev=new Date(date); prev.setUTCDate(prev.getUTCDate()-1); const prevStr=prev.toISOString().slice(0,10);
213
- logger.log('INFO', `[PassRunner] Loading YESTERDAY history data for ${prevStr}`);
214
- updated.yesterdayHistoryData=await loadFullDayMap(config,deps,await getHistoryPartRefs(config,deps,prevStr));
215
- })());
216
- }
125
+ // --- REMOVED: needsYesterdayPortfolio ---
126
+ // --- REMOVED: needsTodayHistory ---
127
+ // --- REMOVED: needsYesterdayHistory ---
128
+ const needsYesterdayInsights = calcs.some(c => c.isHistorical && c.rootDataDependencies.includes('insights'));
129
+ const needsYesterdaySocial = calcs.some(c => c.isHistorical && c.rootDataDependencies.includes('social'));
130
+
131
+ // --- REMOVED: All async tasks for portfolio and history data ---
132
+
217
133
  if(needsYesterdayInsights) {
218
134
  tasks.push((async()=>{ const prev=new Date(date); prev.setUTCDate(prev.getUTCDate()-1); const prevStr=prev.toISOString().slice(0,10);
219
- logger.log('INFO', `[PassRunner] Loading YESTERDAY insights data for ${prevStr}`);
220
- updated.yesterdayInsights=await loadDailyInsights(config,deps,prevStr);
221
- })());
222
- }
135
+ logger.log('INFO', `[PassRunner] Loading YESTERDAY insights data for ${prevStr}`);
136
+ updated.yesterdayInsights=await loadDailyInsights(config,deps,prevStr); })());}
223
137
  if(needsYesterdaySocial) {
224
138
  tasks.push((async()=>{ const prev=new Date(date); prev.setUTCDate(prev.getUTCDate()-1); const prevStr=prev.toISOString().slice(0,10);
225
- logger.log('INFO', `[PassRunner] Loading YESTERDAY social data for ${prevStr}`);
226
- updated.yesterdaySocialPostInsights=await loadDailySocialPostInsights(config,deps,prevStr);
227
- })());
228
- }
229
-
230
- await Promise.all(tasks); return updated;
139
+ logger.log('INFO', `[PassRunner] Loading YESTERDAY social data for ${prevStr}`);
140
+ updated.yesterdaySocialPostInsights=await loadDailySocialPostInsights(config,deps,prevStr); })());}
141
+
142
+ await Promise.all(tasks);
143
+ return updated; // This no longer contains the large data maps
231
144
  }
232
145
 
233
-
234
- /** * --- MODIFIED: Stage 8: Stream and process data for standard calculations ---
235
- * This function now uses an async generator to stream portfolio data
236
- * 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.
237
153
  */
238
154
  async function streamAndProcess(dateStr, state, passName, config, deps, rootData) {
239
155
  const { logger, calculationUtils } = deps;
240
- // --- MODIFIED: yesterdayInsights/Social are now loaded by loadHistoricalData ---
241
- // --- THIS WILL NOW WORK, as rootData contains 'todayInsights' from Stage 3 ---
242
- const { todayInsights, yesterdayInsights, todaySocialPostInsights, yesterdaySocialPostInsights, todayHistoryData, yesterdayHistoryData, yesterdayPortfolios } = rootData;
243
156
 
244
- // --- NEW: Check if streaming is even needed ---
245
- const calcsThatStreamPortfolio = Object.values(state).filter(calc => calc && calc.manifest && (calc.manifest.rootDataDependencies.includes('portfolio') || calc.manifest.category === 'speculators'));
157
+ // --- MODIFIED: yesterdayPortfolios & todayHistoryData are no longer in rootData ---
158
+ const { todayInsights, yesterdayInsights, todaySocialPostInsights, yesterdaySocialPostInsights } = rootData;
246
159
 
160
+ const calcsThatStreamPortfolio = Object.values(state).filter(calc => calc && calc.manifest && (calc.manifest.rootDataDependencies.includes('portfolio') || calc.manifest.category === 'speculators'));
247
161
  const context={instrumentMappings:(await calculationUtils.loadInstrumentMappings()).instrumentToTicker, sectorMapping:(await calculationUtils.loadInstrumentMappings()).instrumentToSector, todayDateStr:dateStr, dependencies:deps, config};
248
-
249
- // --- MODIFIED: Run non-streaming calcs first (social/insights) ---
250
- // This allows them to run even if portfolio data is missing
251
- let firstUser=true; // Used to run them only once
162
+ let firstUser=true;
163
+
164
+ // --- (Non-streaming (insights/social) calculation logic remains unchanged) ---
252
165
  for(const name in state){
253
166
  const calc=state[name]; if(!calc||typeof calc.process!=='function') continue;
254
167
  const cat=calc.manifest.category;
255
168
  if(cat==='socialPosts'||cat==='insights') {
256
169
  if (firstUser) {
257
170
  logger.log('INFO', `[${passName}] Running non-streaming calc: ${name}`);
258
- // --- This 'args' array will now receive the correct data ---
259
- let args=[null,null,null,{...context, userType: 'n/a'},todayInsights,yesterdayInsights,todaySocialPostInsights,yesterdaySocialPostInsights,todayHistoryData,yesterdayHistoryData];
260
- // Pass historical data if needed
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];
261
173
  if(calc.manifest.isHistorical) {
262
- 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];
263
175
  }
264
176
  try{ await Promise.resolve(calc.process(...args)); } catch(e){logger.log('WARN',`Process error ${name} (non-stream)`,{err:e.message});}
265
177
  }
266
178
  }
267
179
  }
268
-
180
+ // --- (End of non-streaming calc logic) ---
269
181
 
270
182
  if (calcsThatStreamPortfolio.length === 0) {
271
183
  logger.log('INFO', `[${passName}] No portfolio-streaming calcs to run for ${dateStr}. Skipping stream.`);
272
- return; // Exit stream function
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.`);
273
213
  }
274
214
 
275
- logger.log('INFO', `[${passName}] Streaming portfolio data for ${calcsThatStreamPortfolio.length} calcs...`);
276
-
215
+ // --- MODIFIED: Main streaming loop (driven by TODAY's portfolio stream) ---
277
216
  for await (const chunk of streamPortfolioData(config, deps, dateStr)) {
278
- 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;
279
229
  const userType=p.PublicPositions?'speculator':'normal';
280
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
+
281
237
  for(const name in state){
282
238
  const calc=state[name]; if(!calc||typeof calc.process!=='function') continue;
283
239
  const cat=calc.manifest.category, isSocialOrInsights=cat==='socialPosts'||cat==='insights', isHistorical=calc.manifest.isHistorical, isSpec=cat==='speculators';
284
240
 
285
- // --- MODIFIED: Skip social/insights here, they ran above ---
286
- if(isSocialOrInsights) continue;
287
-
288
- // --- This 'args' array will now receive the correct data ---
289
- let args=[p,null,uid,context,todayInsights,yesterdayInsights,todaySocialPostInsights,yesterdaySocialPostInsights,todayHistoryData,yesterdayHistoryData];
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];
290
245
 
291
246
  if(isHistorical){
292
- const pY=yesterdayPortfolios ? yesterdayPortfolios[uid] : null; // Check if yesterdayPortfolios exists
293
- // V3 behavioural calcs (like history aggregator) *can* run without pY
247
+ // pY is now the streamed yesterday's portfolio for this user
294
248
  if(!pY && (cat !== 'behavioural' && name !== 'historical-performance-aggregator')) continue;
295
- args=[p,pY,uid,context,todayInsights,yesterdayInsights,todaySocialPostInsights,yesterdaySocialPostInsights,todayHistoryData,yesterdayHistoryData];
249
+ args=[p,pY,uid,context,todayInsights,yesterdayInsights,todaySocialPostInsights,yesterdaySocialPostInsights,hT,null];
296
250
  }
251
+
297
252
  if((userType==='normal'&&isSpec)||(userType==='speculator'&&!isSpec&&name!=='users-processed')) continue;
298
- 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
+ }
299
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]; }
300
261
  }
301
262
  }
263
+ logger.log('INFO', `[${passName}] Finished streaming data for ${dateStr}.`);
302
264
  }
303
265
 
304
266
  /** Stage 9: Run standard computations */
305
267
  async function runStandardComputationPass(date, calcs, passName, config, deps, rootData) {
306
268
  const dStr = date.toISOString().slice(0, 10), logger = deps.logger;
307
- // --- THIS IS THE CRITICAL CHANGE ---
308
- // If 'calcs' is empty *because of the new filter*, this log won't even appear.
309
269
  if (calcs.length === 0) {
310
270
  logger.log('INFO', `[${passName}] No standard calcs to run for ${dStr} after filtering.`);
311
- return;
312
- }
313
- // This log now only appears if there is *actually* work to do.
271
+ return; }
314
272
  logger.log('INFO', `[${passName}] Running ${dStr} with ${calcs.length} calcs.`);
315
-
316
273
  const fullRoot = await loadHistoricalData(date, calcs, config, deps, rootData);
317
274
  const state = initializeCalculators(calcs, logger);
318
-
319
275
  await streamAndProcess(dStr, state, passName, config, deps, fullRoot);
320
-
321
276
  let success = 0;
322
277
  const standardWrites = [];
323
278
  const shardedWrites = {};
@@ -354,8 +309,8 @@ async function runStandardComputationPass(date, calcs, passName, config, deps, r
354
309
  if (standardWrites.length > 0) {
355
310
  await commitBatchInChunks(config, deps, standardWrites, `${passName} Standard ${dStr}`);
356
311
  }
357
- for (const docPath in shardedWrites) { // 'docPath' is the key, e.g., 'user_profile_history_shard_0' or 'social_.../history'
358
- const docData = shardedWrites[docPath]; // 'docData' is the object to write, e.g., { profiles: ... }
312
+ for (const docPath in shardedWrites) {
313
+ const docData = shardedWrites[docPath];
359
314
  const shardedDocWrites = [];
360
315
  let docRef;
361
316
  if (docPath.includes('/')) {
@@ -381,17 +336,12 @@ async function runStandardComputationPass(date, calcs, passName, config, deps, r
381
336
  /** Stage 10: Run meta computations */
382
337
  async function runMetaComputationPass(date, calcs, passName, config, deps, fetchedDeps, rootData) {
383
338
  const dStr = date.toISOString().slice(0, 10), logger = deps.logger;
384
- // --- THIS IS THE CRITICAL CHANGE ---
385
339
  if (calcs.length === 0) {
386
340
  logger.log('INFO', `[${passName}] No meta calcs to run for ${dStr} after filtering.`);
387
341
  return;
388
342
  }
389
343
  logger.log('INFO', `[${passName}] Running ${dStr} with ${calcs.length} calcs.`);
390
-
391
- // --- NEW: Load historical data for meta calcs if needed ---
392
- // (This might be redundant if standard pass ran, but meta-calcs can run standalone)
393
344
  const fullRoot = await loadHistoricalData(date, calcs, config, deps, rootData);
394
-
395
345
  let success = 0;
396
346
  const standardWrites = [];
397
347
  const shardedWrites = {};
@@ -400,7 +350,6 @@ async function runMetaComputationPass(date, calcs, passName, config, deps, fetch
400
350
  if (typeof Cl !== 'function') {logger.log('ERROR', `Invalid class ${name}`);continue;}
401
351
  const inst = new Cl();
402
352
  try {
403
- // --- MODIFIED: Pass fullRoot to meta calcs ---
404
353
  const result = await Promise.resolve(inst.process(dStr, { ...deps, rootData: fullRoot }, config, fetchedDeps));
405
354
  if (result && Object.keys(result).length > 0) {const standardResult = {}; for (const key in result) {
406
355
  if (key.startsWith('sharded_')) {const shardedData = result[key];for (const collectionName in shardedData)
@@ -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.144",
3
+ "version": "1.0.146",
4
4
  "description": "Helper Functions for Bulltrackers.",
5
5
  "main": "index.js",
6
6
  "files": [