bulltrackers-module 1.0.105 → 1.0.107

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.
Files changed (33) hide show
  1. package/README.MD +222 -222
  2. package/functions/appscript-api/helpers/errors.js +19 -19
  3. package/functions/appscript-api/index.js +58 -58
  4. package/functions/computation-system/helpers/orchestration_helpers.js +667 -113
  5. package/functions/computation-system/utils/data_loader.js +191 -191
  6. package/functions/computation-system/utils/utils.js +149 -254
  7. package/functions/core/utils/firestore_utils.js +433 -433
  8. package/functions/core/utils/pubsub_utils.js +53 -53
  9. package/functions/dispatcher/helpers/dispatch_helpers.js +47 -47
  10. package/functions/dispatcher/index.js +52 -52
  11. package/functions/etoro-price-fetcher/helpers/handler_helpers.js +124 -124
  12. package/functions/fetch-insights/helpers/handler_helpers.js +91 -91
  13. package/functions/generic-api/helpers/api_helpers.js +379 -379
  14. package/functions/generic-api/index.js +150 -150
  15. package/functions/invalid-speculator-handler/helpers/handler_helpers.js +75 -75
  16. package/functions/orchestrator/helpers/discovery_helpers.js +226 -226
  17. package/functions/orchestrator/helpers/update_helpers.js +92 -92
  18. package/functions/orchestrator/index.js +147 -147
  19. package/functions/price-backfill/helpers/handler_helpers.js +116 -123
  20. package/functions/social-orchestrator/helpers/orchestrator_helpers.js +61 -61
  21. package/functions/social-task-handler/helpers/handler_helpers.js +288 -288
  22. package/functions/task-engine/handler_creator.js +78 -78
  23. package/functions/task-engine/helpers/discover_helpers.js +125 -125
  24. package/functions/task-engine/helpers/update_helpers.js +118 -118
  25. package/functions/task-engine/helpers/verify_helpers.js +162 -162
  26. package/functions/task-engine/utils/firestore_batch_manager.js +258 -258
  27. package/index.js +105 -113
  28. package/package.json +45 -45
  29. package/functions/computation-system/computation_dependencies.json +0 -120
  30. package/functions/computation-system/helpers/worker_helpers.js +0 -340
  31. package/functions/computation-system/utils/computation_state_manager.js +0 -178
  32. package/functions/computation-system/utils/dependency_graph.js +0 -191
  33. package/functions/speculator-cleanup-orchestrator/helpers/cleanup_helpers.js +0 -160
