bulltrackers-module 1.0.206 → 1.0.207

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,5 +1,6 @@
1
1
  /*
2
2
  * FILENAME: CloudFunctions/NpmWrappers/bulltrackers-module/functions/task-engine/helpers/update_helpers.js
3
+ * (OPTIMIZED V4: Auto-Speculator Detection via History/Portfolio Intersection)
3
4
  * (OPTIMIZED V3: Removed obsolete username lookup logic)
4
5
  * (OPTIMIZED V2: Added "Circuit Breaker" for Proxy failures)
5
6
  * (REFACTORED: Concurrency set to 1, added fallback and verbose logging)
@@ -26,27 +27,54 @@ function shouldTryProxy() {
26
27
  */
27
28
  function recordProxyOutcome(success) {
28
29
  if (success) {
29
- if (_consecutiveProxyFailures > 0) {
30
- // Optional: Only log recovery to reduce noise
31
- }
32
30
  _consecutiveProxyFailures = 0;
33
31
  } else {
34
32
  _consecutiveProxyFailures++;
35
33
  }
36
34
  }
37
35
 
36
+ /**
37
+ * --- NEW HELPER: Speculator Detector ---
38
+ * intersections: (History: Leverage > 1) AND (Portfolio: Currently Owned)
39
+ */
40
+ function detectSpeculatorTargets(historyData, portfolioData) {
41
+ if (!historyData?.PublicHistoryPositions || !portfolioData?.AggregatedPositions) return [];
42
+
43
+ // 1. Identify assets that have EVER been traded with leverage > 1
44
+ const leveragedAssets = new Set();
45
+ for (const pos of historyData.PublicHistoryPositions) {
46
+ if (pos.Leverage > 1 && pos.InstrumentID) {
47
+ leveragedAssets.add(pos.InstrumentID);
48
+ }
49
+ }
50
+
51
+ if (leveragedAssets.size === 0) return [];
52
+
53
+ // 2. Check if the user CURRENTLY owns any of these assets
54
+ const targets = [];
55
+ for (const pos of portfolioData.AggregatedPositions) {
56
+ if (leveragedAssets.has(pos.InstrumentID)) {
57
+ targets.push(pos.InstrumentID);
58
+ }
59
+ }
60
+
61
+ return targets;
62
+ }
63
+
38
64
  /**
39
65
  * (REFACTORED: Fully sequential, verbose logging, node-fetch fallback)
40
66
  */
