bulltrackers-module 1.0.732 → 1.0.734

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 (106) hide show
  1. package/functions/computation-system-v2/README.md +152 -0
  2. package/functions/computation-system-v2/computations/PopularInvestorProfileMetrics.js +720 -0
  3. package/functions/computation-system-v2/computations/PopularInvestorRiskAssessment.js +176 -0
  4. package/functions/computation-system-v2/computations/PopularInvestorRiskMetrics.js +294 -0
  5. package/functions/computation-system-v2/computations/TestComputation.js +46 -0
  6. package/functions/computation-system-v2/computations/UserPortfolioSummary.js +172 -0
  7. package/functions/computation-system-v2/config/bulltrackers.config.js +317 -0
  8. package/functions/computation-system-v2/framework/core/Computation.js +73 -0
  9. package/functions/computation-system-v2/framework/core/Manifest.js +223 -0
  10. package/functions/computation-system-v2/framework/core/RuleInjector.js +53 -0
  11. package/functions/computation-system-v2/framework/core/Rules.js +231 -0
  12. package/functions/computation-system-v2/framework/core/RunAnalyzer.js +163 -0
  13. package/functions/computation-system-v2/framework/cost/CostTracker.js +154 -0
  14. package/functions/computation-system-v2/framework/data/DataFetcher.js +399 -0
  15. package/functions/computation-system-v2/framework/data/QueryBuilder.js +232 -0
  16. package/functions/computation-system-v2/framework/data/SchemaRegistry.js +287 -0
  17. package/functions/computation-system-v2/framework/execution/Orchestrator.js +498 -0
  18. package/functions/computation-system-v2/framework/execution/TaskRunner.js +35 -0
  19. package/functions/computation-system-v2/framework/execution/middleware/CostTrackerMiddleware.js +32 -0
  20. package/functions/computation-system-v2/framework/execution/middleware/LineageMiddleware.js +32 -0
  21. package/functions/computation-system-v2/framework/execution/middleware/Middleware.js +14 -0
  22. package/functions/computation-system-v2/framework/execution/middleware/ProfilerMiddleware.js +47 -0
  23. package/functions/computation-system-v2/framework/index.js +45 -0
  24. package/functions/computation-system-v2/framework/lineage/LineageTracker.js +147 -0
  25. package/functions/computation-system-v2/framework/monitoring/Profiler.js +80 -0
  26. package/functions/computation-system-v2/framework/resilience/Checkpointer.js +66 -0
  27. package/functions/computation-system-v2/framework/scheduling/ScheduleValidator.js +327 -0
  28. package/functions/computation-system-v2/framework/storage/StateRepository.js +286 -0
  29. package/functions/computation-system-v2/framework/storage/StorageManager.js +469 -0
  30. package/functions/computation-system-v2/framework/storage/index.js +9 -0
  31. package/functions/computation-system-v2/framework/testing/ComputationTester.js +86 -0
  32. package/functions/computation-system-v2/framework/utils/Graph.js +205 -0
  33. package/functions/computation-system-v2/handlers/dispatcher.js +109 -0
  34. package/functions/computation-system-v2/handlers/index.js +23 -0
  35. package/functions/computation-system-v2/handlers/onDemand.js +289 -0
  36. package/functions/computation-system-v2/handlers/scheduler.js +327 -0
  37. package/functions/computation-system-v2/index.js +163 -0
  38. package/functions/computation-system-v2/rules/index.js +49 -0
  39. package/functions/computation-system-v2/rules/instruments.js +465 -0
  40. package/functions/computation-system-v2/rules/metrics.js +304 -0
  41. package/functions/computation-system-v2/rules/portfolio.js +534 -0
  42. package/functions/computation-system-v2/rules/rankings.js +655 -0
  43. package/functions/computation-system-v2/rules/social.js +562 -0
  44. package/functions/computation-system-v2/rules/trades.js +545 -0
  45. package/functions/computation-system-v2/scripts/migrate-sectors.js +73 -0
  46. package/functions/computation-system-v2/test/test-dispatcher.js +317 -0
  47. package/functions/computation-system-v2/test/test-framework.js +500 -0
  48. package/functions/computation-system-v2/test/test-real-execution.js +166 -0
  49. package/functions/computation-system-v2/test/test-real-integration.js +194 -0
  50. package/functions/computation-system-v2/test/test-refactor-e2e.js +131 -0
  51. package/functions/computation-system-v2/test/test-results.json +31 -0
  52. package/functions/computation-system-v2/test/test-risk-metrics-computation.js +329 -0
  53. package/functions/computation-system-v2/test/test-scheduler.js +204 -0
  54. package/functions/computation-system-v2/test/test-storage.js +449 -0
  55. package/functions/orchestrator/index.js +24 -30
  56. package/index.js +8 -29
  57. package/package.json +3 -2
  58. package/functions/computation-system/WorkflowOrchestrator.js +0 -213
  59. package/functions/computation-system/config/monitoring_config.js +0 -31
  60. package/functions/computation-system/config/validation_overrides.js +0 -10
  61. package/functions/computation-system/context/ContextFactory.js +0 -143
  62. package/functions/computation-system/context/ManifestBuilder.js +0 -379
  63. package/functions/computation-system/data/AvailabilityChecker.js +0 -236
  64. package/functions/computation-system/data/CachedDataLoader.js +0 -325
  65. package/functions/computation-system/data/DependencyFetcher.js +0 -455
  66. package/functions/computation-system/executors/MetaExecutor.js +0 -279
  67. package/functions/computation-system/executors/PriceBatchExecutor.js +0 -108
  68. package/functions/computation-system/executors/StandardExecutor.js +0 -465
  69. package/functions/computation-system/helpers/computation_dispatcher.js +0 -750
  70. package/functions/computation-system/helpers/computation_worker.js +0 -375
  71. package/functions/computation-system/helpers/monitor.js +0 -64
  72. package/functions/computation-system/helpers/on_demand_helpers.js +0 -154
  73. package/functions/computation-system/layers/extractors.js +0 -1097
  74. package/functions/computation-system/layers/index.js +0 -40
  75. package/functions/computation-system/layers/mathematics.js +0 -522
  76. package/functions/computation-system/layers/profiling.js +0 -537
  77. package/functions/computation-system/layers/validators.js +0 -170
  78. package/functions/computation-system/legacy/AvailabilityCheckerOld.js +0 -388
  79. package/functions/computation-system/legacy/CachedDataLoaderOld.js +0 -357
  80. package/functions/computation-system/legacy/DependencyFetcherOld.js +0 -478
  81. package/functions/computation-system/legacy/MetaExecutorold.js +0 -364
  82. package/functions/computation-system/legacy/StandardExecutorold.js +0 -476
  83. package/functions/computation-system/legacy/computation_dispatcherold.js +0 -944
  84. package/functions/computation-system/logger/logger.js +0 -297
  85. package/functions/computation-system/persistence/ContractValidator.js +0 -81
  86. package/functions/computation-system/persistence/FirestoreUtils.js +0 -56
  87. package/functions/computation-system/persistence/ResultCommitter.js +0 -283
  88. package/functions/computation-system/persistence/ResultsValidator.js +0 -130
  89. package/functions/computation-system/persistence/RunRecorder.js +0 -142
  90. package/functions/computation-system/persistence/StatusRepository.js +0 -52
  91. package/functions/computation-system/reporter_epoch.js +0 -6
  92. package/functions/computation-system/scripts/UpdateContracts.js +0 -128
  93. package/functions/computation-system/services/SnapshotService.js +0 -148
  94. package/functions/computation-system/simulation/Fabricator.js +0 -285
  95. package/functions/computation-system/simulation/SeededRandom.js +0 -41
  96. package/functions/computation-system/simulation/SimRunner.js +0 -51
  97. package/functions/computation-system/system_epoch.js +0 -2
  98. package/functions/computation-system/tools/BuildReporter.js +0 -531
  99. package/functions/computation-system/tools/ContractDiscoverer.js +0 -144
  100. package/functions/computation-system/tools/DeploymentValidator.js +0 -536
  101. package/functions/computation-system/tools/FinalSweepReporter.js +0 -322
  102. package/functions/computation-system/topology/HashManager.js +0 -55
  103. package/functions/computation-system/topology/ManifestLoader.js +0 -47
  104. package/functions/computation-system/utils/data_loader.js +0 -675
  105. package/functions/computation-system/utils/schema_capture.js +0 -121
  106. 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
- };