@@ -1,340 +0,0 @@
1
- /**
2
- * @fileoverview Main pipe: pipe.computationSystem.handleNodeTask
3
- *
4
- * This function is the "Worker" for the event-driven DAG.
5
- * It is triggered by a Pub/Sub message for a *single node* (computation).
6
- *
7
- * Its responsibilities are to:
8
- * 1. Receive a task: { date, nodeName }.
9
- * 2. Execute that single computation.
10
- * 3. On success:
11
- * a. Mark the node as 'success' in the StateManager.
12
- * b. Ask the StateManager for children that are now 'ready'.
13
- * c. Publish new tasks for those ready children.
14
- * 4. On failure:
15
- * a. Log the error.
16
- * b. Acknowledge the message (do not re-throw).
17
- * c. The system will "heal" when the missing dependency eventually runs
18
- * and re-triggers this failed node.
19
- */
20
-
21
- const { FieldPath } = require('@google-cloud/firestore');
22
- // Import data loaders
23
- const {
24
- getPortfolioPartRefs,
25
- loadFullDayMap,
26
- loadDataByRefs,
27
- loadDailyInsights,
28
- loadDailySocialPostInsights
29
- } = require('../utils/data_loader.js');
30
- // Import utils
31
- const { commitBatchInChunks, categorizeCalculations } = require('../utils/utils.js');
32
- const stateManager = require('../utils/computation_state_manager.js');
33
-
34
- // --- Internal Helper: findCalcClass (no changes) ---
35
- function findCalcClass(calculations, category, name) {
36
- if (calculations[category]) {
37
- if (typeof calculations[category][name] === 'function') {
38
- return calculations[category][name];
39
- }
40
- if (calculations[category].historical && typeof calculations[category].historical[name] === 'function') {
41
- return calculations[category].historical[name];
42
- }
43
- }
44
- return null;
45
- }
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
-
54
- /**
55
- * Streams data and runs a *single* Batch 1-style computation.
56
- * --- THIS IS THE FULLY UPDATED FUNCTION ---
57
- */
58
- async function runStreamingComputation(dateStr, node, config, dependencies, calculations) {
59
- const { db, logger } = dependencies;
60
-
61
- // --- MODIFIED: Destructure 'requires' from the node ---
62
- const { category, name: calcName, calcClass: CalculationClass, requires } = node;
63
- const passName = `[Worker ${dateStr}/${calcName}]`;
64
-
65
- logger.log('INFO', `${passName} Starting streaming computation... (Requires: ${requires.join(', ') || 'none'})`);
66
-
67
- // --- Dynamic Data Loading ---
68
- let todayInsightsData = null;
69
- let yesterdayInsightsData = null;
70
- let todaySocialPostInsightsData = null;
71
- let yesterdaySocialPostInsightsData = null;
72
- let todayRefs = [];
73
- let yesterdayPortfolios = {};
74
- const prevStr = getPrevStr(dateStr); // Get yesterday's date string
75
-
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')) {
92
- const yesterdayRefs = await getPortfolioPartRefs(config, dependencies, prevStr);
93
- if (yesterdayRefs.length > 0) {
94
- yesterdayPortfolios = await loadFullDayMap(config, dependencies, yesterdayRefs);
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}.`);
98
- }
99
- }
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;
106
- }
107
-
108
- // --- Context Object (no change) ---
109
- const { instrumentToTicker, instrumentToSector } = await dependencies.calculationUtils.loadInstrumentMappings();
110
- const context = {
111
- instrumentMappings: instrumentToTicker,
112
- sectorMapping: instrumentToSector,
113
- todayDateStr: dateStr,
114
- yesterdayDateStr: prevStr,
115
- dependencies: dependencies,
116
- config: config
117
- };
118
-
119
- const calc = new CalculationClass();
120
- const batchSize = config.partRefBatchSize || 10;
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';
138
- if ((userType === 'normal' && category === 'speculators') || (userType === 'speculator' && !['speculators', 'sanity'].includes(category))) {
139
- continue;
140
- }
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;
153
- }
154
- }
155
- }
156
-
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.`);
160
- await Promise.resolve(calc.process(null, null, null, ...allContextArgs));
161
- }
162
- // --- End Stream ---
163
-
164
- // --- Get Result and Commit (no change) ---
165
- const result = await Promise.resolve(calc.getResult());
166
-
167
- if (result === null) {
168
- throw new Error(`Calculation ${calcName} returned null. Dependency likely missing.`);
169
- }
170
-
171
- const resultsCollectionRef = db.collection(config.resultsCollection).doc(dateStr).collection(config.resultsSubcollection);
172
- const pendingWrites = [];
173
- const summaryData = {};
174
-
175
- if (result && Object.keys(result).length > 0) {
176
- let isSharded = false;
177
- const shardedCollections = {
178
- 'sharded_user_profile': config.shardedUserProfileCollection,
179
- 'sharded_user_profitability': config.shardedProfitabilityCollection
180
- };
181
-
182
- for (const resultKey in shardedCollections) {
183
- if (result[resultKey]) {
184
- isSharded = true;
185
- const shardCollectionName = shardedCollections[resultKey];
186
- const shardedData = result[resultKey];
187
- for (const shardId in shardedData) {
188
- const shardDocData = shardedData[shardId];
189
- if (shardDocData && Object.keys(shardDocData).length > 0) {
190
- const shardRef = db.collection(shardCollectionName).doc(shardId);
191
- pendingWrites.push({ ref: shardRef, data: shardDocData });
192
- }
193
- }
194
- const { [resultKey]: _, ...otherResults } = result;
195
- if (Object.keys(otherResults).length > 0) {
196
- const computationDocRef = resultsCollectionRef.doc(category)
197
- .collection(config.computationsSubcollection)
198
- .doc(calcName);
199
- pendingWrites.push({ ref: computationDocRef, data: otherResults });
200
- }
201
- }
202
- }
203
-
204
- if (!isSharded) {
205
- const computationDocRef = resultsCollectionRef.doc(category)
206
- .collection(config.computationsSubcollection)
207
- .doc(calcName);
208
- pendingWrites.push({ ref: computationDocRef, data: result });
209
- }
210
-
211
- summaryData[`${category}.${calcName}`] = true; // Use dot notation for summary
212
- const topLevelDocRef = db.collection(config.resultsCollection).doc(dateStr);
213
- pendingWrites.push({ ref: topLevelDocRef, data: summaryData });
214
-
215
- await commitBatchInChunks(config, dependencies, pendingWrites, `${passName} Commit`);
216
- } else {
217
- logger.log('WARN', `${passName} Calculation produced no results. Skipping write.`);
218
- }
219
- }
220
-
221
- /**
222
- * Runs a *single* meta/backtest computation.
223
- * (This function remains unchanged as 'meta' calcs handle their own data loading)
224
- */
225
- async function runMetaComputation(dateStr, node, config, dependencies) {
226
- const { db, logger } = dependencies;
227
- const { category, name: calcName, calcClass: CalculationClass } = node;
228
- const passName = `[Worker ${dateStr}/${calcName}]`;
229
-
230
- logger.log('INFO', `${passName} Starting meta computation...`);
231
-
232
- const calc = new CalculationClass();
233
- const result = await Promise.resolve(calc.process(dateStr, dependencies, config));
234
-
235
- // Handle null result as a dependency failure
236
- if (result === null) {
237
- throw new Error(`Meta-calculation ${calcName} returned null. Dependency likely missing.`);
238
- }
239
-
240
- const resultsCollectionRef = db.collection(config.resultsCollection).doc(dateStr).collection(config.resultsSubcollection);
241
- const pendingWrites = [];
242
- const summaryData = {};
243
-
244
- if (result && Object.keys(result).length > 0) {
245
- const computationDocRef = resultsCollectionRef.doc(category)
246
- .collection(config.computationsSubcollection)
247
- .doc(calcName);
248
- pendingWrites.push({ ref: computationDocRef, data: result });
249
-
250
- summaryData[`${category}.${calcName}`] = true; // Use dot notation for summary
251
- const topLevelDocRef = db.collection(config.resultsCollection).doc(dateStr);
252
- pendingWrites.push({ ref: topLevelDocRef, data: summaryData });
253
-
254
- await commitBatchInChunks(config, dependencies, pendingWrites, `${passName} Commit`);
255
- } else {
256
- logger.log('WARN', `${passName} Meta-calculation produced no results. Skipping write.`);
257
- }
258
- }
259
-
260
-
261
- // --- Main Exported Function ---
262
-
263
- /**
264
- * Main pipe: pipe.computationSystem.handleNodeTask
265
- * (This function remains unchanged)
266
- */
267
- async function handleNodeTask(message, context, config, dependencies, calculations) {
268
- const { logger, pubsubUtils } = dependencies;
269
- const graph = dependencies.computationGraph; // Get the graph from dependencies
270
-
271
- let task;
272
- try {
273
- task = JSON.parse(Buffer.from(message.data, 'base64').toString('utf-8'));
274
- } catch (e) {
275
- logger.log('ERROR', '[Compute Worker] Failed to parse Pub/Sub message data.', { error: e.message, data: message.data });
276
- return; // Acknowledge the message
277
- }
278
-
279
- const { date, nodeName } = task;
280
- const taskId = `[Worker ${date}/${nodeName}]`;
281
-
282
- if (!graph || !graph.isBuilt) {
283
- logger.log('ERROR', `${taskId} CRITICAL: ComputationGraph not available. Cannot proceed.`);
284
- return; // Acknowledge
285
- }
286
-
287
- const node = graph.getNode(nodeName);
288
- if (!node) {
289
- logger.log('ERROR', `${taskId} CRITICAL: Node name not found in graph. Task is invalid.`);
290
- return; // Acknowledge
291
- }
292
-
293
- logger.log('INFO', `🚀 ${taskId} Received task. Category: ${node.category}`);
294
-
295
- try {
296
- // 1. Route to the correct execution helper
297
- // --- MODIFIED: Check for 'meta' requirement key ---
298
- if (node.requires.includes('meta')) {
299
- await runMetaComputation(date, node, config, dependencies);
300
- } else {
301
- // All other calcs (pnl, behavioural, etc.) use the streaming helper
302
- await runStreamingComputation(date, node, config, dependencies, calculations);
303
- }
304
-
305
- // 2. On Success: Mark success and trigger children
306
- const readyChildren = await stateManager.markNodeSuccessAndGetReadyChildren(dependencies, date, nodeName);
307
-
308
- if (readyChildren.length > 0) {
309
- const nextTasks = readyChildren.map(childName => ({
310
- date: date,
311
- nodeName: childName
312
- }));
313
-
314
- const topicName = config.computeNodeTopicName;
315
- await pubsubUtils.batchPublishTasks(dependencies, {
316
- topicName: topicName,
317
- tasks: nextTasks,
318
- taskType: `Compute Node Trigger`
319
- });
320
- logger.log('SUCCESS', `${taskId} Complete. Triggering ${readyChildren.length} child node(s): ${readyChildren.join(', ')}`);
321
-
322
- } else {
323
- logger.log('SUCCESS', `${taskId} Complete. No new children are ready.`);
324
- }
325
-
326
- } catch (error) {
327
- logger.log('WARN', `${taskId} FAILED (Will retry when dependencies are met).`, {
328
- err: error.message,
329
- stack: error.stack
330
- });
331
- return;
332
- }
333
- }
334
-
335
- module.exports = {
336
- handleNodeTask,
337
- // Exporting these for testing
338
- runStreamingComputation,
339
- runMetaComputation
340
- };
@@ -1,178 +0,0 @@
1
- /**
2
- * @fileoverview Manages the state of a computation DAG run in Firestore.
3
- *
4
- * This allows the system to be event-driven, resilient, and self-healing.
5
- * It tracks which nodes have completed and which are ready to run.
6
- */
7
-
8
- const { FieldValue, FieldPath } = require('@google-cloud/firestore');
9
-
10
- const STATE_COLLECTION = 'computation_state'; // Stores state docs, e.g., /computation_state/2025-10-31
11
-
12
- /**
13
- * Initializes the state document for a new DAG run.
14
- * @param {object} dependencies - Contains db, logger.
15
- * @param {string} dateStr - The date (YYYY-MM-DD) to run for.
16
- * @param {Array<object>} allNodes - Array of all node objects from the graph.
17
- */
18
- async function initializeState(dependencies, dateStr, allNodes) {
19
- const { db, logger } = dependencies;
20
- const stateDocRef = db.collection(STATE_COLLECTION).doc(dateStr);
21
-
22
- logger.log('INFO', `[StateManager] Initializing state for ${dateStr}...`);
23
-
24
- const initialState = {
25
- status: 'processing',
26
- lastUpdated: FieldValue.serverTimestamp(),
27
- nodes: {}
28
- };
29
-
30
- allNodes.forEach(node => {
31
- initialState.nodes[node.name] = {
32
- status: 'pending',
33
- category: node.category,
34
- parentCount: node.parents.size,
35
- parents: Array.from(node.parents),
36
- children: Array.from(node.children)
37
- };
38
- });
39
-
40
- await stateDocRef.set(initialState);
41
- logger.log('INFO', `[StateManager] State for ${dateStr} initialized with ${allNodes.length} nodes.`);
42
- }
43
-
44
- /**
45
- * Marks a node as 'success' and returns all children that are now ready to run.
46
- * @param {object} dependencies - Contains db, logger.
47
- * @param {string} dateStr - The date (YYYY-MM-DD) of the run.
48
- * @param {string} nodeName - The name of the node that just completed.
49
- * @returns {Promise<string[]>} A list of child node names that are ready to run.
50
- */
51
- async function markNodeSuccessAndGetReadyChildren(dependencies, dateStr, nodeName) {
52
- const { db, logger } = dependencies;
53
- const stateDocRef = db.collection(STATE_COLLECTION).doc(dateStr);
54
-
55
- logger.log('INFO', `[StateManager] Marking node '${nodeName}' as success for ${dateStr}.`);
56
-
57
- const readyChildren = [];
58
-
59
- try {
60
- await db.runTransaction(async (transaction) => {
61
- const stateDoc = await transaction.get(stateDocRef);
62
- if (!stateDoc.exists) {
63
- logger.log('ERROR', `[StateManager] State doc for ${dateStr} does not exist. Cannot mark success.`);
64
- return;
65
- }
66
-
67
- const state = stateDoc.data();
68
- const nodes = state.nodes;
69
-
70
- // 1. Mark this node as success
71
- const update = {};
72
- update[`nodes.${nodeName}.status`] = 'success';
73
- update['lastUpdated'] = FieldValue.serverTimestamp();
74
-
75
- // 2. Check all children of this node
76
- const children = nodes[nodeName]?.children || [];
77
- for (const childName of children) {
78
- const childNode = nodes[childName];
79
- if (!childNode) {
80
- logger.log('WARN', `[StateManager] Child node ${childName} not found in state doc.`);
81
- continue;
82
- }
83
-
84
- // If child is already done or failed, skip it
85
- if (childNode.status !== 'pending') {
86
- continue;
87
- }
88
-
89
- // Check if all parents of this child are now 'success'
90
- let allParentsSuccess = true;
91
- for (const parentName of childNode.parents) {
92
- const parentStatus = (parentName === nodeName) ? 'success' : nodes[parentName]?.status;
93
- if (parentStatus !== 'success') {
94
- allParentsSuccess = false;
95
- break;
96
- }
97
- }
98
-
99
- if (allParentsSuccess) {
100
- logger.log('INFO', `[StateManager] All dependencies for '${childName}' are met. Marking as 'ready'.`);
101
- update[`nodes.${childName}.status`] = 'ready'; // Mark as 'ready' to avoid double-runs
102
- readyChildren.push(childName);
103
- }
104
- }
105
-
106
- transaction.update(stateDocRef, update);
107
- });
108
-
109
- return readyChildren;
110
-
111
- } catch (error) {
112
- logger.log('ERROR', `[StateManager] Transaction failed for ${nodeName} on ${dateStr}.`, { err: error.message });
113
- return []; // Return empty on error
114
- }
115
- }
116
-
117
- /**
118
- * Finds all dates that are incomplete (status 'processing') or missing.
119
- * @param {object} dependencies - Contains db, logger.
120
- * @param {string[]} allExpectedDates - All dates that *should* exist.
121
- * @returns {Promise<string[]>} A list of date strings (YYYY-MM-DD) to process.
122
- */
123
- async function getIncompleteDates(dependencies, allExpectedDates) {
124
- const { db, logger } = dependencies;
125
- if (allExpectedDates.length === 0) return [];
126
-
127
- logger.log('INFO', `[StateManager] Checking for incomplete dates...`);
128
-
129
- const datesToProcess = new Set();
130
- const refs = allExpectedDates.map(dateStr => db.collection(STATE_COLLECTION).doc(dateStr));
131
- const snapshots = await db.getAll(...refs);
132
-
133
- for (let i = 0; i < snapshots.length; i++) {
134
- const dateStr = allExpectedDates[i];
135
- const doc = snapshots[i];
136
-
137
- if (!doc.exists) {
138
- // Date is completely missing
139
- datesToProcess.add(dateStr);
140
- continue;
141
- }
142
-
143
- const data = doc.data();
144
- if (data.status === 'processing') {
145
- // Date was started but not finished (e.g., a node failed)
146
- datesToProcess.add(dateStr);
147
- continue;
148
- }
149
-
150
- // If status is 'complete', we're good.
151
- }
152
-
153
- logger.log('INFO', `[StateManager] Found ${datesToProcess.size} dates to process.`);
154
- return Array.from(datesToProcess);
155
- }
156
-
157
- /**
158
- * Marks an entire DAG run as 'complete'.
159
- * @param {object} dependencies - Contains db, logger.
160
- * @param {string} dateStr - The date (YYYY-MM-DD) to mark as complete.
161
- */
162
- async function markDagComplete(dependencies, dateStr) {
163
- const { db, logger } = dependencies;
164
- const stateDocRef = db.collection(STATE_COLLECTION).doc(dateStr);
165
-
166
- logger.log('SUCCESS', `[StateManager] Marking DAG for ${dateStr} as complete.`);
167
- await stateDocRef.update({
168
- status: 'complete',
169
- lastUpdated: FieldValue.serverTimestamp()
170
- });
171
- }
172
-
173
- module.exports = {
174
- initializeState,
175
- markNodeSuccessAndGetReadyChildren,
176
- getIncompleteDates,
177
- markDagComplete
178
- };