bulltrackers-module 1.0.404 → 1.0.406

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,6 +1,7 @@
1
1
  /**
2
2
  * @fileoverview Factory for creating the Computation Context.
3
3
  * UPDATED: Injects verification and rankings data into context globally and locally.
4
+ * UPDATED: Added support for historical ranking data in both Standard and Meta contexts.
4
5
  */
5
6
  const mathLayer = require('../layers/index');
6
7
  const { LEGACY_MAPPING } = require('../topology/HashManager');
@@ -21,8 +22,10 @@ class ContextFactory {
21
22
  todayPortfolio, yesterdayPortfolio, todayHistory, yesterdayHistory,
22
23
  userId, userType, dateStr, metadata, mappings, insights, socialData,
23
24
  computedDependencies, previousComputedDependencies, config, deps,
24
- verification, rankings, // User-specific
25
- allRankings, allVerifications // [NEW] Global context for comparative analysis
25
+ verification,
26
+ rankings, yesterdayRankings, // User-specific rank entries
27
+ allRankings, allRankingsYesterday, // Global rank lists
28
+ allVerifications
26
29
  } = options;
27
30
 
28
31
  return {
@@ -32,7 +35,8 @@ class ContextFactory {
32
35
  portfolio: { today: todayPortfolio, yesterday: yesterdayPortfolio },
33
36
  history: { today: todayHistory, yesterday: yesterdayHistory },
34
37
  verification: verification || null,
35
- rankEntry: rankings || null // Renamed for clarity (single entry)
38
+ rankEntry: rankings || null,
39
+ rankEntryYesterday: yesterdayRankings || null
36
40
  },
37
41
  date: { today: dateStr },
38
42
  insights: { today: insights?.today, yesterday: insights?.yesterday },
@@ -42,9 +46,9 @@ class ContextFactory {
42
46
  computed: computedDependencies || {},
43
47
  previousComputed: previousComputedDependencies || {},
44
48
  meta: metadata, config, deps,
45
- // [NEW] Global Data Injection for Comparative Logic (Similarity, Ranking position, etc.)
46
49
  globalData: {
47
50
  rankings: allRankings || [],
51
+ rankingsYesterday: allRankingsYesterday || [],
48
52
  verifications: allVerifications || {}
49
53
  }
50
54
  };
@@ -54,7 +58,8 @@ class ContextFactory {
54
58
  const {
55
59
  dateStr, metadata, mappings, insights, socialData, prices,
56
60
  computedDependencies, previousComputedDependencies, config, deps,
57
- allRankings, allVerifications
61
+ allRankings, allRankingsYesterday, // [UPDATED] Accepted here
62
+ allVerifications
58
63
  } = options;
59
64
 
60
65
  return {
@@ -69,6 +74,7 @@ class ContextFactory {
69
74
  meta: metadata, config, deps,
70
75
  globalData: {
71
76
  rankings: allRankings || [],
77
+ rankingsYesterday: allRankingsYesterday || [], // [UPDATED] Injected here
72
78
  verifications: allVerifications || {}
73
79
  }
74
80
  };
@@ -3,6 +3,7 @@
3
3
  * UPDATED: Uses CachedDataLoader for all data access.
4
4
  * UPDATED: Tracks processed shard/item counts.
5
5
  * UPDATED: Sends 'isInitialWrite: true' for robust cleanup.
6
+ * UPDATED: Support for historical rankings in Meta Context.
6
7
  */
7
8
  const { normalizeName } = require('../utils/utils');
8
9
  const { CachedDataLoader } = require('../data/CachedDataLoader');
@@ -13,9 +14,20 @@ class MetaExecutor {
13
14
  static async run(date, calcs, passName, config, deps, rootData, fetchedDeps, previousFetchedDeps) {
14
15
  const dStr = date.toISOString().slice(0, 10);
15
16
  const { logger, db } = deps;
16
- const { CachedDataLoader } = require('../data/CachedDataLoader');
17
17
  const loader = new CachedDataLoader(config, deps);
18
18
 
19
+ // [FIX] Check if any meta calculation needs history
20
+ const needsHistory = calcs.some(c => c.isHistorical);
21
+ let rankingsYesterday = null;
22
+
23
+ if (needsHistory) {
24
+ const prevDate = new Date(date);
25
+ prevDate.setUTCDate(prevDate.getUTCDate() - 1);
26
+ const prevStr = prevDate.toISOString().slice(0, 10);
27
+ logger.log('INFO', `[MetaExecutor] Loading historical rankings for ${prevStr}`);
28
+ rankingsYesterday = await loader.loadRankings(prevStr);
29
+ }
30
+
19
31
  // 1. Load Global Dependencies
20
32
  const [mappings, rankings, verifications] = await Promise.all([
21
33
  loader.loadMappings(),
@@ -38,11 +50,14 @@ class MetaExecutor {
38
50
  previousComputedDependencies: previousFetchedDeps,
39
51
  config, deps,
40
52
  allRankings: rankings,
53
+ allRankingsYesterday: rankingsYesterday, // [FIX] Injected
41
54
  allVerifications: verifications
42
55
  });
43
56
 
44
57
  try {
45
58
  const result = await inst.process(context);
59
+ // Meta results are usually wrapped in a global key or just the result object
60
+ // The structure below implies we store it under the date key
46
61
  inst.results = { [dStr]: { global: result } };
47
62
  state[c.name] = inst;
48
63
  } catch (e) {
@@ -61,6 +76,18 @@ class MetaExecutor {
61
76
  const insights = metadata.rootDataDependencies?.includes('insights') ? { today: await loader.loadInsights(dateStr) } : null;
62
77
  const social = metadata.rootDataDependencies?.includes('social') ? { today: await loader.loadSocial(dateStr) } : null;
63
78
 
79
+ // [FIX] Historical support for Batch/OncePerDay execution
80
+ let rankingsYesterday = null;
81
+ if (metadata.isHistorical) {
82
+ const prevDate = new Date(dateStr);
83
+ prevDate.setUTCDate(prevDate.getUTCDate() - 1);
84
+ const prevStr = prevDate.toISOString().slice(0, 10);
85
+ rankingsYesterday = await loader.loadRankings(prevStr);
86
+ }
87
+
88
+ // Load current rankings (often needed for ContextFactory.buildMetaContext)
89
+ const rankings = await loader.loadRankings(dateStr);
90
+
64
91
  if (metadata.rootDataDependencies?.includes('price')) {
65
92
  logger.log('INFO', `[Executor] Running Batched/Sharded Execution for ${metadata.name}`);
66
93
  const shardRefs = await loader.getPriceShardReferences();
@@ -72,7 +99,9 @@ class MetaExecutor {
72
99
  const partialContext = ContextFactory.buildMetaContext({
73
100
  dateStr, metadata, mappings, insights, socialData: social,
74
101
  prices: { history: shardData }, computedDependencies: computedDeps,
75
- previousComputedDependencies: prevDeps, config, deps
102
+ previousComputedDependencies: prevDeps, config, deps,
103
+ allRankings: rankings,
104
+ allRankingsYesterday: rankingsYesterday
76
105
  });
77
106
 
78
107
  await calcInstance.process(partialContext);
@@ -90,7 +119,9 @@ class MetaExecutor {
90
119
  const context = ContextFactory.buildMetaContext({
91
120
  dateStr, metadata, mappings, insights, socialData: social,
92
121
  prices: {}, computedDependencies: computedDeps,
93
- previousComputedDependencies: prevDeps, config, deps
122
+ previousComputedDependencies: prevDeps, config, deps,
123
+ allRankings: rankings,
124
+ allRankingsYesterday: rankingsYesterday
94
125
  });
95
126
  const res = await calcInstance.process(context);
96
127
 
@@ -102,4 +133,4 @@ class MetaExecutor {
102
133
  }
103
134
  }
104
135
 
105
- module.exports = { MetaExecutor };
136
+ module.exports = { MetaExecutor };
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * {
3
3
  * type: uploaded file
4
- * fileName: bulltrackers-module/functions/computation-system/executors/StandardExecutor.js
4
+ * fileName: computation-system/executors/StandardExecutor.js
5
5
  * }
6
6
  */
7
7
  const { normalizeName, getEarliestDataDates } = require('../utils/utils');
@@ -141,6 +141,7 @@ class StandardExecutor {
141
141
  }
142
142
 
143
143
  static async flushBuffer(state, dateStr, passName, config, deps, shardIndexMap, executionStats, mode, skipStatusWrite, isInitialWrite = false) {
144
+ // ... (No changes to flushBuffer)
144
145
  const transformedState = {};
145
146
  for (const [name, inst] of Object.entries(state)) {
146
147
  const rawResult = inst.results || {};
@@ -178,6 +179,7 @@ class StandardExecutor {
178
179
  }
179
180
 
180
181
  static mergeReports(successAcc, failureAcc, newResult) {
182
+ // ... (No changes to mergeReports)
181
183
  if (!newResult) return;
182
184
  for (const [name, update] of Object.entries(newResult.successUpdates)) {
183
185
  if (!successAcc[name]) {
@@ -218,8 +220,15 @@ class StandardExecutor {
218
220
  const verifications = metadata.rootDataDependencies?.includes('verification') ? await loader.loadVerifications() : null;
219
221
  const rankings = metadata.rootDataDependencies?.includes('rankings') ? await loader.loadRankings(dateStr) : null;
220
222
 
221
- // [UPDATED] Load the Partitioned Social Data Container
222
- // socialContainer = { generic: {}, pi: { userId: {} }, signedIn: { userId: {} } }
223
+ // [FIX] Load Yesterday's Rankings if isHistorical is true
224
+ let yesterdayRankings = null;
225
+ if (metadata.rootDataDependencies?.includes('rankings') && metadata.isHistorical) {
226
+ const prevDate = new Date(dateStr); prevDate.setUTCDate(prevDate.getUTCDate() - 1);
227
+ const prevStr = prevDate.toISOString().slice(0, 10);
228
+ // Assuming CachedDataLoader handles caching for efficiency
229
+ yesterdayRankings = await loader.loadRankings(prevStr);
230
+ }
231
+
223
232
  const socialContainer = metadata.rootDataDependencies?.includes('social') ? await loader.loadSocial(dateStr) : null;
224
233
 
225
234
  let chunkSuccess = 0;
@@ -229,7 +238,6 @@ class StandardExecutor {
229
238
  const yesterdayPortfolio = yesterdayPortfolioData ? yesterdayPortfolioData[userId] : null;
230
239
  const todayHistory = historyData ? historyData[userId] : null;
231
240
 
232
- // 2. Identify User Type
233
241
  let actualUserType = todayPortfolio._userType;
234
242
  if (!actualUserType) {
235
243
  if (todayPortfolio.PublicPositions) {
@@ -240,7 +248,6 @@ class StandardExecutor {
240
248
  }
241
249
  }
242
250
 
243
- // 3. Strict User Type Filtering
244
251
  if (targetUserType && targetUserType !== 'all') {
245
252
  if (targetUserType !== actualUserType) {
246
253
  if (stats) stats.skippedUsers++;
@@ -248,22 +255,19 @@ class StandardExecutor {
248
255
  }
249
256
  }
250
257
 
251
- // 4. Resolve Contextual Data
252
258
  const userVerification = verifications ? verifications[userId] : null;
259
+
260
+ // [FIX] Extract current AND yesterday's rank entry for this user
253
261
  const userRanking = rankings ? (rankings.find(r => String(r.CustomerId) === String(userId)) || null) : null;
262
+ const userRankingYesterday = yesterdayRankings ? (yesterdayRankings.find(r => String(r.CustomerId) === String(userId)) || null) : null;
254
263
 
255
- // [CRITICAL FIX] Select Specific Social Data to Prevent Skew
256
264
  let effectiveSocialData = null;
257
265
  if (socialContainer) {
258
266
  if (actualUserType === 'POPULAR_INVESTOR') {
259
- // PI Route: Only gets PI-specific posts for this user
260
- // (PIs do not need generic noise usually, or you can merge it if required)
261
267
  effectiveSocialData = socialContainer.pi[userId] || {};
262
268
  } else if (actualUserType === 'SIGNED_IN_USER') {
263
- // Signed-In Route: Only gets Signed-In specific posts for this user
264
269
  effectiveSocialData = socialContainer.signedIn[userId] || {};
265
270
  } else {
266
- // Normal/Speculator/All Route: Gets Generic Market Posts
267
271
  effectiveSocialData = socialContainer.generic || {};
268
272
  }
269
273
  }
@@ -271,15 +275,19 @@ class StandardExecutor {
271
275
  const context = ContextFactory.buildPerUserContext({
272
276
  todayPortfolio, yesterdayPortfolio, todayHistory, userId,
273
277
  userType: actualUserType, dateStr, metadata, mappings, insights,
274
-
275
- // Inject the filtered subset
276
278
  socialData: effectiveSocialData ? { today: effectiveSocialData } : null,
277
-
278
279
  computedDependencies: computedDeps, previousComputedDependencies: prevDeps,
279
280
  config, deps,
280
281
  verification: userVerification,
282
+
283
+ // [FIX] Pass both ranking entries
281
284
  rankings: userRanking,
285
+ yesterdayRankings: userRankingYesterday,
286
+
287
+ // [FIX] Pass both global lists
282
288
  allRankings: rankings,
289
+ allRankingsYesterday: yesterdayRankings,
290
+
283
291
  allVerifications: verifications
284
292
  });
285
293
 
@@ -202,90 +202,66 @@ async function handlePopularInvestorUpdate(taskData, config, dependencies) {
202
202
  logger.log('INFO', `[PI Update] Deep portfolio fetching is disabled. Skipping deep positions for ${username}`);
203
203
  }
204
204
 
205
- // --- 3. Fetch Trade History (Last 1 Year) with Pagination ---
205
+ // --- 3. Fetch Trade History (Last 1 Year) - Single Page with Large ItemsPerPage ---
206
206
  const oneYearAgo = new Date();
207
207
  oneYearAgo.setFullYear(oneYearAgo.getFullYear() - 1);
208
208
 
209
209
  const historyBaseUrl = ETORO_API_HISTORY_URL || 'https://www.etoro.com/sapi/trade-data-real/history/public/credit/flat';
210
+ const historyUrl = `${historyBaseUrl}?StartTime=${oneYearAgo.toISOString()}&PageNumber=1&ItemsPerPage=30000&PublicHistoryPortfolioFilter=&CID=${cid}&client_request_id=${uuid}`;
210
211
 
211
- let allHistoryTrades = [];
212
- let page = 1;
213
- let hasMore = true;
214
- const MAX_PAGES = 10;
215
-
216
- while (hasMore && page <= MAX_PAGES) {
217
- const historyUrl = `${historyBaseUrl}?StartTime=${oneYearAgo.toISOString()}&PageNumber=${page}&ItemsPerPage=1000&PublicHistoryPortfolioFilter=&CID=${cid}&client_request_id=${uuid}`;
218
-
219
- logger.log('INFO', `[PI DEBUG] Fetching History Page ${page} for ${username} URL: ${historyUrl}`);
220
-
221
- try {
222
- // Reuse headers
223
- let historyRes = null;
224
- let historySuccess = false;
225
-
226
- // [CIRCUIT BREAKER] Try proxy only if circuit is not open
227
- if (shouldTryProxy()) {
228
- try {
229
- historyRes = await proxyManager.fetch(historyUrl, requestOptions);
230
- if (historyRes.ok) {
231
- historySuccess = true;
232
- recordProxyOutcome(true);
233
- } else {
234
- throw new Error(`Proxy returned non-ok status: ${historyRes.status}`);
235
- }
236
- } catch (proxyError) {
237
- recordProxyOutcome(false);
238
- logger.log('WARN', `[PI Update] Proxy failed for history page ${page} (${getFailureCount()}/${getMaxFailures()}). Trying direct...`);
239
- }
212
+ logger.log('INFO', `[PI DEBUG] Fetching History for ${username} URL: ${historyUrl}`);
213
+
214
+ let historyRes = null;
215
+ let historySuccess = false;
216
+
217
+ // [CIRCUIT BREAKER] Try proxy only if circuit is not open
218
+ if (shouldTryProxy()) {
219
+ try {
220
+ historyRes = await proxyManager.fetch(historyUrl, requestOptions);
221
+ if (historyRes.ok) {
222
+ historySuccess = true;
223
+ recordProxyOutcome(true);
240
224
  } else {
241
- // Circuit breaker open, skip proxy
242
- }
243
-
244
- // Fallback to direct fetch if proxy failed or circuit is open
245
- if (!historySuccess) {
246
- try {
247
- const directFetch = typeof fetch !== 'undefined' ? fetch : require('node-fetch');
248
- historyRes = await directFetch(historyUrl, requestOptions);
249
- if (historyRes.ok) {
250
- historySuccess = true;
251
- } else {
252
- throw new Error(`Direct fetch returned non-ok status: ${historyRes.status}`);
253
- }
254
- } catch (fetchErr) {
255
- logger.log('WARN', `[PI Update] Direct fetch also failed for history page ${page}`, fetchErr);
256
- hasMore = false;
257
- continue;
258
- }
225
+ throw new Error(`Proxy returned non-ok status: ${historyRes.status}`);
259
226
  }
260
-
261
- if (historySuccess && historyRes.ok) {
262
- const data = await historyRes.json();
263
- const trades = data.PublicHistoryPositions || [];
264
-
265
- if (trades.length > 0) {
266
- allHistoryTrades = allHistoryTrades.concat(trades);
267
- page++;
268
- hasMore = (trades.length === 1000);
269
- } else {
270
- hasMore = false;
271
- }
227
+ } catch (proxyError) {
228
+ recordProxyOutcome(false);
229
+ logger.log('WARN', `[PI Update] Proxy failed for history (${getFailureCount()}/${getMaxFailures()}). Trying direct...`);
230
+ }
231
+ } else {
232
+ // Circuit breaker open, skip proxy
233
+ }
234
+
235
+ // Fallback to direct fetch if proxy failed or circuit is open
236
+ if (!historySuccess) {
237
+ try {
238
+ const directFetch = typeof fetch !== 'undefined' ? fetch : require('node-fetch');
239
+ historyRes = await directFetch(historyUrl, requestOptions);
240
+ if (historyRes.ok) {
241
+ historySuccess = true;
272
242
  } else {
273
- logger.log('WARN', `[PI Update] History fetch failed on page ${page} for ${username}. Status: ${historyRes?.status || 'unknown'}`);
274
- hasMore = false;
243
+ throw new Error(`Direct fetch returned non-ok status: ${historyRes.status}`);
275
244
  }
276
- } catch (e) {
277
- logger.log('WARN', `[PI Update] History fetch error page ${page}: ${e.message}`);
278
- hasMore = false;
279
- }
245
+ } catch (fetchErr) {
246
+ logger.log('WARN', `[PI Update] Direct fetch also failed for history`, fetchErr);
247
+ historyRes = { ok: false, status: 500 };
248
+ }
280
249
  }
281
250
 
282
- if (allHistoryTrades.length > 0) {
283
- const historyPayload = {
284
- PublicHistoryPositions: allHistoryTrades,
285
- fetchedAt: new Date()
286
- };
287
- await batchManager.addToTradingHistoryBatch(String(cid), blockId, today, historyPayload, 'popular_investor');
288
- logger.log('INFO', `[PI Update] Fetched total ${allHistoryTrades.length} trades for ${username}`);
251
+ if (historySuccess && historyRes.ok) {
252
+ const historyData = await historyRes.json();
253
+ const trades = historyData.PublicHistoryPositions || [];
254
+
255
+ if (trades.length > 0) {
256
+ const historyPayload = {
257
+ PublicHistoryPositions: trades,
258
+ fetchedAt: new Date()
259
+ };
260
+ await batchManager.addToTradingHistoryBatch(String(cid), blockId, today, historyPayload, 'popular_investor');
261
+ logger.log('INFO', `[PI Update] Fetched ${trades.length} trades for ${username}`);
262
+ }
263
+ } else {
264
+ logger.log('WARN', `[PI Update] History fetch failed for ${username}. Status: ${historyRes?.status || 'unknown'}`);
289
265
  }
290
266
 
291
267
  logger.log('SUCCESS', `[PI Update] Completed full update for ${username}`);
@@ -389,11 +365,11 @@ async function handleOnDemandUserUpdate(taskData, config, dependencies) {
389
365
 
390
366
  await batchManager.addToPortfolioBatch(String(cid), blockId, today, portfolioData, 'signed_in_user');
391
367
 
392
- // History Fetch
368
+ // History Fetch - Single Page with Large ItemsPerPage
393
369
  const oneYearAgo = new Date();
394
370
  oneYearAgo.setFullYear(oneYearAgo.getFullYear() - 1);
395
371
  const historyBaseUrl = ETORO_API_HISTORY_URL || 'https://www.etoro.com/sapi/trade-data-real/history/public/credit/flat';
396
- const historyUrl = `${historyBaseUrl}?StartTime=${oneYearAgo.toISOString()}&PageNumber=1&ItemsPerPage=1000&PublicHistoryPortfolioFilter=&CID=${cid}&client_request_id=${uuid}`;
372
+ const historyUrl = `${historyBaseUrl}?StartTime=${oneYearAgo.toISOString()}&PageNumber=1&ItemsPerPage=30000&PublicHistoryPortfolioFilter=&CID=${cid}&client_request_id=${uuid}`;
397
373
 
398
374
  logger.log('INFO', `[On-Demand DEBUG] Fetching History URL: ${historyUrl}`);
399
375
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bulltrackers-module",
3
- "version": "1.0.404",
3
+ "version": "1.0.406",
4
4
  "description": "Helper Functions for Bulltrackers.",
5
5
  "main": "index.js",
6
6
  "files": [