bulltrackers-module 1.0.693 → 1.0.695
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.
|
@@ -11,6 +11,8 @@ const { PubSubUtils } = require('../../core/utils/pubsub_utils');
|
|
|
11
11
|
const { fetchComputationStatus } = require('../persistence/StatusRepository');
|
|
12
12
|
const { checkRootDataAvailability } = require('../data/AvailabilityChecker');
|
|
13
13
|
const { runFinalSweepCheck } = require('../tools/FinalSweepReporter');
|
|
14
|
+
const { resolveDependencyChain } = require('./on_demand_helpers');
|
|
15
|
+
const { checkRootDependencies } = require('../data/AvailabilityChecker');
|
|
14
16
|
// 1. IMPORT SNAPSHOT SERVICE
|
|
15
17
|
const { generateDailySnapshots } = require('../services/SnapshotService');
|
|
16
18
|
const crypto = require('crypto');
|
|
@@ -318,6 +320,16 @@ async function handleForceRun(config, dependencies, computationManifest, reqBody
|
|
|
318
320
|
const manifestItem = computationManifest.find(c => normalizeName(c.name) === normalizeName(computationName));
|
|
319
321
|
if (!manifestItem) throw new Error(`Computation '${computationName}' not found.`);
|
|
320
322
|
|
|
323
|
+
// --- STEP 1: RESOLVE FULL ANCESTRY ---
|
|
324
|
+
// We get the full chain of dependencies (ancestors) for the target.
|
|
325
|
+
// This includes the target itself and all upstream computations.
|
|
326
|
+
const chainPasses = resolveDependencyChain(computationName, computationManifest);
|
|
327
|
+
const allAncestors = chainPasses.flatMap(p => p.computations); // Flat list of all required names
|
|
328
|
+
|
|
329
|
+
// Create a map for quick lookup of ancestor manifests
|
|
330
|
+
const manifestMap = new Map(computationManifest.map(c => [normalizeName(c.name), c]));
|
|
331
|
+
|
|
332
|
+
// --- STEP 2: DETERMINE CANDIDATE DATES ---
|
|
321
333
|
let candidateDates = [];
|
|
322
334
|
if (dateInput) {
|
|
323
335
|
candidateDates = [dateInput];
|
|
@@ -327,50 +339,85 @@ async function handleForceRun(config, dependencies, computationManifest, reqBody
|
|
|
327
339
|
candidateDates = getExpectedDateStrings(earliest.absoluteEarliest, new Date());
|
|
328
340
|
}
|
|
329
341
|
|
|
330
|
-
logger.log('INFO', `[ForceRun] Checking ${candidateDates.length} candidate dates for runnability...`);
|
|
342
|
+
logger.log('INFO', `[ForceRun] Checking ${candidateDates.length} candidate dates for runnability (Deep Check)...`);
|
|
331
343
|
|
|
332
344
|
const runnableDates = [];
|
|
333
345
|
const skippedDates = [];
|
|
334
|
-
const manifestMap = new Map(computationManifest.map(c => [normalizeName(c.name), c]));
|
|
335
|
-
|
|
336
|
-
const targetComp = { ...manifestItem, schedule: null };
|
|
337
346
|
const targetComputationNormalized = normalizeName(computationName);
|
|
347
|
+
// Remove schedule constraints for the force run assessment
|
|
348
|
+
const targetComp = { ...manifestItem, schedule: null };
|
|
338
349
|
|
|
339
350
|
for (const date of candidateDates) {
|
|
351
|
+
// --- STEP 3: DEEP ROOT CHECK ---
|
|
352
|
+
// Before even asking if the *target* is runnable, we ask:
|
|
353
|
+
// "Are the raw ingredients available for the ENTIRE chain?"
|
|
354
|
+
|
|
355
|
+
// Fetch Root Data Status for this date once
|
|
356
|
+
const availability = await checkRootDataAvailability(date, config, dependencies, DEFINITIVE_EARLIEST_DATES);
|
|
357
|
+
const rootStatus = availability ? availability.status : null;
|
|
358
|
+
|
|
359
|
+
if (!rootStatus) {
|
|
360
|
+
skippedDates.push({ date, reason: 'Availability Index Missing' });
|
|
361
|
+
continue;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
let deepImpossibleReason = null;
|
|
365
|
+
|
|
366
|
+
// Check EVERY ancestor's root requirements
|
|
367
|
+
for (const ancName of allAncestors) {
|
|
368
|
+
const ancManifest = manifestMap.get(normalizeName(ancName));
|
|
369
|
+
if (!ancManifest) continue;
|
|
370
|
+
|
|
371
|
+
// Re-use the standard checker for each ancestor
|
|
372
|
+
const ancCheck = checkRootDependencies(ancManifest, rootStatus);
|
|
373
|
+
|
|
374
|
+
if (!ancCheck.canRun) {
|
|
375
|
+
// If an ancestor cannot exist, the target cannot exist.
|
|
376
|
+
deepImpossibleReason = `Ancestor '${ancName}' is missing roots: ${ancCheck.missing.join(', ')}`;
|
|
377
|
+
break;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
if (deepImpossibleReason) {
|
|
382
|
+
// Skip this date entirely - it is strictly impossible.
|
|
383
|
+
skippedDates.push({ date, reason: deepImpossibleReason });
|
|
384
|
+
continue;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// --- STEP 4: STANDARD RUNNABILITY ---
|
|
388
|
+
// If deep roots are fine, we proceed to the standard check.
|
|
389
|
+
// This handles logic like "Waiting for yesterday" or "Already Complete"
|
|
340
390
|
const result = await assessDateRunnability(date, [targetComp], config, dependencies, manifestMap);
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
391
|
+
|
|
392
|
+
if (!result) {
|
|
393
|
+
skippedDates.push({ date, reason: 'Assessment Failed' });
|
|
394
|
+
continue;
|
|
344
395
|
}
|
|
345
396
|
|
|
346
397
|
const { report } = result;
|
|
347
398
|
const isRunnable = report.runnable.some(t => normalizeName(t.name) === targetComputationNormalized);
|
|
348
399
|
const needsReRun = report.reRuns.some(t => normalizeName(t.name) === targetComputationNormalized);
|
|
349
400
|
const hasFailedDep = report.failedDependency.some(t => normalizeName(t.name) === targetComputationNormalized);
|
|
350
|
-
const isImpossible = report.impossible.some(t => normalizeName(t.name) === targetComputationNormalized);
|
|
351
|
-
const isBlocked = report.blocked.some(t => normalizeName(t.name) === targetComputationNormalized);
|
|
352
401
|
const isSkipped = report.skipped.some(t => normalizeName(t.name) === targetComputationNormalized);
|
|
353
402
|
|
|
354
|
-
//
|
|
355
|
-
//
|
|
356
|
-
//
|
|
357
|
-
if (
|
|
358
|
-
skippedDates.push({ date, reason: report.impossible.find(t => normalizeName(t.name) === targetComputationNormalized)?.reason || 'Impossible' });
|
|
359
|
-
} else if (isRunnable || needsReRun || hasFailedDep || isSkipped) {
|
|
360
|
-
// Runnable, needs re-run, has failed deps (but not impossible), or skipped (already stored)
|
|
361
|
-
// All of these are runnable for force runs - will overwrite existing results if needed
|
|
403
|
+
// NOTE: hasFailedDep is ALLOWED here because we are "Forcing" it.
|
|
404
|
+
// We know the roots exist (checked above), so the missing dependency is likely just
|
|
405
|
+
// "Not Computed Yet", which is exactly what the user wants to fix manually.
|
|
406
|
+
if (isRunnable || needsReRun || hasFailedDep || isSkipped) {
|
|
362
407
|
runnableDates.push(date);
|
|
363
|
-
} else if (
|
|
364
|
-
// Blocked usually means
|
|
365
|
-
//
|
|
408
|
+
} else if (report.blocked.length > 0) {
|
|
409
|
+
// Blocked usually means "Waiting for yesterday"
|
|
410
|
+
// For force runs, we often want to override this, but if it's strictly blocked
|
|
411
|
+
// by logic, we might still count it. Usually, we treat it as runnable.
|
|
366
412
|
runnableDates.push(date);
|
|
367
413
|
} else {
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
runnableDates.push(date);
|
|
414
|
+
const imp = report.impossible.find(t => normalizeName(t.name) === targetComputationNormalized);
|
|
415
|
+
skippedDates.push({ date, reason: imp ? imp.reason : 'Unknown State' });
|
|
371
416
|
}
|
|
372
417
|
}
|
|
373
418
|
|
|
419
|
+
// ... (Remainder of the function remains the same: dispatching tasks) ...
|
|
420
|
+
|
|
374
421
|
logger.log('INFO', `[ForceRun] ✅ Found ${runnableDates.length} runnable dates out of ${candidateDates.length} candidates`);
|
|
375
422
|
|
|
376
423
|
if (runnableDates.length === 0) {
|
|
@@ -384,13 +431,12 @@ async function handleForceRun(config, dependencies, computationManifest, reqBody
|
|
|
384
431
|
};
|
|
385
432
|
}
|
|
386
433
|
|
|
387
|
-
|
|
388
|
-
|
|
434
|
+
// Dispatch Logic
|
|
389
435
|
const topic = (reqBody.resources === 'high-mem')
|
|
390
436
|
? (config.computationTopicHighMem || 'computation-tasks-highmem')
|
|
391
437
|
: (config.computationTopicStandard || 'computation-tasks');
|
|
392
438
|
|
|
393
|
-
const dispatchId = crypto.randomUUID();
|
|
439
|
+
const dispatchId = require('crypto').randomUUID();
|
|
394
440
|
const tasks = runnableDates.map(date =>
|
|
395
441
|
createTaskPayload(manifestItem, date, manifestItem.pass || "1", dispatchId, reqBody.resources, 'MANUAL_FORCE_API')
|
|
396
442
|
);
|
|
@@ -420,7 +420,8 @@ async function writeSingleResult(result, docRef, name, dateContext, category, lo
|
|
|
420
420
|
}
|
|
421
421
|
|
|
422
422
|
const rootUpdate = updates.find(u => u.ref.path === docRef.path && u.type !== 'DELETE');
|
|
423
|
-
|
|
423
|
+
// FIX: Always use merge: false to ensure old fields (like _compressed/payload) are wiped
|
|
424
|
+
if (rootUpdate) { rootUpdate.options = { merge: false }; }
|
|
424
425
|
|
|
425
426
|
const writes = updates.filter(u => u.type !== 'DELETE').length;
|
|
426
427
|
const deletes = updates.filter(u => u.type === 'DELETE').length;
|
package/package.json
CHANGED
|
@@ -1,172 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* file: alert-system/helpers/alert_type_registry.js
|
|
3
|
-
*/
|
|
4
|
-
const ALERT_TYPES = {
|
|
5
|
-
increasedRisk: {
|
|
6
|
-
id: 'increasedRisk',
|
|
7
|
-
name: 'Increased Risk',
|
|
8
|
-
description: 'Alert when a Popular Investor\'s risk score increases',
|
|
9
|
-
computationName: 'RiskScoreIncrease',
|
|
10
|
-
category: 'alerts',
|
|
11
|
-
messageTemplate: '{piUsername}\'s risk score increased by {change} points (from {previous} to {current})',
|
|
12
|
-
severity: 'high',
|
|
13
|
-
enabled: true
|
|
14
|
-
},
|
|
15
|
-
volatilityChanges: {
|
|
16
|
-
id: 'volatilityChanges',
|
|
17
|
-
name: 'Significant Volatility',
|
|
18
|
-
description: 'Alert when a Popular Investor\'s portfolio volatility exceeds 50%',
|
|
19
|
-
computationName: 'SignificantVolatility',
|
|
20
|
-
category: 'alerts',
|
|
21
|
-
messageTemplate: '{piUsername}\'s portfolio volatility is {volatility}% (threshold: {threshold}%)',
|
|
22
|
-
severity: 'medium',
|
|
23
|
-
enabled: true
|
|
24
|
-
},
|
|
25
|
-
newSector: {
|
|
26
|
-
id: 'newSector',
|
|
27
|
-
name: 'New Sector Entry',
|
|
28
|
-
description: 'Alert when a Popular Investor enters a new sector',
|
|
29
|
-
computationName: 'NewSectorExposure',
|
|
30
|
-
category: 'alerts',
|
|
31
|
-
messageTemplate: '{piUsername} entered new sector(s): {sectorName}',
|
|
32
|
-
severity: 'low',
|
|
33
|
-
enabled: true
|
|
34
|
-
},
|
|
35
|
-
increasedPositionSize: {
|
|
36
|
-
id: 'increasedPositionSize',
|
|
37
|
-
name: 'Increased Position Size',
|
|
38
|
-
description: 'Alert when a Popular Investor significantly increases a position size (>5%)',
|
|
39
|
-
computationName: 'PositionInvestedIncrease',
|
|
40
|
-
category: 'alerts',
|
|
41
|
-
messageTemplate: '{piUsername} increased position size for {symbol} by {diff}% (from {prev}% to {curr}%)',
|
|
42
|
-
severity: 'medium',
|
|
43
|
-
enabled: true
|
|
44
|
-
},
|
|
45
|
-
newSocialPost: {
|
|
46
|
-
id: 'newSocialPost',
|
|
47
|
-
name: 'New Social Post',
|
|
48
|
-
description: 'Alert when a Popular Investor makes a new social post',
|
|
49
|
-
computationName: 'NewSocialPost',
|
|
50
|
-
category: 'alerts',
|
|
51
|
-
messageTemplate: '{piUsername} posted a new update: {title}',
|
|
52
|
-
severity: 'low',
|
|
53
|
-
enabled: true
|
|
54
|
-
},
|
|
55
|
-
// [NEW] Behavioral Anomaly Registration
|
|
56
|
-
behavioralAnomaly: {
|
|
57
|
-
id: 'behavioralAnomaly',
|
|
58
|
-
name: 'Behavioral Anomaly',
|
|
59
|
-
description: 'Alert when a Popular Investor deviates significantly from their baseline behavior',
|
|
60
|
-
computationName: 'BehavioralAnomaly',
|
|
61
|
-
category: 'alerts',
|
|
62
|
-
// Uses metadata keys from Behaviour.js: primaryDriver, driverSignificance, anomalyScore
|
|
63
|
-
messageTemplate: 'Behavioral Alert for {piUsername}: {primaryDriver} Deviation ({driverSignificance}) detected.',
|
|
64
|
-
severity: 'high',
|
|
65
|
-
enabled: true
|
|
66
|
-
},
|
|
67
|
-
testSystemProbe: {
|
|
68
|
-
id: 'testSystemProbe',
|
|
69
|
-
name: 'Test System Probe',
|
|
70
|
-
description: 'Always-on debug alert',
|
|
71
|
-
computationName: 'TestSystemProbe',
|
|
72
|
-
category: 'alerts',
|
|
73
|
-
messageTemplate: 'Probe Triggered for {status} at {timestamp}',
|
|
74
|
-
severity: 'info',
|
|
75
|
-
enabled: true
|
|
76
|
-
},
|
|
77
|
-
};
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* Get alert type by ID
|
|
81
|
-
*/
|
|
82
|
-
function getAlertType(alertTypeId) {
|
|
83
|
-
return ALERT_TYPES[alertTypeId] || null;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* Get alert type by computation name
|
|
88
|
-
*/
|
|
89
|
-
function getAlertTypeByComputation(computationName) {
|
|
90
|
-
for (const [id, alertType] of Object.entries(ALERT_TYPES)) {
|
|
91
|
-
if (alertType.computationName === computationName) {
|
|
92
|
-
return alertType;
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
return null;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* Get all enabled alert types
|
|
100
|
-
*/
|
|
101
|
-
function getAllAlertTypes() {
|
|
102
|
-
return Object.values(ALERT_TYPES).filter(type => type.enabled);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/**
|
|
106
|
-
* Check if a computation is an alert computation
|
|
107
|
-
*/
|
|
108
|
-
function isAlertComputation(computationName) {
|
|
109
|
-
return getAlertTypeByComputation(computationName) !== null;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
* Generate alert message from template and metadata
|
|
114
|
-
*/
|
|
115
|
-
function generateAlertMessage(alertType, piUsername, metadata = {}) {
|
|
116
|
-
let message = alertType.messageTemplate;
|
|
117
|
-
|
|
118
|
-
// Replace placeholders
|
|
119
|
-
message = message.replace('{piUsername}', piUsername || 'Unknown');
|
|
120
|
-
message = message.replace('{count}', metadata.count || metadata.positions?.length || metadata.moveCount || 0);
|
|
121
|
-
message = message.replace('{change}', metadata.change || metadata.changePercent || metadata.diff || 'N/A');
|
|
122
|
-
message = message.replace('{previous}', metadata.previous || metadata.previousValue || metadata.prev || metadata.previousRisk || 'N/A');
|
|
123
|
-
message = message.replace('{current}', metadata.current || metadata.currentValue || metadata.curr || metadata.currentRisk || 'N/A');
|
|
124
|
-
message = message.replace('{sectorName}', metadata.sectorName || (metadata.newExposures && metadata.newExposures.length > 0 ? metadata.newExposures.join(', ') : 'Unknown'));
|
|
125
|
-
message = message.replace('{ticker}', metadata.ticker || metadata.symbol || 'Unknown');
|
|
126
|
-
|
|
127
|
-
// Format numeric values
|
|
128
|
-
message = message.replace('{volatility}', metadata.volatility ? `${(metadata.volatility * 100).toFixed(1)}` : 'N/A');
|
|
129
|
-
message = message.replace('{threshold}', metadata.threshold ? `${(metadata.threshold * 100).toFixed(0)}` : 'N/A');
|
|
130
|
-
message = message.replace('{diff}', metadata.diff ? `${metadata.diff.toFixed(1)}` : 'N/A');
|
|
131
|
-
message = message.replace('{prev}', metadata.prev ? `${metadata.prev.toFixed(1)}` : 'N/A');
|
|
132
|
-
message = message.replace('{curr}', metadata.curr ? `${metadata.curr.toFixed(1)}` : 'N/A');
|
|
133
|
-
message = message.replace('{title}', metadata.title || 'New Update');
|
|
134
|
-
|
|
135
|
-
// [FIX] Probe Placeholders (Missing in original)
|
|
136
|
-
message = message.replace('{status}', metadata.status || 'Unknown Status');
|
|
137
|
-
message = message.replace('{timestamp}', metadata.timestamp || new Date().toISOString());
|
|
138
|
-
|
|
139
|
-
// [NEW] Behavioral Anomaly Placeholders
|
|
140
|
-
message = message.replace('{primaryDriver}', metadata.primaryDriver || 'Unknown Factor');
|
|
141
|
-
message = message.replace('{driverSignificance}', metadata.driverSignificance || 'N/A');
|
|
142
|
-
message = message.replace('{anomalyScore}', metadata.anomalyScore || 'N/A');
|
|
143
|
-
|
|
144
|
-
// Handle positions list if available
|
|
145
|
-
if (metadata.positions && Array.isArray(metadata.positions) && metadata.positions.length > 0) {
|
|
146
|
-
const positionsList = metadata.positions
|
|
147
|
-
.slice(0, 3)
|
|
148
|
-
.map(p => p.ticker || p.instrumentId)
|
|
149
|
-
.join(', ');
|
|
150
|
-
message = message.replace('{positions}', positionsList);
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
// Handle moves array for PositionInvestedIncrease
|
|
154
|
-
if (metadata.moves && Array.isArray(metadata.moves) && metadata.moves.length > 0) {
|
|
155
|
-
const firstMove = metadata.moves[0];
|
|
156
|
-
message = message.replace('{symbol}', firstMove.symbol || 'Unknown');
|
|
157
|
-
message = message.replace('{diff}', firstMove.diff ? `${firstMove.diff.toFixed(1)}` : 'N/A');
|
|
158
|
-
message = message.replace('{prev}', firstMove.prev ? `${firstMove.prev.toFixed(1)}` : 'N/A');
|
|
159
|
-
message = message.replace('{curr}', firstMove.curr ? `${firstMove.curr.toFixed(1)}` : 'N/A');
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
return message;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
module.exports = {
|
|
166
|
-
ALERT_TYPES,
|
|
167
|
-
getAlertType,
|
|
168
|
-
getAlertTypeByComputation,
|
|
169
|
-
getAllAlertTypes,
|
|
170
|
-
isAlertComputation,
|
|
171
|
-
generateAlertMessage
|
|
172
|
-
};
|