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
|
-
*
|
|
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',
|
|
71
|
+
computationName: 'TestSystemProbe',
|
|
62
72
|
category: 'alerts',
|
|
63
|
-
messageTemplate: 'Probe Triggered for {status} at {timestamp}',
|
|
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
|
|
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'
|
|
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
|
|
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
|
|
175
|
+
// --- [UPDATED] Batched Series Loading Logic ---
|
|
166
176
|
/**
|
|
167
|
-
* Optimistically loads a series of root data over a lookback period.
|
|
168
|
-
*
|
|
169
|
-
*
|
|
170
|
-
*
|
|
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));
|
|
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
|
|
201
|
+
// 3. Identify Collection & Required Flag
|
|
202
|
+
const collectionInfo = LOADER_COLLECTION_MAP[loaderMethod];
|
|
195
203
|
const requiredFlag = LOADER_DEPENDENCY_MAP[loaderMethod];
|
|
196
204
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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.
|
|
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
|
-
//
|
|
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
|
|
223
|
-
if (dayStatus) {
|
|
224
|
-
|
|
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
|
|
230
|
-
|
|
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
|
-
|
|
243
|
-
|
|
244
|
-
|
|
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
|
-
|
|
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
|
-
|
|
266
|
-
|
|
267
|
-
|
|
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 };
|