bulltrackers-module 1.0.297 → 1.0.298
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.
|
@@ -6,12 +6,14 @@ const { FieldValue, FieldPath } = require('@google-cloud/firestore');
|
|
|
6
6
|
const crypto = require('crypto');
|
|
7
7
|
|
|
8
8
|
// [NEW] Single Source of Truth for Data Availability
|
|
9
|
+
// These are defaults/fallbacks. The dynamic function getEarliestDataDates below is the primary source.
|
|
9
10
|
const DEFINITIVE_EARLIEST_DATES = {
|
|
10
|
-
portfolio: new Date('2025-
|
|
11
|
-
history: new Date('2025-
|
|
12
|
-
social: new Date('2025-
|
|
13
|
-
insights: new Date('2025-08-
|
|
14
|
-
price: new Date('2025-08-01T00:00:00Z')
|
|
11
|
+
portfolio: new Date('2025-01-01T00:00:00Z'),
|
|
12
|
+
history: new Date('2025-08-01T00:00:00Z'),
|
|
13
|
+
social: new Date('2025-08-01T00:00:00Z'),
|
|
14
|
+
insights: new Date('2025-08-01T00:00:00Z'),
|
|
15
|
+
price: new Date('2025-08-01T00:00:00Z'),
|
|
16
|
+
absoluteEarliest: new Date('2023-08-01T00:00:00Z')
|
|
15
17
|
};
|
|
16
18
|
|
|
17
19
|
/** Stage 1: Normalize a calculation name to kebab-case */
|
|
@@ -162,95 +164,75 @@ function getExpectedDateStrings(startDate, endDate) {
|
|
|
162
164
|
return dateStrings;
|
|
163
165
|
}
|
|
164
166
|
|
|
165
|
-
/** Stage 4: Get the earliest date
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
const query = db.collection(collectionName).where(FieldPath.documentId(), '>=', '2000-01-01').orderBy(FieldPath.documentId(), 'asc').limit(1);
|
|
171
|
-
const snapshot = await withRetry(() => query.get(), `GetEarliestDoc(${collectionName})`);
|
|
172
|
-
if (!snapshot.empty && /^\d{4}-\d{2}-\d{2}$/.test(snapshot.docs[0].id)) { return new Date(snapshot.docs[0].id + 'T00:00:00Z'); }
|
|
173
|
-
} catch (e) { logger.log('ERROR', `GetFirstDate failed for ${collectionName}`, { errorMessage: e.message }); }
|
|
174
|
-
return null;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
/** Stage 4: Get the earliest date in a sharded collection */
|
|
178
|
-
async function getFirstDateFromCollection(config, deps, collectionName) {
|
|
167
|
+
/** * Stage 4: Get the earliest date from the Centralized Root Data Index.
|
|
168
|
+
* This REPLACES the expensive/error-prone raw collection scanning.
|
|
169
|
+
* It queries the 'system_root_data_index' to find the first date where data flags are TRUE.
|
|
170
|
+
*/
|
|
171
|
+
async function getEarliestDataDates(config, deps) {
|
|
179
172
|
const { db, logger } = deps;
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
173
|
+
// Default to 'system_root_data_index' if not configured
|
|
174
|
+
const indexCollection = process.env.ROOT_DATA_AVAILABILITY_COLLECTION ||
|
|
175
|
+
config.rootDataAvailabilityCollection ||
|
|
176
|
+
'system_root_data_index';
|
|
177
|
+
|
|
178
|
+
// Helper to find earliest date where a specific flag is true
|
|
179
|
+
// Efficient: Uses the document ID (date string) index.
|
|
180
|
+
const getEarliestForType = async (flagName) => {
|
|
181
|
+
try {
|
|
182
|
+
const snapshot = await db.collection(indexCollection)
|
|
183
|
+
.where(flagName, '==', true)
|
|
184
|
+
.orderBy(FieldPath.documentId(), 'asc')
|
|
185
|
+
.limit(1)
|
|
186
|
+
.get();
|
|
187
|
+
|
|
188
|
+
if (!snapshot.empty) {
|
|
189
|
+
const dateStr = snapshot.docs[0].id; // YYYY-MM-DD
|
|
190
|
+
// Safety check on date format
|
|
191
|
+
if (/^\d{4}-\d{2}-\d{2}$/.test(dateStr)) {
|
|
192
|
+
return new Date(dateStr + 'T00:00:00Z');
|
|
193
|
+
}
|
|
191
194
|
}
|
|
195
|
+
} catch (e) {
|
|
196
|
+
logger.log('WARN', `[Utils] Failed to query index for ${flagName}: ${e.message}`);
|
|
192
197
|
}
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
}
|
|
198
|
+
return null;
|
|
199
|
+
};
|
|
196
200
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
getFirstDateFromCollection (config, deps, config.speculatorHistoryCollection),
|
|
205
|
-
getFirstDateFromSimpleCollection (config, deps, config.insightsCollectionName),
|
|
206
|
-
getFirstDateFromSimpleCollection (config, deps, config.socialInsightsCollectionName),
|
|
207
|
-
getFirstDateFromPriceCollection (config, deps)
|
|
201
|
+
// Parallel query for all data types
|
|
202
|
+
const [portfolioDate, historyDate, socialDate, insightsDate, priceDate] = await Promise.all([
|
|
203
|
+
getEarliestForType('hasPortfolio'),
|
|
204
|
+
getEarliestForType('hasHistory'),
|
|
205
|
+
getEarliestForType('hasSocial'),
|
|
206
|
+
getEarliestForType('hasInsights'),
|
|
207
|
+
getEarliestForType('hasPrices')
|
|
208
208
|
]);
|
|
209
209
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
210
|
+
// Calculate absolute earliest among found dates
|
|
211
|
+
const foundDates = [portfolioDate, historyDate, socialDate, insightsDate, priceDate].filter(d => d !== null);
|
|
212
|
+
|
|
213
|
+
let absoluteEarliest = null;
|
|
214
|
+
if (foundDates.length > 0) {
|
|
215
|
+
absoluteEarliest = new Date(Math.min(...foundDates));
|
|
216
|
+
} else {
|
|
217
|
+
// Fallback if index is empty
|
|
218
|
+
const configStart = config.earliestComputationDate || '2023-01-01';
|
|
219
|
+
absoluteEarliest = new Date(configStart + 'T00:00:00Z');
|
|
220
|
+
logger.log('WARN', `[Utils] No data found in Root Data Index (${indexCollection}). Defaulting to ${configStart}`);
|
|
221
|
+
}
|
|
222
222
|
|
|
223
|
-
|
|
223
|
+
// Update the static export for consumers who use it (best effort synchronization)
|
|
224
|
+
DEFINITIVE_EARLIEST_DATES.absoluteEarliest = absoluteEarliest;
|
|
224
225
|
|
|
225
226
|
return {
|
|
226
|
-
portfolio:
|
|
227
|
-
history:
|
|
228
|
-
insights:
|
|
229
|
-
social:
|
|
230
|
-
price:
|
|
231
|
-
absoluteEarliest: absoluteEarliest
|
|
227
|
+
portfolio: portfolioDate || new Date('2999-12-31T00:00:00Z'),
|
|
228
|
+
history: historyDate || new Date('2999-12-31T00:00:00Z'),
|
|
229
|
+
insights: insightsDate || new Date('2999-12-31T00:00:00Z'),
|
|
230
|
+
social: socialDate || new Date('2999-12-31T00:00:00Z'),
|
|
231
|
+
price: priceDate || new Date('2999-12-31T00:00:00Z'),
|
|
232
|
+
absoluteEarliest: absoluteEarliest
|
|
232
233
|
};
|
|
233
234
|
}
|
|
234
235
|
|
|
235
|
-
async function getFirstDateFromPriceCollection(config, deps) {
|
|
236
|
-
const { db, logger } = deps;
|
|
237
|
-
const collection = config.priceCollection || 'asset_prices';
|
|
238
|
-
try {
|
|
239
|
-
const snapshot = await withRetry(() => db.collection(collection).limit(10).get(), `GetPriceShards(${collection})`);
|
|
240
|
-
let earliestDate = null;
|
|
241
|
-
snapshot.forEach(doc => {
|
|
242
|
-
const shardData = doc.data();
|
|
243
|
-
for (const instrumentId in shardData) {
|
|
244
|
-
const instrumentData = shardData[instrumentId];
|
|
245
|
-
if (!instrumentData.prices) continue;
|
|
246
|
-
const dates = Object.keys(instrumentData.prices).filter(d => /^\d{4}-\d{2}-\d{2}$/.test(d)).sort();
|
|
247
|
-
if (dates.length > 0) { const firstDate = new Date(dates[0] + 'T00:00:00Z'); if (!earliestDate || firstDate < earliestDate) earliestDate = firstDate; }
|
|
248
|
-
}
|
|
249
|
-
});
|
|
250
|
-
return earliestDate;
|
|
251
|
-
} catch (e) { logger.log('ERROR', `Failed to get earliest price date from ${collection}`, { errorMessage: e.message }); return null; }
|
|
252
|
-
}
|
|
253
|
-
|
|
254
236
|
module.exports = {
|
|
255
237
|
FieldValue,
|
|
256
238
|
FieldPath,
|
|
@@ -259,7 +241,7 @@ module.exports = {
|
|
|
259
241
|
getExpectedDateStrings,
|
|
260
242
|
getEarliestDataDates,
|
|
261
243
|
generateCodeHash,
|
|
262
|
-
generateDataHash,
|
|
244
|
+
generateDataHash,
|
|
263
245
|
withRetry,
|
|
264
246
|
DEFINITIVE_EARLIEST_DATES
|
|
265
|
-
};
|
|
247
|
+
};
|