bulltrackers-module 1.0.138 → 1.0.140
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,97 +1,210 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
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
|
-
|
|
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);
|
|
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
|
-
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
21
|
+
|
|
22
|
+
// Hardcoded earliest dates
|
|
23
|
+
logger.log('INFO', 'Using hardcoded earliest data dates to bypass faulty discovery.');
|
|
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');
|
|
28
|
+
|
|
29
|
+
const earliestDates = {
|
|
30
|
+
portfolio: earliestPortfolio,
|
|
31
|
+
history: earliestHistory,
|
|
32
|
+
social: earliestSocial,
|
|
33
|
+
insights: earliestInsights,
|
|
34
|
+
absoluteEarliest: [earliestPortfolio, earliestHistory, earliestSocial, earliestInsights].reduce((a, b) => a < b ? a : b)
|
|
35
|
+
};
|
|
36
|
+
|
|
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)}`);
|
|
38
|
+
|
|
34
39
|
const passes = groupByPass(computationManifest);
|
|
35
|
-
const calcsInThisPass = passes[passToRun] || [];
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
//
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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;
|
|
54
63
|
}
|
|
55
|
-
}
|
|
64
|
+
}
|
|
56
65
|
|
|
57
|
-
|
|
58
|
-
|
|
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)}`);
|
|
59
75
|
}
|
|
60
76
|
}
|
|
61
|
-
|
|
62
|
-
//
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
logger.log('INFO', `
|
|
69
|
-
|
|
70
|
-
//
|
|
71
|
-
|
|
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()));
|
|
72
90
|
const allExpectedDates = getExpectedDateStrings(startDateUTC, endDateUTC);
|
|
73
|
-
const firstDayOfBackfill = allExpectedDates.length > 0 ? allExpectedDates[0] : null; // --- MOVED FROM ABOVE ---
|
|
74
91
|
|
|
75
|
-
|
|
76
|
-
|
|
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
|
+
// ============================================
|
|
77
100
|
const processDate = async (dateStr) => {
|
|
78
|
-
const dateToProcess = new Date(dateStr+'T00:00:00Z');
|
|
101
|
+
const dateToProcess = new Date(dateStr + 'T00:00:00Z');
|
|
102
|
+
|
|
79
103
|
try {
|
|
104
|
+
// Check root data availability
|
|
80
105
|
const rootData = await checkRootDataAvailability(dateStr, config, dependencies, earliestDates);
|
|
81
|
-
if (!rootData) {
|
|
106
|
+
if (!rootData) {
|
|
107
|
+
logger.log('WARN', `[PassRunner] Skipping ${dateStr} for Pass ${passToRun}: No root data.`);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Fetch existing results
|
|
82
112
|
const existingResults = await fetchExistingResults(dateStr, calcsInThisPass, computationManifest, config, dependencies);
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
+
}
|
|
88
198
|
};
|
|
199
|
+
|
|
200
|
+
// Process in batches
|
|
89
201
|
logger.log('INFO', `[PassRunner] Processing ${allExpectedDates.length} total dates in batches of ${PARALLEL_BATCH_SIZE}...`);
|
|
90
202
|
for (let i = 0; i < allExpectedDates.length; i += PARALLEL_BATCH_SIZE) {
|
|
91
203
|
const batch = allExpectedDates.slice(i, i + PARALLEL_BATCH_SIZE);
|
|
92
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]})`);
|
|
93
205
|
await Promise.all(batch.map(dateStr => processDate(dateStr)));
|
|
94
206
|
}
|
|
207
|
+
|
|
95
208
|
logger.log('INFO', `[PassRunner] Pass ${passToRun} orchestration finished.`);
|
|
96
209
|
}
|
|
97
210
|
|