bulltrackers-module 1.0.127 → 1.0.129
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.
- package/functions/computation-system/helpers/computation_pass_runner.js +20 -773
- package/functions/computation-system/helpers/orchestration_helpers.js +88 -867
- package/functions/computation-system/utils/data_loader.js +84 -151
- package/functions/computation-system/utils/utils.js +55 -98
- package/functions/orchestrator/helpers/discovery_helpers.js +40 -188
- package/functions/orchestrator/helpers/update_helpers.js +21 -61
- package/functions/orchestrator/index.js +42 -121
- package/functions/task-engine/handler_creator.js +22 -143
- package/functions/task-engine/helpers/discover_helpers.js +20 -90
- package/functions/task-engine/helpers/update_helpers.js +90 -185
- package/functions/task-engine/helpers/verify_helpers.js +43 -159
- package/functions/task-engine/utils/firestore_batch_manager.js +97 -290
- package/functions/task-engine/utils/task_engine_utils.js +99 -0
- package/package.json +1 -1
- package/functions/task-engine/utils/api_calls.js +0 -0
- package/functions/task-engine/utils/firestore_ops.js +0 -0
|
@@ -1,26 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @fileoverview Main pipe for the Task Engine.
|
|
3
|
-
* REFACTORED: This
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* --- MODIFICATION ---
|
|
7
|
-
* The `handleRequest` function is now designed to receive a *batch*
|
|
8
|
-
* of tasks (e.g., { tasks: [...] }) from the Dispatcher. It loops
|
|
9
|
-
* through all tasks in a single function instance, allowing the
|
|
10
|
-
* FirestoreBatchManager to accumulate state. It then explicitly
|
|
11
|
-
* calls `batchManager.flushBatches()` in the `finally` block
|
|
12
|
-
* to commit all changes.
|
|
13
|
-
* --- V2 MODIFICATION ---
|
|
14
|
-
* - Loads username map at start.
|
|
15
|
-
* - Sorts tasks into "run now" (username known) and "lookup needed".
|
|
16
|
-
* - Runs a batch lookup for missing usernames.
|
|
17
|
-
* - Runs all update tasks *after* lookups are complete.
|
|
3
|
+
* REFACTORED: This function is the high-level "manual of processes"
|
|
4
|
+
* that orchestrates the task engine logic.
|
|
18
5
|
*/
|
|
19
6
|
|
|
20
|
-
|
|
21
|
-
const {
|
|
22
|
-
|
|
23
|
-
|
|
7
|
+
// Import the new utility functions that contain the logic
|
|
8
|
+
const {
|
|
9
|
+
parseTaskPayload,
|
|
10
|
+
prepareTaskBatches,
|
|
11
|
+
runUsernameLookups,
|
|
12
|
+
executeTasks
|
|
13
|
+
} = require('./utils/task_engine_utils'); // <-- Import from our new utils file
|
|
24
14
|
|
|
25
15
|
/**
|
|
26
16
|
* Main pipe: pipe.taskEngine.handleRequest
|
|
@@ -31,131 +21,20 @@ const { handleUpdate, lookupUsernames } = require('./helpers/update_helpers');
|
|
|
31
21
|
* @param {object} dependencies - Contains all clients: db, pubsub, logger, headerManager, proxyManager, batchManager.
|
|
32
22
|
*/
|
|
33
23
|
async function handleRequest(message, context, config, dependencies) {
|
|
34
|
-
const { logger, batchManager
|
|
35
|
-
|
|
36
|
-
if (!message || !message.data) {
|
|
37
|
-
logger.log('ERROR', '[TaskEngine Module] Received invalid message structure.', { message });
|
|
38
|
-
return;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
let payload;
|
|
24
|
+
const { logger, batchManager } = dependencies;
|
|
25
|
+
const taskId = `batch-${context.eventId || Date.now()}`;
|
|
42
26
|
try {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
logger.log('
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
logger.log('ERROR', `[TaskEngine] Received invalid payload. Expected { tasks: [...] }`, { payload });
|
|
51
|
-
return;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
if (payload.tasks.length === 0) {
|
|
55
|
-
logger.log('WARN', '[TaskEngine] Received message with an empty tasks array.');
|
|
56
|
-
return;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
const taskId = `batch-${payload.tasks[0]?.type || 'unknown'}-${Date.now()}`;
|
|
60
|
-
logger.log('INFO', `[TaskEngine/${taskId}] Received batch of ${payload.tasks.length} tasks.`);
|
|
61
|
-
|
|
62
|
-
try {
|
|
63
|
-
// --- START V2 MODIFICATION ---
|
|
64
|
-
|
|
65
|
-
// 1. Load the username map into the batch manager's memory
|
|
66
|
-
await batchManager.loadUsernameMap();
|
|
67
|
-
|
|
68
|
-
// 2. Sort tasks
|
|
69
|
-
const tasksToRun = []; // { task, username }
|
|
70
|
-
const cidsToLookup = new Map(); // Map<string, object>
|
|
71
|
-
const otherTasks = []; // for 'discover' and 'verify'
|
|
72
|
-
|
|
73
|
-
for (const task of payload.tasks) {
|
|
74
|
-
if (task.type === 'update') {
|
|
75
|
-
const username = batchManager.getUsername(task.userId);
|
|
76
|
-
if (username) {
|
|
77
|
-
tasksToRun.push({ task, username });
|
|
78
|
-
} else {
|
|
79
|
-
cidsToLookup.set(String(task.userId), task);
|
|
80
|
-
}
|
|
81
|
-
} else if (task.type === 'discover' || task.type === 'verify') {
|
|
82
|
-
otherTasks.push(task);
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
logger.log('INFO', `[TaskEngine/${taskId}] Usernames known for ${tasksToRun.length} users. Looking up ${cidsToLookup.size} users. Processing ${otherTasks.length} other tasks.`);
|
|
87
|
-
|
|
88
|
-
// 3. Run username lookups if needed
|
|
89
|
-
if (cidsToLookup.size > 0) {
|
|
90
|
-
const foundUsers = await lookupUsernames(Array.from(cidsToLookup.keys()), dependencies, config);
|
|
91
|
-
|
|
92
|
-
for (const user of foundUsers) {
|
|
93
|
-
const cidStr = String(user.CID);
|
|
94
|
-
const username = user.Value.UserName;
|
|
95
|
-
|
|
96
|
-
// Add to batch manager to save this new mapping
|
|
97
|
-
batchManager.addUsernameMapUpdate(cidStr, username);
|
|
98
|
-
|
|
99
|
-
// Get the original task
|
|
100
|
-
const task = cidsToLookup.get(cidStr);
|
|
101
|
-
if (task) {
|
|
102
|
-
// Add to the list of tasks to run
|
|
103
|
-
tasksToRun.push({ task, username });
|
|
104
|
-
// Remove from lookup map
|
|
105
|
-
cidsToLookup.delete(cidStr);
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
if (cidsToLookup.size > 0) {
|
|
110
|
-
logger.log('WARN', `[TaskEngine/${taskId}] Could not find usernames for ${cidsToLookup.size} CIDs (likely private). They will be skipped.`, { skippedCids: Array.from(cidsToLookup.keys()) });
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
// 4. Process all tasks
|
|
115
|
-
|
|
116
|
-
// Process discover/verify tasks
|
|
117
|
-
for (const task of otherTasks) {
|
|
118
|
-
const subTaskId = `${task.type || 'unknown'}-${task.userType || 'unknown'}-${task.userId || task.cids?.[0] || 'sub'}`;
|
|
119
|
-
const handlerFunction = {
|
|
120
|
-
discover: handleDiscover,
|
|
121
|
-
verify: handleVerify,
|
|
122
|
-
}[task.type];
|
|
123
|
-
|
|
124
|
-
if (handlerFunction) {
|
|
125
|
-
await handlerFunction(task, subTaskId, dependencies, config);
|
|
126
|
-
} else {
|
|
127
|
-
logger.log('ERROR', `[TaskEngine/${taskId}] Unknown task type in 'otherTasks': ${task.type}`);
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
// Process update tasks (now with usernames)
|
|
132
|
-
for (const { task, username } of tasksToRun) {
|
|
133
|
-
const subTaskId = `${task.type || 'unknown'}-${task.userType || 'unknown'}-${task.userId || 'sub'}`;
|
|
134
|
-
try {
|
|
135
|
-
// Pass the username to handleUpdate
|
|
136
|
-
await handleUpdate(task, subTaskId, dependencies, config, username);
|
|
137
|
-
} catch (updateError) {
|
|
138
|
-
logger.log('ERROR', `[TaskEngine/${taskId}] Error in handleUpdate for user ${task.userId}`, { errorMessage: updateError.message, errorStack: updateError.stack });
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
logger.log('SUCCESS', `[TaskEngine/${taskId}] Processed all tasks. Done.`);
|
|
143
|
-
// --- END V2 MODIFICATION ---
|
|
144
|
-
|
|
145
|
-
} catch (error) {
|
|
146
|
-
logger.log('ERROR', `[TaskEngine/${taskId}] Failed during batch processing.`, { errorMessage: error.message, errorStack: error.stack });
|
|
147
|
-
|
|
27
|
+
const tasks = parseTaskPayload(message, logger);
|
|
28
|
+
if (!tasks) {return; }
|
|
29
|
+
logger.log('INFO', `[TaskEngine/${taskId}] Received batch of ${tasks.length} tasks.`);
|
|
30
|
+
const { tasksToRun, cidsToLookup, otherTasks } = await prepareTaskBatches(tasks, batchManager, logger);
|
|
31
|
+
await runUsernameLookups(tasksToRun, cidsToLookup, dependencies, config, batchManager, logger);
|
|
32
|
+
await executeTasks(tasksToRun, otherTasks, dependencies, config, taskId);
|
|
33
|
+
} catch (error) {logger.log('ERROR', `[TaskEngine/${taskId}] Failed during batch processing.`, { errorMessage: error.message, errorStack: error.stack });
|
|
148
34
|
} finally {
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
logger.log('INFO', `[TaskEngine/${taskId}] Final batch and header flush complete.`);
|
|
153
|
-
} catch (flushError) {
|
|
154
|
-
logger.log('ERROR', `[TaskEngine/${taskId}] Error during final flush attempt.`, { error: flushError.message });
|
|
155
|
-
}
|
|
156
|
-
}
|
|
35
|
+
try {logger.log('INFO', `[TaskEngine/${taskId}] Flushing all accumulated batches...`);
|
|
36
|
+
await batchManager.flushBatches();
|
|
37
|
+
logger.log('INFO', `[TaskEngine/${taskId}] Final batch and header flush complete.`);} catch (flushError) {logger.log('ERROR', `[TaskEngine/${taskId}] Error during final flush attempt.`, { error: flushError.message });}}
|
|
157
38
|
}
|
|
158
39
|
|
|
159
|
-
module.exports = {
|
|
160
|
-
handleRequest,
|
|
161
|
-
};
|
|
40
|
+
module.exports = {handleRequest};
|
|
@@ -1,8 +1,3 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* FILENAME: CloudFunctions/NpmWrappers/bulltrackers-module/functions/task-engine/helpers/discover_helpers.js
|
|
3
|
-
* (FIXED: Publishes 'verify' task in the correct batch format { tasks: [...] })
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
1
|
/**
|
|
7
2
|
* @fileoverview Sub-pipe: pipe.taskEngine.handleDiscover
|
|
8
3
|
* REFACTORED: Now stateless and receives dependencies.
|
|
@@ -19,120 +14,55 @@
|
|
|
19
14
|
async function handleDiscover(task, taskId, dependencies, config) {
|
|
20
15
|
const { logger, headerManager, proxyManager, pubsub, batchManager } = dependencies;
|
|
21
16
|
const { cids, blockId, instrument, userType } = task;
|
|
22
|
-
|
|
23
17
|
const url = `${config.ETORO_API_RANKINGS_URL}?Period=LastTwoYears`;
|
|
24
18
|
const selectedHeader = await headerManager.selectHeader();
|
|
25
19
|
if (!selectedHeader) throw new Error("Could not select a header.");
|
|
26
|
-
|
|
27
20
|
let wasSuccess = false;
|
|
28
|
-
try {
|
|
29
|
-
if (userType === 'speculator') {
|
|
30
|
-
// Use the batchManager from dependencies
|
|
31
|
-
batchManager.addProcessedSpeculatorCids(cids);
|
|
32
|
-
logger.log('INFO', `[DISCOVER] Added ${cids.length} speculator CIDs to the in-memory set to be flushed.`);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
// Use the proxyManager from dependencies
|
|
36
|
-
const response = await proxyManager.fetch(url, {
|
|
37
|
-
method: 'POST',
|
|
38
|
-
headers: { ...selectedHeader.header, 'Content-Type': 'application/json' },
|
|
39
|
-
body: JSON.stringify(cids),
|
|
40
|
-
});
|
|
41
|
-
if (!response.ok) {
|
|
42
|
-
throw new Error(`API status ${response.status}`);
|
|
43
|
-
}
|
|
44
|
-
wasSuccess = true;
|
|
45
21
|
|
|
22
|
+
// Step 1. Discover Speculators
|
|
23
|
+
try {if (userType === 'speculator') {batchManager.addProcessedSpeculatorCids(cids);logger.log('INFO', `[DISCOVER] Added ${cids.length} speculator CIDs to the in-memory set to be flushed.`);}
|
|
24
|
+
const response = await proxyManager.fetch(url, {method: 'POST',headers: { ...selectedHeader.header, 'Content-Type': 'application/json' },body: JSON.stringify(cids),});
|
|
25
|
+
if (!response.ok) {throw new Error(`API status ${response.status}`);}
|
|
26
|
+
wasSuccess = true;
|
|
46
27
|
const publicUsers = await response.json();
|
|
47
28
|
if (!Array.isArray(publicUsers)) return;
|
|
48
|
-
|
|
49
29
|
const oneMonthAgo = new Date();
|
|
50
30
|
oneMonthAgo.setMonth(oneMonthAgo.getMonth() - 1);
|
|
51
|
-
|
|
52
|
-
const preliminaryActiveUsers = publicUsers.filter(user =>
|
|
53
|
-
new Date(user.Value.LastActivity) > oneMonthAgo &&
|
|
54
|
-
user.Value.DailyGain !== 0 &&
|
|
55
|
-
user.Value.Exposure !== 0 &&
|
|
56
|
-
user.Value.RiskScore !== 0
|
|
57
|
-
);
|
|
58
|
-
|
|
31
|
+
// Step 2. We know a user is "active" if their risk score is NOT 0 and they logged in, in the last month. This covers the edge case that a user IS active but does NOT hold positions (cannot hold no positions and have 0 risk score)
|
|
32
|
+
const preliminaryActiveUsers = publicUsers.filter(user => new Date(user.Value.LastActivity) > oneMonthAgo &&user.Value.DailyGain !== 0 &&user.Value.Exposure !== 0 &&user.Value.RiskScore !== 0);
|
|
59
33
|
let finalActiveUsers = [];
|
|
60
34
|
const invalidCidsToLog = [];
|
|
61
|
-
|
|
62
|
-
if (userType === 'speculator') {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
if (publicUserCids.size > 0 && publicUserCids.size < cids.length) {
|
|
66
|
-
const privateUserCids = cids.filter(cid => !publicUserCids.has(cid));
|
|
67
|
-
invalidCidsToLog.push(...privateUserCids);
|
|
68
|
-
}
|
|
69
|
-
|
|
35
|
+
// Step 3. Users that were passed INTO the discovery API but did not come OUT OF the discovery API, are private accounts, the API filters this for us, so mark them as such to avoid processing them again.
|
|
36
|
+
if (userType === 'speculator') {const publicUserCids = new Set(publicUsers.map(u => u.CID));
|
|
37
|
+
if (publicUserCids.size > 0 && publicUserCids.size < cids.length) {const privateUserCids = cids.filter(cid => !publicUserCids.has(cid));invalidCidsToLog.push(...privateUserCids);}
|
|
70
38
|
const activeUserCids = new Set(preliminaryActiveUsers.map(u => u.CID));
|
|
71
|
-
const inactiveUserCids = publicUsers
|
|
72
|
-
.filter(u => !activeUserCids.has(u.CID))
|
|
73
|
-
.map(u => u.CID);
|
|
39
|
+
const inactiveUserCids = publicUsers.filter(u => !activeUserCids.has(u.CID)).map(u => u.CID);
|
|
74
40
|
invalidCidsToLog.push(...inactiveUserCids);
|
|
75
|
-
|
|
76
41
|
logger.log('INFO', `[DISCOVER] Applying new speculator pre-filter to ${preliminaryActiveUsers.length} active users.`);
|
|
77
42
|
const nonSpeculatorCids = [];
|
|
78
|
-
|
|
43
|
+
// Step 4. For the remaining active users, apply heuristic filters to weed out non-speculators.
|
|
79
44
|
for (const user of preliminaryActiveUsers) {
|
|
80
45
|
const v = user.Value;
|
|
81
46
|
const totalLeverage = (v.MediumLeveragePct || 0) + (v.HighLeveragePct || 0);
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
(v.Trades || 0) > 500 ||
|
|
85
|
-
(v.TotalTradedInstruments || 0) > 50 ||
|
|
86
|
-
totalLeverage > 50 ||
|
|
87
|
-
(v.WeeklyDD || 0) < -25
|
|
88
|
-
);
|
|
89
|
-
|
|
90
|
-
if (isLikelySpeculator) {
|
|
91
|
-
finalActiveUsers.push(user);
|
|
92
|
-
} else {
|
|
93
|
-
nonSpeculatorCids.push(user.CID);
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
47
|
+
const isLikelySpeculator = ((v.Trades || 0) > 500 ||(v.TotalTradedInstruments || 0) > 50 ||totalLeverage > 50 ||(v.WeeklyDD || 0) < -25);
|
|
48
|
+
if (isLikelySpeculator) {finalActiveUsers.push(user);} else {nonSpeculatorCids.push(user.CID);}}
|
|
97
49
|
invalidCidsToLog.push(...nonSpeculatorCids);
|
|
98
50
|
logger.log('INFO', `[DISCOVER] Pre-filter complete. ${finalActiveUsers.length} users passed. ${nonSpeculatorCids.length} users failed heuristic.`);
|
|
99
|
-
|
|
100
51
|
if (invalidCidsToLog.length > 0) {
|
|
101
|
-
|
|
102
|
-
await pubsub.topic(config.PUBSUB_TOPIC_INVALID_SPECULATOR_LOG)
|
|
103
|
-
.publishMessage({ json: { invalidCids: invalidCidsToLog } });
|
|
52
|
+
await pubsub.topic(config.PUBSUB_TOPIC_INVALID_SPECULATOR_LOG).publishMessage({ json: { invalidCids: invalidCidsToLog } });
|
|
104
53
|
logger.log('INFO', `[DISCOVER] Reported ${invalidCidsToLog.length} invalid (private, inactive, or failed heuristic) speculator IDs.`);
|
|
105
54
|
}
|
|
106
|
-
|
|
107
|
-
} else {
|
|
55
|
+
// Step 5. Remaining users are public active non speculators
|
|
56
|
+
} else {
|
|
108
57
|
finalActiveUsers = preliminaryActiveUsers;
|
|
109
58
|
}
|
|
110
|
-
|
|
59
|
+
// Step 6. Publish 'verify' task for all active users found
|
|
111
60
|
if (finalActiveUsers.length > 0) {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
const verificationTask = {
|
|
115
|
-
type: 'verify',
|
|
116
|
-
users: finalActiveUsers.map(u => ({
|
|
117
|
-
cid: u.CID,
|
|
118
|
-
isBronze: u.Value.IsBronze,
|
|
119
|
-
username: u.Value.UserName // <-- ADD THIS
|
|
120
|
-
})),
|
|
121
|
-
blockId,
|
|
122
|
-
instrument,
|
|
123
|
-
userType
|
|
124
|
-
};
|
|
125
|
-
// --- END MODIFICATION ---
|
|
126
|
-
|
|
127
|
-
// --- FIX: WRAP IN BATCH FORMAT ---
|
|
128
|
-
// Use pubsub from dependencies
|
|
129
|
-
await pubsub.topic(config.PUBSUB_TOPIC_USER_FETCH)
|
|
130
|
-
.publishMessage({ json: { tasks: [verificationTask] } });
|
|
61
|
+
const verificationTask = {type: 'verify',users: finalActiveUsers.map(u => ({ cid: u.CID, isBronze: u.Value.IsBronze,username: u.Value.UserName})), blockId, instrument, userType};
|
|
62
|
+
await pubsub.topic(config.PUBSUB_TOPIC_USER_FETCH).publishMessage({ json: { tasks: [verificationTask] } });
|
|
131
63
|
logger.log('INFO', `[DISCOVER] Verification message published was : ${JSON.stringify({ tasks: [verificationTask] })} `);
|
|
132
|
-
// --- END FIX ---
|
|
133
64
|
logger.log('INFO', `[DISCOVER] Chaining to 'verify' task for ${finalActiveUsers.length} active users.`);
|
|
134
65
|
}
|
|
135
|
-
|
|
136
66
|
} finally {
|
|
137
67
|
if (selectedHeader) headerManager.updatePerformance(selectedHeader.id, wasSuccess);
|
|
138
68
|
}
|