bulltrackers-module 1.0.692 → 1.0.694
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.
|
@@ -3028,12 +3028,18 @@ const getWatchlistTriggerCounts = async (db, userId, watchlistId) => {
|
|
|
3028
3028
|
|
|
3029
3029
|
/**
|
|
3030
3030
|
* Query PIs matching dynamic watchlist criteria
|
|
3031
|
-
*
|
|
3031
|
+
*
|
|
3032
|
+
* IMPORTANT: This function only evaluates PIs based on the MOST RECENT available data.
|
|
3033
|
+
* The timeRange parameter controls how far back we look to FIND data (in case today's
|
|
3034
|
+
* computation hasn't run yet), but we DO NOT aggregate matches across multiple dates.
|
|
3035
|
+
*
|
|
3036
|
+
* If a PI matched criteria 3 days ago but doesn't match on the most recent data,
|
|
3037
|
+
* they will NOT be included - they "dropped off" the watchlist.
|
|
3032
3038
|
*
|
|
3033
3039
|
* @param {Object} db - Firestore instance
|
|
3034
3040
|
* @param {string} computationName - Name of the computation to query
|
|
3035
3041
|
* @param {Object} parameters - Threshold parameters from watchlist config (e.g., {minChange: 1, minRiskLevel: 5})
|
|
3036
|
-
* @param {string} timeRange - Time range (today, last_7_days, last_30_days)
|
|
3042
|
+
* @param {string} timeRange - Time range to look back for data availability (today, last_7_days, last_30_days)
|
|
3037
3043
|
* @param {number} limit - Maximum number of results
|
|
3038
3044
|
* @returns {Promise<Object>} Matching PIs with their values
|
|
3039
3045
|
*/
|
|
@@ -3041,7 +3047,7 @@ const queryDynamicWatchlistMatches = async (db, computationName, parameters = {}
|
|
|
3041
3047
|
try {
|
|
3042
3048
|
console.log(`[queryDynamicWatchlistMatches] Querying ${computationName} with params:`, parameters, `timeRange: ${timeRange}`);
|
|
3043
3049
|
|
|
3044
|
-
// Determine
|
|
3050
|
+
// Determine how far back to look for data availability
|
|
3045
3051
|
const endDate = new Date();
|
|
3046
3052
|
const startDate = new Date();
|
|
3047
3053
|
|
|
@@ -3059,24 +3065,22 @@ const queryDynamicWatchlistMatches = async (db, computationName, parameters = {}
|
|
|
3059
3065
|
startDate.setDate(startDate.getDate() - 7);
|
|
3060
3066
|
}
|
|
3061
3067
|
|
|
3062
|
-
// Build list of dates to
|
|
3068
|
+
// Build list of dates to check (most recent first)
|
|
3063
3069
|
const dates = [];
|
|
3064
3070
|
for (let d = new Date(endDate); d >= startDate; d.setDate(d.getDate() - 1)) {
|
|
3065
3071
|
dates.push(d.toISOString().split('T')[0]);
|
|
3066
3072
|
}
|
|
3067
3073
|
|
|
3068
|
-
console.log(`[queryDynamicWatchlistMatches]
|
|
3074
|
+
console.log(`[queryDynamicWatchlistMatches] Looking for most recent data in date range:`, dates);
|
|
3069
3075
|
|
|
3070
|
-
|
|
3076
|
+
// Find the MOST RECENT date that has computation data
|
|
3077
|
+
let mostRecentDate = null;
|
|
3078
|
+
let docRef = null;
|
|
3079
|
+
let docSnapshot = null;
|
|
3071
3080
|
|
|
3072
|
-
// Query each date
|
|
3073
3081
|
for (const dateStr of dates) {
|
|
3074
3082
|
try {
|
|
3075
|
-
// Try
|
|
3076
|
-
let docSnapshot = null;
|
|
3077
|
-
let docRef = null;
|
|
3078
|
-
|
|
3079
|
-
// First try alerts path
|
|
3083
|
+
// Try alerts path first
|
|
3080
3084
|
docRef = db.collection('unified_insights')
|
|
3081
3085
|
.doc(dateStr)
|
|
3082
3086
|
.collection('results')
|
|
@@ -3098,118 +3102,134 @@ const queryDynamicWatchlistMatches = async (db, computationName, parameters = {}
|
|
|
3098
3102
|
docSnapshot = await docRef.get();
|
|
3099
3103
|
}
|
|
3100
3104
|
|
|
3101
|
-
if (
|
|
3102
|
-
|
|
3103
|
-
|
|
3105
|
+
if (docSnapshot.exists) {
|
|
3106
|
+
mostRecentDate = dateStr;
|
|
3107
|
+
console.log(`[queryDynamicWatchlistMatches] Found most recent data on ${dateStr}`);
|
|
3108
|
+
break; // Stop searching - we found the most recent data
|
|
3104
3109
|
}
|
|
3105
|
-
|
|
3106
|
-
console.
|
|
3107
|
-
|
|
3108
|
-
|
|
3109
|
-
|
|
3110
|
-
|
|
3111
|
-
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
|
|
3116
|
-
|
|
3117
|
-
|
|
3118
|
-
|
|
3119
|
-
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
|
|
3123
|
-
|
|
3124
|
-
|
|
3125
|
-
|
|
3126
|
-
|
|
3127
|
-
|
|
3128
|
-
|
|
3110
|
+
} catch (dateErr) {
|
|
3111
|
+
console.warn(`[queryDynamicWatchlistMatches] Error checking date ${dateStr}: ${dateErr.message}`);
|
|
3112
|
+
continue;
|
|
3113
|
+
}
|
|
3114
|
+
}
|
|
3115
|
+
|
|
3116
|
+
// If no data found in the entire range, return empty
|
|
3117
|
+
if (!mostRecentDate || !docSnapshot || !docSnapshot.exists) {
|
|
3118
|
+
console.log(`[queryDynamicWatchlistMatches] No computation data found for ${computationName} in date range`);
|
|
3119
|
+
return {
|
|
3120
|
+
success: true,
|
|
3121
|
+
matches: [],
|
|
3122
|
+
count: 0,
|
|
3123
|
+
totalScanned: 0,
|
|
3124
|
+
dateRange: {
|
|
3125
|
+
start: startDate.toISOString().split('T')[0],
|
|
3126
|
+
end: endDate.toISOString().split('T')[0]
|
|
3127
|
+
},
|
|
3128
|
+
dataDate: null,
|
|
3129
|
+
computationName,
|
|
3130
|
+
parameters,
|
|
3131
|
+
message: `No computation data found for ${computationName} in the selected time range`
|
|
3132
|
+
};
|
|
3133
|
+
}
|
|
3134
|
+
|
|
3135
|
+
const docData = docSnapshot.data();
|
|
3136
|
+
|
|
3137
|
+
// Read all per-user data from the most recent date
|
|
3138
|
+
let allUserData = {};
|
|
3139
|
+
|
|
3140
|
+
if (docData._sharded === true) {
|
|
3141
|
+
// Read from shards
|
|
3142
|
+
const shardsSnapshot = await docRef.collection('_shards').get();
|
|
3143
|
+
console.log(`[queryDynamicWatchlistMatches] Found ${shardsSnapshot.size} shards for ${mostRecentDate}`);
|
|
3144
|
+
|
|
3145
|
+
for (const shardDoc of shardsSnapshot.docs) {
|
|
3146
|
+
const shardData = shardDoc.data();
|
|
3147
|
+
Object.entries(shardData).forEach(([key, value]) => {
|
|
3148
|
+
// Skip metadata keys, only include CID keys
|
|
3149
|
+
if (key.startsWith('_') || key === 'cids' || key === 'metadata') return;
|
|
3150
|
+
if (/^\d+$/.test(key)) {
|
|
3151
|
+
allUserData[key] = value;
|
|
3129
3152
|
}
|
|
3130
|
-
}
|
|
3131
|
-
|
|
3132
|
-
|
|
3133
|
-
|
|
3134
|
-
|
|
3135
|
-
|
|
3136
|
-
|
|
3137
|
-
|
|
3138
|
-
}
|
|
3139
|
-
});
|
|
3153
|
+
});
|
|
3154
|
+
}
|
|
3155
|
+
} else {
|
|
3156
|
+
// Data is in the document itself
|
|
3157
|
+
Object.entries(docData).forEach(([key, value]) => {
|
|
3158
|
+
if (key.startsWith('_') || key === 'cids' || key === 'metadata' || key === 'globalMetadata') return;
|
|
3159
|
+
if (/^\d+$/.test(key)) {
|
|
3160
|
+
allUserData[key] = value;
|
|
3140
3161
|
}
|
|
3141
|
-
|
|
3142
|
-
|
|
3143
|
-
|
|
3144
|
-
|
|
3145
|
-
|
|
3146
|
-
|
|
3147
|
-
|
|
3148
|
-
|
|
3149
|
-
|
|
3150
|
-
|
|
3151
|
-
|
|
3152
|
-
|
|
3153
|
-
|
|
3154
|
-
|
|
3155
|
-
|
|
3156
|
-
|
|
3157
|
-
|
|
3158
|
-
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
|
|
3162
|
-
|
|
3163
|
-
|
|
3164
|
-
|
|
3165
|
-
|
|
3166
|
-
|
|
3167
|
-
|
|
3168
|
-
|
|
3169
|
-
|
|
3170
|
-
|
|
3171
|
-
|
|
3172
|
-
|
|
3173
|
-
|
|
3174
|
-
currentValue: passesFilter.currentValue,
|
|
3175
|
-
previousValue: passesFilter.previousValue,
|
|
3176
|
-
change: passesFilter.change,
|
|
3177
|
-
metadata: {
|
|
3178
|
-
...piData,
|
|
3179
|
-
computationDate: dateStr
|
|
3180
|
-
}
|
|
3181
|
-
});
|
|
3182
|
-
|
|
3183
|
-
// Stop if we've hit the limit
|
|
3184
|
-
if (matchingPIs.size >= limit) break;
|
|
3185
|
-
}
|
|
3162
|
+
});
|
|
3163
|
+
}
|
|
3164
|
+
|
|
3165
|
+
const totalPIs = Object.keys(allUserData).length;
|
|
3166
|
+
console.log(`[queryDynamicWatchlistMatches] Evaluating ${totalPIs} PIs from ${mostRecentDate} against criteria`);
|
|
3167
|
+
|
|
3168
|
+
// Filter PIs that match the criteria on this most recent date
|
|
3169
|
+
const matchingPIs = [];
|
|
3170
|
+
const cidsToLookup = [];
|
|
3171
|
+
|
|
3172
|
+
for (const [piCidStr, piData] of Object.entries(allUserData)) {
|
|
3173
|
+
const piCid = Number(piCidStr);
|
|
3174
|
+
|
|
3175
|
+
// Skip error data
|
|
3176
|
+
if (piData.error) continue;
|
|
3177
|
+
|
|
3178
|
+
// Check if PI matches the criteria
|
|
3179
|
+
const filterResult = checkPIMatchesCriteria(computationName, piData, parameters);
|
|
3180
|
+
|
|
3181
|
+
if (filterResult.passes) {
|
|
3182
|
+
cidsToLookup.push({ piCid, piData, filterResult });
|
|
3183
|
+
}
|
|
3184
|
+
}
|
|
3185
|
+
|
|
3186
|
+
console.log(`[queryDynamicWatchlistMatches] ${cidsToLookup.length} PIs match criteria, fetching usernames...`);
|
|
3187
|
+
|
|
3188
|
+
// Fetch usernames for matching PIs (batch for efficiency)
|
|
3189
|
+
for (const { piCid, piData, filterResult } of cidsToLookup.slice(0, limit)) {
|
|
3190
|
+
let username = `PI-${piCid}`;
|
|
3191
|
+
try {
|
|
3192
|
+
const piProfile = await fetchPopularInvestorMasterList(db, String(piCid));
|
|
3193
|
+
if (piProfile && piProfile.username) {
|
|
3194
|
+
username = piProfile.username;
|
|
3186
3195
|
}
|
|
3187
|
-
|
|
3188
|
-
//
|
|
3189
|
-
if (matchingPIs.size >= limit) break;
|
|
3190
|
-
|
|
3191
|
-
} catch (dateErr) {
|
|
3192
|
-
console.warn(`[queryDynamicWatchlistMatches] Error processing date ${dateStr}: ${dateErr.message}`);
|
|
3193
|
-
continue;
|
|
3196
|
+
} catch (e) {
|
|
3197
|
+
// Use default username
|
|
3194
3198
|
}
|
|
3199
|
+
|
|
3200
|
+
matchingPIs.push({
|
|
3201
|
+
cid: piCid,
|
|
3202
|
+
username,
|
|
3203
|
+
matchedAt: mostRecentDate,
|
|
3204
|
+
matchValue: filterResult.matchValue,
|
|
3205
|
+
currentValue: filterResult.currentValue,
|
|
3206
|
+
previousValue: filterResult.previousValue,
|
|
3207
|
+
change: filterResult.change,
|
|
3208
|
+
metadata: {
|
|
3209
|
+
...piData,
|
|
3210
|
+
computationDate: mostRecentDate
|
|
3211
|
+
}
|
|
3212
|
+
});
|
|
3195
3213
|
}
|
|
3196
3214
|
|
|
3197
|
-
// Sort by match value (descending)
|
|
3198
|
-
const sortedMatches =
|
|
3215
|
+
// Sort by match value (descending)
|
|
3216
|
+
const sortedMatches = matchingPIs
|
|
3199
3217
|
.sort((a, b) => Math.abs(b.matchValue) - Math.abs(a.matchValue))
|
|
3200
3218
|
.slice(0, limit);
|
|
3201
3219
|
|
|
3202
|
-
console.log(`[queryDynamicWatchlistMatches] Returning ${sortedMatches.length} matches`);
|
|
3220
|
+
console.log(`[queryDynamicWatchlistMatches] Returning ${sortedMatches.length} matches from ${mostRecentDate}`);
|
|
3203
3221
|
|
|
3204
3222
|
return {
|
|
3205
3223
|
success: true,
|
|
3206
3224
|
matches: sortedMatches,
|
|
3207
3225
|
count: sortedMatches.length,
|
|
3208
|
-
totalScanned:
|
|
3226
|
+
totalScanned: totalPIs,
|
|
3227
|
+
totalMatching: cidsToLookup.length,
|
|
3209
3228
|
dateRange: {
|
|
3210
3229
|
start: startDate.toISOString().split('T')[0],
|
|
3211
3230
|
end: endDate.toISOString().split('T')[0]
|
|
3212
3231
|
},
|
|
3232
|
+
dataDate: mostRecentDate, // The actual date the data is from
|
|
3213
3233
|
computationName,
|
|
3214
3234
|
parameters
|
|
3215
3235
|
};
|
|
@@ -420,7 +420,8 @@ async function writeSingleResult(result, docRef, name, dateContext, category, lo
|
|
|
420
420
|
}
|
|
421
421
|
|
|
422
422
|
const rootUpdate = updates.find(u => u.ref.path === docRef.path && u.type !== 'DELETE');
|
|
423
|
-
|
|
423
|
+
// FIX: Always use merge: false to ensure old fields (like _compressed/payload) are wiped
|
|
424
|
+
if (rootUpdate) { rootUpdate.options = { merge: false }; }
|
|
424
425
|
|
|
425
426
|
const writes = updates.filter(u => u.type !== 'DELETE').length;
|
|
426
427
|
const deletes = updates.filter(u => u.type === 'DELETE').length;
|
package/package.json
CHANGED
|
@@ -1,172 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* file: alert-system/helpers/alert_type_registry.js
|
|
3
|
-
*/
|
|
4
|
-
const ALERT_TYPES = {
|
|
5
|
-
increasedRisk: {
|
|
6
|
-
id: 'increasedRisk',
|
|
7
|
-
name: 'Increased Risk',
|
|
8
|
-
description: 'Alert when a Popular Investor\'s risk score increases',
|
|
9
|
-
computationName: 'RiskScoreIncrease',
|
|
10
|
-
category: 'alerts',
|
|
11
|
-
messageTemplate: '{piUsername}\'s risk score increased by {change} points (from {previous} to {current})',
|
|
12
|
-
severity: 'high',
|
|
13
|
-
enabled: true
|
|
14
|
-
},
|
|
15
|
-
volatilityChanges: {
|
|
16
|
-
id: 'volatilityChanges',
|
|
17
|
-
name: 'Significant Volatility',
|
|
18
|
-
description: 'Alert when a Popular Investor\'s portfolio volatility exceeds 50%',
|
|
19
|
-
computationName: 'SignificantVolatility',
|
|
20
|
-
category: 'alerts',
|
|
21
|
-
messageTemplate: '{piUsername}\'s portfolio volatility is {volatility}% (threshold: {threshold}%)',
|
|
22
|
-
severity: 'medium',
|
|
23
|
-
enabled: true
|
|
24
|
-
},
|
|
25
|
-
newSector: {
|
|
26
|
-
id: 'newSector',
|
|
27
|
-
name: 'New Sector Entry',
|
|
28
|
-
description: 'Alert when a Popular Investor enters a new sector',
|
|
29
|
-
computationName: 'NewSectorExposure',
|
|
30
|
-
category: 'alerts',
|
|
31
|
-
messageTemplate: '{piUsername} entered new sector(s): {sectorName}',
|
|
32
|
-
severity: 'low',
|
|
33
|
-
enabled: true
|
|
34
|
-
},
|
|
35
|
-
increasedPositionSize: {
|
|
36
|
-
id: 'increasedPositionSize',
|
|
37
|
-
name: 'Increased Position Size',
|
|
38
|
-
description: 'Alert when a Popular Investor significantly increases a position size (>5%)',
|
|
39
|
-
computationName: 'PositionInvestedIncrease',
|
|
40
|
-
category: 'alerts',
|
|
41
|
-
messageTemplate: '{piUsername} increased position size for {symbol} by {diff}% (from {prev}% to {curr}%)',
|
|
42
|
-
severity: 'medium',
|
|
43
|
-
enabled: true
|
|
44
|
-
},
|
|
45
|
-
newSocialPost: {
|
|
46
|
-
id: 'newSocialPost',
|
|
47
|
-
name: 'New Social Post',
|
|
48
|
-
description: 'Alert when a Popular Investor makes a new social post',
|
|
49
|
-
computationName: 'NewSocialPost',
|
|
50
|
-
category: 'alerts',
|
|
51
|
-
messageTemplate: '{piUsername} posted a new update: {title}',
|
|
52
|
-
severity: 'low',
|
|
53
|
-
enabled: true
|
|
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
|
-
},
|
|
67
|
-
testSystemProbe: {
|
|
68
|
-
id: 'testSystemProbe',
|
|
69
|
-
name: 'Test System Probe',
|
|
70
|
-
description: 'Always-on debug alert',
|
|
71
|
-
computationName: 'TestSystemProbe',
|
|
72
|
-
category: 'alerts',
|
|
73
|
-
messageTemplate: 'Probe Triggered for {status} at {timestamp}',
|
|
74
|
-
severity: 'info',
|
|
75
|
-
enabled: true
|
|
76
|
-
},
|
|
77
|
-
};
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* Get alert type by ID
|
|
81
|
-
*/
|
|
82
|
-
function getAlertType(alertTypeId) {
|
|
83
|
-
return ALERT_TYPES[alertTypeId] || null;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* Get alert type by computation name
|
|
88
|
-
*/
|
|
89
|
-
function getAlertTypeByComputation(computationName) {
|
|
90
|
-
for (const [id, alertType] of Object.entries(ALERT_TYPES)) {
|
|
91
|
-
if (alertType.computationName === computationName) {
|
|
92
|
-
return alertType;
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
return null;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* Get all enabled alert types
|
|
100
|
-
*/
|
|
101
|
-
function getAllAlertTypes() {
|
|
102
|
-
return Object.values(ALERT_TYPES).filter(type => type.enabled);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/**
|
|
106
|
-
* Check if a computation is an alert computation
|
|
107
|
-
*/
|
|
108
|
-
function isAlertComputation(computationName) {
|
|
109
|
-
return getAlertTypeByComputation(computationName) !== null;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
* Generate alert message from template and metadata
|
|
114
|
-
*/
|
|
115
|
-
function generateAlertMessage(alertType, piUsername, metadata = {}) {
|
|
116
|
-
let message = alertType.messageTemplate;
|
|
117
|
-
|
|
118
|
-
// Replace placeholders
|
|
119
|
-
message = message.replace('{piUsername}', piUsername || 'Unknown');
|
|
120
|
-
message = message.replace('{count}', metadata.count || metadata.positions?.length || metadata.moveCount || 0);
|
|
121
|
-
message = message.replace('{change}', metadata.change || metadata.changePercent || metadata.diff || 'N/A');
|
|
122
|
-
message = message.replace('{previous}', metadata.previous || metadata.previousValue || metadata.prev || metadata.previousRisk || 'N/A');
|
|
123
|
-
message = message.replace('{current}', metadata.current || metadata.currentValue || metadata.curr || metadata.currentRisk || 'N/A');
|
|
124
|
-
message = message.replace('{sectorName}', metadata.sectorName || (metadata.newExposures && metadata.newExposures.length > 0 ? metadata.newExposures.join(', ') : 'Unknown'));
|
|
125
|
-
message = message.replace('{ticker}', metadata.ticker || metadata.symbol || 'Unknown');
|
|
126
|
-
|
|
127
|
-
// Format numeric values
|
|
128
|
-
message = message.replace('{volatility}', metadata.volatility ? `${(metadata.volatility * 100).toFixed(1)}` : 'N/A');
|
|
129
|
-
message = message.replace('{threshold}', metadata.threshold ? `${(metadata.threshold * 100).toFixed(0)}` : 'N/A');
|
|
130
|
-
message = message.replace('{diff}', metadata.diff ? `${metadata.diff.toFixed(1)}` : 'N/A');
|
|
131
|
-
message = message.replace('{prev}', metadata.prev ? `${metadata.prev.toFixed(1)}` : 'N/A');
|
|
132
|
-
message = message.replace('{curr}', metadata.curr ? `${metadata.curr.toFixed(1)}` : 'N/A');
|
|
133
|
-
message = message.replace('{title}', metadata.title || 'New Update');
|
|
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
|
-
|
|
144
|
-
// Handle positions list if available
|
|
145
|
-
if (metadata.positions && Array.isArray(metadata.positions) && metadata.positions.length > 0) {
|
|
146
|
-
const positionsList = metadata.positions
|
|
147
|
-
.slice(0, 3)
|
|
148
|
-
.map(p => p.ticker || p.instrumentId)
|
|
149
|
-
.join(', ');
|
|
150
|
-
message = message.replace('{positions}', positionsList);
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
// Handle moves array for PositionInvestedIncrease
|
|
154
|
-
if (metadata.moves && Array.isArray(metadata.moves) && metadata.moves.length > 0) {
|
|
155
|
-
const firstMove = metadata.moves[0];
|
|
156
|
-
message = message.replace('{symbol}', firstMove.symbol || 'Unknown');
|
|
157
|
-
message = message.replace('{diff}', firstMove.diff ? `${firstMove.diff.toFixed(1)}` : 'N/A');
|
|
158
|
-
message = message.replace('{prev}', firstMove.prev ? `${firstMove.prev.toFixed(1)}` : 'N/A');
|
|
159
|
-
message = message.replace('{curr}', firstMove.curr ? `${firstMove.curr.toFixed(1)}` : 'N/A');
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
return message;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
module.exports = {
|
|
166
|
-
ALERT_TYPES,
|
|
167
|
-
getAlertType,
|
|
168
|
-
getAlertTypeByComputation,
|
|
169
|
-
getAllAlertTypes,
|
|
170
|
-
isAlertComputation,
|
|
171
|
-
generateAlertMessage
|
|
172
|
-
};
|