bulltrackers-module 1.0.710 → 1.0.713
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/api-v2/helpers/data-fetchers/firestore.js +119 -63
- package/functions/computation-system/data/CachedDataLoader.js +22 -1
- package/functions/computation-system/data/DependencyFetcher.js +118 -0
- package/functions/computation-system/persistence/ResultCommitter.js +94 -3
- package/functions/computation-system/utils/data_loader.js +244 -13
- package/functions/core/utils/bigquery_utils.js +1655 -0
- package/functions/core/utils/firestore_utils.js +99 -30
- package/functions/etoro-price-fetcher/helpers/handler_helpers.js +85 -13
- package/functions/fetch-insights/helpers/handler_helpers.js +26 -0
- package/functions/fetch-popular-investors/helpers/fetch_helpers.js +66 -0
- package/functions/maintenance/backfill-instrument-insights/index.js +180 -0
- package/functions/maintenance/backfill-pi-master-list-rankings/index.js +293 -0
- package/functions/maintenance/backfill-task-engine-data/README.md +72 -0
- package/functions/maintenance/backfill-task-engine-data/index.js +844 -0
- package/functions/price-backfill/helpers/handler_helpers.js +59 -10
- package/functions/root-data-indexer/index.js +79 -27
- package/functions/task-engine/helpers/data_storage_helpers.js +194 -102
- package/functions/task-engine/helpers/popular_investor_helpers.js +13 -7
- package/functions/task-engine/utils/bigquery_batch_manager.js +201 -0
- package/functions/task-engine/utils/firestore_batch_manager.js +21 -1
- package/index.js +34 -2
- package/package.json +7 -3
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @fileoverview Main pipe: pipe.maintenance.runBackfillAssetPrices
|
|
3
3
|
* A one-time function to backfill historical price data from eToro's
|
|
4
|
-
* candle API into
|
|
4
|
+
* candle API into BigQuery `asset_prices` table.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
const { FieldValue } = require('@google-cloud/firestore');
|
|
8
8
|
const pLimit = require('p-limit');
|
|
9
9
|
const CONCURRENT_REQUESTS = 10;
|
|
10
10
|
const DAYS_TO_FETCH = 365;
|
|
11
|
-
const SHARD_SIZE = 40;
|
|
11
|
+
const SHARD_SIZE = 40; // Legacy, kept for Firestore compatibility if needed
|
|
12
|
+
|
|
13
|
+
// BigQuery batch size for load jobs (100MB limit, but we batch smaller for efficiency)
|
|
14
|
+
const BIGQUERY_BATCH_SIZE = 10000; // rows per batch
|
|
12
15
|
|
|
13
16
|
/**
|
|
14
17
|
* Main pipe: pipe.maintenance.runBackfillAssetPrices
|
|
@@ -42,14 +45,60 @@ exports.runBackfillAssetPrices = async (config, dependencies) => {
|
|
|
42
45
|
headerManager.updatePerformance(selectedHeader.id, wasSuccess);
|
|
43
46
|
const data = await response.json();
|
|
44
47
|
const candles = data?.Candles?.[0]?.Candles;
|
|
45
|
-
if (!Array.isArray(candles) || candles.length === 0) {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
48
|
+
if (!Array.isArray(candles) || candles.length === 0) {
|
|
49
|
+
logger.log('WARN', `[PriceBackfill] No candle data returned for ${ticker} (${instrumentId})`);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Transform candles to BigQuery rows
|
|
54
|
+
const fetchedAt = new Date().toISOString();
|
|
55
|
+
const bigqueryRows = candles.map(candle => {
|
|
56
|
+
const dateStr = candle.FromDate.substring(0, 10); // Extract YYYY-MM-DD
|
|
57
|
+
return {
|
|
58
|
+
date: dateStr,
|
|
59
|
+
instrument_id: parseInt(instrumentId, 10),
|
|
60
|
+
ticker: ticker,
|
|
61
|
+
price: candle.Close || null,
|
|
62
|
+
open: candle.Open || null,
|
|
63
|
+
high: candle.High || null,
|
|
64
|
+
low: candle.Low || null,
|
|
65
|
+
close: candle.Close || null,
|
|
66
|
+
volume: candle.Volume || null,
|
|
67
|
+
fetched_at: fetchedAt
|
|
68
|
+
};
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// Write to BigQuery using load jobs (free, batched)
|
|
72
|
+
if (process.env.BIGQUERY_ENABLED !== 'false') {
|
|
73
|
+
try {
|
|
74
|
+
const { insertRows, ensureAssetPricesTable } = require('../../core/utils/bigquery_utils');
|
|
75
|
+
await ensureAssetPricesTable(logger);
|
|
76
|
+
|
|
77
|
+
const datasetId = process.env.BIGQUERY_DATASET_ID || 'bulltrackers_data';
|
|
78
|
+
await insertRows(datasetId, 'asset_prices', bigqueryRows, logger);
|
|
79
|
+
|
|
80
|
+
logger.log('TRACE', `[PriceBackfill] Successfully stored ${bigqueryRows.length} price records for ${ticker} (${instrumentId}) to BigQuery`);
|
|
81
|
+
} catch (bqError) {
|
|
82
|
+
logger.log('ERROR', `[PriceBackfill] BigQuery write failed for ${ticker} (${instrumentId}): ${bqError.message}`);
|
|
83
|
+
// Continue - don't fail the entire backfill for one instrument
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Also write to Firestore for backward compatibility (if needed)
|
|
88
|
+
// This can be removed once all readers are migrated to BigQuery
|
|
89
|
+
if (process.env.FIRESTORE_PRICE_BACKFILL !== 'false') {
|
|
90
|
+
const prices = {};
|
|
91
|
+
for (const candle of candles) {
|
|
92
|
+
const dateKey = candle.FromDate.substring(0, 10);
|
|
93
|
+
prices[dateKey] = candle.Close;
|
|
94
|
+
}
|
|
95
|
+
const shardId = `shard_${parseInt(instrumentId, 10) % SHARD_SIZE}`;
|
|
96
|
+
const docRef = db.collection('asset_prices').doc(shardId);
|
|
97
|
+
const payload = { [instrumentId]: { ticker: ticker, prices: prices, lastUpdated: FieldValue.serverTimestamp() } };
|
|
98
|
+
await docRef.set(payload, { merge: true });
|
|
99
|
+
logger.log('TRACE', `[PriceBackfill] Also stored data for ${ticker} (${instrumentId}) in Firestore ${shardId}`);
|
|
100
|
+
}
|
|
101
|
+
|
|
53
102
|
successCount++;
|
|
54
103
|
} catch (err) {
|
|
55
104
|
logger.log('ERROR', `[PriceBackfill] Failed to process instrument ${instrumentId}`, { err: err.message });
|
|
@@ -245,6 +245,56 @@ exports.runRootDataIndexer = async (config, dependencies) => {
|
|
|
245
245
|
|
|
246
246
|
// --- Define Refs & Check Paths ---
|
|
247
247
|
|
|
248
|
+
// =========================================================================
|
|
249
|
+
// BIGQUERY FIRST: Check BigQuery tables for data availability
|
|
250
|
+
// =========================================================================
|
|
251
|
+
let bigqueryHasPortfolio = false;
|
|
252
|
+
let bigqueryHasHistory = false;
|
|
253
|
+
let bigqueryHasSocial = false;
|
|
254
|
+
let bigqueryHasInsights = false;
|
|
255
|
+
let bigqueryHasPrices = false;
|
|
256
|
+
let bigqueryHasPIRankings = false;
|
|
257
|
+
let bigqueryHasPIMasterList = false;
|
|
258
|
+
|
|
259
|
+
if (process.env.BIGQUERY_ENABLED !== 'false') {
|
|
260
|
+
try {
|
|
261
|
+
const {
|
|
262
|
+
queryPortfolioData,
|
|
263
|
+
queryHistoryData,
|
|
264
|
+
querySocialData,
|
|
265
|
+
queryInstrumentInsights,
|
|
266
|
+
queryAssetPrices,
|
|
267
|
+
queryPIRankings,
|
|
268
|
+
queryPIMasterList
|
|
269
|
+
} = require('../../core/utils/bigquery_utils');
|
|
270
|
+
|
|
271
|
+
// Check BigQuery for today's data (or target date)
|
|
272
|
+
const [portfolioData, historyData, socialData, insightsData, pricesData, rankingsData, masterListData] = await Promise.all([
|
|
273
|
+
queryPortfolioData(dateStr, null, null, logger).catch(() => null),
|
|
274
|
+
queryHistoryData(dateStr, null, null, logger).catch(() => null),
|
|
275
|
+
querySocialData(dateStr, null, null, logger).catch(() => null),
|
|
276
|
+
queryInstrumentInsights(dateStr, logger).catch(() => null),
|
|
277
|
+
queryAssetPrices(dateStr, dateStr, null, logger).catch(() => null), // Check for specific date
|
|
278
|
+
queryPIRankings(dateStr, logger).catch(() => null),
|
|
279
|
+
queryPIMasterList(logger).catch(() => null)
|
|
280
|
+
]);
|
|
281
|
+
|
|
282
|
+
bigqueryHasPortfolio = portfolioData && Object.keys(portfolioData).length > 0;
|
|
283
|
+
bigqueryHasHistory = historyData && Object.keys(historyData).length > 0;
|
|
284
|
+
bigqueryHasSocial = socialData && Object.keys(socialData).length > 0;
|
|
285
|
+
bigqueryHasInsights = insightsData && Array.isArray(insightsData) && insightsData.length > 0;
|
|
286
|
+
bigqueryHasPrices = pricesData && Object.keys(pricesData).length > 0;
|
|
287
|
+
bigqueryHasPIRankings = rankingsData && Array.isArray(rankingsData) && rankingsData.length > 0;
|
|
288
|
+
bigqueryHasPIMasterList = masterListData && Object.keys(masterListData).length > 0;
|
|
289
|
+
|
|
290
|
+
if (bigqueryHasPortfolio || bigqueryHasHistory || bigqueryHasSocial || bigqueryHasInsights || bigqueryHasPrices || bigqueryHasPIRankings) {
|
|
291
|
+
logger.log('INFO', `[RootDataIndexer/${dateStr}] ✅ Found data in BigQuery: portfolio=${bigqueryHasPortfolio}, history=${bigqueryHasHistory}, social=${bigqueryHasSocial}, insights=${bigqueryHasInsights}, prices=${bigqueryHasPrices}, rankings=${bigqueryHasPIRankings}`);
|
|
292
|
+
}
|
|
293
|
+
} catch (bqError) {
|
|
294
|
+
logger.log('WARN', `[RootDataIndexer/${dateStr}] BigQuery check failed, using Firestore fallback: ${bqError.message}`);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
248
298
|
// 1. Standard Retail
|
|
249
299
|
// Path: {normalPortfolios}/19M/snapshots/{YYYY-MM-DD}/parts/part_*
|
|
250
300
|
const normPortPartsRef = db.collection(safeCollections.normalPortfolios).doc(CANARY_BLOCK_ID).collection('snapshots').doc(dateStr).collection('parts');
|
|
@@ -435,25 +485,29 @@ exports.runRootDataIndexer = async (config, dependencies) => {
|
|
|
435
485
|
logger.log('INFO', `[RootDataIndexer/${dateStr}] Social check results - PI: ${foundPISocial}, Signed-in: ${foundSignedInSocial}`);
|
|
436
486
|
|
|
437
487
|
// --- Assign to Availability ---
|
|
438
|
-
|
|
439
|
-
availability.details.
|
|
440
|
-
availability.details.
|
|
441
|
-
availability.details.
|
|
442
|
-
availability.details.
|
|
488
|
+
// Use BigQuery results if available, otherwise fall back to Firestore
|
|
489
|
+
availability.details.normalPortfolio = bigqueryHasPortfolio || normPortExists;
|
|
490
|
+
availability.details.speculatorPortfolio = bigqueryHasPortfolio || specPortExists;
|
|
491
|
+
availability.details.normalHistory = bigqueryHasHistory || normHistExists;
|
|
492
|
+
availability.details.speculatorHistory = bigqueryHasHistory || specHistExists;
|
|
493
|
+
availability.details.piRankings = bigqueryHasPIRankings || piRankingsSnap.exists;
|
|
443
494
|
|
|
444
|
-
availability.details.piPortfolios = piPortExists;
|
|
445
|
-
availability.details.piDeepPortfolios = piDeepExists;
|
|
446
|
-
availability.details.piHistory = piHistExists;
|
|
495
|
+
availability.details.piPortfolios = bigqueryHasPortfolio || piPortExists;
|
|
496
|
+
availability.details.piDeepPortfolios = piDeepExists; // Legacy only, no BigQuery equivalent
|
|
497
|
+
availability.details.piHistory = bigqueryHasHistory || piHistExists;
|
|
447
498
|
|
|
448
499
|
// PI & Signed-In Social Flags (Strict)
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
availability.details.
|
|
453
|
-
|
|
500
|
+
// Use BigQuery if available, otherwise use Firestore tracking documents
|
|
501
|
+
const finalPISocial = bigqueryHasSocial || foundPISocial;
|
|
502
|
+
const finalSignedInSocial = bigqueryHasSocial || foundSignedInSocial;
|
|
503
|
+
availability.details.piSocial = finalPISocial;
|
|
504
|
+
availability.details.hasPISocial = finalPISocial;
|
|
505
|
+
availability.details.signedInSocial = finalSignedInSocial;
|
|
506
|
+
availability.details.hasSignedInSocial = finalSignedInSocial;
|
|
507
|
+
|
|
454
508
|
// Signed-In Flags
|
|
455
|
-
availability.details.signedInUserPortfolio = signedInPortExists;
|
|
456
|
-
availability.details.signedInUserHistory = signedInHistExists;
|
|
509
|
+
availability.details.signedInUserPortfolio = bigqueryHasPortfolio || signedInPortExists;
|
|
510
|
+
availability.details.signedInUserHistory = bigqueryHasHistory || signedInHistExists;
|
|
457
511
|
availability.details.signedInUserVerification = !verificationsQuery.empty;
|
|
458
512
|
|
|
459
513
|
// New Root Data Types for Profile Metrics
|
|
@@ -461,20 +515,18 @@ exports.runRootDataIndexer = async (config, dependencies) => {
|
|
|
461
515
|
availability.details.piPageViews = piPageViewsSnap.exists;
|
|
462
516
|
availability.details.watchlistMembership = watchlistMembershipSnap.exists;
|
|
463
517
|
availability.details.piAlertHistory = piAlertHistorySnap.exists;
|
|
518
|
+
|
|
519
|
+
// Aggregates (use BigQuery if available, otherwise Firestore)
|
|
520
|
+
availability.hasPortfolio = bigqueryHasPortfolio || normPortExists || specPortExists || piPortExists || signedInPortExists;
|
|
521
|
+
availability.hasHistory = bigqueryHasHistory || normHistExists || specHistExists || piHistExists || signedInHistExists;
|
|
522
|
+
availability.hasInsights = bigqueryHasInsights || insightsSnap.exists;
|
|
523
|
+
availability.hasSocial = bigqueryHasSocial || finalPISocial || finalSignedInSocial || genericSocialExists;
|
|
464
524
|
|
|
465
|
-
//
|
|
466
|
-
availability.
|
|
467
|
-
availability.hasHistory = normHistExists || specHistExists || piHistExists || signedInHistExists;
|
|
468
|
-
availability.hasInsights = insightsSnap.exists;
|
|
469
|
-
availability.hasSocial = foundPISocial || foundSignedInSocial || genericSocialExists;
|
|
470
|
-
|
|
471
|
-
// [NEW] Assign Master List Availability
|
|
472
|
-
availability.details.piMasterList = piMasterListSnap.exists;
|
|
525
|
+
// [NEW] Assign Master List Availability (BigQuery first, then Firestore)
|
|
526
|
+
availability.details.piMasterList = bigqueryHasPIMasterList || piMasterListSnap.exists;
|
|
473
527
|
|
|
474
|
-
// Price Check
|
|
475
|
-
|
|
476
|
-
// The set is populated from the price tracking document's datesAvailable array
|
|
477
|
-
const hasPriceForDate = priceAvailabilitySet.has(dateStr);
|
|
528
|
+
// Price Check (BigQuery first, then Firestore tracking document)
|
|
529
|
+
const hasPriceForDate = bigqueryHasPrices || priceAvailabilitySet.has(dateStr);
|
|
478
530
|
availability.hasPrices = hasPriceForDate;
|
|
479
531
|
|
|
480
532
|
if (targetDate && !hasPriceForDate) {
|
|
@@ -2,13 +2,20 @@
|
|
|
2
2
|
* @fileoverview Data Storage Helpers for New Collection Structure
|
|
3
3
|
*
|
|
4
4
|
* Stores data to:
|
|
5
|
-
* 1.
|
|
5
|
+
* 1. BigQuery (date-based, per-user) - for computation system (NEW)
|
|
6
6
|
* 2. User-centric collections (latest snapshot) - for fallback/quick access
|
|
7
7
|
*
|
|
8
|
+
* UPDATED: Removed root data Firestore writes, now writes to BigQuery instead
|
|
8
9
|
* Uses the centralized collection registry for all paths.
|
|
9
10
|
*/
|
|
10
11
|
|
|
11
12
|
const { FieldValue } = require('@google-cloud/firestore');
|
|
13
|
+
const {
|
|
14
|
+
ensurePortfolioSnapshotsTable,
|
|
15
|
+
ensureTradeHistorySnapshotsTable,
|
|
16
|
+
ensureSocialPostSnapshotsTable,
|
|
17
|
+
insertRows
|
|
18
|
+
} = require('../../core/utils/bigquery_utils');
|
|
12
19
|
|
|
13
20
|
/**
|
|
14
21
|
* Store portfolio data for a signed-in user
|
|
@@ -19,23 +26,38 @@ const { FieldValue } = require('@google-cloud/firestore');
|
|
|
19
26
|
* @param {string} params.cid - User CID
|
|
20
27
|
* @param {string} params.date - Date string (YYYY-MM-DD)
|
|
21
28
|
* @param {object} params.portfolioData - Portfolio data to store
|
|
29
|
+
* @param {object} params.bigqueryBatchManager - Optional BigQuery batch manager (if provided, batches writes)
|
|
22
30
|
* @returns {Promise<void>}
|
|
23
31
|
*/
|
|
24
|
-
async function storeSignedInUserPortfolio({ db, logger, collectionRegistry, cid, date, portfolioData }) {
|
|
25
|
-
// 1.
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
32
|
+
async function storeSignedInUserPortfolio({ db, logger, collectionRegistry, cid, date, portfolioData, bigqueryBatchManager = null }) {
|
|
33
|
+
// 1. Write to BigQuery (for computations) - use batch manager if provided, otherwise direct write
|
|
34
|
+
if (process.env.BIGQUERY_ENABLED !== 'false') {
|
|
35
|
+
try {
|
|
36
|
+
const datasetId = process.env.BIGQUERY_DATASET_ID || 'bulltrackers_data';
|
|
37
|
+
const row = {
|
|
38
|
+
date: date,
|
|
39
|
+
user_id: Number(cid),
|
|
40
|
+
user_type: 'SIGNED_IN_USER',
|
|
41
|
+
portfolio_data: JSON.stringify(portfolioData), // BigQuery JSON type requires a string
|
|
42
|
+
fetched_at: new Date().toISOString()
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
if (bigqueryBatchManager) {
|
|
46
|
+
// Add to batch (will flush with Firestore batches)
|
|
47
|
+
await bigqueryBatchManager.addPortfolioRow(row);
|
|
48
|
+
} else {
|
|
49
|
+
// Direct write (fallback for when batch manager not available)
|
|
50
|
+
await ensurePortfolioSnapshotsTable(logger);
|
|
51
|
+
await insertRows(datasetId, 'portfolio_snapshots', [row], logger);
|
|
52
|
+
logger.log('INFO', `[DataStorage] ✅ Wrote portfolio to BigQuery for signed-in user ${cid} (date: ${date})`);
|
|
53
|
+
}
|
|
54
|
+
} catch (bqError) {
|
|
55
|
+
logger.log('WARN', `[DataStorage] BigQuery write failed for signed-in user ${cid} (${date}): ${bqError.message}`);
|
|
56
|
+
// Continue to Firestore write (fallback)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
37
59
|
|
|
38
|
-
// 2. Store latest snapshot to user-centric collection (for fallback)
|
|
60
|
+
// 2. Store latest snapshot to user-centric collection (for fallback/quick access)
|
|
39
61
|
const { getCollectionPath } = collectionRegistry || {};
|
|
40
62
|
if (!getCollectionPath) {
|
|
41
63
|
throw new Error('collectionRegistry.getCollectionPath is required');
|
|
@@ -63,23 +85,38 @@ async function storeSignedInUserPortfolio({ db, logger, collectionRegistry, cid,
|
|
|
63
85
|
* @param {string} params.cid - User CID
|
|
64
86
|
* @param {string} params.date - Date string (YYYY-MM-DD)
|
|
65
87
|
* @param {object} params.historyData - Trade history data to store
|
|
88
|
+
* @param {object} params.bigqueryBatchManager - Optional BigQuery batch manager (if provided, batches writes)
|
|
66
89
|
* @returns {Promise<void>}
|
|
67
90
|
*/
|
|
68
|
-
async function storeSignedInUserTradeHistory({ db, logger, collectionRegistry, cid, date, historyData }) {
|
|
69
|
-
// 1.
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
91
|
+
async function storeSignedInUserTradeHistory({ db, logger, collectionRegistry, cid, date, historyData, bigqueryBatchManager = null }) {
|
|
92
|
+
// 1. Write to BigQuery (for computations) - use batch manager if provided, otherwise direct write
|
|
93
|
+
if (process.env.BIGQUERY_ENABLED !== 'false') {
|
|
94
|
+
try {
|
|
95
|
+
const datasetId = process.env.BIGQUERY_DATASET_ID || 'bulltrackers_data';
|
|
96
|
+
const row = {
|
|
97
|
+
date: date,
|
|
98
|
+
user_id: Number(cid),
|
|
99
|
+
user_type: 'SIGNED_IN_USER',
|
|
100
|
+
history_data: JSON.stringify(historyData), // BigQuery JSON type requires a string
|
|
101
|
+
fetched_at: new Date().toISOString()
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
if (bigqueryBatchManager) {
|
|
105
|
+
// Add to batch (will flush with Firestore batches)
|
|
106
|
+
await bigqueryBatchManager.addHistoryRow(row);
|
|
107
|
+
} else {
|
|
108
|
+
// Direct write (fallback for when batch manager not available)
|
|
109
|
+
await ensureTradeHistorySnapshotsTable(logger);
|
|
110
|
+
await insertRows(datasetId, 'trade_history_snapshots', [row], logger);
|
|
111
|
+
logger.log('INFO', `[DataStorage] ✅ Wrote trade history to BigQuery for signed-in user ${cid} (date: ${date})`);
|
|
112
|
+
}
|
|
113
|
+
} catch (bqError) {
|
|
114
|
+
logger.log('WARN', `[DataStorage] BigQuery write failed for signed-in user ${cid} (${date}): ${bqError.message}`);
|
|
115
|
+
// Continue to Firestore write (fallback)
|
|
116
|
+
}
|
|
117
|
+
}
|
|
81
118
|
|
|
82
|
-
// 2. Store latest snapshot to user-centric collection (for fallback)
|
|
119
|
+
// 2. Store latest snapshot to user-centric collection (for fallback/quick access)
|
|
83
120
|
const { getCollectionPath } = collectionRegistry || {};
|
|
84
121
|
if (!getCollectionPath) {
|
|
85
122
|
throw new Error('collectionRegistry.getCollectionPath is required');
|
|
@@ -107,30 +144,39 @@ async function storeSignedInUserTradeHistory({ db, logger, collectionRegistry, c
|
|
|
107
144
|
* @param {string} params.cid - User CID
|
|
108
145
|
* @param {string} params.date - Date string (YYYY-MM-DD)
|
|
109
146
|
* @param {Array} params.posts - Array of social posts
|
|
147
|
+
* @param {object} params.bigqueryBatchManager - Optional BigQuery batch manager (if provided, batches writes)
|
|
110
148
|
* @returns {Promise<void>}
|
|
111
149
|
*/
|
|
112
|
-
async function storeSignedInUserSocialPosts({ db, logger, collectionRegistry, cid, date, posts }) {
|
|
113
|
-
// 1.
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
150
|
+
async function storeSignedInUserSocialPosts({ db, logger, collectionRegistry, cid, date, posts, bigqueryBatchManager = null }) {
|
|
151
|
+
// 1. Write to BigQuery (for computations)
|
|
152
|
+
if (process.env.BIGQUERY_ENABLED !== 'false') {
|
|
153
|
+
try {
|
|
154
|
+
await ensureSocialPostSnapshotsTable(logger);
|
|
155
|
+
|
|
156
|
+
const postsMap = {};
|
|
157
|
+
for (const post of posts) {
|
|
158
|
+
if (post.id || post.postId) {
|
|
159
|
+
postsMap[post.id || post.postId] = post;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const datasetId = process.env.BIGQUERY_DATASET_ID || 'bulltrackers_data';
|
|
164
|
+
const row = {
|
|
165
|
+
date: date,
|
|
166
|
+
user_id: Number(cid),
|
|
167
|
+
user_type: 'SIGNED_IN_USER',
|
|
168
|
+
posts_data: JSON.stringify({ posts: postsMap, postCount: posts.length }), // BigQuery JSON type requires a string
|
|
169
|
+
fetched_at: new Date().toISOString()
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
await insertRows(datasetId, 'social_post_snapshots', [row], logger);
|
|
173
|
+
logger.log('INFO', `[DataStorage] ✅ Wrote social posts to BigQuery for signed-in user ${cid} (date: ${date}, ${posts.length} posts)`);
|
|
174
|
+
} catch (bqError) {
|
|
175
|
+
logger.log('WARN', `[DataStorage] BigQuery write failed for signed-in user ${cid} (${date}): ${bqError.message}`);
|
|
176
|
+
// Continue to Firestore write (fallback)
|
|
124
177
|
}
|
|
125
178
|
}
|
|
126
179
|
|
|
127
|
-
await rootDataRef.set({
|
|
128
|
-
posts: postsMap,
|
|
129
|
-
fetchedAt: FieldValue.serverTimestamp(),
|
|
130
|
-
cid: String(cid),
|
|
131
|
-
postCount: posts.length
|
|
132
|
-
}, { merge: false });
|
|
133
|
-
|
|
134
180
|
// 2. Store latest posts to user-centric collection (for fallback)
|
|
135
181
|
// Path structure: SignedInUsers/{cid}/posts/{postId}
|
|
136
182
|
// Construct path directly - we know the structure
|
|
@@ -180,31 +226,48 @@ async function storeSignedInUserSocialPosts({ db, logger, collectionRegistry, ci
|
|
|
180
226
|
* @param {string} params.date - Date string (YYYY-MM-DD)
|
|
181
227
|
* @param {object} params.portfolioData - Portfolio data to store
|
|
182
228
|
* @param {object} params.deepPortfolioData - Optional deep portfolio data
|
|
229
|
+
* @param {object} params.bigqueryBatchManager - Optional BigQuery batch manager (if provided, batches writes)
|
|
183
230
|
* @returns {Promise<void>}
|
|
184
231
|
*/
|
|
185
|
-
async function storePopularInvestorPortfolio({ db, logger, collectionRegistry, cid, date, portfolioData, deepPortfolioData = null }) {
|
|
186
|
-
// 1.
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
232
|
+
async function storePopularInvestorPortfolio({ db, logger, collectionRegistry, cid, date, portfolioData, deepPortfolioData = null, bigqueryBatchManager = null }) {
|
|
233
|
+
// 1. Write to BigQuery (for computations) - use batch manager if provided, otherwise direct write
|
|
234
|
+
if (process.env.BIGQUERY_ENABLED !== 'false') {
|
|
235
|
+
try {
|
|
236
|
+
const portfolioDoc = {
|
|
237
|
+
...portfolioData,
|
|
238
|
+
cid: String(cid)
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
// If deep portfolio data exists, merge it
|
|
242
|
+
if (deepPortfolioData && deepPortfolioData.positions) {
|
|
243
|
+
portfolioDoc.deepPositions = deepPortfolioData.positions;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const datasetId = process.env.BIGQUERY_DATASET_ID || 'bulltrackers_data';
|
|
247
|
+
const row = {
|
|
248
|
+
date: date,
|
|
249
|
+
user_id: Number(cid),
|
|
250
|
+
user_type: 'POPULAR_INVESTOR',
|
|
251
|
+
portfolio_data: JSON.stringify(portfolioDoc), // BigQuery JSON type requires a string
|
|
252
|
+
fetched_at: new Date().toISOString()
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
if (bigqueryBatchManager) {
|
|
256
|
+
// Add to batch (will flush with Firestore batches)
|
|
257
|
+
await bigqueryBatchManager.addPortfolioRow(row);
|
|
258
|
+
} else {
|
|
259
|
+
// Direct write (fallback for when batch manager not available)
|
|
260
|
+
await ensurePortfolioSnapshotsTable(logger);
|
|
261
|
+
await insertRows(datasetId, 'portfolio_snapshots', [row], logger);
|
|
262
|
+
logger.log('INFO', `[DataStorage] ✅ Wrote portfolio to BigQuery for PI ${cid} (date: ${date})`);
|
|
263
|
+
}
|
|
264
|
+
} catch (bqError) {
|
|
265
|
+
logger.log('WARN', `[DataStorage] BigQuery write failed for PI ${cid} (${date}): ${bqError.message}`);
|
|
266
|
+
// Continue to Firestore write (fallback)
|
|
267
|
+
}
|
|
203
268
|
}
|
|
204
269
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
// 3. Store latest snapshot to user-centric collection (for fallback)
|
|
270
|
+
// 2. Store latest snapshot to user-centric collection (for fallback/quick access)
|
|
208
271
|
const { getCollectionPath } = collectionRegistry || {};
|
|
209
272
|
if (!getCollectionPath) {
|
|
210
273
|
throw new Error('collectionRegistry.getCollectionPath is required');
|
|
@@ -231,23 +294,38 @@ async function storePopularInvestorPortfolio({ db, logger, collectionRegistry, c
|
|
|
231
294
|
* @param {string} params.cid - PI CID
|
|
232
295
|
* @param {string} params.date - Date string (YYYY-MM-DD)
|
|
233
296
|
* @param {object} params.historyData - Trade history data to store
|
|
297
|
+
* @param {object} params.bigqueryBatchManager - Optional BigQuery batch manager (if provided, batches writes)
|
|
234
298
|
* @returns {Promise<void>}
|
|
235
299
|
*/
|
|
236
|
-
async function storePopularInvestorTradeHistory({ db, logger, collectionRegistry, cid, date, historyData }) {
|
|
237
|
-
// 1.
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
300
|
+
async function storePopularInvestorTradeHistory({ db, logger, collectionRegistry, cid, date, historyData, bigqueryBatchManager = null }) {
|
|
301
|
+
// 1. Write to BigQuery (for computations) - use batch manager if provided, otherwise direct write
|
|
302
|
+
if (process.env.BIGQUERY_ENABLED !== 'false') {
|
|
303
|
+
try {
|
|
304
|
+
const datasetId = process.env.BIGQUERY_DATASET_ID || 'bulltrackers_data';
|
|
305
|
+
const row = {
|
|
306
|
+
date: date,
|
|
307
|
+
user_id: Number(cid),
|
|
308
|
+
user_type: 'POPULAR_INVESTOR',
|
|
309
|
+
history_data: JSON.stringify(historyData), // BigQuery JSON type requires a string
|
|
310
|
+
fetched_at: new Date().toISOString()
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
if (bigqueryBatchManager) {
|
|
314
|
+
// Add to batch (will flush with Firestore batches)
|
|
315
|
+
await bigqueryBatchManager.addHistoryRow(row);
|
|
316
|
+
} else {
|
|
317
|
+
// Direct write (fallback for when batch manager not available)
|
|
318
|
+
await ensureTradeHistorySnapshotsTable(logger);
|
|
319
|
+
await insertRows(datasetId, 'trade_history_snapshots', [row], logger);
|
|
320
|
+
logger.log('INFO', `[DataStorage] ✅ Wrote trade history to BigQuery for PI ${cid} (date: ${date})`);
|
|
321
|
+
}
|
|
322
|
+
} catch (bqError) {
|
|
323
|
+
logger.log('WARN', `[DataStorage] BigQuery write failed for PI ${cid} (${date}): ${bqError.message}`);
|
|
324
|
+
// Continue to Firestore write (fallback)
|
|
325
|
+
}
|
|
326
|
+
}
|
|
249
327
|
|
|
250
|
-
// 2. Store latest snapshot to user-centric collection (for fallback)
|
|
328
|
+
// 2. Store latest snapshot to user-centric collection (for fallback/quick access)
|
|
251
329
|
const { getCollectionPath } = collectionRegistry || {};
|
|
252
330
|
if (!getCollectionPath) {
|
|
253
331
|
throw new Error('collectionRegistry.getCollectionPath is required');
|
|
@@ -275,30 +353,44 @@ async function storePopularInvestorTradeHistory({ db, logger, collectionRegistry
|
|
|
275
353
|
* @param {string} params.cid - PI CID
|
|
276
354
|
* @param {string} params.date - Date string (YYYY-MM-DD)
|
|
277
355
|
* @param {Array} params.posts - Array of social posts
|
|
356
|
+
* @param {object} params.bigqueryBatchManager - Optional BigQuery batch manager (if provided, batches writes)
|
|
278
357
|
* @returns {Promise<void>}
|
|
279
358
|
*/
|
|
280
|
-
async function storePopularInvestorSocialPosts({ db, logger, collectionRegistry, cid, date, posts }) {
|
|
281
|
-
// 1.
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
359
|
+
async function storePopularInvestorSocialPosts({ db, logger, collectionRegistry, cid, date, posts, bigqueryBatchManager = null }) {
|
|
360
|
+
// 1. Write to BigQuery (for computations) - use batch manager if provided, otherwise direct write
|
|
361
|
+
if (process.env.BIGQUERY_ENABLED !== 'false') {
|
|
362
|
+
try {
|
|
363
|
+
const postsMap = {};
|
|
364
|
+
for (const post of posts) {
|
|
365
|
+
if (post.id || post.postId) {
|
|
366
|
+
postsMap[post.id || post.postId] = post;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const datasetId = process.env.BIGQUERY_DATASET_ID || 'bulltrackers_data';
|
|
371
|
+
const row = {
|
|
372
|
+
date: date,
|
|
373
|
+
user_id: Number(cid),
|
|
374
|
+
user_type: 'POPULAR_INVESTOR',
|
|
375
|
+
posts_data: JSON.stringify({ posts: postsMap, postCount: posts.length }), // BigQuery JSON type requires a string
|
|
376
|
+
fetched_at: new Date().toISOString()
|
|
377
|
+
};
|
|
378
|
+
|
|
379
|
+
if (bigqueryBatchManager) {
|
|
380
|
+
// Add to batch (will flush with Firestore batches)
|
|
381
|
+
await bigqueryBatchManager.addSocialRow(row);
|
|
382
|
+
} else {
|
|
383
|
+
// Direct write (fallback for when batch manager not available)
|
|
384
|
+
await ensureSocialPostSnapshotsTable(logger);
|
|
385
|
+
await insertRows(datasetId, 'social_post_snapshots', [row], logger);
|
|
386
|
+
logger.log('INFO', `[DataStorage] ✅ Wrote social posts to BigQuery for PI ${cid} (date: ${date}, ${posts.length} posts)`);
|
|
387
|
+
}
|
|
388
|
+
} catch (bqError) {
|
|
389
|
+
logger.log('WARN', `[DataStorage] BigQuery write failed for PI ${cid} (${date}): ${bqError.message}`);
|
|
390
|
+
// Continue to Firestore write (fallback)
|
|
292
391
|
}
|
|
293
392
|
}
|
|
294
393
|
|
|
295
|
-
await rootDataRef.set({
|
|
296
|
-
posts: postsMap,
|
|
297
|
-
fetchedAt: FieldValue.serverTimestamp(),
|
|
298
|
-
cid: String(cid),
|
|
299
|
-
postCount: posts.length
|
|
300
|
-
}, { merge: false });
|
|
301
|
-
|
|
302
394
|
// 2. Store latest posts to user-centric collection (for fallback)
|
|
303
395
|
// Path structure: PopularInvestors/{piCid}/posts/{postId}
|
|
304
396
|
// Construct path directly - we know the structure
|