bulltrackers-module 1.0.232 → 1.0.234
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.
|
@@ -104,6 +104,36 @@ class DataExtractor {
|
|
|
104
104
|
}
|
|
105
105
|
static getHasTSL(position) { return position ? (position.HasTrailingStopLoss === true) : false; }
|
|
106
106
|
static getOpenDateTime(position) { return (!position || !position.OpenDateTime) ? null : new Date(position.OpenDateTime); }
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Returns trades that were active during the given date's 24-hour window.
|
|
111
|
+
* @param {Array} historyTrades - The PublicHistoryPositions array.
|
|
112
|
+
* @param {string} dateStr - YYYY-MM-DD string.
|
|
113
|
+
*/
|
|
114
|
+
static getActiveTradesForDate(historyTrades, dateStr) {
|
|
115
|
+
if (!historyTrades || !Array.isArray(historyTrades)) return [];
|
|
116
|
+
|
|
117
|
+
// Define the day's window
|
|
118
|
+
const startTime = new Date(dateStr + "T00:00:00.000Z").getTime();
|
|
119
|
+
const endTime = new Date(dateStr + "T23:59:59.999Z").getTime();
|
|
120
|
+
|
|
121
|
+
return historyTrades.filter(t => {
|
|
122
|
+
if (!t.OpenDateTime) return false;
|
|
123
|
+
const openTime = new Date(t.OpenDateTime).getTime();
|
|
124
|
+
|
|
125
|
+
// 1. Must be opened before the day ended
|
|
126
|
+
if (openTime > endTime) return false;
|
|
127
|
+
|
|
128
|
+
// 2. If closed, must be closed after the day started
|
|
129
|
+
if (t.CloseDateTime) {
|
|
130
|
+
const closeTime = new Date(t.CloseDateTime).getTime();
|
|
131
|
+
if (closeTime < startTime) return false;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return true;
|
|
135
|
+
});
|
|
136
|
+
}
|
|
107
137
|
}
|
|
108
138
|
|
|
109
139
|
class priceExtractor {
|
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @fileoverview API sub-pipes.
|
|
3
|
-
* REFACTORED:
|
|
3
|
+
* REFACTORED: Fixed Category Resolution to match ManifestBuilder logic.
|
|
4
|
+
* Implements Status-Based Availability Caching and Smart Date Resolution.
|
|
4
5
|
*/
|
|
5
6
|
|
|
6
7
|
const { FieldPath } = require('@google-cloud/firestore');
|
|
7
8
|
|
|
8
9
|
// --- AVAILABILITY CACHE ---
|
|
9
|
-
// Maintains a map of which computations are available on which dates.
|
|
10
10
|
class AvailabilityCache {
|
|
11
11
|
constructor(db, logger, ttlMs = 5 * 60 * 1000) { // 5 Minute TTL
|
|
12
12
|
this.db = db;
|
|
13
13
|
this.logger = logger;
|
|
14
14
|
this.ttlMs = ttlMs;
|
|
15
|
-
this.cache = null;
|
|
15
|
+
this.cache = null;
|
|
16
16
|
this.lastFetched = 0;
|
|
17
17
|
this.statusCollection = 'computation_status';
|
|
18
18
|
}
|
|
@@ -25,8 +25,7 @@ class AvailabilityCache {
|
|
|
25
25
|
|
|
26
26
|
this.logger.log('INFO', '[AvailabilityCache] Refreshing availability map from Firestore...');
|
|
27
27
|
|
|
28
|
-
// Fetch
|
|
29
|
-
// We only fetch keys and small status objects, so this is relatively cheap.
|
|
28
|
+
// Fetch recent status to build the map (Limit 400 days)
|
|
30
29
|
const snapshot = await this.db.collection(this.statusCollection)
|
|
31
30
|
.orderBy(FieldPath.documentId(), 'desc')
|
|
32
31
|
.limit(400)
|
|
@@ -38,11 +37,9 @@ class AvailabilityCache {
|
|
|
38
37
|
const dateStr = doc.id;
|
|
39
38
|
const statusData = doc.data();
|
|
40
39
|
|
|
41
|
-
// Regex to validate date format YYYY-MM-DD
|
|
42
40
|
if (!/^\d{4}-\d{2}-\d{2}$/.test(dateStr)) return;
|
|
43
41
|
|
|
44
42
|
for (const [calcName, status] of Object.entries(statusData)) {
|
|
45
|
-
// We consider it available if status is truthy and NOT 'IMPOSSIBLE'
|
|
46
43
|
if (status && status !== 'IMPOSSIBLE') {
|
|
47
44
|
if (!newMap[calcName]) newMap[calcName] = [];
|
|
48
45
|
newMap[calcName].push(dateStr);
|
|
@@ -62,30 +59,22 @@ class AvailabilityCache {
|
|
|
62
59
|
*/
|
|
63
60
|
async function resolveTargetDates(availabilityCache, computationKeys, mode, limit) {
|
|
64
61
|
const map = await availabilityCache.getMap();
|
|
65
|
-
const datesToFetch = new Set();
|
|
66
|
-
|
|
67
|
-
// 1. Identify all available dates for the requested computations
|
|
68
|
-
// We union the dates: if ANY requested calc is available on a date, we consider that date "relevant".
|
|
69
|
-
// (Alternatively, we could intersect, but union allows sparse data return).
|
|
70
|
-
const relevantDatesSet = new Set();
|
|
71
62
|
|
|
63
|
+
const relevantDatesSet = new Set();
|
|
72
64
|
computationKeys.forEach(key => {
|
|
73
65
|
const dates = map[key] || [];
|
|
74
66
|
dates.forEach(d => relevantDatesSet.add(d));
|
|
75
67
|
});
|
|
76
68
|
|
|
77
|
-
// Sort descending
|
|
78
69
|
const sortedDates = Array.from(relevantDatesSet).sort((a, b) => b.localeCompare(a));
|
|
79
70
|
|
|
80
71
|
if (sortedDates.length === 0) return [];
|
|
81
72
|
|
|
82
73
|
if (mode === 'latest') {
|
|
83
|
-
// Return only the most recent date found
|
|
84
74
|
return [sortedDates[0]];
|
|
85
75
|
}
|
|
86
76
|
|
|
87
77
|
if (mode === 'series') {
|
|
88
|
-
// Return the last N available dates
|
|
89
78
|
return sortedDates.slice(0, limit);
|
|
90
79
|
}
|
|
91
80
|
|
|
@@ -98,7 +87,6 @@ async function resolveTargetDates(availabilityCache, computationKeys, mode, limi
|
|
|
98
87
|
const validateRequest = (query) => {
|
|
99
88
|
if (!query.computations) return "Missing 'computations' parameter.";
|
|
100
89
|
|
|
101
|
-
// New optional params, but computations is mandatory
|
|
102
90
|
const allowedModes = ['latest', 'series'];
|
|
103
91
|
if (query.mode && !allowedModes.includes(query.mode)) {
|
|
104
92
|
return "Invalid 'mode'. Must be 'latest' or 'series'.";
|
|
@@ -116,19 +104,41 @@ const validateRequest = (query) => {
|
|
|
116
104
|
|
|
117
105
|
/**
|
|
118
106
|
* Sub-pipe: pipe.api.helpers.buildCalculationMap
|
|
107
|
+
* FIX APPLIED: Checks class metadata for category override (mirroring ManifestBuilder).
|
|
119
108
|
*/
|
|
120
109
|
const buildCalculationMap = (unifiedCalculations) => {
|
|
121
110
|
const calcMap = {};
|
|
111
|
+
|
|
112
|
+
// Helper to resolve category exactly like ManifestBuilder.js does
|
|
113
|
+
const resolveCategory = (cls, folderName) => {
|
|
114
|
+
if (typeof cls.getMetadata === 'function') {
|
|
115
|
+
const meta = cls.getMetadata();
|
|
116
|
+
// If folder is 'core', allow metadata to override
|
|
117
|
+
if (folderName === 'core' && meta.category) {
|
|
118
|
+
return meta.category;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return folderName;
|
|
122
|
+
};
|
|
123
|
+
|
|
122
124
|
for (const category in unifiedCalculations) {
|
|
123
125
|
for (const subKey in unifiedCalculations[category]) {
|
|
124
126
|
const item = unifiedCalculations[category][subKey];
|
|
127
|
+
|
|
125
128
|
if (subKey === 'historical' && typeof item === 'object') {
|
|
126
129
|
for (const calcName in item) {
|
|
127
|
-
|
|
130
|
+
const cls = item[calcName];
|
|
131
|
+
calcMap[calcName] = {
|
|
132
|
+
category: resolveCategory(cls, category),
|
|
133
|
+
class: cls
|
|
134
|
+
};
|
|
128
135
|
}
|
|
129
136
|
} else if (typeof item === 'function') {
|
|
130
137
|
const calcName = subKey;
|
|
131
|
-
calcMap[calcName] = {
|
|
138
|
+
calcMap[calcName] = {
|
|
139
|
+
category: resolveCategory(item, category),
|
|
140
|
+
class: item
|
|
141
|
+
};
|
|
132
142
|
}
|
|
133
143
|
}
|
|
134
144
|
}
|
|
@@ -137,7 +147,6 @@ const buildCalculationMap = (unifiedCalculations) => {
|
|
|
137
147
|
|
|
138
148
|
/**
|
|
139
149
|
* Sub-pipe: pipe.api.helpers.fetchUnifiedData
|
|
140
|
-
* UPDATED: Uses specific date list derived from availability.
|
|
141
150
|
*/
|
|
142
151
|
const fetchUnifiedData = async (config, dependencies, calcKeys, dateStrings, calcMap) => {
|
|
143
152
|
const { db, logger } = dependencies;
|
|
@@ -151,9 +160,8 @@ const fetchUnifiedData = async (config, dependencies, calcKeys, dateStrings, cal
|
|
|
151
160
|
try {
|
|
152
161
|
const readPromises = [];
|
|
153
162
|
|
|
154
|
-
// Prepare all reads
|
|
155
163
|
for (const date of dateStrings) {
|
|
156
|
-
response[date] = {};
|
|
164
|
+
response[date] = {};
|
|
157
165
|
|
|
158
166
|
for (const key of calcKeys) {
|
|
159
167
|
const pathInfo = calcMap[key];
|
|
@@ -163,13 +171,16 @@ const fetchUnifiedData = async (config, dependencies, calcKeys, dateStrings, cal
|
|
|
163
171
|
.collection(compsSub).doc(key);
|
|
164
172
|
|
|
165
173
|
readPromises.push({ date, key, ref: docRef });
|
|
174
|
+
} else {
|
|
175
|
+
// If no path info, we can't fetch.
|
|
176
|
+
response[date][key] = null;
|
|
166
177
|
}
|
|
167
178
|
}
|
|
168
179
|
}
|
|
169
180
|
|
|
170
181
|
if (readPromises.length === 0) return response;
|
|
171
182
|
|
|
172
|
-
// Batch reads
|
|
183
|
+
// Batch reads
|
|
173
184
|
const CHUNK_SIZE = 100;
|
|
174
185
|
for (let i = 0; i < readPromises.length; i += CHUNK_SIZE) {
|
|
175
186
|
const chunk = readPromises.slice(i, i + CHUNK_SIZE);
|
|
@@ -182,8 +193,6 @@ const fetchUnifiedData = async (config, dependencies, calcKeys, dateStrings, cal
|
|
|
182
193
|
if (doc.exists) {
|
|
183
194
|
response[date][key] = doc.data();
|
|
184
195
|
} else {
|
|
185
|
-
// Start sparse: don't populate nulls to keep payload small?
|
|
186
|
-
// Or populate null to indicate "checked but missing"
|
|
187
196
|
response[date][key] = null;
|
|
188
197
|
}
|
|
189
198
|
});
|
|
@@ -198,12 +207,11 @@ const fetchUnifiedData = async (config, dependencies, calcKeys, dateStrings, cal
|
|
|
198
207
|
|
|
199
208
|
/**
|
|
200
209
|
* Factory for the main API handler.
|
|
201
|
-
* UPDATED: Uses AvailabilityCache to determine dates.
|
|
202
210
|
*/
|
|
203
211
|
const createApiHandler = (config, dependencies, calcMap) => {
|
|
204
212
|
const { logger, db } = dependencies;
|
|
205
213
|
|
|
206
|
-
//
|
|
214
|
+
// Singleton Cache
|
|
207
215
|
const availabilityCache = new AvailabilityCache(db, logger);
|
|
208
216
|
|
|
209
217
|
return async (req, res) => {
|
|
@@ -215,10 +223,10 @@ const createApiHandler = (config, dependencies, calcMap) => {
|
|
|
215
223
|
|
|
216
224
|
try {
|
|
217
225
|
const computationKeys = req.query.computations.split(',');
|
|
218
|
-
const mode = req.query.mode || 'latest';
|
|
219
|
-
const limit = parseInt(req.query.limit) || 30;
|
|
226
|
+
const mode = req.query.mode || 'latest';
|
|
227
|
+
const limit = parseInt(req.query.limit) || 30;
|
|
220
228
|
|
|
221
|
-
// 1. Resolve Dates
|
|
229
|
+
// 1. Resolve Dates
|
|
222
230
|
const dateStrings = await resolveTargetDates(availabilityCache, computationKeys, mode, limit);
|
|
223
231
|
|
|
224
232
|
if (dateStrings.length === 0) {
|
|
@@ -234,12 +242,9 @@ const createApiHandler = (config, dependencies, calcMap) => {
|
|
|
234
242
|
});
|
|
235
243
|
}
|
|
236
244
|
|
|
237
|
-
// 2. Fetch Data
|
|
245
|
+
// 2. Fetch Data
|
|
238
246
|
const data = await fetchUnifiedData(config, dependencies, computationKeys, dateStrings, calcMap);
|
|
239
247
|
|
|
240
|
-
// 3. Cleanup sparse dates (optional: remove dates where all requested keys are null)
|
|
241
|
-
// For now, we return what was fetched.
|
|
242
|
-
|
|
243
248
|
res.set('Cache-Control', 'public, max-age=300, s-maxage=3600');
|
|
244
249
|
res.status(200).send({
|
|
245
250
|
status: 'success',
|
|
@@ -248,8 +253,8 @@ const createApiHandler = (config, dependencies, calcMap) => {
|
|
|
248
253
|
mode,
|
|
249
254
|
limit: mode === 'series' ? limit : 1,
|
|
250
255
|
dateRange: {
|
|
251
|
-
start: dateStrings[dateStrings.length - 1],
|
|
252
|
-
end: dateStrings[0]
|
|
256
|
+
start: dateStrings[dateStrings.length - 1],
|
|
257
|
+
end: dateStrings[0]
|
|
253
258
|
}
|
|
254
259
|
},
|
|
255
260
|
data,
|
|
@@ -261,10 +266,6 @@ const createApiHandler = (config, dependencies, calcMap) => {
|
|
|
261
266
|
};
|
|
262
267
|
};
|
|
263
268
|
|
|
264
|
-
// ... (Previous Helper Functions: getComputationStructure, getDynamicSchema, createManifestHandler stay the same) ...
|
|
265
|
-
/**
|
|
266
|
-
* Internal helper for snippet generation.
|
|
267
|
-
*/
|
|
268
269
|
function createStructureSnippet(data, maxKeys = 20) {
|
|
269
270
|
if (data === null || typeof data !== 'object') {
|
|
270
271
|
if (typeof data === 'number') return 0;
|
|
@@ -298,9 +299,6 @@ function createStructureSnippet(data, maxKeys = 20) {
|
|
|
298
299
|
return newObj;
|
|
299
300
|
}
|
|
300
301
|
|
|
301
|
-
/**
|
|
302
|
-
* Sub-pipe: pipe.api.helpers.getComputationStructure
|
|
303
|
-
*/
|
|
304
302
|
async function getComputationStructure(computationName, calcMap, config, dependencies) {
|
|
305
303
|
const { db, logger } = dependencies;
|
|
306
304
|
try {
|
|
@@ -332,10 +330,6 @@ async function getComputationStructure(computationName, calcMap, config, depende
|
|
|
332
330
|
}
|
|
333
331
|
}
|
|
334
332
|
|
|
335
|
-
|
|
336
|
-
/**
|
|
337
|
-
* --- UPDATED: DYNAMIC SCHEMA GENERATION HARNESS ---
|
|
338
|
-
*/
|
|
339
333
|
async function getDynamicSchema(CalcClass, calcName) {
|
|
340
334
|
if (CalcClass && typeof CalcClass.getSchema === 'function') {
|
|
341
335
|
try {
|
|
@@ -349,47 +343,31 @@ async function getDynamicSchema(CalcClass, calcName) {
|
|
|
349
343
|
}
|
|
350
344
|
}
|
|
351
345
|
|
|
352
|
-
|
|
353
|
-
/**
|
|
354
|
-
* --- NEW: MANIFEST API HANDLER (With Filtering) ---
|
|
355
|
-
*/
|
|
356
346
|
const createManifestHandler = (config, dependencies, calcMap) => {
|
|
357
347
|
const { db, logger } = dependencies;
|
|
358
348
|
const schemaCollection = config.schemaCollection || 'computation_schemas';
|
|
359
349
|
|
|
360
350
|
return async (req, res) => {
|
|
361
351
|
try {
|
|
362
|
-
logger.log('INFO', '[API /manifest] Fetching all computation schemas...');
|
|
363
352
|
const snapshot = await db.collection(schemaCollection).get();
|
|
364
|
-
|
|
365
|
-
if (snapshot.empty) {
|
|
366
|
-
logger.log('WARN', '[API /manifest] No schemas found in collection.');
|
|
367
|
-
return res.status(404).send({ status: 'error', message: 'No computation schemas have been generated yet.' });
|
|
368
|
-
}
|
|
353
|
+
if (snapshot.empty) { return res.status(404).send({ status: 'error', message: 'No computation schemas have been generated yet.' }); }
|
|
369
354
|
|
|
370
|
-
// --- FILTERING LOGIC ---
|
|
371
355
|
const manifest = {};
|
|
372
356
|
const now = Date.now();
|
|
373
|
-
const MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
|
|
357
|
+
const MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
|
|
374
358
|
|
|
375
359
|
let activeCount = 0;
|
|
376
360
|
let staleCount = 0;
|
|
377
361
|
|
|
378
362
|
snapshot.forEach(doc => {
|
|
379
363
|
const data = doc.data();
|
|
380
|
-
|
|
381
|
-
// Safe Timestamp conversion
|
|
382
364
|
let lastUpdatedMs = 0;
|
|
383
365
|
if (data.lastUpdated && typeof data.lastUpdated.toMillis === 'function') {
|
|
384
366
|
lastUpdatedMs = data.lastUpdated.toMillis();
|
|
385
367
|
} else if (data.lastUpdated instanceof Date) {
|
|
386
368
|
lastUpdatedMs = data.lastUpdated.getTime();
|
|
387
|
-
} else {
|
|
388
|
-
// Fallback for very old records without a timestamp
|
|
389
|
-
lastUpdatedMs = 0;
|
|
390
369
|
}
|
|
391
370
|
|
|
392
|
-
// Exclude stale records
|
|
393
371
|
if ((now - lastUpdatedMs) < MAX_AGE_MS) {
|
|
394
372
|
manifest[doc.id] = {
|
|
395
373
|
category: data.category,
|
|
@@ -398,16 +376,9 @@ const createManifestHandler = (config, dependencies, calcMap) => {
|
|
|
398
376
|
lastUpdated: data.lastUpdated
|
|
399
377
|
};
|
|
400
378
|
activeCount++;
|
|
401
|
-
} else {
|
|
402
|
-
staleCount++;
|
|
403
|
-
}
|
|
379
|
+
} else { staleCount++; }
|
|
404
380
|
});
|
|
405
381
|
|
|
406
|
-
// Log filtering results
|
|
407
|
-
if (staleCount > 0) {
|
|
408
|
-
logger.log('INFO', `[API /manifest] Filtered out ${staleCount} stale schemas (older than 7 days). Returning ${activeCount} active.`);
|
|
409
|
-
}
|
|
410
|
-
|
|
411
382
|
res.status(200).send({
|
|
412
383
|
status: 'success',
|
|
413
384
|
summary: {
|
|
@@ -415,15 +386,13 @@ const createManifestHandler = (config, dependencies, calcMap) => {
|
|
|
415
386
|
totalComputations: snapshot.size,
|
|
416
387
|
schemasAvailable: activeCount,
|
|
417
388
|
schemasFiltered: staleCount,
|
|
418
|
-
lastUpdated:
|
|
419
|
-
(m.lastUpdated && m.lastUpdated.toMillis) ? m.lastUpdated.toMillis() : 0
|
|
420
|
-
), 0)
|
|
389
|
+
lastUpdated: Date.now()
|
|
421
390
|
},
|
|
422
391
|
manifest: manifest
|
|
423
392
|
});
|
|
424
393
|
|
|
425
394
|
} catch (error) {
|
|
426
|
-
logger.log('ERROR', 'API /manifest handler failed.', { errorMessage: error.message
|
|
395
|
+
logger.log('ERROR', 'API /manifest handler failed.', { errorMessage: error.message });
|
|
427
396
|
res.status(500).send({ status: 'error', message: 'An internal error occurred.' });
|
|
428
397
|
}
|
|
429
398
|
};
|