bulltrackers-module 1.0.94 → 1.0.96

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.
@@ -2,6 +2,7 @@
2
2
  * @fileoverview Main pipe: pipe.computationSystem.runOrchestration
3
3
  * REFACTORED: Now stateless and receives dependencies.
4
4
  * All internal helpers now receive (config, dependencies) as well.
5
+ * UPDATED: Added Pass 4 for 'backtest' calculations.
5
6
  */
6
7
 
7
8
  const { FieldPath } = require('@google-cloud/firestore');
@@ -41,13 +42,15 @@ async function runComputationOrchestrator(config, dependencies, calculations) {
41
42
  historicalCalculations,
42
43
  dailyCalculations,
43
44
  metaCalculations,
45
+ backtestCalculations, // <-- ADD THIS
44
46
  HISTORICAL_CALC_NAMES,
45
- META_CALC_NAMES
47
+ META_CALC_NAMES,
48
+ BACKTEST_CALC_NAMES // <-- ADD THIS
46
49
  } = categorizeCalculations(calculations);
47
50
  // --- END NEW ---
48
51
 
49
- // --- MODIFIED: Add pass3_results to summary ---
50
- const summary = { pass1_results: [], pass2_results: [], pass3_results: [] };
52
+ // --- MODIFIED: Add pass4_results to summary ---
53
+ const summary = { pass1_results: [], pass2_results: [], pass3_results: [], pass4_results: [] };
51
54
  // --- END MODIFIED ---
52
55
  const yesterday = new Date();
53
56
  yesterday.setUTCDate(yesterday.getUTCDate() - 1);
@@ -72,13 +75,20 @@ async function runComputationOrchestrator(config, dependencies, calculations) {
72
75
  Object.keys(calcs).map(name => ({ category: cat, calcName: name }))
73
76
  );
74
77
  // --- END NEW ---
78
+
79
+ // --- NEW: Create master list for backtests ---
80
+ const masterBacktestList = Object.entries(backtestCalculations).flatMap(([cat, calcs]) => // <-- ADD THIS
81
+ Object.keys(calcs).map(name => ({ category: cat, calcName: name }))
82
+ );
83
+ // --- END NEW ---
75
84
 
76
85
  const masterFullList = [
77
86
  ...masterHistoricalList,
78
87
  ...masterDailyList,
79
88
  ...masterInsightsList,
80
89
  ...masterSocialPostList,
81
- ...masterMetaList // <-- NEW
90
+ ...masterMetaList, // <-- NEW
91
+ ...masterBacktestList // <-- ADD THIS
82
92
  ];
83
93
 
84
94
  // Pass dependencies to sub-pipe
