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
|
|
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
|
|
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
|
|
22
|
-
throw new Error("Missing required configuration: etoroApiUrl
|
|
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
|
|
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 (!
|
|
56
|
+
if (!Array.isArray(results)) {
|
|
55
57
|
throw new Error('Invalid response format from API. Expected an array.');
|
|
56
58
|
}
|
|
57
59
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
|
|
99
|
+
await Promise.all(batchPromises);
|
|
100
|
+
|
|
101
|
+
// --- END MODIFICATION ---
|
|
98
102
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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.
|
|
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.
|
|
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": "
|
|
39
|
+
"p-limit": "^3.1.0"
|
|
39
40
|
},
|
|
40
41
|
"engines": {
|
|
41
42
|
"node": ">=20"
|