bulltrackers-module 1.0.204 → 1.0.205
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,27 +1,26 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* FILENAME: bulltrackers-module/functions/computation-system/helpers/orchestration_helpers.js
|
|
3
|
-
* FIXED:
|
|
4
|
-
*
|
|
5
|
-
*
|
|
3
|
+
* FIXED: 'commitResults' now isolates commits PER COMPUTATION.
|
|
4
|
+
* A single failure (e.g., size limit) will only fail that specific calculation,
|
|
5
|
+
* allowing others in the same pass/date to succeed and be recorded.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
const { ComputationController }
|
|
9
|
-
const { batchStoreSchemas }
|
|
8
|
+
const { ComputationController } = require('../controllers/computation_controller');
|
|
9
|
+
const { batchStoreSchemas } = require('../utils/schema_capture');
|
|
10
10
|
const { normalizeName, commitBatchInChunks } = require('../utils/utils');
|
|
11
|
-
const {
|
|
12
|
-
getPortfolioPartRefs, loadDailyInsights, loadDailySocialPostInsights,
|
|
11
|
+
const {
|
|
12
|
+
getPortfolioPartRefs, loadDailyInsights, loadDailySocialPostInsights,
|
|
13
13
|
getHistoryPartRefs, streamPortfolioData, streamHistoryData,
|
|
14
14
|
getRelevantShardRefs, loadDataByRefs
|
|
15
15
|
} = require('../utils/data_loader');
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
TimeSeries, priceExtractor
|
|
17
|
+
const {
|
|
18
|
+
DataExtractor, HistoryExtractor, MathPrimitives, Aggregators,
|
|
19
|
+
Validators, SCHEMAS, SignalPrimitives, DistributionAnalytics,
|
|
20
|
+
TimeSeries, priceExtractor
|
|
22
21
|
} = require('../layers/math_primitives.js');
|
|
23
22
|
|
|
24
|
-
const pLimit = require('p-limit');
|
|
23
|
+
const pLimit = require('p-limit');
|
|
25
24
|
|
|
26
25
|
/**
|
|
27
26
|
* Groups calculations from a manifest by their 'pass' property.
|
|
@@ -30,21 +29,20 @@ function groupByPass(manifest) { return manifest.reduce((acc, calc) => { (acc[ca
|
|
|
30
29
|
|
|
31
30
|
/**
|
|
32
31
|
* --- PASSIVE DATA VALIDATION ---
|
|
33
|
-
* Scans a result set for suspicious patterns (e.g., a field is NULL for 100% of tickers).
|
|
34
32
|
*/
|
|
35
|
-
function validateResultPatterns(logger, calcName, results, category) {
|
|
33
|
+
function validateResultPatterns(logger, calcName, results, category) {
|
|
36
34
|
if (category === 'speculator' || category === 'speculators') return;
|
|
37
35
|
|
|
38
36
|
const tickers = Object.keys(results);
|
|
39
37
|
const totalItems = tickers.length;
|
|
40
|
-
|
|
41
|
-
if (totalItems < 5) return;
|
|
38
|
+
|
|
39
|
+
if (totalItems < 5) return;
|
|
42
40
|
|
|
43
41
|
const sampleTicker = tickers.find(t => results[t] && typeof results[t] === 'object');
|
|
44
42
|
if (!sampleTicker) return;
|
|
45
|
-
|
|
43
|
+
|
|
46
44
|
const keys = Object.keys(results[sampleTicker]);
|
|
47
|
-
|
|
45
|
+
|
|
48
46
|
keys.forEach(key => {
|
|
49
47
|
if (key.startsWith('_')) return;
|
|
50
48
|
|
|
@@ -60,28 +58,25 @@ function validateResultPatterns(logger, calcName, results, category) {
|
|
|
60
58
|
}
|
|
61
59
|
|
|
62
60
|
if (nanCount === totalItems) {
|
|
63
|
-
logger.log('ERROR', `[DataQuality] Calc '${calcName}' field '${key}' is NaN for 100% of ${totalItems} items
|
|
61
|
+
logger.log('ERROR', `[DataQuality] Calc '${calcName}' field '${key}' is NaN for 100% of ${totalItems} items.`);
|
|
64
62
|
} else if (undefinedCount === totalItems) {
|
|
65
|
-
logger.log('ERROR', `[DataQuality] Calc '${calcName}' field '${key}' is UNDEFINED for 100% of ${totalItems} items
|
|
66
|
-
}
|
|
63
|
+
logger.log('ERROR', `[DataQuality] Calc '${calcName}' field '${key}' is UNDEFINED for 100% of ${totalItems} items.`);
|
|
64
|
+
}
|
|
67
65
|
else if (nullCount > (totalItems * 0.9)) {
|
|
68
|
-
logger.log('WARN', `[DataQuality] Calc '${calcName}' field '${key}' is NULL for ${nullCount}/${totalItems} items
|
|
66
|
+
logger.log('WARN', `[DataQuality] Calc '${calcName}' field '${key}' is NULL for ${nullCount}/${totalItems} items.`);
|
|
69
67
|
}
|
|
70
68
|
});
|
|
71
69
|
}
|
|
72
70
|
|
|
73
|
-
/**
|
|
74
|
-
* Checks if all root data dependencies for a given calculation are met.
|
|
75
|
-
*/
|
|
76
71
|
function checkRootDependencies(calcManifest, rootDataStatus) {
|
|
77
72
|
const missing = [];
|
|
78
73
|
if (!calcManifest.rootDataDependencies) return { canRun: true, missing };
|
|
79
74
|
for (const dep of calcManifest.rootDataDependencies) {
|
|
80
|
-
if
|
|
81
|
-
else if (dep === 'insights'
|
|
82
|
-
else if (dep === 'social'
|
|
83
|
-
else if (dep === 'history'
|
|
84
|
-
else if (dep === 'price'
|
|
75
|
+
if (dep === 'portfolio' && !rootDataStatus.hasPortfolio) missing.push('portfolio');
|
|
76
|
+
else if (dep === 'insights' && !rootDataStatus.hasInsights) missing.push('insights');
|
|
77
|
+
else if (dep === 'social' && !rootDataStatus.hasSocial) missing.push('social');
|
|
78
|
+
else if (dep === 'history' && !rootDataStatus.hasHistory) missing.push('history');
|
|
79
|
+
else if (dep === 'price' && !rootDataStatus.hasPrices) missing.push('price');
|
|
85
80
|
}
|
|
86
81
|
return { canRun: missing.length === 0, missing };
|
|
87
82
|
}
|
|
@@ -89,30 +84,30 @@ function checkRootDependencies(calcManifest, rootDataStatus) {
|
|
|
89
84
|
async function checkRootDataAvailability(dateStr, config, dependencies, earliestDates) {
|
|
90
85
|
const { logger } = dependencies;
|
|
91
86
|
const dateToProcess = new Date(dateStr + 'T00:00:00Z');
|
|
92
|
-
let portfolioRefs
|
|
93
|
-
let hasPortfolio
|
|
94
|
-
let insightsData
|
|
87
|
+
let portfolioRefs = [], historyRefs = [];
|
|
88
|
+
let hasPortfolio = false, hasInsights = false, hasSocial = false, hasHistory = false, hasPrices = false;
|
|
89
|
+
let insightsData = null, socialData = null;
|
|
95
90
|
|
|
96
91
|
try {
|
|
97
92
|
const tasks = [];
|
|
98
93
|
if (dateToProcess >= earliestDates.portfolio) tasks.push(getPortfolioPartRefs(config, dependencies, dateStr).then(r => { portfolioRefs = r; hasPortfolio = !!r.length; }));
|
|
99
|
-
if (dateToProcess >= earliestDates.insights)
|
|
100
|
-
if (dateToProcess >= earliestDates.social)
|
|
101
|
-
if (dateToProcess >= earliestDates.history)
|
|
102
|
-
|
|
94
|
+
if (dateToProcess >= earliestDates.insights) tasks.push(loadDailyInsights(config, dependencies, dateStr).then(r => { insightsData = r; hasInsights = !!r; }));
|
|
95
|
+
if (dateToProcess >= earliestDates.social) tasks.push(loadDailySocialPostInsights(config, dependencies, dateStr).then(r => { socialData = r; hasSocial = !!r; }));
|
|
96
|
+
if (dateToProcess >= earliestDates.history) tasks.push(getHistoryPartRefs(config, dependencies, dateStr).then(r => { historyRefs = r; hasHistory = !!r.length; }));
|
|
97
|
+
|
|
103
98
|
if (dateToProcess >= earliestDates.price) {
|
|
104
99
|
tasks.push(checkPriceDataAvailability(config, dependencies).then(r => { hasPrices = r; }));
|
|
105
100
|
}
|
|
106
|
-
|
|
101
|
+
|
|
107
102
|
await Promise.all(tasks);
|
|
108
|
-
|
|
103
|
+
|
|
109
104
|
if (!(hasPortfolio || hasInsights || hasSocial || hasHistory || hasPrices)) return null;
|
|
110
|
-
|
|
111
|
-
return {
|
|
112
|
-
portfolioRefs,
|
|
113
|
-
historyRefs,
|
|
114
|
-
todayInsights: insightsData,
|
|
115
|
-
todaySocialPostInsights: socialData,
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
portfolioRefs,
|
|
108
|
+
historyRefs,
|
|
109
|
+
todayInsights: insightsData,
|
|
110
|
+
todaySocialPostInsights: socialData,
|
|
116
111
|
status: { hasPortfolio, hasInsights, hasSocial, hasHistory, hasPrices }
|
|
117
112
|
};
|
|
118
113
|
|
|
@@ -140,8 +135,8 @@ async function checkPriceDataAvailability(config, dependencies) {
|
|
|
140
135
|
|
|
141
136
|
async function fetchComputationStatus(dateStr, config, { db }) {
|
|
142
137
|
const collection = config.computationStatusCollection || 'computation_status';
|
|
143
|
-
const docRef
|
|
144
|
-
const snap
|
|
138
|
+
const docRef = db.collection(collection).doc(dateStr);
|
|
139
|
+
const snap = await docRef.get();
|
|
145
140
|
return snap.exists ? snap.data() : {};
|
|
146
141
|
}
|
|
147
142
|
|
|
@@ -155,8 +150,8 @@ async function fetchGlobalComputationStatus(config, { db }) {
|
|
|
155
150
|
async function updateComputationStatus(dateStr, updates, config, { db }) {
|
|
156
151
|
if (!updates || Object.keys(updates).length === 0) return;
|
|
157
152
|
const collection = config.computationStatusCollection || 'computation_status';
|
|
158
|
-
const docRef
|
|
159
|
-
await docRef.set(updates, { merge: true });
|
|
153
|
+
const docRef = db.collection(collection).doc(dateStr);
|
|
154
|
+
await docRef.set(updates, { merge: true });
|
|
160
155
|
}
|
|
161
156
|
|
|
162
157
|
async function updateGlobalComputationStatus(updatesByDate, config, { db }) {
|
|
@@ -172,38 +167,41 @@ async function updateGlobalComputationStatus(updatesByDate, config, { db }) {
|
|
|
172
167
|
try {
|
|
173
168
|
await docRef.update(flattenUpdates);
|
|
174
169
|
} catch (err) {
|
|
175
|
-
if (err.code === 5) {
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
170
|
+
if (err.code === 5) {
|
|
171
|
+
const deepObj = {};
|
|
172
|
+
for (const [date, statuses] of Object.entries(updatesByDate)) {
|
|
173
|
+
deepObj[date] = statuses;
|
|
174
|
+
}
|
|
175
|
+
await docRef.set(deepObj, { merge: true });
|
|
181
176
|
} else {
|
|
182
|
-
|
|
177
|
+
throw err;
|
|
183
178
|
}
|
|
184
179
|
}
|
|
185
180
|
}
|
|
186
181
|
|
|
187
182
|
async function fetchExistingResults(dateStr, calcsInPass, fullManifest, config, { db }, includeSelf = false) {
|
|
188
|
-
const manifestMap
|
|
183
|
+
const manifestMap = new Map(fullManifest.map(c => [normalizeName(c.name), c]));
|
|
189
184
|
const calcsToFetch = new Set();
|
|
190
185
|
for (const calc of calcsInPass) {
|
|
191
|
-
if (calc.dependencies)
|
|
186
|
+
if (calc.dependencies) { calc.dependencies.forEach(d => calcsToFetch.add(normalizeName(d))); }
|
|
192
187
|
if (includeSelf && calc.isHistorical) { calcsToFetch.add(normalizeName(calc.name)); }
|
|
193
188
|
}
|
|
194
189
|
if (!calcsToFetch.size) return {};
|
|
195
190
|
const fetched = {};
|
|
196
191
|
const docRefs = [];
|
|
197
|
-
const names
|
|
192
|
+
const names = [];
|
|
198
193
|
for (const name of calcsToFetch) {
|
|
199
194
|
const m = manifestMap.get(name);
|
|
200
|
-
if (m) {
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
195
|
+
if (m) {
|
|
196
|
+
docRefs.push(db.collection(config.resultsCollection).doc(dateStr)
|
|
197
|
+
.collection(config.resultsSubcollection).doc(m.category || 'unknown')
|
|
198
|
+
.collection(config.computationsSubcollection).doc(name));
|
|
199
|
+
names.push(name);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
204
202
|
if (docRefs.length) {
|
|
205
203
|
const snaps = await db.getAll(...docRefs);
|
|
206
|
-
snaps.forEach((doc, i) => {
|
|
204
|
+
snaps.forEach((doc, i) => { if (doc.exists && doc.data()._completed) { fetched[names[i]] = doc.data(); } });
|
|
207
205
|
}
|
|
208
206
|
return fetched;
|
|
209
207
|
}
|
|
@@ -212,8 +210,8 @@ async function streamAndProcess(dateStr, state, passName, config, deps, rootData
|
|
|
212
210
|
const { logger } = deps;
|
|
213
211
|
const controller = new ComputationController(config, deps);
|
|
214
212
|
const calcs = Object.values(state).filter(c => c && c.manifest);
|
|
215
|
-
const streamingCalcs = calcs.filter(c =>
|
|
216
|
-
c.manifest.rootDataDependencies.includes('portfolio') ||
|
|
213
|
+
const streamingCalcs = calcs.filter(c =>
|
|
214
|
+
c.manifest.rootDataDependencies.includes('portfolio') ||
|
|
217
215
|
c.manifest.rootDataDependencies.includes('history')
|
|
218
216
|
);
|
|
219
217
|
|
|
@@ -227,7 +225,7 @@ async function streamAndProcess(dateStr, state, passName, config, deps, rootData
|
|
|
227
225
|
|
|
228
226
|
const tP_iter = streamPortfolioData(config, deps, dateStr, portfolioRefs);
|
|
229
227
|
const needsYesterdayPortfolio = streamingCalcs.some(c => c.manifest.isHistorical);
|
|
230
|
-
const yP_iter = (needsYesterdayPortfolio && rootData.yesterdayPortfolioRefs)
|
|
228
|
+
const yP_iter = (needsYesterdayPortfolio && rootData.yesterdayPortfolioRefs) ? streamPortfolioData(config, deps, prevDateStr, rootData.yesterdayPortfolioRefs) : null;
|
|
231
229
|
const needsTradingHistory = streamingCalcs.some(c => c.manifest.rootDataDependencies.includes('history'));
|
|
232
230
|
const tH_iter = (needsTradingHistory && historyRefs) ? streamHistoryData(config, deps, dateStr, historyRefs) : null;
|
|
233
231
|
|
|
@@ -238,14 +236,14 @@ async function streamAndProcess(dateStr, state, passName, config, deps, rootData
|
|
|
238
236
|
if (yP_iter) yP_chunk = (await yP_iter.next()).value || {};
|
|
239
237
|
if (tH_iter) tH_chunk = (await tH_iter.next()).value || {};
|
|
240
238
|
|
|
241
|
-
const promises = streamingCalcs.map(calc =>
|
|
239
|
+
const promises = streamingCalcs.map(calc =>
|
|
242
240
|
controller.executor.executePerUser(
|
|
243
241
|
calc,
|
|
244
242
|
calc.manifest,
|
|
245
243
|
dateStr,
|
|
246
244
|
tP_chunk,
|
|
247
|
-
yP_chunk,
|
|
248
|
-
tH_chunk,
|
|
245
|
+
yP_chunk,
|
|
246
|
+
tH_chunk,
|
|
249
247
|
fetchedDeps,
|
|
250
248
|
previousFetchedDeps
|
|
251
249
|
)
|
|
@@ -260,20 +258,20 @@ async function runStandardComputationPass(date, calcs, passName, config, deps, r
|
|
|
260
258
|
const logger = deps.logger;
|
|
261
259
|
const fullRoot = { ...rootData };
|
|
262
260
|
if (calcs.some(c => c.isHistorical)) {
|
|
263
|
-
const prev
|
|
261
|
+
const prev = new Date(date); prev.setUTCDate(prev.getUTCDate() - 1);
|
|
264
262
|
const prevStr = prev.toISOString().slice(0, 10);
|
|
265
263
|
fullRoot.yesterdayPortfolioRefs = await getPortfolioPartRefs(config, deps, prevStr);
|
|
266
264
|
}
|
|
267
265
|
|
|
268
266
|
const state = {};
|
|
269
267
|
for (const c of calcs) {
|
|
270
|
-
try {
|
|
271
|
-
const inst = new c.class();
|
|
272
|
-
inst.manifest = c;
|
|
273
|
-
state[normalizeName(c.name)] = inst;
|
|
268
|
+
try {
|
|
269
|
+
const inst = new c.class();
|
|
270
|
+
inst.manifest = c;
|
|
271
|
+
state[normalizeName(c.name)] = inst;
|
|
274
272
|
logger.log('INFO', `${c.name} calculation running for ${dStr}`);
|
|
275
|
-
}
|
|
276
|
-
catch(e)
|
|
273
|
+
}
|
|
274
|
+
catch (e) { logger.log('WARN', `Failed to init ${c.name}`); }
|
|
277
275
|
}
|
|
278
276
|
|
|
279
277
|
await streamAndProcess(dStr, state, passName, config, deps, fullRoot, rootData.portfolioRefs, rootData.historyRefs, fetchedDeps, previousFetchedDeps);
|
|
@@ -282,8 +280,8 @@ async function runStandardComputationPass(date, calcs, passName, config, deps, r
|
|
|
282
280
|
|
|
283
281
|
async function runMetaComputationPass(date, calcs, passName, config, deps, fetchedDeps, previousFetchedDeps, rootData, skipStatusWrite = false) {
|
|
284
282
|
const controller = new ComputationController(config, deps);
|
|
285
|
-
const dStr
|
|
286
|
-
const state
|
|
283
|
+
const dStr = date.toISOString().slice(0, 10);
|
|
284
|
+
const state = {};
|
|
287
285
|
|
|
288
286
|
for (const mCalc of calcs) {
|
|
289
287
|
try {
|
|
@@ -297,28 +295,46 @@ async function runMetaComputationPass(date, calcs, passName, config, deps, fetch
|
|
|
297
295
|
return await commitResults(state, dStr, passName, config, deps, skipStatusWrite);
|
|
298
296
|
}
|
|
299
297
|
|
|
298
|
+
/**
|
|
299
|
+
* --- REFACTORED: commitResults ---
|
|
300
|
+
* Commits results individually per calculation.
|
|
301
|
+
* If one calculation fails (e.g. size limit), others still succeed.
|
|
302
|
+
*/
|
|
300
303
|
async function commitResults(stateObj, dStr, passName, config, deps, skipStatusWrite = false) {
|
|
301
|
-
const
|
|
302
|
-
const
|
|
304
|
+
const successUpdates = {};
|
|
305
|
+
const schemas = [];
|
|
303
306
|
|
|
307
|
+
// Iterate PER CALCULATION to isolate failures
|
|
304
308
|
for (const name in stateObj) {
|
|
305
309
|
const calc = stateObj[name];
|
|
310
|
+
let hasData = false;
|
|
311
|
+
|
|
306
312
|
try {
|
|
307
|
-
const result = await calc.getResult();
|
|
313
|
+
const result = await calc.getResult();
|
|
308
314
|
if (!result) {
|
|
309
|
-
deps.logger.log('INFO', `${name}
|
|
315
|
+
deps.logger.log('INFO', `${name} for ${dStr}: Skipped (Empty Result)`);
|
|
310
316
|
continue;
|
|
311
317
|
}
|
|
312
|
-
|
|
318
|
+
|
|
313
319
|
const standardRes = {};
|
|
314
|
-
|
|
320
|
+
const shardedWrites = [];
|
|
321
|
+
const calcWrites = []; // Accumulate all writes for THIS specific calculation
|
|
315
322
|
|
|
323
|
+
// 1. Separate Standard and Sharded Data
|
|
316
324
|
for (const key in result) {
|
|
317
325
|
if (key.startsWith('sharded_')) {
|
|
318
326
|
const sData = result[key];
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
327
|
+
// sData structure: { CollectionName: { DocId: { ...data } } }
|
|
328
|
+
for (const colName in sData) {
|
|
329
|
+
const docsMap = sData[colName];
|
|
330
|
+
for (const docId in docsMap) {
|
|
331
|
+
// Support both full path or collection-relative path
|
|
332
|
+
const ref = docId.includes('/') ? deps.db.doc(docId) : deps.db.collection(colName).doc(docId);
|
|
333
|
+
shardedWrites.push({
|
|
334
|
+
ref,
|
|
335
|
+
data: { ...docsMap[docId], _completed: true }
|
|
336
|
+
});
|
|
337
|
+
}
|
|
322
338
|
}
|
|
323
339
|
if (Object.keys(sData).length > 0) hasData = true;
|
|
324
340
|
} else {
|
|
@@ -326,70 +342,75 @@ async function commitResults(stateObj, dStr, passName, config, deps, skipStatusW
|
|
|
326
342
|
}
|
|
327
343
|
}
|
|
328
344
|
|
|
345
|
+
// 2. Prepare Standard Result Write
|
|
329
346
|
if (Object.keys(standardRes).length) {
|
|
330
347
|
validateResultPatterns(deps.logger, name, standardRes, calc.manifest.category);
|
|
331
|
-
|
|
332
348
|
standardRes._completed = true;
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
349
|
+
|
|
350
|
+
const docRef = deps.db.collection(config.resultsCollection).doc(dStr)
|
|
351
|
+
.collection(config.resultsSubcollection).doc(calc.manifest.category)
|
|
352
|
+
.collection(config.computationsSubcollection).doc(name);
|
|
353
|
+
|
|
354
|
+
calcWrites.push({
|
|
355
|
+
ref: docRef,
|
|
337
356
|
data: standardRes
|
|
338
357
|
});
|
|
339
358
|
hasData = true;
|
|
340
359
|
}
|
|
341
|
-
|
|
360
|
+
|
|
361
|
+
// 3. Queue Schema (Safe to accumulate)
|
|
342
362
|
if (calc.manifest.class.getSchema) {
|
|
343
363
|
const { class: _cls, ...safeMetadata } = calc.manifest;
|
|
344
|
-
schemas.push({
|
|
345
|
-
name, category: calc.manifest.category, schema: calc.manifest.class.getSchema(), metadata: safeMetadata
|
|
364
|
+
schemas.push({
|
|
365
|
+
name, category: calc.manifest.category, schema: calc.manifest.class.getSchema(), metadata: safeMetadata
|
|
346
366
|
});
|
|
347
367
|
}
|
|
348
|
-
|
|
368
|
+
|
|
369
|
+
// 4. ATTEMPT COMMIT FOR THIS CALCULATION ONLY
|
|
349
370
|
if (hasData) {
|
|
350
|
-
|
|
351
|
-
|
|
371
|
+
// Combine standard + sharded writes for this unit of work
|
|
372
|
+
const allWritesForCalc = [...calcWrites, ...shardedWrites];
|
|
373
|
+
|
|
374
|
+
if (allWritesForCalc.length > 0) {
|
|
375
|
+
await commitBatchInChunks(config, deps, allWritesForCalc, `${name} Results`);
|
|
376
|
+
|
|
377
|
+
// IF we get here, the commit succeeded.
|
|
378
|
+
successUpdates[name] = true;
|
|
379
|
+
deps.logger.log('INFO', `${name} for ${dStr}: \u2714 Success (Written)`);
|
|
380
|
+
} else {
|
|
381
|
+
deps.logger.log('INFO', `${name} for ${dStr}: - No Data to Write`);
|
|
382
|
+
}
|
|
352
383
|
} else {
|
|
353
|
-
deps.logger.log('INFO', `${name}
|
|
384
|
+
deps.logger.log('INFO', `${name} for ${dStr}: - Empty`);
|
|
354
385
|
}
|
|
355
386
|
|
|
356
|
-
} catch (e) {
|
|
357
|
-
|
|
358
|
-
deps.logger.log('
|
|
387
|
+
} catch (e) {
|
|
388
|
+
// CRITICAL: Catch errors here so the loop continues for other calculations
|
|
389
|
+
deps.logger.log('ERROR', `${name} for ${dStr}: \u2716 FAILED Commit: ${e.message}`);
|
|
390
|
+
// Do NOT add to successUpdates
|
|
359
391
|
}
|
|
360
392
|
}
|
|
361
393
|
|
|
362
|
-
|
|
363
|
-
if (
|
|
364
|
-
for (const col in sharded) {
|
|
365
|
-
const sWrites = [];
|
|
366
|
-
for (const id in sharded[col]) {
|
|
367
|
-
const ref = id.includes('/') ? deps.db.doc(id) : deps.db.collection(col).doc(id);
|
|
368
|
-
sWrites.push({ ref, data: { ...sharded[col][id], _completed: true } });
|
|
369
|
-
}
|
|
370
|
-
if (sWrites.length) await commitBatchInChunks(config, deps, sWrites, `${passName} Sharded ${col}`);
|
|
371
|
-
}
|
|
394
|
+
// Save Schemas (Best effort, isolated)
|
|
395
|
+
if (schemas.length) batchStoreSchemas(deps, config, schemas).catch(() => { });
|
|
372
396
|
|
|
397
|
+
// Update Status Document (Only for the ones that succeeded)
|
|
373
398
|
if (!skipStatusWrite && Object.keys(successUpdates).length > 0) {
|
|
374
399
|
await updateComputationStatus(dStr, successUpdates, config, deps);
|
|
375
|
-
deps.logger.log('INFO', `[${passName}] Updated status document for ${Object.keys(successUpdates).length} computations.`);
|
|
400
|
+
deps.logger.log('INFO', `[${passName}] Updated status document for ${Object.keys(successUpdates).length} successful computations.`);
|
|
376
401
|
}
|
|
377
402
|
return successUpdates;
|
|
378
403
|
}
|
|
379
404
|
|
|
380
405
|
/**
|
|
381
406
|
* --- UPDATED: runBatchPriceComputation ---
|
|
382
|
-
* Now supports subset/specific ticker execution via 'targetTickers'
|
|
383
|
-
* OPTIMIZED: Implements concurrency for both Shard Processing and Write Commits
|
|
384
407
|
*/
|
|
385
408
|
async function runBatchPriceComputation(config, deps, dateStrings, calcs, targetTickers = []) {
|
|
386
|
-
const { logger, db, calculationUtils } = deps;
|
|
409
|
+
const { logger, db, calculationUtils } = deps;
|
|
387
410
|
const controller = new ComputationController(config, deps);
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
// 2. Resolve Shards (All or Subset)
|
|
411
|
+
|
|
412
|
+
const mappings = await controller.loader.loadMappings();
|
|
413
|
+
|
|
393
414
|
let targetInstrumentIds = [];
|
|
394
415
|
if (targetTickers && targetTickers.length > 0) {
|
|
395
416
|
const tickerToInst = mappings.tickerToInstrument || {};
|
|
@@ -399,22 +420,20 @@ async function runBatchPriceComputation(config, deps, dateStrings, calcs, target
|
|
|
399
420
|
return;
|
|
400
421
|
}
|
|
401
422
|
}
|
|
402
|
-
|
|
423
|
+
|
|
403
424
|
const allShardRefs = await getRelevantShardRefs(config, deps, targetInstrumentIds);
|
|
404
|
-
|
|
425
|
+
|
|
405
426
|
if (!allShardRefs.length) {
|
|
406
427
|
logger.log('WARN', '[BatchPrice] No relevant price shards found. Exiting.');
|
|
407
428
|
return;
|
|
408
429
|
}
|
|
409
430
|
|
|
410
|
-
|
|
411
|
-
const OUTER_CONCURRENCY_LIMIT = 2;
|
|
431
|
+
const OUTER_CONCURRENCY_LIMIT = 2;
|
|
412
432
|
const SHARD_BATCH_SIZE = 20;
|
|
413
|
-
const WRITE_BATCH_LIMIT = 50;
|
|
433
|
+
const WRITE_BATCH_LIMIT = 50;
|
|
414
434
|
|
|
415
435
|
logger.log('INFO', `[BatchPrice] Execution Plan: ${dateStrings.length} days, ${allShardRefs.length} shards. Concurrency: ${OUTER_CONCURRENCY_LIMIT}.`);
|
|
416
436
|
|
|
417
|
-
// 4. Create Chunks of Shards
|
|
418
437
|
const shardChunks = [];
|
|
419
438
|
for (let i = 0; i < allShardRefs.length; i += SHARD_BATCH_SIZE) {
|
|
420
439
|
shardChunks.push(allShardRefs.slice(i, i + SHARD_BATCH_SIZE));
|
|
@@ -422,114 +441,104 @@ async function runBatchPriceComputation(config, deps, dateStrings, calcs, target
|
|
|
422
441
|
|
|
423
442
|
const outerLimit = pLimit(OUTER_CONCURRENCY_LIMIT);
|
|
424
443
|
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
444
|
+
const chunkPromises = [];
|
|
445
|
+
for (let index = 0; index < shardChunks.length; index++) {
|
|
446
|
+
const shardChunkRefs = shardChunks[index];
|
|
447
|
+
chunkPromises.push(outerLimit(async () => {
|
|
448
|
+
try {
|
|
449
|
+
logger.log('INFO', `[BatchPrice] Processing chunk ${index + 1}/${shardChunks.length} (${shardChunkRefs.length} shards)...`);
|
|
450
|
+
|
|
451
|
+
const pricesData = await loadDataByRefs(config, deps, shardChunkRefs);
|
|
452
|
+
|
|
453
|
+
if (targetInstrumentIds.length > 0) {
|
|
454
|
+
const requestedSet = new Set(targetInstrumentIds);
|
|
455
|
+
for (const loadedInstrumentId in pricesData) {
|
|
456
|
+
if (!requestedSet.has(loadedInstrumentId)) {
|
|
457
|
+
delete pricesData[loadedInstrumentId];
|
|
458
|
+
}
|
|
438
459
|
}
|
|
439
460
|
}
|
|
440
|
-
}
|
|
441
|
-
const writes = [];
|
|
442
|
-
|
|
443
|
-
// --- CALCULATION PHASE ---
|
|
444
|
-
for (const dateStr of dateStrings) {
|
|
445
|
-
// --- FIX 2: Manually map math primitives to their alias names ---
|
|
446
|
-
// This matches the ContextBuilder logic in ComputationController
|
|
447
|
-
// and fixes the "Cannot read properties of undefined (reading 'standardDeviation')" error.
|
|
448
|
-
const context = {
|
|
449
|
-
mappings,
|
|
450
|
-
prices: { history: pricesData },
|
|
451
|
-
date: { today: dateStr },
|
|
452
|
-
math: {
|
|
453
|
-
extract: DataExtractor,
|
|
454
|
-
history: HistoryExtractor,
|
|
455
|
-
compute: MathPrimitives,
|
|
456
|
-
aggregate: Aggregators,
|
|
457
|
-
validate: Validators,
|
|
458
|
-
signals: SignalPrimitives,
|
|
459
|
-
schemas: SCHEMAS,
|
|
460
|
-
distribution : DistributionAnalytics,
|
|
461
|
-
TimeSeries: TimeSeries,
|
|
462
|
-
priceExtractor : priceExtractor
|
|
463
|
-
}
|
|
464
|
-
};
|
|
465
|
-
|
|
466
|
-
for (const calcManifest of calcs) {
|
|
467
|
-
try {
|
|
468
|
-
// --- LOGGING FIX: Log start of calculation ---
|
|
469
|
-
logger.log('INFO', `[BatchPrice] >> Running ${calcManifest.name} for ${dateStr}...`);
|
|
470
|
-
|
|
471
|
-
const instance = new calcManifest.class();
|
|
472
|
-
await instance.process(context);
|
|
473
|
-
const result = await instance.getResult();
|
|
474
|
-
|
|
475
|
-
let hasContent = false;
|
|
476
|
-
if (result && Object.keys(result).length > 0) {
|
|
477
|
-
let dataToWrite = result;
|
|
478
|
-
if (result.by_instrument) dataToWrite = result.by_instrument;
|
|
479
|
-
|
|
480
|
-
if (Object.keys(dataToWrite).length > 0) {
|
|
481
|
-
hasContent = true;
|
|
482
|
-
const docRef = db.collection(config.resultsCollection).doc(dateStr)
|
|
483
|
-
.collection(config.resultsSubcollection).doc(calcManifest.category)
|
|
484
|
-
.collection(config.computationsSubcollection).doc(normalizeName(calcManifest.name));
|
|
485
|
-
|
|
486
|
-
writes.push({
|
|
487
|
-
ref: docRef,
|
|
488
|
-
data: { ...dataToWrite, _completed: true },
|
|
489
|
-
options: { merge: true }
|
|
490
|
-
});
|
|
491
|
-
}
|
|
492
|
-
}
|
|
493
461
|
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
462
|
+
// We now accumulate writes per calc to allow partial success, though batching optimization is tricky here.
|
|
463
|
+
// For safety, let's keep the existing structure but wrap individual calc processing in try/catch
|
|
464
|
+
// inside the write phase if possible.
|
|
465
|
+
// However, runBatchPrice is optimized for BULK throughput.
|
|
466
|
+
// To prevent total failure, we will use a safe array.
|
|
467
|
+
const writes = [];
|
|
468
|
+
|
|
469
|
+
for (const dateStr of dateStrings) {
|
|
470
|
+
const context = {
|
|
471
|
+
mappings,
|
|
472
|
+
prices: { history: pricesData },
|
|
473
|
+
date: { today: dateStr },
|
|
474
|
+
math: {
|
|
475
|
+
extract: DataExtractor,
|
|
476
|
+
history: HistoryExtractor,
|
|
477
|
+
compute: MathPrimitives,
|
|
478
|
+
aggregate: Aggregators,
|
|
479
|
+
validate: Validators,
|
|
480
|
+
signals: SignalPrimitives,
|
|
481
|
+
schemas: SCHEMAS,
|
|
482
|
+
distribution: DistributionAnalytics,
|
|
483
|
+
TimeSeries: TimeSeries,
|
|
484
|
+
priceExtractor: priceExtractor
|
|
485
|
+
}
|
|
486
|
+
};
|
|
487
|
+
|
|
488
|
+
for (const calcManifest of calcs) {
|
|
489
|
+
try {
|
|
490
|
+
// logger.log('INFO', `[BatchPrice] >> Running ${calcManifest.name} for ${dateStr}...`); // Verbose
|
|
491
|
+
const instance = new calcManifest.class();
|
|
492
|
+
await instance.process(context);
|
|
493
|
+
const result = await instance.getResult();
|
|
494
|
+
|
|
495
|
+
if (result && Object.keys(result).length > 0) {
|
|
496
|
+
let dataToWrite = result;
|
|
497
|
+
if (result.by_instrument) dataToWrite = result.by_instrument;
|
|
498
|
+
|
|
499
|
+
if (Object.keys(dataToWrite).length > 0) {
|
|
500
|
+
const docRef = db.collection(config.resultsCollection).doc(dateStr)
|
|
501
|
+
.collection(config.resultsSubcollection).doc(calcManifest.category)
|
|
502
|
+
.collection(config.computationsSubcollection).doc(normalizeName(calcManifest.name));
|
|
503
|
+
|
|
504
|
+
writes.push({
|
|
505
|
+
ref: docRef,
|
|
506
|
+
data: { ...dataToWrite, _completed: true },
|
|
507
|
+
options: { merge: true }
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
} catch (err) {
|
|
512
|
+
logger.log('ERROR', `[BatchPrice] \u2716 Failed ${calcManifest.name} for ${dateStr}: ${err.message}`);
|
|
499
513
|
}
|
|
500
|
-
|
|
501
|
-
} catch (err) {
|
|
502
|
-
// --- LOGGING FIX: Explicit failure log ---
|
|
503
|
-
logger.log('ERROR', `[BatchPrice] \u2716 Failed ${calcManifest.name} for ${dateStr}: ${err.message}`);
|
|
504
514
|
}
|
|
505
515
|
}
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
// --- PARALLEL COMMIT PHASE ---
|
|
509
|
-
if (writes.length > 0) {
|
|
510
|
-
const commitBatches = [];
|
|
511
|
-
for (let i = 0; i < writes.length; i += WRITE_BATCH_LIMIT) {
|
|
512
|
-
commitBatches.push(writes.slice(i, i + WRITE_BATCH_LIMIT));
|
|
513
|
-
}
|
|
514
516
|
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
batchWrites.forEach(w => batch.set(w.ref, w.data, w.options));
|
|
520
|
-
|
|
521
|
-
try {
|
|
522
|
-
await calculationUtils.withRetry(() => batch.commit(), `BatchPrice-C${index}-B${bIndex}`);
|
|
523
|
-
} catch (commitErr) {
|
|
524
|
-
logger.log('ERROR', `[BatchPrice] Commit failed for Chunk ${index} Batch ${bIndex}.`, { error: commitErr.message });
|
|
517
|
+
if (writes.length > 0) {
|
|
518
|
+
const commitBatches = [];
|
|
519
|
+
for (let i = 0; i < writes.length; i += WRITE_BATCH_LIMIT) {
|
|
520
|
+
commitBatches.push(writes.slice(i, i + WRITE_BATCH_LIMIT));
|
|
525
521
|
}
|
|
526
|
-
})));
|
|
527
|
-
}
|
|
528
522
|
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
523
|
+
const commitLimit = pLimit(10);
|
|
524
|
+
|
|
525
|
+
await Promise.all(commitBatches.map((batchWrites, bIndex) => commitLimit(async () => {
|
|
526
|
+
const batch = db.batch();
|
|
527
|
+
batchWrites.forEach(w => batch.set(w.ref, w.data, w.options));
|
|
528
|
+
|
|
529
|
+
try {
|
|
530
|
+
await calculationUtils.withRetry(() => batch.commit(), `BatchPrice-C${index}-B${bIndex}`);
|
|
531
|
+
} catch (commitErr) {
|
|
532
|
+
logger.log('ERROR', `[BatchPrice] Commit failed for Chunk ${index} Batch ${bIndex}.`, { error: commitErr.message });
|
|
533
|
+
}
|
|
534
|
+
})));
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
} catch (chunkErr) {
|
|
538
|
+
logger.log('ERROR', `[BatchPrice] Fatal error processing Chunk ${index}.`, { error: chunkErr.message });
|
|
539
|
+
}
|
|
540
|
+
}));
|
|
541
|
+
}
|
|
533
542
|
|
|
534
543
|
await Promise.all(chunkPromises);
|
|
535
544
|
logger.log('INFO', '[BatchPrice] Optimization pass complete.');
|
|
@@ -541,9 +550,9 @@ module.exports = {
|
|
|
541
550
|
checkRootDataAvailability,
|
|
542
551
|
fetchExistingResults,
|
|
543
552
|
fetchComputationStatus,
|
|
544
|
-
fetchGlobalComputationStatus,
|
|
545
|
-
updateComputationStatus,
|
|
546
|
-
updateGlobalComputationStatus,
|
|
553
|
+
fetchGlobalComputationStatus,
|
|
554
|
+
updateComputationStatus,
|
|
555
|
+
updateGlobalComputationStatus,
|
|
547
556
|
runStandardComputationPass,
|
|
548
557
|
runMetaComputationPass,
|
|
549
558
|
runBatchPriceComputation
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
|
|
2
|
+
// Mock types
|
|
3
|
+
namespace Firestore {
|
|
4
|
+
export class DocumentReference { }
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
const pLimit = (concurrency: number) => {
|
|
8
|
+
return (fn: () => Promise<any>) => fn();
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const OUTER_CONCURRENCY_LIMIT = 2;
|
|
12
|
+
const outerLimit = pLimit(OUTER_CONCURRENCY_LIMIT);
|
|
13
|
+
|
|
14
|
+
const shardChunks: Firestore.DocumentReference[][] = [];
|
|
15
|
+
|
|
16
|
+
// The problematic code
|
|
17
|
+
const chunkPromises = shardChunks.map((shardChunkRefs, index) => outerLimit(async () => {
|
|
18
|
+
console.log(index);
|
|
19
|
+
}));
|