bulltrackers-module 1.0.131 → 1.0.132
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.
|
@@ -6,9 +6,11 @@
|
|
|
6
6
|
* It reads its pass number from the config and executes only those calculations.
|
|
7
7
|
* This file contains the high-level "manual" of steps. The "how-to" logic
|
|
8
8
|
* is extracted into 'computation_system_utils.js'.
|
|
9
|
+
* --- MODIFIED: To use getEarliestDataDates and pass the date map to the orchestrator helpers. ---
|
|
9
10
|
*/
|
|
10
11
|
const { groupByPass, checkRootDataAvailability, fetchDependenciesForPass, filterCalculations, runStandardComputationPass, runMetaComputationPass } = require('./orchestration_helpers.js');
|
|
11
|
-
|
|
12
|
+
// --- MODIFIED: Import getEarliestDataDates ---
|
|
13
|
+
const { getExpectedDateStrings, getEarliestDataDates } = require('../utils/utils.js');
|
|
12
14
|
|
|
13
15
|
async function runComputationPass(config, dependencies, computationManifest) {
|
|
14
16
|
const { logger } = dependencies;
|
|
@@ -17,7 +19,12 @@ async function runComputationPass(config, dependencies, computationManifest) {
|
|
|
17
19
|
|
|
18
20
|
const yesterday = new Date(); yesterday.setUTCDate(yesterday.getUTCDate()-1);
|
|
19
21
|
const endDateUTC = new Date(Date.UTC(yesterday.getUTCFullYear(), yesterday.getUTCMonth(), yesterday.getUTCDate()));
|
|
20
|
-
|
|
22
|
+
|
|
23
|
+
// --- MODIFIED: Call new date function ---
|
|
24
|
+
const earliestDates = await getEarliestDataDates(config, dependencies);
|
|
25
|
+
const firstDate = earliestDates.absoluteEarliest; // Use the absolute earliest for the loop
|
|
26
|
+
// --- END MODIFICATION ---
|
|
27
|
+
|
|
21
28
|
const startDateUTC = firstDate ? new Date(Date.UTC(firstDate.getUTCFullYear(), firstDate.getUTCMonth(), firstDate.getUTCDate())) : new Date(config.earliestComputationDate+'T00:00:00Z');
|
|
22
29
|
const allExpectedDates = getExpectedDateStrings(startDateUTC, endDateUTC);
|
|
23
30
|
|
|
@@ -30,7 +37,10 @@ async function runComputationPass(config, dependencies, computationManifest) {
|
|
|
30
37
|
for (const dateStr of allExpectedDates) {
|
|
31
38
|
const dateToProcess = new Date(dateStr+'T00:00:00Z');
|
|
32
39
|
try {
|
|
33
|
-
|
|
40
|
+
// --- MODIFIED: Pass earliestDates map to checkRootDataAvailability ---
|
|
41
|
+
const rootData = await checkRootDataAvailability(dateStr, config, dependencies, earliestDates); if (!rootData) continue;
|
|
42
|
+
// --- END MODIFICATION ---
|
|
43
|
+
|
|
34
44
|
const fetchedDeps = await fetchDependenciesForPass(dateStr, calcsInThisPass, computationManifest, config, dependencies);
|
|
35
45
|
const { standardCalcsToRun, metaCalcsToRun } = filterCalculations(standardCalcs, metaCalcs, rootData.status, fetchedDeps, passToRun, dateStr, logger);
|
|
36
46
|
if (standardCalcsToRun.length) await runStandardComputationPass(dateToProcess, standardCalcsToRun, `Pass ${passToRun} (Standard)`, config, dependencies, rootData);
|
|
@@ -44,4 +54,4 @@ async function runComputationPass(config, dependencies, computationManifest) {
|
|
|
44
54
|
logger.log('INFO', `[PassRunner] Pass ${passToRun} orchestration finished.`);
|
|
45
55
|
}
|
|
46
56
|
|
|
47
|
-
module.exports = { runComputationPass };
|
|
57
|
+
module.exports = { runComputationPass };
|
|
@@ -5,30 +5,96 @@ const { normalizeName, commitBatchInChunks } = require('../utils/utils.js');
|
|
|
5
5
|
/** Stage 1: Group manifest by pass number */
|
|
6
6
|
function groupByPass(manifest) { return manifest.reduce((acc, calc) => { (acc[calc.pass] = acc[calc.pass] || []).push(calc); return acc; }, {}); }
|
|
7
7
|
|
|
8
|
-
/**
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
8
|
+
/** * --- MODIFIED: Returns detailed missing dependencies for logging ---
|
|
9
|
+
* Stage 2: Check root data dependencies for a calc
|
|
10
|
+
*/
|
|
11
|
+
function checkRootDependencies(calcManifest, rootDataStatus) {
|
|
12
|
+
const missing = [];
|
|
13
|
+
if (!calcManifest.rootDataDependencies || !calcManifest.rootDataDependencies.length) {
|
|
14
|
+
return { canRun: true, missing };
|
|
15
|
+
}
|
|
16
|
+
for (const dep of calcManifest.rootDataDependencies) {
|
|
17
|
+
if (dep === 'portfolio' && !rootDataStatus.hasPortfolio) missing.push('portfolio');
|
|
18
|
+
else if (dep === 'insights' && !rootDataStatus.hasInsights) missing.push('insights');
|
|
19
|
+
else if (dep === 'social' && !rootDataStatus.hasSocial) missing.push('social');
|
|
20
|
+
else if (dep === 'history' && !rootDataStatus.hasHistory) missing.push('history');
|
|
21
|
+
}
|
|
22
|
+
return { canRun: missing.length === 0, missing };
|
|
12
23
|
}
|
|
13
24
|
|
|
14
|
-
/**
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
25
|
+
/** * --- MODIFIED: Uses earliestDates map to avoid unnecessary queries ---
|
|
26
|
+
* Stage 3: Check root data availability for a date
|
|
27
|
+
*/
|
|
28
|
+
async function checkRootDataAvailability(dateStr, config, dependencies, earliestDates) {
|
|
29
|
+
const { logger } = dependencies;
|
|
18
30
|
logger.log('INFO', `[PassRunner] Checking root data for ${dateStr}...`);
|
|
31
|
+
|
|
32
|
+
const dateToProcess = new Date(dateStr + 'T00:00:00Z');
|
|
33
|
+
let portfolioRefs = [], insightsData = null, socialData = null, historyRefs = [];
|
|
34
|
+
let hasPortfolio = false, hasInsights = false, hasSocial = false, hasHistory = false;
|
|
35
|
+
|
|
19
36
|
try {
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
37
|
+
const tasks = [];
|
|
38
|
+
|
|
39
|
+
if (dateToProcess >= earliestDates.portfolio) {
|
|
40
|
+
tasks.push(
|
|
41
|
+
getPortfolioPartRefs(config, dependencies, dateStr).then(res => {
|
|
42
|
+
portfolioRefs = res;
|
|
43
|
+
hasPortfolio = !!(res?.length);
|
|
44
|
+
})
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (dateToProcess >= earliestDates.insights) {
|
|
49
|
+
tasks.push(
|
|
50
|
+
loadDailyInsights(config, dependencies, dateStr).then(res => {
|
|
51
|
+
insightsData = res;
|
|
52
|
+
hasInsights = !!res;
|
|
53
|
+
})
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (dateToProcess >= earliestDates.social) {
|
|
58
|
+
tasks.push(
|
|
59
|
+
loadDailySocialPostInsights(config, dependencies, dateStr).then(res => {
|
|
60
|
+
socialData = res;
|
|
61
|
+
hasSocial = !!res;
|
|
62
|
+
})
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (dateToProcess >= earliestDates.history) {
|
|
67
|
+
tasks.push(
|
|
68
|
+
getHistoryPartRefs(config, dependencies, dateStr).then(res => {
|
|
69
|
+
historyRefs = res;
|
|
70
|
+
hasHistory = !!(res?.length);
|
|
71
|
+
})
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
await Promise.all(tasks);
|
|
76
|
+
|
|
77
|
+
if (!(hasPortfolio || hasInsights || hasSocial || hasHistory)) {
|
|
78
|
+
logger.log('WARN', `[PassRunner] No root data for ${dateStr}.`);
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
portfolioRefs,
|
|
84
|
+
insightsData,
|
|
85
|
+
socialData,
|
|
86
|
+
historyRefs,
|
|
87
|
+
status: { hasPortfolio, hasInsights, hasSocial, hasHistory }
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
} catch (err) {
|
|
91
|
+
logger.log('ERROR', `[PassRunner] Error checking data for ${dateStr}`, { errorMessage: err.message });
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
30
94
|
}
|
|
31
95
|
|
|
96
|
+
|
|
97
|
+
|
|
32
98
|
/** Stage 4: Fetch computed dependencies from Firestore */
|
|
33
99
|
async function fetchDependenciesForPass(dateStr, calcsInPass, fullManifest, config, { db, logger }) {
|
|
34
100
|
const manifestMap = new Map(fullManifest.map(c => [normalizeName(c.name), c]));
|
|
@@ -47,14 +113,47 @@ async function fetchDependenciesForPass(dateStr, calcsInPass, fullManifest, conf
|
|
|
47
113
|
return fetched;
|
|
48
114
|
}
|
|
49
115
|
|
|
50
|
-
/**
|
|
116
|
+
/** * --- MODIFIED: Added detailed logging ---
|
|
117
|
+
* Stage 5: Filter calculations based on available root data and dependencies
|
|
118
|
+
*/
|
|
51
119
|
function filterCalculations(standardCalcs, metaCalcs, rootDataStatus, fetchedDeps, passToRun, dateStr, logger) {
|
|
52
120
|
const skipped = new Set();
|
|
53
|
-
|
|
54
|
-
|
|
121
|
+
|
|
122
|
+
// Filter Standard Calcs
|
|
123
|
+
const standardCalcsToRun = standardCalcs.filter(c => {
|
|
124
|
+
const { canRun, missing } = checkRootDependencies(c, rootDataStatus);
|
|
125
|
+
if (canRun) return true;
|
|
126
|
+
|
|
127
|
+
logger.log('INFO', `[Pass ${passToRun}] Skipping ${c.name} for ${dateStr}. Missing root data: [${missing.join(', ')}]`);
|
|
128
|
+
skipped.add(c.name);
|
|
129
|
+
return false;
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// Filter Meta Calcs
|
|
133
|
+
const metaCalcsToRun = metaCalcs.filter(c => {
|
|
134
|
+
// 1. Check root data
|
|
135
|
+
const { canRun, missing: missingRoot } = checkRootDependencies(c, rootDataStatus);
|
|
136
|
+
if (!canRun) {
|
|
137
|
+
logger.log('INFO', `[Pass ${passToRun} Meta] Skipping ${c.name} for ${dateStr}. Missing root data: [${missingRoot.join(', ')}]`);
|
|
138
|
+
skipped.add(c.name);
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// 2. Check computed dependencies
|
|
143
|
+
const missingDeps = (c.dependencies || []).map(normalizeName).filter(d => !fetchedDeps[d]);
|
|
144
|
+
if (missingDeps.length > 0) {
|
|
145
|
+
logger.log('WARN', `[Pass ${passToRun} Meta] Skipping ${c.name} for ${dateStr}. Missing computed deps: [${missingDeps.join(', ')}]`);
|
|
146
|
+
skipped.add(c.name);
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return true; // All checks passed
|
|
151
|
+
});
|
|
152
|
+
|
|
55
153
|
return { standardCalcsToRun, metaCalcsToRun };
|
|
56
154
|
}
|
|
57
155
|
|
|
156
|
+
|
|
58
157
|
/** Stage 6: Initialize calculator instances */
|
|
59
158
|
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; }
|
|
60
159
|
|
|
@@ -2,8 +2,10 @@
|
|
|
2
2
|
* @fileoverview Computation system sub-pipes and utils.
|
|
3
3
|
* REFACTORED: Now stateless and receive dependencies where needed.
|
|
4
4
|
* DYNAMIC: Categorization logic is removed, replaced by manifest.
|
|
5
|
+
* --- MODIFIED: getFirstDateFromSourceData is now getEarliestDataDates
|
|
6
|
+
* and queries all data sources to build an availability map. ---
|
|
5
7
|
*/
|
|
6
|
-
/** --- Computation System Sub-Pipes & Utils (Stateless) --- */
|
|
8
|
+
/** --- Computation System Sub-Pipes & Utils (Stateless, Dependency-Injection) --- */
|
|
7
9
|
|
|
8
10
|
const { FieldValue, FieldPath } = require('@google-cloud/firestore');
|
|
9
11
|
|
|
@@ -46,13 +48,46 @@ function getExpectedDateStrings(startDate, endDate) {
|
|
|
46
48
|
return dateStrings;
|
|
47
49
|
}
|
|
48
50
|
|
|
49
|
-
/**
|
|
51
|
+
/**
|
|
52
|
+
* --- NEW HELPER ---
|
|
53
|
+
* Stage 4: Get the earliest date in a *flat* collection where doc IDs are dates.
|
|
54
|
+
*/
|
|
55
|
+
async function getFirstDateFromSimpleCollection(config, deps, collectionName) {
|
|
56
|
+
const { db, logger, calculationUtils } = deps;
|
|
57
|
+
const { withRetry } = calculationUtils;
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
if (!collectionName) {
|
|
61
|
+
logger.log('WARN', `[Core Utils] Collection name not provided for simple date query.`);
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
const query = db.collection(collectionName)
|
|
65
|
+
.where(FieldPath.documentId(), '>=', '2000-01-01')
|
|
66
|
+
.orderBy(FieldPath.documentId(), 'asc')
|
|
67
|
+
.limit(1);
|
|
68
|
+
|
|
69
|
+
const snapshot = await withRetry(() => query.get(), `GetEarliestDoc(${collectionName})`);
|
|
70
|
+
|
|
71
|
+
if (!snapshot.empty && /^\d{4}-\d{2}-\d{2}$/.test(snapshot.docs[0].id)) {
|
|
72
|
+
return new Date(snapshot.docs[0].id + 'T00:00:00Z');
|
|
73
|
+
}
|
|
74
|
+
} catch (e) {
|
|
75
|
+
logger.log('ERROR', `GetFirstDate failed for ${collectionName}`, { errorMessage: e.message });
|
|
76
|
+
}
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Stage 4: Get the earliest date in a sharded collection */
|
|
50
81
|
async function getFirstDateFromCollection(config, deps, collectionName) {
|
|
51
82
|
const { db, logger, calculationUtils } = deps;
|
|
52
83
|
const { withRetry } = calculationUtils;
|
|
53
84
|
|
|
54
85
|
let earliestDate = null;
|
|
55
86
|
try {
|
|
87
|
+
if (!collectionName) {
|
|
88
|
+
logger.log('WARN', `[Core Utils] Collection name not provided for sharded date query.`);
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
56
91
|
const blockDocRefs = await withRetry(() => db.collection(collectionName).listDocuments(), `GetBlocks(${collectionName})`);
|
|
57
92
|
if (!blockDocRefs.length) { logger.log('WARN', `No block documents in collection: ${collectionName}`); return null; }
|
|
58
93
|
|
|
@@ -75,26 +110,69 @@ async function getFirstDateFromCollection(config, deps, collectionName) {
|
|
|
75
110
|
return earliestDate;
|
|
76
111
|
}
|
|
77
112
|
|
|
78
|
-
/**
|
|
79
|
-
|
|
113
|
+
/** * --- MODIFIED FUNCTION ---
|
|
114
|
+
* Stage 5: Determine the earliest date from *all* source data.
|
|
115
|
+
*/
|
|
116
|
+
async function getEarliestDataDates(config, deps) {
|
|
80
117
|
const { logger } = deps;
|
|
81
|
-
logger.log('INFO', 'Querying for earliest date from source
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
118
|
+
logger.log('INFO', 'Querying for earliest date from ALL source data collections...');
|
|
119
|
+
|
|
120
|
+
// These return null on error or if empty
|
|
121
|
+
const [
|
|
122
|
+
investorDate,
|
|
123
|
+
speculatorDate,
|
|
124
|
+
investorHistoryDate,
|
|
125
|
+
speculatorHistoryDate,
|
|
126
|
+
insightsDate,
|
|
127
|
+
socialDate
|
|
128
|
+
] = await Promise.all([
|
|
129
|
+
getFirstDateFromCollection(config, deps, config.normalUserPortfolioCollection),
|
|
130
|
+
getFirstDateFromCollection(config, deps, config.speculatorPortfolioCollection),
|
|
131
|
+
getFirstDateFromCollection(config, deps, config.normalUserHistoryCollection),
|
|
132
|
+
getFirstDateFromCollection(config, deps, config.speculatorHistoryCollection),
|
|
133
|
+
getFirstDateFromSimpleCollection(config, deps, config.insightsCollectionName),
|
|
134
|
+
getFirstDateFromSimpleCollection(config, deps, config.socialInsightsCollectionName)
|
|
135
|
+
]);
|
|
136
|
+
|
|
137
|
+
// Helper to find the minimum (earliest) of a set of dates
|
|
138
|
+
const getMinDate = (...dates) => {
|
|
139
|
+
const validDates = dates.filter(Boolean); // Filter out nulls
|
|
140
|
+
if (validDates.length === 0) return null;
|
|
141
|
+
return new Date(Math.min(...validDates));
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const earliestPortfolioDate = getMinDate(investorDate, speculatorDate);
|
|
145
|
+
const earliestHistoryDate = getMinDate(investorHistoryDate, speculatorHistoryDate);
|
|
146
|
+
const earliestInsightsDate = getMinDate(insightsDate); // Already a single date
|
|
147
|
+
const earliestSocialDate = getMinDate(socialDate); // Already a single date
|
|
148
|
+
|
|
149
|
+
const absoluteEarliest = getMinDate(
|
|
150
|
+
earliestPortfolioDate,
|
|
151
|
+
earliestHistoryDate,
|
|
152
|
+
earliestInsightsDate,
|
|
153
|
+
earliestSocialDate
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
// Fallback date
|
|
157
|
+
const fallbackDate = new Date(config.earliestComputationDate + 'T00:00:00Z' || '2023-01-01T00:00:00Z');
|
|
158
|
+
|
|
159
|
+
const result = {
|
|
160
|
+
portfolio: earliestPortfolioDate || new Date('2999-12-31'), // Use a 'far future' date if null
|
|
161
|
+
history: earliestHistoryDate || new Date('2999-12-31'),
|
|
162
|
+
insights: earliestInsightsDate || new Date('2999-12-31'),
|
|
163
|
+
social: earliestSocialDate || new Date('2999-12-31'),
|
|
164
|
+
absoluteEarliest: absoluteEarliest || fallbackDate // Use fallback for the main loop
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
logger.log('INFO', 'Earliest data availability map built:', {
|
|
168
|
+
portfolio: result.portfolio.toISOString().slice(0, 10),
|
|
169
|
+
history: result.history.toISOString().slice(0, 10),
|
|
170
|
+
insights: result.insights.toISOString().slice(0, 10),
|
|
171
|
+
social: result.social.toISOString().slice(0, 10),
|
|
172
|
+
absoluteEarliest: result.absoluteEarliest.toISOString().slice(0, 10)
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
return result;
|
|
98
176
|
}
|
|
99
177
|
|
|
100
178
|
module.exports = {
|
|
@@ -103,5 +181,6 @@ module.exports = {
|
|
|
103
181
|
normalizeName,
|
|
104
182
|
commitBatchInChunks,
|
|
105
183
|
getExpectedDateStrings,
|
|
106
|
-
getFirstDateFromSourceData,
|
|
107
|
-
|
|
184
|
+
// getFirstDateFromSourceData, // This is replaced
|
|
185
|
+
getEarliestDataDates, // <-- EXPORT NEW FUNCTION
|
|
186
|
+
};
|