bulltrackers-module 1.0.139 → 1.0.141

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,125 +1,210 @@
1
1
  /**
2
- * @fileoverview
3
- * Main "Pass Runner" for the V2 Computation System.
4
- *
5
- * This orchestrator is designed to be run by a separate Cloud Function for each "pass".
6
- * It reads its pass number from the config and executes only those calculations.
7
- * This file contains the high-level "manual" of steps. The "how-to" logic
8
- * is extracted into 'computation_system_utils.js'.
9
- * --- MODIFIED: To use getEarliestDataDates and pass the date map to the orchestrator helpers. ---
10
- * --- MODIFIED: To run date processing in parallel batches. ---
11
- * --- MODIFIED: To fetch ALL existing results to enable incremental (skip) logic. ---
2
+ * FIXED: computation_pass_runner.js
3
+ * Now calculates earliest date PER CALCULATION, not per pass
12
4
  */
13
5
 
14
- // --- MODIFIED: Renamed fetchDependenciesForPass to fetchExistingResults ---
15
6
  const { groupByPass, checkRootDataAvailability, fetchExistingResults, filterCalculations, runStandardComputationPass, runMetaComputationPass } = require('./orchestration_helpers.js');
16
- // --- MODIFIED: Import getEarliestDataDates ---
17
7
  const { getExpectedDateStrings, getEarliestDataDates } = require('../utils/utils.js');
18
8
 
19
- // --- NEW: Parallel processing batch size ---
20
- const PARALLEL_BATCH_SIZE = 7; // Process a week at a time
9
+ const PARALLEL_BATCH_SIZE = 7;
21
10
 
