bulltrackers-module 1.0.657 → 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 (23) hide show
  1. package/functions/api-v2/routes/popular_investors.js +80 -0
  2. package/functions/computation-system/data/AvailabilityChecker.js +163 -317
  3. package/functions/computation-system/data/CachedDataLoader.js +158 -222
  4. package/functions/computation-system/data/DependencyFetcher.js +201 -406
  5. package/functions/computation-system/executors/MetaExecutor.js +176 -280
  6. package/functions/computation-system/executors/StandardExecutor.js +325 -383
  7. package/functions/computation-system/helpers/computation_dispatcher.js +294 -699
  8. package/functions/computation-system/helpers/computation_worker.js +3 -2
  9. package/functions/computation-system/legacy/AvailabilityCheckerOld.js +382 -0
  10. package/functions/computation-system/legacy/CachedDataLoaderOld.js +357 -0
  11. package/functions/computation-system/legacy/DependencyFetcherOld.js +478 -0
  12. package/functions/computation-system/legacy/MetaExecutorold.js +364 -0
  13. package/functions/computation-system/legacy/StandardExecutorold.js +476 -0
  14. package/functions/computation-system/legacy/computation_dispatcherold.js +944 -0
  15. package/functions/computation-system/persistence/ResultCommitter.js +137 -188
  16. package/functions/computation-system/services/SnapshotService.js +129 -0
  17. package/functions/computation-system/tools/BuildReporter.js +12 -7
  18. package/functions/computation-system/utils/data_loader.js +213 -238
  19. package/package.json +3 -2
  20. package/functions/computation-system/workflows/bulltrackers_pipeline.yaml +0 -163
  21. package/functions/computation-system/workflows/data_feeder_pipeline.yaml +0 -115
  22. package/functions/computation-system/workflows/datafeederpipelineinstructions.md +0 -30
  23. package/functions/computation-system/workflows/morning_prep_pipeline.yaml +0 -55
@@ -1,186 +1,52 @@
1
1
  /**
2
2
  * @fileoverview Executor for "Meta" (global) calculations.
3
- * UPDATED: Uses CachedDataLoader for all data access.
4
- * UPDATED: Tracks processed shard/item counts.
5
- * UPDATED: Sends 'isInitialWrite: true' for robust cleanup.
6
- * UPDATED: Support for historical rankings in Meta Context.
7
- * UPDATED: Added support for loading Series Data (Root & Results) for lookbacks.
8
- * UPDATED: Builds Manifest Lookup for DependencyFetcher.
3
+ * REFACTORED: Applied DRY principles to Root Data and Series loading.
9
4
  */
10
5
  const { normalizeName } = require('../utils/utils');
11
6
  const { CachedDataLoader } = require('../data/CachedDataLoader');
12
7
  const { ContextFactory } = require('../context/ContextFactory');
13
8
  const { commitResults } = require('../persistence/ResultCommitter');
14
- const { fetchResultSeries, fetchDependencies } = require('../data/DependencyFetcher');
15
- const { getManifest } = require('../topology/ManifestLoader');
9
+ const { fetchResultSeries } = require('../data/DependencyFetcher');
10
+ const { getManifest } = require('../topology/ManifestLoader');
16
11
 
