bulltrackers-module 1.0.658 → 1.0.659

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 (22) hide show
  1. package/functions/computation-system/data/AvailabilityChecker.js +163 -317
  2. package/functions/computation-system/data/CachedDataLoader.js +158 -222
  3. package/functions/computation-system/data/DependencyFetcher.js +201 -406
  4. package/functions/computation-system/executors/MetaExecutor.js +176 -280
  5. package/functions/computation-system/executors/StandardExecutor.js +325 -383
  6. package/functions/computation-system/helpers/computation_dispatcher.js +294 -699
  7. package/functions/computation-system/helpers/computation_worker.js +3 -2
  8. package/functions/computation-system/legacy/AvailabilityCheckerOld.js +382 -0
  9. package/functions/computation-system/legacy/CachedDataLoaderOld.js +357 -0
  10. package/functions/computation-system/legacy/DependencyFetcherOld.js +478 -0
  11. package/functions/computation-system/legacy/MetaExecutorold.js +364 -0
  12. package/functions/computation-system/legacy/StandardExecutorold.js +476 -0
  13. package/functions/computation-system/legacy/computation_dispatcherold.js +944 -0
  14. package/functions/computation-system/persistence/ResultCommitter.js +137 -188
  15. package/functions/computation-system/services/SnapshotService.js +129 -0
  16. package/functions/computation-system/tools/BuildReporter.js +12 -7
  17. package/functions/computation-system/utils/data_loader.js +213 -238
  18. package/package.json +3 -2
  19. package/functions/computation-system/workflows/bulltrackers_pipeline.yaml +0 -163
  20. package/functions/computation-system/workflows/data_feeder_pipeline.yaml +0 -115
  21. package/functions/computation-system/workflows/datafeederpipelineinstructions.md +0 -30
  22. package/functions/computation-system/workflows/morning_prep_pipeline.yaml +0 -55
@@ -3,6 +3,7 @@
3
3
  * type: uploaded file
4
4
  * fileName: computation-system/data/CachedDataLoader.js
5
5
  * }
6
+ * REFACTORED: Unified Loader Configuration for DRY principles.
6
7
  */
