bulltrackers-module 1.0.171 → 1.0.173
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.
- package/README.MD +3 -179
- package/functions/computation-system/controllers/computation_controller.js +211 -0
- package/functions/computation-system/helpers/computation_manifest_builder.js +15 -20
- package/functions/computation-system/helpers/computation_pass_runner.js +68 -35
- package/functions/computation-system/helpers/orchestration_helpers.js +185 -717
- package/functions/computation-system/layers/math_primitives.js +747 -0
- package/functions/task-engine/helpers/update_helpers.js +1 -1
- package/index.js +1 -1
- package/package.json +1 -1
|
@@ -1,800 +1,268 @@
|
|
|
1
|
-
const { FieldPath } = require('@google-cloud/firestore');
|
|
2
|
-
const {
|
|
3
|
-
getPortfolioPartRefs,
|
|
4
|
-
loadFullDayMap,
|
|
5
|
-
loadDataByRefs,
|
|
6
|
-
loadDailyInsights,
|
|
7
|
-
loadDailySocialPostInsights,
|
|
8
|
-
getHistoryPartRefs,
|
|
9
|
-
streamPortfolioData,
|
|
10
|
-
streamHistoryData
|
|
11
|
-
} = require('../utils/data_loader.js');
|
|
12
|
-
const { normalizeName, commitBatchInChunks } = require('../utils/utils.js');
|
|
13
|
-
const { batchStoreSchemas } = require('../utils/schema_capture.js');
|
|
14
|
-
|
|
15
1
|
/**
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
2
|
+
* FIXED: orchestration_helpers.js
|
|
3
|
+
* V3.2: Enabled streaming of Trading History data (tH_iter) for computations
|
|
4
|
+
* that require it, alongside Portfolio data.
|
|
19
5
|
*/
|
|
20
|
-
function groupByPass(manifest) { return manifest.reduce((acc, calc) => { (acc[calc.pass] = acc[calc.pass] || []).push(calc); return acc; }, {}); }
|
|
21
6
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
7
|
+
const { ComputationController } = require('../controllers/computation_controller');
|
|
8
|
+
const {
|
|
9
|
+
getPortfolioPartRefs, loadDailyInsights, loadDailySocialPostInsights,
|
|
10
|
+
getHistoryPartRefs, streamPortfolioData, streamHistoryData
|
|
11
|
+
} = require('../utils/data_loader');
|
|
12
|
+
const { batchStoreSchemas } = require('../utils/schema_capture');
|
|
13
|
+
const { normalizeName, commitBatchInChunks } = require('../utils/utils');
|
|
14
|
+
|
|
15
|
+
// --- Helpers ---
|
|
16
|
+
|
|
17
|
+
function groupByPass(manifest) {
|
|
18
|
+
return manifest.reduce((acc, calc) => { (acc[calc.pass] = acc[calc.pass] || []).push(calc); return acc; }, {});
|
|
19
|
+
}
|
|
20
|
+
|
|
28
21
|
function checkRootDependencies(calcManifest, rootDataStatus) {
|
|
29
22
|
const missing = [];
|
|
30
|
-
if (!calcManifest.rootDataDependencies
|
|
23
|
+
if (!calcManifest.rootDataDependencies) return { canRun: true, missing };
|
|
31
24
|
for (const dep of calcManifest.rootDataDependencies) {
|
|
32
|
-
if
|
|
33
|
-
else if (dep === 'insights'
|
|
34
|
-
else if (dep === 'social'
|
|
35
|
-
else if (dep === 'history'
|
|
25
|
+
if (dep === 'portfolio' && !rootDataStatus.hasPortfolio) missing.push('portfolio');
|
|
26
|
+
else if (dep === 'insights' && !rootDataStatus.hasInsights) missing.push('insights');
|
|
27
|
+
else if (dep === 'social' && !rootDataStatus.hasSocial) missing.push('social');
|
|
28
|
+
else if (dep === 'history' && !rootDataStatus.hasHistory) missing.push('history');
|
|
36
29
|
}
|
|
37
30
|
return { canRun: missing.length === 0, missing };
|
|
38
31
|
}
|
|
39
32
|
|
|
40
|
-
/**
|
|
41
|
-
* Stage 3: Check root data availability for a date
|
|
42
|
-
* @param {string} dateStr - The date to check (YYYY-MM-DD).
|
|
43
|
-
* @param {object} config - The computation system config.
|
|
44
|
-
* @param {object} dependencies - All shared dependencies (db, logger, etc.).
|
|
45
|
-
* @param {object} earliestDates - The map of earliest data dates.
|
|
46
|
-
* @returns {Promise<object|null>} The root data object or null.
|
|
47
|
-
*/
|
|
48
33
|
async function checkRootDataAvailability(dateStr, config, dependencies, earliestDates) {
|
|
49
34
|
const { logger } = dependencies;
|
|
50
|
-
logger.log('INFO', `[PassRunner] Checking root data for ${dateStr}...`);
|
|
51
35
|
const dateToProcess = new Date(dateStr + 'T00:00:00Z');
|
|
52
36
|
|
|
53
|
-
let portfolioRefs = [],
|
|
37
|
+
let portfolioRefs = [], historyRefs = [];
|
|
54
38
|
let hasPortfolio = false, hasInsights = false, hasSocial = false, hasHistory = false;
|
|
39
|
+
let insightsData = null, socialData = null;
|
|
55
40
|
|
|
56
41
|
try {
|
|
57
42
|
const tasks = [];
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
if (dateToProcess >= earliestDates.
|
|
63
|
-
tasks.push(
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
tasks.push(loadDailySocialPostInsights(config, dependencies, dateStr).then(res => { socialData = res; hasSocial = !!res; }));
|
|
67
|
-
}
|
|
68
|
-
if (dateToProcess >= earliestDates.history) {
|
|
69
|
-
tasks.push(getHistoryPartRefs(config, dependencies, dateStr).then(res => { historyRefs = res; hasHistory = !!(res?.length); }));
|
|
70
|
-
}
|
|
43
|
+
if (dateToProcess >= earliestDates.portfolio)
|
|
44
|
+
tasks.push(getPortfolioPartRefs(config, dependencies, dateStr).then(r => { portfolioRefs = r; hasPortfolio = !!r.length; }));
|
|
45
|
+
if (dateToProcess >= earliestDates.insights)
|
|
46
|
+
tasks.push(loadDailyInsights(config, dependencies, dateStr).then(r => { insightsData = r; hasInsights = !!r; }));
|
|
47
|
+
if (dateToProcess >= earliestDates.social)
|
|
48
|
+
tasks.push(loadDailySocialPostInsights(config, dependencies, dateStr).then(r => { socialData = r; hasSocial = !!r; }));
|
|
49
|
+
if (dateToProcess >= earliestDates.history)
|
|
50
|
+
tasks.push(getHistoryPartRefs(config, dependencies, dateStr).then(r => { historyRefs = r; hasHistory = !!r.length; }));
|
|
71
51
|
|
|
72
52
|
await Promise.all(tasks);
|
|
73
53
|
|
|
74
|
-
|
|
54
|
+
if (!(hasPortfolio || hasInsights || hasSocial || hasHistory)) return null;
|
|
75
55
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
todayInsights: insightsData,
|
|
81
|
-
todaySocialPostInsights: socialData,
|
|
82
|
-
historyRefs,
|
|
83
|
-
status: { hasPortfolio, hasInsights, hasSocial, hasHistory }
|
|
56
|
+
return {
|
|
57
|
+
portfolioRefs, historyRefs,
|
|
58
|
+
todayInsights: insightsData, todaySocialPostInsights: socialData,
|
|
59
|
+
status: { hasPortfolio, hasInsights, hasSocial, hasHistory }
|
|
84
60
|
};
|
|
85
61
|
} catch (err) {
|
|
86
|
-
logger.log('ERROR', `
|
|
62
|
+
logger.log('ERROR', `Error checking data: ${err.message}`);
|
|
87
63
|
return null;
|
|
88
64
|
}
|
|
89
65
|
}
|
|
90
66
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
* @param {string} dateStr - The date to fetch (YYYY-MM-DD).
|
|
94
|
-
* @param {Array} calcsInPass - The calculations in the *current* pass.
|
|
95
|
-
* @param {Array} fullManifest - The *entire* computation manifest.
|
|
96
|
-
* @param {object} config - The computation system config.
|
|
97
|
-
* @param {object} dependencies - Shared dependencies.
|
|
98
|
-
* @returns {Promise<object>} A map of { [calcName]: result } for all found dependencies.
|
|
99
|
-
*/
|
|
100
|
-
async function fetchExistingResults(dateStr, calcsInPass, fullManifest, config, { db, logger }) {
|
|
67
|
+
// --- OPTIMIZED FETCH ---
|
|
68
|
+
async function fetchExistingResults(dateStr, calcsInPass, fullManifest, config, { db }, includeSelf = false) {
|
|
101
69
|
const manifestMap = new Map(fullManifest.map(c => [normalizeName(c.name), c]));
|
|
102
70
|
const calcsToFetch = new Set();
|
|
103
71
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
if (
|
|
109
|
-
|
|
110
|
-
calcsToFetch.add(normalizeName(depName));
|
|
111
|
-
}
|
|
72
|
+
for (const calc of calcsInPass) {
|
|
73
|
+
if (calc.dependencies) {
|
|
74
|
+
calc.dependencies.forEach(d => calcsToFetch.add(normalizeName(d)));
|
|
75
|
+
}
|
|
76
|
+
if (includeSelf && calc.isHistorical) {
|
|
77
|
+
calcsToFetch.add(normalizeName(calc.name));
|
|
112
78
|
}
|
|
113
79
|
}
|
|
114
80
|
|
|
115
|
-
if (!calcsToFetch.size)
|
|
116
|
-
|
|
117
|
-
logger.log('INFO', `[PassRunner] Checking for ${calcsToFetch.size} existing results and dependencies for ${dateStr}...`);
|
|
81
|
+
if (!calcsToFetch.size) return {};
|
|
118
82
|
|
|
83
|
+
const fetched = {};
|
|
119
84
|
const docRefs = [];
|
|
120
|
-
const
|
|
85
|
+
const names = [];
|
|
121
86
|
|
|
122
|
-
for (const
|
|
123
|
-
const
|
|
124
|
-
if (
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
.doc(calcManifest.category || 'unknown')
|
|
131
|
-
.collection(config.computationsSubcollection)
|
|
132
|
-
.doc(calcName)
|
|
133
|
-
);
|
|
134
|
-
depNames.push(calcName);
|
|
87
|
+
for (const name of calcsToFetch) {
|
|
88
|
+
const m = manifestMap.get(name);
|
|
89
|
+
if (m) {
|
|
90
|
+
docRefs.push(db.collection(config.resultsCollection).doc(dateStr)
|
|
91
|
+
.collection(config.resultsSubcollection).doc(m.category || 'unknown')
|
|
92
|
+
.collection(config.computationsSubcollection).doc(name));
|
|
93
|
+
names.push(name);
|
|
94
|
+
}
|
|
135
95
|
}
|
|
136
|
-
|
|
137
|
-
const fetched = {};
|
|
96
|
+
|
|
138
97
|
if (docRefs.length) {
|
|
139
|
-
const
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
if (data && data._completed === true) { fetched[depNames[i]] = data;
|
|
144
|
-
} else { fetched[depNames[i]] = null; // Treat as not existing
|
|
98
|
+
const snaps = await db.getAll(...docRefs);
|
|
99
|
+
snaps.forEach((doc, i) => {
|
|
100
|
+
if(doc.exists && doc.data()._completed) {
|
|
101
|
+
fetched[names[i]] = doc.data();
|
|
145
102
|
}
|
|
146
103
|
});
|
|
147
104
|
}
|
|
148
|
-
|
|
149
|
-
// Verbose logging for what was found/missing
|
|
150
|
-
const foundDeps = Object.entries(fetched).filter(([, data]) => data !== null).map(([key]) => key);
|
|
151
|
-
const missingDeps = Object.entries(fetched).filter(([, data]) => data === null).map(([key]) => key);
|
|
152
|
-
if (foundDeps.length > 0) { logger.log('TRACE', `[PassRunner] Found ${foundDeps.length} existing results for ${dateStr}: [${foundDeps.join(', ')}]`); }
|
|
153
|
-
if (missingDeps.length > 0) { logger.log('TRACE', `[PassRunner] Did not find ${missingDeps.length} results for ${dateStr}: [${missingDeps.join(', ')}]`); }
|
|
154
|
-
|
|
155
105
|
return fetched;
|
|
156
106
|
}
|
|
157
107
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
let earliestRunDate = new Date('1970-01-01T00:00:00Z');
|
|
168
|
-
const dependencies = calc.rootDataDependencies || [];
|
|
169
|
-
|
|
170
|
-
for (const dep of dependencies) {
|
|
171
|
-
if (dep === 'portfolio' && earliestDates.portfolio > earliestRunDate) earliestRunDate = earliestDates.portfolio;
|
|
172
|
-
if (dep === 'history' && earliestDates.history > earliestRunDate) earliestRunDate = earliestDates.history;
|
|
173
|
-
if (dep === 'social' && earliestDates.social > earliestRunDate) earliestRunDate = earliestDates.social;
|
|
174
|
-
if (dep === 'insights' && earliestDates.insights > earliestRunDate) earliestRunDate = earliestDates.insights;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
// If it's historical, it needs T-1 data, so add one day
|
|
178
|
-
if (calc.isHistorical && earliestRunDate.getTime() > 0) { earliestRunDate.setUTCDate(earliestRunDate.getUTCDate() + 1); }
|
|
179
|
-
return earliestRunDate;
|
|
180
|
-
};
|
|
181
|
-
|
|
182
|
-
const filterCalc = (calc) => {
|
|
183
|
-
// 1. Skip if already completed
|
|
184
|
-
if (existingResults[calc.name]) { logger.log('TRACE', `[Pass ${passToRun}] Skipping ${calc.name} for ${dateStr}. Result already exists (and is complete).`);
|
|
185
|
-
skipped.add(calc.name);
|
|
186
|
-
return false;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
// 2. Skip if date is before this calc's earliest possible run date
|
|
190
|
-
const earliestRunDate = getTrueEarliestRunDate(calc);
|
|
191
|
-
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)}).`);
|
|
192
|
-
skipped.add(calc.name);
|
|
193
|
-
return false;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
// 3. Skip if missing root data
|
|
197
|
-
const { canRun, missing: missingRoot } = checkRootDependencies(calc, rootDataStatus);
|
|
198
|
-
if (!canRun) { logger.log('INFO', `[Pass ${passToRun}] Skipping ${calc.name} for ${dateStr}. Data missing for this date: [${missingRoot.join(', ')}]`);
|
|
199
|
-
skipped.add(calc.name);
|
|
200
|
-
return false;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
// 4. (Meta Calcs) Skip if missing computed dependencies
|
|
204
|
-
if (calc.type === 'meta') {
|
|
205
|
-
const missingDeps = (calc.dependencies || [])
|
|
206
|
-
.map(normalizeName)
|
|
207
|
-
.filter(d => !existingResults[d]); // This check is now robust
|
|
208
|
-
|
|
209
|
-
if (missingDeps.length > 0) { logger.log('WARN', `[Pass ${passToRun} Meta] Skipping ${calc.name} for ${dateStr}. Missing computed deps: [${missingDeps.join(', ')}]`);
|
|
210
|
-
skipped.add(calc.name);
|
|
211
|
-
return false;
|
|
212
|
-
}
|
|
213
|
-
}
|
|
108
|
+
function filterCalculations(standardCalcs, metaCalcs, rootDataStatus, existingResults, passToRun, dateStr, earliestDates) { // TODO passtorun is unused, why?
|
|
109
|
+
const filter = (c) => { // Since we are using toplogical sorting for deps, surely the pass being run is implicit
|
|
110
|
+
if (existingResults[c.name]) return false; // Am a bit confused here. Feel this may be a bug.
|
|
111
|
+
let earliest = new Date('1970-01-01');
|
|
112
|
+
(c.rootDataDependencies || []).forEach(d => { if(earliestDates[d] > earliest) earliest = earliestDates[d]; });
|
|
113
|
+
if (c.isHistorical) earliest.setUTCDate(earliest.getUTCDate() + 1);
|
|
114
|
+
if (new Date(dateStr) < earliest) return false;
|
|
115
|
+
if (!checkRootDependencies(c, rootDataStatus).canRun) return false;
|
|
116
|
+
if (c.type === 'meta' && c.dependencies && c.dependencies.some(d => !existingResults[normalizeName(d)])) return false;
|
|
214
117
|
return true;
|
|
215
118
|
};
|
|
216
|
-
|
|
217
|
-
const standardCalcsToRun = standardCalcs.filter(filterCalc);
|
|
218
|
-
const metaCalcsToRun = metaCalcs.filter(filterCalc);
|
|
219
|
-
|
|
220
|
-
return { standardCalcsToRun, metaCalcsToRun };
|
|
119
|
+
return { standardCalcsToRun: standardCalcs.filter(filter), metaCalcsToRun: metaCalcs.filter(filter) };
|
|
221
120
|
}
|
|
222
121
|
|
|
223
|
-
|
|
224
|
-
* Stage 6: Initialize calculator instances
|
|
225
|
-
* @param {Array} calcs - The calculations to initialize.
|
|
226
|
-
* @param {object} logger - The logger instance.
|
|
227
|
-
* @returns {object} A map of { [calcName]: instance }.
|
|
228
|
-
*/
|
|
229
|
-
function initializeCalculators(calcs, logger) {
|
|
230
|
-
const state = {};
|
|
231
|
-
for (const c of calcs) {
|
|
232
|
-
const name = normalizeName(c.name);
|
|
233
|
-
const Cl = c.class;
|
|
234
|
-
if (typeof Cl === 'function') {
|
|
235
|
-
try {
|
|
236
|
-
const inst = new Cl();
|
|
237
|
-
inst.manifest = c; // Attach manifest for context
|
|
238
|
-
state[name] = inst;
|
|
239
|
-
} catch (e) { logger.warn(`Initialization failed for ${name}`, { errorMessage: e.message });
|
|
240
|
-
state[name] = null;
|
|
241
|
-
}
|
|
242
|
-
} else { logger.warn(`Class is missing for ${name}`);
|
|
243
|
-
state[name] = null;
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
return state;
|
|
247
|
-
}
|
|
122
|
+
// --- EXECUTION DELEGATES ---
|
|
248
123
|
|
|
249
|
-
|
|
250
|
-
* Stage 7: Load T-1 (yesterday) data needed by historical calculations.
|
|
251
|
-
* --- THIS IS THE FULLY CORRECTED FUNCTION (WITH FRIEND'S BUG FIX) ---
|
|
252
|
-
*/
|
|
253
|
-
async function loadHistoricalData(date, calcs, config, deps, rootData) {
|
|
124
|
+
async function streamAndProcess(dateStr, state, passName, config, deps, rootData, portfolioRefs, historyRefs, fetchedDeps, previousFetchedDeps) {
|
|
254
125
|
const { logger } = deps;
|
|
255
|
-
const
|
|
256
|
-
const tasks = [];
|
|
257
|
-
|
|
258
|
-
// Check what T-1 data is needed by any calc in this pass
|
|
259
|
-
const needsYesterdayInsights = calcs.some(c => c.isHistorical && c.rootDataDependencies.includes('insights'));
|
|
260
|
-
const needsYesterdaySocial = calcs.some(c => c.isHistorical && c.rootDataDependencies.includes('social'));
|
|
261
|
-
const needsYesterdayPortfolio = calcs.some(c => c.isHistorical && c.rootDataDependencies.includes('portfolio'));
|
|
262
|
-
const needsYesterdayHistory = calcs.some(c => c.isHistorical && c.rootDataDependencies.includes('history')); // <-- The check
|
|
263
|
-
const needsYesterdayDependencies = calcs.some(c => c.isHistorical && c.dependencies && c.dependencies.length > 0);
|
|
264
|
-
|
|
265
|
-
const prev = new Date(date);
|
|
266
|
-
prev.setUTCDate(prev.getUTCDate() - 1);
|
|
267
|
-
const prevStr = prev.toISOString().slice(0, 10);
|
|
126
|
+
const controller = new ComputationController(config, deps);
|
|
268
127
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
if (needsYesterdaySocial) {
|
|
275
|
-
tasks.push((async () => { logger.log('INFO', `[PassRunner] Loading YESTERDAY social data for ${prevStr}`);
|
|
276
|
-
updated.yesterdaySocialPostInsights = await loadDailySocialPostInsights(config, deps, prevStr);
|
|
277
|
-
})());
|
|
278
|
-
}
|
|
279
|
-
if (needsYesterdayPortfolio) {
|
|
280
|
-
tasks.push((async () => { logger.log('INFO', `[PassRunner] Getting YESTERDAY portfolio refs for ${prevStr}`);
|
|
281
|
-
updated.yesterdayPortfolioRefs = await getPortfolioPartRefs(config, deps, prevStr);
|
|
282
|
-
})());
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
// --- THIS IS THE MISSING LOGIC BLOCK (FIXED) ---
|
|
286
|
-
if (needsYesterdayHistory) {
|
|
287
|
-
tasks.push((async () => { logger.log('INFO', `[PassRunner] Getting YESTERDAY history refs for ${prevStr}`);
|
|
288
|
-
updated.yesterdayHistoryRefs = await getHistoryPartRefs(config, deps, prevStr);
|
|
289
|
-
})());
|
|
290
|
-
}
|
|
291
|
-
// --- END MISSING LOGIC BLOCK ---
|
|
292
|
-
|
|
293
|
-
if (needsYesterdayDependencies) {
|
|
294
|
-
tasks.push((async () => { logger.log('INFO', `[PassRunner] Loading YESTERDAY computed dependencies for ${prevStr}`);
|
|
295
|
-
// This fetches T-1 results for *all* calcs in the current pass,
|
|
296
|
-
// which is robust and covers all historical dependency needs.
|
|
297
|
-
updated.yesterdayDependencyData = await fetchExistingResults(prevStr, calcs, calcs.map(c => c.manifest), config, deps);
|
|
298
|
-
})());
|
|
299
|
-
}
|
|
128
|
+
const calcs = Object.values(state).filter(c => c && c.manifest);
|
|
129
|
+
const streamingCalcs = calcs.filter(c =>
|
|
130
|
+
c.manifest.rootDataDependencies.includes('portfolio') ||
|
|
131
|
+
c.manifest.rootDataDependencies.includes('history')
|
|
132
|
+
);
|
|
300
133
|
|
|
301
|
-
|
|
302
|
-
return updated;
|
|
303
|
-
}
|
|
134
|
+
if (streamingCalcs.length === 0) return;
|
|
304
135
|
|
|
305
|
-
|
|
306
|
-
* Stage 8: Stream and process data for standard calculations.
|
|
307
|
-
* --- THIS IS THE FULLY CORRECTED FUNCTION (WITH ALL FIXES) ---
|
|
308
|
-
* --- REPLICATES THE 7-ARGUMENT "HACK" SIGNATURE FROM TEST HARNESS ---
|
|
309
|
-
*/
|
|
310
|
-
async function streamAndProcess(dateStr, state, passName, config, deps, rootData, portfolioRefs, historyRefs, fetchedDeps) {
|
|
311
|
-
const { logger, calculationUtils } = deps;
|
|
312
|
-
const { todayInsights, yesterdayInsights, todaySocialPostInsights, yesterdaySocialPostInsights, yesterdayDependencyData } = rootData;
|
|
313
|
-
|
|
314
|
-
// Create the shared context object
|
|
315
|
-
const mappings = await calculationUtils.loadInstrumentMappings();
|
|
316
|
-
const context = {
|
|
317
|
-
instrumentMappings: mappings.instrumentToTicker,
|
|
318
|
-
sectorMapping: mappings.instrumentToSector,
|
|
319
|
-
todayDateStr: dateStr,
|
|
320
|
-
dependencies: deps,
|
|
321
|
-
config,
|
|
322
|
-
yesterdaysDependencyData: yesterdayDependencyData
|
|
323
|
-
};
|
|
136
|
+
logger.log('INFO', `[${passName}] Streaming for ${streamingCalcs.length} computations...`);
|
|
324
137
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
for (const name in state) {
|
|
329
|
-
const calc = state[name];
|
|
330
|
-
if (!calc || typeof calc.process !== 'function') continue;
|
|
331
|
-
|
|
332
|
-
const cat = calc.manifest.category;
|
|
333
|
-
if (cat === 'socialPosts' || cat === 'insights') {
|
|
334
|
-
if (firstUser) {
|
|
335
|
-
logger.log('INFO', `[${passName}] Running non-streaming calc: ${name} for ${dateStr}`);
|
|
336
|
-
|
|
337
|
-
// --- CALLING 7-ARGUMENT "HACK" SIGNATURE (for non-streaming) ---
|
|
338
|
-
// We emulate the test harness's `process` call for social/insights
|
|
339
|
-
const userContext = { ...context, userType: 'n/a' };
|
|
340
|
-
const todayPayload = null; // No user data
|
|
341
|
-
const yesterdayPayload = null; // No user data
|
|
138
|
+
await controller.loader.loadMappings();
|
|
139
|
+
const prevDate = new Date(dateStr + 'T00:00:00Z'); prevDate.setUTCDate(prevDate.getUTCDate() - 1);
|
|
140
|
+
const prevDateStr = prevDate.toISOString().slice(0, 10);
|
|
342
141
|
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
todayPayload, // Arg 1: The data object
|
|
346
|
-
yesterdayPayload, // Arg 2: Yesterday's data
|
|
347
|
-
null, // Arg 3: User ID
|
|
348
|
-
userContext, // Arg 4: Context
|
|
349
|
-
todayInsights, // Arg 5: Today Insights
|
|
350
|
-
yesterdayInsights, // Arg 6: Yesterday Insights
|
|
351
|
-
fetchedDeps // Arg 7: Fetched Dependencies
|
|
352
|
-
));
|
|
353
|
-
} catch (e) { logger.log('WARN', `Process error on ${name} (non-stream) for ${dateStr}`, { err: e.message }); }
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
}
|
|
142
|
+
// 1. Today's Portfolio Stream
|
|
143
|
+
const tP_iter = streamPortfolioData(config, deps, dateStr, portfolioRefs);
|
|
357
144
|
|
|
358
|
-
//
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
)
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
145
|
+
// 2. Yesterday's Portfolio Stream (for 'isHistorical' calcs)
|
|
146
|
+
const needsYesterdayPortfolio = streamingCalcs.some(c => c.manifest.isHistorical);
|
|
147
|
+
const yP_iter = (needsYesterdayPortfolio && rootData.yesterdayPortfolioRefs)
|
|
148
|
+
? streamPortfolioData(config, deps, prevDateStr, rootData.yesterdayPortfolioRefs)
|
|
149
|
+
: null;
|
|
150
|
+
|
|
151
|
+
// 3. Today's History Stream (NEW)
|
|
152
|
+
const needsTradingHistory = streamingCalcs.some(c => c.manifest.rootDataDependencies.includes('history'));
|
|
153
|
+
const tH_iter = (needsTradingHistory && historyRefs)
|
|
154
|
+
? streamHistoryData(config, deps, dateStr, historyRefs)
|
|
155
|
+
: null;
|
|
156
|
+
|
|
157
|
+
let yP_chunk = {};
|
|
158
|
+
let tH_chunk = {};
|
|
159
|
+
|
|
160
|
+
for await (const tP_chunk of tP_iter) {
|
|
161
|
+
if (yP_iter) yP_chunk = (await yP_iter.next()).value || {};
|
|
162
|
+
if (tH_iter) tH_chunk = (await tH_iter.next()).value || {};
|
|
163
|
+
|
|
164
|
+
const promises = streamingCalcs.map(calc =>
|
|
165
|
+
controller.executor.executePerUser(
|
|
166
|
+
calc,
|
|
167
|
+
calc.manifest,
|
|
168
|
+
dateStr,
|
|
169
|
+
tP_chunk,
|
|
170
|
+
yP_chunk, // Yesterday Portfolio
|
|
171
|
+
tH_chunk, // Today History (NEW ARGUMENT)
|
|
172
|
+
fetchedDeps,
|
|
173
|
+
previousFetchedDeps
|
|
174
|
+
)
|
|
175
|
+
);
|
|
176
|
+
await Promise.all(promises);
|
|
371
177
|
}
|
|
372
|
-
|
|
373
|
-
// --- FIX 2: THE TYPO (From friend) ---
|
|
374
|
-
// This log message now correctly references 'calcsThatStream'.
|
|
375
|
-
logger.log('INFO', `[${passName}] Streaming portfolio & historical data for ${calcsThatStream.length} calcs for ${dateStr}...`);
|
|
376
|
-
|
|
377
|
-
const prevDate = new Date(dateStr + 'T00:00:00Z');
|
|
378
|
-
prevDate.setUTCDate(prevDate.getUTCDate() - 1);
|
|
379
|
-
const prevDateStr = prevDate.toISOString().slice(0, 10);
|
|
380
|
-
|
|
381
|
-
// Check which iterators we need
|
|
382
|
-
const needsYesterdayPortfolio = Object.values(state).some(c => c && c.manifest.isHistorical && c.manifest.rootDataDependencies.includes('portfolio'));
|
|
383
|
-
const needsTodayHistory = Object.values(state).some(c => c && c.manifest.rootDataDependencies.includes('history'));
|
|
384
|
-
const needsYesterdayHistory = Object.values(state).some(c => c && c.manifest.isHistorical && c.manifest.rootDataDependencies.includes('history'));
|
|
385
|
-
|
|
386
|
-
// --- Create all necessary iterators ---
|
|
387
|
-
// (This code is now correct because rootData.yesterdayHistoryRefs will be populated by Fix 1)
|
|
388
|
-
const tP_iterator = streamPortfolioData(config, deps, dateStr, portfolioRefs);
|
|
389
|
-
const yP_iterator = needsYesterdayPortfolio ? streamPortfolioData(config, deps, prevDateStr, rootData.yesterdayPortfolioRefs) : null;
|
|
390
|
-
const hT_iterator = needsTodayHistory ? streamHistoryData(config, deps, dateStr, historyRefs) : null;
|
|
391
|
-
const hY_iterator = needsYesterdayHistory ? streamHistoryData(config, deps, prevDateStr, rootData.yesterdayHistoryRefs) : null;
|
|
392
|
-
|
|
393
|
-
let yesterdayPortfolios = {};
|
|
394
|
-
let todayHistoryData = {};
|
|
395
|
-
let yesterdayHistoryData = {};
|
|
396
|
-
|
|
397
|
-
// Pre-load the first chunk of historical data
|
|
398
|
-
if (yP_iterator) { Object.assign(yesterdayPortfolios, (await yP_iterator.next()).value || {}); }
|
|
399
|
-
if (hT_iterator) { Object.assign(todayHistoryData, (await hT_iterator.next()).value || {}); }
|
|
400
|
-
if (hY_iterator) { Object.assign(yesterdayHistoryData, (await hY_iterator.next()).value || {}); }
|
|
401
|
-
|
|
402
|
-
for await (const chunk of tP_iterator) {
|
|
403
|
-
// Load the *next* chunk of historical data to stay in sync
|
|
404
|
-
if (yP_iterator) { Object.assign(yesterdayPortfolios, (await yP_iterator.next()).value || {}); }
|
|
405
|
-
if (hT_iterator) { Object.assign(todayHistoryData, (await hT_iterator.next()).value || {}); }
|
|
406
|
-
if (hY_iterator) { Object.assign(yesterdayHistoryData, (await hY_iterator.next()).value || {}); }
|
|
407
|
-
|
|
408
|
-
for (const uid in chunk) {
|
|
409
|
-
const p = chunk[uid];
|
|
410
|
-
if (!p) continue;
|
|
411
|
-
|
|
412
|
-
const userType = p.PublicPositions ? 'speculator' : 'normal';
|
|
413
|
-
const userContext = { ...context, userType };
|
|
414
|
-
|
|
415
|
-
// Get corresponding T-1 data
|
|
416
|
-
const pY = yesterdayPortfolios[uid] || null;
|
|
417
|
-
const hT = todayHistoryData[uid] || null;
|
|
418
|
-
const hY = yesterdayHistoryData[uid] || null; // <-- This will now have data
|
|
419
|
-
|
|
420
|
-
for (const name in state) {
|
|
421
|
-
const calc = state[name];
|
|
422
|
-
if (!calc || typeof calc.process !== 'function') continue;
|
|
423
|
-
|
|
424
|
-
const manifest = calc.manifest;
|
|
425
|
-
const cat = manifest.category;
|
|
426
|
-
const isSocialOrInsights = cat === 'socialPosts' || cat === 'insights';
|
|
427
|
-
if (isSocialOrInsights) continue; // Handled above
|
|
428
|
-
|
|
429
|
-
const isSpeculatorCalc = cat === 'speculators';
|
|
430
|
-
const isUserProcessed = name === 'users-processed';
|
|
431
|
-
|
|
432
|
-
if (userType === 'normal' && isSpeculatorCalc) continue;
|
|
433
|
-
if (userType === 'speculator' && !isSpeculatorCalc && !isUserProcessed) continue;
|
|
434
|
-
|
|
435
|
-
if (manifest.isHistorical && !pY) {
|
|
436
|
-
if (cat !== 'behavioural' && name !== 'historical-performance-aggregator') {
|
|
437
|
-
continue;
|
|
438
|
-
}
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
// --- FIX 3: REPLICATE 7-ARGUMENT "HACK" SIGNATURE ---
|
|
442
|
-
// This logic block replicates the test harness's 'todayPayload'
|
|
443
|
-
// and 'yesterdayPayload' construction.
|
|
444
|
-
const rootDataDeps = manifest.rootDataDependencies || ['portfolio'];
|
|
445
|
-
const needsHistoricalData = manifest.isHistorical || false;
|
|
446
|
-
|
|
447
|
-
let todayPayload = null;
|
|
448
|
-
let yesterdayPayload = null;
|
|
449
|
-
|
|
450
|
-
if (rootDataDeps.includes('portfolio')) {
|
|
451
|
-
todayPayload = p || {}; // Start with portfolio
|
|
452
|
-
yesterdayPayload = needsHistoricalData ? (pY || {}) : null;
|
|
453
|
-
// Nest history if also requested
|
|
454
|
-
if (rootDataDeps.includes('history')) {
|
|
455
|
-
todayPayload.history = hT;
|
|
456
|
-
if (yesterdayPayload) yesterdayPayload.history = hY;
|
|
457
|
-
}
|
|
458
|
-
} else if (rootDataDeps.includes('history')) {
|
|
459
|
-
// If *only* history is requested, it becomes Arg 1
|
|
460
|
-
todayPayload = hT;
|
|
461
|
-
yesterdayPayload = needsHistoricalData ? hY : null;
|
|
462
|
-
} else {
|
|
463
|
-
// Fallback for calcs like price-metrics
|
|
464
|
-
todayPayload = p || {};
|
|
465
|
-
yesterdayPayload = needsHistoricalData ? (pY || {}) : null;
|
|
466
|
-
}
|
|
467
|
-
// --- END PAYLOAD CONSTRUCTION ---
|
|
468
|
-
|
|
469
|
-
try {
|
|
470
|
-
// Call with the 7-argument signature
|
|
471
|
-
await Promise.resolve(calc.process(
|
|
472
|
-
todayPayload, // Arg 1: The data object (built above)
|
|
473
|
-
yesterdayPayload, // Arg 2: Yesterday's data
|
|
474
|
-
uid, // Arg 3: User ID
|
|
475
|
-
userContext, // Arg 4: Context
|
|
476
|
-
todayInsights, // Arg 5: Today Insights
|
|
477
|
-
yesterdayInsights, // Arg 6: Yesterday Insights
|
|
478
|
-
fetchedDeps // Arg 7: Fetched Dependencies
|
|
479
|
-
));
|
|
480
|
-
} catch (e) {
|
|
481
|
-
logger.log('WARN', `Process error on ${name} for ${uid} on ${dateStr}`, { err: e.message });
|
|
482
|
-
}
|
|
483
|
-
} // end for(calc)
|
|
484
|
-
|
|
485
|
-
firstUser = false;
|
|
486
|
-
|
|
487
|
-
// Clear processed users from memory
|
|
488
|
-
if (pY) { delete yesterdayPortfolios[uid]; }
|
|
489
|
-
if (hT) { delete todayHistoryData[uid]; }
|
|
490
|
-
if (hY) { delete yesterdayHistoryData[uid]; }
|
|
491
|
-
} // end for(uid in chunk)
|
|
492
|
-
} // end for await(chunk)
|
|
493
|
-
|
|
494
|
-
// Clear stale data to prevent memory leaks
|
|
495
|
-
yesterdayPortfolios = {};
|
|
496
|
-
todayHistoryData = {};
|
|
497
|
-
yesterdayHistoryData = {};
|
|
498
|
-
|
|
499
|
-
logger.log('INFO', `[${passName}] Finished streaming data for ${dateStr}.`);
|
|
178
|
+
logger.log('INFO', `[${passName}] Streaming complete.`);
|
|
500
179
|
}
|
|
501
180
|
|
|
181
|
+
// --- RUNNERS ---
|
|
502
182
|
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
*/
|
|
507
|
-
async function runStandardComputationPass(date, calcs, passName, config, deps, rootData, fetchedDeps) {
|
|
508
|
-
const dStr = date.toISOString().slice(0, 10), logger = deps.logger;
|
|
509
|
-
if (calcs.length === 0) {
|
|
510
|
-
logger.log('INFO', `[${passName}] No standard calcs to run for ${dStr} after filtering.`);
|
|
511
|
-
return;
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
logger.log('INFO', `[${passName}] Running ${dStr} with ${calcs.length} standard calcs: [${calcs.map(c => c.name).join(', ')}]`);
|
|
515
|
-
|
|
516
|
-
// Load T-1 data (portfolio, insights, social, history, computed)
|
|
517
|
-
const fullRoot = await loadHistoricalData(date, calcs, config, deps, rootData);
|
|
183
|
+
async function runStandardComputationPass(date, calcs, passName, config, deps, rootData, fetchedDeps, previousFetchedDeps) {
|
|
184
|
+
const dStr = date.toISOString().slice(0, 10);
|
|
185
|
+
const logger = deps.logger;
|
|
518
186
|
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
// --- END CHANGE ---
|
|
526
|
-
|
|
527
|
-
// --- Verbose Logging Setup ---
|
|
528
|
-
const successCalcs = [];
|
|
529
|
-
const failedCalcs = [];
|
|
530
|
-
|
|
531
|
-
const standardWrites = [];
|
|
532
|
-
const shardedWrites = {};
|
|
533
|
-
const schemasToStore = [];
|
|
534
|
-
|
|
535
|
-
// --- Get Results ---
|
|
536
|
-
for (const name in state) {
|
|
537
|
-
const calc = state[name];
|
|
538
|
-
if (!calc || typeof calc.getResult !== 'function') continue;
|
|
187
|
+
const fullRoot = { ...rootData };
|
|
188
|
+
if (calcs.some(c => c.isHistorical)) {
|
|
189
|
+
const prev = new Date(date); prev.setUTCDate(prev.getUTCDate() - 1);
|
|
190
|
+
const prevStr = prev.toISOString().slice(0, 10);
|
|
191
|
+
fullRoot.yesterdayPortfolioRefs = await getPortfolioPartRefs(config, deps, prevStr);
|
|
192
|
+
}
|
|
539
193
|
|
|
194
|
+
const state = {};
|
|
195
|
+
for (const c of calcs) {
|
|
540
196
|
try {
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
if (result && Object.keys(result).length > 0) {
|
|
547
|
-
const standardResult = {};
|
|
548
|
-
|
|
549
|
-
// --- Handle Sharded Writes ---
|
|
550
|
-
for (const key in result) {
|
|
551
|
-
if (key.startsWith('sharded_')) {
|
|
552
|
-
const shardedData = result[key];
|
|
553
|
-
for (const collectionName in shardedData) {
|
|
554
|
-
if (!shardedWrites[collectionName]) shardedWrites[collectionName] = {};
|
|
555
|
-
Object.assign(shardedWrites[collectionName], shardedData[collectionName]);
|
|
556
|
-
}
|
|
557
|
-
} else {
|
|
558
|
-
standardResult[key] = result[key];
|
|
559
|
-
}
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
// --- Handle Standard Writes ---
|
|
563
|
-
if (Object.keys(standardResult).length > 0) {
|
|
564
|
-
const docRef = deps.db.collection(config.resultsCollection).doc(dStr)
|
|
565
|
-
.collection(config.resultsSubcollection).doc(calc.manifest.category)
|
|
566
|
-
.collection(config.computationsSubcollection).doc(name);
|
|
567
|
-
|
|
568
|
-
standardResult._completed = true; // Mark as complete
|
|
569
|
-
standardWrites.push({ ref: docRef, data: standardResult });
|
|
570
|
-
}
|
|
571
|
-
|
|
572
|
-
// --- Capture Schema ---
|
|
573
|
-
const calcClass = calc.manifest.class;
|
|
574
|
-
let staticSchema = null;
|
|
575
|
-
if (calcClass && typeof calcClass.getSchema === 'function') {
|
|
576
|
-
try {
|
|
577
|
-
staticSchema = calcClass.getSchema();
|
|
578
|
-
} catch (e) { logger.log('WARN', `[SchemaCapture] Failed to get static schema for ${name} on ${dStr}`, { err: e.message }); }
|
|
579
|
-
} else { logger.log('TRACE', `[SchemaCapture] No static schema found for ${name}. Skipping manifest entry.`); }
|
|
580
|
-
|
|
581
|
-
if (staticSchema) {
|
|
582
|
-
schemasToStore.push({
|
|
583
|
-
name,
|
|
584
|
-
category: calc.manifest.category,
|
|
585
|
-
schema: staticSchema,
|
|
586
|
-
metadata: {
|
|
587
|
-
isHistorical: calc.manifest.isHistorical || false,
|
|
588
|
-
dependencies: calc.manifest.dependencies || [],
|
|
589
|
-
rootDataDependencies: calc.manifest.rootDataDependencies || [],
|
|
590
|
-
pass: calc.manifest.pass,
|
|
591
|
-
type: calc.manifest.type || 'standard'
|
|
592
|
-
}
|
|
593
|
-
});
|
|
594
|
-
}
|
|
595
|
-
successCalcs.push(name);
|
|
596
|
-
} else {
|
|
597
|
-
// Calc ran but returned no data
|
|
598
|
-
successCalcs.push(name);
|
|
599
|
-
}
|
|
600
|
-
} catch (e) { logger.log('ERROR', `getResult failed for ${name} on ${dStr}`, { err: e.message, stack: e.stack });
|
|
601
|
-
failedCalcs.push({ name, error: e.message });
|
|
602
|
-
}
|
|
603
|
-
} // --- End Get Results Loop ---
|
|
604
|
-
|
|
605
|
-
// --- Commit Writes ---
|
|
606
|
-
if (schemasToStore.length > 0) {
|
|
607
|
-
batchStoreSchemas(deps, config, schemasToStore).catch(err => { logger.log('WARN', `[SchemaCapture] Non-blocking schema storage failed for ${dStr}`, { errorMessage: err.message }); }); }
|
|
608
|
-
|
|
609
|
-
if (standardWrites.length > 0) { await commitBatchInChunks(config, deps, standardWrites, `${passName} Standard ${dStr}`); }
|
|
610
|
-
|
|
611
|
-
for (const docPath in shardedWrites) {
|
|
612
|
-
const docData = shardedWrites[docPath];
|
|
613
|
-
const shardedDocWrites = [];
|
|
614
|
-
let docRef;
|
|
615
|
-
if (docPath.includes('/')) { docRef = deps.db.doc(docPath);
|
|
616
|
-
} else { const collection = (docPath.startsWith('user_profile_history')) ? config.shardedUserProfileCollection : config.shardedProfitabilityCollection; docRef = deps.db.collection(collection).doc(docPath); }
|
|
617
|
-
|
|
618
|
-
if (docData && typeof docData === 'object' && !Array.isArray(docData)) {
|
|
619
|
-
docData._completed = true;
|
|
620
|
-
shardedDocWrites.push({ ref: docRef, data: docData });
|
|
621
|
-
} else { logger.log('ERROR', `[${passName}] Invalid sharded document data for ${docPath} on ${dStr}. Not an object.`, { data: docData }); }
|
|
622
|
-
|
|
623
|
-
if (shardedDocWrites.length > 0) { await commitBatchInChunks(config, deps, shardedDocWrites, `${passName} Sharded ${docPath} ${dStr}`); }
|
|
197
|
+
const inst = new c.class();
|
|
198
|
+
inst.manifest = c;
|
|
199
|
+
state[normalizeName(c.name)] = inst;
|
|
200
|
+
} catch(e) { logger.log('WARN', `Failed to init ${c.name}`); }
|
|
624
201
|
}
|
|
625
202
|
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
total_expected: calcs.length,
|
|
629
|
-
success_count: successCalcs.length,
|
|
630
|
-
failed_count: failedCalcs.length,
|
|
631
|
-
successful_calcs: successCalcs,
|
|
632
|
-
failed_calcs: failedCalcs
|
|
633
|
-
};
|
|
634
|
-
logger.log( failedCalcs.length === 0 ? 'SUCCESS' : 'WARN', `[${passName}] Completed ${dStr}.`, logMetadata );
|
|
203
|
+
await streamAndProcess(dStr, state, passName, config, deps, fullRoot, rootData.portfolioRefs, rootData.historyRefs, fetchedDeps, previousFetchedDeps);
|
|
204
|
+
await commitResults(state, dStr, passName, config, deps);
|
|
635
205
|
}
|
|
636
206
|
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
* @param {string} passName - The name of the pass (for logging).
|
|
642
|
-
* @param {object} config - Computation system config.
|
|
643
|
-
* @param {object} deps - Shared dependencies.
|
|
644
|
-
* @param {object} fetchedDeps - In-memory results from *previous* passes.
|
|
645
|
-
* @param {object} rootData - The loaded root data for today.
|
|
646
|
-
*/
|
|
647
|
-
async function runMetaComputationPass(date, calcs, passName, config, deps, fetchedDeps, rootData) {
|
|
648
|
-
const dStr = date.toISOString().slice(0, 10), logger = deps.logger;
|
|
649
|
-
if (calcs.length === 0) { logger.log('INFO', `[${passName}] No meta calcs to run for ${dStr} after filtering.`); return; }
|
|
650
|
-
|
|
651
|
-
logger.log('INFO', `[${passName}] Running ${dStr} with ${calcs.length} meta calcs: [${calcs.map(c => c.name).join(', ')}]`);
|
|
652
|
-
|
|
653
|
-
// Load T-1 data (needed for stateful meta calcs)
|
|
654
|
-
const fullRoot = await loadHistoricalData(date, calcs, config, deps, rootData);
|
|
655
|
-
|
|
656
|
-
// --- Verbose Logging Setup ---
|
|
657
|
-
const successCalcs = [];
|
|
658
|
-
const failedCalcs = [];
|
|
659
|
-
|
|
660
|
-
const standardWrites = [];
|
|
661
|
-
const shardedWrites = {};
|
|
662
|
-
const schemasToStore = [];
|
|
207
|
+
async function runMetaComputationPass(date, calcs, passName, config, deps, fetchedDeps, previousFetchedDeps, rootData) {
|
|
208
|
+
const controller = new ComputationController(config, deps);
|
|
209
|
+
const dStr = date.toISOString().slice(0, 10);
|
|
210
|
+
const state = {};
|
|
663
211
|
|
|
664
212
|
for (const mCalc of calcs) {
|
|
665
|
-
const name = normalizeName(mCalc.name);
|
|
666
|
-
const Cl = mCalc.class;
|
|
667
|
-
|
|
668
|
-
if (typeof Cl !== 'function') {
|
|
669
|
-
logger.log('ERROR', `Invalid class ${name} on ${dStr}`);
|
|
670
|
-
failedCalcs.push({ name, error: "Invalid class" });
|
|
671
|
-
continue;
|
|
672
|
-
}
|
|
673
|
-
|
|
674
|
-
const inst = new Cl();
|
|
675
|
-
|
|
676
213
|
try {
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
// Meta-calc `process` is different: it receives the date, full dependencies, config, and fetched dependencies.
|
|
684
|
-
// It *also* gets the T-1 root data via the `dependencies.rootData` object.
|
|
685
|
-
|
|
686
|
-
// --- REPLICATE 5-ARGUMENT "HACK" SIGNATURE (from test harness) ---
|
|
687
|
-
// This is the "hack" fix for meta calcs from worker.js.
|
|
688
|
-
const metaPayload = {
|
|
689
|
-
social: rootData.todaySocialPostInsights,
|
|
690
|
-
insights: rootData.todayInsights,
|
|
691
|
-
priceData: null, // You don't load this yet, but test harness has it
|
|
692
|
-
yesterdayInsights: fullRoot.yesterdayInsights,
|
|
693
|
-
yesterdayPriceData: null, // You don't load this yet
|
|
694
|
-
date: dStr
|
|
695
|
-
};
|
|
214
|
+
const inst = new mCalc.class();
|
|
215
|
+
inst.manifest = mCalc;
|
|
216
|
+
await controller.executor.executeOncePerDay(inst, mCalc, dStr, fetchedDeps, previousFetchedDeps);
|
|
217
|
+
state[normalizeName(mCalc.name)] = inst;
|
|
218
|
+
} catch (e) { deps.logger.log('ERROR', `Meta calc failed ${mCalc.name}: ${e.message}`); }
|
|
219
|
+
}
|
|
696
220
|
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
fullRoot, // Arg 2: (rootData)
|
|
700
|
-
deps, // Arg 3: (dependencies)
|
|
701
|
-
config, // Arg 4: (config)
|
|
702
|
-
fetchedDeps // Arg 5: (fetchedDependencies)
|
|
703
|
-
));
|
|
704
|
-
// --- END SIGNATURE REPLICATION ---
|
|
221
|
+
await commitResults(state, dStr, passName, config, deps);
|
|
222
|
+
}
|
|
705
223
|
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
}
|
|
718
|
-
|
|
719
|
-
}
|
|
720
|
-
}
|
|
721
|
-
|
|
722
|
-
// --- Handle Standard Writes ---
|
|
723
|
-
if (Object.keys(standardResult).length > 0) {
|
|
724
|
-
const docRef = deps.db.collection(config.resultsCollection).doc(dStr)
|
|
725
|
-
.collection(config.resultsSubcollection).doc(mCalc.category)
|
|
726
|
-
.collection(config.computationsSubcollection).doc(name);
|
|
727
|
-
|
|
728
|
-
standardResult._completed = true; // Mark as complete
|
|
729
|
-
standardWrites.push({ ref: docRef, data: standardResult });
|
|
730
|
-
}
|
|
731
|
-
|
|
732
|
-
// --- Capture Schema ---
|
|
733
|
-
const calcClass = mCalc.class;
|
|
734
|
-
let staticSchema = null;
|
|
735
|
-
if (calcClass && typeof calcClass.getSchema === 'function') {
|
|
736
|
-
try {
|
|
737
|
-
staticSchema = calcClass.getSchema();
|
|
738
|
-
} catch (e) { logger.log('WARN', `[SchemaCapture] Failed to get static schema for ${name} on ${dStr}`, { err: e.message }); }
|
|
739
|
-
} else { logger.log('TRACE', `[SchemaCapture] No static schema found for ${name}. Skipping manifest entry.`); }
|
|
740
|
-
|
|
741
|
-
if (staticSchema) {
|
|
742
|
-
schemasToStore.push({
|
|
743
|
-
name,
|
|
744
|
-
category: mCalc.category,
|
|
745
|
-
schema: staticSchema,
|
|
746
|
-
metadata: {
|
|
747
|
-
isHistorical: mCalc.isHistorical || false,
|
|
748
|
-
dependencies: mCalc.dependencies || [],
|
|
749
|
-
rootDataDependencies: mCalc.rootDataDependencies || [],
|
|
750
|
-
pass: mCalc.pass,
|
|
751
|
-
type: 'meta'
|
|
752
|
-
}
|
|
753
|
-
});
|
|
754
|
-
}
|
|
755
|
-
successCalcs.push(name);
|
|
756
|
-
} else {
|
|
757
|
-
// Calc ran but returned no data
|
|
758
|
-
successCalcs.push(name);
|
|
224
|
+
async function commitResults(stateObj, dStr, passName, config, deps) {
|
|
225
|
+
const writes = [], schemas = [], sharded = {};
|
|
226
|
+
for (const name in stateObj) {
|
|
227
|
+
const calc = stateObj[name];
|
|
228
|
+
try {
|
|
229
|
+
const result = await calc.getResult();
|
|
230
|
+
if (!result) continue;
|
|
231
|
+
const standardRes = {};
|
|
232
|
+
for (const key in result) {
|
|
233
|
+
if (key.startsWith('sharded_')) { // TODO - This should iedally become redundant, computations themselves should NEVER return an object so large it requires sharding...
|
|
234
|
+
const sData = result[key];
|
|
235
|
+
for (const c in sData) { sharded[c] = sharded[c] || {}; Object.assign(sharded[c], sData[c]); }
|
|
236
|
+
} else standardRes[key] = result[key];
|
|
759
237
|
}
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
238
|
+
if (Object.keys(standardRes).length) {
|
|
239
|
+
standardRes._completed = true;
|
|
240
|
+
writes.push({
|
|
241
|
+
ref: deps.db.collection(config.resultsCollection).doc(dStr)
|
|
242
|
+
.collection(config.resultsSubcollection).doc(calc.manifest.category)
|
|
243
|
+
.collection(config.computationsSubcollection).doc(name),
|
|
244
|
+
data: standardRes
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
if (calc.manifest.class.getSchema) {
|
|
248
|
+
schemas.push({ name, category: calc.manifest.category, schema: calc.manifest.class.getSchema(), metadata: calc.manifest });
|
|
249
|
+
}
|
|
250
|
+
} catch (e) { deps.logger.log('ERROR', `Commit failed ${name}: ${e.message}`); }
|
|
770
251
|
}
|
|
252
|
+
|
|
253
|
+
if (schemas.length) batchStoreSchemas(deps, config, schemas).catch(()=>{});
|
|
254
|
+
if (writes.length) await commitBatchInChunks(config, deps, writes, `${passName} Results`);
|
|
771
255
|
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
for (const docId in docs) {
|
|
778
|
-
const docRef = docId.includes('/') ? deps.db.doc(docId) : deps.db.collection(collectionName).doc(docId);
|
|
779
|
-
const docData = docs[docId];
|
|
780
|
-
docData._completed = true; // Mark as complete
|
|
781
|
-
shardedDocWrites.push({ ref: docRef, data: docData });
|
|
256
|
+
for (const col in sharded) {
|
|
257
|
+
const sWrites = [];
|
|
258
|
+
for (const id in sharded[col]) {
|
|
259
|
+
const ref = id.includes('/') ? deps.db.doc(id) : deps.db.collection(col).doc(id);
|
|
260
|
+
sWrites.push({ ref, data: { ...sharded[col][id], _completed: true } });
|
|
782
261
|
}
|
|
783
|
-
if (
|
|
262
|
+
if (sWrites.length) await commitBatchInChunks(config, deps, sWrites, `${passName} Sharded ${col}`);
|
|
784
263
|
}
|
|
785
|
-
|
|
786
|
-
// --- Final Verbose Log ---
|
|
787
|
-
const logMetadata = {
|
|
788
|
-
total_expected: calcs.length,
|
|
789
|
-
success_count: successCalcs.length,
|
|
790
|
-
failed_count: failedCalcs.length,
|
|
791
|
-
successful_calcs: successCalcs,
|
|
792
|
-
failed_calcs: failedCalcs
|
|
793
|
-
};
|
|
794
|
-
logger.log( failedCalcs.length === 0 ? 'SUCCESS' : 'WARN', `[${passName}] Completed ${dStr}.`,logMetadata );
|
|
795
264
|
}
|
|
796
265
|
|
|
797
|
-
|
|
798
266
|
module.exports = {
|
|
799
267
|
groupByPass,
|
|
800
268
|
checkRootDataAvailability,
|