22
11
  async function runComputationPass(config, dependencies, computationManifest) {
23
12
  const { logger } = dependencies;
24
- const passToRun = String(config.COMPUTATION_PASS_TO_RUN); if (!passToRun) return logger.log('ERROR', '[PassRunner] No pass defined. Aborting.');
13
+ const passToRun = String(config.COMPUTATION_PASS_TO_RUN);
14
+ if (!passToRun) return logger.log('ERROR', '[PassRunner] No pass defined. Aborting.');
15
+
25
16
  logger.log('INFO', `🚀 Starting PASS ${passToRun}...`);
26
- const yesterday = new Date(); yesterday.setUTCDate(yesterday.getUTCDate()-1);
17
+
18
+ const yesterday = new Date();
19
+ yesterday.setUTCDate(yesterday.getUTCDate() - 1);
27
20
  const endDateUTC = new Date(Date.UTC(yesterday.getUTCFullYear(), yesterday.getUTCMonth(), yesterday.getUTCDate()));
28
21
 
29
- // const earliestDates = await getEarliestDataDates(config, dependencies); // <-- 1. COMMENT OUT OR REMOVE THIS BROKEN CALL
30
-
31
- // --- START: 2. NEW HARDCODED DATES (V2 - CORRECTED) ---
22
+ // Hardcoded earliest dates
32
23
  logger.log('INFO', 'Using hardcoded earliest data dates to bypass faulty discovery.');
33
-
34
- // Use the exact dates you provided:
35
- const earliestPortfolio = new Date('2025-09-25T00:00:00Z'); // Earliest of Normal (09-25) and Spec (09-29)
36
- const earliestHistory = new Date('2025-11-05T00:00:00Z'); // Both Normal and Spec history
37
- const earliestSocial = new Date('2025-10-30T00:00:00Z'); // daily_social_insights
38
- const earliestInsights = new Date('2025-08-26T00:00:00Z'); // daily_instrument_insights (Your new date)
24
+ const earliestPortfolio = new Date('2025-09-25T00:00:00Z');
25
+ const earliestHistory = new Date('2025-11-05T00:00:00Z');
26
+ const earliestSocial = new Date('2025-10-30T00:00:00Z');
27
+ const earliestInsights = new Date('2025-08-26T00:00:00Z');
39
28
 
40
29
  const earliestDates = {
41
30
  portfolio: earliestPortfolio,
42
31
  history: earliestHistory,
43
32
  social: earliestSocial,
44
- insights: earliestInsights, // <-- Use the real date
45
- // Calculate absoluteEarliest based on all real data
33
+ insights: earliestInsights,
46
34
  absoluteEarliest: [earliestPortfolio, earliestHistory, earliestSocial, earliestInsights].reduce((a, b) => a < b ? a : b)
47
35
  };
48
36
 
49
37
  logger.log('INFO', `Hardcoded map: portfolio=${earliestDates.portfolio.toISOString().slice(0,10)}, history=${earliestDates.history.toISOString().slice(0,10)}, social=${earliestDates.social.toISOString().slice(0,10)}, insights=${earliestDates.insights.toISOString().slice(0,10)}`);
50
- // --- END: NEW HARDCODED DATES ---
51
-
52
-
38
+
53
39
  const passes = groupByPass(computationManifest);
54
- const calcsInThisPass = passes[passToRun] || []; if (!calcsInThisPass.length) return logger.log('WARN', `[PassRunner] No calcs for Pass ${passToRun}. Exiting.`);
55
-
56
- // --- 3. THIS LOGIC BLOCK (from our last fix) IS NOW CORRECT ---
57
- // It will consume the clean, hardcoded 'earliestDates' map and work as intended.
58
- const requiredRootData = new Set();
59
- calcsInThisPass.forEach(c => {
60
- (c.rootDataDependencies || []).forEach(dep => requiredRootData.add(dep));
61
- });
62
-
63
- let earliestStartDateForPass = null;
64
- if (requiredRootData.size > 0) {
65
- let latestOfEarliestDates = new Date(0); // Start at epoch
66
- const farFutureSentinelYear = 2999; // Keep as a safety check for any future issues
67
- let hasAtLeastOneValidDate = false;
68
-
69
- requiredRootData.forEach(dep => {
70
- const earliestDateForDep = earliestDates[dep];
71
-
72
- // This check is now just a safeguard
73
- if (earliestDateForDep && earliestDateForDep.getUTCFullYear() < farFutureSentinelYear) {
74
- if (earliestDateForDep > latestOfEarliestDates) {
75
- latestOfEarliestDates = earliestDateForDep;
76
- }
77
- hasAtLeastOneValidDate = true;
78
- } else if (earliestDateForDep) {
79
- logger.log('INFO', `[PassRunner] Dependency '${dep}' is not available (date: ${earliestDateForDep.toISOString().slice(0, 10)}). Calcs requiring it will be skipped.`);
80
- } else {
81
- logger.log('WARN', `[PassRunner] Dependency '${dep}' has no earliest date defined in hardcoded map.`);
40
+ const calcsInThisPass = passes[passToRun] || [];
41
+ if (!calcsInThisPass.length) return logger.log('WARN', `[PassRunner] No calcs for Pass ${passToRun}. Exiting.`);
42
+
43
+ // ============================================
44
+ // NEW: Calculate earliest date PER CALCULATION
45
+ // ============================================
46
+ const calcEarliestDates = new Map();
47
+
48
+ for (const calc of calcsInThisPass) {
49
+ const deps = calc.rootDataDependencies || [];
50
+
51
+ if (deps.length === 0) {
52
+ // No dependencies = can run from the absolute earliest
53
+ calcEarliestDates.set(calc.name, earliestDates.absoluteEarliest);
54
+ continue;
55
+ }
56
+
57
+ // Find the LATEST earliest date among THIS calculation's dependencies
58
+ let latestEarliest = new Date(0);
59
+ for (const dep of deps) {
60
+ const depDate = earliestDates[dep];
61
+ if (depDate && depDate > latestEarliest) {
62
+ latestEarliest = depDate;
82
63
  }
83
- });
64
+ }
84
65
 
85
- if (hasAtLeastOneValidDate) {
86
- earliestStartDateForPass = latestOfEarliestDates;
66
+ // If this is a historical calculation, add 1 day (needs yesterday)
67
+ if (calc.isHistorical) {
68
+ const adjusted = new Date(latestEarliest);
69
+ adjusted.setUTCDate(adjusted.getUTCDate() + 1);
70
+ calcEarliestDates.set(calc.name, adjusted);
71
+ logger.log('TRACE', `[PassRunner] ${calc.name}: earliest=${adjusted.toISOString().slice(0,10)} (historical, needs ${latestEarliest.toISOString().slice(0,10)} + 1 day)`);
72
+ } else {
73
+ calcEarliestDates.set(calc.name, latestEarliest);
74
+ logger.log('TRACE', `[PassRunner] ${calc.name}: earliest=${latestEarliest.toISOString().slice(0,10)}`);
87
75
  }
88
76
  }
89
-
90
- // Use the pass-specific date. Fall back to absolute earliest, then config.
91
- const firstDate = earliestStartDateForPass || earliestDates.absoluteEarliest;
92
- const startDateUTC = firstDate
93
- ? new Date(Date.UTC(firstDate.getUTCFullYear(), firstDate.getUTCMonth(), firstDate.getUTCDate()))
94
- : new Date(config.earliestComputationDate+'T00:00:00Z');
95
-
96
- logger.log('INFO', `[PassRunner] Pass ${passToRun} requires data: [${Array.from(requiredRootData).join(', ')}].`);
97
- logger.log('INFO', `[PassRunner] Determined start date for this pass: ${startDateUTC.toISOString().slice(0, 10)}`);
98
- // --- END OF LOGIC BLOCK ---
99
-
77
+
78
+ // The pass can start from the EARLIEST calculation's start date
79
+ const passEarliestDate = new Date(Math.min(...Array.from(calcEarliestDates.values()).map(d => d.getTime())));
80
+
81
+ logger.log('INFO', `[PassRunner] Pass ${passToRun} analysis:`);
82
+ logger.log('INFO', ` Total calculations: ${calcsInThisPass.length}`);
83
+ logger.log('INFO', ` Pass can start from: ${passEarliestDate.toISOString().slice(0,10)}`);
84
+ logger.log('INFO', ` Individual calculation date ranges calculated.`);
85
+
86
+ // ============================================
87
+ // Generate date range from earliest to yesterday
88
+ // ============================================
89
+ const startDateUTC = new Date(Date.UTC(passEarliestDate.getUTCFullYear(), passEarliestDate.getUTCMonth(), passEarliestDate.getUTCDate()));
100
90
  const allExpectedDates = getExpectedDateStrings(startDateUTC, endDateUTC);
101
- const firstDayOfBackfill = allExpectedDates.length > 0 ? allExpectedDates[0] : null; // --- MOVED FROM ABOVE ---
102
91
 
103
- const standardCalcs = calcsInThisPass.filter(c => c.type==='standard');
104
- const metaCalcs = calcsInThisPass.filter(c => c.type==='meta');
92
+ logger.log('INFO', `[PassRunner] Processing ${allExpectedDates.length} total dates from ${allExpectedDates[0]} to ${allExpectedDates[allExpectedDates.length-1]}`);
93
+
94
+ const standardCalcs = calcsInThisPass.filter(c => c.type === 'standard');
95
+ const metaCalcs = calcsInThisPass.filter(c => c.type === 'meta');
96
+
97
+ // ============================================
98
+ // Process each date
99
+ // ============================================
105
100
  const processDate = async (dateStr) => {
106
- const dateToProcess = new Date(dateStr+'T00:00:00Z');
101
+ const dateToProcess = new Date(dateStr + 'T00:00:00Z');
102
+
107
103
  try {
104
+ // Check root data availability
108
105
  const rootData = await checkRootDataAvailability(dateStr, config, dependencies, earliestDates);
109
- if (!rootData) { logger.log('WARN', `[PassRunner] Skipping ${dateStr} for Pass ${passToRun}: No root data.`);return;}
106
+ if (!rootData) {
107
+ logger.log('WARN', `[PassRunner] Skipping ${dateStr} for Pass ${passToRun}: No root data.`);
108
+ return;
109
+ }
110
+
111
+ // Fetch existing results
110
112
  const existingResults = await fetchExistingResults(dateStr, calcsInThisPass, computationManifest, config, dependencies);
111
- const { standardCalcsToRun, metaCalcsToRun } = filterCalculations(standardCalcs, metaCalcs, rootData.status, existingResults, passToRun, dateStr, logger,dateStr === firstDayOfBackfill );
112
- if (standardCalcsToRun.length === 0 && metaCalcsToRun.length === 0) {logger.log('INFO', `[PassRunner] All calcs for ${dateStr} Pass ${passToRun} are already complete. Skipping.`);return;}
113
- if (standardCalcsToRun.length) await runStandardComputationPass(dateToProcess, standardCalcsToRun, `Pass ${passToRun} (Standard)`, config, dependencies, rootData);
114
- if (metaCalcsToRun.length) await runMetaComputationPass(dateToProcess, metaCalcsToRun, `Pass ${passToRun} (Meta)`, config, dependencies, existingResults, rootData);
115
- } catch (err) {logger.log('ERROR', `[PassRunner] FAILED Pass ${passToRun} for ${dateStr}`, { errorMessage: err.message, stack: err.stack });}
113
+
114
+ // ============================================
115
+ // NEW: Filter based on per-calculation earliest dates
116
+ // ============================================
117
+ const standardCalcsToRun = standardCalcs.filter(c => {
118
+ // Skip if already exists
119
+ if (existingResults[c.name]) {
120
+ logger.log('TRACE', `[Pass ${passToRun}] Skipping ${c.name} for ${dateStr}. Result already exists.`);
121
+ return false;
122
+ }
123
+
124
+ // Skip if date is before this calculation's earliest date
125
+ const calcEarliestDate = calcEarliestDates.get(c.name);
126
+ if (calcEarliestDate && dateToProcess < calcEarliestDate) {
127
+ logger.log('TRACE', `[Pass ${passToRun}] Skipping ${c.name} for ${dateStr}. Date before calc's earliest (${calcEarliestDate.toISOString().slice(0,10)}).`);
128
+ return false;
129
+ }
130
+
131
+ // Check root data dependencies
132
+ const missingDeps = (c.rootDataDependencies || []).filter(dep => {
133
+ if (dep === 'portfolio') return !rootData.status.hasPortfolio;
134
+ if (dep === 'insights') return !rootData.status.hasInsights;
135
+ if (dep === 'social') return !rootData.status.hasSocial;
136
+ if (dep === 'history') return !rootData.status.hasHistory;
137
+ return false;
138
+ });
139
+
140
+ if (missingDeps.length > 0) {
141
+ logger.log('INFO', `[Pass ${passToRun}] Skipping ${c.name} for ${dateStr}. Missing deps: ${missingDeps.join(', ')}`);
142
+ return false;
143
+ }
144
+
145
+ return true;
146
+ });
147
+
148
+ const metaCalcsToRun = metaCalcs.filter(c => {
149
+ if (existingResults[c.name]) return false;
150
+
151
+ const calcEarliestDate = calcEarliestDates.get(c.name);
152
+ if (calcEarliestDate && dateToProcess < calcEarliestDate) {
153
+ logger.log('TRACE', `[Pass ${passToRun} Meta] Skipping ${c.name} for ${dateStr}. Date before calc's earliest.`);
154
+ return false;
155
+ }
156
+
157
+ // Check root dependencies
158
+ const missingRootDeps = (c.rootDataDependencies || []).filter(dep => {
159
+ if (dep === 'portfolio') return !rootData.status.hasPortfolio;
160
+ if (dep === 'insights') return !rootData.status.hasInsights;
161
+ if (dep === 'social') return !rootData.status.hasSocial;
162
+ if (dep === 'history') return !rootData.status.hasHistory;
163
+ return false;
164
+ });
165
+
166
+ if (missingRootDeps.length > 0) {
167
+ logger.log('INFO', `[Pass ${passToRun} Meta] Skipping ${c.name} for ${dateStr}. Missing root deps: ${missingRootDeps.join(', ')}`);
168
+ return false;
169
+ }
170
+
171
+ // Check computed dependencies
172
+ const missingComputedDeps = (c.dependencies || []).filter(d => !existingResults[d]);
173
+ if (missingComputedDeps.length > 0) {
174
+ logger.log('WARN', `[Pass ${passToRun} Meta] Skipping ${c.name} for ${dateStr}. Missing computed deps: ${missingComputedDeps.join(', ')}`);
175
+ return false;
176
+ }
177
+
178
+ return true;
179
+ });
180
+
181
+ if (standardCalcsToRun.length === 0 && metaCalcsToRun.length === 0) {
182
+ logger.log('INFO', `[PassRunner] All eligible calcs for ${dateStr} Pass ${passToRun} are already complete. Skipping.`);
183
+ return;
184
+ }
185
+
186
+ logger.log('INFO', `[PassRunner] Running ${dateStr}: ${standardCalcsToRun.length} standard, ${metaCalcsToRun.length} meta`);
187
+
188
+ if (standardCalcsToRun.length) {
189
+ await runStandardComputationPass(dateToProcess, standardCalcsToRun, `Pass ${passToRun} (Standard)`, config, dependencies, rootData);
190
+ }
191
+ if (metaCalcsToRun.length) {
192
+ await runMetaComputationPass(dateToProcess, metaCalcsToRun, `Pass ${passToRun} (Meta)`, config, dependencies, existingResults, rootData);
193
+ }
194
+
195
+ } catch (err) {
196
+ logger.log('ERROR', `[PassRunner] FAILED Pass ${passToRun} for ${dateStr}`, { errorMessage: err.message, stack: err.stack });
197
+ }
116
198
  };
199
+
200
+ // Process in batches
117
201
  logger.log('INFO', `[PassRunner] Processing ${allExpectedDates.length} total dates in batches of ${PARALLEL_BATCH_SIZE}...`);
118
202
  for (let i = 0; i < allExpectedDates.length; i += PARALLEL_BATCH_SIZE) {
119
203
  const batch = allExpectedDates.slice(i, i + PARALLEL_BATCH_SIZE);
120
204
  logger.log('INFO', `[PassRunner] Processing batch ${Math.floor(i / PARALLEL_BATCH_SIZE) + 1}/${Math.ceil(allExpectedDates.length / PARALLEL_BATCH_SIZE)} (Dates: ${batch[0]}...${batch[batch.length-1]})`);
121
205
  await Promise.all(batch.map(dateStr => processDate(dateStr)));
122
206
  }
207
+
123
208
  logger.log('INFO', `[PassRunner] Pass ${passToRun} orchestration finished.`);
124
209
  }
125
210
 
@@ -7,7 +7,8 @@ const { normalizeName, commitBatchInChunks } = require('../utils/utils.js');
7
7
  function groupByPass(manifest) { return manifest.reduce((acc, calc) => { (acc[calc.pass] = acc[calc.pass] || []).push(calc); return acc; }, {}); }
8
8
 
9
9
  /** * --- MODIFIED: Returns detailed missing dependencies for logging ---
10
- * Stage 2: Check root data dependencies for a calc
10
+ * Stage 2: Check root data dependencies for a calc
11
+ * --- THIS FUNCTION IS NOW MORE GRANULAR ---
11
12
  */
12
13
  function checkRootDependencies(calcManifest, rootDataStatus) {
13
14
  const missing = [];
@@ -23,7 +24,7 @@ function checkRootDependencies(calcManifest, rootDataStatus) {
23
24
  }
24
25
 
25
26
  /** * --- MODIFIED: Uses earliestDates map to avoid unnecessary queries ---
26
- * Stage 3: Check root data availability for a date
27
+ * Stage 3: Check root data availability for a date
27
28
  */
28
29
  async function checkRootDataAvailability(dateStr, config, dependencies, earliestDates) {
29
30
  const { logger } = dependencies;
@@ -33,7 +34,9 @@ async function checkRootDataAvailability(dateStr, config, dependencies, earliest
33
34
  let hasPortfolio = false, hasInsights = false, hasSocial = false, hasHistory = false;
34
35
  try {
35
36
  const tasks = [];
36
- if (dateToProcess >= earliestDates.portfolio)
37
+ // This logic is correct. It *avoids* calling get...Refs
38
+ // if the dateToProcess is before the earliest known data.
39
+ if (dateToProcess >= earliestDates.portfolio)
37
40
  {tasks.push(getPortfolioPartRefs(config, dependencies, dateStr).then(res => {portfolioRefs = res;hasPortfolio = !!(res?.length);}));}
38
41
  if (dateToProcess >= earliestDates.insights) {
39
42
  tasks.push(loadDailyInsights(config, dependencies, dateStr).then(res => {insightsData = res;hasInsights = !!res;}));}
@@ -41,8 +44,17 @@ async function checkRootDataAvailability(dateStr, config, dependencies, earliest
41
44
  tasks.push(loadDailySocialPostInsights(config, dependencies, dateStr).then(res => {socialData = res;hasSocial = !!res;}));}
42
45
  if (dateToProcess >= earliestDates.history) {
43
46
  tasks.push(getHistoryPartRefs(config, dependencies, dateStr).then(res => {historyRefs = res;hasHistory = !!(res?.length);}));}
47
+
44
48
  await Promise.all(tasks);
45
- if (!(hasPortfolio || hasInsights || hasSocial || hasHistory)) {logger.log('WARN', `[PassRunner] No root data for ${dateStr}.`); return null;}
49
+
50
+ // --- NEW: Log what was *actually* found ---
51
+ logger.log('INFO', `[PassRunner] Data availability for ${dateStr}: P:${hasPortfolio}, I:${hasInsights}, S:${hasSocial}, H:${hasHistory}`);
52
+
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
+ }
46
58
  return {portfolioRefs, insightsData,socialData,historyRefs,status: { hasPortfolio, hasInsights, hasSocial, hasHistory }};
47
59
  } catch (err) { logger.log('ERROR', `[PassRunner] Error checking data for ${dateStr}`, { errorMessage: err.message }); return null; }
48
60
  }
@@ -70,34 +82,85 @@ async function fetchExistingResults(dateStr, calcsInPass, fullManifest, config,
70
82
  return fetched;
71
83
  }
72
84
 
73
- /** --- MODIFIED: Stage 5: Filter calculations to skip completed work ---
85
+ /**
86
+ * --- ENTIRELY REWRITTEN: Stage 5: Filter calculations ---
87
+ * This function now implements your "even better design".
88
+ * It calculates the *true earliest run date* for every calculation
89
+ * and filters them out *before* the "Running..." log ever appears.
74
90
  */
75
- function filterCalculations(standardCalcs, metaCalcs, rootDataStatus, existingResults, passToRun, dateStr, logger, isFirstDayOfBackfill = false) {
91
+ function filterCalculations(standardCalcs, metaCalcs, rootDataStatus, existingResults, passToRun, dateStr, earliestDates, logger) {
76
92
  const skipped = new Set();
77
- const standardCalcsToRun = standardCalcs.filter(c => {
78
- if (existingResults[c.name]) {logger.log('TRACE', `[Pass ${passToRun}] Skipping ${c.name} for ${dateStr}. Result already exists.`);return false;}
79
- if (isFirstDayOfBackfill && c.isHistorical) {
80
- logger.log('INFO', `[Pass ${passToRun}] Skipping ${c.name} for ${dateStr}. Historical calc on the first day of backfill (no yesterday).`);
81
- skipped.add(c.name);
93
+ const dateToProcess = new Date(dateStr + 'T00:00:00Z');
94
+
95
+ /**
96
+ * --- CORRECTED LOGIC ---
97
+ * Helper to get the true earliest date a calc can run.
98
+ */
99
+ const getTrueEarliestRunDate = (calc) => {
100
+ let earliestRunDate = new Date('1970-01-01T00:00:00Z'); // Start at zero
101
+ const dependencies = calc.rootDataDependencies || [];
102
+
103
+ // 1. Find the LATEST "today" dependency
104
+ // This is the date where all *today* data is first available
105
+ for (const dep of dependencies) {
106
+ if (dep === 'portfolio' && earliestDates.portfolio > earliestRunDate) earliestRunDate = earliestDates.portfolio;
107
+ if (dep === 'history' && earliestDates.history > earliestRunDate) earliestRunDate = earliestDates.history;
108
+ if (dep === 'social' && earliestDates.social > earliestRunDate) earliestRunDate = earliestDates.social;
109
+ if (dep === 'insights' && earliestDates.insights > earliestRunDate) earliestRunDate = earliestDates.insights;
110
+ }
111
+
112
+ // 2. If the calc is historical, shift the *final* date by +1
113
+ // This universally applies the "+1" rule if *any* yesterday data is needed,
114
+ // (as long as we found a dependency in step 1).
115
+ if (calc.isHistorical && earliestRunDate.getTime() > 0) {
116
+ earliestRunDate.setUTCDate(earliestRunDate.getUTCDate() + 1);
117
+ }
118
+
119
+ return earliestRunDate;
120
+ };
121
+
122
+ const filterCalc = (calc) => {
123
+ // 1. Skip if result already exists
124
+ if (existingResults[calc.name]) {
125
+ logger.log('TRACE', `[Pass ${passToRun}] Skipping ${calc.name} for ${dateStr}. Result already exists.`);
126
+ skipped.add(calc.name);
82
127
  return false;
83
128
  }
84
- });
85
- // Filter Meta Calcs
86
- const metaCalcsToRun = metaCalcs.filter(c => {
87
- if (existingResults[c.name]) {logger.log('TRACE', `[Pass ${passToRun} Meta] Skipping ${c.name} for ${dateStr}. Result already exists.`);skipped.add(c.name);return false;}
88
- // --- START: RECOMMENDED ADDITION ---
89
- if (isFirstDayOfBackfill && c.isHistorical) {
90
- logger.log('INFO', `[Pass ${passToRun} Meta] Skipping ${c.name} for ${dateStr}. Historical calc on the first day of backfill (no yesterday).`);
91
- skipped.add(c.name);
129
+
130
+ // 2. Check *true* earliest run date
131
+ const earliestRunDate = getTrueEarliestRunDate(calc);
132
+ if (dateToProcess < earliestRunDate) {
133
+ logger.log('TRACE', `[Pass ${passToRun}] Skipping ${calc.name} for ${dateStr}. Date is before true earliest run date (${earliestRunDate.toISOString().slice(0, 10)}).`);
134
+ skipped.add(calc.name);
135
+ return false;
136
+ }
137
+
138
+ // 3. Check if *today's* root data was *actually* found
139
+ // This handles gaps *after* the earliest date
140
+ const { canRun, missing: missingRoot } = checkRootDependencies(calc, rootDataStatus);
141
+ if (!canRun) {
142
+ logger.log('INFO', `[Pass ${passToRun}] Skipping ${calc.name} for ${dateStr}. Data missing for this date: [${missingRoot.join(', ')}]`);
143
+ skipped.add(calc.name);
92
144
  return false;
93
145
  }
94
- // --- END: RECOMMENDED ADDITION ---
95
- // 1. Check root data
96
- const { canRun, missing: missingRoot } = checkRootDependencies(c, rootDataStatus);
97
- if (!canRun) {logger.log('INFO', `[Pass ${passToRun} Meta] Skipping ${c.name} for ${dateStr}. Missing root data: [${missingRoot.join(', ')}]`);skipped.add(c.name);return false;}
98
- // 2. Check computed dependencies
99
- const missingDeps = (c.dependencies || []).map(normalizeName).filter(d => !existingResults[d]);
100
- if (missingDeps.length > 0) {logger.log('WARN', `[Pass ${passToRun} Meta] Skipping ${c.name} for ${dateStr}. Missing computed deps: [${missingDeps.join(', ')}]`);skipped.add(c.name);return false;} return true;});
146
+
147
+ // 4. (For Meta Calcs) Check computed dependencies
148
+ if (calc.type === 'meta') {
149
+ const missingDeps = (calc.dependencies || []).map(normalizeName).filter(d => !existingResults[d]);
150
+ if (missingDeps.length > 0) {
151
+ logger.log('WARN', `[Pass ${passToRun} Meta] Skipping ${calc.name} for ${dateStr}. Missing computed deps: [${missingDeps.join(', ')}]`);
152
+ skipped.add(calc.name);
153
+ return false;
154
+ }
155
+ }
156
+
157
+ // If it passed all checks, run it.
158
+ return true;
159
+ };
160
+
161
+ const standardCalcsToRun = standardCalcs.filter(filterCalc);
162
+ const metaCalcsToRun = metaCalcs.filter(filterCalc);
163
+
101
164
  return { standardCalcsToRun, metaCalcsToRun };
102
165
  }
103
166
 
@@ -107,21 +170,91 @@ function initializeCalculators(calcs, logger) { const state = {}; for (const c o
107
170
 
108
171
  /** Stage 7: Load historical data required for calculations */
109
172
  async function loadHistoricalData(date, calcs, config, deps, rootData) { const updated = {...rootData}, dStr=date.toISOString().slice(0,10); const tasks = [];
110
- if(calcs.some(c=>c.isHistorical)) tasks.push((async()=>{ const prev=new Date(date); prev.setUTCDate(prev.getUTCDate()-1); const prevStr=prev.toISOString().slice(0,10); updated.yesterdayPortfolios=await loadFullDayMap(config,deps,await getPortfolioPartRefs(config,deps,prevStr)); })());
111
- if(calcs.some(c=>c.rootDataDependencies.includes('history'))) tasks.push((async()=>{ updated.todayHistoryData=await loadFullDayMap(config,deps,rootData.historyRefs); })());
112
- if(calcs.some(c=>c.isHistorical)) tasks.push((async()=>{ const prev=new Date(date); prev.setUTCDate(prev.getUTCDate()-1); const prevStr=prev.toISOString().slice(0,10); updated.yesterdayHistoryData=await loadFullDayMap(config,deps,await getHistoryPartRefs(config,deps,prevStr)); })());
173
+ const needsYesterdayPortfolio = calcs.some(c => c.isHistorical && c.rootDataDependencies.includes('portfolio'));
174
+ const needsTodayHistory = calcs.some(c => c.rootDataDependencies.includes('history'));
175
+ const needsYesterdayHistory = calcs.some(c => c.isHistorical && c.rootDataDependencies.includes('history'));
176
+ // --- NEW: Add checks for historical insights and social ---
177
+ const needsYesterdayInsights = calcs.some(c => c.isHistorical && c.rootDataDependencies.includes('insights'));
178
+ const needsYesterdaySocial = calcs.some(c => c.isHistorical && c.rootDataDependencies.includes('social'));
179
+
180
+ // --- MODIFIED: Be smarter about loading data ---
181
+ if(needsYesterdayPortfolio) {
182
+ tasks.push((async()=>{ const prev=new Date(date); prev.setUTCDate(prev.getUTCDate()-1); const prevStr=prev.toISOString().slice(0,10);
183
+ logger.log('INFO', `[PassRunner] Loading YESTERDAY portfolio data for ${prevStr}`);
184
+ updated.yesterdayPortfolios=await loadFullDayMap(config,deps,await getPortfolioPartRefs(config,deps,prevStr));
185
+ })());
186
+ }
187
+ if(needsTodayHistory) {
188
+ tasks.push((async()=>{
189
+ logger.log('INFO', `[PassRunner] Loading TODAY history data for ${dStr}`);
190
+ updated.todayHistoryData=await loadFullDayMap(config,deps,rootData.historyRefs);
191
+ })());
192
+ }
193
+ if(needsYesterdayHistory) {
194
+ tasks.push((async()=>{ const prev=new Date(date); prev.setUTCDate(prev.getUTCDate()-1); const prevStr=prev.toISOString().slice(0,10);
195
+ logger.log('INFO', `[PassRunner] Loading YESTERDAY history data for ${prevStr}`);
196
+ updated.yesterdayHistoryData=await loadFullDayMap(config,deps,await getHistoryPartRefs(config,deps,prevStr));
197
+ })());
198
+ }
199
+ // --- NEW: Load historical insights/social ---
200
+ if(needsYesterdayInsights) {
201
+ tasks.push((async()=>{ const prev=new Date(date); prev.setUTCDate(prev.getUTCDate()-1); const prevStr=prev.toISOString().slice(0,10);
202
+ logger.log('INFO', `[PassRunner] Loading YESTERDAY insights data for ${prevStr}`);
203
+ updated.yesterdayInsights=await loadDailyInsights(config,deps,prevStr);
204
+ })());
205
+ }
206
+ if(needsYesterdaySocial) {
207
+ tasks.push((async()=>{ const prev=new Date(date); prev.setUTCDate(prev.getUTCDate()-1); const prevStr=prev.toISOString().slice(0,10);
208
+ logger.log('INFO', `[PassRunner] Loading YESTERDAY social data for ${prevStr}`);
209
+ updated.yesterdaySocialPostInsights=await loadDailySocialPostInsights(config,deps,prevStr);
210
+ })());
211
+ }
212
+
113
213
  await Promise.all(tasks); return updated;
114
214
  }
115
215
 
216
+
116
217
  /** * --- MODIFIED: Stage 8: Stream and process data for standard calculations ---
117
218
  * This function now uses an async generator to stream portfolio data
118
219
  * instead of loading it all into memory.
119
220
  */
120
221
  async function streamAndProcess(dateStr, state, passName, config, deps, rootData) {
121
222
  const { logger, calculationUtils } = deps;
223
+ // --- MODIFIED: yesterdayInsights/Social are now loaded by loadHistoricalData ---
122
224
  const { todayInsights, yesterdayInsights, todaySocialPostInsights, yesterdaySocialPostInsights, todayHistoryData, yesterdayHistoryData, yesterdayPortfolios } = rootData;
123
- let firstUser=true;
225
+
226
+ // --- NEW: Check if streaming is even needed ---
227
+ const calcsThatStreamPortfolio = Object.values(state).filter(calc => calc && calc.manifest && (calc.manifest.rootDataDependencies.includes('portfolio') || calc.manifest.category === 'speculators'));
228
+
124
229
  const context={instrumentMappings:(await calculationUtils.loadInstrumentMappings()).instrumentToTicker, sectorMapping:(await calculationUtils.loadInstrumentMappings()).instrumentToSector, todayDateStr:dateStr, dependencies:deps, config};
230
+
231
+ // --- MODIFIED: Run non-streaming calcs first (social/insights) ---
232
+ // This allows them to run even if portfolio data is missing
233
+ let firstUser=true; // Used to run them only once
234
+ for(const name in state){
235
+ const calc=state[name]; if(!calc||typeof calc.process!=='function') continue;
236
+ const cat=calc.manifest.category;
237
+ if(cat==='socialPosts'||cat==='insights') {
238
+ if (firstUser) {
239
+ logger.log('INFO', `[${passName}] Running non-streaming calc: ${name}`);
240
+ let args=[null,null,null,{...context, userType: 'n/a'},todayInsights,yesterdayInsights,todaySocialPostInsights,yesterdaySocialPostInsights,todayHistoryData,yesterdayHistoryData];
241
+ // Pass historical data if needed
242
+ if(calc.manifest.isHistorical) {
243
+ args=[null,null,null,{...context, userType: 'n/a'},todayInsights,yesterdayInsights,todaySocialPostInsights,yesterdaySocialPostInsights,todayHistoryData,yesterdayHistoryData];
244
+ }
245
+ try{ await Promise.resolve(calc.process(...args)); } catch(e){logger.log('WARN',`Process error ${name} (non-stream)`,{err:e.message});}
246
+ }
247
+ }
248
+ }
249
+
250
+
251
+ if (calcsThatStreamPortfolio.length === 0) {
252
+ logger.log('INFO', `[${passName}] No portfolio-streaming calcs to run for ${dateStr}. Skipping stream.`);
253
+ return; // Exit stream function
254
+ }
255
+
256
+ logger.log('INFO', `[${passName}] Streaming portfolio data for ${calcsThatStreamPortfolio.length} calcs...`);
257
+
125
258
  for await (const chunk of streamPortfolioData(config, deps, dateStr)) {
126
259
  for(const uid in chunk){ const p=chunk[uid]; if(!p) continue;
127
260
  const userType=p.PublicPositions?'speculator':'normal';
@@ -129,9 +262,18 @@ async function streamAndProcess(dateStr, state, passName, config, deps, rootData
129
262
  for(const name in state){
130
263
  const calc=state[name]; if(!calc||typeof calc.process!=='function') continue;
131
264
  const cat=calc.manifest.category, isSocialOrInsights=cat==='socialPosts'||cat==='insights', isHistorical=calc.manifest.isHistorical, isSpec=cat==='speculators';
265
+
266
+ // --- MODIFIED: Skip social/insights here, they ran above ---
267
+ if(isSocialOrInsights) continue;
268
+
132
269
  let args=[p,null,uid,context,todayInsights,yesterdayInsights,todaySocialPostInsights,yesterdaySocialPostInsights,todayHistoryData,yesterdayHistoryData];
133
- if(isSocialOrInsights&&!firstUser) continue;
134
- if(isHistorical){ const pY=yesterdayPortfolios[uid]; if(!pY) continue; args=[p,pY,uid,context,todayInsights,yesterdayInsights,todaySocialPostInsights,yesterdaySocialPostInsights,todayHistoryData,yesterdayHistoryData]; }
270
+
271
+ if(isHistorical){
272
+ const pY=yesterdayPortfolios ? yesterdayPortfolios[uid] : null; // Check if yesterdayPortfolios exists
273
+ // V3 behavioural calcs (like history aggregator) *can* run without pY
274
+ if(!pY && (cat !== 'behavioural' && name !== 'historical-performance-aggregator')) continue;
275
+ args=[p,pY,uid,context,todayInsights,yesterdayInsights,todaySocialPostInsights,yesterdaySocialPostInsights,todayHistoryData,yesterdayHistoryData];
276
+ }
135
277
  if((userType==='normal'&&isSpec)||(userType==='speculator'&&!isSpec&&name!=='users-processed')) continue;
136
278
  try{ await Promise.resolve(calc.process(...args)); } catch(e){logger.log('WARN',`Process error ${name} for ${uid}`,{err:e.message});} }
137
279
  firstUser=false;
@@ -142,11 +284,20 @@ async function streamAndProcess(dateStr, state, passName, config, deps, rootData
142
284
  /** Stage 9: Run standard computations */
143
285
  async function runStandardComputationPass(date, calcs, passName, config, deps, rootData) {
144
286
  const dStr = date.toISOString().slice(0, 10), logger = deps.logger;
145
- if (calcs.length === 0) return;
287
+ // --- THIS IS THE CRITICAL CHANGE ---
288
+ // If 'calcs' is empty *because of the new filter*, this log won't even appear.
289
+ if (calcs.length === 0) {
290
+ logger.log('INFO', `[${passName}] No standard calcs to run for ${dateStr} after filtering.`);
291
+ return;
292
+ }
293
+ // This log now only appears if there is *actually* work to do.
146
294
  logger.log('INFO', `[${passName}] Running ${dStr} with ${calcs.length} calcs.`);
295
+
147
296
  const fullRoot = await loadHistoricalData(date, calcs, config, deps, rootData);
148
297
  const state = initializeCalculators(calcs, logger);
298
+
149
299
  await streamAndProcess(dStr, state, passName, config, deps, fullRoot);
300
+
150
301
  let success = 0;
151
302
  const standardWrites = [];
152
303
  const shardedWrites = {};
@@ -188,25 +339,19 @@ async function runStandardComputationPass(date, calcs, passName, config, deps, r
188
339
  const shardedDocWrites = [];
189
340
  let docRef;
190
341
  if (docPath.includes('/')) {
191
- // Path is absolute, e.g., 'social_prediction_regime_state/history'
192
342
  docRef = deps.db.doc(docPath);
193
343
  } else {
194
- // Path is a docId, e.g., 'user_profile_history_shard_0'
195
- // We must infer its collection from config.
196
344
  const collection = (docPath.startsWith('user_profile_history'))
197
- ? config.shardedUserProfileCollection // 'user_profile_history'
198
- : config.shardedProfitabilityCollection; // Fallback
345
+ ? config.shardedUserProfileCollection
346
+ : config.shardedProfitabilityCollection;
199
347
  docRef = deps.db.collection(collection).doc(docPath);
200
348
  }
201
- // Ensure data is a valid object before pushing
202
349
  if (docData && typeof docData === 'object' && !Array.isArray(docData)) {
203
350
  shardedDocWrites.push({ ref: docRef, data: docData });
204
351
  } else {
205
352
  logger.log('ERROR', `[${passName}] Invalid sharded document data for ${docPath}. Not an object.`, { data: docData });
206
353
  }
207
- // Commit this single document write (or small batch if logic is changed later)
208
354
  if (shardedDocWrites.length > 0) {
209
- // Use the docPath in the operation name for clearer logging
210
355
  await commitBatchInChunks(config, deps, shardedDocWrites, `${passName} Sharded ${docPath} ${dStr}`);
211
356
  }
212
357
  }
@@ -216,8 +361,17 @@ async function runStandardComputationPass(date, calcs, passName, config, deps, r
216
361
  /** Stage 10: Run meta computations */
217
362
  async function runMetaComputationPass(date, calcs, passName, config, deps, fetchedDeps, rootData) {
218
363
  const dStr = date.toISOString().slice(0, 10), logger = deps.logger;
219
- if (calcs.length === 0) return;
364
+ // --- THIS IS THE CRITICAL CHANGE ---
365
+ if (calcs.length === 0) {
366
+ logger.log('INFO', `[${passName}] No meta calcs to run for ${dStr} after filtering.`);
367
+ return;
368
+ }
220
369
  logger.log('INFO', `[${passName}] Running ${dStr} with ${calcs.length} calcs.`);
370
+
371
+ // --- NEW: Load historical data for meta calcs if needed ---
372
+ // (This might be redundant if standard pass ran, but meta-calcs can run standalone)
373
+ const fullRoot = await loadHistoricalData(date, calcs, config, deps, rootData);
374
+
221
375
  let success = 0;
222
376
  const standardWrites = [];
223
377
  const shardedWrites = {};
@@ -226,7 +380,8 @@ async function runMetaComputationPass(date, calcs, passName, config, deps, fetch
226
380
  if (typeof Cl !== 'function') {logger.log('ERROR', `Invalid class ${name}`);continue;}
227
381
  const inst = new Cl();
228
382
  try {
229
- const result = await Promise.resolve(inst.process(dStr, { ...deps, rootData }, config, fetchedDeps));
383
+ // --- MODIFIED: Pass fullRoot to meta calcs ---
384
+ const result = await Promise.resolve(inst.process(dStr, { ...deps, rootData: fullRoot }, config, fetchedDeps));
230
385
  if (result && Object.keys(result).length > 0) {const standardResult = {}; for (const key in result) {
231
386
  if (key.startsWith('sharded_')) {const shardedData = result[key];for (const collectionName in shardedData)
232
387
  {if (!shardedWrites[collectionName]) shardedWrites[collectionName] = {};Object.assign(shardedWrites[collectionName], shardedData[collectionName]);}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bulltrackers-module",
3
- "version": "1.0.139",
3
+ "version": "1.0.141",
4
4
  "description": "Helper Functions for Bulltrackers.",
5
5
  "main": "index.js",
6
6
  "files": [