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
|
-
|
|
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');
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 }
|
|
190
|
-
const updated
|
|
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
|
-
// ---
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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
|
-
|
|
220
|
-
|
|
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
|
-
|
|
226
|
-
|
|
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
|
-
|
|
235
|
-
* This function now
|
|
236
|
-
*
|
|
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
|
-
// ---
|
|
245
|
-
const
|
|
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
|
-
|
|
250
|
-
//
|
|
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
|
-
//
|
|
259
|
-
let args=[null,null,null,{...context, userType: 'n/a'},todayInsights,yesterdayInsights,todaySocialPostInsights,yesterdaySocialPostInsights,
|
|
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,
|
|
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;
|
|
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
|
-
|
|
276
|
-
|
|
215
|
+
// --- MODIFIED: Main streaming loop (driven by TODAY's portfolio stream) ---
|
|
277
216
|
for await (const chunk of streamPortfolioData(config, deps, dateStr)) {
|
|
278
|
-
|
|
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
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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) {
|
|
358
|
-
const docData = shardedWrites[docPath];
|
|
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
|
};
|