bulltrackers-module 1.0.104 → 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.
- package/README.MD +222 -222
- package/functions/appscript-api/helpers/errors.js +19 -19
- package/functions/appscript-api/index.js +58 -58
- package/functions/computation-system/helpers/orchestration_helpers.js +647 -113
- package/functions/computation-system/utils/data_loader.js +191 -191
- package/functions/computation-system/utils/utils.js +149 -254
- package/functions/core/utils/firestore_utils.js +433 -433
- package/functions/core/utils/pubsub_utils.js +53 -53
- package/functions/dispatcher/helpers/dispatch_helpers.js +47 -47
- package/functions/dispatcher/index.js +52 -52
- package/functions/etoro-price-fetcher/helpers/handler_helpers.js +124 -124
- package/functions/fetch-insights/helpers/handler_helpers.js +91 -91
- package/functions/generic-api/helpers/api_helpers.js +379 -379
- package/functions/generic-api/index.js +150 -150
- package/functions/invalid-speculator-handler/helpers/handler_helpers.js +75 -75
- package/functions/orchestrator/helpers/discovery_helpers.js +226 -226
- package/functions/orchestrator/helpers/update_helpers.js +92 -92
- package/functions/orchestrator/index.js +147 -147
- package/functions/price-backfill/helpers/handler_helpers.js +116 -123
- package/functions/social-orchestrator/helpers/orchestrator_helpers.js +61 -61
- package/functions/social-task-handler/helpers/handler_helpers.js +288 -288
- package/functions/task-engine/handler_creator.js +78 -78
- package/functions/task-engine/helpers/discover_helpers.js +125 -125
- package/functions/task-engine/helpers/update_helpers.js +118 -118
- package/functions/task-engine/helpers/verify_helpers.js +162 -162
- package/functions/task-engine/utils/firestore_batch_manager.js +258 -258
- package/index.js +105 -113
- package/package.json +45 -45
- package/functions/computation-system/computation_dependencies.json +0 -120
- package/functions/computation-system/helpers/worker_helpers.js +0 -340
- package/functions/computation-system/utils/computation_state_manager.js +0 -178
- package/functions/computation-system/utils/dependency_graph.js +0 -191
- 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
|
};
|