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.
- package/functions/orchestrator/index.js +19 -17
- package/index.js +8 -29
- package/package.json +1 -1
- package/functions/computation-system/WorkflowOrchestrator.js +0 -213
- package/functions/computation-system/config/monitoring_config.js +0 -31
- package/functions/computation-system/config/validation_overrides.js +0 -10
- package/functions/computation-system/context/ContextFactory.js +0 -143
- package/functions/computation-system/context/ManifestBuilder.js +0 -379
- package/functions/computation-system/data/AvailabilityChecker.js +0 -236
- package/functions/computation-system/data/CachedDataLoader.js +0 -325
- package/functions/computation-system/data/DependencyFetcher.js +0 -455
- package/functions/computation-system/executors/MetaExecutor.js +0 -279
- package/functions/computation-system/executors/PriceBatchExecutor.js +0 -108
- package/functions/computation-system/executors/StandardExecutor.js +0 -465
- package/functions/computation-system/helpers/computation_dispatcher.js +0 -750
- package/functions/computation-system/helpers/computation_worker.js +0 -375
- package/functions/computation-system/helpers/monitor.js +0 -64
- package/functions/computation-system/helpers/on_demand_helpers.js +0 -154
- package/functions/computation-system/layers/extractors.js +0 -1097
- package/functions/computation-system/layers/index.js +0 -40
- package/functions/computation-system/layers/mathematics.js +0 -522
- package/functions/computation-system/layers/profiling.js +0 -537
- package/functions/computation-system/layers/validators.js +0 -170
- package/functions/computation-system/legacy/AvailabilityCheckerOld.js +0 -388
- package/functions/computation-system/legacy/CachedDataLoaderOld.js +0 -357
- package/functions/computation-system/legacy/DependencyFetcherOld.js +0 -478
- package/functions/computation-system/legacy/MetaExecutorold.js +0 -364
- package/functions/computation-system/legacy/StandardExecutorold.js +0 -476
- package/functions/computation-system/legacy/computation_dispatcherold.js +0 -944
- package/functions/computation-system/logger/logger.js +0 -297
- package/functions/computation-system/persistence/ContractValidator.js +0 -81
- package/functions/computation-system/persistence/FirestoreUtils.js +0 -56
- package/functions/computation-system/persistence/ResultCommitter.js +0 -283
- package/functions/computation-system/persistence/ResultsValidator.js +0 -130
- package/functions/computation-system/persistence/RunRecorder.js +0 -142
- package/functions/computation-system/persistence/StatusRepository.js +0 -52
- package/functions/computation-system/reporter_epoch.js +0 -6
- package/functions/computation-system/scripts/UpdateContracts.js +0 -128
- package/functions/computation-system/services/SnapshotService.js +0 -148
- package/functions/computation-system/simulation/Fabricator.js +0 -285
- package/functions/computation-system/simulation/SeededRandom.js +0 -41
- package/functions/computation-system/simulation/SimRunner.js +0 -51
- package/functions/computation-system/system_epoch.js +0 -2
- package/functions/computation-system/tools/BuildReporter.js +0 -531
- package/functions/computation-system/tools/ContractDiscoverer.js +0 -144
- package/functions/computation-system/tools/DeploymentValidator.js +0 -536
- package/functions/computation-system/tools/FinalSweepReporter.js +0 -322
- package/functions/computation-system/topology/HashManager.js +0 -55
- package/functions/computation-system/topology/ManifestLoader.js +0 -47
- package/functions/computation-system/utils/data_loader.js +0 -675
- package/functions/computation-system/utils/schema_capture.js +0 -121
- 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
|
-
};
|