bulltrackers-module 1.0.732 → 1.0.733

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.
Files changed (52) hide show
  1. package/functions/orchestrator/index.js +19 -17
  2. package/index.js +8 -29
  3. package/package.json +1 -1
  4. package/functions/computation-system/WorkflowOrchestrator.js +0 -213
  5. package/functions/computation-system/config/monitoring_config.js +0 -31
  6. package/functions/computation-system/config/validation_overrides.js +0 -10
  7. package/functions/computation-system/context/ContextFactory.js +0 -143
  8. package/functions/computation-system/context/ManifestBuilder.js +0 -379
  9. package/functions/computation-system/data/AvailabilityChecker.js +0 -236
  10. package/functions/computation-system/data/CachedDataLoader.js +0 -325
  11. package/functions/computation-system/data/DependencyFetcher.js +0 -455
  12. package/functions/computation-system/executors/MetaExecutor.js +0 -279
  13. package/functions/computation-system/executors/PriceBatchExecutor.js +0 -108
  14. package/functions/computation-system/executors/StandardExecutor.js +0 -465
  15. package/functions/computation-system/helpers/computation_dispatcher.js +0 -750
  16. package/functions/computation-system/helpers/computation_worker.js +0 -375
  17. package/functions/computation-system/helpers/monitor.js +0 -64
  18. package/functions/computation-system/helpers/on_demand_helpers.js +0 -154
  19. package/functions/computation-system/layers/extractors.js +0 -1097
  20. package/functions/computation-system/layers/index.js +0 -40
  21. package/functions/computation-system/layers/mathematics.js +0 -522
  22. package/functions/computation-system/layers/profiling.js +0 -537
  23. package/functions/computation-system/layers/validators.js +0 -170
  24. package/functions/computation-system/legacy/AvailabilityCheckerOld.js +0 -388
  25. package/functions/computation-system/legacy/CachedDataLoaderOld.js +0 -357
  26. package/functions/computation-system/legacy/DependencyFetcherOld.js +0 -478
  27. package/functions/computation-system/legacy/MetaExecutorold.js +0 -364
  28. package/functions/computation-system/legacy/StandardExecutorold.js +0 -476
  29. package/functions/computation-system/legacy/computation_dispatcherold.js +0 -944
  30. package/functions/computation-system/logger/logger.js +0 -297
  31. package/functions/computation-system/persistence/ContractValidator.js +0 -81
  32. package/functions/computation-system/persistence/FirestoreUtils.js +0 -56
  33. package/functions/computation-system/persistence/ResultCommitter.js +0 -283
  34. package/functions/computation-system/persistence/ResultsValidator.js +0 -130
  35. package/functions/computation-system/persistence/RunRecorder.js +0 -142
  36. package/functions/computation-system/persistence/StatusRepository.js +0 -52
  37. package/functions/computation-system/reporter_epoch.js +0 -6
  38. package/functions/computation-system/scripts/UpdateContracts.js +0 -128
  39. package/functions/computation-system/services/SnapshotService.js +0 -148
  40. package/functions/computation-system/simulation/Fabricator.js +0 -285
  41. package/functions/computation-system/simulation/SeededRandom.js +0 -41
  42. package/functions/computation-system/simulation/SimRunner.js +0 -51
  43. package/functions/computation-system/system_epoch.js +0 -2
  44. package/functions/computation-system/tools/BuildReporter.js +0 -531
  45. package/functions/computation-system/tools/ContractDiscoverer.js +0 -144
  46. package/functions/computation-system/tools/DeploymentValidator.js +0 -536
  47. package/functions/computation-system/tools/FinalSweepReporter.js +0 -322
  48. package/functions/computation-system/topology/HashManager.js +0 -55
  49. package/functions/computation-system/topology/ManifestLoader.js +0 -47
  50. package/functions/computation-system/utils/data_loader.js +0 -675
  51. package/functions/computation-system/utils/schema_capture.js +0 -121
  52. package/functions/computation-system/utils/utils.js +0 -188