7
8
  const {
8
9
  loadDailyInsights,
@@ -21,153 +22,153 @@ const {
21
22
  const { getAvailabilityWindow } = require('./AvailabilityChecker');
22
23
  const zlib = require('zlib');
23
24
 
24
- // [NEW] Mapping of Loader Methods to Availability Flags
25
- const LOADER_DEPENDENCY_MAP = {
26
- 'loadRankings': 'piRankings',
27
- 'loadRatings': 'piRatings',
28
- 'loadPageViews': 'piPageViews',
29
- 'loadWatchlistMembership': 'watchlistMembership',
30
- 'loadAlertHistory': 'piAlertHistory',
31
- 'loadInsights': 'hasInsights',
32
- 'loadSocial': 'hasSocial'
33
- };
34
-
35
- // [NEW] Mapping of Loader Methods to Collection Config Keys / Defaults
36
- // This allows us to construct references for Batch Reading without executing the opaque loader functions.
37
- const LOADER_COLLECTION_MAP = {
38
- 'loadRatings': { configKey: 'piRatingsCollection', default: 'PIRatingsData' },
39
- 'loadPageViews': { configKey: 'piPageViewsCollection', default: 'PIPageViewsData' },
40
- 'loadWatchlistMembership': { configKey: 'watchlistMembershipCollection', default: 'WatchlistMembershipData' },
41
- 'loadAlertHistory': { configKey: 'piAlertHistoryCollection', default: 'PIAlertHistoryData' },
42
- 'loadInsights': { configKey: 'insightsCollectionName', default: 'daily_instrument_insights' },
43
- 'loadRankings': { configKey: 'popularInvestorRankingsCollection', default: 'popular_investor_rankings' }
25
+ // =============================================================================
26
+ // CONFIGURATION: Unified Loader Definitions
27
+ // =============================================================================
28
+ // Centralizes config keys, defaults, loader functions, and availability flags.
29
+ const LOADER_DEFINITIONS = {
30
+ loadRankings: {
31
+ cache: 'rankings',
32
+ configKey: 'popularInvestorRankingsCollection',
33
+ defaultCol: 'popular_investor_rankings',
34
+ fn: loadPopularInvestorRankings,
35
+ flag: 'piRankings'
36
+ },
37
+ loadRatings: {
38
+ cache: 'ratings',
39
+ configKey: 'piRatingsCollection',
40
+ defaultCol: 'PIRatingsData',
41
+ fn: loadPIRatings,
42
+ flag: 'piRatings'
43
+ },
44
+ loadPageViews: {
45
+ cache: 'pageViews',
46
+ configKey: 'piPageViewsCollection',
47
+ defaultCol: 'PIPageViewsData',
48
+ fn: loadPIPageViews,
49
+ flag: 'piPageViews'
50
+ },
51
+ loadWatchlistMembership: {
52
+ cache: 'watchlistMembership',
53
+ configKey: 'watchlistMembershipCollection',
54
+ defaultCol: 'WatchlistMembershipData',
55
+ fn: loadWatchlistMembershipData,
56
+ flag: 'watchlistMembership'
57
+ },
58
+ loadAlertHistory: {
59
+ cache: 'alertHistory',
60
+ configKey: 'piAlertHistoryCollection',
61
+ defaultCol: 'PIAlertHistoryData',
62
+ fn: loadPIAlertHistory,
63
+ flag: 'piAlertHistory'
64
+ },
65
+ loadInsights: {
66
+ cache: 'insights',
67
+ configKey: 'insightsCollectionName',
68
+ defaultCol: 'daily_instrument_insights',
69
+ fn: loadDailyInsights,
70
+ flag: 'hasInsights'
71
+ },
72
+ loadSocial: {
73
+ cache: 'social',
74
+ configKey: 'socialInsightsCollection',
75
+ defaultCol: 'daily_social_insights',
76
+ fn: loadDailySocialPostInsights,
77
+ flag: 'hasSocial'
78
+ },
79
+ loadPIWatchlistData: {
80
+ cache: 'piWatchlistData',
81
+ // No collection key needed for direct implementation, but handled by fn
82
+ fn: loadPIWatchlistData,
83
+ isIdBased: true // Uses ID instead of Date
84
+ }
44
85
  };
45
86
 
46
87
  class CachedDataLoader {
47
88
  constructor(config, dependencies) {
48
89
  this.config = config;
49
90
  this.deps = dependencies;
91
+
92
+ // Initialize caches dynamically based on definitions + static extras
50
93
  this.cache = {
51
94
  mappings: null,
52
- insights: new Map(),
53
- social: new Map(),
54
- verifications: null,
55
- rankings: new Map(),
56
- ratings: new Map(),
57
- pageViews: new Map(),
58
- watchlistMembership: new Map(),
59
- alertHistory: new Map(),
60
- piWatchlistData: new Map(),
61
- piMasterList: null
95
+ verifications: null,
96
+ piMasterList: null,
97
+ ...Object.values(LOADER_DEFINITIONS).reduce((acc, def) => {
98
+ acc[def.cache] = new Map();
99
+ return acc;
100
+ }, {})
62
101
  };
63
102
  }
64
103
 
65
104
  _tryDecompress(data) {
66
- if (data && data._compressed === true && data.payload) {
67
- try {
68
- return JSON.parse(zlib.gunzipSync(data.payload).toString());
69
- } catch (e) {
70
- console.error('[CachedDataLoader] Decompression failed', e);
71
- return {};
72
- }
105
+ if (data?._compressed === true && data.payload) {
106
+ try { return JSON.parse(zlib.gunzipSync(data.payload).toString()); }
107
+ catch (e) { console.error('[CachedDataLoader] Decompression failed', e); return {}; }
73
108
  }
74
109
  return data;
75
110
  }
76
111
 
77
- // ... [Existing single-day load methods remain unchanged] ...
78
- async loadMappings() {
79
- if (this.cache.mappings) return this.cache.mappings;
80
- const { calculationUtils } = this.deps;
81
- this.cache.mappings = await calculationUtils.loadInstrumentMappings();
82
- return this.cache.mappings;
83
- }
112
+ // =========================================================================
113
+ // GENERIC LOADER HELPER
114
+ // =========================================================================
115
+ async _loadGeneric(methodName, key) {
116
+ const def = LOADER_DEFINITIONS[methodName];
117
+ if (!def) throw new Error(`Unknown loader method: ${methodName}`);
84
118
 
85
- async loadInsights(dateStr) {
86
- if (this.cache.insights.has(dateStr)) return this.cache.insights.get(dateStr);
87
- const collectionName = this.config.insightsCollectionName || 'daily_instrument_insights';
88
- const path = `${collectionName}/${dateStr}`;
89
- this.deps.logger?.log('INFO', `[CachedDataLoader] 📂 Loading Root Data 'insights' from: ${path}`);
90
- const promise = loadDailyInsights(this.config, this.deps, dateStr);
91
- this.cache.insights.set(dateStr, promise);
92
- return promise;
93
- }
119
+ const cacheMap = this.cache[def.cache];
120
+ if (cacheMap.has(key)) return cacheMap.get(key);
94
121
 
95
- async loadSocial(dateStr) {
96
- if (this.cache.social.has(dateStr)) return this.cache.social.get(dateStr);
97
- const collectionName = this.config.socialInsightsCollection || 'daily_social_insights';
98
- const path = `${collectionName}/${dateStr}`;
99
- this.deps.logger?.log('INFO', `[CachedDataLoader] 📂 Loading Root Data 'social' from: ${path}`);
100
- const promise = loadDailySocialPostInsights(this.config, this.deps, dateStr);
101
- this.cache.social.set(dateStr, promise);
102
- return promise;
103
- }
104
-
105
- async loadVerifications() {
106
- if (this.cache.verifications) return this.cache.verifications;
107
- const collectionName = this.config.verificationsCollection || 'verification_profiles';
108
- const path = `${collectionName}`;
109
- this.deps.logger?.log('INFO', `[CachedDataLoader] 📂 Loading Root Data 'verifications' from: ${path}`);
110
- const verifications = await loadVerificationProfiles(this.config, this.deps);
111
- this.cache.verifications = verifications;
112
- return verifications;
113
- }
122
+ const collection = this.config[def.configKey] || def.defaultCol;
123
+ // Only log if we have a collection context (some ID-based loaders might differ)
124
+ if (def.configKey) {
125
+ this.deps.logger?.log('INFO', `[CachedDataLoader] 📂 Loading '${def.cache}' from: ${collection}/${key}`);
126
+ }
114
127
 
115
- async loadRankings(dateStr) {
116
- if (this.cache.rankings.has(dateStr)) return this.cache.rankings.get(dateStr);
117
- const collectionName = this.config.popularInvestorRankingsCollection || 'popular_investor_rankings';
118
- const path = `${collectionName}/${dateStr}`;
119
- this.deps.logger?.log('INFO', `[CachedDataLoader] 📂 Loading Root Data 'rankings' from: ${path}`);
120
- const promise = loadPopularInvestorRankings(this.config, this.deps, dateStr);
121
- this.cache.rankings.set(dateStr, promise);
128
+ const promise = def.fn(this.config, this.deps, key);
129
+ cacheMap.set(key, promise);
122
130
  return promise;
123
131
  }
124
132
 
125
- async loadRatings(dateStr) {
126
- if (this.cache.ratings.has(dateStr)) return this.cache.ratings.get(dateStr);
127
- const collectionName = this.config.piRatingsCollection || 'PIRatingsData';
128
- const path = `${collectionName}/${dateStr}`;
129
- this.deps.logger?.log('INFO', `[CachedDataLoader] 📂 Loading Root Data 'ratings' from: ${path}`);
130
- const promise = loadPIRatings(this.config, this.deps, dateStr);
131
- this.cache.ratings.set(dateStr, promise);
132
- return promise;
133
- }
133
+ // =========================================================================
134
+ // PUBLIC ACCESSORS (Generated Wrappers)
135
+ // =========================================================================
136
+
137
+ // Explicitly defined for IDE autocompletion / static analysis,
138
+ // but internally they all delegate to _loadGeneric.
139
+ async loadInsights(dateStr) { return this._loadGeneric('loadInsights' , dateStr); }
140
+ async loadSocial(dateStr) { return this._loadGeneric('loadSocial' , dateStr); }
141
+ async loadRankings(dateStr) { return this._loadGeneric('loadRankings' , dateStr); }
142
+ async loadRatings(dateStr) { return this._loadGeneric('loadRatings' , dateStr); }
143
+ async loadPageViews(dateStr) { return this._loadGeneric('loadPageViews' , dateStr); }
144
+ async loadWatchlistMembership(dateStr) { return this._loadGeneric('loadWatchlistMembership', dateStr); }
145
+ async loadAlertHistory(dateStr) { return this._loadGeneric('loadAlertHistory' , dateStr); }
146
+ async loadPIWatchlistData(piCid) { return this._loadGeneric('loadPIWatchlistData' , String(piCid)); }
147
+
148
+ // =========================================================================
149
+ // SPECIALIZED LOADERS (Non-Standard Patterns)
150
+ // =========================================================================
134
151
 
135
- async loadPageViews(dateStr) {
136
- if (this.cache.pageViews.has(dateStr)) return this.cache.pageViews.get(dateStr);
137
- const collectionName = this.config.piPageViewsCollection || 'PIPageViewsData';
138
- const path = `${collectionName}/${dateStr}`;
139
- this.deps.logger?.log('INFO', `[CachedDataLoader] 📂 Loading Root Data 'pageViews' from: ${path}`);
140
- const promise = loadPIPageViews(this.config, this.deps, dateStr);
141
- this.cache.pageViews.set(dateStr, promise);
142
- return promise;
143
- }
144
-
145
- async loadWatchlistMembership(dateStr) {
146
- if (this.cache.watchlistMembership.has(dateStr)) return this.cache.watchlistMembership.get(dateStr);
147
- const collectionName = this.config.watchlistMembershipCollection || 'WatchlistMembershipData';
148
- const path = `${collectionName}/${dateStr}`;
149
- this.deps.logger?.log('INFO', `[CachedDataLoader] 📂 Loading Root Data 'watchlistMembership' from: ${path}`);
150
- const promise = loadWatchlistMembershipData(this.config, this.deps, dateStr);
151
- this.cache.watchlistMembership.set(dateStr, promise);
152
- return promise;
152
+ async loadMappings() {
153
+ if (this.cache.mappings) return this.cache.mappings;
154
+ this.cache.mappings = await this.deps.calculationUtils.loadInstrumentMappings();
155
+ return this.cache.mappings;
153
156
  }
154
157
 
155
- async loadAlertHistory(dateStr) {
156
- if (this.cache.alertHistory.has(dateStr)) return this.cache.alertHistory.get(dateStr);
157
- const collectionName = this.config.piAlertHistoryCollection || 'PIAlertHistoryData';
158
- const path = `${collectionName}/${dateStr}`;
159
- this.deps.logger?.log('INFO', `[CachedDataLoader] 📂 Loading Root Data 'alertHistory' from: ${path}`);
160
- const promise = loadPIAlertHistory(this.config, this.deps, dateStr);
161
- this.cache.alertHistory.set(dateStr, promise);
162
- return promise;
158
+ async loadVerifications(dateStr) { // <--- Added dateStr arg
159
+ if (this.cache.verifications) return this.cache.verifications;
160
+
161
+ const col = this.config.verificationsCollection || 'verification_profiles';
162
+ this.deps.logger?.log('INFO', `[CachedDataLoader] 📂 Loading 'verifications' from: ${col} (Context: ${dateStr || 'Global'})`);
163
+
164
+ // Pass dateStr so data_loader can check GCS snapshots
165
+ this.cache.verifications = await loadVerificationProfiles(this.config, this.deps, dateStr);
166
+ return this.cache.verifications;
163
167
  }
164
-
165
- async loadPIWatchlistData(piCid) {
166
- const piCidStr = String(piCid);
167
- if (this.cache.piWatchlistData.has(piCidStr)) return this.cache.piWatchlistData.get(piCidStr);
168
- const promise = loadPIWatchlistData(this.config, this.deps, piCidStr);
169
- this.cache.piWatchlistData.set(piCidStr, promise);
170
- return promise;
168
+ async loadPIMasterList() {
169
+ if (this.cache.piMasterList) return this.cache.piMasterList;
170
+ this.cache.piMasterList = await loadPopularInvestorMasterList(this.config, this.deps);
171
+ return this.cache.piMasterList;
171
172
  }
172
173
 
173
174
  async getPriceShardReferences() {
@@ -181,147 +182,92 @@ class CachedDataLoader {
181
182
  async loadPriceShard(docRef) {
182
183
  try {
183
184
  const snap = await docRef.get();
184
- if (!snap.exists) return {};
185
- return this._tryDecompress(snap.data());
185
+ return snap.exists ? this._tryDecompress(snap.data()) : {};
186
186
  } catch (e) {
187
187
  console.error(`Error loading shard ${docRef.path}:`, e);
188
188
  return {};
189
189
  }
190
190
  }
191
191
 
192
- async loadPIMasterList() {
193
- if (this.cache.piMasterList) return this.cache.piMasterList;
194
- const data = await loadPopularInvestorMasterList(this.config, this.deps);
195
- this.cache.piMasterList = data;
196
- return data;
197
- }
198
-
199
- // --- [UPDATED] Batched Series Loading Logic ---
192
+ // =========================================================================
193
+ // BATCH SERIES LOADING
194
+ // =========================================================================
200
195
  /**
201
- * Optimistically loads a series of root data over a lookback period using Batch Reads.
202
- * 1. Checks Availability Index (Range Query).
203
- * 2. Constructs Refs for all existing dates.
204
- * 3. Fetches all in ONE db.getAll() request.
196
+ * Optimistically loads data series using Batch Reads (db.getAll).
197
+ * Uses Availability Index to minimize costs.
205
198
  */
206
199
  async loadSeries(loaderMethod, dateStr, lookbackDays) {
207
- if (!this[loaderMethod]) throw new Error(`[CachedDataLoader] Unknown method ${loaderMethod}`);
200
+ const def = LOADER_DEFINITIONS[loaderMethod];
201
+ if (!def) throw new Error(`[CachedDataLoader] Unknown series method ${loaderMethod}`);
208
202
 
203
+ // Fallback to legacy loop if method isn't configured for batching (missing config/flag)
204
+ if (!def.configKey || !def.flag) return this._loadSeriesLegacy(loaderMethod, dateStr, lookbackDays);
205
+
209
206
  // 1. Calculate Date Range
210
207
  const endDate = new Date(dateStr);
211
208
  const startDate = new Date(endDate);
212
209
  startDate.setUTCDate(startDate.getUTCDate() - (lookbackDays - 1));
213
210
 
214
- const startStr = startDate.toISOString().slice(0, 10);
215
- const endStr = endDate.toISOString().slice(0, 10);
216
-
217
- // 2. Pre-flight: Fetch Availability Window
211
+ // 2. Pre-flight: Check Availability
218
212
  let availabilityMap = new Map();
219
213
  try {
220
- availabilityMap = await getAvailabilityWindow(this.deps, startStr, endStr);
214
+ availabilityMap = await getAvailabilityWindow(this.deps, startDate.toISOString().slice(0, 10), endDate.toISOString().slice(0, 10));
221
215
  } catch (e) {
222
- console.warn(`[CachedDataLoader] Availability check failed for series. Falling back to optimistic batch fetch. Error: ${e.message}`);
223
- }
224
-
225
- // 3. Identify Collection & Required Flag
226
- const collectionInfo = LOADER_COLLECTION_MAP[loaderMethod];
227
- const requiredFlag = LOADER_DEPENDENCY_MAP[loaderMethod];
228
-
229
- if (!collectionInfo) {
230
- // Fallback for methods not in the batch map (use legacy parallel loop)
231
- return this._loadSeriesLegacy(loaderMethod, dateStr, lookbackDays);
216
+ console.warn(`[CachedDataLoader] Availability check failed. Optimistic batching enabled.`);
232
217
  }
233
218
 
234
- const collectionName = this.config[collectionInfo.configKey] || collectionInfo.default;
219
+ // 3. Construct Batch Refs
220
+ const collectionName = this.config[def.configKey] || def.defaultCol;
235
221
  const batchRefs = [];
236
- const dateKeyMap = []; // Keep track of which date corresponds to which ref index
222
+ const dateKeyMap = [];
237
223
 
238
- // 4. Build Batch References
239
224
  for (let i = 0; i < lookbackDays; i++) {
240
225
  const d = new Date(endDate);
241
226
  d.setUTCDate(d.getUTCDate() - i);
242
227
  const dString = d.toISOString().slice(0, 10);
243
228
 
244
- // Check Availability
245
229
  const dayStatus = availabilityMap.get(dString);
246
- let shouldFetch = false;
247
-
248
- if (availabilityMap.size > 0) {
249
- // If index exists, trust it
250
- if (dayStatus && (!requiredFlag || dayStatus[requiredFlag])) {
251
- shouldFetch = true;
252
- }
253
- } else {
254
- // If index check failed/empty, try optimistically
255
- shouldFetch = true;
256
- }
257
-
258
- if (shouldFetch) {
259
- const ref = this.deps.db.collection(collectionName).doc(dString);
260
- batchRefs.push(ref);
230
+ // Fetch if index says data exists OR if index is missing (optimistic)
231
+ if (!dayStatus || dayStatus[def.flag]) {
232
+ batchRefs.push(this.deps.db.collection(collectionName).doc(dString));
261
233
  dateKeyMap.push(dString);
262
234
  }
263
235
  }
264
236
 
265
- // 5. Execute Batch Read
237
+ // 4. Execute Batch Read
266
238
  const results = {};
267
- let foundCount = 0;
268
-
269
239
  if (batchRefs.length > 0) {
270
- // Log summary of all paths being loaded
271
- const paths = batchRefs.map(ref => ref.path).join(', ');
272
- this.deps.logger?.log('INFO', `[CachedDataLoader] 📂 Batch loading ${batchRefs.length} documents for '${loaderMethod}': ${paths}`);
273
-
240
+ this.deps.logger?.log('INFO', `[CachedDataLoader] 📂 Batch loading ${batchRefs.length} docs for '${loaderMethod}'`);
274
241
  try {
275
242
  const snapshots = await this.deps.db.getAll(...batchRefs);
276
-
277
- snapshots.forEach((snap, index) => {
243
+ snapshots.forEach((snap, idx) => {
278
244
  if (snap.exists) {
279
- const dString = dateKeyMap[index];
280
- const rawData = snap.data();
281
-
282
- // Decompress and clean data
283
- const decompressed = this._tryDecompress(rawData);
284
-
285
- // Handle standard data shapes (removing metadata fields if necessary)
286
- // Most root data loaders return the full object, so we do too.
287
- // Specific logic from data_loader.js (like stripping 'date' key) is handled here generically
288
- // or by the consumer. For series data, returning the whole object is usually safer.
289
-
290
- // Special handling for cleaner output (mimicking data_loader.js logic)
291
- if (loaderMethod === 'loadRatings' || loaderMethod === 'loadPageViews' ||
292
- loaderMethod === 'loadWatchlistMembership' || loaderMethod === 'loadAlertHistory') {
293
- const { date, lastUpdated, ...cleanData } = decompressed;
294
- results[dString] = cleanData;
245
+ const raw = this._tryDecompress(snap.data());
246
+ // Clean metadata if necessary
247
+ if (['loadRatings', 'loadPageViews', 'loadWatchlistMembership', 'loadAlertHistory'].includes(loaderMethod)) {
248
+ const { date, lastUpdated, ...clean } = raw;
249
+ results[dateKeyMap[idx]] = clean;
295
250
  } else if (loaderMethod === 'loadRankings') {
296
- results[dString] = decompressed.Items || [];
251
+ results[dateKeyMap[idx]] = raw.Items || [];
297
252
  } else {
298
- results[dString] = decompressed;
253
+ results[dateKeyMap[idx]] = raw;
299
254
  }
300
-
301
- foundCount++;
302
255
  }
303
256
  });
304
257
  } catch (err) {
305
- console.warn(`[CachedDataLoader] Batch fetch failed for ${loaderMethod}: ${err.message}. Falling back to individual fetches.`);
258
+ console.warn(`[CachedDataLoader] Batch failed: ${err.message}. Legacy fallback.`);
306
259
  return this._loadSeriesLegacy(loaderMethod, dateStr, lookbackDays);
307
260
  }
308
261
  }
309
262
 
310
- const summary = {
263
+ return {
311
264
  dates: Object.keys(results).sort(),
312
265
  data: results,
313
- found: foundCount,
266
+ found: Object.keys(results).length,
314
267
  requested: lookbackDays
315
268
  };
316
-
317
- this.deps.logger?.log('INFO', `[CachedDataLoader] ✅ Loaded ${foundCount}/${lookbackDays} dates for '${loaderMethod}' (found: ${summary.dates.join(', ') || 'none'})`);
318
-
319
- return summary;
320
269
  }
321
270
 
322
- /**
323
- * Legacy Fallback: Loads series using parallel promises (for custom/unmapped loaders)
324
- */
325
271
  async _loadSeriesLegacy(loaderMethod, dateStr, lookbackDays) {
326
272
  const results = {};
327
273
  const promises = [];
@@ -330,21 +276,11 @@ class CachedDataLoader {
330
276
  for (let i = 0; i < lookbackDays; i++) {
331
277
  const d = new Date(endDate);
332
278
  d.setUTCDate(d.getUTCDate() - i);
333
- const dString = d.toISOString().slice(0, 10);
334
-
335
- // Log path for legacy loader (will be logged by individual load methods)
336
- promises.push(
337
- this[loaderMethod](dString)
338
- .then(data => ({ date: dString, data }))
339
- .catch(() => ({ date: dString, data: null }))
340
- );
279
+ const dStr = d.toISOString().slice(0, 10);
280
+ promises.push(this[loaderMethod](dStr).then(data => data ? results[dStr] = data : null).catch(() => null));
341
281
  }
342
282
 
343
- const loaded = await Promise.all(promises);
344
- loaded.forEach(({ date, data }) => {
345
- if (data) results[date] = data;
346
- });
347
-
283
+ await Promise.all(promises);
348
284
  return {
349
285
  dates: Object.keys(results).sort(),
350
286
  data: results,