bulltrackers-module 1.0.769 → 1.0.771

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.
@@ -1,13 +1,9 @@
1
1
  /**
2
- * @fileoverview PI Recommender (v2 - Diversification-Aware)
3
- *
4
- * Recommends Popular Investors to signed-in users based on:
5
- * 1. User preferences (sectors, risk tolerance they're drawn to)
6
- * 2. Diversification benefit (low correlation with current portfolio)
7
- * 3. Quality thresholds (risk score, copiers, track record)
8
- *
9
- * This replaces the legacy similarity-based approach which was harmful
10
- * because it doubled down on existing exposure instead of diversifying.
2
+ * @fileoverview PI Recommender (v3 - Recursive Correlation)
3
+ * * Recommends Popular Investors by:
4
+ * 1. Recursively unwrapping the user's copied PIs to find True Net Exposure.
5
+ * 2. Calculating mathematical correlation between User and Candidate using Asset/Sector matrices.
6
+ * 3. Favoring "Uncorrelated" (near 0) over "Inverse" (near -1) or "Correlated" (near 1).
11
7
  */
12
8
  const { Computation } = require('../framework');
13
9
 
@@ -15,31 +11,28 @@ class PiRecommender extends Computation {
15
11
  static getConfig() {
16
12
  return {
17
13
  name: 'PiRecommender',
18
- description: 'Recommends PIs balancing user preferences with diversification',
14
+ description: 'Recommends PIs based on recursive portfolio variance reduction',
19
15
  type: 'per-entity',
20
16
  category: 'signed_in_user',
21
17
  isHistorical: false,
22
18
 
23
- // Depends on global PI feature vectors
24
- dependencies: ['PiFeatureVectors'],
19
+ dependencies: ['pifeaturevectors', 'sectorcorrelations'],
25
20
 
26
21
  requires: {
27
22
  'portfolio_snapshots': {
28
- lookback: 0,
23
+ lookback: 7,
29
24
  mandatory: true,
30
25
  fields: ['user_id', 'portfolio_data', 'date'],
31
26
  filter: { user_type: 'SIGNED_IN_USER' }
32
27
  },
33
- 'ticker_mappings': { mandatory: false },
34
- 'sector_mappings': { mandatory: false }
28
+ 'ticker_mappings': { mandatory: false, fields: ['instrument_id', 'ticker'] },
29
+ 'sector_mappings': { mandatory: false, fields: ['symbol', 'sector'] }
35
30
  },
36
31
 
37
32
  storage: {
38
33
  bigquery: true,
39
34
  firestore: {
40
- enabled: true,
41
- path: 'users/{entityId}/recommendations',
42
- merge: true
35
+ enabled: false
43
36
  }
44
37
  }
45
38
  };
@@ -48,45 +41,40 @@ class PiRecommender extends Computation {
48
41
  async process(context) {
49
42
  const { data, entityId, rules, getDependency, targetDate } = context;
50
43
 
44
+ const portfolioRows = data['portfolio_snapshots'];
45
+ if (!portfolioRows || !Array.isArray(portfolioRows) || portfolioRows.length === 0) {
46
+ this.setResult(entityId, { recommendations: [], reason: 'No portfolio data' });
47
+ return;
48
+ }
49
+
51
50
  // ===========================================================================
52
- // CONFIGURATION: Scoring weights and thresholds
51
+ // 1. LOAD GLOBAL DEPENDENCIES
53
52
  // ===========================================================================
54
- const WEIGHTS = {
55
- interest: 0.30, // How much they match user preferences
56
- diversification: 0.40, // How different they are (highest weight!)
57
- quality: 0.30 // Performance and popularity
58
- };
53
+ const piFeatureData = getDependency('pifeaturevectors', '_global');
54
+ const correlationData = getDependency('sectorcorrelations', '_global');
59
55
 
60
- const THRESHOLDS = {
61
- minCopiers: 10, // Skip unpopular PIs
62
- maxRiskScore: 8, // Skip very high risk
63
- minGain: -10, // Skip extreme losers
64
- excludeAlreadyCopied: true
65
- };
56
+ if (!piFeatureData || !correlationData) {
57
+ this.setResult(entityId, { error: 'Missing global dependencies' });
58
+ return;
59
+ }
66
60
 
67
- const TOP_N = 10; // Return top N recommendations
61
+ const piVectors = piFeatureData.vectors;
62
+ const sectorCorrelations = correlationData.correlationMatrix;
63
+ const sectorList = correlationData.sectors;
68
64
 
69
65
  // ===========================================================================
70
- // SETUP: Build sector lookup and helpers
66
+ // 2. SETUP MAPPINGS
71
67
  // ===========================================================================
72
- const toArray = (input) => {
73
- if (!input) return [];
74
- if (Array.isArray(input)) return input;
75
- return Object.values(input).flat();
76
- };
68
+ const toArray = (input) => Array.isArray(input) ? input : Object.values(input).flat();
77
69
 
78
70
  const tickerMap = new Map();
79
71
  toArray(data['ticker_mappings']).forEach(row => {
80
- if (row.instrument_id && row.ticker) {
81
- tickerMap.set(Number(row.instrument_id), row.ticker.toUpperCase());
82
- }
72
+ if (row.instrument_id && row.ticker) tickerMap.set(Number(row.instrument_id), row.ticker.toUpperCase());
83
73
  });
84
74
 
85
75
  const sectorMap = new Map();
86
76
  toArray(data['sector_mappings']).forEach(row => {
87
- if (row.symbol && row.sector) {
88
- sectorMap.set(row.symbol.toUpperCase(), row.sector);
89
- }
77
+ if (row.symbol && row.sector) sectorMap.set(row.symbol.toUpperCase(), row.sector);
90
78
  });
91
79
 
92
80
  const resolveSector = (instrumentId) => {
@@ -95,265 +83,206 @@ class PiRecommender extends Computation {
95
83
  };
96
84
 
97
85
  // ===========================================================================
98
- // STEP 1: Get user's current portfolio profile
86
+ // 3. BUILD USER'S TRUE RECURSIVE PROFILE
99
87
  // ===========================================================================
100
- const portfolioRows = data['portfolio_snapshots']?.[entityId];
101
- if (!portfolioRows || portfolioRows.length === 0) {
102
- this.setResult(entityId, { recommendations: [], reason: 'No portfolio data' });
103
- return;
104
- }
105
-
106
- const latestRow = Array.isArray(portfolioRows)
107
- ? portfolioRows[portfolioRows.length - 1]
108
- : portfolioRows;
109
-
88
+ const latestRow = portfolioRows[portfolioRows.length - 1];
110
89
  const pData = rules.portfolio.extractPortfolioData(latestRow);
111
- const positions = rules.portfolio.extractPositions(pData);
112
- const mirrors = pData.AggregatedMirrors || [];
113
-
114
- // Already copied PI IDs
115
- const copiedPiIds = new Set(
116
- mirrors.map(m => String(m.ParentCID || m.CID)).filter(Boolean)
117
- );
118
90
 
119
- // Build user's sector exposure
120
- const userSectorWeights = {};
121
- let userTotalValue = 0;
91
+ // Initialize User Vector (Units: % of Portfolio Value)
92
+ const userNetVector = {};
93
+ sectorList.forEach(s => userNetVector[s] = 0);
122
94
 
95
+ // 3a. Add Direct Positions (Signed Value)
96
+ const positions = rules.portfolio.extractPositions(pData);
123
97
  positions.forEach(pos => {
124
98
  const instrumentId = rules.portfolio.getInstrumentId(pos);
125
- const invested = rules.portfolio.getInvested(pos) || 0;
99
+ const value = rules.portfolio.getValue(pos) || 0; // Already % (e.g., 5.0 for 5%)
100
+ const isShort = rules.portfolio.isShort(pos);
126
101
  const sector = resolveSector(instrumentId);
127
102
 
128
- userSectorWeights[sector] = (userSectorWeights[sector] || 0) + invested;
129
- userTotalValue += invested;
130
- });
131
-
132
- // Include mirror exposures
133
- mirrors.forEach(m => {
134
- userTotalValue += (m.Invested || m.Amount || 0);
103
+ const signedValue = isShort ? -value : value;
104
+ if (userNetVector[sector] !== undefined) {
105
+ userNetVector[sector] += signedValue;
106
+ }
135
107
  });
136
108
 
137
- // Normalize to percentages
138
- const userSectorProfile = {};
139
- Object.entries(userSectorWeights).forEach(([sector, weight]) => {
140
- userSectorProfile[sector] = userTotalValue > 0
141
- ? weight / userTotalValue
142
- : 0;
109
+ // 3b. Add Recursive Copy Exposure
110
+ const mirrors = rules.portfolio.extractMirrors(pData);
111
+ const copiedPiIds = new Set();
112
+
113
+ mirrors.forEach(mirror => {
114
+ const parentId = String(mirror.ParentCID || mirror.CID);
115
+ copiedPiIds.add(parentId);
116
+
117
+ // investedInCopy is % of user's portfolio (e.g., 20.0 for 20%)
118
+ const investedInCopy = mirror.Value || 0;
119
+
120
+ // Lookup Parent Vector (Weights are % of Parent's portfolio)
121
+ const parentStats = piVectors[parentId];
122
+ if (parentStats && parentStats.sectors) {
123
+ Object.entries(parentStats.sectors).forEach(([sec, weight]) => {
124
+ if (userNetVector[sec] !== undefined) {
125
+ // Formula: Parent_Sector_% * (User_Alloc_% / 100) = Contributed_%
126
+ // Example: 50.0% * (20.0% / 100) = 10.0%
127
+ userNetVector[sec] += (weight * (investedInCopy / 100));
128
+ }
129
+ });
130
+ }
143
131
  });
144
132
 
145
- // Infer user preferences from copied PIs
146
- const userPreferredRiskRange = this._inferRiskPreference(mirrors, rules);
147
-
148
133
  // ===========================================================================
149
- // STEP 2: Load global PI feature vectors
134
+ // 4. SCORE CANDIDATES
150
135
  // ===========================================================================
151
- const piFeatureData = getDependency('PiFeatureVectors');
152
- if (!piFeatureData || !piFeatureData.vectors) {
153
- this.setResult(entityId, { recommendations: [], reason: 'No PI features available' });
154
- return;
155
- }
156
-
157
- const piVectors = piFeatureData.vectors;
136
+ const candidates = [];
158
137
  const allPiIds = Object.keys(piVectors);
159
138
 
160
- // ===========================================================================
161
- // STEP 3: Score each candidate PI
162
- // ===========================================================================
163
- const scoredCandidates = [];
139
+ const WEIGHTS = {
140
+ diversification: 0.50, // Correlation reduction
141
+ quality: 0.30, // Risk/Reward
142
+ interest: 0.20 // Slight bias to things they might understand
143
+ };
144
+
145
+ const THRESHOLDS = {
146
+ minCopiers: 10,
147
+ maxRiskScore: 8,
148
+ minGain: -10,
149
+ excludeAlreadyCopied: true
150
+ };
164
151
 
165
152
  for (const piId of allPiIds) {
166
- // Skip already copied
167
- if (THRESHOLDS.excludeAlreadyCopied && copiedPiIds.has(piId)) {
168
- continue;
169
- }
153
+ if (THRESHOLDS.excludeAlreadyCopied && copiedPiIds.has(piId)) continue;
170
154
 
171
155
  const piFv = piVectors[piId];
172
-
173
- // Apply quality thresholds
174
156
  if (piFv.copiers < THRESHOLDS.minCopiers) continue;
175
157
  if (piFv.riskScore > THRESHOLDS.maxRiskScore) continue;
176
158
  if (piFv.gain < THRESHOLDS.minGain) continue;
177
159
 
178
- // Calculate scores
179
- const interestScore = this._calcInterestScore(piFv, userSectorProfile, userPreferredRiskRange);
180
- const diversificationScore = this._calcDiversificationScore(piFv, userSectorProfile);
160
+ // Metric: Portfolio Correlation
161
+ const correlation = this._calculatePortfolioCorrelation(
162
+ userNetVector,
163
+ piFv.sectors,
164
+ sectorCorrelations,
165
+ sectorList
166
+ );
167
+
168
+ // Diversification Score:
169
+ // 0.0 = Uncorrelated (Best) -> Score 1.0
170
+ // 1.0 = Highly Correlated (Worst) -> Score 0.0
171
+ // -1.0 = Inverse -> Score 0.0 (We prioritize uncorrelation over hedging here)
172
+ const diversificationScore = 1 - Math.abs(correlation);
173
+
174
+ // Interest Score (Overlap)
175
+ const interestScore = this._calcInterestScore(userNetVector, piFv.sectors);
176
+
177
+ // Quality Score
181
178
  const qualityScore = this._calcQualityScore(piFv);
182
179
 
183
180
  const totalScore =
184
- (interestScore * WEIGHTS.interest) +
185
181
  (diversificationScore * WEIGHTS.diversification) +
186
- (qualityScore * WEIGHTS.quality);
182
+ (qualityScore * WEIGHTS.quality) +
183
+ (interestScore * WEIGHTS.interest);
187
184
 
188
- scoredCandidates.push({
185
+ candidates.push({
189
186
  piId: piFv.piId,
190
187
  username: piFv.username,
191
188
  score: Number(totalScore.toFixed(4)),
192
- breakdown: {
193
- interest: Number(interestScore.toFixed(3)),
194
- diversification: Number(diversificationScore.toFixed(3)),
195
- quality: Number(qualityScore.toFixed(3))
196
- },
197
- // Include key metrics for frontend
198
189
  metrics: {
190
+ correlation: Number(correlation.toFixed(3)),
199
191
  riskScore: piFv.riskScore,
200
192
  gain: piFv.gain,
201
- copiers: piFv.copiers,
202
- winRatio: piFv.winRatio,
203
193
  topSectors: this._getTopSectors(piFv.sectors, 3)
204
194
  },
205
- reason: this._generateReason(diversificationScore, interestScore, piFv)
195
+ reason: this._generateReason(correlation, piFv)
206
196
  });
207
197
  }
208
198
 
209
199
  // ===========================================================================
210
- // STEP 4: Rank and return top N
200
+ // 5. SORT AND RETURN
211
201
  // ===========================================================================
212
- const recommendations = scoredCandidates
202
+ const recommendations = candidates
213
203
  .sort((a, b) => b.score - a.score)
214
- .slice(0, TOP_N);
204
+ .slice(0, 10);
215
205
 
216
206
  this.setResult(entityId, {
217
207
  recommendations,
218
208
  userProfile: {
219
- topSectors: this._getTopSectors(userSectorProfile, 3),
220
- preferredRiskRange: userPreferredRiskRange,
221
- currentCopies: copiedPiIds.size
209
+ topSectors: this._getTopSectors(userNetVector, 5),
210
+ totalExposure: Object.values(userNetVector).reduce((a, b) => a + b, 0)
222
211
  },
223
212
  metadata: {
224
- candidatesScored: scoredCandidates.length,
225
- totalPIs: allPiIds.length,
213
+ candidatesScored: candidates.length,
226
214
  computedAt: targetDate
227
215
  }
228
216
  });
229
217
  }
230
218
 
231
219
  // ===========================================================================
232
- // SCORING FUNCTIONS
220
+ // MATH HELPERS
233
221
  // ===========================================================================
234
222
 
235
- /**
236
- * Interest score: How well does the PI match user preferences?
237
- * Higher = more aligned with sectors/risk they seem to like
238
- */
239
- _calcInterestScore(piFv, userSectorProfile, preferredRiskRange) {
240
- // Sector overlap (dot product of normalized vectors)
241
- let sectorOverlap = 0;
242
- Object.entries(userSectorProfile).forEach(([sector, userWeight]) => {
243
- const piWeight = piFv.sectors[sector] || 0;
244
- sectorOverlap += userWeight * piWeight;
245
- });
223
+ _calculatePortfolioCorrelation(vecA, vecB, corrMatrix, sectors) {
224
+ // Portfolio Variance Proxy: wT * C * w
225
+ // Uses Correlation Matrix as the Kernel.
246
226
 
247
- // Risk preference match (1 = perfect match, 0 = way off)
248
- const riskMatch = preferredRiskRange.min !== null
249
- ? 1 - Math.min(1, Math.abs(piFv.riskScore - preferredRiskRange.avg) / 5)
250
- : 0.5; // Neutral if no preference detected
227
+ let varA = 0;
228
+ let varB = 0;
229
+ let covAB = 0;
251
230
 
252
- return (sectorOverlap * 0.6) + (riskMatch * 0.4);
253
- }
231
+ for (let i = 0; i < sectors.length; i++) {
232
+ for (let j = 0; j < sectors.length; j++) {
233
+ const s1 = sectors[i];
234
+ const s2 = sectors[j];
235
+ const corr = corrMatrix[s1]?.[s2] || 0;
254
236
 
255
- /**
256
- * Diversification score: How different is the PI from current portfolio?
257
- * Higher = more different = better for diversification
258
- */
259
- _calcDiversificationScore(piFv, userSectorProfile) {
260
- // Calculate sector distance (1 - overlap)
261
- let overlap = 0;
262
- let userMagnitude = 0;
263
- let piMagnitude = 0;
264
-
265
- Object.keys(piFv.sectors).forEach(sector => {
266
- const userWeight = userSectorProfile[sector] || 0;
267
- const piWeight = piFv.sectors[sector] || 0;
268
-
269
- overlap += userWeight * piWeight;
270
- userMagnitude += userWeight * userWeight;
271
- piMagnitude += piWeight * piWeight;
272
- });
273
-
274
- userMagnitude = Math.sqrt(userMagnitude);
275
- piMagnitude = Math.sqrt(piMagnitude);
276
-
277
- // Cosine similarity → distance
278
- const cosineSim = (userMagnitude > 0 && piMagnitude > 0)
279
- ? overlap / (userMagnitude * piMagnitude)
280
- : 0;
237
+ const wa_i = vecA[s1] || 0;
238
+ const wa_j = vecA[s2] || 0;
239
+ const wb_i = vecB[s1] || 0;
240
+ const wb_j = vecB[s2] || 0;
281
241
 
282
- const diversityScore = 1 - cosineSim;
242
+ varA += wa_i * wa_j * corr;
243
+ varB += wb_i * wb_j * corr;
244
+ covAB += wa_i * wb_j * corr;
245
+ }
246
+ }
283
247
 
284
- // Bonus for low concentration (well-diversified PI)
285
- const concentrationBonus = (1 - piFv.concentration) * 0.3;
248
+ if (varA <= 0 || varB <= 0) return 0;
249
+ return covAB / Math.sqrt(varA * varB);
250
+ }
286
251
 
287
- return Math.min(1, diversityScore + concentrationBonus);
252
+ _calcInterestScore(userVec, piVec) {
253
+ // Dot product of positive weights only (simple overlap)
254
+ let score = 0;
255
+ Object.keys(userVec).forEach(k => {
256
+ // Check if both have exposure in the same direction (Long/Long or Short/Short)
257
+ if ((userVec[k] > 0 && piVec[k] > 0) || (userVec[k] < 0 && piVec[k] < 0)) {
258
+ score += Math.abs(userVec[k] * piVec[k]);
259
+ }
260
+ });
261
+ // Normalize score roughly to 0-1 range (assuming total portfolio ~ 100%)
262
+ return Math.min(1, score / 1000);
288
263
  }
289
264
 
290
- /**
291
- * Quality score: How good is the PI objectively?
292
- * Combines performance, popularity, and track record
293
- */
294
265
  _calcQualityScore(piFv) {
295
- // Normalize gain to 0-1 scale (-50% to +100% typical range)
296
266
  const gainNorm = Math.max(0, Math.min(1, (piFv.gain + 50) / 150));
297
-
298
- // Win ratio already 0-100, normalize
299
267
  const winRatioNorm = piFv.winRatio / 100;
300
-
301
- // Copiers (log scale, cap at ~10k)
302
268
  const copiersNorm = Math.min(1, Math.log10(Math.max(1, piFv.copiers)) / 4);
303
-
304
- // Risk-adjusted (lower risk = bonus)
305
269
  const riskBonus = (10 - piFv.riskScore) / 10 * 0.2;
306
-
307
270
  return (gainNorm * 0.35) + (winRatioNorm * 0.25) + (copiersNorm * 0.2) + riskBonus;
308
271
  }
309
272
 
310
- // ===========================================================================
311
- // HELPERS
312
- // ===========================================================================
313
-
314
- _inferRiskPreference(mirrors, rules) {
315
- if (!mirrors || mirrors.length === 0) {
316
- return { min: null, max: null, avg: null };
317
- }
318
-
319
- const riskScores = mirrors
320
- .map(m => m.RiskScore || 5)
321
- .filter(r => r > 0 && r <= 10);
322
-
323
- if (riskScores.length === 0) {
324
- return { min: null, max: null, avg: null };
325
- }
326
-
327
- return {
328
- min: Math.min(...riskScores),
329
- max: Math.max(...riskScores),
330
- avg: riskScores.reduce((a, b) => a + b, 0) / riskScores.length
331
- };
332
- }
333
-
334
- _getTopSectors(sectorWeights, n) {
335
- return Object.entries(sectorWeights)
336
- .filter(([_, weight]) => weight > 0.01)
337
- .sort((a, b) => b[1] - a[1])
273
+ _getTopSectors(vec, n) {
274
+ return Object.entries(vec)
275
+ .sort((a, b) => Math.abs(b[1]) - Math.abs(a[1]))
338
276
  .slice(0, n)
339
- .map(([sector, weight]) => ({
340
- sector,
341
- weight: Number((weight * 100).toFixed(1))
342
- }));
277
+ .map(([s, w]) => ({ sector: s, weight: Number(w.toFixed(1)) }));
343
278
  }
344
279
 
345
- _generateReason(divScore, intScore, piFv) {
346
- if (divScore > 0.7) {
347
- return `Diversifies your portfolio with ${this._getTopSectors(piFv.sectors, 1)[0]?.sector || 'different'} exposure`;
348
- }
349
- if (intScore > 0.6) {
350
- return `Matches your investing style with ${piFv.gain.toFixed(0)}% returns`;
351
- }
352
- if (piFv.copiers > 1000) {
353
- return `Popular choice with ${piFv.copiers.toLocaleString()} copiers`;
354
- }
355
- return `Balanced recommendation with ${piFv.winRatio}% win ratio`;
280
+ _generateReason(correlation, piFv) {
281
+ if (Math.abs(correlation) < 0.1) return `Mathematically uncorrelated to your current portfolio`;
282
+ if (correlation < -0.3) return `Hedges your portfolio against market downturns`;
283
+ if (piFv.gain > 30) return `Strong performer with uncorrelated returns`;
284
+ return `Diversifies your holdings in ${this._getTopSectors(piFv.sectors, 1)[0].sector}`;
356
285
  }
357
286
  }
358
287
 
359
- module.exports = PiRecommender;
288
+ module.exports = PiRecommender;
@@ -14,7 +14,7 @@ class PopularInvestorProfileMetrics extends Computation {
14
14
  type: 'per-entity',
15
15
  category: 'popular_investor',
16
16
  isHistorical: true,
17
-
17
+
18
18
  requires: {
19
19
  // --- Core Data ---
20
20
  'portfolio_snapshots': {
@@ -32,7 +32,7 @@ class PopularInvestorProfileMetrics extends Computation {
32
32
  mandatory: false,
33
33
  fields: ['user_id', 'posts_data', 'date']
34
34
  },
35
-
35
+
36
36
  // --- Reference ---
37
37
  'pi_master_list': {
38
38
  lookback: 1,
@@ -76,7 +76,7 @@ class PopularInvestorProfileMetrics extends Computation {
76
76
  bigquery: true,
77
77
  firestore: {
78
78
  enabled: true,
79
- path: 'profiles/{entityId}/metrics/{date}',
79
+ path: 'PiProfiles/{entityId}/metrics/{date}',
80
80
  merge: true
81
81
  }
82
82
  },
@@ -91,7 +91,7 @@ class PopularInvestorProfileMetrics extends Computation {
91
91
  // ==========================================================================================
92
92
  // 0. PREPARATION & HELPER FUNCTIONS
93
93
  // ==========================================================================================
94
-
94
+
95
95
  const toDateStr = (d) => {
96
96
  if (!d) return "";
97
97
  if (d.value) return d.value;
@@ -137,7 +137,7 @@ class PopularInvestorProfileMetrics extends Computation {
137
137
  return dA.localeCompare(dB);
138
138
  });
139
139
  };
140
-
140
+
141
141
  // Mappings (Global, not per-entity)
142
142
  const tickerMap = new Map();
143
143
  toArray(data['ticker_mappings']).forEach(row => {
@@ -178,7 +178,7 @@ class PopularInvestorProfileMetrics extends Computation {
178
178
  // ==========================================================================================
179
179
  // 1. DATA RETRIEVAL (Using Fixed 'getEntityRows')
180
180
  // ==========================================================================================
181
-
181
+
182
182
  // These tables are keyed by 'user_id' (same as entityId) or 'pi_id' (same value)
183
183
  const portfolios = sortAsc(getEntityRows(data['portfolio_snapshots']));
184
184
  const historyData = sortAsc(getEntityRows(data['trade_history_snapshots']));
@@ -188,7 +188,7 @@ class PopularInvestorProfileMetrics extends Computation {
188
188
  const pageViews = sortAsc(getEntityRows(data['pi_page_views']));
189
189
  const watchlists = sortAsc(getEntityRows(data['watchlist_membership']));
190
190
  const alerts = sortAsc(getEntityRows(data['pi_alert_history']));
191
- const masterList = getEntityRows(data['pi_master_list']);
191
+ const masterList = getEntityRows(data['pi_master_list']);
192
192
 
193
193
  const currentPortfolio = portfolios.length > 0 ? portfolios[portfolios.length - 1] : null;
194
194
  const currentRanking = rankings.length > 0 ? rankings[rankings.length - 1] : null;
@@ -234,10 +234,10 @@ class PopularInvestorProfileMetrics extends Computation {
234
234
  trades.forEach(trade => {
235
235
  const closeDate = rules.trades.getCloseDate(trade);
236
236
  if (!closeDate) return;
237
-
237
+
238
238
  const dKey = closeDate.toISOString().split('T')[0];
239
239
  const profit = rules.trades.getNetProfit(trade);
240
-
240
+
241
241
  const entry = profitableMap.get(dKey) || { date: dKey, profitableCount: 0, totalCount: 0 };
242
242
  entry.totalCount++;
243
243
  if (profit > 0) entry.profitableCount++;
@@ -272,7 +272,7 @@ class PopularInvestorProfileMetrics extends Computation {
272
272
  if (currentPortfolio) {
273
273
  const pData = rules.portfolio.extractPortfolioData(currentPortfolio);
274
274
  const positions = rules.portfolio.extractPositions(pData);
275
-
275
+
276
276
  let totalInvested = 0, totalProfit = 0;
277
277
  const secMap = {};
278
278
  const assetMap = {};
@@ -280,12 +280,12 @@ class PopularInvestorProfileMetrics extends Computation {
280
280
 
281
281
  positions.forEach(pos => {
282
282
  const id = rules.portfolio.getInstrumentId(pos);
283
- const invested = rules.portfolio.getInvested(pos);
283
+ const invested = rules.portfolio.getInvested(pos);
284
284
  const netProfit = rules.portfolio.getNetProfit(pos);
285
-
285
+
286
286
  totalInvested += invested;
287
287
  totalProfit += (invested * (netProfit / 100));
288
-
288
+
289
289
  const ticker = resolveTicker(id);
290
290
  const sector = resolveSector(id);
291
291
 
@@ -304,7 +304,7 @@ class PopularInvestorProfileMetrics extends Computation {
304
304
  };
305
305
 
306
306
  Object.entries(assetMap)
307
- .sort(([,a], [,b]) => b - a)
307
+ .sort(([, a], [, b]) => b - a)
308
308
  .slice(0, 10)
309
309
  .forEach(([k, v]) => result.assetExposure.data[k] = Number(v.toFixed(2)));
310
310
 
@@ -331,10 +331,10 @@ class PopularInvestorProfileMetrics extends Computation {
331
331
  const pData = rules.portfolio.extractPortfolioData(entry);
332
332
  const positions = rules.portfolio.extractPositions(pData);
333
333
  const dateStr = toDateStr(entry.date);
334
-
334
+
335
335
  const dailySec = {};
336
336
  const dailyAsset = {};
337
-
337
+
338
338
  positions.forEach(pos => {
339
339
  const id = rules.portfolio.getInstrumentId(pos);
340
340
  const invested = rules.portfolio.getInvested(pos);
@@ -351,8 +351,8 @@ class PopularInvestorProfileMetrics extends Computation {
351
351
  // ==========================================================================================
352
352
  // 7. SOCIAL, RATINGS, PAGEVIEWS, WATCHLIST
353
353
  // ==========================================================================================
354
- const sevenDaysAgo = new Date(new Date(date).setDate(new Date(date).getDate() - 7)).toISOString().slice(0,10);
355
-
354
+ const sevenDaysAgo = new Date(new Date(date).setDate(new Date(date).getDate() - 7)).toISOString().slice(0, 10);
355
+
356
356
  socialData.filter(d => toDateStr(d.date) >= sevenDaysAgo).forEach(day => {
357
357
  const posts = rules.social.extractPosts(day);
358
358
  let likes = 0, comments = 0;
@@ -379,7 +379,7 @@ class PopularInvestorProfileMetrics extends Computation {
379
379
  const d = toDateStr(p.date);
380
380
  if (d) {
381
381
  result.pageViewsData.viewsOverTime.data.push({ date: d, views });
382
- result.pageViewsData.totalViews += views;
382
+ result.pageViewsData.totalViews += views;
383
383
  result.pageViewsData.uniqueViewers = Number(p.unique_viewers || 0);
384
384
  }
385
385
  });
@@ -403,7 +403,7 @@ class PopularInvestorProfileMetrics extends Computation {
403
403
  const d = toDateStr(row.date);
404
404
  const type = row.alert_type;
405
405
  const count = Number(row.trigger_count || 1);
406
-
406
+
407
407
  if (type && type.toLowerCase().includes('test')) return;
408
408
 
409
409
  result.alertMetrics.totalLast7Days += count;