bulltrackers-module 1.0.134 → 1.0.136

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.
@@ -165,7 +165,7 @@ async function runStandardComputationPass(date, calcs, passName, config, deps, r
165
165
  const docRef = deps.db.collection(config.resultsCollection).doc(dStr)
166
166
  .collection(config.resultsSubcollection).doc(calc.manifest.category)
167
167
  .collection(config.computationsSubcollection).doc(name);
168
-
168
+ console.log('Writing standard result for', name, 'on', dStr);
169
169
  standardWrites.push({ ref: docRef, data: standardResult });
170
170
  }
171
171
  success++;
@@ -232,6 +232,7 @@ async function runMetaComputationPass(date, calcs, passName, config, deps, fetch
232
232
  const docRef = deps.db.collection(config.resultsCollection).doc(dStr)
233
233
  .collection(config.resultsSubcollection).doc(mCalc.category)
234
234
  .collection(config.computationsSubcollection).doc(name);
235
+ console.log('Writing standard result for', name, 'on', dStr);
235
236
  standardWrites.push({ ref: docRef, data: standardResult });
236
237
  }
237
238
  success++;
@@ -260,25 +260,48 @@ async function getSpeculatorsToUpdate(dependencies, config) {
260
260
  const { dateThreshold, gracePeriodThreshold, speculatorBlocksCollectionName } = config;
261
261
  logger.log('INFO','[Core Utils] Getting speculators to update...');
262
262
  const updates = [];
263
+
264
+ // ⚠️ NEW: Collect per user first
265
+ const userMap = new Map(); // userId -> { instruments: Set }
266
+
263
267
  try {
264
268
  const blocksRef = db.collection(speculatorBlocksCollectionName);
265
269
  const snapshot = await blocksRef.get();
266
- if (snapshot.empty) { logger.log('INFO','[Core Utils] No speculator blocks found.'); return []; }
270
+ if (snapshot.empty) {
271
+ logger.log('INFO','[Core Utils] No speculator blocks found.');
272
+ return [];
273
+ }
274
+
267
275
  snapshot.forEach(doc => {
268
276
  const blockData = doc.data();
269
277
  for (const key in blockData) {
270
- if (!key.startsWith('users.')) continue;
271
- const userId = key.split('.')[1];
272
- if (!userId) continue;
273
- const userData = blockData[key];
274
- const lastVerified = userData.lastVerified?.toDate ? userData.lastVerified.toDate() : new Date(0);
275
- const lastHeld = userData.lastHeldSpeculatorAsset?.toDate ? userData.lastHeldSpeculatorAsset.toDate() : new Date(0);
276
- if (lastVerified < dateThreshold && lastHeld > gracePeriodThreshold) { if (userData.instruments && Array.isArray(userData.instruments)) { userData.instruments.forEach(instrumentId => { updates.push({ userId, instrumentId }); });
277
- }
278
- }
278
+ if (!key.startsWith('users.')) continue;
279
+ const userId = key.split('.')[1];
280
+ if (!userId) continue;
281
+ const userData = blockData[key];
282
+ const lastVerified = userData.lastVerified?.toDate ? userData.lastVerified.toDate() : new Date(0);
283
+ const lastHeld = userData.lastHeldSpeculatorAsset?.toDate ? userData.lastHeldSpeculatorAsset.toDate() : new Date(0);
284
+
285
+ if (lastVerified < dateThreshold && lastHeld > gracePeriodThreshold) {
286
+ if (!userMap.has(userId)) {
287
+ userMap.set(userId, new Set());
288
+ }
289
+ if (userData.instruments && Array.isArray(userData.instruments)) {
290
+ userData.instruments.forEach(id => userMap.get(userId).add(id));
291
+ }
292
+ }
279
293
  }
280
294
  });
281
- logger.log('INFO',`[Core Utils] Found ${updates.length} speculator user/instrument pairs to update.`);
295
+
296
+ // ⚠️ NEW: Return one task per user with ALL instruments
297
+ for (const [userId, instrumentSet] of userMap) {
298
+ updates.push({
299
+ userId,
300
+ instruments: Array.from(instrumentSet) // ⚠️ Array of all instruments
301
+ });
302
+ }
303
+
304
+ logger.log('INFO',`[Core Utils] Found ${updates.length} speculator users to update (covering ${[...userMap.values()].reduce((sum, set) => sum + set.size, 0)} total instruments).`);
282
305
  return updates;
283
306
  } catch (error) {
284
307
  logger.log('ERROR','[Core Utils] Error getting speculators to update', { errorMessage: error.message });
@@ -40,100 +40,104 @@ async function lookupUsernames(cids, { logger, headerManager, proxyManager }, {
40
40
 
41
41
  // --- START MODIFICATION: Added historyFetchedForUser argument ---
42
42
  async function handleUpdate(task, taskId, { logger, headerManager, proxyManager, db, batchManager }, config, username, historyFetchedForUser) {
43
- // --- END MODIFICATION ---
44
-
45
- const { userId, instrumentId, userType } = task;
46
- const portfolioUrl = userType === 'speculator'
47
- ? `${config.ETORO_API_POSITIONS_URL}?cid=${userId}&InstrumentID=${instrumentId}`
48
- : `${config.ETORO_API_PORTFOLIO_URL}?cid=${userId}`;
43
+ const { userId, instruments, instrumentId, userType } = task; // ⚠️ Now supports both
44
+
45
+ // ⚠️ Support both old (instrumentId) and new (instruments array) format
46
+ const instrumentsToProcess = userType === 'speculator'
47
+ ? (instruments || [instrumentId]) // New format or fallback to old
48
+ : [undefined]; // Normal users don't have instruments
49
49
 
50
- // --- MODIFICATION: Moved historyUrl definition inside conditional ---
51
50
  const today = new Date().toISOString().slice(0, 10);
52
51
  const portfolioBlockId = `${Math.floor(parseInt(userId) / 1000000)}M`;
53
-
54
- // --- MODIFICATION: Select history header only if needed ---
52
+
55
53
  let portfolioHeader = await headerManager.selectHeader();
56
- let historyHeader = null; // Will be selected if needed
54
+ let historyHeader = null;
57
55
  if (!portfolioHeader) throw new Error("Could not select portfolio header.");
58
-
59
- let wasPortfolioSuccess = false, wasHistorySuccess = false, isPrivate = false;
60
-
56
+
57
+ let wasHistorySuccess = false, isPrivate = false;
58
+
61
59
  try {
62
- // --- START MODIFICATION: Build promises conditionally ---
60
+ // Fetch history ONCE per user
63
61
  const promisesToRun = [];
64
-
65
- // 1. Always fetch portfolio
66
- promisesToRun.push(proxyManager.fetch(portfolioUrl, { headers: portfolioHeader.header }));
67
-
68
- // 2. Conditionally fetch history
69
62
  let fetchHistory = false;
63
+
70
64
  if (!historyFetchedForUser.has(userId)) {
71
- historyHeader = await headerManager.selectHeader(); // Select header just-in-time
72
- if (!historyHeader) {
73
- logger.log('WARN', `[UPDATE] Could not select history header for ${userId}, skipping history fetch for this batch.`);
74
- historyFetchedForUser.add(userId); // Add to set to prevent retries in this batch
75
- } else {
65
+ historyHeader = await headerManager.selectHeader();
66
+ if (historyHeader) {
76
67
  fetchHistory = true;
77
- historyFetchedForUser.add(userId); // Mark as fetched for this batch
68
+ historyFetchedForUser.add(userId);
78
69
  const historyUrl = `${config.ETORO_API_USERSTATS_URL}${username}/trades/oneYearAgo?CopyAsAsset=true`;
79
70
  promisesToRun.push(proxyManager.fetch(historyUrl, { headers: historyHeader.header }));
80
71
  }
81
72
  }
82
- // --- END MODIFICATION ---
83
-
84
- // --- Run Promises ---
85
- const results = await Promise.allSettled(promisesToRun);
86
- const portfolioRes = results[0];
87
- const historyRes = fetchHistory ? results[1] : null; // History is only index 1 if we fetched it
88
-
89
- // --- Process Portfolio (results[0]) ---
90
- if (portfolioRes.status === 'fulfilled' && portfolioRes.value.ok) {
91
- const body = await portfolioRes.value.text();
92
- if (body.includes("user is PRIVATE")) isPrivate = true;
93
- else { wasPortfolioSuccess = true; await batchManager.addToPortfolioBatch(userId, portfolioBlockId, today, JSON.parse(body), userType, instrumentId); }
94
- } else {
95
- let errMsg = portfolioRes.status === 'rejected' ? portfolioRes.reason.message : `API status ${portfolioRes.value.status}`;
96
- let rawText = portfolioRes.value?.text ? await portfolioRes.value.text() : 'N/A';
97
- logger.log('WARN', `[UPDATE] Portfolio fetch failed for ${userId}`, { error: errMsg, proxyResponse: rawText });
98
- }
99
-
100
- // --- Process History (results[1], if it exists) ---
101
- if (fetchHistory && historyRes) { // Check if we ran this promise
73
+
74
+ // Process history result (if fetched)
75
+ if (fetchHistory) {
76
+ const results = await Promise.allSettled(promisesToRun);
77
+ const historyRes = results[0];
102
78
  if (historyRes.status === 'fulfilled' && historyRes.value.ok) {
103
79
  const data = await historyRes.value.json();
104
80
  wasHistorySuccess = true;
105
81
  await batchManager.addToTradingHistoryBatch(userId, portfolioBlockId, today, data, userType);
106
- } else {
107
- // History fetch failed
108
- let errMsg = historyRes.status === 'rejected' ? historyRes.reason.message : `API status ${historyRes.value.status}`;
109
- let rawText = historyRes.value?.text ? await historyRes.value.text() : 'N/A';
110
- logger.log('WARN', `[UPDATE] History fetch failed for ${userId} (${username})`, { error: errMsg, proxyResponse: rawText });
111
82
  }
112
83
  }
113
- // --- END MODIFICATION ---
114
-
115
-
116
- // --- Private user handling ---
84
+
85
+ // Now fetch portfolio for EACH instrument (speculators) or once (normal)
86
+ for (const instrumentId of instrumentsToProcess) {
87
+ const portfolioUrl = userType === 'speculator'
88
+ ? `${config.ETORO_API_POSITIONS_URL}?cid=${userId}&InstrumentID=${instrumentId}`
89
+ : `${config.ETORO_API_PORTFOLIO_URL}?cid=${userId}`;
90
+
91
+ let wasPortfolioSuccess = false;
92
+
93
+ const portfolioRes = await proxyManager.fetch(portfolioUrl, { headers: portfolioHeader.header });
94
+
95
+ if (portfolioRes.ok) {
96
+ const body = await portfolioRes.text();
97
+ if (body.includes("user is PRIVATE")) {
98
+ isPrivate = true;
99
+ break; // Stop processing this user
100
+ } else {
101
+ wasPortfolioSuccess = true;
102
+ await batchManager.addToPortfolioBatch(userId, portfolioBlockId, today, JSON.parse(body), userType, instrumentId);
103
+ }
104
+ }
105
+
106
+ headerManager.updatePerformance(portfolioHeader.id, wasPortfolioSuccess);
107
+
108
+ // Re-select header for next instrument
109
+ if (instrumentsToProcess.length > 1 && instrumentId !== instrumentsToProcess[instrumentsToProcess.length - 1]) {
110
+ portfolioHeader = await headerManager.selectHeader();
111
+ }
112
+ }
113
+
114
+ // Handle private user
117
115
  if (isPrivate) {
118
116
  logger.log('WARN', `User ${userId} is private. Removing from updates.`);
119
- await batchManager.deleteFromTimestampBatch(userId, userType, instrumentId);
120
-
121
- const blockCountsRef = db.doc(userType === 'speculator' ? config.FIRESTORE_DOC_SPECULATOR_BLOCK_COUNTS : config.FIRESTORE_DOC_BLOCK_COUNTS);
122
- const incrementField = userType === 'speculator' ? `counts.${instrumentId}_${Math.floor(userId/1e6)*1e6}` : `counts.${Math.floor(userId/1e6)*1e6}`;
123
- await blockCountsRef.set({ [incrementField]: FieldValue.increment(-1) }, { merge: true });
117
+ // Delete for ALL instruments
118
+ for (const instrumentId of instrumentsToProcess) {
119
+ await batchManager.deleteFromTimestampBatch(userId, userType, instrumentId);
120
+ }
121
+ const blockCountsRef = db.doc(config.FIRESTORE_DOC_SPECULATOR_BLOCK_COUNTS);
122
+ for (const instrumentId of instrumentsToProcess) {
123
+ const incrementField = `counts.${instrumentId}_${Math.floor(userId/1e6)*1e6}`;
124
+ await blockCountsRef.set({ [incrementField]: FieldValue.increment(-1) }, { merge: true });
125
+ }
124
126
  return;
125
127
  }
126
-
127
- if (wasPortfolioSuccess || wasHistorySuccess) {
128
+
129
+ // Update timestamps
130
+ for (const instrumentId of instrumentsToProcess) {
128
131
  await batchManager.updateUserTimestamp(userId, userType, instrumentId);
129
- if (userType === 'speculator') await batchManager.addSpeculatorTimestampFix(userId, String(Math.floor(userId/1e6)*1e6));
130
132
  }
131
- } finally {
132
- if (portfolioHeader) headerManager.updatePerformance(portfolioHeader.id, wasPortfolioSuccess);
133
+ if (userType === 'speculator') {
134
+ await batchManager.addSpeculatorTimestampFix(userId, String(Math.floor(userId/1e6)*1e6));
135
+ }
133
136
 
134
- // --- MODIFICATION: Only update history header if we used it ---
135
- if (fetchHistory && historyHeader) headerManager.updatePerformance(historyHeader.id, wasHistorySuccess);
136
- // --- END MODIFICATION ---
137
+ } finally {
138
+ if (historyHeader && fetchHistory) {
139
+ headerManager.updatePerformance(historyHeader.id, wasHistorySuccess);
140
+ }
137
141
  }
138
142
  }
139
143
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bulltrackers-module",
3
- "version": "1.0.134",
3
+ "version": "1.0.136",
4
4
  "description": "Helper Functions for Bulltrackers.",
5
5
  "main": "index.js",
6
6
  "files": [
@@ -27,6 +27,10 @@
27
27
  "calculations",
28
28
  "finance"
29
29
  ],
30
+ "scripts": {
31
+ "postpublish": "node ./auto-deploy.js",
32
+ "release": "node ./release.js"
33
+ },
30
34
  "dependencies": {
31
35
  "@google-cloud/firestore": "^7.11.3",
32
36
  "sharedsetup": "latest",
@@ -34,7 +38,11 @@
34
38
  "@google-cloud/pubsub": "latest",
35
39
  "express": "^4.19.2",
36
40
  "cors": "^2.8.5",
37
- "p-limit": "^3.1.0"
41
+ "p-limit": "^3.1.0",
42
+ "dotenv": "latest"
43
+ },
44
+ "devDependencies": {
45
+ "bulltracker-deployer": "file:../bulltracker-deployer"
38
46
  },
39
47
  "engines": {
40
48
  "node": ">=20"