bulltrackers-module 1.0.104 → 1.0.106

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 (33) hide show
  1. package/README.MD +222 -222
  2. package/functions/appscript-api/helpers/errors.js +19 -19
  3. package/functions/appscript-api/index.js +58 -58
  4. package/functions/computation-system/helpers/orchestration_helpers.js +647 -113
  5. package/functions/computation-system/utils/data_loader.js +191 -191
  6. package/functions/computation-system/utils/utils.js +149 -254
  7. package/functions/core/utils/firestore_utils.js +433 -433
  8. package/functions/core/utils/pubsub_utils.js +53 -53
  9. package/functions/dispatcher/helpers/dispatch_helpers.js +47 -47
  10. package/functions/dispatcher/index.js +52 -52
  11. package/functions/etoro-price-fetcher/helpers/handler_helpers.js +124 -124
  12. package/functions/fetch-insights/helpers/handler_helpers.js +91 -91
  13. package/functions/generic-api/helpers/api_helpers.js +379 -379
  14. package/functions/generic-api/index.js +150 -150
  15. package/functions/invalid-speculator-handler/helpers/handler_helpers.js +75 -75
  16. package/functions/orchestrator/helpers/discovery_helpers.js +226 -226
  17. package/functions/orchestrator/helpers/update_helpers.js +92 -92
  18. package/functions/orchestrator/index.js +147 -147
  19. package/functions/price-backfill/helpers/handler_helpers.js +116 -123
  20. package/functions/social-orchestrator/helpers/orchestrator_helpers.js +61 -61
  21. package/functions/social-task-handler/helpers/handler_helpers.js +288 -288
  22. package/functions/task-engine/handler_creator.js +78 -78
  23. package/functions/task-engine/helpers/discover_helpers.js +125 -125
  24. package/functions/task-engine/helpers/update_helpers.js +118 -118
  25. package/functions/task-engine/helpers/verify_helpers.js +162 -162
  26. package/functions/task-engine/utils/firestore_batch_manager.js +258 -258
  27. package/index.js +105 -113
  28. package/package.json +45 -45
  29. package/functions/computation-system/computation_dependencies.json +0 -120
  30. package/functions/computation-system/helpers/worker_helpers.js +0 -340
  31. package/functions/computation-system/utils/computation_state_manager.js +0 -178
  32. package/functions/computation-system/utils/dependency_graph.js +0 -191
  33. package/functions/speculator-cleanup-orchestrator/helpers/cleanup_helpers.js +0 -160
