bulltrackers-module 1.0.105 → 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,380 +1,380 @@
1
- /**
2
- * @fileoverview API sub-pipes.
3
- * REFACTORED: Now stateless and receive dependencies.
4
- * NEW: Added getDynamicSchema to "test run" calculations
5
- * by mocking async dependencies.
6
- */
7
-
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
-
61
-
62
- /**
63
- * Sub-pipe: pipe.api.helpers.validateRequest
64
- */
65
- const validateRequest = (query, config) => {
66
- if (!query.computations) return "Missing 'computations' parameter.";
67
- if (!query.startDate || !/^\d{4}-\d{2}-\d{2}$/.test(query.startDate)) return "Missing or invalid 'startDate'.";
68
- if (!query.endDate || !/^\d{4}-\d{2}-\d{2}$/.test(query.endDate)) return "Missing or invalid 'endDate'.";
69
-
70
- const start = new Date(query.startDate);
71
- const end = new Date(query.endDate);
72
- if (end < start) return "'endDate' must be after 'startDate'.";
73
-
74
- const maxDateRange = config.maxDateRange || 100;
75
- const diffTime = Math.abs(end - start);
76
- const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)) + 1;
77
- if (diffDays > maxDateRange) return `Date range cannot exceed ${maxDateRange} days.`;
78
-
79
- return null;
80
- };
81
-
82
- /**
83
- * Sub-pipe: pipe.api.helpers.buildCalculationMap
84
- */
85
- const buildCalculationMap = (unifiedCalculations) => {
86
- const calcMap = {};
87
- for (const category in unifiedCalculations) {
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
- }
102
- }
103
- }
104
- return calcMap;
105
- };
106
-
107
- /**
108
- * Internal helper for date strings.
109
- */
110
- const getDateStringsInRange = (startDate, endDate) => {
111
- const dates = [];
112
- const current = new Date(startDate + 'T00:00:00Z');
113
- const end = new Date(endDate + 'T00:00:00Z');
114
-
115
- while (current <= end) {
116
- dates.push(current.toISOString().slice(0, 10));
117
- current.setUTCDate(current.getUTCDate() + 1);
118
- }
119
- return dates;
120
- };
121
-
122
- /**
123
- * Sub-pipe: pipe.api.helpers.fetchData
124
- * @param {object} config - The Generic API V2 configuration object.
125
- * @param {object} dependencies - Contains db, logger.
126
- * @param {string[]} calcKeys - Array of computation keys to fetch.
127
- * @param {string[]} dateStrings - Array of dates to fetch for.
128
- * @param {Object} calcMap - The pre-built calculation lookup map.
129
- * @returns {Promise<Object>} A nested object of [date][computationKey] = data.
130
- */
131
- const fetchUnifiedData = async (config, dependencies, calcKeys, dateStrings, calcMap) => {
132
- const { db, logger } = dependencies;
133
- const response = {};
134
- const insightsCollection = config.unifiedInsightsCollection || 'unified_insights';
135
- const resultsSub = config.resultsSubcollection || 'results';
136
- const compsSub = config.computationsSubcollection || 'computations';
137
-
138
- try {
139
- for (const date of dateStrings) {
140
- response[date] = {};
141
- const docRefs = [];
142
- const keyPaths = [];
143
-
144
- for (const key of calcKeys) {
145
- const pathInfo = calcMap[key];
146
- if (pathInfo) {
147
- // Use db from dependencies
148
- const docRef = db.collection(insightsCollection).doc(date)
149
- .collection(resultsSub).doc(pathInfo.category)
150
- .collection(compsSub).doc(key);
151
-
152
- docRefs.push(docRef);
153
- keyPaths.push(key);
154
- } else {
155
- logger.log('WARN', `[${date}] No path info found for computation key: ${key}`);
156
- }
157
- }
158
-
159
- if (docRefs.length === 0) continue;
160
-
161
- // Use db from dependencies
162
- const snapshots = await db.getAll(...docRefs);
163
- snapshots.forEach((doc, i) => {
164
- const key = keyPaths[i];
165
- if (doc.exists) {
166
- response[date][key] = doc.data();
167
- } else {
168
- response[date][key] = null;
169
- }
170
- });
171
- }
172
- } catch (error) {
173
- logger.log('ERROR', 'API: Error fetching data from Firestore.', { errorMessage: error.message });
174
- throw new Error('Failed to retrieve computation data.');
175
- }
176
- return response;
177
- };
178
-
179
- /**
180
- * Factory for the main API handler.
181
- * @param {object} config - The Generic API V2 configuration object.
182
- * @param {object} dependencies - Contains db, logger.
183
- * @param {Object} calcMap - The pre-built calculation lookup map.
184
- * @returns {Function} An async Express request handler.
185
- */
186
- const createApiHandler = (config, dependencies, calcMap) => {
187
- const { logger } = dependencies; // db is in dependencies
188
-
189
- return async (req, res) => {
190
- const validationError = validateRequest(req.query, config);
191
- if (validationError) {
192
- logger.log('WARN', 'API Bad Request', { error: validationError, query: req.query });
193
- return res.status(400).send({ status: 'error', message: validationError });
194
- }
195
-
196
- try {
197
- const computationKeys = req.query.computations.split(',');
198
- const dateStrings = getDateStringsInRange(req.query.startDate, req.query.endDate);
199
-
200
- // Pass dependencies to sub-pipe
201
- const data = await fetchUnifiedData(config, dependencies, computationKeys, dateStrings, calcMap);
202
-
203
- res.status(200).send({
204
- status: 'success',
205
- metadata: {
206
- computations: computationKeys,
207
- startDate: req.query.startDate,
208
- endDate: req.query.endDate,
209
- },
210
- data,
211
- });
212
- } catch (error) {
213
- logger.log('ERROR', 'API processing failed.', { errorMessage: error.message, stack: error.stack });
214
- res.status(500).send({ status: 'error', message: 'An internal error occurred.' });
215
- }
216
- };
217
- };
218
-
219
- /**
220
- * Internal helper for snippet generation.
221
- */
222
- function createStructureSnippet(data, maxKeys = 20) {
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;
228
- return data;
229
- }
230
- if (Array.isArray(data)) {
231
- if (data.length === 0) {
232
- return "<empty array>";
233
- }
234
- // Generalize array contents to just the first element's structure
235
- return [ createStructureSnippet(data[0], maxKeys) ];
236
- }
237
- const newObj = {};
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
-
249
- if (keys.length > maxKeys) {
250
- const firstKey = keys[0] || "example_key";
251
- newObj[firstKey] = createStructureSnippet(data[firstKey], maxKeys);
252
- newObj[`... (${keys.length - 1} more keys)`] = "<object>";
253
- } else {
254
- for (const key of keys) {
255
- newObj[key] = createStructureSnippet(data[key], maxKeys);
256
- }
257
- }
258
- return newObj;
259
- }
260
-
261
- /**
262
- * Sub-pipe: pipe.api.helpers.getComputationStructure
263
- * (This is now a debug tool to check *live* data)
264
- */
265
- async function getComputationStructure(computationName, calcMap, config, dependencies) {
266
- const { db, logger } = dependencies;
267
- try {
268
- const pathInfo = calcMap[computationName];
269
- if (!pathInfo) {
270
- return { status: 'error', computation: computationName, message: `Computation not found in calculation map.` };
271
- }
272
- const { category } = pathInfo;
273
-
274
- const insightsCollection = config.unifiedInsightsCollection || 'unified_insights';
275
- const resultsSub = config.resultsSubcollection || 'results';
276
- const compsSub = config.computationsSubcollection || 'computations';
277
-
278
- const computationQueryPath = `${category}.${computationName}`;
279
- // Use db from dependencies
280
- const dateQuery = db.collection(insightsCollection)
281
- .where(computationQueryPath, '==', true)
282
- .orderBy(FieldPath.documentId(), 'desc')
283
- .limit(1);
284
-
285
- const dateSnapshot = await dateQuery.get();
286
-
287
- if (dateSnapshot.empty) {
288
- return { status: 'error', computation: computationName, message: `No computed data found. (Query path: ${computationQueryPath})` };
289
- }
290
-
291
- const latestStoredDate = dateSnapshot.docs[0].id;
292
-
293
- // Use db from dependencies
294
- const docRef = db.collection(insightsCollection).doc(latestStoredDate)
295
- .collection(resultsSub).doc(category)
296
- .collection(compsSub).doc(computationName);
297
-
298
- const doc = await docRef.get();
299
-
300
- if (!doc.exists) {
301
- return { status: 'error', computation: computationName, message: `Summary flag was present for ${latestStoredDate} but doc is missing.` };
302
- }
303
-
304
- const fullData = doc.data();
305
- const structureSnippet = createStructureSnippet(fullData);
306
-
307
- return {
308
- status: 'success',
309
- computation: computationName,
310
- category: category,
311
- latestStoredDate: latestStoredDate,
312
- structureSnippet: structureSnippet,
313
- };
314
-
315
- } catch (error) {
316
- logger.log('ERROR', `API /structure/${computationName} helper failed.`, { errorMessage: error.message });
317
- return { status: 'error', computation: computationName, message: error.message };
318
- }
319
- }
320
-
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
-
373
- module.exports = {
374
- validateRequest,
375
- buildCalculationMap,
376
- fetchUnifiedData,
377
- createApiHandler,
378
- getComputationStructure,
379
- getDynamicSchema // <-- EXPORT NEW HELPER
1
+ /**
2
+ * @fileoverview API sub-pipes.
3
+ * REFACTORED: Now stateless and receive dependencies.
4
+ * NEW: Added getDynamicSchema to "test run" calculations
5
+ * by mocking async dependencies.
6
+ */
7
+
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
+
61
+
62
+ /**
63
+ * Sub-pipe: pipe.api.helpers.validateRequest
64
+ */
65
+ const validateRequest = (query, config) => {
66
+ if (!query.computations) return "Missing 'computations' parameter.";
67
+ if (!query.startDate || !/^\d{4}-\d{2}-\d{2}$/.test(query.startDate)) return "Missing or invalid 'startDate'.";
68
+ if (!query.endDate || !/^\d{4}-\d{2}-\d{2}$/.test(query.endDate)) return "Missing or invalid 'endDate'.";
69
+
70
+ const start = new Date(query.startDate);
71
+ const end = new Date(query.endDate);
72
+ if (end < start) return "'endDate' must be after 'startDate'.";
73
+
74
+ const maxDateRange = config.maxDateRange || 100;
75
+ const diffTime = Math.abs(end - start);
76
+ const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)) + 1;
77
+ if (diffDays > maxDateRange) return `Date range cannot exceed ${maxDateRange} days.`;
78
+
79
+ return null;
80
+ };
81
+
82
+ /**
83
+ * Sub-pipe: pipe.api.helpers.buildCalculationMap
84
+ */
85
+ const buildCalculationMap = (unifiedCalculations) => {
86
+ const calcMap = {};
87
+ for (const category in unifiedCalculations) {
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
+ }
102
+ }
103
+ }
104
+ return calcMap;
105
+ };
106
+
107
+ /**
108
+ * Internal helper for date strings.
109
+ */
110
+ const getDateStringsInRange = (startDate, endDate) => {
111
+ const dates = [];
112
+ const current = new Date(startDate + 'T00:00:00Z');
113
+ const end = new Date(endDate + 'T00:00:00Z');
114
+
115
+ while (current <= end) {
116
+ dates.push(current.toISOString().slice(0, 10));
117
+ current.setUTCDate(current.getUTCDate() + 1);
118
+ }
119
+ return dates;
120
+ };
121
+
122
+ /**
123
+ * Sub-pipe: pipe.api.helpers.fetchData
124
+ * @param {object} config - The Generic API V2 configuration object.
125
+ * @param {object} dependencies - Contains db, logger.
126
+ * @param {string[]} calcKeys - Array of computation keys to fetch.
127
+ * @param {string[]} dateStrings - Array of dates to fetch for.
128
+ * @param {Object} calcMap - The pre-built calculation lookup map.
129
+ * @returns {Promise<Object>} A nested object of [date][computationKey] = data.
130
+ */
131
+ const fetchUnifiedData = async (config, dependencies, calcKeys, dateStrings, calcMap) => {
132
+ const { db, logger } = dependencies;
133
+ const response = {};
134
+ const insightsCollection = config.unifiedInsightsCollection || 'unified_insights';
135
+ const resultsSub = config.resultsSubcollection || 'results';
136
+ const compsSub = config.computationsSubcollection || 'computations';
137
+
138
+ try {
139
+ for (const date of dateStrings) {
140
+ response[date] = {};
141
+ const docRefs = [];
142
+ const keyPaths = [];
143
+
144
+ for (const key of calcKeys) {
145
+ const pathInfo = calcMap[key];
146
+ if (pathInfo) {
147
+ // Use db from dependencies
148
+ const docRef = db.collection(insightsCollection).doc(date)
149
+ .collection(resultsSub).doc(pathInfo.category)
150
+ .collection(compsSub).doc(key);
151
+
152
+ docRefs.push(docRef);
153
+ keyPaths.push(key);
154
+ } else {
155
+ logger.log('WARN', `[${date}] No path info found for computation key: ${key}`);
156
+ }
157
+ }
158
+
159
+ if (docRefs.length === 0) continue;
160
+
161
+ // Use db from dependencies
162
+ const snapshots = await db.getAll(...docRefs);
163
+ snapshots.forEach((doc, i) => {
164
+ const key = keyPaths[i];
165
+ if (doc.exists) {
166
+ response[date][key] = doc.data();
167
+ } else {
168
+ response[date][key] = null;
169
+ }
170
+ });
171
+ }
172
+ } catch (error) {
173
+ logger.log('ERROR', 'API: Error fetching data from Firestore.', { errorMessage: error.message });
174
+ throw new Error('Failed to retrieve computation data.');
175
+ }
176
+ return response;
177
+ };
178
+
179
+ /**
180
+ * Factory for the main API handler.
181
+ * @param {object} config - The Generic API V2 configuration object.
182
+ * @param {object} dependencies - Contains db, logger.
183
+ * @param {Object} calcMap - The pre-built calculation lookup map.
184
+ * @returns {Function} An async Express request handler.
185
+ */
186
+ const createApiHandler = (config, dependencies, calcMap) => {
187
+ const { logger } = dependencies; // db is in dependencies
188
+
189
+ return async (req, res) => {
190
+ const validationError = validateRequest(req.query, config);
191
+ if (validationError) {
192
+ logger.log('WARN', 'API Bad Request', { error: validationError, query: req.query });
193
+ return res.status(400).send({ status: 'error', message: validationError });
194
+ }
195
+
196
+ try {
197
+ const computationKeys = req.query.computations.split(',');
198
+ const dateStrings = getDateStringsInRange(req.query.startDate, req.query.endDate);
199
+
200
+ // Pass dependencies to sub-pipe
201
+ const data = await fetchUnifiedData(config, dependencies, computationKeys, dateStrings, calcMap);
202
+
203
+ res.status(200).send({
204
+ status: 'success',
205
+ metadata: {
206
+ computations: computationKeys,
207
+ startDate: req.query.startDate,
208
+ endDate: req.query.endDate,
209
+ },
210
+ data,
211
+ });
212
+ } catch (error) {
213
+ logger.log('ERROR', 'API processing failed.', { errorMessage: error.message, stack: error.stack });
214
+ res.status(500).send({ status: 'error', message: 'An internal error occurred.' });
215
+ }
216
+ };
217
+ };
218
+
219
+ /**
220
+ * Internal helper for snippet generation.
221
+ */
222
+ function createStructureSnippet(data, maxKeys = 20) {
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;
228
+ return data;
229
+ }
230
+ if (Array.isArray(data)) {
231
+ if (data.length === 0) {
232
+ return "<empty array>";
233
+ }
234
+ // Generalize array contents to just the first element's structure
235
+ return [ createStructureSnippet(data[0], maxKeys) ];
236
+ }
237
+ const newObj = {};
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
+
249
+ if (keys.length > maxKeys) {
250
+ const firstKey = keys[0] || "example_key";
251
+ newObj[firstKey] = createStructureSnippet(data[firstKey], maxKeys);
252
+ newObj[`... (${keys.length - 1} more keys)`] = "<object>";
253
+ } else {
254
+ for (const key of keys) {
255
+ newObj[key] = createStructureSnippet(data[key], maxKeys);
256
+ }
257
+ }
258
+ return newObj;
259
+ }
260
+
261
+ /**
262
+ * Sub-pipe: pipe.api.helpers.getComputationStructure
263
+ * (This is now a debug tool to check *live* data)
264
+ */
265
+ async function getComputationStructure(computationName, calcMap, config, dependencies) {
266
+ const { db, logger } = dependencies;
267
+ try {
268
+ const pathInfo = calcMap[computationName];
269
+ if (!pathInfo) {
270
+ return { status: 'error', computation: computationName, message: `Computation not found in calculation map.` };
271
+ }
272
+ const { category } = pathInfo;
273
+
274
+ const insightsCollection = config.unifiedInsightsCollection || 'unified_insights';
275
+ const resultsSub = config.resultsSubcollection || 'results';
276
+ const compsSub = config.computationsSubcollection || 'computations';
277
+
278
+ const computationQueryPath = `${category}.${computationName}`;
279
+ // Use db from dependencies
280
+ const dateQuery = db.collection(insightsCollection)
281
+ .where(computationQueryPath, '==', true)
282
+ .orderBy(FieldPath.documentId(), 'desc')
283
+ .limit(1);
284
+
285
+ const dateSnapshot = await dateQuery.get();
286
+
287
+ if (dateSnapshot.empty) {
288
+ return { status: 'error', computation: computationName, message: `No computed data found. (Query path: ${computationQueryPath})` };
289
+ }
290
+
291
+ const latestStoredDate = dateSnapshot.docs[0].id;
292
+
293
+ // Use db from dependencies
294
+ const docRef = db.collection(insightsCollection).doc(latestStoredDate)
295
+ .collection(resultsSub).doc(category)
296
+ .collection(compsSub).doc(computationName);
297
+
298
+ const doc = await docRef.get();
299
+
300
+ if (!doc.exists) {
301
+ return { status: 'error', computation: computationName, message: `Summary flag was present for ${latestStoredDate} but doc is missing.` };
302
+ }
303
+
304
+ const fullData = doc.data();
305
+ const structureSnippet = createStructureSnippet(fullData);
306
+
307
+ return {
308
+ status: 'success',
309
+ computation: computationName,
310
+ category: category,
311
+ latestStoredDate: latestStoredDate,
312
+ structureSnippet: structureSnippet,
313
+ };
314
+
315
+ } catch (error) {
316
+ logger.log('ERROR', `API /structure/${computationName} helper failed.`, { errorMessage: error.message });
317
+ return { status: 'error', computation: computationName, message: error.message };
318
+ }
319
+ }
320
+
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
+
373
+ module.exports = {
374
+ validateRequest,
375
+ buildCalculationMap,
376
+ fetchUnifiedData,
377
+ createApiHandler,
378
+ getComputationStructure,
379
+ getDynamicSchema // <-- EXPORT NEW HELPER
380
380
  };