17
12
  class MetaExecutor {
13
+
14
+ // =========================================================================
15
+ // PRIMARY ENTRY POINT (Batch Execution)
16
+ // =========================================================================
18
17
  static async run(date, calcs, passName, config, deps, rootData, fetchedDeps, previousFetchedDeps) {
18
+ const { logger } = deps;
19
19
  const dStr = date.toISOString().slice(0, 10);
20
- const { logger, db } = deps;
21
20
  const loader = new CachedDataLoader(config, deps);
22
21
 
23
- // --- [FIXED] Build Global Manifest Lookup using getManifest ---
24
- // getManifest is typically cached, so this is cheap.
22
+ // 1. Setup Manifest Lookup
25
23
  const allManifests = getManifest(config.productLines, config.calculationsDirectory, deps);
26
- const manifestLookup = {};
27
- allManifests.forEach(m => { manifestLookup[normalizeName(m.name)] = m.category; });
28
-
29
- // [FIX] Check if any meta calculation needs history
30
- const needsHistory = calcs.some(c => c.isHistorical);
31
- let rankingsYesterday = null;
32
-
33
- if (needsHistory) {
34
- const prevDate = new Date(date);
35
- prevDate.setUTCDate(prevDate.getUTCDate() - 1);
36
- const prevStr = prevDate.toISOString().slice(0, 10);
37
- logger.log('INFO', `[MetaExecutor] Loading historical rankings for ${prevStr}`);
38
- rankingsYesterday = await loader.loadRankings(prevStr);
39
- }
24
+ const manifestLookup = Object.fromEntries(allManifests.map(m => [normalizeName(m.name), m.category]));
40
25
 
41
- // 1. Load Global Dependencies
26
+ // 2. Load Base Data (Always Required)
42
27
  const [mappings, rankings, verifications] = await Promise.all([
43
28
  loader.loadMappings(),
44
29
  loader.loadRankings(dStr),
45
30
  loader.loadVerifications()
46
31
  ]);
47
-
48
- // [NEW] Load New Root Data Types for Profile Metrics (if any calc needs them)
49
- const needsNewRootData = calcs.some(c => {
50
- const deps = c.rootDataDependencies || [];
51
- return deps.includes('ratings') || deps.includes('pageViews') ||
52
- deps.includes('watchlist') || deps.includes('alerts');
53
- });
54
-
55
- let ratings = null, pageViews = null, watchlistMembership = null, alertHistory = null;
56
- if (needsNewRootData) {
57
- const loadPromises = [];
58
-
59
- // Ratings
60
- if (calcs.some(c => c.rootDataDependencies?.includes('ratings'))) {
61
- loadPromises.push(loader.loadRatings(dStr).then(r => { ratings = r; }).catch(e => {
62
- const allAllowMissing = calcs.every(c => {
63
- const needsRatings = c.rootDataDependencies?.includes('ratings');
64
- return !needsRatings || c.canHaveMissingRoots === true;
65
- });
66
- if (allAllowMissing) { ratings = null; } else { throw e; }
67
- }));
68
- }
69
-
70
- // PageViews
71
- if (calcs.some(c => c.rootDataDependencies?.includes('pageViews'))) {
72
- loadPromises.push(loader.loadPageViews(dStr).then(pv => { pageViews = pv; }).catch(e => {
73
- const allAllowMissing = calcs.every(c => {
74
- const needsPageViews = c.rootDataDependencies?.includes('pageViews');
75
- return !needsPageViews || c.canHaveMissingRoots === true;
76
- });
77
- if (allAllowMissing) { pageViews = null; } else { throw e; }
78
- }));
79
- }
80
-
81
- // Watchlist
82
- if (calcs.some(c => c.rootDataDependencies?.includes('watchlist'))) {
83
- loadPromises.push(loader.loadWatchlistMembership(dStr).then(w => { watchlistMembership = w; }).catch(e => {
84
- const allAllowMissing = calcs.every(c => {
85
- const needsWatchlist = c.rootDataDependencies?.includes('watchlist');
86
- return !needsWatchlist || c.canHaveMissingRoots === true;
87
- });
88
- if (allAllowMissing) { watchlistMembership = null; } else { throw e; }
89
- }));
90
- }
91
-
92
- // Alerts
93
- if (calcs.some(c => c.rootDataDependencies?.includes('alerts'))) {
94
- loadPromises.push(loader.loadAlertHistory(dStr).then(a => { alertHistory = a; }).catch(e => {
95
- const allAllowMissing = calcs.every(c => {
96
- const needsAlerts = c.rootDataDependencies?.includes('alerts');
97
- return !needsAlerts || c.canHaveMissingRoots === true;
98
- });
99
- if (allAllowMissing) { alertHistory = null; } else { throw e; }
100
- }));
101
- }
102
-
103
- await Promise.all(loadPromises);
104
-
105
- // [FIX] Enforce canHaveMissingRoots - validate after loading
106
- for (const c of calcs) {
107
- const deps = c.rootDataDependencies || [];
108
- const canSkip = c.canHaveMissingRoots === true;
109
- const isMissing = (key, val) => deps.includes(key) && (val === null || val === undefined);
110
-
111
- if (!canSkip) {
112
- if (isMissing('ratings', ratings)) throw new Error(`[MetaExecutor] Missing required root 'ratings' for ${c.name}`);
113
- if (isMissing('pageViews', pageViews)) throw new Error(`[MetaExecutor] Missing required root 'pageViews' for ${c.name}`);
114
- if (isMissing('watchlist', watchlistMembership)) throw new Error(`[MetaExecutor] Missing required root 'watchlist' for ${c.name}`);
115
- if (isMissing('alerts', alertHistory)) throw new Error(`[MetaExecutor] Missing required root 'alerts' for ${c.name}`);
116
- }
117
- }
118
- }
119
-
120
- // --- [NEW] Series / Lookback Loading Logic ---
121
- const rootSeriesRequests = {};
122
- const dependencySeriesRequests = {};
123
32
 
124
- // 1. Identify requirements from manifests
125
- calcs.forEach(c => {
126
- if (c.rootDataSeries) {
127
- Object.entries(c.rootDataSeries).forEach(([type, conf]) => {
128
- const days = typeof conf === 'object' ? conf.lookback : conf;
129
- if (!rootSeriesRequests[type] || days > rootSeriesRequests[type]) {
130
- rootSeriesRequests[type] = days;
131
- }
132
- });
133
- }
134
- if (c.dependencySeries) {
135
- Object.entries(c.dependencySeries).forEach(([depName, conf]) => {
136
- const days = typeof conf === 'object' ? conf.lookback : conf;
137
- const normalized = normalizeName(depName);
138
- if (!dependencySeriesRequests[normalized] || days > dependencySeriesRequests[normalized]) {
139
- dependencySeriesRequests[normalized] = days;
140
- }
141
- });
142
- }
143
- });
144
-
145
- const seriesData = { root: {}, results: {} };
146
-
147
- // 2. Load Root Series
148
- for (const [type, days] of Object.entries(rootSeriesRequests)) {
149
- let loaderMethod = null;
150
- if (type === 'alerts') loaderMethod = 'loadAlertHistory';
151
- else if (type === 'insights') loaderMethod = 'loadInsights';
152
- else if (type === 'ratings') loaderMethod = 'loadRatings';
153
- else if (type === 'watchlist') loaderMethod = 'loadWatchlistMembership';
154
- // [CRITICAL UPDATE] Add rankings support for Meta lookbacks
155
- else if (type === 'rankings') loaderMethod = 'loadRankings';
156
-
157
- if (loaderMethod) {
158
- logger.log('INFO', `[MetaExecutor] Loading ${days}-day series for Root Data '${type}'...`);
159
- const series = await loader.loadSeries(loaderMethod, dStr, days);
160
- seriesData.root[type] = series.data;
161
- }
33
+ // 3. Load Historical Rankings (if needed)
34
+ let rankingsYesterday = null;
35
+ if (calcs.some(c => c.isHistorical)) {
36
+ const prevStr = new Date(date.getTime() - 86400000).toISOString().slice(0, 10);
37
+ rankingsYesterday = await loader.loadRankings(prevStr);
162
38
  }
163
39
 
164
- // 3. Load Computation Result Series
165
- const calcNamesToFetch = Object.keys(dependencySeriesRequests);
166
- if (calcNamesToFetch.length > 0) {
167
- const maxDays = Math.max(...Object.values(dependencySeriesRequests));
168
- logger.log('INFO', `[MetaExecutor] Loading up to ${maxDays}-day series for Dependencies: ${calcNamesToFetch.join(', ')}`);
169
-
170
- // Pass manifestLookup to fetcher
171
- const resultsSeries = await fetchResultSeries(dStr, calcNamesToFetch, manifestLookup, config, deps, maxDays);
172
- seriesData.results = resultsSeries;
173
- }
40
+ // 4. Load Variable Root Data & Series (Refactored Helpers)
41
+ const variableRoots = await loadVariableRootData(loader, dStr, calcs, logger);
42
+ const seriesData = await loadSeriesData(loader, dStr, calcs, manifestLookup, config, deps);
174
43
 
44
+ // 5. Execution Loop
175
45
  const state = {};
176
46
  for (const c of calcs) {
177
47
  const inst = new c.class();
178
48
  inst.manifest = c;
179
49
 
180
- // DEBUG: Log what dependencies were fetched
181
- const fetchedDepKeys = Object.keys(fetchedDeps || {});
182
- logger.log('INFO', `[MetaExecutor] 📦 Fetched dependencies available: ${fetchedDepKeys.length > 0 ? fetchedDepKeys.join(', ') : 'NONE'}`);
183
-
184
50
  const context = ContextFactory.buildMetaContext({
185
51
  dateStr: dStr,
186
52
  metadata: c,
@@ -193,172 +59,202 @@ class MetaExecutor {
193
59
  allRankings: rankings,
194
60
  allRankingsYesterday: rankingsYesterday,
195
61
  allVerifications: verifications,
196
- ratings: ratings || {},
197
- pageViews: pageViews || {},
198
- watchlistMembership: watchlistMembership || {},
199
- alertHistory: alertHistory || {},
62
+ // Spread variable roots directly (ratings, pageViews, etc.)
63
+ ...variableRoots,
200
64
  seriesData
201
65
  });
202
66
 
203
- // DEBUG: Log dependency availability
204
- // CRITICAL: Use class.getDependencies() to get original case-sensitive names
205
- const depNames = (c.class && typeof c.class.getDependencies === 'function')
206
- ? c.class.getDependencies()
207
- : (c.getDependencies ? c.getDependencies() : (c.dependencies || []));
208
- depNames.forEach(depName => {
209
- const depData = context.computed[depName];
210
- if (depData) {
211
- const keys = Object.keys(depData);
212
- logger.log('INFO', `[MetaExecutor] ✅ Dependency '${depName}' available for ${c.name}. Keys: ${keys.length} (sample: ${keys.slice(0, 5).join(', ')})`);
213
- } else {
214
- logger.log('ERROR', `[MetaExecutor] ❌ Dependency '${depName}' MISSING for ${c.name} in context.computed`);
215
- }
216
- });
217
-
218
67
  try {
219
68
  const result = await inst.process(context);
220
69
 
221
- // DEBUG: Log result before saving
222
- if (result && typeof result === 'object') {
223
- const resultKeys = Object.keys(result);
224
- logger.log('INFO', `[MetaExecutor] ✅ ${c.name} computed result. Keys: ${resultKeys.length} (sample: ${resultKeys.slice(0, 10).join(', ')})`);
225
- if (resultKeys.length === 0) {
226
- logger.log('WARN', `[MetaExecutor] ⚠️ ${c.name} returned EMPTY result object!`);
227
- }
228
- } else {
229
- logger.log('WARN', `[MetaExecutor] ⚠️ ${c.name} returned non-object result: ${typeof result} - ${result}`);
230
- }
231
-
232
- // CRITICAL FIX: Do NOT overwrite this.results - the computation already sets it correctly
233
- // The computation's process() method sets this.results, and getResult() returns it
234
- // We only use the return value for logging/debugging
235
- // inst.results should already be set by the computation's process() method
236
- if (!inst.results) {
237
- logger.log('WARN', `[MetaExecutor] ⚠️ ${c.name} did not set this.results - using return value as fallback`);
238
- inst.results = result;
239
- }
70
+ // Fallback if the computation didn't set this.results internally
71
+ if (!inst.results) inst.results = result;
240
72
 
241
- // DEBUG: Verify what getResult() will return
242
- const finalResult = await inst.getResult();
243
- if (finalResult && typeof finalResult === 'object') {
244
- const finalKeys = Object.keys(finalResult);
245
- logger.log('INFO', `[MetaExecutor] ✅ ${c.name} getResult() will return ${finalKeys.length} keys`);
246
- } else {
247
- logger.log('WARN', `[MetaExecutor] ⚠️ ${c.name} getResult() returns: ${typeof finalResult}`);
73
+ // Debug logging condensed
74
+ if (inst.results && Object.keys(inst.results).length === 0) {
75
+ logger.log('WARN', `[MetaExecutor] ⚠️ ${c.name} produced EMPTY results.`);
248
76
  }
249
77
 
250
78
  state[c.name] = inst;
251
79
  } catch (e) {
252
- logger.log('ERROR', `Meta calc ${c.name} failed: ${e.message}`);
80
+ logger.log('ERROR', `[MetaExecutor] ${c.name} failed: ${e.message}`);
253
81
  }
254
82
  }
255
83
 
256
- // CRITICAL FIX: Pass 'isInitialWrite: true' to ensure proper cleanup of old meta data
84
+ // Force 'isInitialWrite: true' for robust cleanup of old keys
257
85
  return await commitResults(state, dStr, passName, config, deps, false, { isInitialWrite: true });
258
86
  }
259
87
 
88
+ // =========================================================================
89
+ // SINGLE EXECUTION (Sharded/On-Demand)
90
+ // =========================================================================
260
91
  static async executeOncePerDay(calcInstance, metadata, dateStr, computedDeps, prevDeps, config, deps, loader) {
261
- const mappings = await loader.loadMappings();
262
92
  const { logger } = deps;
263
- const stats = { processedShards: 0, processedItems: 0 };
264
-
265
- const insights = metadata.rootDataDependencies?.includes('insights') ? { today: await loader.loadInsights(dateStr) } : null;
266
- const social = metadata.rootDataDependencies?.includes('social') ? { today: await loader.loadSocial(dateStr) } : null;
267
-
93
+ const calcs = [metadata]; // Treat single as list for helpers
94
+
95
+ // 1. Load Data using Shared Helpers
96
+ const [mappings, rankings, variableRoots, seriesData] = await Promise.all([
97
+ loader.loadMappings(),
98
+ loader.loadRankings(dateStr),
99
+ loadVariableRootData(loader, dateStr, calcs, logger),
100
+ loadSeriesData(loader, dateStr, calcs, {}, config, deps) // Empty lookup fine for single usage usually
101
+ ]);
102
+
268
103
  let rankingsYesterday = null;
269
104
  if (metadata.isHistorical) {
270
- const prevDate = new Date(dateStr);
271
- prevDate.setUTCDate(prevDate.getUTCDate() - 1);
272
- const prevStr = prevDate.toISOString().slice(0, 10);
273
- rankingsYesterday = await loader.loadRankings(prevStr);
105
+ const prevStr = new Date(new Date(dateStr).getTime() - 86400000).toISOString().slice(0, 10);
106
+ rankingsYesterday = await loader.loadRankings(prevStr);
274
107
  }
275
108
 
276
- const rankings = await loader.loadRankings(dateStr);
277
- const allowMissing = metadata.canHaveMissingRoots === true;
278
-
279
- let ratings = null;
280
- if (metadata.rootDataDependencies?.includes('ratings')) {
281
- try { ratings = await loader.loadRatings(dateStr); }
282
- catch (e) { if (!allowMissing) throw e; ratings = null; }
283
- if (!ratings && !allowMissing) throw new Error(`Missing ratings`);
284
- }
285
-
286
- let pageViews = null;
287
- if (metadata.rootDataDependencies?.includes('pageViews')) {
288
- try { pageViews = await loader.loadPageViews(dateStr); }
289
- catch (e) { if (!allowMissing) throw e; pageViews = null; }
290
- if (!pageViews && !allowMissing) throw new Error(`Missing pageViews`);
291
- }
292
-
293
- let watchlistMembership = null;
294
- if (metadata.rootDataDependencies?.includes('watchlist')) {
295
- try { watchlistMembership = await loader.loadWatchlistMembership(dateStr); }
296
- catch (e) { if (!allowMissing) throw e; watchlistMembership = null; }
297
- if (!watchlistMembership && !allowMissing) throw new Error(`Missing watchlist`);
298
- }
299
-
300
- let alertHistory = null;
301
- if (metadata.rootDataDependencies?.includes('alerts')) {
302
- try { alertHistory = await loader.loadAlertHistory(dateStr); }
303
- catch (e) { if (!allowMissing) throw e; alertHistory = null; }
304
- if (!alertHistory && !allowMissing) throw new Error(`Missing alerts`);
305
- }
109
+ const insights = metadata.rootDataDependencies?.includes('insights') ? { today: await loader.loadInsights(dateStr) } : null;
110
+ const social = metadata.rootDataDependencies?.includes('social') ? { today: await loader.loadSocial(dateStr) } : null;
306
111
 
307
- const seriesData = { root: {}, results: {} };
112
+ // 2. Build Context Base
113
+ const contextBase = {
114
+ dateStr, metadata, mappings, insights, socialData: social,
115
+ computedDependencies: computedDeps,
116
+ previousComputedDependencies: prevDeps, config, deps,
117
+ allRankings: rankings,
118
+ allRankingsYesterday: rankingsYesterday,
119
+ ...variableRoots,
120
+ seriesData
121
+ };
308
122
 
123
+ // 3. Sharded Execution (Price) or Standard
309
124
  if (metadata.rootDataDependencies?.includes('price')) {
310
125
  logger.log('INFO', `[Executor] Running Batched/Sharded Execution for ${metadata.name}`);
311
126
  const shardRefs = await loader.getPriceShardReferences();
312
- if (shardRefs.length === 0) { logger.log('WARN', '[Executor] No price shards found.'); return {}; }
127
+ if (!shardRefs.length) {
128
+ logger.log('WARN', '[Executor] No price shards found.');
129
+ return {};
130
+ }
313
131
 
314
- let processedCount = 0;
132
+ const stats = { processedShards: 0, processedItems: 0 };
315
133
  for (const ref of shardRefs) {
316
134
  const shardData = await loader.loadPriceShard(ref);
317
- const partialContext = ContextFactory.buildMetaContext({
318
- dateStr, metadata, mappings, insights, socialData: social,
319
- prices: { history: shardData }, computedDependencies: computedDeps,
320
- previousComputedDependencies: prevDeps, config, deps,
321
- allRankings: rankings,
322
- allRankingsYesterday: rankingsYesterday,
323
- ratings: ratings || {},
324
- pageViews: pageViews || {},
325
- watchlistMembership: watchlistMembership || {},
326
- alertHistory: alertHistory || {},
327
- seriesData
328
- });
329
-
330
- await calcInstance.process(partialContext);
331
- partialContext.prices = null;
332
- processedCount++;
135
+ await calcInstance.process(ContextFactory.buildMetaContext({
136
+ ...contextBase,
137
+ prices: { history: shardData }
138
+ }));
333
139
 
334
140
  stats.processedShards++;
335
141
  stats.processedItems += Object.keys(shardData).length;
336
142
  }
337
- logger.log('INFO', `[Executor] Finished Batched Execution for ${metadata.name} (${processedCount} shards).`);
338
-
339
143
  calcInstance._executionStats = stats;
340
144
  return calcInstance.getResult ? await calcInstance.getResult() : {};
341
- } else {
342
- const context = ContextFactory.buildMetaContext({
343
- dateStr, metadata, mappings, insights, socialData: social,
344
- prices: {}, computedDependencies: computedDeps,
345
- previousComputedDependencies: prevDeps, config, deps,
346
- allRankings: rankings,
347
- allRankingsYesterday: rankingsYesterday,
348
- ratings: ratings || {},
349
- pageViews: pageViews || {},
350
- watchlistMembership: watchlistMembership || {},
351
- alertHistory: alertHistory || {},
352
- seriesData
353
- });
354
- const res = await calcInstance.process(context);
355
-
356
- stats.processedItems = 1;
357
- calcInstance._executionStats = stats;
358
-
145
+ }
146
+ else {
147
+ const res = await calcInstance.process(ContextFactory.buildMetaContext({
148
+ ...contextBase,
149
+ prices: {}
150
+ }));
151
+ calcInstance._executionStats = { processedItems: 1 };
359
152
  return res;
360
153
  }
361
154
  }
362
155
  }
363
156
 
157
+ // =============================================================================
158
+ // INTERNAL HELPERS
159
+ // =============================================================================
160
+
161
+ /**
162
+ * Loads variable root data types (Ratings, PageViews, Watchlist, Alerts)
163
+ * based on calculation requirements. Handles strict vs. optional failures.
164
+ */
165
+ async function loadVariableRootData(loader, dateStr, calcs, logger) {
166
+ const requirements = {};
167
+ const results = { ratings: null, pageViews: null, watchlistMembership: null, alertHistory: null };
168
+
169
+ // Map internal key to Loader Method
170
+ const loaderMap = {
171
+ ratings: { method: 'loadRatings', resultKey: 'ratings' },
172
+ pageViews: { method: 'loadPageViews', resultKey: 'pageViews' },
173
+ watchlist: { method: 'loadWatchlistMembership', resultKey: 'watchlistMembership' },
174
+ alerts: { method: 'loadAlertHistory', resultKey: 'alertHistory' }
175
+ };
176
+
177
+ // 1. Analyze Requirements
178
+ for (const c of calcs) {
179
+ const deps = c.rootDataDependencies || [];
180
+ const strict = c.canHaveMissingRoots !== true;
181
+ deps.forEach(d => {
182
+ if (loaderMap[d]) {
183
+ if (!requirements[d]) requirements[d] = { strict: false };
184
+ if (strict) requirements[d].strict = true;
185
+ }
186
+ });
187
+ }
188
+
189
+ // 2. Fetch Data
190
+ const promises = Object.entries(requirements).map(async ([key, req]) => {
191
+ const { method, resultKey } = loaderMap[key];
192
+ try {
193
+ results[resultKey] = await loader[method](dateStr);
194
+ } catch (e) {
195
+ if (req.strict) throw new Error(`[MetaExecutor] Missing required root '${key}': ${e.message}`);
196
+ logger.log('WARN', `[MetaExecutor] Missing optional root '${key}'.`);
197
+ results[resultKey] = null;
198
+ }
199
+ });
200
+
201
+ await Promise.all(promises);
202
+ return results;
203
+ }
204
+
205
+ /**
206
+ * Loads time-series data for both Root inputs and Computation results.
207
+ */
208
+ async function loadSeriesData(loader, dateStr, calcs, manifestLookup, config, deps) {
209
+ const rootRequests = {};
210
+ const depRequests = {};
211
+
212
+ // 1. Aggregate Lookback Depths
213
+ for (const c of calcs) {
214
+ if (c.rootDataSeries) {
215
+ Object.entries(c.rootDataSeries).forEach(([type, val]) => {
216
+ const days = typeof val === 'object' ? val.lookback : val;
217
+ rootRequests[type] = Math.max(rootRequests[type] || 0, days);
218
+ });
219
+ }
220
+ if (c.dependencySeries) {
221
+ Object.entries(c.dependencySeries).forEach(([name, val]) => {
222
+ const days = typeof val === 'object' ? val.lookback : val;
223
+ const norm = normalizeName(name);
224
+ depRequests[norm] = Math.max(depRequests[norm] || 0, days);
225
+ });
226
+ }
227
+ }
228
+
229
+ const seriesData = { root: {}, results: {} };
230
+
231
+ // 2. Fetch Root Series
232
+ const rootLoaders = {
233
+ alerts: 'loadAlertHistory',
234
+ insights: 'loadInsights',
235
+ ratings: 'loadRatings',
236
+ watchlist: 'loadWatchlistMembership',
237
+ rankings: 'loadRankings'
238
+ };
239
+
240
+ const rootPromises = Object.entries(rootRequests).map(async ([type, days]) => {
241
+ if (rootLoaders[type]) {
242
+ deps.logger.log('INFO', `[MetaExecutor] Loading ${days}-day series for Root '${type}'`);
243
+ const res = await loader.loadSeries(rootLoaders[type], dateStr, days);
244
+ seriesData.root[type] = res.data;
245
+ }
246
+ });
247
+
248
+ // 3. Fetch Dependency Series
249
+ const depNames = Object.keys(depRequests);
250
+ if (depNames.length > 0) {
251
+ const maxDays = Math.max(...Object.values(depRequests));
252
+ deps.logger.log('INFO', `[MetaExecutor] Loading up to ${maxDays}-day series for Dependencies: ${depNames.join(', ')}`);
253
+ seriesData.results = await fetchResultSeries(dateStr, depNames, manifestLookup, config, deps, maxDays);
254
+ }
255
+
256
+ await Promise.all(rootPromises);
257
+ return seriesData;
258
+ }
259
+
364
260
  module.exports = { MetaExecutor };