bulltrackers-module 1.0.733 → 1.0.734
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.
- package/functions/computation-system-v2/README.md +152 -0
- package/functions/computation-system-v2/computations/PopularInvestorProfileMetrics.js +720 -0
- package/functions/computation-system-v2/computations/PopularInvestorRiskAssessment.js +176 -0
- package/functions/computation-system-v2/computations/PopularInvestorRiskMetrics.js +294 -0
- package/functions/computation-system-v2/computations/TestComputation.js +46 -0
- package/functions/computation-system-v2/computations/UserPortfolioSummary.js +172 -0
- package/functions/computation-system-v2/config/bulltrackers.config.js +317 -0
- package/functions/computation-system-v2/framework/core/Computation.js +73 -0
- package/functions/computation-system-v2/framework/core/Manifest.js +223 -0
- package/functions/computation-system-v2/framework/core/RuleInjector.js +53 -0
- package/functions/computation-system-v2/framework/core/Rules.js +231 -0
- package/functions/computation-system-v2/framework/core/RunAnalyzer.js +163 -0
- package/functions/computation-system-v2/framework/cost/CostTracker.js +154 -0
- package/functions/computation-system-v2/framework/data/DataFetcher.js +399 -0
- package/functions/computation-system-v2/framework/data/QueryBuilder.js +232 -0
- package/functions/computation-system-v2/framework/data/SchemaRegistry.js +287 -0
- package/functions/computation-system-v2/framework/execution/Orchestrator.js +498 -0
- package/functions/computation-system-v2/framework/execution/TaskRunner.js +35 -0
- package/functions/computation-system-v2/framework/execution/middleware/CostTrackerMiddleware.js +32 -0
- package/functions/computation-system-v2/framework/execution/middleware/LineageMiddleware.js +32 -0
- package/functions/computation-system-v2/framework/execution/middleware/Middleware.js +14 -0
- package/functions/computation-system-v2/framework/execution/middleware/ProfilerMiddleware.js +47 -0
- package/functions/computation-system-v2/framework/index.js +45 -0
- package/functions/computation-system-v2/framework/lineage/LineageTracker.js +147 -0
- package/functions/computation-system-v2/framework/monitoring/Profiler.js +80 -0
- package/functions/computation-system-v2/framework/resilience/Checkpointer.js +66 -0
- package/functions/computation-system-v2/framework/scheduling/ScheduleValidator.js +327 -0
- package/functions/computation-system-v2/framework/storage/StateRepository.js +286 -0
- package/functions/computation-system-v2/framework/storage/StorageManager.js +469 -0
- package/functions/computation-system-v2/framework/storage/index.js +9 -0
- package/functions/computation-system-v2/framework/testing/ComputationTester.js +86 -0
- package/functions/computation-system-v2/framework/utils/Graph.js +205 -0
- package/functions/computation-system-v2/handlers/dispatcher.js +109 -0
- package/functions/computation-system-v2/handlers/index.js +23 -0
- package/functions/computation-system-v2/handlers/onDemand.js +289 -0
- package/functions/computation-system-v2/handlers/scheduler.js +327 -0
- package/functions/computation-system-v2/index.js +163 -0
- package/functions/computation-system-v2/rules/index.js +49 -0
- package/functions/computation-system-v2/rules/instruments.js +465 -0
- package/functions/computation-system-v2/rules/metrics.js +304 -0
- package/functions/computation-system-v2/rules/portfolio.js +534 -0
- package/functions/computation-system-v2/rules/rankings.js +655 -0
- package/functions/computation-system-v2/rules/social.js +562 -0
- package/functions/computation-system-v2/rules/trades.js +545 -0
- package/functions/computation-system-v2/scripts/migrate-sectors.js +73 -0
- package/functions/computation-system-v2/test/test-dispatcher.js +317 -0
- package/functions/computation-system-v2/test/test-framework.js +500 -0
- package/functions/computation-system-v2/test/test-real-execution.js +166 -0
- package/functions/computation-system-v2/test/test-real-integration.js +194 -0
- package/functions/computation-system-v2/test/test-refactor-e2e.js +131 -0
- package/functions/computation-system-v2/test/test-results.json +31 -0
- package/functions/computation-system-v2/test/test-risk-metrics-computation.js +329 -0
- package/functions/computation-system-v2/test/test-scheduler.js +204 -0
- package/functions/computation-system-v2/test/test-storage.js +449 -0
- package/functions/orchestrator/index.js +18 -26
- package/package.json +3 -2
|
@@ -0,0 +1,720 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Popular Investor Profile Metrics - v2 Adaptation
|
|
3
|
+
*
|
|
4
|
+
* This is the most complex computation, migrated to demonstrate the v2 framework.
|
|
5
|
+
*
|
|
6
|
+
* Original v1 features preserved:
|
|
7
|
+
* - Profile page metrics for Popular Investors
|
|
8
|
+
* - Social engagement (7-day accumulation)
|
|
9
|
+
* - Profitable positions from trade history
|
|
10
|
+
* - Sector/asset exposure analysis
|
|
11
|
+
* - Rankings, ratings, page views, watchlist data
|
|
12
|
+
* - Alert history with 7-day metrics
|
|
13
|
+
* - Exposure over time (historical continuity)
|
|
14
|
+
*
|
|
15
|
+
* v2 Changes:
|
|
16
|
+
* - Uses `getConfig()` instead of `getMetadata()` + `getDependencies()`
|
|
17
|
+
* - Directly references BigQuery table names in `requires`
|
|
18
|
+
* - Data accessed via `context.data['table_name']`
|
|
19
|
+
* - Simplified series handling through lookback configuration
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
const { Computation } = require('../framework');
|
|
23
|
+
|
|
24
|
+
class PopularInvestorProfileMetrics extends Computation {
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* v2 Configuration - Single source of truth for computation requirements.
|
|
28
|
+
*/
|
|
29
|
+
static getConfig() {
|
|
30
|
+
return {
|
|
31
|
+
name: 'PopularInvestorProfileMetrics',
|
|
32
|
+
category: 'popular_investor',
|
|
33
|
+
|
|
34
|
+
// Direct table references with lookback configuration
|
|
35
|
+
requires: {
|
|
36
|
+
// Core data - mandatory for execution
|
|
37
|
+
'portfolio_snapshots': {
|
|
38
|
+
lookback: 0,
|
|
39
|
+
mandatory: true,
|
|
40
|
+
filter: { user_type: 'POPULAR_INVESTOR' }
|
|
41
|
+
},
|
|
42
|
+
'trade_history_snapshots': {
|
|
43
|
+
lookback: 0,
|
|
44
|
+
mandatory: true,
|
|
45
|
+
filter: { user_type: 'POPULAR_INVESTOR' }
|
|
46
|
+
},
|
|
47
|
+
|
|
48
|
+
// Rankings - for AUM, risk score, gain, copiers
|
|
49
|
+
'pi_rankings': {
|
|
50
|
+
lookback: 7, // Self-healing: use latest available
|
|
51
|
+
mandatory: false
|
|
52
|
+
},
|
|
53
|
+
|
|
54
|
+
// Ratings - 30-day average
|
|
55
|
+
'pi_ratings': {
|
|
56
|
+
lookback: 30,
|
|
57
|
+
mandatory: false
|
|
58
|
+
},
|
|
59
|
+
|
|
60
|
+
// Page views - 7-day accumulation
|
|
61
|
+
'pi_page_views': {
|
|
62
|
+
lookback: 7,
|
|
63
|
+
mandatory: false
|
|
64
|
+
},
|
|
65
|
+
|
|
66
|
+
// Watchlist - 30-day trend
|
|
67
|
+
'watchlist_membership': {
|
|
68
|
+
lookback: 30,
|
|
69
|
+
mandatory: false
|
|
70
|
+
},
|
|
71
|
+
|
|
72
|
+
// Alert history - 7-day metrics
|
|
73
|
+
'pi_alert_history': {
|
|
74
|
+
lookback: 7,
|
|
75
|
+
mandatory: false
|
|
76
|
+
},
|
|
77
|
+
|
|
78
|
+
// Social posts - 7-day engagement
|
|
79
|
+
'social_post_snapshots': {
|
|
80
|
+
lookback: 7,
|
|
81
|
+
mandatory: false,
|
|
82
|
+
filter: { user_type: 'POPULAR_INVESTOR' }
|
|
83
|
+
},
|
|
84
|
+
|
|
85
|
+
// Ticker mappings for display names
|
|
86
|
+
'ticker_mappings': {
|
|
87
|
+
lookback: 0, // Not date-partitioned
|
|
88
|
+
mandatory: false
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
|
|
92
|
+
// No computation dependencies
|
|
93
|
+
dependencies: [],
|
|
94
|
+
|
|
95
|
+
// Runs per user
|
|
96
|
+
type: 'per-entity',
|
|
97
|
+
|
|
98
|
+
// Needs previous day's result for exposure over time
|
|
99
|
+
isHistorical: true,
|
|
100
|
+
|
|
101
|
+
// Output config
|
|
102
|
+
ttlDays: 60
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
static getSchema() {
|
|
107
|
+
return {
|
|
108
|
+
type: 'object',
|
|
109
|
+
properties: {
|
|
110
|
+
username: { type: 'string' },
|
|
111
|
+
socialEngagement: { type: 'object' },
|
|
112
|
+
profitablePositions: { type: 'object' },
|
|
113
|
+
topWinningPositions: { type: 'object' },
|
|
114
|
+
sectorPerformance: { type: 'object' },
|
|
115
|
+
sectorExposure: { type: 'object' },
|
|
116
|
+
assetExposure: { type: 'object' },
|
|
117
|
+
portfolioSummary: { type: 'object' },
|
|
118
|
+
rankingsData: { type: 'object' },
|
|
119
|
+
ratingsData: { type: 'object' },
|
|
120
|
+
pageViewsData: { type: 'object' },
|
|
121
|
+
watchlistData: { type: 'object' },
|
|
122
|
+
alertHistoryData: { type: 'object' },
|
|
123
|
+
sectorExposureOverTime: { type: 'object' },
|
|
124
|
+
assetExposureOverTime: { type: 'object' },
|
|
125
|
+
alertMetrics: { type: 'object' }
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
static getWeight() {
|
|
131
|
+
return 10.0; // Complex per-user computation
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Process a single user's profile metrics.
|
|
136
|
+
*/
|
|
137
|
+
async process(context) {
|
|
138
|
+
const { date, entityId: userId, data, previousResult } = context;
|
|
139
|
+
|
|
140
|
+
// Extract data from context
|
|
141
|
+
const portfolio = data['portfolio_snapshots'];
|
|
142
|
+
const historyDoc = data['trade_history_snapshots'];
|
|
143
|
+
const rankings = data['pi_rankings'];
|
|
144
|
+
const ratings = data['pi_ratings'];
|
|
145
|
+
const pageViews = data['pi_page_views'];
|
|
146
|
+
const watchlist = data['watchlist_membership'];
|
|
147
|
+
const alerts = data['pi_alert_history'];
|
|
148
|
+
const social = data['social_post_snapshots'];
|
|
149
|
+
const tickerMappings = data['ticker_mappings'];
|
|
150
|
+
|
|
151
|
+
// Build instrument mappings
|
|
152
|
+
const mappings = this._buildMappings(tickerMappings);
|
|
153
|
+
|
|
154
|
+
// Initialize result structure
|
|
155
|
+
const result = this._initializeResult();
|
|
156
|
+
|
|
157
|
+
// ==========================================================================================
|
|
158
|
+
// 1. Social Engagement (7-Day Accumulation + Chart)
|
|
159
|
+
// ==========================================================================================
|
|
160
|
+
this._processSocialEngagement(result, social, userId);
|
|
161
|
+
|
|
162
|
+
// ==========================================================================================
|
|
163
|
+
// 2. Profitable Positions (From Trade History)
|
|
164
|
+
// ==========================================================================================
|
|
165
|
+
this._processProfitablePositions(result, historyDoc);
|
|
166
|
+
|
|
167
|
+
// ==========================================================================================
|
|
168
|
+
// 3. Top Winning Positions
|
|
169
|
+
// ==========================================================================================
|
|
170
|
+
this._processTopWinningPositions(result, portfolio, historyDoc, mappings);
|
|
171
|
+
|
|
172
|
+
// ==========================================================================================
|
|
173
|
+
// 4. Sector Performance
|
|
174
|
+
// ==========================================================================================
|
|
175
|
+
this._processSectorPerformance(result, portfolio, mappings);
|
|
176
|
+
|
|
177
|
+
// ==========================================================================================
|
|
178
|
+
// 5. & 6. Exposure & Portfolio Summary
|
|
179
|
+
// ==========================================================================================
|
|
180
|
+
this._processExposureAndSummary(result, portfolio, mappings);
|
|
181
|
+
|
|
182
|
+
// ==========================================================================================
|
|
183
|
+
// 7. Rankings Data
|
|
184
|
+
// ==========================================================================================
|
|
185
|
+
this._processRankingsData(result, rankings, userId);
|
|
186
|
+
|
|
187
|
+
// ==========================================================================================
|
|
188
|
+
// 8. Username (from rankings)
|
|
189
|
+
// ==========================================================================================
|
|
190
|
+
if (rankings) {
|
|
191
|
+
const rankEntry = this._findLatestRankEntry(rankings, userId);
|
|
192
|
+
if (rankEntry) {
|
|
193
|
+
result.username = rankEntry.UserName || rankEntry.username || null;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// ==========================================================================================
|
|
198
|
+
// 9. Ratings Data (30-Day Average + Trend)
|
|
199
|
+
// ==========================================================================================
|
|
200
|
+
this._processRatingsData(result, ratings, userId);
|
|
201
|
+
|
|
202
|
+
// ==========================================================================================
|
|
203
|
+
// 10. Page Views Data (7-Day Sum + Daily Chart)
|
|
204
|
+
// ==========================================================================================
|
|
205
|
+
this._processPageViewsData(result, pageViews, userId);
|
|
206
|
+
|
|
207
|
+
// ==========================================================================================
|
|
208
|
+
// 11. Watchlist Data (Latest Snapshot + 30-Day Trend)
|
|
209
|
+
// ==========================================================================================
|
|
210
|
+
this._processWatchlistData(result, watchlist, userId);
|
|
211
|
+
|
|
212
|
+
// ==========================================================================================
|
|
213
|
+
// 12. Alert History (Accumulation + Metrics)
|
|
214
|
+
// ==========================================================================================
|
|
215
|
+
this._processAlertHistory(result, alerts, userId);
|
|
216
|
+
|
|
217
|
+
// ==========================================================================================
|
|
218
|
+
// 13. Exposure Over Time (Requires Previous State)
|
|
219
|
+
// ==========================================================================================
|
|
220
|
+
this._processExposureOverTime(result, previousResult, date);
|
|
221
|
+
|
|
222
|
+
// Store result
|
|
223
|
+
this.setResult(userId, result);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// =========================================================================
|
|
227
|
+
// PRIVATE METHODS
|
|
228
|
+
// =========================================================================
|
|
229
|
+
|
|
230
|
+
_initializeResult() {
|
|
231
|
+
return {
|
|
232
|
+
username: null,
|
|
233
|
+
socialEngagement: { chartType: 'line', data: [] },
|
|
234
|
+
profitablePositions: { chartType: 'bar', data: [] },
|
|
235
|
+
topWinningPositions: { chartType: 'table', data: [] },
|
|
236
|
+
sectorPerformance: { bestSector: null, worstSector: null, bestSectorProfit: 0, worstSectorProfit: 0 },
|
|
237
|
+
sectorExposure: { chartType: 'pie', data: {} },
|
|
238
|
+
assetExposure: { chartType: 'pie', data: {} },
|
|
239
|
+
portfolioSummary: { totalInvested: 0, totalProfit: 0, profitPercent: 0 },
|
|
240
|
+
rankingsData: { aum: 0, riskScore: 0, gain: 0, copiers: 0, winRatio: 0, trades: 0 },
|
|
241
|
+
ratingsData: {
|
|
242
|
+
averageRating: 0,
|
|
243
|
+
totalRatings: 0,
|
|
244
|
+
ratingsOverTime: { chartType: 'line', data: [] }
|
|
245
|
+
},
|
|
246
|
+
pageViewsData: {
|
|
247
|
+
totalViews: 0,
|
|
248
|
+
uniqueViewers: 0,
|
|
249
|
+
viewsOverTime: { chartType: 'bar', data: [] }
|
|
250
|
+
},
|
|
251
|
+
watchlistData: {
|
|
252
|
+
totalUsers: 0,
|
|
253
|
+
publicWatchlistCount: 0,
|
|
254
|
+
privateWatchlistCount: 0,
|
|
255
|
+
watchlistOverTime: { chartType: 'line', data: [] }
|
|
256
|
+
},
|
|
257
|
+
alertHistoryData: { triggeredAlerts: [], alertCountsOverTime: { chartType: 'bar', data: [] } },
|
|
258
|
+
sectorExposureOverTime: { chartType: 'line', data: [] },
|
|
259
|
+
assetExposureOverTime: { chartType: 'line', data: [] },
|
|
260
|
+
alertMetrics: { totalLast7Days: 0, breakdown: {} }
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
_buildMappings(tickerMappings) {
|
|
265
|
+
const mappings = {
|
|
266
|
+
instrumentToTicker: {},
|
|
267
|
+
instrumentToSector: {}
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
if (!tickerMappings) return mappings;
|
|
271
|
+
|
|
272
|
+
// Handle different data structures
|
|
273
|
+
if (Array.isArray(tickerMappings)) {
|
|
274
|
+
for (const row of tickerMappings) {
|
|
275
|
+
const id = row.instrument_id || row.instrumentId;
|
|
276
|
+
if (id) {
|
|
277
|
+
mappings.instrumentToTicker[String(id)] = row.ticker || row.symbol || `ID${id}`;
|
|
278
|
+
if (row.sector) mappings.instrumentToSector[String(id)] = row.sector;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
} else if (typeof tickerMappings === 'object') {
|
|
282
|
+
for (const [id, data] of Object.entries(tickerMappings)) {
|
|
283
|
+
if (typeof data === 'string') {
|
|
284
|
+
mappings.instrumentToTicker[id] = data;
|
|
285
|
+
} else if (typeof data === 'object') {
|
|
286
|
+
mappings.instrumentToTicker[id] = data.ticker || data.symbol || `ID${id}`;
|
|
287
|
+
if (data.sector) mappings.instrumentToSector[id] = data.sector;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
return mappings;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
_processSocialEngagement(result, social, userId) {
|
|
296
|
+
if (!social) return;
|
|
297
|
+
|
|
298
|
+
// social is date-keyed: { "2026-01-24": [...], "2026-01-23": [...] }
|
|
299
|
+
const dates = Object.keys(social).sort();
|
|
300
|
+
|
|
301
|
+
for (const dateStr of dates) {
|
|
302
|
+
const dailyData = social[dateStr];
|
|
303
|
+
if (!dailyData) continue;
|
|
304
|
+
|
|
305
|
+
// Find user's posts
|
|
306
|
+
let userPosts = [];
|
|
307
|
+
if (Array.isArray(dailyData)) {
|
|
308
|
+
userPosts = dailyData.filter(p => String(p.user_id || p.userId) === String(userId));
|
|
309
|
+
} else if (dailyData[userId]) {
|
|
310
|
+
userPosts = Array.isArray(dailyData[userId]) ? dailyData[userId] : [dailyData[userId]];
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if (userPosts.length > 0) {
|
|
314
|
+
let likes = 0, comments = 0;
|
|
315
|
+
for (const post of userPosts) {
|
|
316
|
+
likes += (post.LikesCount || post.likes || 0);
|
|
317
|
+
comments += (post.CommentsCount || post.comments || 0);
|
|
318
|
+
}
|
|
319
|
+
result.socialEngagement.data.push({ date: dateStr, likes, comments });
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
_processProfitablePositions(result, historyDoc) {
|
|
325
|
+
if (!historyDoc) return;
|
|
326
|
+
|
|
327
|
+
const trades = this._extractTrades(historyDoc);
|
|
328
|
+
const profitableMap = new Map();
|
|
329
|
+
|
|
330
|
+
for (const trade of trades) {
|
|
331
|
+
const closeDateTime = trade.CloseDateTime || trade.closeDateTime || trade.close_date;
|
|
332
|
+
if (!closeDateTime) continue;
|
|
333
|
+
|
|
334
|
+
const closeDate = new Date(closeDateTime);
|
|
335
|
+
if (isNaN(closeDate.getTime())) continue;
|
|
336
|
+
|
|
337
|
+
const dateKey = closeDate.toISOString().split('T')[0];
|
|
338
|
+
const existing = profitableMap.get(dateKey) || { date: dateKey, profitableCount: 0, totalCount: 0 };
|
|
339
|
+
existing.totalCount++;
|
|
340
|
+
|
|
341
|
+
const netProfit = trade.NetProfit || trade.netProfit || trade.net_profit || 0;
|
|
342
|
+
if (netProfit > 0) existing.profitableCount++;
|
|
343
|
+
|
|
344
|
+
profitableMap.set(dateKey, existing);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
result.profitablePositions.data = Array.from(profitableMap.values())
|
|
348
|
+
.sort((a, b) => a.date.localeCompare(b.date))
|
|
349
|
+
.slice(-30);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
_processTopWinningPositions(result, portfolio, historyDoc, mappings) {
|
|
353
|
+
const topWinning = [];
|
|
354
|
+
|
|
355
|
+
// From current portfolio
|
|
356
|
+
if (portfolio) {
|
|
357
|
+
const positions = this._extractPositions(portfolio);
|
|
358
|
+
for (const pos of positions) {
|
|
359
|
+
const instrumentId = pos.InstrumentID || pos.instrumentId || pos.instrument_id;
|
|
360
|
+
const netProfit = pos.NetProfit || pos.netProfit || pos.net_profit || 0;
|
|
361
|
+
const invested = pos.Amount || pos.amount || pos.invested || 0;
|
|
362
|
+
const value = pos.Value || pos.value || invested + netProfit;
|
|
363
|
+
|
|
364
|
+
if (netProfit > 0 && instrumentId) {
|
|
365
|
+
const ticker = mappings.instrumentToTicker[String(instrumentId)] || `ID${instrumentId}`;
|
|
366
|
+
topWinning.push({ instrumentId, ticker, netProfit, invested, value, isCurrent: true, closeDate: null });
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// From trade history
|
|
372
|
+
if (historyDoc) {
|
|
373
|
+
const trades = this._extractTrades(historyDoc);
|
|
374
|
+
for (const trade of trades) {
|
|
375
|
+
const netProfit = trade.NetProfit || trade.netProfit || trade.net_profit || 0;
|
|
376
|
+
const instrumentId = trade.InstrumentID || trade.instrumentId || trade.instrument_id;
|
|
377
|
+
|
|
378
|
+
if (netProfit > 0 && instrumentId) {
|
|
379
|
+
const ticker = mappings.instrumentToTicker[String(instrumentId)] || `ID${instrumentId}`;
|
|
380
|
+
const closeDate = trade.CloseDateTime || trade.closeDateTime || trade.close_date || null;
|
|
381
|
+
topWinning.push({ instrumentId, ticker, netProfit, invested: 0, value: 0, isCurrent: false, closeDate });
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
result.topWinningPositions.data = topWinning
|
|
387
|
+
.sort((a, b) => b.netProfit - a.netProfit)
|
|
388
|
+
.slice(0, 20);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
_processSectorPerformance(result, portfolio, mappings) {
|
|
392
|
+
if (!portfolio || !mappings.instrumentToSector || Object.keys(mappings.instrumentToSector).length === 0) {
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const positions = this._extractPositions(portfolio);
|
|
397
|
+
const sectorProfits = new Map();
|
|
398
|
+
|
|
399
|
+
for (const pos of positions) {
|
|
400
|
+
const instrumentId = pos.InstrumentID || pos.instrumentId || pos.instrument_id;
|
|
401
|
+
const netProfit = pos.NetProfit || pos.netProfit || pos.net_profit || 0;
|
|
402
|
+
const invested = pos.Amount || pos.amount || pos.invested || 0;
|
|
403
|
+
|
|
404
|
+
if (instrumentId && invested > 0) {
|
|
405
|
+
const sector = mappings.instrumentToSector[String(instrumentId)] || 'Unknown';
|
|
406
|
+
const weightedProfit = invested * (netProfit / 100);
|
|
407
|
+
const existing = sectorProfits.get(sector) || { totalProfit: 0, totalInvested: 0 };
|
|
408
|
+
existing.totalProfit += weightedProfit;
|
|
409
|
+
existing.totalInvested += invested;
|
|
410
|
+
sectorProfits.set(sector, existing);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
let bestSector = null, worstSector = null;
|
|
415
|
+
let bestProfit = -Infinity, worstProfit = Infinity;
|
|
416
|
+
|
|
417
|
+
for (const [sector, data] of sectorProfits.entries()) {
|
|
418
|
+
const profitPercent = data.totalInvested > 0 ? (data.totalProfit / data.totalInvested) * 100 : 0;
|
|
419
|
+
if (profitPercent > bestProfit) { bestProfit = profitPercent; bestSector = sector; }
|
|
420
|
+
if (profitPercent < worstProfit) { worstProfit = profitPercent; worstSector = sector; }
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
result.sectorPerformance = {
|
|
424
|
+
bestSector,
|
|
425
|
+
worstSector,
|
|
426
|
+
bestSectorProfit: bestProfit !== -Infinity ? bestProfit : 0,
|
|
427
|
+
worstSectorProfit: worstProfit !== Infinity ? worstProfit : 0
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
_processExposureAndSummary(result, portfolio, mappings) {
|
|
432
|
+
if (!portfolio) return;
|
|
433
|
+
|
|
434
|
+
const positions = this._extractPositions(portfolio);
|
|
435
|
+
const sectorMap = {};
|
|
436
|
+
const assetMap = {};
|
|
437
|
+
let total = 0, totalInvested = 0, totalProfit = 0;
|
|
438
|
+
|
|
439
|
+
for (const pos of positions) {
|
|
440
|
+
const instrumentId = pos.InstrumentID || pos.instrumentId || pos.instrument_id;
|
|
441
|
+
const invested = pos.Amount || pos.amount || pos.invested || 0;
|
|
442
|
+
const netProfit = pos.NetProfit || pos.netProfit || pos.net_profit || 0;
|
|
443
|
+
const value = pos.Value || pos.value || invested + netProfit;
|
|
444
|
+
|
|
445
|
+
total += value;
|
|
446
|
+
totalInvested += invested;
|
|
447
|
+
totalProfit += invested * (netProfit / 100);
|
|
448
|
+
|
|
449
|
+
const sector = mappings.instrumentToSector[String(instrumentId)] || 'Unknown';
|
|
450
|
+
sectorMap[sector] = (sectorMap[sector] || 0) + value;
|
|
451
|
+
|
|
452
|
+
const ticker = mappings.instrumentToTicker[String(instrumentId)] || `ID${instrumentId}`;
|
|
453
|
+
assetMap[ticker] = (assetMap[ticker] || 0) + value;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
if (total > 0) {
|
|
457
|
+
Object.keys(sectorMap).forEach(k => {
|
|
458
|
+
result.sectorExposure.data[k] = Number(((sectorMap[k] / total) * 100).toFixed(2));
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
Object.entries(assetMap)
|
|
462
|
+
.sort(([, a], [, b]) => b - a)
|
|
463
|
+
.slice(0, 10)
|
|
464
|
+
.forEach(([k, v]) => {
|
|
465
|
+
result.assetExposure.data[k] = Number(((v / total) * 100).toFixed(2));
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
result.portfolioSummary = {
|
|
470
|
+
totalInvested: Number(totalInvested.toFixed(2)),
|
|
471
|
+
totalProfit: Number(totalProfit.toFixed(2)),
|
|
472
|
+
profitPercent: totalInvested > 0 ? Number(((totalProfit / totalInvested) * 100).toFixed(2)) : 0
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
_processRankingsData(result, rankings, userId) {
|
|
477
|
+
const rankEntry = this._findLatestRankEntry(rankings, userId);
|
|
478
|
+
if (!rankEntry) return;
|
|
479
|
+
|
|
480
|
+
result.rankingsData = {
|
|
481
|
+
aum: rankEntry.AUMTierDesc || rankEntry.Aum || rankEntry.aum || 0,
|
|
482
|
+
riskScore: rankEntry.RiskScore || rankEntry.riskScore || 0,
|
|
483
|
+
gain: rankEntry.Gain || rankEntry.gain || 0,
|
|
484
|
+
copiers: rankEntry.Copiers || rankEntry.copiers || 0,
|
|
485
|
+
winRatio: rankEntry.WinRatio || rankEntry.winRatio || 0,
|
|
486
|
+
trades: rankEntry.Trades || rankEntry.trades || 0
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
_findLatestRankEntry(rankings, userId) {
|
|
491
|
+
if (!rankings) return null;
|
|
492
|
+
|
|
493
|
+
// rankings is date-keyed: { "2026-01-24": [...], ... }
|
|
494
|
+
if (typeof rankings === 'object' && !Array.isArray(rankings)) {
|
|
495
|
+
const dates = Object.keys(rankings).sort().reverse();
|
|
496
|
+
for (const dateStr of dates) {
|
|
497
|
+
const dayRankings = rankings[dateStr];
|
|
498
|
+
if (Array.isArray(dayRankings)) {
|
|
499
|
+
const entry = dayRankings.find(r =>
|
|
500
|
+
String(r.CustomerId || r.pi_id || r.userId) === String(userId)
|
|
501
|
+
);
|
|
502
|
+
if (entry) return entry;
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// Direct array
|
|
508
|
+
if (Array.isArray(rankings)) {
|
|
509
|
+
return rankings.find(r =>
|
|
510
|
+
String(r.CustomerId || r.pi_id || r.userId) === String(userId)
|
|
511
|
+
);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
return null;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
_processRatingsData(result, ratings, userId) {
|
|
518
|
+
if (!ratings) return;
|
|
519
|
+
|
|
520
|
+
const dates = Object.keys(ratings).sort();
|
|
521
|
+
let sumRatings = 0, countRatings = 0;
|
|
522
|
+
|
|
523
|
+
for (const dateStr of dates) {
|
|
524
|
+
const dayRatings = ratings[dateStr];
|
|
525
|
+
const userRating = this._findUserData(dayRatings, userId);
|
|
526
|
+
|
|
527
|
+
if (userRating) {
|
|
528
|
+
const avg = userRating.average_rating || userRating.avgRating || 0;
|
|
529
|
+
const total = userRating.total_ratings || userRating.totalRatings || 0;
|
|
530
|
+
|
|
531
|
+
result.ratingsData.ratingsOverTime.data.push({
|
|
532
|
+
date: dateStr,
|
|
533
|
+
rating: avg,
|
|
534
|
+
count: total
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
if (avg > 0) {
|
|
538
|
+
sumRatings += avg;
|
|
539
|
+
countRatings++;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
result.ratingsData.totalRatings = total;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
if (countRatings > 0) {
|
|
547
|
+
result.ratingsData.averageRating = Number((sumRatings / countRatings).toFixed(2));
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
_processPageViewsData(result, pageViews, userId) {
|
|
552
|
+
if (!pageViews) return;
|
|
553
|
+
|
|
554
|
+
const dates = Object.keys(pageViews).sort();
|
|
555
|
+
|
|
556
|
+
for (const dateStr of dates) {
|
|
557
|
+
const dayViews = pageViews[dateStr];
|
|
558
|
+
const userViews = this._findUserData(dayViews, userId);
|
|
559
|
+
|
|
560
|
+
if (userViews) {
|
|
561
|
+
const views = userViews.total_views || userViews.totalViews || 0;
|
|
562
|
+
const uniques = userViews.unique_viewers || userViews.uniqueViewers || 0;
|
|
563
|
+
|
|
564
|
+
result.pageViewsData.viewsOverTime.data.push({
|
|
565
|
+
date: dateStr,
|
|
566
|
+
views: views
|
|
567
|
+
});
|
|
568
|
+
|
|
569
|
+
result.pageViewsData.totalViews += views;
|
|
570
|
+
result.pageViewsData.uniqueViewers = uniques;
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
_processWatchlistData(result, watchlist, userId) {
|
|
576
|
+
if (!watchlist) return;
|
|
577
|
+
|
|
578
|
+
const dates = Object.keys(watchlist).sort();
|
|
579
|
+
|
|
580
|
+
for (const dateStr of dates) {
|
|
581
|
+
const dayWatchlist = watchlist[dateStr];
|
|
582
|
+
const userWatchlist = this._findUserData(dayWatchlist, userId);
|
|
583
|
+
|
|
584
|
+
if (userWatchlist) {
|
|
585
|
+
const total = userWatchlist.total_users || userWatchlist.totalUsers || 0;
|
|
586
|
+
|
|
587
|
+
result.watchlistData.watchlistOverTime.data.push({
|
|
588
|
+
date: dateStr,
|
|
589
|
+
count: total
|
|
590
|
+
});
|
|
591
|
+
|
|
592
|
+
result.watchlistData.totalUsers = total;
|
|
593
|
+
result.watchlistData.publicWatchlistCount = userWatchlist.public_watchlist_count || userWatchlist.publicCount || 0;
|
|
594
|
+
result.watchlistData.privateWatchlistCount = userWatchlist.private_watchlist_count || userWatchlist.privateCount || 0;
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
_processAlertHistory(result, alerts, userId) {
|
|
600
|
+
if (!alerts) return;
|
|
601
|
+
|
|
602
|
+
const dates = Object.keys(alerts).sort();
|
|
603
|
+
let totalAlertsLast7Days = 0;
|
|
604
|
+
const alertBreakdown = {};
|
|
605
|
+
|
|
606
|
+
for (const dateStr of dates) {
|
|
607
|
+
const dayAlerts = alerts[dateStr];
|
|
608
|
+
const userAlerts = this._findUserData(dayAlerts, userId);
|
|
609
|
+
|
|
610
|
+
if (userAlerts && typeof userAlerts === 'object') {
|
|
611
|
+
let dayCount = 0;
|
|
612
|
+
|
|
613
|
+
for (const [alertType, data] of Object.entries(userAlerts)) {
|
|
614
|
+
if (alertType.toLowerCase().includes('test')) continue;
|
|
615
|
+
|
|
616
|
+
const count = data.count || data.trigger_count || 1;
|
|
617
|
+
totalAlertsLast7Days += count;
|
|
618
|
+
dayCount += count;
|
|
619
|
+
|
|
620
|
+
alertBreakdown[alertType] = (alertBreakdown[alertType] || 0) + count;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
if (dayCount > 0) {
|
|
624
|
+
result.alertHistoryData.alertCountsOverTime.data.push({
|
|
625
|
+
date: dateStr,
|
|
626
|
+
count: dayCount
|
|
627
|
+
});
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
result.alertMetrics.totalLast7Days = totalAlertsLast7Days;
|
|
633
|
+
result.alertMetrics.breakdown = alertBreakdown;
|
|
634
|
+
|
|
635
|
+
// Sort chart
|
|
636
|
+
result.alertHistoryData.alertCountsOverTime.data.sort((a, b) => a.date.localeCompare(b.date));
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
_processExposureOverTime(result, previousResult, currentDate) {
|
|
640
|
+
// Restore from previous day
|
|
641
|
+
if (previousResult) {
|
|
642
|
+
if (previousResult.sectorExposureOverTime?.data) {
|
|
643
|
+
result.sectorExposureOverTime.data = [...previousResult.sectorExposureOverTime.data];
|
|
644
|
+
}
|
|
645
|
+
if (previousResult.assetExposureOverTime?.data) {
|
|
646
|
+
result.assetExposureOverTime.data = [...previousResult.assetExposureOverTime.data];
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
// Add today's exposure
|
|
651
|
+
if (Object.keys(result.sectorExposure.data).length > 0) {
|
|
652
|
+
const todaySectorEntry = { date: currentDate, sectors: result.sectorExposure.data };
|
|
653
|
+
const idx = result.sectorExposureOverTime.data.findIndex(d => d.date === currentDate);
|
|
654
|
+
if (idx >= 0) {
|
|
655
|
+
result.sectorExposureOverTime.data[idx] = todaySectorEntry;
|
|
656
|
+
} else {
|
|
657
|
+
result.sectorExposureOverTime.data.push(todaySectorEntry);
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
if (Object.keys(result.assetExposure.data).length > 0) {
|
|
662
|
+
const todayAssetEntry = { date: currentDate, assets: result.assetExposure.data };
|
|
663
|
+
const idx = result.assetExposureOverTime.data.findIndex(d => d.date === currentDate);
|
|
664
|
+
if (idx >= 0) {
|
|
665
|
+
result.assetExposureOverTime.data[idx] = todayAssetEntry;
|
|
666
|
+
} else {
|
|
667
|
+
result.assetExposureOverTime.data.push(todayAssetEntry);
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
// Limit to 30 days
|
|
672
|
+
if (result.sectorExposureOverTime.data.length > 30) {
|
|
673
|
+
result.sectorExposureOverTime.data = result.sectorExposureOverTime.data.slice(-30);
|
|
674
|
+
}
|
|
675
|
+
if (result.assetExposureOverTime.data.length > 30) {
|
|
676
|
+
result.assetExposureOverTime.data = result.assetExposureOverTime.data.slice(-30);
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
// =========================================================================
|
|
681
|
+
// DATA EXTRACTION HELPERS
|
|
682
|
+
// =========================================================================
|
|
683
|
+
|
|
684
|
+
_extractPositions(portfolio) {
|
|
685
|
+
if (!portfolio) return [];
|
|
686
|
+
if (Array.isArray(portfolio)) return portfolio;
|
|
687
|
+
if (portfolio.AggregatedPositions) return portfolio.AggregatedPositions;
|
|
688
|
+
if (portfolio.Positions) return portfolio.Positions;
|
|
689
|
+
if (portfolio.positions) return portfolio.positions;
|
|
690
|
+
return [];
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
_extractTrades(historyDoc) {
|
|
694
|
+
if (!historyDoc) return [];
|
|
695
|
+
if (Array.isArray(historyDoc)) return historyDoc;
|
|
696
|
+
if (historyDoc.Trades) return historyDoc.Trades;
|
|
697
|
+
if (historyDoc.trades) return historyDoc.trades;
|
|
698
|
+
if (historyDoc.History) return historyDoc.History;
|
|
699
|
+
return [];
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
_findUserData(dayData, userId) {
|
|
703
|
+
if (!dayData) return null;
|
|
704
|
+
|
|
705
|
+
// Direct lookup by user ID
|
|
706
|
+
if (dayData[userId]) return dayData[userId];
|
|
707
|
+
if (dayData[String(userId)]) return dayData[String(userId)];
|
|
708
|
+
|
|
709
|
+
// Array - find by user ID field
|
|
710
|
+
if (Array.isArray(dayData)) {
|
|
711
|
+
return dayData.find(item =>
|
|
712
|
+
String(item.pi_id || item.user_id || item.userId) === String(userId)
|
|
713
|
+
);
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
return null;
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
module.exports = PopularInvestorProfileMetrics;
|