bulltrackers-module 1.0.586 → 1.0.588

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.
@@ -127,6 +127,8 @@ async function findSubscriptionsForPI(db, logger, piCid, alertTypeId, computatio
127
127
  'NewSectorExposure': 'newSector',
128
128
  'PositionInvestedIncrease': 'increasedPositionSize',
129
129
  'NewSocialPost': 'newSocialPost',
130
+ // [NEW] Mapping for BehavioralAnomaly
131
+ 'BehavioralAnomaly': 'behavioralAnomaly',
130
132
  'TestSystemProbe': 'increasedRisk' // Hack: Map to 'increasedRisk' key
131
133
  };
132
134
 
@@ -155,7 +157,9 @@ async function findSubscriptionsForPI(db, logger, piCid, alertTypeId, computatio
155
157
  newSector: true,
156
158
  increasedPositionSize: true,
157
159
  newSocialPost: true,
158
- newPositions: true
160
+ newPositions: true,
161
+ // [NEW] Enable for Dev Override
162
+ behavioralAnomaly: true
159
163
  };
160
164
 
161
165
  // Check all developer accounts
@@ -224,8 +228,8 @@ async function findSubscriptionsForPI(db, logger, piCid, alertTypeId, computatio
224
228
 
225
229
  // Step 2: For each user, read their watchlists from SignedInUsers/{cid}/watchlists
226
230
  const { readWithMigration } = require('../../generic-api/user-api/helpers/core/path_resolution_helpers');
227
- const { collectionRegistry } = dependencies;
228
- const config = dependencies.config || {};
231
+ // const { collectionRegistry } = dependencies; // Already destructured above
232
+ // const config = dependencies.config || {}; // Already defined
229
233
 
230
234
  for (const userCidStr of userCids) {
231
235
  try {
@@ -1,8 +1,6 @@
1
1
  /**
2
- * @fileoverview Alert Type Registry
3
- * Defines all available alert types and their associated computations
2
+ * file: alert-system/helpers/alert_type_registry.js
4
3
  */
5
-
6
4
  const ALERT_TYPES = {
7
5
  increasedRisk: {
8
6
  id: 'increasedRisk',
@@ -54,13 +52,25 @@ const ALERT_TYPES = {
54
52
  severity: 'low',
55
53
  enabled: true
56
54
  },
55
+ // [NEW] Behavioral Anomaly Registration
56
+ behavioralAnomaly: {
57
+ id: 'behavioralAnomaly',
58
+ name: 'Behavioral Anomaly',
59
+ description: 'Alert when a Popular Investor deviates significantly from their baseline behavior',
60
+ computationName: 'BehavioralAnomaly',
61
+ category: 'alerts',
62
+ // Uses metadata keys from Behaviour.js: primaryDriver, driverSignificance, anomalyScore
63
+ messageTemplate: 'Behavioral Alert for {piUsername}: {primaryDriver} Deviation ({driverSignificance}) detected.',
64
+ severity: 'high',
65
+ enabled: true
66
+ },
57
67
  testSystemProbe: {
58
68
  id: 'testSystemProbe',
59
69
  name: 'Test System Probe',
60
70
  description: 'Always-on debug alert',
61
- computationName: 'TestSystemProbe', // Must match the class name above
71
+ computationName: 'TestSystemProbe',
62
72
  category: 'alerts',
63
- messageTemplate: 'Probe Triggered for {status} at {timestamp}', // Uses keys from the result object
73
+ messageTemplate: 'Probe Triggered for {status} at {timestamp}',
64
74
  severity: 'info',
65
75
  enabled: true
66
76
  },
@@ -113,6 +123,8 @@ function generateAlertMessage(alertType, piUsername, metadata = {}) {
113
123
  message = message.replace('{current}', metadata.current || metadata.currentValue || metadata.curr || metadata.currentRisk || 'N/A');
114
124
  message = message.replace('{sectorName}', metadata.sectorName || (metadata.newExposures && metadata.newExposures.length > 0 ? metadata.newExposures.join(', ') : 'Unknown'));
115
125
  message = message.replace('{ticker}', metadata.ticker || metadata.symbol || 'Unknown');
126
+
127
+ // Format numeric values
116
128
  message = message.replace('{volatility}', metadata.volatility ? `${(metadata.volatility * 100).toFixed(1)}` : 'N/A');
117
129
  message = message.replace('{threshold}', metadata.threshold ? `${(metadata.threshold * 100).toFixed(0)}` : 'N/A');
118
130
  message = message.replace('{diff}', metadata.diff ? `${metadata.diff.toFixed(1)}` : 'N/A');
@@ -120,6 +132,15 @@ function generateAlertMessage(alertType, piUsername, metadata = {}) {
120
132
  message = message.replace('{curr}', metadata.curr ? `${metadata.curr.toFixed(1)}` : 'N/A');
121
133
  message = message.replace('{title}', metadata.title || 'New Update');
122
134
 
135
+ // [FIX] Probe Placeholders (Missing in original)
136
+ message = message.replace('{status}', metadata.status || 'Unknown Status');
137
+ message = message.replace('{timestamp}', metadata.timestamp || new Date().toISOString());
138
+
139
+ // [NEW] Behavioral Anomaly Placeholders
140
+ message = message.replace('{primaryDriver}', metadata.primaryDriver || 'Unknown Factor');
141
+ message = message.replace('{driverSignificance}', metadata.driverSignificance || 'N/A');
142
+ message = message.replace('{anomalyScore}', metadata.anomalyScore || 'N/A');
143
+
123
144
  // Handle positions list if available
124
145
  if (metadata.positions && Array.isArray(metadata.positions) && metadata.positions.length > 0) {
125
146
  const positionsList = metadata.positions
@@ -148,5 +169,4 @@ module.exports = {
148
169
  getAllAlertTypes,
149
170
  isAlertComputation,
150
171
  generateAlertMessage
151
- };
152
-
172
+ };
@@ -21,8 +21,7 @@ const {
21
21
  const { getAvailabilityWindow } = require('./AvailabilityChecker');
22
22
  const zlib = require('zlib');
23
23
 
24
- // [NEW] Mapping of Loader Methods to Availability Flags in the Index
25
- // Used to intelligently skip reads for data that is known to be missing.
24
+ // [NEW] Mapping of Loader Methods to Availability Flags
26
25
  const LOADER_DEPENDENCY_MAP = {
27
26
  'loadRankings': 'piRankings',
28
27
  'loadRatings': 'piRatings',
@@ -30,7 +29,18 @@ const LOADER_DEPENDENCY_MAP = {
30
29
  'loadWatchlistMembership': 'watchlistMembership',
31
30
  'loadAlertHistory': 'piAlertHistory',
32
31
  'loadInsights': 'hasInsights',
33
- 'loadSocial': 'hasSocial' // broad check covering PI, SignedIn, and Generic
32
+ 'loadSocial': 'hasSocial'
33
+ };
34
+
35
+ // [NEW] Mapping of Loader Methods to Collection Config Keys / Defaults
36
+ // This allows us to construct references for Batch Reading without executing the opaque loader functions.
37
+ const LOADER_COLLECTION_MAP = {
38
+ 'loadRatings': { configKey: 'piRatingsCollection', default: 'PIRatingsData' },
39
+ 'loadPageViews': { configKey: 'piPageViewsCollection', default: 'PIPageViewsData' },
40
+ 'loadWatchlistMembership': { configKey: 'watchlistMembershipCollection', default: 'WatchlistMembershipData' },
41
+ 'loadAlertHistory': { configKey: 'piAlertHistoryCollection', default: 'PIAlertHistoryData' },
42
+ 'loadInsights': { configKey: 'insightsCollectionName', default: 'daily_instrument_insights' },
43
+ 'loadRankings': { configKey: 'popularInvestorRankingsCollection', default: 'popular_investor_rankings' }
34
44
  };
35
45
 
36
46
  class CachedDataLoader {
@@ -64,7 +74,7 @@ class CachedDataLoader {
64
74
  return data;
65
75
  }
66
76
 
67
- // ... [Existing load methods: loadMappings, loadInsights, etc. unchanged] ...
77
+ // ... [Existing single-day load methods remain unchanged] ...
68
78
  async loadMappings() {
69
79
  if (this.cache.mappings) return this.cache.mappings;
70
80
  const { calculationUtils } = this.deps;
@@ -162,13 +172,12 @@ class CachedDataLoader {
162
172
  return data;
163
173
  }
164
174
 
165
- // --- [UPDATED] Series Loading Logic with Pre-flight Availability Check ---
175
+ // --- [UPDATED] Batched Series Loading Logic ---
166
176
  /**
167
- * Optimistically loads a series of root data over a lookback period.
168
- * Uses the Root Data Index (getAvailabilityWindow) to avoid reading non-existent data.
169
- * @param {string} loaderMethod - The method name to call (e.g., 'loadAlertHistory')
170
- * @param {string} dateStr - The end date (exclusive or inclusive depending on data availability)
171
- * @param {number} lookbackDays - Number of days to look back
177
+ * Optimistically loads a series of root data over a lookback period using Batch Reads.
178
+ * 1. Checks Availability Index (Range Query).
179
+ * 2. Constructs Refs for all existing dates.
180
+ * 3. Fetches all in ONE db.getAll() request.
172
181
  */
173
182
  async loadSeries(loaderMethod, dateStr, lookbackDays) {
174
183
  if (!this[loaderMethod]) throw new Error(`[CachedDataLoader] Unknown method ${loaderMethod}`);
@@ -176,95 +185,98 @@ class CachedDataLoader {
176
185
  // 1. Calculate Date Range
177
186
  const endDate = new Date(dateStr);
178
187
  const startDate = new Date(endDate);
179
- startDate.setUTCDate(startDate.getUTCDate() - (lookbackDays - 1)); // -1 because range is inclusive of end
188
+ startDate.setUTCDate(startDate.getUTCDate() - (lookbackDays - 1));
180
189
 
181
190
  const startStr = startDate.toISOString().slice(0, 10);
182
191
  const endStr = endDate.toISOString().slice(0, 10);
183
192
 
184
193
  // 2. Pre-flight: Fetch Availability Window
185
- // This is a single range query that tells us exactly which days have data.
186
194
  let availabilityMap = new Map();
187
195
  try {
188
196
  availabilityMap = await getAvailabilityWindow(this.deps, startStr, endStr);
189
197
  } catch (e) {
190
- console.warn(`[CachedDataLoader] Availability check failed for series. Falling back to optimistic fetch. Error: ${e.message}`);
191
- // If availability check fails, we proceed with optimistic fetching (empty map)
198
+ console.warn(`[CachedDataLoader] Availability check failed for series. Falling back to optimistic batch fetch. Error: ${e.message}`);
192
199
  }
193
200
 
194
- // 3. Identify Required Flag in Availability Index
201
+ // 3. Identify Collection & Required Flag
202
+ const collectionInfo = LOADER_COLLECTION_MAP[loaderMethod];
195
203
  const requiredFlag = LOADER_DEPENDENCY_MAP[loaderMethod];
196
204
 
197
- const results = {};
198
- const promises = [];
199
- let skippedCount = 0;
205
+ if (!collectionInfo) {
206
+ // Fallback for methods not in the batch map (use legacy parallel loop)
207
+ return this._loadSeriesLegacy(loaderMethod, dateStr, lookbackDays);
208
+ }
209
+
210
+ const collectionName = this.config[collectionInfo.configKey] || collectionInfo.default;
211
+ const batchRefs = [];
212
+ const dateKeyMap = []; // Keep track of which date corresponds to which ref index
200
213
 
201
- // 4. Fetch N days back
214
+ // 4. Build Batch References
202
215
  for (let i = 0; i < lookbackDays; i++) {
203
216
  const d = new Date(endDate);
204
217
  d.setUTCDate(d.getUTCDate() - i);
205
218
  const dString = d.toISOString().slice(0, 10);
206
219
 
207
- // CHECK: Does index exist AND does it have our data?
208
- // If map is empty (e.g. error), we default to trying (optimistic) unless we strictly trust the map.
209
- // But if the query succeeded, map only contains dates that exist.
220
+ // Check Availability
210
221
  const dayStatus = availabilityMap.get(dString);
211
-
212
- // Logic:
213
- // 1. If dayStatus is undefined, it means the DATE itself is missing from the index (no data at all). -> SKIP
214
- // 2. If dayStatus is defined, check the specific flag. -> IF FALSE SKIP
215
-
216
- // Note: If availabilityMap is empty but dates SHOULD exist, we might have an issue.
217
- // However, getAvailabilityWindow returns only existing docs. So if it's not in map, it's not in DB.
218
-
219
222
  let shouldFetch = false;
220
223
 
221
224
  if (availabilityMap.size > 0) {
222
- // If we have index data, we trust it.
223
- if (dayStatus) {
224
- if (!requiredFlag || dayStatus[requiredFlag]) {
225
- shouldFetch = true;
226
- }
225
+ // If index exists, trust it
226
+ if (dayStatus && (!requiredFlag || dayStatus[requiredFlag])) {
227
+ shouldFetch = true;
227
228
  }
228
229
  } else {
229
- // If map is empty, it could mean NO data exists in that range, OR check failed.
230
- // If check failed (caught above), we might want to try anyway?
231
- // For now, if map is empty (validly), we assume no data.
232
- // To be safe against empty map meaning "everything missing", we can verify if the map was populated.
233
- // But getAvailabilityWindow returns a new Map(), so size 0 means 0 results found.
234
- // Thus: Skip everything.
235
- shouldFetch = false;
230
+ // If index check failed/empty, try optimistically
231
+ shouldFetch = true;
236
232
  }
237
233
 
238
- // Fallback for when we didn't run availability check (e.g. no deps provided or import fail)?
239
- // The availabilityMap is initialized above.
240
-
241
234
  if (shouldFetch) {
242
- promises.push(
243
- this[loaderMethod](dString)
244
- .then(data => ({ date: dString, data }))
245
- .catch(err => {
246
- console.warn(`[CachedDataLoader] Failed to load series item ${loaderMethod} for ${dString}: ${err.message}`);
247
- return { date: dString, data: null };
248
- })
249
- );
250
- } else {
251
- skippedCount++;
235
+ const ref = this.deps.db.collection(collectionName).doc(dString);
236
+ batchRefs.push(ref);
237
+ dateKeyMap.push(dString);
252
238
  }
253
239
  }
254
240
 
255
- const loaded = await Promise.all(promises);
256
-
241
+ // 5. Execute Batch Read
242
+ const results = {};
257
243
  let foundCount = 0;
258
- loaded.forEach(({ date, data }) => {
259
- if (data) {
260
- results[date] = data;
261
- foundCount++;
262
- }
263
- });
264
244
 
265
- // Debug log to confirm efficiency
266
- if (skippedCount > 0) {
267
- // console.debug(`[CachedDataLoader] Smart Series Load: Requested ${lookbackDays}, Found ${foundCount}, Skipped ${skippedCount} missing dates.`);
245
+ if (batchRefs.length > 0) {
246
+ try {
247
+ const snapshots = await this.deps.db.getAll(...batchRefs);
248
+
249
+ snapshots.forEach((snap, index) => {
250
+ if (snap.exists) {
251
+ const dString = dateKeyMap[index];
252
+ const rawData = snap.data();
253
+
254
+ // Decompress and clean data
255
+ const decompressed = this._tryDecompress(rawData);
256
+
257
+ // Handle standard data shapes (removing metadata fields if necessary)
258
+ // Most root data loaders return the full object, so we do too.
259
+ // Specific logic from data_loader.js (like stripping 'date' key) is handled here generically
260
+ // or by the consumer. For series data, returning the whole object is usually safer.
261
+
262
+ // Special handling for cleaner output (mimicking data_loader.js logic)
263
+ if (loaderMethod === 'loadRatings' || loaderMethod === 'loadPageViews' ||
264
+ loaderMethod === 'loadWatchlistMembership' || loaderMethod === 'loadAlertHistory') {
265
+ const { date, lastUpdated, ...cleanData } = decompressed;
266
+ results[dString] = cleanData;
267
+ } else if (loaderMethod === 'loadRankings') {
268
+ results[dString] = decompressed.Items || [];
269
+ } else {
270
+ results[dString] = decompressed;
271
+ }
272
+
273
+ foundCount++;
274
+ }
275
+ });
276
+ } catch (err) {
277
+ console.warn(`[CachedDataLoader] Batch fetch failed for ${loaderMethod}: ${err.message}. Falling back to individual fetches.`);
278
+ return this._loadSeriesLegacy(loaderMethod, dateStr, lookbackDays);
279
+ }
268
280
  }
269
281
 
270
282
  return {
@@ -274,6 +286,39 @@ class CachedDataLoader {
274
286
  requested: lookbackDays
275
287
  };
276
288
  }
289
+
290
+ /**
291
+ * Legacy Fallback: Loads series using parallel promises (for custom/unmapped loaders)
292
+ */
293
+ async _loadSeriesLegacy(loaderMethod, dateStr, lookbackDays) {
294
+ const results = {};
295
+ const promises = [];
296
+ const endDate = new Date(dateStr);
297
+
298
+ for (let i = 0; i < lookbackDays; i++) {
299
+ const d = new Date(endDate);
300
+ d.setUTCDate(d.getUTCDate() - i);
301
+ const dString = d.toISOString().slice(0, 10);
302
+
303
+ promises.push(
304
+ this[loaderMethod](dString)
305
+ .then(data => ({ date: dString, data }))
306
+ .catch(() => ({ date: dString, data: null }))
307
+ );
308
+ }
309
+
310
+ const loaded = await Promise.all(promises);
311
+ loaded.forEach(({ date, data }) => {
312
+ if (data) results[date] = data;
313
+ });
314
+
315
+ return {
316
+ dates: Object.keys(results).sort(),
317
+ data: results,
318
+ found: Object.keys(results).length,
319
+ requested: lookbackDays
320
+ };
321
+ }
277
322
  }
278
323
 
279
- module.exports = { CachedDataLoader };
324
+ module.exports = { CachedDataLoader };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bulltrackers-module",
3
- "version": "1.0.586",
3
+ "version": "1.0.588",
4
4
  "description": "Helper Functions for Bulltrackers.",
5
5
  "main": "index.js",
6
6
  "files": [