@@ -1,675 +0,0 @@
1
- /**
2
- * @fileoverview Data Loading Layer.
3
- * REFACTORED:
4
- * 1. Routes 90% of data fetching to BigQuery (Portfolios, History, Prices, Insights, Rankings, etc.).
5
- * 2. Retains Firestore logic ONLY for:
6
- * - Verifications (user_verifications)
7
- * - Retail Users (Normal/Speculator Portfolios/History)
8
- * - Generic Social Feed (Legacy compatibility)
9
- */
10
- const {
11
- queryPortfolioData,
12
- queryHistoryData,
13
- querySocialData,
14
- queryAssetPrices,
15
- queryAllPricesForDate,
16
- queryPricesForTickers,
17
- queryInstrumentInsights,
18
- queryPIRankings,
19
- queryTickerMappings,
20
- queryPIMasterList,
21
- queryPIRatings,
22
- queryPIPageViews,
23
- queryWatchlistMembership,
24
- queryPIAlertHistory
25
- } = require('../../core/utils/bigquery_utils');
26
-
27
- const { normalizeName } = require('./utils');
28
- const { BigQuery } = require('@google-cloud/bigquery'); // [NEW] Import BigQuery Client
29
-
30
- // [NEW] Map root types to likely BQ tables (Configuration should ideally override this)
31
- const ROOT_TABLE_MAP = {
32
- portfolio: 'data-platform.feature_store.portfolios',
33
- history: 'data-platform.feature_store.trade_history'
34
- };
35
-
36
- const bigquery = new BigQuery();
37
-
38
- // =============================================================================
39
- // [NEW] ON-DEMAND LATEST DATA FETCHER
40
- // =============================================================================
41
- /**
42
- * Fetches the latest available root data for a specific user from BigQuery.
43
- * Used as a fallback when 'today's' data is missing due to outages.
44
- * @param {object} config - System config
45
- * @param {object} deps - Dependencies
46
- * @param {string} rootType - 'portfolio' or 'history'
47
- * @param {string} userId - The user ID (CID)
48
- * @param {string} userType - The user type (e.g. POPULAR_INVESTOR)
49
- * @param {number} lookbackDays - How far back to search (default 7)
50
- */
51
- exports.fetchLatestRootData = async (config, deps, rootType, userId, userType, lookbackDays = 7) => {
52
- const { logger } = deps;
53
- const tableName = config.bigQuery?.tables?.[rootType] || ROOT_TABLE_MAP[rootType];
54
-
55
- if (!tableName) {
56
- logger.log('WARN', `[DataLoader] No BigQuery table mapped for rootType '${rootType}'`);
57
- return null;
58
- }
59
-
60
- try {
61
- // Construct Dynamic Query to get the Last Available Record
62
- // Assumes schema: CustomerId, Date, UserType, and a payload column (e.g. PortfolioData/HistoryData)
63
- // We select * to get the payload wrapper.
64
- const query = `
65
- SELECT *
66
- FROM \`${tableName}\`
67
- WHERE CustomerId = @userId
68
- AND Date >= DATE_SUB(CURRENT_DATE(), INTERVAL @lookbackDays DAY)
69
- ORDER BY Date DESC
70
- LIMIT 1
71
- `;
72
-
73
- const options = {
74
- query: query,
75
- params: { userId: String(userId), lookbackDays: lookbackDays }
76
- };
77
-
78
- const [rows] = await bigquery.query(options);
79
-
80
- if (rows && rows.length > 0) {
81
- const record = rows[0];
82
- const dateFound = record.Date ? record.Date.value || record.Date : 'unknown';
83
- logger.log('INFO', `[DataLoader] 🔄 Fetched LATEST ${rootType} for ${userId} from ${dateFound}`);
84
-
85
- // Normalize result to match stream format
86
- // Assumes the payload is either the whole row or nested in a specific column like 'portfolio_data'
87
- // We return the payload with _userType injected.
88
- let payload = record;
89
- if (rootType === 'portfolio' && record.portfolio_data) payload = record.portfolio_data;
90
- if (rootType === 'history' && record.history_data) payload = record.history_data;
91
-
92
- return {
93
- ...payload,
94
- _userType: userType || record.UserType,
95
- _isFallback: true,
96
- _fetchedAt: new Date().toISOString()
97
- };
98
- }
99
-
100
- return null;
101
- } catch (e) {
102
- logger.log('ERROR', `[DataLoader] Failed to fetch latest ${rootType} for ${userId}: ${e.message}`);
103
- return null;
104
- }
105
- };
106
-
107
- // =============================================================================
108
- // 1. PORTFOLIOS
109
- // =============================================================================
110
- exports.loadDailyPortfolios = async (config, deps, dateStr, userTypes = []) => {
111
- const { db, logger } = deps;
112
-
113
- // Normalize user types
114
- const types = Array.isArray(userTypes) ? userTypes : [userTypes];
115
- const isRetail = types.some(t => ['NORMAL', 'SPECULATOR'].includes(t.toUpperCase()));
116
- const isMigrated = types.some(t => ['POPULAR_INVESTOR', 'SIGNED_IN_USER'].includes(t.toUpperCase()));
117
-
118
- let results = {};
119
-
120
- // A. BigQuery (PIs & SignedIn)
121
- if (isMigrated && process.env.BIGQUERY_ENABLED !== 'false') {
122
- const bqData = await queryPortfolioData(dateStr, null, types, logger);
123
- if (bqData) Object.assign(results, bqData);
124
- }
125
-
126
- // B. Firestore (Retail / Fallback)
127
- // Note: If we need Retail data, we MUST check Firestore as it wasn't migrated.
128
- if (isRetail) {
129
- if (types.includes('NORMAL')) {
130
- const normalData = await loadRetailFirestore(db, 'NormalUserPortfolios', dateStr);
131
- Object.assign(results, normalData);
132
- }
133
- if (types.includes('SPECULATOR')) {
134
- const specData = await loadRetailFirestore(db, 'SpeculatorPortfolios', dateStr);
135
- Object.assign(results, specData);
136
- }
137
- }
138
-
139
- return results;
140
- };
141
-
142
- // =============================================================================
143
- // 2. TRADE HISTORY
144
- // =============================================================================
145
- exports.loadDailyHistory = async (config, deps, dateStr, userTypes = []) => {
146
- const { db, logger } = deps;
147
- const types = Array.isArray(userTypes) ? userTypes : [userTypes];
148
- const isRetail = types.some(t => ['NORMAL', 'SPECULATOR'].includes(t.toUpperCase()));
149
- const isMigrated = types.some(t => ['POPULAR_INVESTOR', 'SIGNED_IN_USER'].includes(t.toUpperCase()));
150
-
151
- let results = {};
152
-
153
- if (isMigrated && process.env.BIGQUERY_ENABLED !== 'false') {
154
- const bqData = await queryHistoryData(dateStr, null, types, logger);
155
- if (bqData) Object.assign(results, bqData);
156
- }
157
-
158
- if (isRetail) {
159
- if (types.includes('NORMAL')) {
160
- const normalData = await loadRetailFirestore(db, 'NormalUserTradeHistory', dateStr);
161
- Object.assign(results, normalData);
162
- }
163
- if (types.includes('SPECULATOR')) {
164
- const specData = await loadRetailFirestore(db, 'SpeculatorTradeHistory', dateStr);
165
- Object.assign(results, specData);
166
- }
167
- }
168
- return results;
169
- };
170
-
171
- // =============================================================================
172
- // 3. SOCIAL
173
- // =============================================================================
174
- exports.loadDailySocialPostInsights = async (config, deps, dateStr, userTypes = []) => {
175
- const { db, logger } = deps;
176
- const types = Array.isArray(userTypes) ? userTypes : (userTypes ? [userTypes] : []);
177
-
178
- // A. BigQuery (User-Specific Social)
179
- if (types.length > 0 && process.env.BIGQUERY_ENABLED !== 'false') {
180
- return querySocialData(dateStr, null, types, logger);
181
- }
182
-
183
- // B. Firestore (Generic Feed - Legacy)
184
- // If no user types specified, assume generic feed fetch
185
- const collection = config.socialInsightsCollection || 'daily_social_insights';
186
- try {
187
- const snap = await db.collection(collection).doc(dateStr).collection('posts').get();
188
- if (snap.empty) return {};
189
- const data = {};
190
- snap.forEach(doc => data[doc.id] = doc.data());
191
- return data;
192
- } catch (e) {
193
- logger.log('WARN', `[DataLoader] Failed to load generic social for ${dateStr}: ${e.message}`);
194
- return {};
195
- }
196
- };
197
-
198
- // =============================================================================
199
- // 4. MARKET DATA (Prices)
200
- // =============================================================================
201
- exports.getPriceShardRefs = async (config, deps) => {
202
- // Legacy Shard Helper - In BQ world, we don't use shards but CachedDataLoader expects this structure.
203
- // We return a "virtual" shard array that signals CachedDataLoader to load from BQ.
204
- if (process.env.BIGQUERY_ENABLED !== 'false') {
205
- return [ { _bigquery: true } ];
206
- }
207
- // Fallback to Firestore Logic - return array of doc refs
208
- const { db } = deps;
209
- const collection = config.assetPricesCollection || 'asset_prices';
210
- const snapshot = await db.collection(collection).listDocuments();
211
- const refs = [];
212
- snapshot.forEach(doc => refs.push(doc));
213
- return refs;
214
- };
215
-
216
- exports.getRelevantShardRefs = async (config, deps, targetIds) => {
217
- // In BQ mode, we don't shard by instrument; return single virtual shard
218
- if (process.env.BIGQUERY_ENABLED !== 'false') {
219
- return [ { _bigquery: true, targetIds: targetIds || [] } ];
220
- }
221
- // Firestore behavior - return array of doc refs (same as getPriceShardRefs for now)
222
- return exports.getPriceShardRefs(config, deps);
223
- };
224
-
225
- // =============================================================================
226
- // 5. ROOT DATA TYPES (Simple Mappings)
227
- // =============================================================================
228
-
229
- exports.loadDailyInsights = async (config, deps, dateStr) => {
230
- const { logger } = deps;
231
-
232
- if (process.env.BIGQUERY_ENABLED !== 'false') {
233
- try {
234
- const rows = await queryInstrumentInsights(dateStr, logger);
235
- if (Array.isArray(rows) && rows.length > 0) {
236
- logger.log('INFO', `[DataLoader] ✅ Using BigQuery for instrument insights (${dateStr}): ${rows.length} instruments`);
237
- // Wrap in Firestore-shaped document format for InsightsExtractor compatibility
238
- return { insights: rows };
239
- }
240
- } catch (e) {
241
- logger.log('WARN', `[DataLoader] BigQuery insights query failed for ${dateStr}: ${e.message}`);
242
- }
243
- }
244
-
245
- // No Firestore fallback by design – return empty but correctly shaped
246
- return { insights: [] };
247
- };
248
-
249
- exports.loadPopularInvestorRankings = async (config, deps, dateStr) => {
250
- const data = await queryPIRankings(dateStr, deps.logger);
251
- return data ? data.Items : [];
252
- };
253
-
254
- exports.loadPIRatings = async (config, deps, dateStr) => {
255
- return queryPIRatings(dateStr, deps.logger);
256
- };
257
-
258
- exports.loadPIPageViews = async (config, deps, dateStr) => {
259
- return queryPIPageViews(dateStr, deps.logger);
260
- };
261
-
262
- exports.loadWatchlistMembership = async (config, deps, dateStr) => {
263
- return queryWatchlistMembership(dateStr, deps.logger);
264
- };
265
-
266
- exports.loadPIAlertHistory = async (config, deps, dateStr) => {
267
- return queryPIAlertHistory(dateStr, deps.logger);
268
- };
269
-
270
- exports.loadPopularInvestorMasterList = async (config, deps) => {
271
- return queryPIMasterList(deps.logger);
272
- };
273
-
274
- exports.loadPIWatchlistData = async (config, deps, piCid) => {
275
- // Watchlist data is time-series in BQ. For "Current State" (ID based),
276
- // we query the most recent date available for this PI.
277
- // This is a specialized query not in standard utils, so we implement it here or assume caller passes date.
278
- // However, CachedDataLoader expects (cid) -> Data.
279
- // We'll return null here as WatchlistMembership (by date) is the preferred method now.
280
- deps.logger.log('WARN', '[DataLoader] loadPIWatchlistData (by CID) is deprecated in favor of loadWatchlistMembership (by Date).');
281
- return null;
282
- };
283
-
284
- // =============================================================================
285
- // 6. EXCEPTIONS (Firestore Only)
286
- // =============================================================================
287
-
288
- exports.loadVerificationProfiles = async (config, deps, dateStr) => {
289
- const { db, logger } = deps;
290
- try {
291
- // Verifications are a single collection, not date-partitioned snapshots
292
- const snap = await db.collection('user_verifications').get();
293
- const verifications = {};
294
- snap.forEach(doc => verifications[doc.id] = doc.data());
295
- return verifications;
296
- } catch (e) {
297
- logger.log('ERROR', `[DataLoader] Failed to load verifications: ${e.message}`);
298
- return {};
299
- }
300
- };
301
-
302
- // =============================================================================
303
- // HELPERS
304
- // =============================================================================
305
-
306
- // =============================================================================
307
- // 7. PRICE DATA BY REFS (For PriceBatchExecutor)
308
- // =============================================================================
309
-
310
- /**
311
- * Load price data from an array of shard references (virtual or Firestore doc refs).
312
- * Used by PriceBatchExecutor for batch price computations.
313
- * @param {object} config - Configuration object
314
- * @param {object} deps - Dependencies (db, logger, etc.)
315
- * @param {Array} shardRefs - Array of shard references (virtual BigQuery objects or Firestore doc refs)
316
- * @returns {Promise<object>} Combined price data object keyed by instrument ID
317
- */
318
- exports.loadDataByRefs = async (config, deps, shardRefs) => {
319
- const { logger } = deps;
320
-
321
- if (!Array.isArray(shardRefs) || shardRefs.length === 0) {
322
- return {};
323
- }
324
-
325
- // Check if we're in BigQuery mode (virtual shards)
326
- const isBigQuery = shardRefs.some(ref => ref && ref._bigquery === true);
327
-
328
- if (isBigQuery && process.env.BIGQUERY_ENABLED !== 'false') {
329
- try {
330
- // Extract targetIds from virtual shards if present
331
- const targetIds = shardRefs
332
- .filter(ref => ref._bigquery && ref.targetIds && ref.targetIds.length > 0)
333
- .flatMap(ref => ref.targetIds);
334
-
335
- // Query BigQuery for prices
336
- // queryAssetPrices signature: (startDateStr, endDateStr, instrumentIds, logger)
337
- const { queryAssetPrices } = require('../../core/utils/bigquery_utils');
338
- const pricesData = await queryAssetPrices(null, null, targetIds.length > 0 ? targetIds : null, logger);
339
-
340
- // Filter by targetIds if specified
341
- if (targetIds.length > 0 && pricesData) {
342
- const targetSet = new Set(targetIds.map(id => String(id)));
343
- const filtered = {};
344
- for (const [instrumentId, priceData] of Object.entries(pricesData)) {
345
- if (targetSet.has(String(instrumentId))) {
346
- filtered[instrumentId] = priceData;
347
- }
348
- }
349
- return filtered;
350
- }
351
-
352
- return pricesData || {};
353
- } catch (e) {
354
- logger.log('ERROR', `[DataLoader] BigQuery price load failed: ${e.message}`);
355
- return {};
356
- }
357
- }
358
-
359
- // Firestore fallback - load from doc refs
360
- const combined = {};
361
- try {
362
- const loadPromises = shardRefs.map(async (docRef) => {
363
- try {
364
- const snap = await docRef.get();
365
- if (snap.exists) {
366
- const data = snap.data();
367
- // Firestore price shards are nested: { instrumentId: { prices: {...} } }
368
- Object.assign(combined, data);
369
- }
370
- } catch (e) {
371
- logger.log('WARN', `[DataLoader] Failed to load price shard: ${e.message}`);
372
- }
373
- });
374
-
375
- await Promise.all(loadPromises);
376
- } catch (e) {
377
- logger.log('ERROR', `[DataLoader] Failed to load price data from refs: ${e.message}`);
378
- }
379
-
380
- return combined;
381
- };
382
-
383
- // =============================================================================
384
- // HELPERS
385
- // =============================================================================
386
-
387
- async function loadRetailFirestore(db, collectionName, dateStr) {
388
- const CANARY_ID = '19M'; // Legacy Block
389
- try {
390
- const partsRef = db.collection(collectionName).doc(CANARY_ID)
391
- .collection('snapshots').doc(dateStr).collection('parts');
392
-
393
- const snap = await partsRef.get();
394
- if (snap.empty) return {};
395
-
396
- const combined = {};
397
- snap.forEach(doc => Object.assign(combined, doc.data()));
398
- return combined;
399
- } catch (e) {
400
- return {};
401
- }
402
- }
403
-
404
- // =============================================================================
405
- // 8. STREAMING DATA (For StandardExecutor)
406
- // =============================================================================
407
-
408
- /**
409
- * Get portfolio part references for streaming.
410
- * In BigQuery mode, returns virtual refs that signal to load from BQ.
411
- * @param {object} config - Configuration object
412
- * @param {object} deps - Dependencies (db, logger)
413
- * @param {string} dateStr - Date string (YYYY-MM-DD)
414
- * @param {Array<string>|null} userTypes - User types to filter (null = all)
415
- * @returns {Promise<Array>} Array of part references
416
- */
417
- exports.getPortfolioPartRefs = async (config, deps, dateStr, userTypes = null) => {
418
- const { logger } = deps;
419
-
420
- // In BigQuery mode, return virtual refs with metadata
421
- if (process.env.BIGQUERY_ENABLED !== 'false') {
422
- logger.log('INFO', `[DataLoader] Using BigQuery virtual refs for portfolios (${dateStr})`);
423
- return [{
424
- _bigquery: true,
425
- type: 'portfolio',
426
- date: dateStr,
427
- userTypes: userTypes
428
- }];
429
- }
430
-
431
- // Firestore fallback - return actual document references
432
- const { db } = deps;
433
- const refs = [];
434
-
435
- // Check both PI and SignedIn collections
436
- const collections = [
437
- { name: 'PopularInvestorPortfolios', userType: 'POPULAR_INVESTOR' },
438
- { name: 'SignedInUserPortfolios', userType: 'SIGNED_IN_USER' }
439
- ];
440
-
441
- for (const col of collections) {
442
- if (userTypes && !userTypes.includes(col.userType)) continue;
443
-
444
- try {
445
- const partsSnap = await db.collection(col.name)
446
- .doc('latest')
447
- .collection('snapshots')
448
- .doc(dateStr)
449
- .collection('parts')
450
- .listDocuments();
451
-
452
- for (const docRef of partsSnap) {
453
- refs.push({ ref: docRef, userType: col.userType, collection: col.name });
454
- }
455
- } catch (e) {
456
- logger.log('WARN', `[DataLoader] Failed to get portfolio refs from ${col.name}: ${e.message}`);
457
- }
458
- }
459
-
460
- return refs;
461
- };
462
-
463
- /**
464
- * Get history part references for streaming.
465
- * @param {object} config - Configuration object
466
- * @param {object} deps - Dependencies (db, logger)
467
- * @param {string} dateStr - Date string (YYYY-MM-DD)
468
- * @param {Array<string>|null} userTypes - User types to filter (null = all)
469
- * @returns {Promise<Array>} Array of part references
470
- */
471
- exports.getHistoryPartRefs = async (config, deps, dateStr, userTypes = null) => {
472
- const { logger } = deps;
473
-
474
- // In BigQuery mode, return virtual refs with metadata
475
- if (process.env.BIGQUERY_ENABLED !== 'false') {
476
- logger.log('INFO', `[DataLoader] Using BigQuery virtual refs for history (${dateStr})`);
477
- return [{
478
- _bigquery: true,
479
- type: 'history',
480
- date: dateStr,
481
- userTypes: userTypes
482
- }];
483
- }
484
-
485
- // Firestore fallback - return actual document references
486
- const { db } = deps;
487
- const refs = [];
488
-
489
- const collections = [
490
- { name: 'PopularInvestorTradeHistory', userType: 'POPULAR_INVESTOR' },
491
- { name: 'SignedInUserTradeHistory', userType: 'SIGNED_IN_USER' }
492
- ];
493
-
494
- for (const col of collections) {
495
- if (userTypes && !userTypes.includes(col.userType)) continue;
496
-
497
- try {
498
- const partsSnap = await db.collection(col.name)
499
- .doc('latest')
500
- .collection('snapshots')
501
- .doc(dateStr)
502
- .collection('parts')
503
- .listDocuments();
504
-
505
- for (const docRef of partsSnap) {
506
- refs.push({ ref: docRef, userType: col.userType, collection: col.name });
507
- }
508
- } catch (e) {
509
- logger.log('WARN', `[DataLoader] Failed to get history refs from ${col.name}: ${e.message}`);
510
- }
511
- }
512
-
513
- return refs;
514
- };
515
-
516
- /**
517
- * Stream portfolio data in chunks (async generator).
518
- * Yields objects of { cid: portfolioData } for memory efficiency.
519
- * @param {object} config - Configuration object
520
- * @param {object} deps - Dependencies (db, logger)
521
- * @param {string} dateStr - Date string (YYYY-MM-DD)
522
- * @param {Array} refs - Part references from getPortfolioPartRefs
523
- * @param {Array<string>|null} userTypes - User types to filter
524
- * @yields {object} Chunk of portfolio data { cid: data }
525
- */
526
- exports.streamPortfolioData = async function* (config, deps, dateStr, refs, userTypes = null) {
527
- const { logger } = deps;
528
-
529
- if (!refs || refs.length === 0) {
530
- logger.log('WARN', `[DataLoader] No portfolio refs provided for streaming (${dateStr})`);
531
- return;
532
- }
533
-
534
- // BigQuery mode - load all at once and yield in chunks
535
- const isBigQuery = refs.some(r => r && r._bigquery === true);
536
-
537
- if (isBigQuery && process.env.BIGQUERY_ENABLED !== 'false') {
538
- try {
539
- const types = userTypes || ['POPULAR_INVESTOR', 'SIGNED_IN_USER'];
540
- const rawData = await queryPortfolioData(dateStr, null, types, logger);
541
-
542
- if (rawData && Object.keys(rawData).length > 0) {
543
- // Transform BigQuery format to computation system format
544
- // BigQuery returns: { cid: { portfolio_data: {...}, user_type: '...', fetched_at: '...' } }
545
- // Computation system expects: { cid: { ...portfolioFields..., _userType: '...' } }
546
- const data = {};
547
- for (const [cid, record] of Object.entries(rawData)) {
548
- // Unwrap portfolio_data and add _userType metadata
549
- const portfolioData = record.portfolio_data || {};
550
- data[cid] = {
551
- ...portfolioData,
552
- _userType: record.user_type
553
- };
554
- }
555
-
556
- logger.log('INFO', `[DataLoader] ✅ Streaming ${Object.keys(data).length} portfolios from BigQuery (${dateStr})`);
557
-
558
- // Yield in chunks of 100 users for memory efficiency
559
- const CHUNK_SIZE = 100;
560
- const entries = Object.entries(data);
561
-
562
- for (let i = 0; i < entries.length; i += CHUNK_SIZE) {
563
- const chunk = {};
564
- entries.slice(i, i + CHUNK_SIZE).forEach(([cid, portfolio]) => {
565
- chunk[cid] = portfolio;
566
- });
567
- yield chunk;
568
- }
569
- return;
570
- }
571
- } catch (e) {
572
- logger.log('ERROR', `[DataLoader] BigQuery portfolio stream failed: ${e.message}`);
573
- }
574
- }
575
-
576
- // Firestore fallback - stream from refs
577
- for (const refInfo of refs) {
578
- if (!refInfo.ref) continue;
579
-
580
- try {
581
- const snap = await refInfo.ref.get();
582
- if (snap.exists) {
583
- const data = snap.data();
584
- // Add user type metadata
585
- const enriched = {};
586
- for (const [cid, portfolio] of Object.entries(data)) {
587
- enriched[cid] = { ...portfolio, _userType: refInfo.userType };
588
- }
589
- yield enriched;
590
- }
591
- } catch (e) {
592
- logger.log('WARN', `[DataLoader] Failed to stream portfolio part: ${e.message}`);
593
- }
594
- }
595
- };
596
-
597
- /**
598
- * Stream history data in chunks (async generator).
599
- * @param {object} config - Configuration object
600
- * @param {object} deps - Dependencies (db, logger)
601
- * @param {string} dateStr - Date string (YYYY-MM-DD)
602
- * @param {Array} refs - Part references from getHistoryPartRefs
603
- * @param {Array<string>|null} userTypes - User types to filter
604
- * @yields {object} Chunk of history data { cid: data }
605
- */
606
- exports.streamHistoryData = async function* (config, deps, dateStr, refs, userTypes = null) {
607
- const { logger } = deps;
608
-
609
- if (!refs || refs.length === 0) {
610
- logger.log('WARN', `[DataLoader] No history refs provided for streaming (${dateStr})`);
611
- return;
612
- }
613
-
614
- // BigQuery mode - load all at once and yield in chunks
615
- const isBigQuery = refs.some(r => r && r._bigquery === true);
616
-
617
- if (isBigQuery && process.env.BIGQUERY_ENABLED !== 'false') {
618
- try {
619
- const types = userTypes || ['POPULAR_INVESTOR', 'SIGNED_IN_USER'];
620
- const rawData = await queryHistoryData(dateStr, null, types, logger);
621
-
622
- if (rawData && Object.keys(rawData).length > 0) {
623
- // Transform BigQuery format to computation system format
624
- // BigQuery returns: { cid: { history_data: {...}, user_type: '...', fetched_at: '...' } }
625
- // Computation system expects: { cid: { ...historyFields..., _userType: '...' } }
626
- const data = {};
627
- for (const [cid, record] of Object.entries(rawData)) {
628
- // Unwrap history_data and add _userType metadata
629
- const historyData = record.history_data || {};
630
- data[cid] = {
631
- ...historyData,
632
- _userType: record.user_type
633
- };
634
- }
635
-
636
- logger.log('INFO', `[DataLoader] ✅ Streaming ${Object.keys(data).length} history records from BigQuery (${dateStr})`);
637
-
638
- // Yield in chunks of 100 users
639
- const CHUNK_SIZE = 100;
640
- const entries = Object.entries(data);
641
-
642
- for (let i = 0; i < entries.length; i += CHUNK_SIZE) {
643
- const chunk = {};
644
- entries.slice(i, i + CHUNK_SIZE).forEach(([cid, history]) => {
645
- chunk[cid] = history;
646
- });
647
- yield chunk;
648
- }
649
- return;
650
- }
651
- } catch (e) {
652
- logger.log('ERROR', `[DataLoader] BigQuery history stream failed: ${e.message}`);
653
- }
654
- }
655
-
656
- // Firestore fallback - stream from refs
657
- for (const refInfo of refs) {
658
- if (!refInfo.ref) continue;
659
-
660
- try {
661
- const snap = await refInfo.ref.get();
662
- if (snap.exists) {
663
- const data = snap.data();
664
- // Add user type metadata
665
- const enriched = {};
666
- for (const [cid, history] of Object.entries(data)) {
667
- enriched[cid] = { ...history, _userType: refInfo.userType };
668
- }
669
- yield enriched;
670
- }
671
- } catch (e) {
672
- logger.log('WARN', `[DataLoader] Failed to stream history part: ${e.message}`);
673
- }
674
- }
675
- };