@@ -1,148 +1,148 @@
1
- /**
2
- * @fileoverview Main orchestration logic.
3
- * REFACTORED: This file now contains the main pipe functions
4
- * that are called by the Cloud Function entry points.
5
- * They receive all dependencies.
6
- */
7
-
8
- const { checkDiscoveryNeed, getDiscoveryCandidates, dispatchDiscovery } = require('./helpers/discovery_helpers');
9
- const { getUpdateTargets, dispatchUpdates } = require('./helpers/update_helpers');
10
-
11
-
12
- /**
13
- * Main pipe for running the complete discovery orchestration process.
14
- * This is the function the Cloud Function entry point will call.
15
- * @param {object} config - The full orchestratorConfig object.
16
- * @param {object} dependencies - Contains db, pubsub, logger, firestoreUtils.
17
- * @returns {Promise<void>}
18
- */
19
- async function runDiscoveryOrchestrator(config, dependencies) {
20
- const { logger, firestoreUtils } = dependencies;
21
- logger.log('INFO', '🚀 Discovery Orchestrator triggered via module...');
22
-
23
- // Call sub-pipe from core
24
- await firestoreUtils.resetProxyLocks(dependencies, config);
25
-
26
- // Call sub-pipe for 'normal'
27
- // Pass the directly required sub-pipe functions implicitly via runDiscovery call context
28
- await runDiscovery('normal', config.discoveryConfig.normal, config, dependencies);
29
-
30
- // Call sub-pipe for 'speculator'
31
- await runDiscovery('speculator', config.discoveryConfig.speculator, config, dependencies);
32
- }
33
-
34
- /**
35
- * Main pipe for running the complete update orchestration process.
36
- * This is the function the Cloud Function entry point will call.
37
- * @param {object} config - The full orchestratorConfig object.
38
- * @param {object} dependencies - Contains db, pubsub, logger, firestoreUtils, pubsubUtils.
39
- * @returns {Promise<void>}
40
- */
41
- async function runUpdateOrchestrator(config, dependencies) {
42
- const { logger, firestoreUtils } = dependencies;
43
- logger.log('INFO', '🚀 Update Orchestrator triggered via module...');
44
-
45
- // Call sub-pipe from core
46
- await firestoreUtils.resetProxyLocks(dependencies, config);
47
-
48
- // Call sub-pipe for 'normal'
49
- await runUpdates('normal', config.updateConfig, config, dependencies);
50
-
51
- // Call sub-pipe for 'speculator'
52
- await runUpdates('speculator', config.updateConfig, config, dependencies);
53
- }
54
-
55
- // --- Internal Helpers (which are also exposed as sub-pipes) ---
56
-
57
- /**
58
- * Runs the discovery process for a *single* user type.
59
- * This is an internal helper, but also maps to pipe.orchestrator.runDiscovery
60
- * @param {string} userType - 'normal' or 'speculator'.
61
- * @param {object} userTypeConfig - The specific config slice (e.g., config.discoveryConfig.normal).
62
- * @param {object} globalConfig - Global config.
63
- * @param {object} dependencies - Contains db, pubsub, logger, firestoreUtils, pubsubUtils.
64
- */
65
- async function runDiscovery(userType, userTypeConfig, globalConfig, dependencies) {
66
- const { logger } = dependencies;
67
- logger.log('INFO', `[Module Orchestrator] Starting discovery for ${userType} users...`);
68
-
69
- // --- 3. Use the directly required functions ---
70
- // 1. pipe.orchestrator.checkDiscoveryNeed -> becomes checkDiscoveryNeed
71
- const { needsDiscovery, blocksToFill } = await checkDiscoveryNeed(
72
- userType,
73
- userTypeConfig,
74
- dependencies
75
- );
76
-
77
- if (!needsDiscovery) {
78
- logger.log('INFO', `[Module Orchestrator] No discovery needed for ${userType}.`);
79
- return;
80
- }
81
-
82
- // 2. pipe.orchestrator.getDiscoveryCandidates -> becomes getDiscoveryCandidates
83
- const candidates = await getDiscoveryCandidates(
84
- userType,
85
- blocksToFill,
86
- userTypeConfig,
87
- dependencies
88
- );
89
-
90
- // 3. pipe.orchestrator.dispatchDiscovery -> becomes dispatchDiscovery
91
- await dispatchDiscovery(
92
- userType,
93
- candidates,
94
- userTypeConfig,
95
- dependencies
96
- );
97
-
98
- logger.log('SUCCESS', `[Module Orchestrator] Dispatched discovery tasks for ${userType}.`);
99
- }
100
-
101
- /**
102
- * Runs the update process for a *single* user type.
103
- * This is an internal helper, but also maps to pipe.orchestrator.runUpdates
104
- * @param {string} userType - 'normal' or 'speculator'.
105
- * @param {object} updateConfig - The config.updateConfig object.
106
- * @param {object} globalConfig - Global config.
107
- * @param {object} dependencies - Contains db, pubsub, logger, firestoreUtils, pubsubUtils.
108
- */
109
- async function runUpdates(userType, updateConfig, globalConfig, dependencies) {
110
- const { logger } = dependencies;
111
- logger.log('INFO', `[Module Orchestrator] Collecting users for daily update (${userType})...`);
112
-
113
- const now = new Date();
114
- const startOfTodayUTC = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate()));
115
- const DaysAgoUTC = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000);
116
-
117
- const thresholds = {
118
- dateThreshold: startOfTodayUTC,
119
- gracePeriodThreshold: DaysAgoUTC
120
- };
121
-
122
- // --- 4. Use the directly required functions ---
123
- // 1. pipe.orchestrator.getUpdateTargets -> becomes getUpdateTargets
124
- const targets = await getUpdateTargets(
125
- userType,
126
- thresholds,
127
- updateConfig,
128
- dependencies
129
- );
130
-
131
- // 2. pipe.orchestrator.dispatchUpdates -> becomes dispatchUpdates
132
- await dispatchUpdates(
133
- targets,
134
- userType,
135
- updateConfig,
136
- dependencies
137
- );
138
-
139
- logger.log('SUCCESS', `[Module Orchestrator] Dispatched update tasks for ${userType}.`);
140
- }
141
-
142
- module.exports = {
143
- runDiscoveryOrchestrator,
144
- runUpdateOrchestrator,
145
- // Exporting these internal helpers so the main index.js can map them
146
- runDiscovery,
147
- runUpdates
1
+ /**
2
+ * @fileoverview Main orchestration logic.
3
+ * REFACTORED: This file now contains the main pipe functions
4
+ * that are called by the Cloud Function entry points.
5
+ * They receive all dependencies.
6
+ */
7
+
8
+ const { checkDiscoveryNeed, getDiscoveryCandidates, dispatchDiscovery } = require('./helpers/discovery_helpers');
9
+ const { getUpdateTargets, dispatchUpdates } = require('./helpers/update_helpers');
10
+
11
+
12
+ /**
13
+ * Main pipe for running the complete discovery orchestration process.
14
+ * This is the function the Cloud Function entry point will call.
15
+ * @param {object} config - The full orchestratorConfig object.
16
+ * @param {object} dependencies - Contains db, pubsub, logger, firestoreUtils.
17
+ * @returns {Promise<void>}
18
+ */
19
+ async function runDiscoveryOrchestrator(config, dependencies) {
20
+ const { logger, firestoreUtils } = dependencies;
21
+ logger.log('INFO', '🚀 Discovery Orchestrator triggered via module...');
22
+
23
+ // Call sub-pipe from core
24
+ await firestoreUtils.resetProxyLocks(dependencies, config);
25
+
26
+ // Call sub-pipe for 'normal'
27
+ // Pass the directly required sub-pipe functions implicitly via runDiscovery call context
28
+ await runDiscovery('normal', config.discoveryConfig.normal, config, dependencies);
29
+
30
+ // Call sub-pipe for 'speculator'
31
+ await runDiscovery('speculator', config.discoveryConfig.speculator, config, dependencies);
32
+ }
33
+
34
+ /**
35
+ * Main pipe for running the complete update orchestration process.
36
+ * This is the function the Cloud Function entry point will call.
37
+ * @param {object} config - The full orchestratorConfig object.
38
+ * @param {object} dependencies - Contains db, pubsub, logger, firestoreUtils, pubsubUtils.
39
+ * @returns {Promise<void>}
40
+ */
41
+ async function runUpdateOrchestrator(config, dependencies) {
42
+ const { logger, firestoreUtils } = dependencies;
43
+ logger.log('INFO', '🚀 Update Orchestrator triggered via module...');
44
+
45
+ // Call sub-pipe from core
46
+ await firestoreUtils.resetProxyLocks(dependencies, config);
47
+
48
+ // Call sub-pipe for 'normal'
49
+ await runUpdates('normal', config.updateConfig, config, dependencies);
50
+
51
+ // Call sub-pipe for 'speculator'
52
+ await runUpdates('speculator', config.updateConfig, config, dependencies);
53
+ }
54
+
55
+ // --- Internal Helpers (which are also exposed as sub-pipes) ---
56
+
57
+ /**
58
+ * Runs the discovery process for a *single* user type.
59
+ * This is an internal helper, but also maps to pipe.orchestrator.runDiscovery
60
+ * @param {string} userType - 'normal' or 'speculator'.
61
+ * @param {object} userTypeConfig - The specific config slice (e.g., config.discoveryConfig.normal).
62
+ * @param {object} globalConfig - Global config.
63
+ * @param {object} dependencies - Contains db, pubsub, logger, firestoreUtils, pubsubUtils.
64
+ */
65
+ async function runDiscovery(userType, userTypeConfig, globalConfig, dependencies) {
66
+ const { logger } = dependencies;
67
+ logger.log('INFO', `[Module Orchestrator] Starting discovery for ${userType} users...`);
68
+
69
+ // --- 3. Use the directly required functions ---
70
+ // 1. pipe.orchestrator.checkDiscoveryNeed -> becomes checkDiscoveryNeed
71
+ const { needsDiscovery, blocksToFill } = await checkDiscoveryNeed(
72
+ userType,
73
+ userTypeConfig,
74
+ dependencies
75
+ );
76
+
77
+ if (!needsDiscovery) {
78
+ logger.log('INFO', `[Module Orchestrator] No discovery needed for ${userType}.`);
79
+ return;
80
+ }
81
+
82
+ // 2. pipe.orchestrator.getDiscoveryCandidates -> becomes getDiscoveryCandidates
83
+ const candidates = await getDiscoveryCandidates(
84
+ userType,
85
+ blocksToFill,
86
+ userTypeConfig,
87
+ dependencies
88
+ );
89
+
90
+ // 3. pipe.orchestrator.dispatchDiscovery -> becomes dispatchDiscovery
91
+ await dispatchDiscovery(
92
+ userType,
93
+ candidates,
94
+ userTypeConfig,
95
+ dependencies
96
+ );
97
+
98
+ logger.log('SUCCESS', `[Module Orchestrator] Dispatched discovery tasks for ${userType}.`);
99
+ }
100
+
101
+ /**
102
+ * Runs the update process for a *single* user type.
103
+ * This is an internal helper, but also maps to pipe.orchestrator.runUpdates
104
+ * @param {string} userType - 'normal' or 'speculator'.
105
+ * @param {object} updateConfig - The config.updateConfig object.
106
+ * @param {object} globalConfig - Global config.
107
+ * @param {object} dependencies - Contains db, pubsub, logger, firestoreUtils, pubsubUtils.
108
+ */
109
+ async function runUpdates(userType, updateConfig, globalConfig, dependencies) {
110
+ const { logger } = dependencies;
111
+ logger.log('INFO', `[Module Orchestrator] Collecting users for daily update (${userType})...`);
112
+
113
+ const now = new Date();
114
+ const startOfTodayUTC = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate()));
115
+ const DaysAgoUTC = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000);
116
+
117
+ const thresholds = {
118
+ dateThreshold: startOfTodayUTC,
119
+ gracePeriodThreshold: DaysAgoUTC
120
+ };
121
+
122
+ // --- 4. Use the directly required functions ---
123
+ // 1. pipe.orchestrator.getUpdateTargets -> becomes getUpdateTargets
124
+ const targets = await getUpdateTargets(
125
+ userType,
126
+ thresholds,
127
+ updateConfig,
128
+ dependencies
129
+ );
130
+
131
+ // 2. pipe.orchestrator.dispatchUpdates -> becomes dispatchUpdates
132
+ await dispatchUpdates(
133
+ targets,
134
+ userType,
135
+ updateConfig,
136
+ dependencies
137
+ );
138
+
139
+ logger.log('SUCCESS', `[Module Orchestrator] Dispatched update tasks for ${userType}.`);
140
+ }
141
+
142
+ module.exports = {
143
+ runDiscoveryOrchestrator,
144
+ runUpdateOrchestrator,
145
+ // Exporting these internal helpers so the main index.js can map them
146
+ runDiscovery,
147
+ runUpdates
148
148
  };
