bulltrackers-module 1.0.496 → 1.0.498
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.
- package/functions/computation-system/reporter_epoch.js +1 -1
- package/functions/computation-system/system_epoch.js +1 -1
- package/functions/computation-system/utils/data_loader.js +294 -92
- package/functions/etoro-price-fetcher/helpers/handler_helpers.js +21 -3
- package/functions/fetch-insights/helpers/handler_helpers.js +19 -4
- package/functions/fetch-popular-investors/helpers/fetch_helpers.js +20 -5
- package/functions/root-data-indexer/index.js +62 -28
- package/functions/task-engine/handler_creator.js +35 -20
- package/functions/task-engine/helpers/data_storage_helpers.js +315 -0
- package/functions/task-engine/helpers/popular_investor_helpers.js +110 -43
- package/functions/task-engine/helpers/root_data_indexer_helpers.js +171 -0
- package/functions/task-engine/helpers/social_helpers.js +72 -44
- package/functions/task-engine/utils/task_engine_utils.js +145 -32
- package/package.json +1 -1
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// Change this string to force a global re-computation
|
|
2
|
-
module.exports = "v2.0-epoch-
|
|
2
|
+
module.exports = "v2.0-epoch-3";
|
|
@@ -24,52 +24,107 @@ function tryDecompress(data) {
|
|
|
24
24
|
|
|
25
25
|
/** Stage 1: Get portfolio part document references for a given date */
|
|
26
26
|
async function getPortfolioPartRefs(config, deps, dateString) {
|
|
27
|
-
const { db, logger, calculationUtils } = deps;
|
|
27
|
+
const { db, logger, calculationUtils, collectionRegistry } = deps;
|
|
28
28
|
const { withRetry } = calculationUtils;
|
|
29
|
+
const { getCollectionPath } = collectionRegistry || {};
|
|
29
30
|
|
|
30
31
|
logger.log('INFO', `Getting portfolio part references for date: ${dateString}`);
|
|
31
32
|
|
|
32
|
-
|
|
33
|
+
const allPartRefs = [];
|
|
34
|
+
|
|
35
|
+
// NEW STRUCTURE: Read from date-based collections (per-user documents)
|
|
36
|
+
// Structure: Collection/{date}/{cid}/{cid} where {date} is document, {cid} is subcollection, {cid} is document
|
|
37
|
+
try {
|
|
38
|
+
// Signed-In User Portfolios: SignedInUserPortfolioData/{date}/{cid}/{cid}
|
|
39
|
+
const signedInPortCollectionName = 'SignedInUserPortfolioData';
|
|
40
|
+
const signedInPortDateDoc = db.collection(signedInPortCollectionName).doc(dateString);
|
|
41
|
+
const signedInPortSubcollections = await withRetry(
|
|
42
|
+
() => signedInPortDateDoc.listCollections(),
|
|
43
|
+
`listSignedInPortfolios(${dateString})`
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
signedInPortSubcollections.forEach(subcol => {
|
|
47
|
+
// Each subcollection is a CID, the document ID is also the CID
|
|
48
|
+
const cid = subcol.id;
|
|
49
|
+
const cidDocRef = subcol.doc(cid);
|
|
50
|
+
allPartRefs.push({
|
|
51
|
+
ref: cidDocRef,
|
|
52
|
+
type: 'SIGNED_IN_USER',
|
|
53
|
+
cid: cid,
|
|
54
|
+
collectionType: 'NEW_STRUCTURE'
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// Popular Investor Portfolios: PopularInvestorPortfolioData/{date}/{cid}/{cid}
|
|
59
|
+
const piPortCollectionName = 'PopularInvestorPortfolioData';
|
|
60
|
+
const piPortDateDoc = db.collection(piPortCollectionName).doc(dateString);
|
|
61
|
+
const piPortSubcollections = await withRetry(
|
|
62
|
+
() => piPortDateDoc.listCollections(),
|
|
63
|
+
`listPIPortfolios(${dateString})`
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
piPortSubcollections.forEach(subcol => {
|
|
67
|
+
const cid = subcol.id;
|
|
68
|
+
const cidDocRef = subcol.doc(cid);
|
|
69
|
+
allPartRefs.push({
|
|
70
|
+
ref: cidDocRef,
|
|
71
|
+
type: 'POPULAR_INVESTOR',
|
|
72
|
+
cid: cid,
|
|
73
|
+
collectionType: 'NEW_STRUCTURE'
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
logger.log('INFO', `Found ${allPartRefs.length} portfolio refs from new structure for ${dateString}`);
|
|
78
|
+
} catch (newStructError) {
|
|
79
|
+
logger.log('WARN', `Failed to load from new structure, falling back to legacy: ${newStructError.message}`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// LEGACY STRUCTURE: Read from block-based collections (for backward compatibility)
|
|
33
83
|
const collectionsToQuery = [
|
|
34
84
|
config.normalUserPortfolioCollection,
|
|
35
85
|
config.speculatorPortfolioCollection,
|
|
36
|
-
config.piPortfolioCollection, //
|
|
37
|
-
config.signedInUsersCollection //
|
|
86
|
+
config.piPortfolioCollection, // Legacy: PI Overall
|
|
87
|
+
config.signedInUsersCollection // Legacy: Signed-In Users
|
|
38
88
|
].filter(Boolean);
|
|
39
89
|
|
|
40
|
-
const allPartRefs = [];
|
|
41
|
-
|
|
42
90
|
for (const collectionName of collectionsToQuery) {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
91
|
+
try {
|
|
92
|
+
// Assume standard structure: Collection -> Block(e.g. 19M) -> snapshots -> date -> parts
|
|
93
|
+
const blockDocsQuery = db.collection(collectionName);
|
|
94
|
+
const blockDocRefs = await withRetry(() => blockDocsQuery.listDocuments(), `listDocuments(${collectionName})`);
|
|
95
|
+
|
|
96
|
+
if (!blockDocRefs.length) {
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
51
99
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
100
|
+
const partsPromises = blockDocRefs.map(blockDocRef => {
|
|
101
|
+
const partsCollectionRef = blockDocRef
|
|
102
|
+
.collection(config.snapshotsSubcollection || 'snapshots')
|
|
103
|
+
.doc(dateString)
|
|
104
|
+
.collection(config.partsSubcollection || 'parts');
|
|
105
|
+
return withRetry(() => partsCollectionRef.listDocuments(), `listParts(${partsCollectionRef.path})`);
|
|
106
|
+
});
|
|
59
107
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
partDocArrays.forEach(partDocs => {
|
|
63
|
-
// Tag them so loadDataByRefs knows how to handle them (especially PI Deep Fetch)
|
|
64
|
-
let type = 'PART';
|
|
65
|
-
if (collectionName === config.piPortfolioCollection) type = 'POPULAR_INVESTOR';
|
|
66
|
-
if (collectionName === config.signedInUsersCollection) type = 'SIGNED_IN_USER';
|
|
108
|
+
const partDocArrays = await Promise.all(partsPromises);
|
|
67
109
|
|
|
68
|
-
|
|
69
|
-
|
|
110
|
+
partDocArrays.forEach(partDocs => {
|
|
111
|
+
// Tag them so loadDataByRefs knows how to handle them
|
|
112
|
+
let type = 'PART';
|
|
113
|
+
if (collectionName === config.piPortfolioCollection) type = 'POPULAR_INVESTOR';
|
|
114
|
+
if (collectionName === config.signedInUsersCollection) type = 'SIGNED_IN_USER';
|
|
115
|
+
|
|
116
|
+
allPartRefs.push(...partDocs.map(ref => ({
|
|
117
|
+
ref,
|
|
118
|
+
type,
|
|
119
|
+
collectionType: 'LEGACY'
|
|
120
|
+
})));
|
|
121
|
+
});
|
|
122
|
+
} catch (legacyError) {
|
|
123
|
+
logger.log('WARN', `Failed to load legacy collection ${collectionName}: ${legacyError.message}`);
|
|
124
|
+
}
|
|
70
125
|
}
|
|
71
126
|
|
|
72
|
-
logger.log('INFO', `Found ${allPartRefs.length} portfolio
|
|
127
|
+
logger.log('INFO', `Found ${allPartRefs.length} total portfolio refs for ${dateString}`);
|
|
73
128
|
return allPartRefs;
|
|
74
129
|
}
|
|
75
130
|
|
|
@@ -101,48 +156,73 @@ async function loadDataByRefs(config, deps, refObjects) {
|
|
|
101
156
|
if (!doc.exists) continue;
|
|
102
157
|
|
|
103
158
|
const rawData = doc.data();
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
//
|
|
159
|
+
let chunkData;
|
|
160
|
+
|
|
161
|
+
// NEW STRUCTURE: Single user document per CID
|
|
162
|
+
if (meta.collectionType === 'NEW_STRUCTURE') {
|
|
163
|
+
const cid = meta.cid || doc.id;
|
|
164
|
+
// Data is stored directly in the document, not as a map
|
|
165
|
+
const userData = tryDecompress(rawData);
|
|
166
|
+
// Convert to map format: { cid: data }
|
|
167
|
+
chunkData = { [cid]: userData };
|
|
110
168
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
169
|
+
// Tag user type
|
|
170
|
+
if (meta.type === 'POPULAR_INVESTOR') {
|
|
171
|
+
chunkData[cid]._userType = 'POPULAR_INVESTOR';
|
|
172
|
+
// Check for deep positions in the same document
|
|
173
|
+
if (chunkData[cid].deepPositions) {
|
|
174
|
+
chunkData[cid].DeepPositions = chunkData[cid].deepPositions;
|
|
175
|
+
}
|
|
176
|
+
} else if (meta.type === 'SIGNED_IN_USER') {
|
|
177
|
+
chunkData[cid]._userType = 'SIGNED_IN_USER';
|
|
178
|
+
}
|
|
115
179
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
180
|
+
deepFetchPromises.push(Promise.resolve(chunkData));
|
|
181
|
+
} else {
|
|
182
|
+
// LEGACY STRUCTURE: Sharded parts with multiple users per document
|
|
183
|
+
chunkData = tryDecompress(rawData); // Map: { userId: data }
|
|
184
|
+
|
|
185
|
+
if (meta.type === 'POPULAR_INVESTOR' && config.piDeepPortfolioCollection) {
|
|
186
|
+
// Construct Deep Path
|
|
187
|
+
// Current: pi_portfolios_overall/19M/snapshots/{date}/parts/{part_X}
|
|
188
|
+
// Target: pi_portfolios_deep/19M/snapshots/{date}/parts/{part_X}
|
|
189
|
+
|
|
190
|
+
const pathSegments = doc.ref.path.split('/'); // [col, block, snap, date, parts, partId]
|
|
191
|
+
// Replace collection name with deep collection name
|
|
192
|
+
const deepCollection = config.piDeepPortfolioCollection;
|
|
193
|
+
const deepPath = `${deepCollection}/${pathSegments[1]}/${pathSegments[2]}/${pathSegments[3]}/${pathSegments[4]}/${pathSegments[5]}`;
|
|
194
|
+
|
|
195
|
+
// Fetch deeply
|
|
196
|
+
deepFetchPromises.push(
|
|
197
|
+
db.doc(deepPath).get().then(deepSnap => {
|
|
198
|
+
if (deepSnap.exists) {
|
|
199
|
+
const deepChunk = tryDecompress(deepSnap.data());
|
|
200
|
+
// Merge deep positions into overall data
|
|
201
|
+
for (const [uid, pData] of Object.entries(chunkData)) {
|
|
202
|
+
if (deepChunk[uid] && deepChunk[uid].positions) {
|
|
203
|
+
pData.DeepPositions = deepChunk[uid].positions;
|
|
204
|
+
}
|
|
125
205
|
}
|
|
126
206
|
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
)
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
207
|
+
// Tag internal type for ContextFactory
|
|
208
|
+
for (const pData of Object.values(chunkData)) {
|
|
209
|
+
pData._userType = 'POPULAR_INVESTOR';
|
|
210
|
+
}
|
|
211
|
+
return chunkData;
|
|
212
|
+
}).catch(err => {
|
|
213
|
+
// If deep fetch fails, return chunkData as is (graceful degradation)
|
|
214
|
+
return chunkData;
|
|
215
|
+
})
|
|
216
|
+
);
|
|
217
|
+
} else if (meta.type === 'SIGNED_IN_USER') {
|
|
218
|
+
for (const pData of Object.values(chunkData)) {
|
|
219
|
+
pData._userType = 'SIGNED_IN_USER';
|
|
220
|
+
}
|
|
221
|
+
deepFetchPromises.push(Promise.resolve(chunkData));
|
|
222
|
+
} else {
|
|
223
|
+
// Standard Part
|
|
224
|
+
deepFetchPromises.push(Promise.resolve(chunkData));
|
|
141
225
|
}
|
|
142
|
-
deepFetchPromises.push(Promise.resolve(chunkData));
|
|
143
|
-
} else {
|
|
144
|
-
// Standard Part
|
|
145
|
-
deepFetchPromises.push(Promise.resolve(chunkData));
|
|
146
226
|
}
|
|
147
227
|
}
|
|
148
228
|
|
|
@@ -187,8 +267,9 @@ async function loadDailyInsights(config, deps, dateString) {
|
|
|
187
267
|
|
|
188
268
|
/** Stage 5: Load and Partition Social Data */
|
|
189
269
|
async function loadDailySocialPostInsights(config, deps, dateString) {
|
|
190
|
-
const { db, logger, calculationUtils } = deps;
|
|
270
|
+
const { db, logger, calculationUtils, collectionRegistry } = deps;
|
|
191
271
|
const { withRetry } = calculationUtils;
|
|
272
|
+
const { getCollectionPath } = collectionRegistry || {};
|
|
192
273
|
|
|
193
274
|
logger.log('INFO', `Loading and partitioning social data for ${dateString}`);
|
|
194
275
|
|
|
@@ -199,6 +280,70 @@ async function loadDailySocialPostInsights(config, deps, dateString) {
|
|
|
199
280
|
signedIn: {} // Map<UserId, Map<PostId, Data>> - For Signed-In Users
|
|
200
281
|
};
|
|
201
282
|
|
|
283
|
+
// NEW STRUCTURE: Read from date-based collections
|
|
284
|
+
// Structure: Collection/{date}/{cid}/{cid} for user social, Collection/{date}/posts/{postId} for instrument
|
|
285
|
+
try {
|
|
286
|
+
// Signed-In User Social: SignedInUserSocialPostData/{date}/{cid}/{cid}
|
|
287
|
+
const signedInSocialCollectionName = 'SignedInUserSocialPostData';
|
|
288
|
+
const signedInSocialDateDoc = db.collection(signedInSocialCollectionName).doc(dateString);
|
|
289
|
+
const signedInSocialSubcollections = await withRetry(
|
|
290
|
+
() => signedInSocialDateDoc.listCollections(),
|
|
291
|
+
`listSignedInSocial(${dateString})`
|
|
292
|
+
);
|
|
293
|
+
|
|
294
|
+
for (const subcol of signedInSocialSubcollections) {
|
|
295
|
+
const cid = subcol.id;
|
|
296
|
+
const cidDoc = await subcol.doc(cid).get();
|
|
297
|
+
if (cidDoc.exists) {
|
|
298
|
+
const cidData = tryDecompress(cidDoc.data());
|
|
299
|
+
if (cidData.posts && typeof cidData.posts === 'object') {
|
|
300
|
+
if (!result.signedIn[cid]) result.signedIn[cid] = {};
|
|
301
|
+
// Posts are stored as a map in the document
|
|
302
|
+
Object.assign(result.signedIn[cid], cidData.posts);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Popular Investor Social: PopularInvestorSocialPostData/{date}/{cid}/{cid}
|
|
308
|
+
const piSocialCollectionName = 'PopularInvestorSocialPostData';
|
|
309
|
+
const piSocialDateDoc = db.collection(piSocialCollectionName).doc(dateString);
|
|
310
|
+
const piSocialSubcollections = await withRetry(
|
|
311
|
+
() => piSocialDateDoc.listCollections(),
|
|
312
|
+
`listPISocial(${dateString})`
|
|
313
|
+
);
|
|
314
|
+
|
|
315
|
+
for (const subcol of piSocialSubcollections) {
|
|
316
|
+
const cid = subcol.id;
|
|
317
|
+
const cidDoc = await subcol.doc(cid).get();
|
|
318
|
+
if (cidDoc.exists) {
|
|
319
|
+
const cidData = tryDecompress(cidDoc.data());
|
|
320
|
+
if (cidData.posts && typeof cidData.posts === 'object') {
|
|
321
|
+
if (!result.pi[cid]) result.pi[cid] = {};
|
|
322
|
+
Object.assign(result.pi[cid], cidData.posts);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Instrument Social: InstrumentFeedSocialPostData/{date}/posts/{postId}
|
|
328
|
+
const instrumentSocialCollectionName = 'InstrumentFeedSocialPostData';
|
|
329
|
+
const instrumentSocialDateDoc = db.collection(instrumentSocialCollectionName).doc(dateString);
|
|
330
|
+
const instrumentSocialPostsCol = instrumentSocialDateDoc.collection('posts');
|
|
331
|
+
const instrumentSocialSnapshot = await withRetry(
|
|
332
|
+
() => instrumentSocialPostsCol.limit(1000).get(),
|
|
333
|
+
`getInstrumentSocial(${dateString})`
|
|
334
|
+
);
|
|
335
|
+
|
|
336
|
+
instrumentSocialSnapshot.forEach(doc => {
|
|
337
|
+
const data = tryDecompress(doc.data());
|
|
338
|
+
result.generic[doc.id] = data;
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
logger.log('INFO', `Loaded Social Data (NEW): ${Object.keys(result.generic).length} Generic, ${Object.keys(result.pi).length} PIs, ${Object.keys(result.signedIn).length} SignedIn.`);
|
|
342
|
+
} catch (newStructError) {
|
|
343
|
+
logger.log('WARN', `Failed to load from new structure, falling back to legacy: ${newStructError.message}`);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// LEGACY STRUCTURE: CollectionGroup query (for backward compatibility)
|
|
202
347
|
const PI_COL_NAME = config.piSocialCollectionName || config.piSocialCollection || 'pi_social_posts';
|
|
203
348
|
const SIGNED_IN_COL_NAME = config.signedInUserSocialCollection || 'signed_in_users_social';
|
|
204
349
|
|
|
@@ -246,7 +391,7 @@ async function loadDailySocialPostInsights(config, deps, dateString) {
|
|
|
246
391
|
result.generic[doc.id] = data;
|
|
247
392
|
}
|
|
248
393
|
});
|
|
249
|
-
logger.log('INFO', `Loaded Social Data: ${Object.keys(result.generic).length} Generic, ${Object.keys(result.pi).length} PIs, ${Object.keys(result.signedIn).length} SignedIn.`);
|
|
394
|
+
logger.log('INFO', `Loaded Social Data (LEGACY): ${Object.keys(result.generic).length} Generic, ${Object.keys(result.pi).length} PIs, ${Object.keys(result.signedIn).length} SignedIn.`);
|
|
250
395
|
} else {
|
|
251
396
|
logger.log('WARN', `No social posts found for ${dateString} via CollectionGroup.`);
|
|
252
397
|
}
|
|
@@ -260,39 +405,96 @@ async function loadDailySocialPostInsights(config, deps, dateString) {
|
|
|
260
405
|
|
|
261
406
|
/** Stage 6: Get history part references for a given date */
|
|
262
407
|
async function getHistoryPartRefs(config, deps, dateString) {
|
|
263
|
-
const { db, logger, calculationUtils } = deps;
|
|
408
|
+
const { db, logger, calculationUtils, collectionRegistry } = deps;
|
|
264
409
|
const { withRetry } = calculationUtils;
|
|
410
|
+
const { getCollectionPath } = collectionRegistry || {};
|
|
411
|
+
|
|
265
412
|
logger.log('INFO', `Getting history part references for ${dateString}`);
|
|
266
413
|
|
|
267
|
-
|
|
414
|
+
const allPartRefs = [];
|
|
415
|
+
|
|
416
|
+
// NEW STRUCTURE: Read from date-based collections
|
|
417
|
+
// Structure: Collection/{date}/{cid}/{cid}
|
|
418
|
+
try {
|
|
419
|
+
// Signed-In User History: SignedInUserTradeHistoryData/{date}/{cid}/{cid}
|
|
420
|
+
const signedInHistCollectionName = 'SignedInUserTradeHistoryData';
|
|
421
|
+
const signedInHistDateDoc = db.collection(signedInHistCollectionName).doc(dateString);
|
|
422
|
+
const signedInHistSubcollections = await withRetry(
|
|
423
|
+
() => signedInHistDateDoc.listCollections(),
|
|
424
|
+
`listSignedInHistory(${dateString})`
|
|
425
|
+
);
|
|
426
|
+
|
|
427
|
+
signedInHistSubcollections.forEach(subcol => {
|
|
428
|
+
const cid = subcol.id;
|
|
429
|
+
const cidDocRef = subcol.doc(cid);
|
|
430
|
+
allPartRefs.push({
|
|
431
|
+
ref: cidDocRef,
|
|
432
|
+
type: 'SIGNED_IN_USER',
|
|
433
|
+
cid: cid,
|
|
434
|
+
collectionType: 'NEW_STRUCTURE'
|
|
435
|
+
});
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
// Popular Investor History: PopularInvestorTradeHistoryData/{date}/{cid}/{cid}
|
|
439
|
+
const piHistCollectionName = 'PopularInvestorTradeHistoryData';
|
|
440
|
+
const piHistDateDoc = db.collection(piHistCollectionName).doc(dateString);
|
|
441
|
+
const piHistSubcollections = await withRetry(
|
|
442
|
+
() => piHistDateDoc.listCollections(),
|
|
443
|
+
`listPIHistory(${dateString})`
|
|
444
|
+
);
|
|
445
|
+
|
|
446
|
+
piHistSubcollections.forEach(subcol => {
|
|
447
|
+
const cid = subcol.id;
|
|
448
|
+
const cidDocRef = subcol.doc(cid);
|
|
449
|
+
allPartRefs.push({
|
|
450
|
+
ref: cidDocRef,
|
|
451
|
+
type: 'POPULAR_INVESTOR',
|
|
452
|
+
cid: cid,
|
|
453
|
+
collectionType: 'NEW_STRUCTURE'
|
|
454
|
+
});
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
logger.log('INFO', `Found ${allPartRefs.length} history refs from new structure for ${dateString}`);
|
|
458
|
+
} catch (newStructError) {
|
|
459
|
+
logger.log('WARN', `Failed to load from new structure, falling back to legacy: ${newStructError.message}`);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// LEGACY STRUCTURE: Read from block-based collections
|
|
268
463
|
const collectionsToQuery = [
|
|
269
464
|
config.normalUserHistoryCollection,
|
|
270
465
|
config.speculatorHistoryCollection,
|
|
271
466
|
config.piHistoryCollection,
|
|
272
|
-
config.signedInHistoryCollection
|
|
467
|
+
config.signedInHistoryCollection
|
|
273
468
|
].filter(Boolean);
|
|
274
469
|
|
|
275
|
-
const allPartRefs = [];
|
|
276
|
-
|
|
277
470
|
for (const collectionName of collectionsToQuery) {
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
const
|
|
285
|
-
.
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
471
|
+
try {
|
|
472
|
+
const blockDocsQuery = db.collection(collectionName);
|
|
473
|
+
const blockDocRefs = await withRetry(() => blockDocsQuery.listDocuments(), `listDocuments(${collectionName})`);
|
|
474
|
+
|
|
475
|
+
if (!blockDocRefs.length) { continue; }
|
|
476
|
+
|
|
477
|
+
const partsPromises = blockDocRefs.map(blockDocRef => {
|
|
478
|
+
const partsCollectionRef = blockDocRef.collection(config.snapshotsSubcollection || 'snapshots')
|
|
479
|
+
.doc(dateString).collection(config.partsSubcollection || 'parts');
|
|
480
|
+
return withRetry(() => partsCollectionRef.listDocuments(), `listParts(${partsCollectionRef.path})`);
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
const partDocArrays = await Promise.all(partsPromises);
|
|
484
|
+
partDocArrays.forEach(partDocs => {
|
|
485
|
+
// History parts are standard, no deep merge needed usually
|
|
486
|
+
allPartRefs.push(...partDocs.map(ref => ({
|
|
487
|
+
ref,
|
|
488
|
+
type: 'PART',
|
|
489
|
+
collectionType: 'LEGACY'
|
|
490
|
+
})));
|
|
491
|
+
});
|
|
492
|
+
} catch (legacyError) {
|
|
493
|
+
logger.log('WARN', `Failed to load legacy history collection ${collectionName}: ${legacyError.message}`);
|
|
494
|
+
}
|
|
294
495
|
}
|
|
295
|
-
|
|
496
|
+
|
|
497
|
+
logger.log('INFO', `Found ${allPartRefs.length} total history refs for ${dateString}`);
|
|
296
498
|
return allPartRefs;
|
|
297
499
|
}
|
|
298
500
|
|
|
@@ -16,11 +16,29 @@ const SHARD_SIZE = 40;
|
|
|
16
16
|
* @returns {Promise<{success: boolean, message: string, instrumentsProcessed?: number}>}
|
|
17
17
|
*/
|
|
18
18
|
exports.fetchAndStorePrices = async (config, dependencies) => {
|
|
19
|
-
const { db, logger, headerManager, proxyManager } = dependencies;
|
|
19
|
+
const { db, logger, headerManager, proxyManager, collectionRegistry } = dependencies;
|
|
20
20
|
logger.log('INFO', '[PriceFetcherHelpers] Starting Daily Closing Price Update...');
|
|
21
21
|
let selectedHeader = null;
|
|
22
22
|
let wasSuccessful = false;
|
|
23
|
-
|
|
23
|
+
|
|
24
|
+
// Get collection names from registry if available, fallback to hardcoded
|
|
25
|
+
const { getCollectionPath } = collectionRegistry || {};
|
|
26
|
+
let priceCollectionName = 'asset_prices';
|
|
27
|
+
let priceTrackingCollectionName = 'pricedatastoreddates';
|
|
28
|
+
|
|
29
|
+
if (getCollectionPath) {
|
|
30
|
+
try {
|
|
31
|
+
// Extract collection name from registry path: asset_prices/shard_{shardId}
|
|
32
|
+
const basePath = getCollectionPath('rootData', 'assetPrices', { shardId: '0' });
|
|
33
|
+
priceCollectionName = basePath.split('/')[0];
|
|
34
|
+
|
|
35
|
+
// Extract price tracking collection name
|
|
36
|
+
const trackingPath = getCollectionPath('rootData', 'priceTracking', { fetchDate: '2025-01-01' });
|
|
37
|
+
priceTrackingCollectionName = trackingPath.split('/')[0];
|
|
38
|
+
} catch (e) {
|
|
39
|
+
logger.log('WARN', `[PriceFetcherHelpers] Failed to get collections from registry, using defaults: ${e.message}`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
24
42
|
try { if (!config.etoroApiUrl) { throw new Error("Missing required configuration: etoroApiUrl."); }
|
|
25
43
|
selectedHeader = await headerManager.selectHeader();
|
|
26
44
|
if (!selectedHeader || !selectedHeader.header) { throw new Error("Could not select a valid header for the request."); }
|
|
@@ -81,7 +99,7 @@ exports.fetchAndStorePrices = async (config, dependencies) => {
|
|
|
81
99
|
|
|
82
100
|
// Write date tracking document
|
|
83
101
|
const today = new Date().toISOString().split('T')[0];
|
|
84
|
-
const dateTrackingRef = db.collection(
|
|
102
|
+
const dateTrackingRef = db.collection(priceTrackingCollectionName).doc(today);
|
|
85
103
|
const priceDatesArray = Array.from(priceDatesSet).sort();
|
|
86
104
|
|
|
87
105
|
await dateTrackingRef.set({
|
|
@@ -13,12 +13,27 @@ const zlib = require('zlib'); // [NEW] Required for compression
|
|
|
13
13
|
* @returns {Promise<{success: boolean, message: string, instrumentCount?: number}>}
|
|
14
14
|
*/
|
|
15
15
|
exports.fetchAndStoreInsights = async (config, dependencies) => {
|
|
16
|
-
const { db, logger, headerManager, proxyManager } = dependencies;
|
|
16
|
+
const { db, logger, headerManager, proxyManager, collectionRegistry } = dependencies;
|
|
17
17
|
logger.log('INFO', '[FetchInsightsHelpers] Starting eToro insights data fetch...');
|
|
18
18
|
let selectedHeader = null; let wasSuccessful = false;
|
|
19
19
|
|
|
20
|
+
// Get collection name from registry if available, fallback to config
|
|
21
|
+
const { getCollectionPath } = collectionRegistry || {};
|
|
22
|
+
let insightsCollectionName = config.insightsCollectionName;
|
|
23
|
+
|
|
24
|
+
if (getCollectionPath) {
|
|
25
|
+
try {
|
|
26
|
+
// Extract collection name from registry path: daily_instrument_insights/{date}
|
|
27
|
+
const basePath = getCollectionPath('rootData', 'instrumentInsights', { date: '2025-01-01' });
|
|
28
|
+
// Path is like "daily_instrument_insights/2025-01-01", extract collection name
|
|
29
|
+
insightsCollectionName = basePath.split('/')[0];
|
|
30
|
+
} catch (e) {
|
|
31
|
+
logger.log('WARN', `[FetchInsightsHelpers] Failed to get collection from registry, using config: ${e.message}`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
20
35
|
try {
|
|
21
|
-
if (!config.etoroInsightsUrl || !
|
|
36
|
+
if (!config.etoroInsightsUrl || !insightsCollectionName) {
|
|
22
37
|
throw new Error("Missing required configuration: etoroInsightsUrl or insightsCollectionName.");
|
|
23
38
|
}
|
|
24
39
|
|
|
@@ -71,7 +86,7 @@ exports.fetchAndStoreInsights = async (config, dependencies) => {
|
|
|
71
86
|
}
|
|
72
87
|
|
|
73
88
|
const today = new Date().toISOString().slice(0, 10);
|
|
74
|
-
const docRef = db.collection(
|
|
89
|
+
const docRef = db.collection(insightsCollectionName).doc(today);
|
|
75
90
|
|
|
76
91
|
// [FIX] --- COMPRESSION LOGIC START ---
|
|
77
92
|
|
|
@@ -130,7 +145,7 @@ exports.fetchAndStoreInsights = async (config, dependencies) => {
|
|
|
130
145
|
...config.rootDataIndexer,
|
|
131
146
|
collections: {
|
|
132
147
|
...config.rootDataIndexer.collections,
|
|
133
|
-
insights:
|
|
148
|
+
insights: insightsCollectionName // Override with actual collection name used
|
|
134
149
|
},
|
|
135
150
|
targetDate: today // Index only today's date for speed
|
|
136
151
|
};
|
|
@@ -16,10 +16,25 @@ const { IntelligentHeaderManager } = require('../../core/utils/intelligent_heade
|
|
|
16
16
|
* @param {object} config.headerConfig - Configuration for the IntelligentHeaderManager.
|
|
17
17
|
*/
|
|
18
18
|
async function fetchAndStorePopularInvestors(config, dependencies) {
|
|
19
|
-
const { db, logger } = dependencies;
|
|
19
|
+
const { db, logger, collectionRegistry } = dependencies;
|
|
20
20
|
const { rankingsApiUrl, rankingsCollectionName, proxyConfig, headerConfig } = config;
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
// Get collection name from registry if available, fallback to config
|
|
23
|
+
const { getCollectionPath } = collectionRegistry || {};
|
|
24
|
+
let finalRankingsCollectionName = rankingsCollectionName;
|
|
25
|
+
|
|
26
|
+
if (getCollectionPath) {
|
|
27
|
+
try {
|
|
28
|
+
// Extract collection name from registry path: popular_investor_rankings/{date}
|
|
29
|
+
const basePath = getCollectionPath('rootData', 'popularInvestorRankings', { date: '2025-01-01' });
|
|
30
|
+
// Path is like "popular_investor_rankings/2025-01-01", extract collection name
|
|
31
|
+
finalRankingsCollectionName = basePath.split('/')[0];
|
|
32
|
+
} catch (e) {
|
|
33
|
+
logger.log('WARN', `[PopularInvestorFetch] Failed to get collection from registry, using config: ${e.message}`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (!rankingsApiUrl || !finalRankingsCollectionName || !proxyConfig || !headerConfig) {
|
|
23
38
|
throw new Error("[PopularInvestorFetch] Missing required config (rankingsApiUrl, rankingsCollectionName, proxyConfig, headerConfig).");
|
|
24
39
|
}
|
|
25
40
|
|
|
@@ -103,7 +118,7 @@ async function fetchAndStorePopularInvestors(config, dependencies) {
|
|
|
103
118
|
// 6. Final Validation & Storage
|
|
104
119
|
if (data && data.Items && Array.isArray(data.Items)) {
|
|
105
120
|
try {
|
|
106
|
-
const docRef = db.collection(
|
|
121
|
+
const docRef = db.collection(finalRankingsCollectionName).doc(today);
|
|
107
122
|
|
|
108
123
|
await docRef.set({
|
|
109
124
|
fetchedAt: new Date(),
|
|
@@ -112,7 +127,7 @@ async function fetchAndStorePopularInvestors(config, dependencies) {
|
|
|
112
127
|
...data
|
|
113
128
|
});
|
|
114
129
|
|
|
115
|
-
logger.log('SUCCESS', `[PopularInvestorFetch] Stored ${data.TotalRows} rankings into ${
|
|
130
|
+
logger.log('SUCCESS', `[PopularInvestorFetch] Stored ${data.TotalRows} rankings into ${finalRankingsCollectionName}/${today}`);
|
|
116
131
|
|
|
117
132
|
// Update root data indexer for today's date after rankings data is stored
|
|
118
133
|
try {
|
|
@@ -132,7 +147,7 @@ async function fetchAndStorePopularInvestors(config, dependencies) {
|
|
|
132
147
|
...rootDataIndexerConfig,
|
|
133
148
|
collections: {
|
|
134
149
|
...rootDataIndexerConfig.collections,
|
|
135
|
-
piRankings:
|
|
150
|
+
piRankings: finalRankingsCollectionName // Override with actual collection name used
|
|
136
151
|
},
|
|
137
152
|
targetDate: today // Index only today's date for speed
|
|
138
153
|
};
|