bulltrackers-module 1.0.360 → 1.0.362
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.
|
@@ -257,6 +257,10 @@ async function getPopularInvestorsToUpdate(dependencies, config) {
|
|
|
257
257
|
username: item.UserName
|
|
258
258
|
}));
|
|
259
259
|
|
|
260
|
+
if (targets.length > 0) {
|
|
261
|
+
logger.log('INFO', `[Core Utils Debug] First PI Target Mapped: ${JSON.stringify(targets[0])}`);
|
|
262
|
+
}
|
|
263
|
+
|
|
260
264
|
logger.log('INFO', `[Core Utils] Found ${targets.length} Popular Investors from ${today} ranking.`);
|
|
261
265
|
return targets;
|
|
262
266
|
|
|
@@ -2,13 +2,16 @@
|
|
|
2
2
|
* @fileoverview Logic for handling Popular Investor and On-Demand User updates.
|
|
3
3
|
* REFACTORED: Uses FirestoreBatchManager for sharded storage.
|
|
4
4
|
* UPDATED: On-Demand User Update now fetches Trade History (1 Year).
|
|
5
|
+
* FIXED: Added client_request_id to all PI fetches to prevent 500 errors.
|
|
6
|
+
* FIXED: Changed Deep Dive limit to Top 10 assets.
|
|
7
|
+
* DEBUG: Added verbose URL logging.
|
|
5
8
|
*/
|
|
6
9
|
|
|
7
10
|
const crypto = require('crypto');
|
|
8
11
|
|
|
9
12
|
/**
|
|
10
13
|
* Handles the update task for a Popular Investor.
|
|
11
|
-
* Fetches: Overall Portfolio, Top
|
|
14
|
+
* Fetches: Overall Portfolio, Top 10 Deep Positions, Trade History.
|
|
12
15
|
* Stores them via BatchManager to ensure sharding.
|
|
13
16
|
* @param {object} taskData - Contains { cid, username }.
|
|
14
17
|
* @param {object} config - Task Engine configuration.
|
|
@@ -29,13 +32,20 @@ async function handlePopularInvestorUpdate(taskData, config, dependencies) {
|
|
|
29
32
|
const today = new Date().toISOString().split('T')[0];
|
|
30
33
|
const blockId = '19M';
|
|
31
34
|
|
|
35
|
+
// Generate Request UUID (Critical for preventing 500s on PI endpoints)
|
|
36
|
+
const uuid = crypto.randomUUID ? crypto.randomUUID() : `req-${Date.now()}-${Math.floor(Math.random()*10000)}`;
|
|
37
|
+
|
|
32
38
|
try {
|
|
33
|
-
// 1. Fetch Overall Portfolio
|
|
34
|
-
const portfolioUrl = `${ETORO_API_PORTFOLIO_URL}?cid=${cid}`;
|
|
39
|
+
// --- 1. Fetch Overall Portfolio ---
|
|
40
|
+
const portfolioUrl = `${ETORO_API_PORTFOLIO_URL}?cid=${cid}&client_request_id=${uuid}`;
|
|
41
|
+
|
|
42
|
+
// [DEBUG] Log the EXACT URL being used
|
|
43
|
+
logger.log('INFO', `[PI DEBUG] Fetching Portfolio for ${username} (${cid}) URL: ${portfolioUrl}`);
|
|
44
|
+
|
|
35
45
|
const portfolioRes = await proxyManager.fetch(portfolioUrl, { method: 'GET' });
|
|
36
46
|
|
|
37
47
|
if (!portfolioRes.ok) {
|
|
38
|
-
throw new Error(`Failed to fetch portfolio for ${cid}: ${portfolioRes.status}`);
|
|
48
|
+
throw new Error(`Failed to fetch portfolio for ${cid}. Status: ${portfolioRes.status} URL: ${portfolioUrl}`);
|
|
39
49
|
}
|
|
40
50
|
const portfolioData = await portfolioRes.json();
|
|
41
51
|
|
|
@@ -46,22 +56,28 @@ async function handlePopularInvestorUpdate(taskData, config, dependencies) {
|
|
|
46
56
|
// Batch Write (Overall)
|
|
47
57
|
await batchManager.addToPortfolioBatch(String(cid), blockId, today, portfolioData, 'popular_investor');
|
|
48
58
|
|
|
49
|
-
// 2. Identify Top Assets for Deep Dive
|
|
59
|
+
// --- 2. Identify Top Assets for Deep Dive ---
|
|
50
60
|
const aggregatedPositions = portfolioData.AggregatedPositions || [];
|
|
51
61
|
const topPositions = aggregatedPositions
|
|
52
62
|
.sort((a, b) => b.Invested - a.Invested)
|
|
53
|
-
.slice(0,
|
|
63
|
+
.slice(0, 10); // Limit to Top 10
|
|
54
64
|
|
|
55
65
|
logger.log('INFO', `[PI Update] Fetching deep positions for top ${topPositions.length} assets for ${username}`);
|
|
56
66
|
|
|
57
67
|
const deepPositions = [];
|
|
58
68
|
for (const pos of topPositions) {
|
|
59
|
-
const posUrl = `${ETORO_API_POSITIONS_URL}?cid=${cid}&InstrumentID=${pos.InstrumentID}`;
|
|
69
|
+
const posUrl = `${ETORO_API_POSITIONS_URL}?cid=${cid}&InstrumentID=${pos.InstrumentID}&client_request_id=${uuid}`;
|
|
70
|
+
|
|
71
|
+
// [DEBUG] Log the EXACT Deep Dive URL
|
|
72
|
+
logger.log('INFO', `[PI DEBUG] Fetching Position for ${username} InstID: ${pos.InstrumentID} URL: ${posUrl}`);
|
|
73
|
+
|
|
60
74
|
try {
|
|
61
75
|
const deepRes = await proxyManager.fetch(posUrl, { method: 'GET' });
|
|
62
76
|
if (deepRes.ok) {
|
|
63
77
|
const deepData = await deepRes.json();
|
|
64
78
|
deepPositions.push({ instrumentId: pos.InstrumentID, ...deepData });
|
|
79
|
+
} else {
|
|
80
|
+
logger.log('WARN', `[PI DEBUG] Failed deep fetch. Status: ${deepRes.status} URL: ${posUrl}`);
|
|
65
81
|
}
|
|
66
82
|
} catch (err) {
|
|
67
83
|
logger.log('WARN', `[PI Update] Failed deep fetch for inst ${pos.InstrumentID} user ${cid}`, err);
|
|
@@ -73,21 +89,23 @@ async function handlePopularInvestorUpdate(taskData, config, dependencies) {
|
|
|
73
89
|
await batchManager.addToPortfolioBatch(String(cid), blockId, today, deepPayload, 'popular_investor_deep');
|
|
74
90
|
}
|
|
75
91
|
|
|
76
|
-
// 3. Fetch Trade History (Last 1 Year) with Pagination
|
|
77
|
-
// [FIX] Implemented Pagination
|
|
92
|
+
// --- 3. Fetch Trade History (Last 1 Year) with Pagination ---
|
|
78
93
|
const oneYearAgo = new Date();
|
|
79
94
|
oneYearAgo.setFullYear(oneYearAgo.getFullYear() - 1);
|
|
80
|
-
|
|
95
|
+
|
|
81
96
|
const historyBaseUrl = ETORO_API_HISTORY_URL || 'https://www.etoro.com/sapi/trade-data-real/history/public/credit/flat';
|
|
82
97
|
|
|
83
98
|
let allHistoryTrades = [];
|
|
84
99
|
let page = 1;
|
|
85
100
|
let hasMore = true;
|
|
86
|
-
const MAX_PAGES = 10;
|
|
101
|
+
const MAX_PAGES = 10;
|
|
87
102
|
|
|
88
103
|
while (hasMore && page <= MAX_PAGES) {
|
|
89
104
|
const historyUrl = `${historyBaseUrl}?StartTime=${oneYearAgo.toISOString()}&PageNumber=${page}&ItemsPerPage=1000&PublicHistoryPortfolioFilter=&CID=${cid}&client_request_id=${uuid}`;
|
|
90
105
|
|
|
106
|
+
// [DEBUG] Log the EXACT History URL
|
|
107
|
+
logger.log('INFO', `[PI DEBUG] Fetching History Page ${page} for ${username} URL: ${historyUrl}`);
|
|
108
|
+
|
|
91
109
|
try {
|
|
92
110
|
const historyRes = await proxyManager.fetch(historyUrl, { method: 'GET' });
|
|
93
111
|
if (historyRes.ok) {
|
|
@@ -97,12 +115,12 @@ async function handlePopularInvestorUpdate(taskData, config, dependencies) {
|
|
|
97
115
|
if (trades.length > 0) {
|
|
98
116
|
allHistoryTrades = allHistoryTrades.concat(trades);
|
|
99
117
|
page++;
|
|
100
|
-
hasMore = (trades.length === 1000);
|
|
118
|
+
hasMore = (trades.length === 1000);
|
|
101
119
|
} else {
|
|
102
120
|
hasMore = false;
|
|
103
121
|
}
|
|
104
122
|
} else {
|
|
105
|
-
logger.log('WARN', `[PI Update] History fetch failed on page ${page} for ${username}`);
|
|
123
|
+
logger.log('WARN', `[PI Update] History fetch failed on page ${page} for ${username}. Status: ${historyRes.status}`);
|
|
106
124
|
hasMore = false;
|
|
107
125
|
}
|
|
108
126
|
} catch (e) {
|
|
@@ -130,8 +148,6 @@ async function handlePopularInvestorUpdate(taskData, config, dependencies) {
|
|
|
130
148
|
|
|
131
149
|
/**
|
|
132
150
|
* Handles the On-Demand update for a Signed-In User.
|
|
133
|
-
* Fetches: Overall Portfolio AND Trade History.
|
|
134
|
-
* Triggered by User Login/Verification.
|
|
135
151
|
*/
|
|
136
152
|
async function handleOnDemandUserUpdate(taskData, config, dependencies) {
|
|
137
153
|
const { cid, username } = taskData;
|
|
@@ -141,11 +157,15 @@ async function handleOnDemandUserUpdate(taskData, config, dependencies) {
|
|
|
141
157
|
logger.log('INFO', `[On-Demand Update] Fetching Portfolio & History for Signed-In User: ${username} (${cid})`);
|
|
142
158
|
|
|
143
159
|
const today = new Date().toISOString().split('T')[0];
|
|
144
|
-
const blockId = '19M';
|
|
160
|
+
const blockId = '19M';
|
|
161
|
+
const uuid = crypto.randomUUID ? crypto.randomUUID() : `req-${Date.now()}-${Math.floor(Math.random()*10000)}`;
|
|
145
162
|
|
|
146
163
|
try {
|
|
147
|
-
|
|
148
|
-
|
|
164
|
+
const portfolioUrl = `${ETORO_API_PORTFOLIO_URL}?cid=${cid}&client_request_id=${uuid}`;
|
|
165
|
+
|
|
166
|
+
// [DEBUG] Log URL for On-Demand
|
|
167
|
+
logger.log('INFO', `[On-Demand DEBUG] Fetching Portfolio URL: ${portfolioUrl}`);
|
|
168
|
+
|
|
149
169
|
const portfolioRes = await proxyManager.fetch(portfolioUrl, { method: 'GET' });
|
|
150
170
|
|
|
151
171
|
if (!portfolioRes.ok) {
|
|
@@ -156,43 +176,34 @@ async function handleOnDemandUserUpdate(taskData, config, dependencies) {
|
|
|
156
176
|
portfolioData.lastUpdated = new Date();
|
|
157
177
|
portfolioData.username = username;
|
|
158
178
|
|
|
159
|
-
// Batch Write (Signed-In Portfolio)
|
|
160
179
|
await batchManager.addToPortfolioBatch(String(cid), blockId, today, portfolioData, 'signed_in_user');
|
|
161
180
|
|
|
162
|
-
//
|
|
163
|
-
// This is critical for the new DAG requirements
|
|
181
|
+
// History Fetch
|
|
164
182
|
const oneYearAgo = new Date();
|
|
165
183
|
oneYearAgo.setFullYear(oneYearAgo.getFullYear() - 1);
|
|
166
|
-
|
|
167
|
-
const uuid = crypto.randomUUID ? crypto.randomUUID() : '0205aca7-bd37-4884-8455-f28ce1add2de';
|
|
168
184
|
const historyBaseUrl = ETORO_API_HISTORY_URL || 'https://www.etoro.com/sapi/trade-data-real/history/public/credit/flat';
|
|
169
185
|
const historyUrl = `${historyBaseUrl}?StartTime=${oneYearAgo.toISOString()}&PageNumber=1&ItemsPerPage=1000&PublicHistoryPortfolioFilter=&CID=${cid}&client_request_id=${uuid}`;
|
|
170
186
|
|
|
187
|
+
logger.log('INFO', `[On-Demand DEBUG] Fetching History URL: ${historyUrl}`);
|
|
188
|
+
|
|
171
189
|
const historyRes = await proxyManager.fetch(historyUrl, { method: 'GET' });
|
|
172
190
|
|
|
173
191
|
if (historyRes.ok) {
|
|
174
192
|
const historyData = await historyRes.json();
|
|
175
193
|
historyData.fetchedAt = new Date();
|
|
176
194
|
|
|
177
|
-
|
|
178
|
-
const VALID_REASONS = [0, 1, 5]; // 1=Close, 5=SL/TP etc. Remove 17/18 (Copy)
|
|
195
|
+
const VALID_REASONS = [0, 1, 5];
|
|
179
196
|
if (historyData.PublicHistoryPositions) {
|
|
180
|
-
const originalCount = historyData.PublicHistoryPositions.length;
|
|
181
197
|
historyData.PublicHistoryPositions = historyData.PublicHistoryPositions.filter(
|
|
182
198
|
p => VALID_REASONS.includes(p.CloseReason)
|
|
183
199
|
);
|
|
184
|
-
logger.log('INFO', `[On-Demand Update] Filtered history for ${username}: ${originalCount} -> ${historyData.PublicHistoryPositions.length}`);
|
|
185
200
|
}
|
|
186
|
-
|
|
187
|
-
// Batch Write (Signed-In History)
|
|
188
201
|
await batchManager.addToTradingHistoryBatch(String(cid), blockId, today, historyData, 'signed_in_user');
|
|
189
|
-
|
|
190
202
|
} else {
|
|
191
|
-
logger.log('WARN', `[On-Demand Update] History fetch failed for ${username} (${historyRes.status})
|
|
203
|
+
logger.log('WARN', `[On-Demand Update] History fetch failed for ${username} (${historyRes.status})`);
|
|
192
204
|
}
|
|
193
205
|
|
|
194
206
|
logger.log('SUCCESS', `[On-Demand Update] Complete for ${username}`);
|
|
195
|
-
|
|
196
207
|
} catch (error) {
|
|
197
208
|
logger.log('ERROR', `[On-Demand Update] Failed for ${username}`, error);
|
|
198
209
|
throw error;
|
|
@@ -11,7 +11,9 @@
|
|
|
11
11
|
|
|
12
12
|
const { handleDiscover } = require('../helpers/discover_helpers');
|
|
13
13
|
const { handleVerify } = require('../helpers/verify_helpers');
|
|
14
|
-
const { handleUpdate } = require('../helpers/update_helpers');
|
|
14
|
+
const { handleUpdate } = require('../helpers/update_helpers');
|
|
15
|
+
// --- FIX: Import the handlers for the new task types ---
|
|
16
|
+
const { handlePopularInvestorUpdate, handleOnDemandUserUpdate } = require('../helpers/popular_investor_helpers');
|
|
15
17
|
const pLimit = require('p-limit');
|
|
16
18
|
|
|
17
19
|
/**
|
|
@@ -27,24 +29,21 @@ function parseTaskPayload(message, logger) {
|
|
|
27
29
|
}
|
|
28
30
|
|
|
29
31
|
/**
|
|
30
|
-
* Sorts tasks into update and other (discover/verify).
|
|
31
|
-
* REFACTORED: Simplified. No username lookup logic needed.
|
|
32
|
+
* Sorts tasks into update and other (discover/verify/PI/OnDemand).
|
|
32
33
|
*/
|
|
33
34
|
async function prepareTaskBatches(tasks, batchManager, logger) {
|
|
34
35
|
const tasksToRun = [], otherTasks = [];
|
|
35
36
|
|
|
36
37
|
for (const task of tasks) {
|
|
37
38
|
if (task.type === 'update') {
|
|
38
|
-
//
|
|
39
|
+
// Standard portfolio updates (Normal/Speculator)
|
|
39
40
|
tasksToRun.push(task);
|
|
40
41
|
} else {
|
|
42
|
+
// Discover, Verify, Popular Investor, Signed-In User
|
|
41
43
|
otherTasks.push(task);
|
|
42
44
|
}
|
|
43
45
|
}
|
|
44
46
|
|
|
45
|
-
// We explicitly return empty structures for compatibility if handler_creator expects them,
|
|
46
|
-
// though ideally handler_creator should also be simplified.
|
|
47
|
-
// For now, we return compatible object structure.
|
|
48
47
|
return { tasksToRun, cidsToLookup: new Map(), otherTasks };
|
|
49
48
|
}
|
|
50
49
|
|
|
@@ -53,26 +52,53 @@ async function prepareTaskBatches(tasks, batchManager, logger) {
|
|
|
53
52
|
* (REFACTORED: Concurrency limit set to 1)
|
|
54
53
|
*/
|
|
55
54
|
async function executeTasks(tasksToRun, otherTasks, dependencies, config, taskId) {
|
|
56
|
-
const { logger
|
|
55
|
+
const { logger } = dependencies;
|
|
57
56
|
|
|
58
|
-
// --- REFACTOR 1: REMOVE CONCURRENCY ---
|
|
59
|
-
// Set limit to 1 to serialize all tasks and prevent AppScript throttling.
|
|
60
57
|
const limit = pLimit(1);
|
|
61
|
-
// --- END REFACTOR 1 ---
|
|
62
58
|
|
|
63
59
|
const allTaskPromises = [];
|
|
64
|
-
let taskCounters = { update: 0, discover: 0, verify: 0, unknown: 0, failed: 0 };
|
|
60
|
+
let taskCounters = { update: 0, discover: 0, verify: 0, popular_investor: 0, on_demand: 0, unknown: 0, failed: 0 };
|
|
65
61
|
|
|
66
|
-
// 1. Queue 'other' tasks
|
|
62
|
+
// 1. Queue 'other' tasks
|
|
67
63
|
for (const task of otherTasks) {
|
|
68
|
-
const subTaskId = `${task.type}-${task.userType || 'unknown'}-${task.userId || task.cids?.[0] || 'sub'}`;
|
|
69
|
-
|
|
64
|
+
const subTaskId = `${task.type}-${task.userType || 'unknown'}-${task.userId || task.cids?.[0] || task.cid || 'sub'}`;
|
|
65
|
+
|
|
66
|
+
// --- FIX START: Explicitly handle new task types ---
|
|
67
|
+
if (task.type === 'POPULAR_INVESTOR_UPDATE') {
|
|
68
|
+
allTaskPromises.push(limit(() =>
|
|
69
|
+
handlePopularInvestorUpdate(task, config, dependencies)
|
|
70
|
+
.then(() => taskCounters.popular_investor++)
|
|
71
|
+
.catch(err => {
|
|
72
|
+
logger.log('ERROR', `[TaskEngine/${taskId}] Error in POPULAR_INVESTOR_UPDATE for ${task.cid || task.username}`, { errorMessage: err.message });
|
|
73
|
+
taskCounters.failed++;
|
|
74
|
+
})
|
|
75
|
+
));
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (task.type === 'ON_DEMAND_USER_UPDATE') {
|
|
80
|
+
allTaskPromises.push(limit(() =>
|
|
81
|
+
handleOnDemandUserUpdate(task, config, dependencies)
|
|
82
|
+
.then(() => taskCounters.on_demand++)
|
|
83
|
+
.catch(err => {
|
|
84
|
+
logger.log('ERROR', `[TaskEngine/${taskId}] Error in ON_DEMAND_USER_UPDATE for ${task.cid || task.username}`, { errorMessage: err.message });
|
|
85
|
+
taskCounters.failed++;
|
|
86
|
+
})
|
|
87
|
+
));
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
// --- FIX END ---
|
|
91
|
+
|
|
92
|
+
// Handle legacy types (discover/verify)
|
|
93
|
+
// Normalize type to lowercase to handle potential mismatch if Orchestrator sends uppercase
|
|
94
|
+
const normalizedType = task.type.toLowerCase();
|
|
95
|
+
const handler = { discover: handleDiscover, verify: handleVerify }[normalizedType];
|
|
70
96
|
|
|
71
97
|
if (handler) {
|
|
72
98
|
allTaskPromises.push(
|
|
73
99
|
limit(() =>
|
|
74
100
|
handler(task, subTaskId, dependencies, config)
|
|
75
|
-
.then(() => taskCounters[
|
|
101
|
+
.then(() => taskCounters[normalizedType]++)
|
|
76
102
|
.catch(err => {
|
|
77
103
|
logger.log('ERROR', `[TaskEngine/${taskId}] Error in ${task.type} for ${subTaskId}`, { errorMessage: err.message });
|
|
78
104
|
taskCounters.failed++;
|
|
@@ -85,13 +111,8 @@ async function executeTasks(tasksToRun, otherTasks, dependencies, config, taskId
|
|
|
85
111
|
}
|
|
86
112
|
}
|
|
87
113
|
|
|
88
|
-
// 2. Queue 'update' tasks
|
|
114
|
+
// 2. Queue 'update' tasks (Standard Normal/Speculator)
|
|
89
115
|
for (const task of tasksToRun) {
|
|
90
|
-
// We unpack 'task' directly now, no wrapping object {task, username}
|
|
91
|
-
// However, we must ensure backward compatibility if the array was {task, username} before.
|
|
92
|
-
// In prepareTaskBatches above, we pushed raw 'task'.
|
|
93
|
-
// So we use 'task' directly.
|
|
94
|
-
|
|
95
116
|
const subTaskId = `${task.type}-${task.userType || 'unknown'}-${task.userId}`;
|
|
96
117
|
allTaskPromises.push(
|
|
97
118
|
limit(() =>
|
|
@@ -105,15 +126,14 @@ async function executeTasks(tasksToRun, otherTasks, dependencies, config, taskId
|
|
|
105
126
|
);
|
|
106
127
|
}
|
|
107
128
|
|
|
108
|
-
// 3. Wait for ALL tasks to complete
|
|
129
|
+
// 3. Wait for ALL tasks to complete
|
|
109
130
|
await Promise.all(allTaskPromises);
|
|
110
131
|
|
|
111
132
|
// 4. Log final summary
|
|
112
133
|
logger.log(
|
|
113
134
|
taskCounters.failed > 0 ? 'WARN' : 'SUCCESS',
|
|
114
|
-
`[TaskEngine/${taskId}] Processed all tasks. Updates: ${taskCounters.update}, Discovers: ${taskCounters.discover}, Verifies: ${taskCounters.verify}, Unknown: ${taskCounters.unknown}, Failed: ${taskCounters.failed}.`
|
|
135
|
+
`[TaskEngine/${taskId}] Processed all tasks. Updates: ${taskCounters.update}, Discovers: ${taskCounters.discover}, Verifies: ${taskCounters.verify}, PI: ${taskCounters.popular_investor}, OnDemand: ${taskCounters.on_demand}, Unknown: ${taskCounters.unknown}, Failed: ${taskCounters.failed}.`
|
|
115
136
|
);
|
|
116
137
|
}
|
|
117
138
|
|
|
118
|
-
// Note: runUsernameLookups removed from exports
|
|
119
139
|
module.exports = { parseTaskPayload, prepareTaskBatches, executeTasks };
|