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.
- package/functions/computation-system/helpers/orchestration_helpers.js +95 -21
- package/functions/computation-system/utils/utils.js +18 -2
- package/functions/generic-api/helpers/api_helpers.js +137 -10
- package/functions/generic-api/index.js +58 -46
- package/functions/price-backfill/helpers/handler_helpers.js +4 -6
- package/package.json +1 -1
|
@@ -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
|
|
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
|
|
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
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
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
|
-
|
|
486
|
-
|
|
557
|
+
}
|
|
558
|
+
// --- END SPECIAL HANDLING ---
|
|
487
559
|
|
|
488
|
-
|
|
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}
|
|
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
|
|
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
|
-
//
|
|
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
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
*
|
|
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:
|
|
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
|
|
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
|
-
// ---
|
|
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:
|
|
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
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
|
|
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
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
|
|
9
|
-
|
|
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;
|