@@ -1,123 +1,116 @@
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'); // Todo inject this
8
- const pLimit = require('p-limit'); // TODO inject this
9
-
10
- // How many tickers to fetch in parallel
11
- const CONCURRENT_REQUESTS = 10;
12
- // How many days of history to fetch
13
- const DAYS_TO_FETCH = 365;
14
- // How many tickers to group into one Firestore document
15
- const SHARD_SIZE = 40;
16
-
17
- // EXP BACKOFF
18
- async function fetchWithExponentialBackoff(fn, maxAttempts = 8) {
19
- let attempt = 0;
20
- while (attempt < maxAttempts) {
21
- try {
22
- return await fn();
23
- } catch (err) {
24
- attempt++;
25
- if (attempt >= maxAttempts) throw err;
26
- const delay = Math.min(1000 * Math.pow(2, attempt), 60000); // capped 60s
27
- await new Promise(res => setTimeout(res, delay));
28
- }
29
- }
30
- }
31
-
32
- /**
33
- * Main pipe: pipe.maintenance.runBackfillAssetPrices
34
- * @param {object} config - Configuration object.
35
- * @param {object} dependencies - Contains db, logger, headerManager, proxyManager, calculationUtils.
36
- */
37
-
38
- exports.runBackfillAssetPrices = async (config, dependencies) => {
39
- const { db, logger, headerManager, proxyManager, calculationUtils } = dependencies;
40
- const { loadInstrumentMappings } = calculationUtils;
41
-
42
- logger.log('INFO', '[PriceBackfill] Starting historical price backfill...');
43
-
44
- let mappings;
45
- try {
46
- mappings = await loadInstrumentMappings();
47
- if (!mappings || !mappings.instrumentToTicker) {
48
- throw new Error("Failed to load instrument mappings.");
49
- }
50
- } catch (e) {
51
- logger.log('ERROR', '[PriceBackfill] Could not load instrument mappings.', { err: e.message });
52
- return;
53
- }
54
-
55
- const instrumentIds = Object.keys(mappings.instrumentToTicker);
56
- logger.log('INFO', `[PriceBackfill] Found ${instrumentIds.length} instruments to backfill.`);
57
-
58
- const limit = pLimit(CONCURRENT_REQUESTS);
59
- let successCount = 0;
60
- let errorCount = 0;
61
-
62
- const promises = instrumentIds.map(instrumentId => {
63
- return limit(async () => {
64
- try {
65
- const ticker = mappings.instrumentToTicker[instrumentId] || `unknown_${instrumentId}`;
66
- const url = `https://candle.etoro.com/candles/asc.json/OneDay/${DAYS_TO_FETCH}/${instrumentId}`;
67
-
68
- const selectedHeader = await headerManager.selectHeader();
69
-
70
- const response = await fetchWithExponentialBackoff(() =>
71
- proxyManager.fetch(url, {
72
- headers: selectedHeader.header,
73
- timeout: 20000
74
- })
75
- );
76
-
77
- if (!response.ok) {
78
- throw new Error(`API error ${response.status} for instrument ${instrumentId}`);
79
- }
80
-
81
- headerManager.updatePerformance(selectedHeader.id, true);
82
-
83
- const data = await response.json();
84
- const candles = data?.Candles?.[0]?.Candles;
85
-
86
- if (!Array.isArray(candles) || candles.length === 0) {
87
- logger.log('WARN', `[PriceBackfill] No candle data returned for ${ticker} (${instrumentId})`);
88
- return;
89
- }
90
-
91
- const prices = {};
92
- for (const candle of candles) {
93
- const dateKey = candle.FromDate.substring(0, 10);
94
- prices[dateKey] = candle.Close;
95
- }
96
-
97
- const shardId = `shard_${parseInt(instrumentId, 10) % SHARD_SIZE}`;
98
- const docRef = db.collection('asset_prices').doc(shardId);
99
-
100
- const payload = {
101
- [instrumentId]: {
102
- ticker: ticker,
103
- prices: prices,
104
- lastUpdated: FieldValue.serverTimestamp()
105
- }
106
- };
107
-
108
- await docRef.set(payload, { merge: true });
109
- logger.log('TRACE', `[PriceBackfill] Successfully stored data for ${ticker} (${instrumentId}) in ${shardId}`);
110
- successCount++;
111
-
112
- } catch (err) {
113
- logger.log('ERROR', `[PriceBackfill] Failed to process instrument ${instrumentId}`, { err: err.message });
114
- errorCount++;
115
- }
116
- });
117
- });
118
-
119
- await Promise.all(promises);
120
- await headerManager.flushPerformanceUpdates();
121
-
122
- logger.log('SUCCESS', `[PriceBackfill] Backfill complete. Success: ${successCount}, Failed: ${errorCount}`);
123
- };
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'); // Todo inject this
8
+
9
+ const pLimit = require('p-limit'); // TODO inject this
10
+
11
+ // How many tickers to fetch in parallel
12
+ const CONCURRENT_REQUESTS = 10;
13
+ // How many days of history to fetch
14
+ const DAYS_TO_FETCH = 365;
15
+ // How many tickers to group into one Firestore document
16
+ const SHARD_SIZE = 40;
17
+
18
+ /**
19
+ * Main pipe: pipe.maintenance.runBackfillAssetPrices
20
+ * @param {object} config - Configuration object.
21
+ * @param {object} dependencies - Contains db, logger, headerManager, proxyManager, calculationUtils.
22
+ */
23
+ // --- MODIFIED: Removed calculationUtils, as it's in dependencies ---
24
+ exports.runBackfillAssetPrices = async (config, dependencies) => {
25
+ const { db, logger, headerManager, proxyManager, calculationUtils } = dependencies;
26
+ const { loadInstrumentMappings } = calculationUtils; // <-- Get function from dependencies
27
+ // --- END MODIFIED ---
28
+
29
+ logger.log('INFO', '[PriceBackfill] Starting historical price backfill...');
30
+
31
+ let mappings;
32
+ try {
33
+ // --- MODIFIED: Use the injected utils ---
34
+ mappings = await loadInstrumentMappings();
35
+ // --- END MODIFIED ---
36
+ if (!mappings || !mappings.instrumentToTicker) {
37
+ throw new Error("Failed to load instrument mappings.");
38
+ }
39
+ } catch (e) {
40
+ logger.log('ERROR', '[PriceBackfill] Could not load instrument mappings.', { err: e.message });
41
+ return;
42
+ }
43
+
44
+ const instrumentIds = Object.keys(mappings.instrumentToTicker);
45
+ logger.log('INFO', `[PriceBackfill] Found ${instrumentIds.length} instruments to backfill.`);
46
+
47
+ const limit = pLimit(CONCURRENT_REQUESTS);
48
+ let successCount = 0;
49
+ let errorCount = 0;
50
+
51
+ const promises = instrumentIds.map(instrumentId => {
52
+ return limit(async () => {
53
+ try {
54
+ const ticker = mappings.instrumentToTicker[instrumentId] || `unknown_${instrumentId}`;
55
+ const url = `https://candle.etoro.com/candles/asc.json/OneDay/${DAYS_TO_FETCH}/${instrumentId}`; //TODO implement config value
56
+
57
+ const selectedHeader = await headerManager.selectHeader();
58
+ let wasSuccess = false;
59
+
60
+ const response = await proxyManager.fetch(url, {
61
+ headers: selectedHeader.header,
62
+ timeout: 20000
63
+ });
64
+
65
+ if (!response.ok) {
66
+ throw new Error(`API error ${response.status} for instrument ${instrumentId}`);
67
+ }
68
+ wasSuccess = true;
69
+ headerManager.updatePerformance(selectedHeader.id, wasSuccess);
70
+
71
+ const data = await response.json();
72
+ const candles = data?.Candles?.[0]?.Candles;
73
+
74
+ if (!Array.isArray(candles) || candles.length === 0) {
75
+ logger.log('WARN', `[PriceBackfill] No candle data returned for ${ticker} (${instrumentId})`);
76
+ return;
77
+ }
78
+
79
+ // Format data as a map
80
+ const prices = {};
81
+ for (const candle of candles) {
82
+ const dateKey = candle.FromDate.substring(0, 10);
83
+ prices[dateKey] = candle.Close;
84
+ }
85
+
86
+ // Determine shard ID
87
+ const shardId = `shard_${parseInt(instrumentId, 10) % SHARD_SIZE}`;
88
+ const docRef = db.collection('asset_prices').doc(shardId); // TODO implement config value
89
+
90
+ const payload = {
91
+ [instrumentId]: {
92
+ ticker: ticker,
93
+ prices: prices,
94
+ lastUpdated: FieldValue.serverTimestamp()
95
+ }
96
+ };
97
+
98
+ // Write to Firestore
99
+ await docRef.set(payload, { merge: true });
100
+ logger.log('TRACE', `[PriceBackfill] Successfully stored data for ${ticker} (${instrumentId}) in ${shardId}`);
101
+ successCount++;
102
+
103
+ } catch (err) {
104
+ logger.log('ERROR', `[PriceBackfill] Failed to process instrument ${instrumentId}`, { err: err.message });
105
+ errorCount++;
106
+ }
107
+ });
108
+ });
109
+
110
+ await Promise.all(promises);
111
+
112
+ // Flush any remaining header updates
113
+ await headerManager.flushPerformanceUpdates();
114
+
115
+ logger.log('SUCCESS', `[PriceBackfill] Backfill complete. Success: ${successCount}, Failed: ${errorCount}`);
116
+ };