bulltrackers-module 1.0.198 → 1.0.200
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,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* FILENAME: bulltrackers-module/functions/computation-system/helpers/orchestration_helpers.js
|
|
3
|
-
* FIXED:
|
|
4
|
-
*
|
|
5
|
-
* OPTIMIZED:
|
|
3
|
+
* FIXED: Context math mapping in runBatchPriceComputation (resolves 'undefined' crash).
|
|
4
|
+
* IMPROVED: Explicit logging for every calculation run (Start, Success, Failure).
|
|
5
|
+
* OPTIMIZED: Parallel Commits & Pipelined Shards.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
const { ComputationController } = require('../controllers/computation_controller');
|
|
@@ -14,7 +14,14 @@ const {
|
|
|
14
14
|
getRelevantShardRefs, loadDataByRefs
|
|
15
15
|
} = require('../utils/data_loader');
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
// --- FIX 1: Import Math Layer Primitives for Correct Context Mapping ---
|
|
18
|
+
const {
|
|
19
|
+
DataExtractor, HistoryExtractor, MathPrimitives, Aggregators,
|
|
20
|
+
Validators, SCHEMAS, SignalPrimitives, DistributionAnalytics,
|
|
21
|
+
TimeSeries, priceExtractor
|
|
22
|
+
} = require('../layers/math_primitives.js');
|
|
23
|
+
|
|
24
|
+
const pLimit = require('p-limit');
|
|
18
25
|
|
|
19
26
|
/**
|
|
20
27
|
* Groups calculations from a manifest by their 'pass' property.
|
|
@@ -22,29 +29,23 @@ const pLimit = require('p-limit'); // TODO, THIS OUGHT TO BE INJECTED.
|
|
|
22
29
|
function groupByPass(manifest) { return manifest.reduce((acc, calc) => { (acc[calc.pass] = acc[calc.pass] || []).push(calc); return acc; }, {}); }
|
|
23
30
|
|
|
24
31
|
/**
|
|
25
|
-
* ---
|
|
32
|
+
* --- PASSIVE DATA VALIDATION ---
|
|
26
33
|
* Scans a result set for suspicious patterns (e.g., a field is NULL for 100% of tickers).
|
|
27
|
-
* Logs warnings but DOES NOT block the commit.
|
|
28
34
|
*/
|
|
29
|
-
function validateResultPatterns(logger, calcName, results, category) {
|
|
30
|
-
// 1. Skip Speculators (Too sparse, nulls are expected)
|
|
35
|
+
function validateResultPatterns(logger, calcName, results, category) {
|
|
31
36
|
if (category === 'speculator' || category === 'speculators') return;
|
|
32
37
|
|
|
33
38
|
const tickers = Object.keys(results);
|
|
34
39
|
const totalItems = tickers.length;
|
|
35
40
|
|
|
36
|
-
// 2. Need a decent sample size to judge patterns
|
|
37
41
|
if (totalItems < 5) return;
|
|
38
42
|
|
|
39
|
-
// 3. Get all keys from the first valid object
|
|
40
|
-
// We assume schema is roughly consistent across tickers
|
|
41
43
|
const sampleTicker = tickers.find(t => results[t] && typeof results[t] === 'object');
|
|
42
44
|
if (!sampleTicker) return;
|
|
43
45
|
|
|
44
46
|
const keys = Object.keys(results[sampleTicker]);
|
|
45
47
|
|
|
46
48
|
keys.forEach(key => {
|
|
47
|
-
// Skip internal keys or metadata
|
|
48
49
|
if (key.startsWith('_')) return;
|
|
49
50
|
|
|
50
51
|
let nullCount = 0;
|
|
@@ -58,14 +59,11 @@ function validateResultPatterns(logger, calcName, results, category) { // TODO,
|
|
|
58
59
|
if (typeof val === 'number' && isNaN(val)) nanCount++;
|
|
59
60
|
}
|
|
60
61
|
|
|
61
|
-
// 4. Define Thresholds
|
|
62
|
-
// If 100% of data is NaN or Undefined, that's almost certainly a bug.
|
|
63
62
|
if (nanCount === totalItems) {
|
|
64
63
|
logger.log('ERROR', `[DataQuality] Calc '${calcName}' field '${key}' is NaN for 100% of ${totalItems} items. Code bug likely.`);
|
|
65
64
|
} else if (undefinedCount === totalItems) {
|
|
66
65
|
logger.log('ERROR', `[DataQuality] Calc '${calcName}' field '${key}' is UNDEFINED for 100% of ${totalItems} items. Code bug likely.`);
|
|
67
66
|
}
|
|
68
|
-
// 5. Nulls are tricky. warn if >90%, but don't error (might be valid logic like "no shorts")
|
|
69
67
|
else if (nullCount > (totalItems * 0.9)) {
|
|
70
68
|
logger.log('WARN', `[DataQuality] Calc '${calcName}' field '${key}' is NULL for ${nullCount}/${totalItems} items. Check logic if this is unexpected.`);
|
|
71
69
|
}
|
|
@@ -329,10 +327,7 @@ async function commitResults(stateObj, dStr, passName, config, deps, skipStatusW
|
|
|
329
327
|
}
|
|
330
328
|
|
|
331
329
|
if (Object.keys(standardRes).length) {
|
|
332
|
-
// --- NEW: Run Passive Validation ---
|
|
333
|
-
// We do this BEFORE marking it completed, but we do NOT stop the write.
|
|
334
330
|
validateResultPatterns(deps.logger, name, standardRes, calc.manifest.category);
|
|
335
|
-
// -----------------------------------
|
|
336
331
|
|
|
337
332
|
standardRes._completed = true;
|
|
338
333
|
writes.push({
|
|
@@ -388,10 +383,10 @@ async function commitResults(stateObj, dStr, passName, config, deps, skipStatusW
|
|
|
388
383
|
* OPTIMIZED: Implements concurrency for both Shard Processing and Write Commits
|
|
389
384
|
*/
|
|
390
385
|
async function runBatchPriceComputation(config, deps, dateStrings, calcs, targetTickers = []) {
|
|
391
|
-
const { logger, db, calculationUtils } = deps;
|
|
386
|
+
const { logger, db, calculationUtils } = deps;
|
|
392
387
|
const controller = new ComputationController(config, deps);
|
|
393
388
|
|
|
394
|
-
// 1.
|
|
389
|
+
// 1. Call loadMappings() correctly and get the result
|
|
395
390
|
const mappings = await controller.loader.loadMappings();
|
|
396
391
|
|
|
397
392
|
// 2. Resolve Shards (All or Subset)
|
|
@@ -413,12 +408,9 @@ async function runBatchPriceComputation(config, deps, dateStrings, calcs, target
|
|
|
413
408
|
}
|
|
414
409
|
|
|
415
410
|
// 3. Execution Planning
|
|
416
|
-
// CONCURRENCY SETTING:
|
|
417
|
-
// Limit outer concurrency (processing shard chunks) to 2 to prevent contention on daily result docs.
|
|
418
|
-
// While Firestore handles concurrent writes to the same doc, limiting this avoids excessive retries.
|
|
419
411
|
const OUTER_CONCURRENCY_LIMIT = 2;
|
|
420
412
|
const SHARD_BATCH_SIZE = 20;
|
|
421
|
-
const WRITE_BATCH_LIMIT = 50;
|
|
413
|
+
const WRITE_BATCH_LIMIT = 50;
|
|
422
414
|
|
|
423
415
|
logger.log('INFO', `[BatchPrice] Execution Plan: ${dateStrings.length} days, ${allShardRefs.length} shards. Concurrency: ${OUTER_CONCURRENCY_LIMIT}.`);
|
|
424
416
|
|
|
@@ -440,7 +432,6 @@ async function runBatchPriceComputation(config, deps, dateStrings, calcs, target
|
|
|
440
432
|
// Optional Filtering for Subset Mode
|
|
441
433
|
if (targetInstrumentIds.length > 0) {
|
|
442
434
|
const requestedSet = new Set(targetInstrumentIds);
|
|
443
|
-
// Iterate over the loaded data and delete anything we didn't ask for
|
|
444
435
|
for (const loadedInstrumentId in pricesData) {
|
|
445
436
|
if (!requestedSet.has(loadedInstrumentId)) {
|
|
446
437
|
delete pricesData[loadedInstrumentId];
|
|
@@ -450,26 +441,44 @@ async function runBatchPriceComputation(config, deps, dateStrings, calcs, target
|
|
|
450
441
|
const writes = [];
|
|
451
442
|
|
|
452
443
|
// --- CALCULATION PHASE ---
|
|
453
|
-
// This builds up the array of writes (one per date)
|
|
454
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.
|
|
455
448
|
const context = {
|
|
456
449
|
mappings,
|
|
457
450
|
prices: { history: pricesData },
|
|
458
451
|
date: { today: dateStr },
|
|
459
|
-
math:
|
|
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
|
+
}
|
|
460
464
|
};
|
|
461
465
|
|
|
462
466
|
for (const calcManifest of calcs) {
|
|
463
467
|
try {
|
|
468
|
+
// --- LOGGING FIX: Log start of calculation ---
|
|
469
|
+
logger.log('INFO', `[BatchPrice] >> Running ${calcManifest.name} for ${dateStr}...`);
|
|
470
|
+
|
|
464
471
|
const instance = new calcManifest.class();
|
|
465
472
|
await instance.process(context);
|
|
466
473
|
const result = await instance.getResult();
|
|
467
474
|
|
|
475
|
+
let hasContent = false;
|
|
468
476
|
if (result && Object.keys(result).length > 0) {
|
|
469
477
|
let dataToWrite = result;
|
|
470
478
|
if (result.by_instrument) dataToWrite = result.by_instrument;
|
|
471
479
|
|
|
472
480
|
if (Object.keys(dataToWrite).length > 0) {
|
|
481
|
+
hasContent = true;
|
|
473
482
|
const docRef = db.collection(config.resultsCollection).doc(dateStr)
|
|
474
483
|
.collection(config.resultsSubcollection).doc(calcManifest.category)
|
|
475
484
|
.collection(config.computationsSubcollection).doc(normalizeName(calcManifest.name));
|
|
@@ -481,22 +490,28 @@ async function runBatchPriceComputation(config, deps, dateStrings, calcs, target
|
|
|
481
490
|
});
|
|
482
491
|
}
|
|
483
492
|
}
|
|
493
|
+
|
|
494
|
+
// --- LOGGING FIX: Log success/completion ---
|
|
495
|
+
if (hasContent) {
|
|
496
|
+
logger.log('INFO', `[BatchPrice] \u2714 Finished ${calcManifest.name} for ${dateStr}. Found data.`);
|
|
497
|
+
} else {
|
|
498
|
+
logger.log('INFO', `[BatchPrice] - Finished ${calcManifest.name} for ${dateStr}. No result data.`);
|
|
499
|
+
}
|
|
500
|
+
|
|
484
501
|
} catch (err) {
|
|
485
|
-
|
|
502
|
+
// --- LOGGING FIX: Explicit failure log ---
|
|
503
|
+
logger.log('ERROR', `[BatchPrice] \u2716 Failed ${calcManifest.name} for ${dateStr}: ${err.message}`);
|
|
486
504
|
}
|
|
487
505
|
}
|
|
488
506
|
}
|
|
489
507
|
|
|
490
508
|
// --- PARALLEL COMMIT PHASE ---
|
|
491
|
-
// Instead of committing sequentially via commitBatchInChunks, we process these writes in parallel.
|
|
492
|
-
// Since each write targets a DIFFERENT date (different document), parallelizing this is safe and fast.
|
|
493
509
|
if (writes.length > 0) {
|
|
494
510
|
const commitBatches = [];
|
|
495
511
|
for (let i = 0; i < writes.length; i += WRITE_BATCH_LIMIT) {
|
|
496
512
|
commitBatches.push(writes.slice(i, i + WRITE_BATCH_LIMIT));
|
|
497
513
|
}
|
|
498
514
|
|
|
499
|
-
// Use a higher concurrency for commits since they target disjoint documents
|
|
500
515
|
const commitLimit = pLimit(10);
|
|
501
516
|
|
|
502
517
|
await Promise.all(commitBatches.map((batchWrites, bIndex) => commitLimit(async () => {
|
|
@@ -507,7 +522,6 @@ async function runBatchPriceComputation(config, deps, dateStrings, calcs, target
|
|
|
507
522
|
await calculationUtils.withRetry(() => batch.commit(), `BatchPrice-C${index}-B${bIndex}`);
|
|
508
523
|
} catch (commitErr) {
|
|
509
524
|
logger.log('ERROR', `[BatchPrice] Commit failed for Chunk ${index} Batch ${bIndex}.`, { error: commitErr.message });
|
|
510
|
-
// We log but don't throw, to allow other batches to succeed
|
|
511
525
|
}
|
|
512
526
|
})));
|
|
513
527
|
}
|
|
@@ -131,6 +131,7 @@ const createApiHandler = (config, dependencies, calcMap) => {
|
|
|
131
131
|
const computationKeys = req.query.computations.split(',');
|
|
132
132
|
const dateStrings = getDateStringsInRange(req.query.startDate, req.query.endDate);
|
|
133
133
|
const data = await fetchUnifiedData(config, dependencies, computationKeys, dateStrings, calcMap);
|
|
134
|
+
res.set('Cache-Control', 'public, max-age=300, s-maxage=3600');
|
|
134
135
|
res.status(200).send({
|
|
135
136
|
status: 'success',
|
|
136
137
|
metadata: {
|