41
- async function handleUpdate(task, taskId, { logger, headerManager, proxyManager, db, batchManager }, config) {
42
- // Note: 'username' param removed from signature as it is no longer needed.
67
+ async function handleUpdate(task, taskId, { logger, headerManager, proxyManager, db, batchManager, pubsub }, config) {
43
68
  const { userId, instruments, instrumentId, userType } = task;
44
69
  const instrumentsToProcess = userType === 'speculator' ? (instruments || [instrumentId]) : [undefined];
45
70
  const today = new Date().toISOString().slice(0, 10);
46
71
  const portfolioBlockId = `${Math.floor(parseInt(userId) / 1000000)}M`;
47
72
  let isPrivate = false;
48
73
 
49
- // DOWNGRADED TO TRACE
74
+ // Captured data for detection logic
75
+ let capturedHistory = null;
76
+ let capturedPortfolio = null;
77
+
50
78
  logger.log('TRACE', `[handleUpdate/${userId}] Starting update task. Type: ${userType}. Instruments: ${instrumentsToProcess.join(', ')}`);
51
79
 
52
80
  // --- 1. Process History Fetch (Sequentially) ---
@@ -63,11 +91,10 @@ async function handleUpdate(task, taskId, { logger, headerManager, proxyManager,
63
91
  } else {
64
92
 
65
93
  // --- REFACTOR: New Granular API Logic ---
66
- // No username required. Uses CID (userId) directly.
67
94
  const d = new Date();
68
95
  d.setFullYear(d.getFullYear() - 1);
69
96
  const oneYearAgoStr = d.toISOString();
70
- const uuid = crypto.randomUUID ? crypto.randomUUID() : '0205aca7-bd37-4884-8455-f28ce1add2de'; // Fallback for older nodes
97
+ const uuid = crypto.randomUUID ? crypto.randomUUID() : '0205aca7-bd37-4884-8455-f28ce1add2de';
71
98
 
72
99
  const historyUrl = `https://www.etoro.com/sapi/trade-data-real/history/public/credit/flat?StartTime=${oneYearAgoStr}&PageNumber=1&ItemsPerPage=30000&PublicHistoryPortfolioFilter=&CID=${userId}&client_request_id=${uuid}`;
73
100
  const options = { headers: historyHeader.header };
@@ -79,13 +106,12 @@ async function handleUpdate(task, taskId, { logger, headerManager, proxyManager,
79
106
  logger.log('TRACE', `[handleUpdate/${userId}] Attempting history fetch via AppScript proxy...`);
80
107
  response = await proxyManager.fetch(historyUrl, options);
81
108
  if (!response.ok) throw new Error(`AppScript proxy failed with status ${response.status}`);
82
-
83
109
  wasHistorySuccess = true;
84
110
  proxyUsedForHistory = true;
85
- recordProxyOutcome(true); // Reset
111
+ recordProxyOutcome(true);
86
112
 
87
113
  } catch (proxyError) {
88
- recordProxyOutcome(false); // Count failure
114
+ recordProxyOutcome(false);
89
115
  logger.log('WARN', `[handleUpdate/${userId}] History fetch via AppScript proxy FAILED. Error: ${proxyError.message}. Failures: ${_consecutiveProxyFailures}/${MAX_PROXY_FAILURES}.`, { error: proxyError.message, source: 'AppScript' });
90
116
  }
91
117
  }
@@ -105,6 +131,7 @@ async function handleUpdate(task, taskId, { logger, headerManager, proxyManager,
105
131
 
106
132
  if (wasHistorySuccess) {
107
133
  const data = await response.json();
134
+ capturedHistory = data; // Capture for later
108
135
  await batchManager.addToTradingHistoryBatch(userId, portfolioBlockId, today, data, userType);
109
136
  }
110
137
  }
@@ -118,7 +145,7 @@ async function handleUpdate(task, taskId, { logger, headerManager, proxyManager,
118
145
  }
119
146
 
120
147
  // --- 2. Process Portfolio Fetches (Sequentially) ---
121
- logger.log('TRACE', `[handleUpdate/${userId}] Starting ${instrumentsToProcess.length} sequential portfolio fetches.`); // DOWNGRADED TO TRACE
148
+ logger.log('TRACE', `[handleUpdate/${userId}] Starting ${instrumentsToProcess.length} sequential portfolio fetches.`);
122
149
 
123
150
  for (const instId of instrumentsToProcess) {
124
151
  if (isPrivate) {
@@ -141,13 +168,12 @@ async function handleUpdate(task, taskId, { logger, headerManager, proxyManager,
141
168
  logger.log('TRACE', `[handleUpdate/${userId}] Attempting portfolio fetch via AppScript proxy...`);
142
169
  response = await proxyManager.fetch(portfolioUrl, options);
143
170
  if (!response.ok) throw new Error(`AppScript proxy failed with status ${response.status}`);
144
-
145
171
  wasPortfolioSuccess = true;
146
172
  proxyUsedForPortfolio = true;
147
- recordProxyOutcome(true); // Reset
173
+ recordProxyOutcome(true);
148
174
 
149
175
  } catch (proxyError) {
150
- recordProxyOutcome(false); // Count failure
176
+ recordProxyOutcome(false);
151
177
  logger.log('WARN', `[handleUpdate/${userId}] Portfolio fetch via Proxy FAILED. Error: ${proxyError.message}. Failures: ${_consecutiveProxyFailures}/${MAX_PROXY_FAILURES}.`, { error: proxyError.message, source: 'AppScript' });
152
178
  }
153
179
  }
@@ -172,8 +198,9 @@ async function handleUpdate(task, taskId, { logger, headerManager, proxyManager,
172
198
 
173
199
  try {
174
200
  const portfolioJson = JSON.parse(body);
201
+ capturedPortfolio = portfolioJson; // Capture for detection
175
202
  await batchManager.addToPortfolioBatch(userId, portfolioBlockId, today, portfolioJson, userType, instId);
176
- logger.log('TRACE', `[handleUpdate/${userId}] Portfolio processed successfully.`); // DOWNGRADED TO TRACE
203
+ logger.log('TRACE', `[handleUpdate/${userId}] Portfolio processed successfully.`);
177
204
 
178
205
  } catch (parseError) {
179
206
  wasPortfolioSuccess = false;
@@ -186,7 +213,34 @@ async function handleUpdate(task, taskId, { logger, headerManager, proxyManager,
186
213
  if (proxyUsedForPortfolio) { headerManager.updatePerformance(portfolioHeader.id, wasPortfolioSuccess); }
187
214
  }
188
215
 
189
- // --- 5. Handle Private Users & Timestamps ---
216
+ // --- 5. SPECULATOR DETECTION & QUEUEING (NEW) ---
217
+ // Only run detection if:
218
+ // 1. We are processing a Normal User (userType !== 'speculator')
219
+ // 2. We successfully fetched both history and portfolio
220
+ // 3. We have PubSub available to queue new tasks
221
+ if (userType !== 'speculator' && capturedHistory && capturedPortfolio && pubsub && config.PUBSUB_TOPIC_TASK_ENGINE) {
222
+ try {
223
+ const speculatorAssets = detectSpeculatorTargets(capturedHistory, capturedPortfolio);
224
+ if (speculatorAssets.length > 0) {
225
+ logger.log('INFO', `[handleUpdate/${userId}] DETECTED SPECULATOR BEHAVIOR. Queuing ${speculatorAssets.length} targeted updates.`);
226
+
227
+ const newTasks = speculatorAssets.map(assetId => ({
228
+ type: 'update',
229
+ userType: 'speculator',
230
+ userId: userId,
231
+ instrumentId: assetId
232
+ }));
233
+
234
+ // Publish to Task Engine (Tasks are wrapped in a 'tasks' array payload)
235
+ const dataBuffer = Buffer.from(JSON.stringify({ tasks: newTasks }));
236
+ await pubsub.topic(config.PUBSUB_TOPIC_TASK_ENGINE).publishMessage({ data: dataBuffer });
237
+ }
238
+ } catch (detectionError) {
239
+ logger.log('ERROR', `[handleUpdate/${userId}] Error during Speculator Detection.`, { error: detectionError.message });
240
+ }
241
+ }
242
+
243
+ // --- 6. Handle Private Users & Timestamps ---
190
244
  if (isPrivate) {
191
245
  logger.log('WARN', `[handleUpdate/${userId}] Removing private user from updates.`);
192
246
  for (const instrumentId of instrumentsToProcess) {
@@ -207,7 +261,7 @@ async function handleUpdate(task, taskId, { logger, headerManager, proxyManager,
207
261
 
208
262
  if (userType === 'speculator') { await batchManager.addSpeculatorTimestampFix(userId, String(Math.floor(userId/1e6)*1e6)); }
209
263
 
210
- logger.log('TRACE', `[handleUpdate/${userId}] Update task finished successfully.`); // DOWNGRADED TO TRACE
264
+ logger.log('TRACE', `[handleUpdate/${userId}] Update task finished successfully.`);
211
265
  }
212
266
 
213
267
  module.exports = { handleUpdate };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bulltrackers-module",
3
- "version": "1.0.206",
3
+ "version": "1.0.207",
4
4
  "description": "Helper Functions for Bulltrackers.",
5
5
  "main": "index.js",
6
6
  "files": [