@@ -121,7 +131,8 @@ async function runComputationOrchestrator(config, dependencies, calculations) {
121
131
  if (!/^\d{4}-\d{2}-\d{2}$/.test(doc.id)) return;
122
132
  const data = doc.data();
123
133
  const missingCalcs = masterFullList.filter(({ category, calcName }) =>
124
- !META_CALC_NAMES.has(calcName) && // <-- Exclude meta calcs from Pass 2
134
+ !META_CALC_NAMES.has(calcName) && // <-- Exclude meta calcs
135
+ !BACKTEST_CALC_NAMES.has(calcName) && // <-- Exclude backtest calcs
125
136
  !data?.[category]?.[calcName]
126
137
  );
127
138
 
@@ -204,6 +215,43 @@ async function runComputationOrchestrator(config, dependencies, calculations) {
204
215
  summary.pass3_results = pass3Results.map((r, i) => r.status === 'fulfilled' ? r.value : { success: false, date: pass3Jobs[i].date, error: r.reason?.message });
205
216
  // --- END NEW PASS 3 ---
206
217
 
218
+ // --- NEW: PASS 4 (Backtests) ---
219
+ // This pass runs *after* Pass 3, checking for missing *backtest* calculations.
220
+ const pass4Jobs = [];
221
+ // We can re-use the final doc list from the end of Pass 3
222
+ finalInsightDocs.forEach(doc => {
223
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(doc.id)) return;
224
+ if (!allExpectedDates.includes(doc.id)) return;
225
+
226
+ const data = doc.data();
227
+ const missingBacktestCalcs = masterBacktestList.filter(({ category, calcName }) =>
228
+ !data?.[category]?.[calcName]
229
+ );
230
+
231
+ if (missingBacktestCalcs.length > 0) {
232
+ pass4Jobs.push({ date: doc.id, missing: missingBacktestCalcs });
233
+ }
234
+ });
235
+
236
+ logger.log('INFO', `[Orchestrator] Pass 4: Found ${pass4Jobs.length} dates to process for backtest-calculations.`);
237
+
238
+ // We can REUSE runMetaComputation because it has the same signature
239
+ const pass4Results = await processJobsInParallel(
240
+ pass4Jobs,
241
+ (date, missing) => runMetaComputation( // Re-using this handler
242
+ date,
243
+ missing,
244
+ 'Pass 4 (Backtest)',
245
+ backtestCalculations, // Pass the backtest calcs package
246
+ config,
247
+ dependencies
248
+ ),
249
+ 'Pass 4',
250
+ config
251
+ );
252
+ summary.pass4_results = pass4Results.map((r, i) => r.status === 'fulfilled' ? r.value : { success: false, date: pass4Jobs[i].date, error: r.reason?.message });
253
+ // --- END NEW PASS 4 ---
254
+
207
255
 
208
256
  logger.log('INFO', '[Orchestrator] Computation orchestration finished.');
209
257
  return summary;
@@ -260,7 +308,9 @@ async function streamAndProcess(
260
308
  instrumentMappings: instrumentToTicker,
261
309
  sectorMapping: instrumentToSector,
262
310
  todayDateStr: dateStr,
263
- yesterdayDateStr: yesterdayStr
311
+ yesterdayDateStr: yesterdayStr,
312
+ dependencies: dependencies, // <-- Pass dependencies into context for meta-calcs
313
+ config: config // <-- Pass config into context for meta-calcs
264
314
  };
265
315
 
266
316
  const batchSize = config.partRefBatchSize || 10;
@@ -474,26 +524,50 @@ async function runUnifiedComputation(dateToProcess, calculationsToRun, passName,
474
524
  const summaryData = {};
475
525
 
476
526
  if (result && Object.keys(result).length > 0) {
477
- if (result.sharded_user_profitability) {
478
- for (const shardId in result.sharded_user_profitability) {
479
- const shardData = result.sharded_user_profitability[shardId];
480
- if (shardData && Object.keys(shardData).length > 0) {
481
- const shardRef = db.collection(config.shardedProfitabilityCollection).doc(shardId); // Use db
482
- pendingWrites.push({ ref: shardRef, data: { profits: shardData } });
483
- }
527
+ // --- SPECIAL HANDLING FOR SHARDED CALCS ---
528
+ // (This is a bit hardcoded, but necessary for sharded outputs)
529
+ let isSharded = false;
530
+ const shardedCollections = {
531
+ 'sharded_user_profile': config.shardedUserProfileCollection,
532
+ 'sharded_user_profitability': config.shardedProfitabilityCollection
533
+ };
534
+
535
+ for (const resultKey in shardedCollections) {
536
+ if (result[resultKey]) {
537
+ isSharded = true;
538
+ const shardCollectionName = shardedCollections[resultKey];
539
+ const shardedData = result[resultKey];
540
+
541
+ for (const shardId in shardedData) {
542
+ const shardDocData = shardedData[shardId];
543
+ if (shardDocData && Object.keys(shardDocData).length > 0) {
544
+ const shardRef = db.collection(shardCollectionName).doc(shardId);
545
+ pendingWrites.push({ ref: shardRef, data: shardDocData });
546
+ }
547
+ }
548
+ // Store the *other* results from the calc, if any
549
+ const { [resultKey]: _, ...otherResults } = result;
550
+ if (Object.keys(otherResults).length > 0) {
551
+ const computationDocRef = resultsCollectionRef.doc(category)
552
+ .collection(config.computationsSubcollection)
553
+ .doc(calcName);
554
+ pendingWrites.push({ ref: computationDocRef, data: otherResults });
555
+ }
484
556
  }
485
- if (!summaryData[category]) summaryData[category] = {};
486
- summaryData[category][calcName] = true;
557
+ }
558
+ // --- END SPECIAL HANDLING ---
487
559
 
488
- } else {
560
+ if (!isSharded) {
489
561
  const computationDocRef = resultsCollectionRef.doc(category)
490
562
  .collection(config.computationsSubcollection)
491
563
  .doc(calcName);
492
564
  pendingWrites.push({ ref: computationDocRef, data: result });
493
- if (!summaryData[category]) summaryData[category] = {};
494
- summaryData[category][calcName] = true;
495
565
  }
496
566
 
567
+ // Add summary flag
568
+ if (!summaryData[category]) summaryData[category] = {};
569
+ summaryData[category][calcName] = true;
570
+
497
571
  if (Object.keys(summaryData).length > 0) {
498
572
  const topLevelDocRef = db.collection(config.resultsCollection).doc(dateStr); // Use db
499
573
  pendingWrites.push({ ref: topLevelDocRef, data: summaryData });
@@ -531,14 +605,14 @@ async function runUnifiedComputation(dateToProcess, calculationsToRun, passName,
531
605
 
532
606
  // --- NEW: Add the handler for meta-calculations ---
533
607
  /**
534
- * Internal sub-pipe: Runs "meta" computations for a single date.
608
+ * Internal sub-pipe: Runs "meta" or "backtest" computations for a single date.
535
609
  * These calculations do NOT stream user data, but rather use the
536
610
  * special `process(dateStr, dependencies, config)` signature.
537
611
  */
538
612
  async function runMetaComputation(dateToProcess, calculationsToRun, passName, sourcePackage, config, dependencies) {
539
613
  const { db, logger } = dependencies;
540
614
  const dateStr = dateToProcess.toISOString().slice(0, 10);
541
- logger.log('INFO', `[${passName}] Starting run for ${dateStr} with ${calculationsToRun.length} meta-calcs.`);
615
+ logger.log('INFO', `[${passName}] Starting run for ${dateStr} with ${calculationsToRun.length} calcs.`);
542
616
 
543
617
  try {
544
618
  const state = initializeCalculators(calculationsToRun, sourcePackage, logger);
@@ -561,7 +635,7 @@ async function runMetaComputation(dateToProcess, calculationsToRun, passName, so
561
635
 
562
636
  // Check if the result is valid
563
637
  if (result && Object.keys(result).length > 0) {
564
- // Meta calcs are not expected to be sharded, but we handle the standard case
638
+ // Meta calcs are not expected to be sharded
565
639
  const computationDocRef = resultsCollectionRef.doc(category)
566
640
  .collection(config.computationsSubcollection)
567
641
  .doc(calcName);
@@ -21,10 +21,12 @@ const { FieldValue, FieldPath } = require('@google-cloud/firestore');
21
21
  function categorizeCalculations(calculations) {
22
22
  const HISTORICAL_CALC_NAMES = new Set();
23
23
  const META_CALC_NAMES = new Set();
24
+ const BACKTEST_CALC_NAMES = new Set(); // <-- ADD THIS
24
25
 
25
26
  const historicalCalculations = {};
26
27
  const dailyCalculations = {};
27
28
  const metaCalculations = {};
29
+ const backtestCalculations = {}; // <-- ADD THIS
28
30
 
29
31
  for (const category in calculations) {
30
32
  // 1. Check for 'meta' category first (by top-level directory name)
@@ -37,8 +39,20 @@ function categorizeCalculations(calculations) {
37
39
  }
38
40
  continue; // Done with this category
39
41
  }
42
+
43
+ // 2. Check for 'backtests' category
44
+ if (category === 'backtests') { // <-- ADD THIS BLOCK
45
+ if (!backtestCalculations[category]) backtestCalculations[category] = {};
46
+ for (const calcName in calculations[category]) {
47
+ const CalculationClass = calculations[category][calcName];
48
+ backtestCalculations[category][calcName] = CalculationClass;
49
+ BACKTEST_CALC_NAMES.add(calcName);
50
+ }
51
+ continue;
52
+ }
53
+
40
54
 
41
- // 2. Process other categories (e.g., 'pnl', 'capital_flow')
55
+ // 3. Process other categories (e.g., 'pnl', 'capital_flow')
42
56
  for (const subKey in calculations[category]) {
43
57
  const item = calculations[category][subKey];
44
58
 
@@ -71,8 +85,10 @@ function categorizeCalculations(calculations) {
71
85
  historicalCalculations,
72
86
  dailyCalculations,
73
87
  metaCalculations,
88
+ backtestCalculations, // <-- ADD THIS
74
89
  HISTORICAL_CALC_NAMES,
75
- META_CALC_NAMES
90
+ META_CALC_NAMES,
91
+ BACKTEST_CALC_NAMES // <-- ADD THIS
76
92
  };
77
93
  }
78
94
  // --- End Dynamic Categorization ---
@@ -1,9 +1,63 @@
1
1
  /**
2
2
  * @fileoverview API sub-pipes.
3
3
  * REFACTORED: Now stateless and receive dependencies.
4
+ * NEW: Added getDynamicSchema to "test run" calculations
5
+ * by mocking async dependencies.
4
6
  */
5
7
 
6
8
  const { FieldPath } = require('@google-cloud/firestore');
9
+ // --- NEW: Import calculation utils for mocking ---
10
+ // We import 'aiden-shared-calculations-unified' to access its 'utils'
11
+ const { utils } = require('aiden-shared-calculations-unified');
12
+
13
+ // --- NEW: Store original utils ---
14
+ const originalLoadMappings = utils.loadInstrumentMappings;
15
+ const originalLoadPrices = utils.loadAllPriceData;
16
+ const originalGetSectorMap = utils.getInstrumentSectorMap;
17
+
18
+ // --- NEW: Define Mocks ---
19
+ // This mock data will be "injected" into the calculations during the test run
20
+ const mockMappings = { instrumentToTicker: { 1: 'TEST_TICKER', 2: 'ANOTHER' }, instrumentToSector: { 1: 'Test Sector', 2: 'Other' } };
21
+ const mockPrices = { 1: { '2025-01-01': 100, '2025-01-02': 102 } };
22
+
23
+ const mockPos = { InstrumentID: 1, NetProfit: 0.05, InvestedAmount: 50, Amount: 1000, Value: 55, Direction: 'Buy', IsBuy: true, PositionID: 123, OpenRate: 100, StopLossRate: 90, TakeProfitRate: 120, Leverage: 1, IsTslEnabled: false, OpenDateTime: '2025-01-01T12:00:00Z', CurrentRate: 105 };
24
+ const mockToday = { AggregatedPositions: [mockPos, { ...mockPos, InstrumentID: 2 }], PublicPositions: [mockPos, { ...mockPos, InstrumentID: 2 }], PortfolioValue: 110 };
25
+ const mockYesterday = { AggregatedPositions: [mockPos], PublicPositions: [mockPos], PortfolioValue: 100 };
26
+ const mockInsights = { insights: [{ instrumentId: 1, total: 100, buy: 50, sell: 50 }] };
27
+ const mockSocial = { 'post1': { tickers: ['TEST_TICKER'], sentiment: { overallSentiment: 'Bullish', topics: ['AI'] }, likeCount: 5, commentCount: 2, fullText: 'TEST_TICKER AI' } };
28
+
29
+ // A mock context that's passed to process()
30
+ const mockContext = {
31
+ instrumentMappings: mockMappings.instrumentToTicker,
32
+ sectorMapping: mockMappings.instrumentToSector,
33
+ todayDateStr: '2025-01-02',
34
+ yesterdayDateStr: '2025-01-01',
35
+ dependencies: { // For meta-calcs that read other calc results
36
+ db: { // Mock the DB to return fake data
37
+ collection: function() { return this; },
38
+ doc: function() { return this; },
39
+ get: async () => ({
40
+ exists: true,
41
+ data: () => ({ /* mock data for meta-calc deps */
42
+ 'asset-crowd-flow': { 'TEST_TICKER': { net_crowd_flow_pct: 1.5 } },
43
+ 'social_sentiment_aggregation': { 'tickerSentiment': { 'TEST_TICKER': { sentimentRatio: 80 } } },
44
+ 'daily_investor_scores': { 'user123': 8.5 }
45
+ })
46
+ }),
47
+ getAll: async (...refs) => refs.map(ref => ({
48
+ exists: true,
49
+ data: () => ({ /* mock data for meta-calc deps */
50
+ 'asset-crowd-flow': { 'TEST_TICKER': { net_crowd_flow_pct: 1.5 } },
51
+ 'social_sentiment_aggregation': { 'tickerSentiment': { 'TEST_TICKER': { sentimentRatio: 80 } } }
52
+ })
53
+ }))
54
+ },
55
+ logger: { log: () => {} } // Suppress logs during test run
56
+ },
57
+ config: {} // For meta-calcs
58
+ };
59
+ // --- END NEW MOCKS ---
60
+
7
61
 
8
62
  /**
9
63
  * Sub-pipe: pipe.api.helpers.validateRequest
@@ -31,10 +85,20 @@ const validateRequest = (query, config) => {
31
85
  const buildCalculationMap = (unifiedCalculations) => {
32
86
  const calcMap = {};
33
87
  for (const category in unifiedCalculations) {
34
- for (const calcName in unifiedCalculations[category]) {
35
- calcMap[calcName] = {
36
- category: category,
37
- };
88
+ for (const subKey in unifiedCalculations[category]) {
89
+ const item = unifiedCalculations[category][subKey];
90
+
91
+ // Handle historical subdirectory
92
+ if (subKey === 'historical' && typeof item === 'object') {
93
+ for (const calcName in item) {
94
+ calcMap[calcName] = { category: category };
95
+ }
96
+ }
97
+ // Handle regular daily/meta/social calc
98
+ else if (typeof item === 'function') {
99
+ const calcName = subKey;
100
+ calcMap[calcName] = { category: category };
101
+ }
38
102
  }
39
103
  }
40
104
  return calcMap;
@@ -157,16 +221,31 @@ const createApiHandler = (config, dependencies, calcMap) => {
157
221
  */
158
222
  function createStructureSnippet(data, maxKeys = 20) {
159
223
  if (data === null || typeof data !== 'object') {
224
+ // Handle primitive types
225
+ if (typeof data === 'number') return 0;
226
+ if (typeof data === 'string') return "string";
227
+ if (typeof data === 'boolean') return true;
160
228
  return data;
161
229
  }
162
230
  if (Array.isArray(data)) {
163
231
  if (data.length === 0) {
164
232
  return "<empty array>";
165
233
  }
234
+ // Generalize array contents to just the first element's structure
166
235
  return [ createStructureSnippet(data[0], maxKeys) ];
167
236
  }
168
237
  const newObj = {};
169
238
  const keys = Object.keys(data);
239
+
240
+ // Check if it's an "example" object (like { "AAPL": {...} })
241
+ // This heuristic identifies keys that are all-caps or look like example tickers
242
+ if (keys.length > 0 && keys.every(k => k.match(/^[A-Z.]+$/) || k.includes('_') || k.match(/^[0-9]+$/))) {
243
+ const exampleKey = keys[0];
244
+ newObj[exampleKey] = createStructureSnippet(data[exampleKey], maxKeys);
245
+ newObj["... (more items)"] = "...";
246
+ return newObj;
247
+ }
248
+
170
249
  if (keys.length > maxKeys) {
171
250
  const firstKey = keys[0] || "example_key";
172
251
  newObj[firstKey] = createStructureSnippet(data[firstKey], maxKeys);
@@ -181,11 +260,7 @@ function createStructureSnippet(data, maxKeys = 20) {
181
260
 
182
261
  /**
183
262
  * Sub-pipe: pipe.api.helpers.getComputationStructure
184
- * @param {string} computationName
185
- * @param {object} calcMap
186
- * @param {object} config
187
- * @param {object} dependencies - Contains db, logger.
188
- * @returns {Promise<object>} A result object with status and data or error.
263
+ * (This is now a debug tool to check *live* data)
189
264
  */
190
265
  async function getComputationStructure(computationName, calcMap, config, dependencies) {
191
266
  const { db, logger } = dependencies;
@@ -244,10 +319,62 @@ async function getComputationStructure(computationName, calcMap, config, depende
244
319
  }
245
320
 
246
321
 
322
+ /**
323
+ * --- NEW: DYNAMIC SCHEMA GENERATION HARNESS ---
324
+ * @param {class} CalcClass The calculation class to test.
325
+ * @param {string} calcName The name of the calculation for logging.
326
+ * @returns {Promise<object>} A snippet of the output structure.
327
+ */
328
+ async function getDynamicSchema(CalcClass, calcName) {
329
+ // 1. Apply Mocks (Monkey-Patching)
330
+ utils.loadInstrumentMappings = async () => mockMappings;
331
+ utils.loadAllPriceData = async () => mockPrices;
332
+ utils.getInstrumentSectorMap = async () => mockMappings.instrumentToSector;
333
+
334
+ let result = {};
335
+ const calc = new CalcClass();
336
+
337
+ try {
338
+ // 2. Check for Meta-Calculation signature: process(dateStr, dependencies, config)
339
+ const processStr = calc.process.toString();
340
+ if (processStr.includes('dateStr') && processStr.includes('dependencies')) {
341
+ // It's a meta-calc. Run its process() with mock dependencies
342
+ result = await calc.process('2025-01-02', mockContext.dependencies, mockContext.config);
343
+ } else {
344
+ // It's a standard calculation. Run process() + getResult()
345
+ await calc.process(
346
+ mockToday,
347
+ mockYesterday,
348
+ 'test-user-123',
349
+ mockContext,
350
+ mockInsights, // todayInsights
351
+ mockInsights, // yesterdayInsights
352
+ mockSocial, // todaySocial
353
+ mockSocial // yesterdaySocial
354
+ );
355
+ result = await calc.getResult();
356
+ }
357
+ } catch (e) {
358
+ console.error(`Error running schema test for ${calcName}: ${e.message}`);
359
+ result = { "ERROR": `Failed to generate schema: ${e.message}` };
360
+ } finally {
361
+ // 3. Restore Original Functions
362
+ utils.loadInstrumentMappings = originalLoadMappings;
363
+ utils.loadAllPriceData = originalLoadPrices;
364
+ utils.getInstrumentSectorMap = originalGetSectorMap;
365
+ }
366
+
367
+ // 4. Sanitize the result to just a "structure"
368
+ return createStructureSnippet(result);
369
+ }
370
+ // --- END NEW HARNESS ---
371
+
372
+
247
373
  module.exports = {
248
374
  validateRequest,
249
375
  buildCalculationMap,
250
376
  fetchUnifiedData,
251
377
  createApiHandler,
252
378
  getComputationStructure,
253
- };
379
+ getDynamicSchema // <-- EXPORT NEW HELPER
380
+ };
@@ -1,7 +1,8 @@
1
1
  /**
2
2
  * @fileoverview Main entry point for the Generic API module.
3
3
  * Exports the 'createApiApp' main pipe function.
4
- * REFACTORED: 'createApiApp' now passes dependencies to sub-pipes.
4
+ * REFACTORED: /manifest endpoint now uses a dynamic "test harness"
5
+ * to generate the schema without modifying calculation files.
5
6
  */
6
7
 
7
8
  const express = require('express');
@@ -10,7 +11,8 @@ const { FieldPath } = require('@google-cloud/firestore');
10
11
  const {
11
12
  buildCalculationMap,
12
13
  createApiHandler,
13
- getComputationStructure
14
+ getComputationStructure,
15
+ getDynamicSchema // <-- IMPORT NEW HELPER
14
16
  } = require('./helpers/api_helpers.js');
15
17
 
16
18
  /**
@@ -18,12 +20,12 @@ const {
18
20
  * Creates and configures the Express app for the Generic API.
19
21
  * @param {object} config - The Generic API V2 configuration object.
20
22
  * @param {object} dependencies - Shared dependencies { db, logger }.
21
- * @param {Object} unifiedCalculations - The calculations manifest.
23
+ * @param {Object} unifiedCalculations - The calculations manifest from 'aiden-shared-calculations-unified'.
22
24
  * @returns {express.Application} The configured Express app.
23
25
  */
24
26
  function createApiApp(config, dependencies, unifiedCalculations) {
25
27
  const app = express();
26
- const { logger } = dependencies; // db is inside dependencies
28
+ const { logger } = dependencies; // db is in dependencies
27
29
 
28
30
  // --- Pre-compute Calculation Map ---
29
31
  const calcMap = buildCalculationMap(unifiedCalculations);
@@ -33,7 +35,6 @@ function createApiApp(config, dependencies, unifiedCalculations) {
33
35
  app.use(express.json());
34
36
 
35
37
  // --- Main API Endpoint ---
36
- // createApiHandler is a factory that returns the actual handler
37
38
  app.get('/', createApiHandler(config, dependencies, calcMap));
38
39
 
39
40
  // --- Health Check Endpoint ---
@@ -56,7 +57,7 @@ function createApiApp(config, dependencies, unifiedCalculations) {
56
57
  }
57
58
  });
58
59
 
59
- // --- REVISED: Debug Endpoint to get the structure from Firestore ---
60
+ // --- Debug Endpoint to get *stored* structure from Firestore ---
60
61
  app.get('/structure/:computationName', async (req, res) => {
61
62
  const { computationName } = req.params;
62
63
 
@@ -71,56 +72,67 @@ function createApiApp(config, dependencies, unifiedCalculations) {
71
72
  res.status(200).send(result);
72
73
  });
73
74
 
74
- // --- NEW: Debug Endpoint to generate the full structure manifest ---
75
+ // --- NEW: Fully Refactored Manifest Endpoint (Dynamic Test Run) ---
75
76
  app.get('/manifest', async (req, res) => {
77
+ logger.log('INFO', 'API /manifest dynamic generation starting...');
76
78
  try {
77
- const allKeys = Object.keys(calcMap).sort();
78
-
79
- const promises = allKeys.map(key =>
80
- // Call sub-pipe, passing dependencies
81
- getComputationStructure(key, calcMap, config, dependencies)
82
- );
83
-
84
- const results = await Promise.allSettled(promises);
85
-
86
79
  const manifest = {};
87
- const errors = [];
88
80
  let successCount = 0;
89
-
90
- results.forEach(result => {
91
- if (result.status === 'fulfilled' && result.value.status === 'success') {
92
- const data = result.value;
93
- manifest[data.computation] = {
94
- category: data.category,
95
- latestStoredDate: data.latestStoredDate,
96
- structureSnippet: data.structureSnippet,
97
- };
98
- successCount++;
99
- } else if (result.status === 'fulfilled') {
100
- const errorData = result.value;
101
- errors.push({
102
- computation: errorData.computation,
103
- message: errorData.message,
104
- });
105
- } else {
106
- errors.push({
107
- computation: "Unknown (Promise rejected)",
108
- message: result.reason.message,
109
- });
81
+ const errors = [];
82
+
83
+ // This logic iterates through the calculation module structure
84
+ for (const category in unifiedCalculations) {
85
+ for (const subKey in unifiedCalculations[category]) {
86
+ const item = unifiedCalculations[category][subKey];
87
+ let calcName = null;
88
+ let CalcClass = null;
89
+
90
+ // Handle nested 'historical' directory
91
+ if (subKey === 'historical' && typeof item === 'object') {
92
+ for (const name in item) {
93
+ calcName = name;
94
+ CalcClass = item[name];
95
+ if (CalcClass && typeof CalcClass === 'function') {
96
+ try {
97
+ manifest[calcName] = {
98
+ category: category,
99
+ structure: await getDynamicSchema(CalcClass, calcName) // <-- DYNAMIC CALL
100
+ };
101
+ successCount++;
102
+ } catch (e) { errors.push(`${category}/${calcName}: ${e.message}`); }
103
+ }
104
+ }
105
+ }
106
+ // Handle regular calc at root of category
107
+ else if (typeof item === 'function') {
108
+ calcName = subKey;
109
+ CalcClass = item;
110
+ if (CalcClass && typeof CalcClass === 'function') {
111
+ try {
112
+ manifest[calcName] = {
113
+ category: category,
114
+ structure: await getDynamicSchema(CalcClass, calcName) // <-- DYNAMIC CALL
115
+ };
116
+ successCount++;
117
+ } catch (e) { errors.push(`${category}/${calcName}: ${e.message}`); }
118
+ }
119
+ }
110
120
  }
111
- });
121
+ }
112
122
 
113
- logger.log('INFO', `API /manifest complete. ${successCount}/${allKeys.length} computations successful.`);
123
+ const totalComputations = Object.keys(calcMap).length;
124
+ logger.log('INFO', `API /manifest complete. Generated schema for ${successCount}/${totalComputations} computations.`);
114
125
 
115
126
  res.status(200).send({
116
127
  status: 'success',
117
128
  summary: {
118
- totalComputations: allKeys.length,
119
- successful: successCount,
120
- failed: errors.length,
129
+ source: 'computation_module_dynamic_test',
130
+ totalComputations: totalComputations,
131
+ schemasGenerated: successCount,
132
+ schemasFailed: errors.length,
121
133
  },
122
- manifest,
123
- errors,
134
+ manifest: manifest,
135
+ errors: errors.length > 0 ? errors : undefined,
124
136
  });
125
137
 
126
138
  } catch (error) {
@@ -136,4 +148,4 @@ module.exports = {
136
148
  createApiApp,
137
149
  // Exporting helpers so they can be part of the pipe.api.helpers object
138
150
  helpers: require('./helpers/api_helpers'),
139
- };
151
+ };
@@ -4,11 +4,9 @@
4
4
  * candle API into the new sharded `asset_prices` collection.
5
5
  */
6
6
 
7
- const { FieldValue } = require('@google-cloud/firestore');
8
- // --- REMOVED ---
9
- // const { loadInstrumentMappings } = require('aiden-shared-calculations-unified').utils;
10
- // --- END REMOVED ---
11
- const pLimit = require('p-limit');
7
+ const { FieldValue } = require('@google-cloud/firestore'); // Todo inject this
8
+
9
+ const pLimit = require('p-limit'); // TODO inject this
12
10
 
13
11
  // How many tickers to fetch in parallel
14
12
  const CONCURRENT_REQUESTS = 10;
@@ -54,7 +52,7 @@ exports.runBackfillAssetPrices = async (config, dependencies) => {
54
52
  return limit(async () => {
55
53
  try {
56
54
  const ticker = mappings.instrumentToTicker[instrumentId] || `unknown_${instrumentId}`;
57
- const url = `https://candle.etoro.com/candles/asc.json/OneDay/${DAYS_TO_FETCH}/${instrumentId}`;
55
+ const url = `https://candle.etoro.com/candles/asc.json/OneDay/${DAYS_TO_FETCH}/${instrumentId}`; //TODO implement config value
58
56
 
59
57
  const selectedHeader = await headerManager.selectHeader();
60
58
  let wasSuccess = false;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bulltrackers-module",
3
- "version": "1.0.94",
3
+ "version": "1.0.96",
4
4
  "description": "Helper Functions for Bulltrackers.",
5
5
  "main": "index.js",
6
6
  "files": [