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: Implements Status-Based Availability Caching and Smart Date Resolution.
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; // { "calcName": ["2023-10-01", "2023-09-30"] } (Sorted DESC)
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 last 400 days of status to build the map
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
- calcMap[calcName] = { category: category, class: item[calcName] };
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] = { category: category, class: item };
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] = {}; // Init structure
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 in chunks (Firestore getAll limit is usually high, but let's be safe)
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
- // Instantiate Cache (Singleton scope within the closure)
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'; // Default to latest
219
- const limit = parseInt(req.query.limit) || 30; // Default 30 days for series
226
+ const mode = req.query.mode || 'latest';
227
+ const limit = parseInt(req.query.limit) || 30;
220
228
 
221
- // 1. Resolve Dates based on Availability
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 for these dates
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], // Oldest
252
- end: dateStrings[0] // Newest
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; // 7 days
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: Math.max(...Object.values(manifest).map(m =>
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, stack: error.stack });
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
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bulltrackers-module",
3
- "version": "1.0.232",
3
+ "version": "1.0.234",
4
4
  "description": "Helper Functions for Bulltrackers.",
5
5
  "main": "index.js",
6
6
  "files": [