bulltrackers-module 1.0.540 → 1.0.542
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.
|
@@ -274,7 +274,8 @@ class StandardExecutor {
|
|
|
274
274
|
const targetUserType = metadata.userType;
|
|
275
275
|
// [NEW] Always load Global Helpers
|
|
276
276
|
const mappings = await loader.loadMappings();
|
|
277
|
-
|
|
277
|
+
// [FIX] Correct method name: loadPIMasterList (not loadPopularInvestorMasterList)
|
|
278
|
+
const piMasterList = await loader.loadPIMasterList();
|
|
278
279
|
const SCHEMAS = mathLayer.SCHEMAS;
|
|
279
280
|
|
|
280
281
|
// 1. Load Root Data
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
const { getPIMasterList } = require('../core/user_status_helpers');
|
|
7
|
+
const { tryDecompress } = require('../core/compression_helpers');
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* GET /user/search/pis
|
|
@@ -29,16 +30,111 @@ async function searchPopularInvestors(req, res, dependencies, config) {
|
|
|
29
30
|
// Search by username (case-insensitive, partial match)
|
|
30
31
|
const searchQuery = query.toLowerCase().trim();
|
|
31
32
|
const matches = [];
|
|
33
|
+
const rankingsCollection = config.popularInvestorRankingsCollection || process.env.FIRESTORE_COLLECTION_PI_RANKINGS || 'popular_investor_rankings';
|
|
34
|
+
|
|
35
|
+
// Cache rankings data by date to avoid fetching the same date multiple times
|
|
36
|
+
const rankingsCache = new Map();
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Helper to get rankings data for a specific date
|
|
40
|
+
*/
|
|
41
|
+
const getRankingsForDate = async (dateStr) => {
|
|
42
|
+
if (rankingsCache.has(dateStr)) {
|
|
43
|
+
return rankingsCache.get(dateStr);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
const rankingsRef = db.collection(rankingsCollection).doc(dateStr);
|
|
48
|
+
const rankingsDoc = await rankingsRef.get();
|
|
49
|
+
|
|
50
|
+
if (!rankingsDoc.exists) {
|
|
51
|
+
rankingsCache.set(dateStr, null);
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const rawRankingsData = rankingsDoc.data();
|
|
56
|
+
const rankingsData = tryDecompress(rawRankingsData);
|
|
57
|
+
const rankingsItems = rankingsData.Items || [];
|
|
58
|
+
|
|
59
|
+
// Create a map for quick lookup by CID
|
|
60
|
+
const rankingsMap = new Map();
|
|
61
|
+
for (const item of rankingsItems) {
|
|
62
|
+
if (item.CustomerId) {
|
|
63
|
+
rankingsMap.set(String(item.CustomerId), item);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
rankingsCache.set(dateStr, rankingsMap);
|
|
68
|
+
return rankingsMap;
|
|
69
|
+
} catch (error) {
|
|
70
|
+
if (logger) {
|
|
71
|
+
logger.log('WARN', `[searchPopularInvestors] Failed to fetch rankings for date ${dateStr}: ${error.message}`);
|
|
72
|
+
}
|
|
73
|
+
rankingsCache.set(dateStr, null);
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Convert timestamp to date string (YYYY-MM-DD)
|
|
80
|
+
*/
|
|
81
|
+
const timestampToDateStr = (timestamp) => {
|
|
82
|
+
if (!timestamp) return null;
|
|
83
|
+
|
|
84
|
+
// Handle Firestore Timestamp
|
|
85
|
+
if (timestamp.toDate && typeof timestamp.toDate === 'function') {
|
|
86
|
+
return timestamp.toDate().toISOString().split('T')[0];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Handle Date object
|
|
90
|
+
if (timestamp instanceof Date) {
|
|
91
|
+
return timestamp.toISOString().split('T')[0];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Handle number (milliseconds since epoch)
|
|
95
|
+
if (typeof timestamp === 'number') {
|
|
96
|
+
return new Date(timestamp).toISOString().split('T')[0];
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return null;
|
|
100
|
+
};
|
|
32
101
|
|
|
33
102
|
for (const [cid, investor] of Object.entries(investors)) {
|
|
34
103
|
if (investor.username) {
|
|
35
104
|
const username = investor.username.toLowerCase();
|
|
36
105
|
if (username.includes(searchQuery)) {
|
|
106
|
+
// Get ranking metrics from rankings collection using lastSeenAt
|
|
107
|
+
let aum = null;
|
|
108
|
+
let riskScore = null;
|
|
109
|
+
let gain = null;
|
|
110
|
+
let copiers = null;
|
|
111
|
+
|
|
112
|
+
if (investor.lastSeenAt) {
|
|
113
|
+
const rankingsDate = timestampToDateStr(investor.lastSeenAt);
|
|
114
|
+
|
|
115
|
+
if (rankingsDate) {
|
|
116
|
+
const rankingsMap = await getRankingsForDate(rankingsDate);
|
|
117
|
+
|
|
118
|
+
if (rankingsMap) {
|
|
119
|
+
const rankingEntry = rankingsMap.get(cid);
|
|
120
|
+
|
|
121
|
+
if (rankingEntry) {
|
|
122
|
+
aum = rankingEntry.AUMValue || null;
|
|
123
|
+
riskScore = rankingEntry.RiskScore || null;
|
|
124
|
+
gain = rankingEntry.Gain || null;
|
|
125
|
+
copiers = rankingEntry.Copiers || null;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
37
131
|
matches.push({
|
|
38
132
|
cid: Number(cid),
|
|
39
|
-
username: investor.username
|
|
40
|
-
|
|
41
|
-
|
|
133
|
+
username: investor.username,
|
|
134
|
+
aum: aum,
|
|
135
|
+
riskScore: riskScore,
|
|
136
|
+
gain: gain,
|
|
137
|
+
copiers: copiers
|
|
42
138
|
});
|
|
43
139
|
|
|
44
140
|
if (matches.length >= parseInt(limit)) {
|