bulltrackers-module 1.0.84 → 1.0.86

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.
@@ -1,9 +1,12 @@
1
1
  /**
2
2
  * @fileoverview Main pipe: pipe.maintenance.runFetchPrices
3
- * REFACTORED: Now stateless and receives dependencies.
3
+ * REFACTORED: Now writes to the new sharded `asset_prices` collection.
4
4
  */
5
5
  const { FieldValue } = require('@google-cloud/firestore');
6
6
 
7
+ // How many tickers to group into one Firestore document
8
+ const SHARD_SIZE = 40;
9
+
7
10
  /**
8
11
  * Main pipe: pipe.maintenance.runFetchPrices
9
12
  * @param {object} config - Configuration object.
@@ -13,13 +16,16 @@ const { FieldValue } = require('@google-cloud/firestore');
13
16
  exports.fetchAndStorePrices = async (config, dependencies) => {
14
17
  const { db, logger, headerManager, proxyManager } = dependencies;
15
18
 
16
- logger.log('INFO', '[PriceFetcherHelpers] Starting Daily Closing Price Update via module...');
19
+ logger.log('INFO', '[PriceFetcherHelpers] Starting Daily Closing Price Update...');
17
20
  let selectedHeader = null;
18
21
  let wasSuccessful = false;
19
22
 
23
+ // --- NEW: Use the new config key, or fallback to the old one --- TODO Implement the config
24
+ const priceCollectionName = 'asset_prices';
25
+
20
26
  try {
21
- if (!config.etoroApiUrl || !config.firestoreCollection || !config.batchLimit) {
22
- throw new Error("Missing required configuration: etoroApiUrl, firestoreCollection, or batchLimit.");
27
+ if (!config.etoroApiUrl) {
28
+ throw new Error("Missing required configuration: etoroApiUrl.");
23
29
  }
24
30
 
25
31
  selectedHeader = await headerManager.selectHeader();
@@ -32,15 +38,11 @@ exports.fetchAndStorePrices = async (config, dependencies) => {
32
38
  timeout: 60000
33
39
  };
34
40
 
35
- logger.log('INFO', `[PriceFetcherHelpers] Using header ID: ${selectedHeader.id}`, {
36
- userAgent: selectedHeader.header['User-Agent']
37
- });
41
+ logger.log('INFO', `[PriceFetcherHelpers] Using header ID: ${selectedHeader.id}`);
38
42
 
39
- const response = await proxyManager.fetch(config.etoroApiUrl, fetchOptions);
43
+ const response = await proxyManager.fetch(config.etoroApiUrl, fetchOptions);
40
44
 
41
45
  if (!response || typeof response.text !== 'function') {
42
- const responseString = JSON.stringify(response, null, 2);
43
- logger.log('ERROR', `[PriceFetcherHelpers] Invalid or incomplete response received. Response object: ${responseString}`);
44
46
  throw new Error(`Invalid response structure received from proxy.`);
45
47
  }
46
48
 
@@ -51,54 +53,56 @@ exports.fetchAndStorePrices = async (config, dependencies) => {
51
53
  wasSuccessful = true;
52
54
 
53
55
  const results = await response.json();
54
- if (!results || !Array.isArray(results)) {
56
+ if (!Array.isArray(results)) {
55
57
  throw new Error('Invalid response format from API. Expected an array.');
56
58
  }
57
59
 
58
- const promises = [];
59
- let batch = db.batch(); // Use db
60
- let instrumentsProcessed = 0;
61
- let batchCount = 0;
62
-
63
- for (const instrumentData of results) {
64
- const dailyData = instrumentData?.ClosingPrices?.Daily;
65
-
66
- if (instrumentData.InstrumentId && dailyData?.Price && dailyData?.Date) {
67
- const instrumentIdStr = String(instrumentData.InstrumentId);
68
- const dateKey = dailyData.Date.substring(0, 10);
69
-
70
- const updatePayload = {
71
- instrumentId: instrumentData.InstrumentId,
72
- lastUpdated: FieldValue.serverTimestamp(),
73
- [`prices.${dateKey}`]: dailyData.Price
74
- };
75
-
76
- const docRef = db.collection(config.firestoreCollection).doc(instrumentIdStr); // Use db
77
- batch.set(docRef, updatePayload, { merge: true });
78
-
79
- instrumentsProcessed++;
80
- batchCount++;
81
-
82
- if (batchCount >= config.batchLimit) {
83
- logger.log('TRACE', `[PriceFetcherHelpers] Committing batch of ${batchCount} instruments...`);
84
- promises.push(batch.commit());
85
- batch = db.batch(); // Use db
86
- batchCount = 0;
87
- await new Promise(resolve => setTimeout(resolve, 50));
88
- }
89
- }
90
- }
60
+ // --- START MODIFICATION ---
61
+
62
+ logger.log('INFO', `[PriceFetcherHelpers] Received ${results.length} instrument prices. Sharding...`);
63
+ const shardUpdates = {}; // { "shard_0": { ... }, "shard_1": { ... } }
64
+
65
+ for (const instrumentData of results) {
66
+ const dailyData = instrumentData?.ClosingPrices?.Daily;
67
+ const instrumentId = instrumentData.InstrumentId;
68
+
69
+ if (instrumentId && dailyData?.Price && dailyData?.Date) {
70
+ const instrumentIdStr = String(instrumentId);
71
+ const dateKey = dailyData.Date.substring(0, 10);
72
+
73
+ // Determine shard ID
74
+ const shardId = `shard_${parseInt(instrumentIdStr, 10) % SHARD_SIZE}`;
75
+
76
+ if (!shardUpdates[shardId]) {
77
+ shardUpdates[shardId] = {};
78
+ }
79
+
80
+ // Use dot notation to update only the specific price for the specific instrument
81
+ const pricePath = `${instrumentIdStr}.prices.${dateKey}`;
82
+ const updatePath = `${instrumentIdStr}.lastUpdated`;
83
+
84
+ shardUpdates[shardId][pricePath] = dailyData.Price;
85
+ shardUpdates[shardId][updatePath] = FieldValue.serverTimestamp();
86
+ }
87
+ }
91
88
 
92
- if (batchCount > 0) {
93
- logger.log('TRACE', `[PriceFetcherHelpers] Committing final batch of ${batchCount} instruments...`);
94
- promises.push(batch.commit());
95
- }
89
+ // Commit all shard updates in parallel
90
+ const batchPromises = [];
91
+ for (const shardId in shardUpdates) {
92
+ const docRef = db.collection(priceCollectionName).doc(shardId);
93
+ const payload = shardUpdates[shardId];
94
+
95
+ // Use set with merge:true to update all instruments in the shard doc at once
96
+ batchPromises.push(docRef.set(payload, { merge: true }));
97
+ }
96
98
 
97
- await Promise.all(promises);
99
+ await Promise.all(batchPromises);
100
+
101
+ // --- END MODIFICATION ---
98
102
 
99
- const successMessage = `Successfully processed and saved daily prices for ${instrumentsProcessed} instruments via module.`;
100
- logger.log('SUCCESS', `[PriceFetcherHelpers] ${successMessage}`, { instrumentsProcessed });
101
- return { success: true, message: successMessage, instrumentsProcessed };
103
+ const successMessage = `Successfully processed and saved daily prices for ${results.length} instruments to ${batchPromises.length} shards.`;
104
+ logger.log('SUCCESS', `[PriceFetcherHelpers] ${successMessage}`);
105
+ return { success: true, message: successMessage, instrumentsProcessed: results.length };
102
106
 
103
107
  } catch (error) {
104
108
  logger.log('ERROR', '[PriceFetcherHelpers] Fatal error during closing price update', {
@@ -110,8 +114,7 @@ exports.fetchAndStorePrices = async (config, dependencies) => {
110
114
  } finally {
111
115
  if (selectedHeader) {
112
116
  await headerManager.updatePerformance(selectedHeader.id, wasSuccessful);
113
- // Also flush performance, as this is a standalone function
114
117
  await headerManager.flushPerformanceUpdates();
115
118
  }
116
119
  }
117
- };
120
+ };
@@ -0,0 +1,112 @@
1
+ /**
2
+ * @fileoverview Main pipe: pipe.maintenance.runBackfillAssetPrices
3
+ * A one-time function to backfill historical price data from eToro's
4
+ * candle API into the new sharded `asset_prices` collection.
5
+ */
6
+
7
+ const { FieldValue } = require('@google-cloud/firestore');
8
+ // Import the mapping loader from your shared calculations package
9
+ const { loadInstrumentMappings } = require('aiden-shared-calculations-unified').utils;
10
+ const pLimit = require('p-limit');
11
+
12
+ // How many tickers to fetch in parallel
13
+ const CONCURRENT_REQUESTS = 10;
14
+ // How many days of history to fetch
15
+ const DAYS_TO_FETCH = 365;
16
+ // How many tickers to group into one Firestore document
17
+ const SHARD_SIZE = 40;
18
+
19
+ /**
20
+ * Main pipe: pipe.maintenance.runBackfillAssetPrices
21
+ * @param {object} config - Configuration object.
22
+ * @param {object} dependencies - Contains db, logger, headerManager, proxyManager.
23
+ */
24
+ exports.runBackfillAssetPrices = async (config, dependencies) => {
25
+ const { db, logger, headerManager, proxyManager } = dependencies;
26
+
27
+ logger.log('INFO', '[PriceBackfill] Starting historical price backfill...');
28
+
29
+ let mappings;
30
+ try {
31
+ mappings = await loadInstrumentMappings();
32
+ if (!mappings || !mappings.instrumentToTicker) {
33
+ throw new Error("Failed to load instrument mappings.");
34
+ }
35
+ } catch (e) {
36
+ logger.log('ERROR', '[PriceBackfill] Could not load instrument mappings.', { err: e.message });
37
+ return;
38
+ }
39
+
40
+ const instrumentIds = Object.keys(mappings.instrumentToTicker);
41
+ logger.log('INFO', `[PriceBackfill] Found ${instrumentIds.length} instruments to backfill.`);
42
+
43
+ const limit = pLimit(CONCURRENT_REQUESTS);
44
+ let successCount = 0;
45
+ let errorCount = 0;
46
+
47
+ const promises = instrumentIds.map(instrumentId => {
48
+ return limit(async () => {
49
+ try {
50
+ const ticker = mappings.instrumentToTicker[instrumentId] || `unknown_${instrumentId}`;
51
+ const url = `https://candle.etoro.com/candles/asc.json/OneDay/${DAYS_TO_FETCH}/${instrumentId}`;
52
+
53
+ const selectedHeader = await headerManager.selectHeader();
54
+ let wasSuccess = false;
55
+
56
+ const response = await proxyManager.fetch(url, {
57
+ headers: selectedHeader.header,
58
+ timeout: 20000
59
+ });
60
+
61
+ if (!response.ok) {
62
+ throw new Error(`API error ${response.status} for instrument ${instrumentId}`);
63
+ }
64
+ wasSuccess = true;
65
+ headerManager.updatePerformance(selectedHeader.id, wasSuccess);
66
+
67
+ const data = await response.json();
68
+ const candles = data?.Candles?.[0]?.Candles;
69
+
70
+ if (!Array.isArray(candles) || candles.length === 0) {
71
+ logger.log('WARN', `[PriceBackfill] No candle data returned for ${ticker} (${instrumentId})`);
72
+ return;
73
+ }
74
+
75
+ // Format data as a map
76
+ const prices = {};
77
+ for (const candle of candles) {
78
+ const dateKey = candle.FromDate.substring(0, 10);
79
+ prices[dateKey] = candle.Close;
80
+ }
81
+
82
+ // Determine shard ID
83
+ const shardId = `shard_${parseInt(instrumentId, 10) % SHARD_SIZE}`;
84
+ const docRef = db.collection('asset_prices').doc(shardId); // TODO implement config value
85
+
86
+ const payload = {
87
+ [instrumentId]: {
88
+ ticker: ticker,
89
+ prices: prices,
90
+ lastUpdated: FieldValue.serverTimestamp()
91
+ }
92
+ };
93
+
94
+ // Write to Firestore
95
+ await docRef.set(payload, { merge: true });
96
+ logger.log('TRACE', `[PriceBackfill] Successfully stored data for ${ticker} (${instrumentId}) in ${shardId}`);
97
+ successCount++;
98
+
99
+ } catch (err) {
100
+ logger.log('ERROR', `[PriceBackfill] Failed to process instrument ${instrumentId}`, { err: err.message });
101
+ errorCount++;
102
+ }
103
+ });
104
+ });
105
+
106
+ await Promise.all(promises);
107
+
108
+ // Flush any remaining header updates
109
+ await headerManager.flushPerformanceUpdates();
110
+
111
+ logger.log('SUCCESS', `[PriceBackfill] Backfill complete. Success: ${successCount}, Failed: ${errorCount}`);
112
+ };
package/index.js CHANGED
@@ -90,6 +90,7 @@ const maintenance = {
90
90
  // --- NEW SOCIAL SENTIMENT FUNCTIONS ---
91
91
  runSocialOrchestrator: require('./functions/social-orchestrator/helpers/orchestrator_helpers').runSocialOrchestrator,
92
92
  handleSocialTask: require('./functions/social-task-handler/helpers/handler_helpers').handleSocialTask,
93
+ runBackfillAssetPrices: require('./functions/price-backfill/helpers/handler_helpers').runBackfillAssetPrices,
93
94
  // --- END NEW ---
94
95
  };
95
96
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bulltrackers-module",
3
- "version": "1.0.84",
3
+ "version": "1.0.86",
4
4
  "description": "Helper Functions for Bulltrackers.",
5
5
  "main": "index.js",
6
6
  "files": [
@@ -18,7 +18,8 @@
18
18
  "functions/appscript-api/",
19
19
  "functions/user-activity-sampler/",
20
20
  "functions/social-orchestrator/",
21
- "functions/social-task-handler/"
21
+ "functions/social-task-handler/",
22
+ "functions/price-backfill/"
22
23
  ],
23
24
  "keywords": [
24
25
  "bulltrackers",
@@ -31,11 +32,11 @@
31
32
  "@google-cloud/firestore": "^7.11.3",
32
33
  "sharedsetup": "latest",
33
34
  "require-all": "^3.0.0",
34
- "aiden-shared-calculations-unified": "1.0.9",
35
+ "aiden-shared-calculations-unified": "1.0.11",
35
36
  "@google-cloud/pubsub": "latest",
36
37
  "express": "^4.19.2",
37
38
  "cors": "^2.8.5",
38
- "p-limit": "latest"
39
+ "p-limit": "^3.1.0"
39
40
  },
40
41
  "engines": {
41
42
  "node": ">=20"