bulltrackers-module 1.0.102 → 1.0.103
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,38 +1,120 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
2
|
+
"dependencies": {
|
|
3
|
+
"smart-cohort-flow": ["user-investment-profile"],
|
|
4
|
+
"dumb-cohort-flow": ["user-investment-profile"],
|
|
5
|
+
"crowd_sharpe_ratio_proxy": ["pnl-distribution-per-stock"],
|
|
6
|
+
"social_flow_correlation": [
|
|
7
|
+
"social_sentiment_aggregation",
|
|
8
|
+
"asset_crowd_flow"
|
|
9
|
+
],
|
|
10
|
+
"cash-flow-deployment": [
|
|
11
|
+
"crowd-cash-flow-proxy",
|
|
12
|
+
"asset_crowd_flow"
|
|
13
|
+
],
|
|
14
|
+
"cash-flow-liquidation": [
|
|
15
|
+
"crowd-cash-flow-proxy",
|
|
16
|
+
"asset_crowd_flow"
|
|
17
|
+
],
|
|
18
|
+
"capital_deployment_strategy": [
|
|
19
|
+
"crowd-cash-flow-proxy",
|
|
20
|
+
"new-allocation-percentage",
|
|
21
|
+
"reallocation-increase-percentage"
|
|
22
|
+
],
|
|
23
|
+
"profit_cohort_divergence": [
|
|
24
|
+
"in-profit-asset-crowd-flow",
|
|
25
|
+
"in-loss-asset-crowd-flow"
|
|
26
|
+
],
|
|
27
|
+
"smart-dumb-divergence-index": [
|
|
28
|
+
"smart-cohort-flow",
|
|
29
|
+
"dumb-cohort-flow"
|
|
30
|
+
],
|
|
31
|
+
"capital_vintage_performance": ["cash-flow-deployment"],
|
|
32
|
+
"capital_liquidation_performance": ["cash-flow-liquidation"],
|
|
33
|
+
"strategy-performance": [
|
|
34
|
+
"smart-dumb-divergence-index",
|
|
35
|
+
"profit_cohort_divergence"
|
|
36
|
+
]
|
|
37
|
+
},
|
|
38
|
+
"requirements": {
|
|
39
|
+
"activity_by_pnl_status": ["today_portfolio", "yesterday_portfolio"],
|
|
40
|
+
"daily_asset_activity": ["today_portfolio", "yesterday_portfolio"],
|
|
41
|
+
"daily_user_activity_tracker": ["today_portfolio", "yesterday_portfolio"],
|
|
42
|
+
"speculator_adjustment_activity": ["today_portfolio", "yesterday_portfolio"],
|
|
43
|
+
"asset_position_size": ["today_portfolio"],
|
|
44
|
+
"strategy-performance": ["meta"],
|
|
45
|
+
"asset_crowd_flow": ["today_portfolio", "yesterday_portfolio"],
|
|
46
|
+
"drawdown_response": ["today_portfolio", "yesterday_portfolio"],
|
|
47
|
+
"dumb-cohort-flow": ["today_portfolio", "yesterday_portfolio"],
|
|
48
|
+
"gain_response": ["today_portfolio", "yesterday_portfolio"],
|
|
49
|
+
"in_loss_asset_crowd_flow": ["today_portfolio", "yesterday_portfolio"],
|
|
50
|
+
"in_profit_asset_crowd_flow": ["today_portfolio", "yesterday_portfolio"],
|
|
51
|
+
"paper_vs_diamond_hands": ["today_portfolio", "yesterday_portfolio"],
|
|
52
|
+
"position_count_pnl": ["today_portfolio"],
|
|
53
|
+
"smart-cohort-flow": ["today_portfolio", "yesterday_portfolio"],
|
|
54
|
+
"smart_money_flow": ["today_portfolio", "yesterday_portfolio"],
|
|
55
|
+
"user-investment-profile": ["today_portfolio", "yesterday_portfolio"],
|
|
56
|
+
"crowd-cash-flow-proxy": ["today_portfolio", "yesterday_portfolio"],
|
|
57
|
+
"deposit_withdrawal_percentage": ["today_portfolio", "yesterday_portfolio"],
|
|
58
|
+
"new_allocation_percentage": ["today_portfolio", "yesterday_portfolio"],
|
|
59
|
+
"reallocation_increase_percentage": ["today_portfolio", "yesterday_portfolio"],
|
|
60
|
+
"daily_bought_vs_sold_count": ["today_insights", "yesterday_insights"],
|
|
61
|
+
"daily_buy_sell_sentiment_count": ["today_insights"],
|
|
62
|
+
"daily_ownership_delta": ["today_insights", "yesterday_insights"],
|
|
63
|
+
"daily_total_positions_held": ["today_insights"],
|
|
64
|
+
"capital_deployment_strategy": ["meta"],
|
|
65
|
+
"cash-flow-deployment": ["meta"],
|
|
66
|
+
"cash-flow-liquidation": ["meta"],
|
|
67
|
+
"crowd_sharpe_ratio_proxy": ["meta"],
|
|
68
|
+
"profit_cohort_divergence": ["meta"],
|
|
69
|
+
"smart-dumb-divergence-index": ["meta"],
|
|
70
|
+
"social_flow_correlation": ["meta"],
|
|
71
|
+
"capital_liquidation_performance": ["meta"],
|
|
72
|
+
"capital_vintage_performance": ["meta"],
|
|
73
|
+
"profitability_migration": ["today_portfolio", "yesterday_portfolio"],
|
|
74
|
+
"user_profitability_tracker": ["today_portfolio"],
|
|
75
|
+
"asset_pnl_status": ["today_portfolio"],
|
|
76
|
+
"average_daily_pnl_all_users": ["today_portfolio"],
|
|
77
|
+
"average_daily_pnl_per_sector": ["today_portfolio"],
|
|
78
|
+
"average_daily_pnl_per_stock": ["today_portfolio"],
|
|
79
|
+
"average_daily_position_pnl": ["today_portfolio"],
|
|
80
|
+
"pnl_distribution_per_stock": ["today_portfolio"],
|
|
81
|
+
"profitability_ratio_per_stock": ["today_portfolio"],
|
|
82
|
+
"profitability_skew_per_stock": ["today_portfolio"],
|
|
83
|
+
"profitable_and_unprofitable_status": ["today_portfolio"],
|
|
84
|
+
"users_processed": ["today_portfolio"],
|
|
85
|
+
"diversification_pnl": ["today_portfolio", "yesterday_portfolio"],
|
|
86
|
+
"sector_rotation": ["today_portfolio", "yesterday_portfolio"],
|
|
87
|
+
"total_long_per_sector": ["today_portfolio"],
|
|
88
|
+
"total_short_per_sector": ["today_portfolio"],
|
|
89
|
+
"crowd_conviction_score": ["today_portfolio", "yesterday_portfolio"],
|
|
90
|
+
"long_position_per_stock": ["today_portfolio"],
|
|
91
|
+
"sentiment_per_stock": ["today_portfolio"],
|
|
92
|
+
"short_position_per_stock": ["today_portfolio"],
|
|
93
|
+
"total_long_figures": ["today_portfolio"],
|
|
94
|
+
"total_short_figures": ["today_portfolio"],
|
|
95
|
+
"social-asset-posts-trend": ["today_social"],
|
|
96
|
+
"social-top-mentioned-words": ["today_social", "yesterday_social"],
|
|
97
|
+
"social-topic-interest-evolution": ["today_social"],
|
|
98
|
+
"social-word-mentions-trend": ["today_social"],
|
|
99
|
+
"social_activity_aggregation": ["today_social"],
|
|
100
|
+
"social_event_correlation": ["today_social", "yesterday_social"],
|
|
101
|
+
"social_sentiment_aggregation": ["today_social"],
|
|
102
|
+
"risk_appetite_change": ["today_portfolio", "yesterday_portfolio"],
|
|
103
|
+
"tsl_effectiveness": ["today_portfolio"],
|
|
104
|
+
"distance_to_stop_loss_per_leverage": ["today_portfolio"],
|
|
105
|
+
"distance_to_tp_per_leverage": ["today_portfolio"],
|
|
106
|
+
"entry_distance_to_sl_per_leverage": ["today_portfolio"],
|
|
107
|
+
"entry_distance_to_tp_per_leverage": ["today_portfolio"],
|
|
108
|
+
"holding_duration_per_asset": ["today_portfolio"],
|
|
109
|
+
"leverage_per_asset": ["today_portfolio"],
|
|
110
|
+
"leverage_per_sector": ["today_portfolio"],
|
|
111
|
+
"risk_reward_ratio_per_asset": ["today_portfolio"],
|
|
112
|
+
"speculator_asset_sentiment": ["today_portfolio"],
|
|
113
|
+
"speculator_danger_zone": ["today_portfolio"],
|
|
114
|
+
"stop_loss_distance_by_sector_short_long_breakdown": ["today_portfolio"],
|
|
115
|
+
"stop_loss_distance_by_ticker_short_long_breakdown": ["today_portfolio"],
|
|
116
|
+
"stop_loss_per_asset": ["today_portfolio"],
|
|
117
|
+
"take_profit_per_asset": ["today_portfolio"],
|
|
118
|
+
"tsl_per_asset": ["today_portfolio"]
|
|
119
|
+
}
|
|
38
120
|
}
|
|
@@ -44,56 +44,68 @@ function findCalcClass(calculations, category, name) {
|
|
|
44
44
|
return null;
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
// --- NEW: Internal helper for previous date string ---
|
|
48
|
+
function getPrevStr(dateStr) {
|
|
49
|
+
const prev = new Date(dateStr + 'T00:00:00Z');
|
|
50
|
+
prev.setUTCDate(prev.getUTCDate() - 1);
|
|
51
|
+
return prev.toISOString().slice(0, 10);
|
|
52
|
+
}
|
|
53
|
+
|
|
47
54
|
/**
|
|
48
55
|
* Streams data and runs a *single* Batch 1-style computation.
|
|
56
|
+
* --- THIS IS THE FULLY UPDATED FUNCTION ---
|
|
49
57
|
*/
|
|
50
58
|
async function runStreamingComputation(dateStr, node, config, dependencies, calculations) {
|
|
51
59
|
const { db, logger } = dependencies;
|
|
52
|
-
|
|
60
|
+
|
|
61
|
+
// --- MODIFIED: Destructure 'requires' from the node ---
|
|
62
|
+
const { category, name: calcName, calcClass: CalculationClass, requires } = node;
|
|
53
63
|
const passName = `[Worker ${dateStr}/${calcName}]`;
|
|
54
64
|
|
|
55
|
-
logger.log('INFO', `${passName} Starting streaming computation
|
|
56
|
-
|
|
57
|
-
// --- Data Loading ---
|
|
58
|
-
const todayInsightsData = await loadDailyInsights(config, dependencies, dateStr);
|
|
59
|
-
const todaySocialPostInsightsData = await loadDailySocialPostInsights(config, dependencies, dateStr);
|
|
60
|
-
const todayRefs = await getPortfolioPartRefs(config, dependencies, dateStr);
|
|
61
|
-
|
|
62
|
-
if (todayRefs.length === 0 && !todayInsightsData && !todaySocialPostInsightsData) {
|
|
63
|
-
logger.log('WARN', `${passName} No portfolio, instrument insights, OR social post data found for ${dateStr}. Skipping.`);
|
|
64
|
-
// Return null to indicate "no result, but not an error"
|
|
65
|
-
return null;
|
|
66
|
-
}
|
|
65
|
+
logger.log('INFO', `${passName} Starting streaming computation... (Requires: ${requires.join(', ') || 'none'})`);
|
|
67
66
|
|
|
68
|
-
|
|
69
|
-
let
|
|
67
|
+
// --- Dynamic Data Loading ---
|
|
68
|
+
let todayInsightsData = null;
|
|
70
69
|
let yesterdayInsightsData = null;
|
|
70
|
+
let todaySocialPostInsightsData = null;
|
|
71
71
|
let yesterdaySocialPostInsightsData = null;
|
|
72
|
+
let todayRefs = [];
|
|
73
|
+
let yesterdayPortfolios = {};
|
|
74
|
+
const prevStr = getPrevStr(dateStr); // Get yesterday's date string
|
|
72
75
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
76
|
+
if (requires.includes('today_insights')) {
|
|
77
|
+
todayInsightsData = await loadDailyInsights(config, dependencies, dateStr);
|
|
78
|
+
}
|
|
79
|
+
if (requires.includes('yesterday_insights')) {
|
|
80
|
+
yesterdayInsightsData = await loadDailyInsights(config, dependencies, prevStr);
|
|
81
|
+
}
|
|
82
|
+
if (requires.includes('today_social')) {
|
|
83
|
+
todaySocialPostInsightsData = await loadDailySocialPostInsights(config, dependencies, dateStr);
|
|
84
|
+
}
|
|
85
|
+
if (requires.includes('yesterday_social')) {
|
|
86
|
+
yesterdaySocialPostInsightsData = await loadDailySocialPostInsights(config, dependencies, prevStr);
|
|
87
|
+
}
|
|
88
|
+
if (requires.includes('today_portfolio')) {
|
|
89
|
+
todayRefs = await getPortfolioPartRefs(config, dependencies, dateStr);
|
|
90
|
+
}
|
|
91
|
+
if (requires.includes('yesterday_portfolio')) {
|
|
82
92
|
const yesterdayRefs = await getPortfolioPartRefs(config, dependencies, prevStr);
|
|
83
93
|
if (yesterdayRefs.length > 0) {
|
|
84
94
|
yesterdayPortfolios = await loadFullDayMap(config, dependencies, yesterdayRefs);
|
|
85
|
-
} else {
|
|
86
|
-
|
|
95
|
+
} else if (requires.includes('today_portfolio')) {
|
|
96
|
+
// Only warn if today_portfolio was also expected, otherwise this might be normal
|
|
97
|
+
logger.log('WARN', `${passName} Requires 'yesterday_portfolio' but no data was found for ${prevStr}.`);
|
|
87
98
|
}
|
|
88
99
|
}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
if (
|
|
93
|
-
|
|
100
|
+
// --- End Dynamic Data Loading ---
|
|
101
|
+
|
|
102
|
+
// Check if any primary data source was loaded. If not, we can't run.
|
|
103
|
+
if (todayRefs.length === 0 && !todayInsightsData && !todaySocialPostInsightsData) {
|
|
104
|
+
logger.log('WARN', `${passName} No primary data sources (portfolio, insights, social) found for ${dateStr}. Skipping.`);
|
|
105
|
+
return null;
|
|
94
106
|
}
|
|
95
|
-
// --- End Data Loading ---
|
|
96
107
|
|
|
108
|
+
// --- Context Object (no change) ---
|
|
97
109
|
const { instrumentToTicker, instrumentToSector } = await dependencies.calculationUtils.loadInstrumentMappings();
|
|
98
110
|
const context = {
|
|
99
111
|
instrumentMappings: instrumentToTicker,
|
|
@@ -106,55 +118,52 @@ async function runStreamingComputation(dateStr, node, config, dependencies, calc
|
|
|
106
118
|
|
|
107
119
|
const calc = new CalculationClass();
|
|
108
120
|
const batchSize = config.partRefBatchSize || 10;
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
//
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
for (
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
processArgs = [null, null, null, ...allContextArgs];
|
|
127
|
-
} else {
|
|
128
|
-
continue; // Only run once
|
|
129
|
-
}
|
|
130
|
-
} else if (requiresYesterdayPortfolio) {
|
|
131
|
-
const pYesterday = yesterdayPortfolios[uid];
|
|
132
|
-
if (!pYesterday) continue;
|
|
133
|
-
processArgs = [p, pYesterday, uid, ...allContextArgs];
|
|
134
|
-
} else {
|
|
121
|
+
|
|
122
|
+
// --- MODIFIED: Stream and Process ---
|
|
123
|
+
let processedOnce = false; // Flag for insights/social calcs
|
|
124
|
+
const allContextArgs = [context, todayInsightsData, yesterdayInsightsData, todaySocialPostInsightsData, yesterdaySocialPostInsightsData];
|
|
125
|
+
|
|
126
|
+
// Only stream portfolios if they are required and were found
|
|
127
|
+
if (requires.includes('today_portfolio') && todayRefs.length > 0) {
|
|
128
|
+
for (let i = 0; i < todayRefs.length; i += batchSize) {
|
|
129
|
+
const batchRefs = todayRefs.slice(i, i + batchSize);
|
|
130
|
+
const todayPortfoliosChunk = await loadDataByRefs(config, dependencies, batchRefs);
|
|
131
|
+
|
|
132
|
+
for (const uid in todayPortfoliosChunk) {
|
|
133
|
+
const p = todayPortfoliosChunk[uid];
|
|
134
|
+
if (!p) continue;
|
|
135
|
+
|
|
136
|
+
// User-type filtering
|
|
137
|
+
const userType = p.PublicPositions ? 'speculator' : 'normal';
|
|
135
138
|
if ((userType === 'normal' && category === 'speculators') || (userType === 'speculator' && !['speculators', 'sanity'].includes(category))) {
|
|
136
139
|
continue;
|
|
137
140
|
}
|
|
138
|
-
|
|
141
|
+
|
|
142
|
+
let processArgs;
|
|
143
|
+
if (requires.includes('yesterday_portfolio')) {
|
|
144
|
+
const pYesterday = yesterdayPortfolios[uid];
|
|
145
|
+
if (!pYesterday) continue; // Skip if user is missing yesterday's data
|
|
146
|
+
processArgs = [p, pYesterday, uid, ...allContextArgs];
|
|
147
|
+
} else {
|
|
148
|
+
processArgs = [p, null, uid, ...allContextArgs];
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
await Promise.resolve(calc.process(...processArgs));
|
|
152
|
+
processedOnce = true;
|
|
139
153
|
}
|
|
140
|
-
await Promise.resolve(calc.process(...processArgs));
|
|
141
|
-
isFirstUser = false;
|
|
142
154
|
}
|
|
143
155
|
}
|
|
144
156
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
157
|
+
// If this is an insights/social calc and no portfolios ran (or were needed), run it once.
|
|
158
|
+
if (!processedOnce && (requires.includes('today_insights') || requires.includes('today_social'))) {
|
|
159
|
+
logger.log('INFO', `${passName} Running once for insights/social data.`);
|
|
148
160
|
await Promise.resolve(calc.process(null, null, null, ...allContextArgs));
|
|
149
161
|
}
|
|
150
162
|
// --- End Stream ---
|
|
151
163
|
|
|
152
|
-
// --- Get Result and Commit ---
|
|
164
|
+
// --- Get Result and Commit (no change) ---
|
|
153
165
|
const result = await Promise.resolve(calc.getResult());
|
|
154
166
|
|
|
155
|
-
// If a calc returns null (e.g., dependency missing), it's a "soft fail".
|
|
156
|
-
// We throw an error to be caught by the main handler, which will log it
|
|
157
|
-
// and NOT mark the node as "success", allowing for a re-trigger.
|
|
158
167
|
if (result === null) {
|
|
159
168
|
throw new Error(`Calculation ${calcName} returned null. Dependency likely missing.`);
|
|
160
169
|
}
|
|
@@ -173,7 +182,6 @@ async function runStreamingComputation(dateStr, node, config, dependencies, calc
|
|
|
173
182
|
for (const resultKey in shardedCollections) {
|
|
174
183
|
if (result[resultKey]) {
|
|
175
184
|
isSharded = true;
|
|
176
|
-
// ... (sharding logic from old worker_helpers.js, no changes) ...
|
|
177
185
|
const shardCollectionName = shardedCollections[resultKey];
|
|
178
186
|
const shardedData = result[resultKey];
|
|
179
187
|
for (const shardId in shardedData) {
|
|
@@ -212,6 +220,7 @@ async function runStreamingComputation(dateStr, node, config, dependencies, calc
|
|
|
212
220
|
|
|
213
221
|
/**
|
|
214
222
|
* Runs a *single* meta/backtest computation.
|
|
223
|
+
* (This function remains unchanged as 'meta' calcs handle their own data loading)
|
|
215
224
|
*/
|
|
216
225
|
async function runMetaComputation(dateStr, node, config, dependencies) {
|
|
217
226
|
const { db, logger } = dependencies;
|
|
@@ -253,13 +262,7 @@ async function runMetaComputation(dateStr, node, config, dependencies) {
|
|
|
253
262
|
|
|
254
263
|
/**
|
|
255
264
|
* Main pipe: pipe.computationSystem.handleNodeTask
|
|
256
|
-
* This
|
|
257
|
-
*
|
|
258
|
-
* @param {object} message - The Pub/Sub message { date, nodeName }.
|
|
259
|
-
* @param {object} context - The message context.
|
|
260
|
-
* @param {object} config - The task-engine-specific config.
|
|
261
|
-
* @param {object} dependencies - Contains all clients: db, pubsub, logger, etc.
|
|
262
|
-
* @param {object} calculations - The injected calculations object from 'aiden-shared-calculations-unified'.
|
|
265
|
+
* (This function remains unchanged)
|
|
263
266
|
*/
|
|
264
267
|
async function handleNodeTask(message, context, config, dependencies, calculations) {
|
|
265
268
|
const { logger, pubsubUtils } = dependencies;
|
|
@@ -291,7 +294,8 @@ async function handleNodeTask(message, context, config, dependencies, calculatio
|
|
|
291
294
|
|
|
292
295
|
try {
|
|
293
296
|
// 1. Route to the correct execution helper
|
|
294
|
-
|
|
297
|
+
// --- MODIFIED: Check for 'meta' requirement key ---
|
|
298
|
+
if (node.requires.includes('meta')) {
|
|
295
299
|
await runMetaComputation(date, node, config, dependencies);
|
|
296
300
|
} else {
|
|
297
301
|
// All other calcs (pnl, behavioural, etc.) use the streaming helper
|
|
@@ -317,22 +321,13 @@ async function handleNodeTask(message, context, config, dependencies, calculatio
|
|
|
317
321
|
|
|
318
322
|
} else {
|
|
319
323
|
logger.log('SUCCESS', `${taskId} Complete. No new children are ready.`);
|
|
320
|
-
// Optional: Check if the *entire* DAG is now complete for this day
|
|
321
|
-
// This is a "nice-to-have" but good for logging.
|
|
322
324
|
}
|
|
323
325
|
|
|
324
326
|
} catch (error) {
|
|
325
|
-
// --- THIS IS THE SELF-HEALING LOGIC ---
|
|
326
|
-
// A calc failed (e.g., dependency missing). We log it and stop.
|
|
327
|
-
// We do *not* mark it as "success" in the state manager.
|
|
328
|
-
// We do *not* trigger its children.
|
|
329
|
-
// The system will wait until the dependency that *caused* this
|
|
330
|
-
// failure is fixed and re-triggers this node.
|
|
331
327
|
logger.log('WARN', `${taskId} FAILED (Will retry when dependencies are met).`, {
|
|
332
328
|
err: error.message,
|
|
333
329
|
stack: error.stack
|
|
334
330
|
});
|
|
335
|
-
// Acknowledge the Pub/Sub message so it doesn't retry immediately.
|
|
336
331
|
return;
|
|
337
332
|
}
|
|
338
333
|
}
|
|
@@ -16,6 +16,7 @@ class ComputationNode {
|
|
|
16
16
|
this.calcClass = calcClass;
|
|
17
17
|
this.children = new Set(); // Nodes that depend on this one
|
|
18
18
|
this.parents = new Set(); // Nodes this one depends on
|
|
19
|
+
this.requires = []; // <-- ADDED: Stores data requirements
|
|
19
20
|
}
|
|
20
21
|
}
|
|
21
22
|
|
|
@@ -34,14 +35,16 @@ class ComputationGraph {
|
|
|
34
35
|
this.sortedNodes = []; // Array of node names, in valid execution order
|
|
35
36
|
this.isBuilt = false;
|
|
36
37
|
|
|
37
|
-
|
|
38
|
+
// --- MODIFIED: Pass requirements config to _registerNodes ---
|
|
39
|
+
this._registerNodes(unifiedCalculations, dependencyConfig.requirements || {});
|
|
38
40
|
this._buildAdjacencyLists(dependencyConfig.dependencies || {});
|
|
39
41
|
}
|
|
40
42
|
|
|
41
43
|
/**
|
|
42
44
|
* Pass 1: Register all calculations as nodes in the graph.
|
|
45
|
+
* --- MODIFIED: Added requirementsConfig parameter ---
|
|
43
46
|
*/
|
|
44
|
-
_registerNodes(unifiedCalculations) {
|
|
47
|
+
_registerNodes(unifiedCalculations, requirementsConfig) {
|
|
45
48
|
this.logger.log('INFO', '[DepGraph] Pass 1: Registering all calculation nodes...');
|
|
46
49
|
for (const category in unifiedCalculations) {
|
|
47
50
|
if (category === 'utils') continue;
|
|
@@ -53,14 +56,22 @@ class ComputationGraph {
|
|
|
53
56
|
if (typeof item === 'object' && item !== null && !Array.isArray(item)) {
|
|
54
57
|
for (const calcName in item) {
|
|
55
58
|
if (typeof item[calcName] === 'function') {
|
|
56
|
-
|
|
59
|
+
// --- MODIFIED: Create node, set requirements ---
|
|
60
|
+
const node = new ComputationNode(category, calcName, item[calcName]);
|
|
61
|
+
node.requires = requirementsConfig[calcName] || [];
|
|
62
|
+
this.nodes.set(calcName, node);
|
|
63
|
+
// --- END MODIFICATION ---
|
|
57
64
|
}
|
|
58
65
|
}
|
|
59
66
|
}
|
|
60
67
|
// Handle regular calcs at the root of the category
|
|
61
68
|
else if (typeof item === 'function') {
|
|
62
69
|
const calcName = subKey;
|
|
63
|
-
|
|
70
|
+
// --- MODIFIED: Create node, set requirements ---
|
|
71
|
+
const node = new ComputationNode(category, calcName, item[calcName]);
|
|
72
|
+
node.requires = requirementsConfig[calcName] || [];
|
|
73
|
+
this.nodes.set(calcName, node);
|
|
74
|
+
// --- END MODIFICATION ---
|
|
64
75
|
}
|
|
65
76
|
}
|
|
66
77
|
}
|
|
@@ -69,6 +80,7 @@ class ComputationGraph {
|
|
|
69
80
|
|
|
70
81
|
/**
|
|
71
82
|
* Pass 2: Build parent/child relationships from the dependency config.
|
|
83
|
+
* (This function remains unchanged as it only reads the 'dependencies' key)
|
|
72
84
|
*/
|
|
73
85
|
_buildAdjacencyLists(dependencies) {
|
|
74
86
|
this.logger.log('INFO', '[DepGraph] Pass 2: Building adjacency lists...');
|
|
@@ -99,6 +111,7 @@ class ComputationGraph {
|
|
|
99
111
|
/**
|
|
100
112
|
* Pass 3: Perform Topological Sort (Kahn's Algorithm) to find execution order
|
|
101
113
|
* and detect cycles. This is the "build" step.
|
|
114
|
+
* (This function remains unchanged)
|
|
102
115
|
*/
|
|
103
116
|
build() {
|
|
104
117
|
if (this.isBuilt) return;
|