aiden-shared-calculations-unified 1.0.148 → 1.0.150

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.
@@ -3,8 +3,6 @@
3
3
  * Uses Mahalanobis Distance to detect complex deviations in investor behavior.
4
4
  * Features: Concentration (HHI), Martingale Propensity, Capacity Strain, Risk Score.
5
5
  */
6
-
7
-
8
6
  class BehavioralAnomaly {
9
7
  constructor() {
10
8
  this.results = {};
@@ -17,9 +15,8 @@ class BehavioralAnomaly {
17
15
  type: 'standard',
18
16
  isHistorical: true,
19
17
 
20
- // We request 60 days to build a strong baseline.
21
- // [UPDATED] The system now intelligently checks the Root Data Index
22
- // and only fetches dates that actually exist, saving reads automatically.
18
+ // RESTORED: Request full 60-day history for both Portfolio and Rankings.
19
+ // CachedDataLoader handles this efficiently via reference pointers.
23
20
  rootDataSeries: {
24
21
  portfolio: 60,
25
22
  rankings: 60
@@ -28,7 +25,6 @@ class BehavioralAnomaly {
28
25
  rootDataDependencies: ['portfolio', 'rankings', 'history'],
29
26
  userType: 'POPULAR_INVESTOR',
30
27
 
31
- // Allows the computation to run even if today's specific root data is missing
32
28
  canHaveMissingRoots: true,
33
29
  isAlertComputation: true,
34
30
  mandatoryRoots: ['portfolio', 'history'],
@@ -39,14 +35,21 @@ class BehavioralAnomaly {
39
35
 
40
36
  // --- Helpers ---
41
37
 
42
- /**
43
- * Helper: Calculates the Herfindahl-Hirschman Index (Concentration)
44
- * [FIX] Updated to accept 'math' context instead of 'layers'
45
- */
38
+ findClosestData(dateStr, seriesMap, maxLookback = 5) {
39
+ if (!seriesMap) return null;
40
+ if (seriesMap[dateStr]) return seriesMap[dateStr];
41
+
42
+ let current = new Date(dateStr);
43
+ for (let i = 0; i < maxLookback; i++) {
44
+ current.setUTCDate(current.getUTCDate() - 1);
45
+ const dString = current.toISOString().slice(0, 10);
46
+ if (seriesMap[dString]) return seriesMap[dString];
47
+ }
48
+ return null;
49
+ }
50
+
46
51
  calculateHHI(portfolio, math) {
47
52
  if (!portfolio) return 0;
48
-
49
- // [FIX] DataExtractor is available directly on the flattened math object
50
53
  const { DataExtractor } = math;
51
54
  const positions = DataExtractor.getPositions(portfolio, 'POPULAR_INVESTOR');
52
55
 
@@ -55,29 +58,22 @@ class BehavioralAnomaly {
55
58
  let sumSquares = 0;
56
59
  let totalValue = 0;
57
60
 
58
- positions.forEach(p => {
59
- const val = DataExtractor.getPositionValue(p);
60
- totalValue += val;
61
- });
61
+ positions.forEach(p => totalValue += DataExtractor.getPositionValue(p));
62
62
 
63
63
  if (totalValue === 0) return 0;
64
64
 
65
65
  positions.forEach(p => {
66
- const val = DataExtractor.getPositionValue(p);
67
- const weight = val / totalValue;
66
+ const weight = DataExtractor.getPositionValue(p) / totalValue;
68
67
  sumSquares += (weight * weight);
69
68
  });
70
69
 
71
70
  return sumSquares;
72
71
  }
73
72
 
74
- /**
75
- * Helper: Calculates Martingale Score (Doubling down after loss)
76
- */
77
73
  calculateMartingaleScore(tradeHistory) {
78
74
  if (!tradeHistory || tradeHistory.length < 2) return 0;
79
75
 
80
- // Sort ascending
76
+ // Trades are already filtered by date in the main loop logic
81
77
  const sorted = [...tradeHistory].sort((a, b) => new Date(a.CloseDateTime) - new Date(b.CloseDateTime));
82
78
  const recentTrades = sorted.slice(-30);
83
79
 
@@ -100,27 +96,7 @@ class BehavioralAnomaly {
100
96
  return lossEvents > 0 ? (martingaleResponses / lossEvents) : 0;
101
97
  }
102
98
 
103
- /**
104
- * "Use Closest" Logic: Looks for recent valid data point
105
- */
106
- findClosestData(dateStr, seriesMap, maxLookback = 5) {
107
- if (!seriesMap) return null;
108
- if (seriesMap[dateStr]) return seriesMap[dateStr];
109
-
110
- let current = new Date(dateStr);
111
- for (let i = 0; i < maxLookback; i++) {
112
- current.setUTCDate(current.getUTCDate() - 1);
113
- const dString = current.toISOString().slice(0, 10);
114
- if (seriesMap[dString]) return seriesMap[dString];
115
- }
116
- return null;
117
- }
118
-
119
- /**
120
- * [FIX] Updated to accept 'math' context instead of 'layers'
121
- */
122
99
  getDailyVector(portfolio, rankings, history, math) {
123
- // [FIX] RankingsExtractor is available directly on the flattened math object
124
100
  const { RankingsExtractor } = math;
125
101
 
126
102
  const hhi = this.calculateHHI(portfolio, math);
@@ -140,79 +116,79 @@ class BehavioralAnomaly {
140
116
  }
141
117
 
142
118
  process(context) {
143
- // [FIX] Use 'math' from context (ContextFactory flattens layers into 'math')
144
- // [FIX] Access 'globalData' to retrieve series
145
119
  const { math, globalData } = context;
146
120
  const { LinearAlgebra } = math;
147
121
  const userId = context.user.id;
148
122
 
149
- // [FIX] Series data is located in globalData.series
150
123
  const seriesData = globalData?.series?.root || {};
151
124
  const portfolioSeries = seriesData.portfolio || {};
152
125
  const rankingsSeries = seriesData.rankings || {};
153
126
 
154
- // The Loader now provides only existing data, so Object.keys(portfolioSeries)
155
- // is already the "clean" list of available dates.
127
+ // Full Trade History (Contains ALL time, so we must filter it)
128
+ const fullHistory = context.user.history || [];
129
+
156
130
  const availableDates = Object.keys(portfolioSeries).sort();
157
131
  const trainingVectors = [];
158
132
 
159
- // 1. Build Baseline from Available Series Data
133
+ // 1. Build Baseline
160
134
  for (const date of availableDates) {
161
135
  const datePortfolio = portfolioSeries[date][userId];
162
136
  if (!datePortfolio) continue;
163
137
 
138
+ // CRITICAL FIX: Filter History to avoid "Time Travel"
139
+ // We only look at trades that had closed ON or BEFORE this specific loop date.
140
+ const loopDateObj = new Date(date);
141
+ const historyAsOfDate = fullHistory.filter(t => new Date(t.CloseDateTime) <= loopDateObj);
142
+
143
+ // Robust Lookup: Find ranking for this date (or closest recent date)
164
144
  const rankingsSnapshot = this.findClosestData(date, rankingsSeries);
165
145
  const userRanking = rankingsSnapshot ? rankingsSnapshot.find(r => String(r.CustomerId) === String(userId)) : null;
166
146
 
167
147
  const vec = this.getDailyVector(
168
148
  datePortfolio,
169
149
  userRanking,
170
- context.user.history,
171
- math // [FIX] Pass math object
150
+ historyAsOfDate, // Pass filtered history
151
+ math
172
152
  );
173
153
  trainingVectors.push(vec);
174
154
  }
175
155
 
176
- // 2. Minimum Viable Baseline Constraint
177
- // We requested 60 days, but we accept as few as 15 (approx 3 weeks)
178
- // to avoid "2 month" delay for new users while ensuring statistical fairness.
156
+ // 2. Minimum Viable Baseline
179
157
  const MIN_DATAPOINTS = 15;
180
-
181
158
  if (trainingVectors.length < MIN_DATAPOINTS) {
182
159
  this.results[userId] = {
183
160
  status: 'INSUFFICIENT_BASELINE',
184
- dataPoints: trainingVectors.length,
185
- required: MIN_DATAPOINTS,
186
- info: 'Waiting for more history to generate fair alerts'
161
+ dataPoints: trainingVectors.length
187
162
  };
188
163
  return;
189
164
  }
190
165
 
191
- // 3. Compute Statistics
166
+ // 3. Compute Statistics (Covariance Matrix)
192
167
  const stats = LinearAlgebra.covarianceMatrix(trainingVectors);
193
168
  const inverseCov = LinearAlgebra.invertMatrix(stats.matrix);
194
169
 
195
- // Singular Matrix Check
170
+ // Singular Matrix Check (If behavior is perfectly identical every day)
196
171
  if (!inverseCov) {
197
- this.results[userId] = { status: 'STABLE_STATE', info: 'Variance too low to detect anomalies' };
172
+ this.results[userId] = { status: 'STABLE_STATE', info: 'Variance too low' };
198
173
  return;
199
174
  }
200
175
 
201
176
  // 4. Compute Today's Vector
177
+ // We use the explicit 'today' injection, or fallback to the series
202
178
  const todayRanking = context.user.rankEntry ||
203
179
  (this.findClosestData(context.date.today, rankingsSeries) || [])
204
180
  .find(r => String(r.CustomerId) === String(userId));
205
181
 
206
182
  const todayVector = this.getDailyVector(
207
- context.user.portfolio,
183
+ context.user.portfolio.today,
208
184
  todayRanking,
209
- context.user.history,
210
- math // [FIX] Pass math object
185
+ fullHistory, // Today includes all history
186
+ math
211
187
  );
212
188
 
213
189
  // 5. Calculate Distance
214
190
  const distance = LinearAlgebra.mahalanobisDistance(todayVector, stats.means, inverseCov);
215
- const IS_ANOMALY = distance > 3.5; // Slightly higher threshold for safety with smaller baselines
191
+ const IS_ANOMALY = distance > 3.5;
216
192
 
217
193
  if (IS_ANOMALY) {
218
194
  const featureNames = ['Concentration (HHI)', 'Martingale Behavior', 'Capacity Strain', 'Risk Score'];
@@ -240,6 +216,7 @@ class BehavioralAnomaly {
240
216
  }
241
217
  };
242
218
 
219
+ // Add CID to index for downstream alert handlers
243
220
  if (!this.results.cids) this.results.cids = [];
244
221
  this.results.cids.push(userId);
245
222
  } else {
@@ -249,8 +226,6 @@ class BehavioralAnomaly {
249
226
  };
250
227
  }
251
228
  }
252
-
253
- async getResult() { return this.results; }
254
229
  }
255
230
 
256
231
  module.exports = BehavioralAnomaly;
@@ -5,7 +5,7 @@ class AumLeaderboard {
5
5
  static getMetadata() {
6
6
  return {
7
7
  type: 'meta',
8
- category: 'popular-investor',
8
+ category: 'analytics',
9
9
  // We only need the rankings root data
10
10
  rootDataDependencies: ['rankings'],
11
11
  mandatoryRoots: ['rankings'],
@@ -58,9 +58,8 @@ class AumLeaderboard {
58
58
 
59
59
  // 4. Output Global Result
60
60
  this.results = rankedResult;
61
- }
62
-
63
- async getResult() {
61
+
62
+ // CRITICAL FIX: MetaExecutor requires the data to be returned
64
63
  return this.results;
65
64
  }
66
65
  }
@@ -1,42 +1,51 @@
1
- class GlobalAumAggregator {
1
+ /**
2
+ * @fileoverview Aggregates PI Asset AUM over the last 30 days.
3
+ */
4
+ class GlobalAumPerAsset30D {
2
5
  static getMetadata() {
3
6
  return {
4
7
  type: 'meta',
5
8
  category: 'analytics',
6
9
  rootDataDependencies: [],
7
- // No dependencySeries needed! We just need Today's results.
10
+ // No dependencySeries needed! We just need Today's results via getDependencies.
8
11
  schedule: { type: 'DAILY' }
9
12
  };
10
13
  }
11
14
 
12
15
  static getDependencies() {
13
16
  // Force running AFTER the Standard comp
14
- return ['PIDailyRollingAUM'];
17
+ // CRITICAL: Must match the File Name of the standard computation
18
+ return ['PIDailyAssetAUM'];
15
19
  }
16
20
 
17
21
  async process(context) {
18
- // We access the STANDARD dependencies (Today's snapshot)
19
- // Because of getDependencies(), system injected this into context.dependencies
20
- const allUserResults = context.computed['PIDailyRollingAUM'];
22
+ // Access the results injected by getDependencies()
23
+ // CRITICAL: Must match the File Name
24
+ const allUserResults = context.computed['PIDailyAssetAUM'];
21
25
 
22
26
  const globalAverages = {};
23
27
  let userCount = 0;
24
28
 
25
- Object.values(allUserResults).forEach(userOutput => {
26
- // userOutput is the object we saved above: { dailyMap, averageMap }
27
- const avgMap = userOutput.averageMap;
28
- if (!avgMap) return;
29
+ if (allUserResults) {
30
+ Object.values(allUserResults).forEach(userOutput => {
31
+ // userOutput is: { dailyMap, averageMap }
32
+ const avgMap = userOutput.averageMap;
33
+ if (!avgMap) return;
29
34
 
30
- userCount++;
31
- Object.entries(avgMap).forEach(([ticker, avgVal]) => {
32
- if (!globalAverages[ticker]) globalAverages[ticker] = 0;
33
- globalAverages[ticker] += avgVal;
35
+ userCount++;
36
+ Object.entries(avgMap).forEach(([ticker, avgVal]) => {
37
+ if (!globalAverages[ticker]) globalAverages[ticker] = 0;
38
+ globalAverages[ticker] += avgVal;
39
+ });
34
40
  });
35
- });
41
+ }
36
42
 
37
43
  // Result: The Sum of everyone's 30-Day Average Allocations
38
44
  this.results = globalAverages;
45
+
46
+ // CRITICAL FIX: MetaExecutor requires the data to be returned
47
+ return this.results;
39
48
  }
49
+ }
40
50
 
41
- async getResult() { return this.results; }
42
- }
51
+ module.exports = GlobalAumPerAsset30D;
@@ -1,26 +1,30 @@
1
- class PIDailyRollingAUM {
1
+ /**
2
+ * @fileoverview Calculates the $ AUM allocated to each asset for a single PI using a Rolling 30-Day Window.
3
+ */
4
+ class PIDailyAssetAUM {
2
5
  static getMetadata() {
3
6
  return {
4
7
  type: 'standard',
5
8
  category: 'analytics',
6
9
  userType: 'POPULAR_INVESTOR',
7
10
 
8
- // 1. Load TODAY'S raw data (Cheap)
11
+ // 1. Core Data
9
12
  rootDataDependencies: ['portfolio', 'rankings'],
10
13
  mandatoryRoots: ['portfolio', 'rankings'],
11
14
 
12
- // 2. Load MY OWN history (Last 29 days of results)
13
- // This reads small result docs, not heavy portfolios
15
+ // 2. History: Load MY OWN results from the last 29 days
16
+ // CRITICAL: Name must match the File Name (PIDailyAssetAUM)
14
17
  dependencySeries: {
15
- 'PIDailyRollingAUM': 29
16
- }
18
+ 'PIDailyAssetAUM': 29
19
+ },
20
+
21
+ // 3. Bootstrap Flag: Allows running even if history is empty (First Run)
22
+ canHaveMissingSeries: true
17
23
  };
18
24
  }
19
25
 
20
26
  static getDependencies() { return []; }
21
27
 
22
- //
23
-
24
28
  async process(context) {
25
29
  const { extract, RankingsExtractor } = context.math;
26
30
  const userId = context.user.id;
@@ -47,55 +51,52 @@ class PIDailyRollingAUM {
47
51
  }
48
52
 
49
53
  // --- STEP B: Load History (T-1 to T-29) ---
50
- // Context: context.seriesData.results['PIDailyRollingAUM'][date][userId]
51
- const historyMap = context.globalData.series?.results?.['PIDailyRollingAUM'] || {};
54
+ // Access using the FILE NAME key
55
+ const historyMap = context.globalData.series?.results?.['PIDailyAssetAUM'] || {};
52
56
 
53
- // Collect all daily maps (History + Today)
54
57
  const rollingWindow = [];
55
58
 
56
- // 1. Add History
57
- Object.values(historyMap).forEach(dayResult => {
58
- // Depending on how you stored it, extract the 'dailyMap'
59
- // In this logic, we store { dailyMap: {...}, averageMap: {...} }
60
- const userResult = dayResult[userId];
61
- if (userResult && userResult.dailyMap) {
62
- rollingWindow.push(userResult.dailyMap);
63
- }
64
- });
59
+ if (historyMap) {
60
+ Object.values(historyMap).forEach(dayResult => {
61
+ const userResult = dayResult[userId];
62
+ if (userResult && userResult.dailyMap) {
63
+ rollingWindow.push(userResult.dailyMap);
64
+ }
65
+ });
66
+ }
65
67
 
66
- // 2. Add Today
68
+ // Always add Today
67
69
  rollingWindow.push(todayAllocation);
68
70
 
69
71
  // --- STEP C: Compute Rolling Average ---
70
- const summedStats = {}; // { AAPL: { total: 5000, count: 5 } }
72
+ const summedStats = {};
71
73
 
72
74
  rollingWindow.forEach(dayAllocations => {
73
75
  Object.entries(dayAllocations).forEach(([ticker, val]) => {
74
76
  if (!summedStats[ticker]) summedStats[ticker] = { total: 0, count: 0 };
75
77
  summedStats[ticker].total += val;
76
- summedStats[ticker].count += 1; // It existed on this day
78
+ summedStats[ticker].count += 1;
77
79
  });
78
80
  });
79
81
 
80
82
  const rollingAverage = {};
81
- const validDays = rollingWindow.length; // Denominator is number of valid days captured
83
+ const validDays = rollingWindow.length;
82
84
 
83
- Object.entries(summedStats).forEach(([ticker, stat]) => {
84
- // Average = Total Value / Number of Days in the Window
85
- // Note: If you want "Average Allocation when held", divide by stat.count
86
- // If you want "Average Portfolio Weight over 30 days", divide by validDays
87
- rollingAverage[ticker] = stat.total / validDays;
88
- });
85
+ if (validDays > 0) {
86
+ Object.entries(summedStats).forEach(([ticker, stat]) => {
87
+ rollingAverage[ticker] = stat.total / validDays;
88
+ });
89
+ }
89
90
 
90
- // --- STEP D: Save Hybrid Result ---
91
- // We save Today's values (for future history) AND the pre-calculated Average (for Meta)
91
+ // --- STEP D: Output Results ---
92
+ // StandardExecutor reads 'this.results', it does NOT use the return value.
92
93
  this.results = {
93
94
  [userId]: {
94
- dailyMap: todayAllocation, // Saved for tomorrow's history lookup
95
- averageMap: rollingAverage // Saved for today's Meta aggregator
95
+ dailyMap: todayAllocation,
96
+ averageMap: rollingAverage
96
97
  }
97
98
  };
98
99
  }
100
+ }
99
101
 
100
- async getResult() { return this.results; }
101
- }
102
+ module.exports = PIDailyAssetAUM;
@@ -1,19 +1,25 @@
1
1
  /**
2
- * @fileoverview Recommends PIs to Signed-In Users based on views, holdings, and sector alignment.
2
+ * @fileoverview Recommends PIs to Signed-In Users using concrete ID matching.
3
+ * MATCHING STRATEGY:
4
+ * 1. Exclusion: Filter PIs the user already copies (ParentCID).
5
+ * 2. Interest: Page Views (30D History) using 'viewsByUser' schema.
6
+ * 3. Asset Class: Match User's holdings (InstrumentTypeID) vs PI's TopTradedAssetClassId.
7
+ * 4. Instrument: Match User's holdings (InstrumentID) vs PI's TopTradedInstrumentId.
3
8
  */
4
9
  class RecommendedPopularInvestors {
5
10
  static getMetadata() {
6
11
  return {
7
12
  type: 'standard',
8
13
  category: 'recommendations',
9
- userType: 'SIGNED_IN_USER', // Targeted audience
14
+ userType: 'SIGNED_IN_USER',
10
15
 
11
- // We need Portfolio (for mirrors/sectors) and Rankings (for PI metadata)
16
+ // We need User Portfolio (Deep) and Global Rankings (Summary)
12
17
  rootDataDependencies: ['portfolio', 'rankings'],
13
18
  mandatoryRoots: ['portfolio', 'rankings'],
14
- canHaveMissingRoots: true, // Allow running even if pageViews are incomplete
19
+ canHaveMissingRoots: true, // Allow running even if pageViews/ratings missing
15
20
 
16
21
  // 30-Day History of Page Views for Interest Graph
22
+ // System loads the global daily docs: { [PiCID]: { viewsByUser: { [UserCID]: { viewCount: N } } } }
17
23
  rootDataSeries: {
18
24
  pageViews: 30
19
25
  }
@@ -26,7 +32,7 @@ class RecommendedPopularInvestors {
26
32
  return {
27
33
  type: 'object',
28
34
  patternProperties: {
29
- '^[0-9]+$': { // Keyed by User ID
35
+ '^[0-9]+$': {
30
36
  type: 'array',
31
37
  items: {
32
38
  type: 'object',
@@ -43,90 +49,124 @@ class RecommendedPopularInvestors {
43
49
  }
44
50
 
45
51
  async process(context) {
46
- const { extract, RankingsExtractor, CopyTradingExtractor, PageViewsExtractor } = context.math;
52
+ const { RankingsExtractor, CopyTradingExtractor, PageViewsExtractor, DataExtractor } = context.math;
47
53
  const userPortfolio = context.user.portfolio.today;
48
54
  const allRankings = context.globalData.rankings || [];
49
55
  const pageViewSeries = context.globalData.series.root.pageViews || {};
50
56
 
51
- // 1. Identify Already Copied PIs (Exclusion List)
57
+ // --- STEP 1: USER PROFILE EXTRACTION ---
58
+
59
+ // A. Who do I copy? (Exclusion List)
60
+ // Schema: AggregatedMirrors -> ParentCID (e.g., 5125148)
52
61
  const mirrors = CopyTradingExtractor.getAggregatedMirrors(userPortfolio);
53
62
  const copiedCids = new Set(mirrors.map(m => String(CopyTradingExtractor.getParentCID(m))));
54
63
  copiedCids.add(String(context.user.id)); // Exclude self
55
64
 
56
- // 2. Build Interest Score from Page Views (30 Day Lookback)
57
- // We traverse the global map: Series -> Date -> PI -> ViewsByUser -> ThisUser
58
- const piInterestScore = new Map(); // CID -> Score
59
-
60
- Object.values(pageViewSeries).forEach(dailyGlobalData => {
61
- // Optimization: We must iterate PIs to find our user
62
- // In a real high-scale scenario, we might want an inverted index,
63
- // but for <5000 PIs this is acceptable O(N).
64
- const allPiCids = PageViewsExtractor.getAllPIs(dailyGlobalData);
65
-
66
- for (const piCid of allPiCids) {
67
- if (copiedCids.has(piCid)) continue;
68
-
69
- // Check if *this* user viewed *this* PI
70
- const userViews = PageViewsExtractor.getUserViewCount(dailyGlobalData, piCid, context.user.id);
71
- if (userViews > 0) {
72
- const current = piInterestScore.get(piCid) || 0;
73
- piInterestScore.set(piCid, current + (userViews * 2)); // 2 points per view
74
- }
75
- }
65
+ // B. What Asset Classes do I hold?
66
+ // Schema: AggregatedPositionsByInstrumentTypeID -> InstrumentTypeID (e.g., 5=Crypto, 4=Stocks)
67
+ const userAssetClasses = new Set();
68
+ if (userPortfolio && Array.isArray(userPortfolio.AggregatedPositionsByInstrumentTypeID)) {
69
+ userPortfolio.AggregatedPositionsByInstrumentTypeID.forEach(grp => {
70
+ if (grp.InstrumentTypeID) userAssetClasses.add(Number(grp.InstrumentTypeID));
71
+ });
72
+ }
73
+
74
+ // C. What Instruments do I hold?
75
+ // Schema: AggregatedPositions -> InstrumentID (e.g., 1353)
76
+ const userInstruments = new Set();
77
+ const positions = DataExtractor.getPositions(userPortfolio, context.user.type);
78
+ positions.forEach(pos => {
79
+ const id = DataExtractor.getInstrumentId(pos);
80
+ if (id) userInstruments.add(Number(id));
76
81
  });
77
82
 
78
- // 3. Analyze User Sectors
79
- const userSectors = new Set(extract.getCurrentSectors(userPortfolio, context.mappings, context.user.type));
83
+ // --- STEP 2: INTEREST GRAPH (Page Views) ---
84
+ // Iterate through 30 days of global page view maps
85
+ const piInterestScore = new Map();
86
+ if (pageViewSeries) {
87
+ Object.values(pageViewSeries).forEach(dailyGlobalData => {
88
+ // dailyGlobalData matches your schema: { "31075566": { viewsByUser: { ... } } }
89
+
90
+ // Extractor handles filtering 'lastUpdated' and iterating PIs
91
+ const allPiCids = PageViewsExtractor.getAllPIs(dailyGlobalData);
92
+
93
+ for (const piCid of allPiCids) {
94
+ if (copiedCids.has(piCid)) continue;
95
+
96
+ // Extractor looks inside 'viewsByUser' -> [UserCID] -> 'viewCount'
97
+ const userViews = PageViewsExtractor.getUserViewCount(dailyGlobalData, piCid, context.user.id);
98
+
99
+ if (userViews > 0) {
100
+ const current = piInterestScore.get(piCid) || 0;
101
+ // Score logic: 2 points per view
102
+ piInterestScore.set(piCid, current + (userViews * 2));
103
+ }
104
+ }
105
+ });
106
+ }
80
107
 
81
- // 4. Score Candidates
108
+ // --- STEP 3: MATCHING LOGIC ---
82
109
  const recommendations = [];
83
110
 
84
111
  for (const piEntry of allRankings) {
85
112
  const piCid = String(piEntry.CustomerId || piEntry.cid);
113
+
114
+ // 1. Exclusion
86
115
  if (copiedCids.has(piCid)) continue;
87
116
 
88
117
  let score = 0;
89
118
  const reasons = [];
90
119
 
91
- // A. Page View Interest
120
+ // 2. Interest (Page Views)
92
121
  const viewScore = piInterestScore.get(piCid) || 0;
93
122
  if (viewScore > 0) {
94
- score += Math.min(20, viewScore); // Cap at 20 points
123
+ score += Math.min(20, viewScore);
95
124
  reasons.push('Recently Viewed');
96
125
  }
97
126
 
98
- // B. Sector Match
99
- const piTags = RankingsExtractor.getTags(piEntry);
100
- const hasSectorMatch = piTags.some(tag => userSectors.has(tag));
101
- if (hasSectorMatch) {
127
+ // 3. Asset Class Match (Hard ID Match)
128
+ // Schema: Rankings -> TopTradedAssetClassId (e.g., 5)
129
+ const piAssetClass = piEntry.TopTradedAssetClassId;
130
+ if (piAssetClass && userAssetClasses.has(piAssetClass)) {
102
131
  score += 15;
103
- reasons.push('Matches Your Sectors');
132
+ reasons.push('Trades Your Asset Classes');
133
+ }
134
+
135
+ // 4. Top Instrument Match (Hard ID Match)
136
+ // Schema: Rankings -> TopTradedInstrumentId (e.g., 1155)
137
+ const piTopInst = piEntry.TopTradedInstrumentId;
138
+ if (piTopInst && userInstruments.has(piTopInst)) {
139
+ score += 10;
140
+ reasons.push('Trades Your Top Assets');
104
141
  }
105
142
 
106
- // C. Base Quality (Risk/AUM)
143
+ // 5. Risk Filter (Baseline Quality)
144
+ // Schema: Rankings -> RiskScore
107
145
  const risk = RankingsExtractor.getRiskScore(piEntry);
108
- if (risk <= 6) score += 5; // Prefer stable PIs
109
-
110
- // Only recommend if there is some signal
146
+ if (risk <= 6) {
147
+ score += 5;
148
+ reasons.push('Stable Risk Score');
149
+ }
150
+
151
+ // Push if there is a signal
111
152
  if (score > 0) {
112
153
  recommendations.push({
113
154
  cid: piCid,
114
155
  username: piEntry.UserName || "Unknown",
115
156
  matchScore: score,
116
- reason: reasons[0]
157
+ // Ensure a default reason exists
158
+ reason: reasons[0] || 'Recommended Strategy'
117
159
  });
118
160
  }
119
161
  }
120
162
 
121
- // 5. Sort and Limit
163
+ // --- STEP 4: SORT & LIMIT ---
122
164
  recommendations.sort((a, b) => b.matchScore - a.matchScore);
123
165
 
124
166
  this.results = {
125
167
  [context.user.id]: recommendations.slice(0, 5)
126
168
  };
127
169
  }
128
-
129
- async getResult() { return this.results; }
130
170
  }
131
171
 
132
172
  module.exports = RecommendedPopularInvestors;
@@ -54,9 +54,8 @@ class RiskLeaderboard {
54
54
  }));
55
55
 
56
56
  this.results = rankedResult;
57
- }
58
-
59
- async getResult() {
57
+
58
+ // CRITICAL FIX: MetaExecutor requires the data to be returned
60
59
  return this.results;
61
60
  }
62
61
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aiden-shared-calculations-unified",
3
- "version": "1.0.148",
3
+ "version": "1.0.150",
4
4
  "description": "Shared calculation modules for the BullTrackers Computation System.",
5
5
  "main": "index.js",
6
6
  "files": [
@@ -1,70 +0,0 @@
1
- /**
2
- * @fileoverview Aggregate AUM per Asset Across All Users.
3
- * Returns the AUM invested into each asset total across all users.
4
- */
5
-
6
- class AggregateAssetAUM {
7
- constructor() {
8
- this.results = {};
9
- }
10
-
11
- static getMetadata() {
12
- return {
13
- type: 'meta',
14
- category: 'popular_investor',
15
- userType: 'POPULAR_INVESTOR',
16
- // [FIX] Enforce strict availability check.
17
- // Prevents execution if PI data is missing, even if upstream calc exists.
18
- rootDataDependencies: ['portfolio']
19
- };
20
- }
21
-
22
- static getDependencies() { return ['UserAUMPerAsset']; }
23
-
24
- static getSchema() {
25
- return {
26
- type: 'object',
27
- additionalProperties: { type: 'number' },
28
- description: 'Map of instrumentId -> total AUM across all users'
29
- };
30
- }
31
-
32
- process(context) {
33
- // Input: Map of { userId: { instrumentId: aum } }
34
- const aumPerAssetData = context.computed['UserAUMPerAsset'];
35
-
36
- if (!aumPerAssetData) {
37
- return {};
38
- }
39
-
40
- // Aggregate AUM per instrument across all users
41
- const instrumentAUM = new Map();
42
-
43
- for (const [userId, userAssets] of Object.entries(aumPerAssetData)) {
44
- if (!userAssets || typeof userAssets !== 'object') continue;
45
-
46
- for (const [instrumentIdStr, aum] of Object.entries(userAssets)) {
47
- const instrumentId = Number(instrumentIdStr);
48
- // Validate AUM is a number and valid instrument ID
49
- if (isNaN(instrumentId) || typeof aum !== 'number' || aum <= 0) continue;
50
-
51
- const current = instrumentAUM.get(instrumentId) || 0;
52
- instrumentAUM.set(instrumentId, current + aum);
53
- }
54
- }
55
-
56
- // Convert Map to output Object
57
- const result = {};
58
- for (const [instrumentId, totalAUM] of instrumentAUM.entries()) {
59
- result[instrumentId] = Number(totalAUM.toFixed(2));
60
- }
61
-
62
- return result;
63
- }
64
-
65
- async getResult() {
66
- return this.results;
67
- }
68
- }
69
-
70
- module.exports = AggregateAssetAUM;
@@ -1,72 +0,0 @@
1
- /**
2
- * @fileoverview Aggregate Daily AUM Across All Users.
3
- * Returns the overall AUM values each day total across all users.
4
- */
5
-
6
- class AggregateDailyAUM {
7
- constructor() {
8
- this.results = {};
9
- }
10
-
11
- static getMetadata() {
12
- return {
13
- type: 'meta',
14
- category: 'popular_investor',
15
- userType: 'POPULAR_INVESTOR'
16
- };
17
- }
18
-
19
- static getDependencies() { return ['UserAUM30Day']; }
20
-
21
- static getSchema() {
22
- return {
23
- type: 'object',
24
- additionalProperties: { type: 'number' },
25
- description: 'Map of date -> total AUM across all users'
26
- };
27
- }
28
-
29
- process(context) {
30
- const aumData = context.computed['UserAUM30Day'];
31
-
32
- // [FIX] Defensive check
33
- if (!aumData) {
34
- return {};
35
- }
36
-
37
- // Aggregate AUM by date
38
- const dateMap = {};
39
-
40
- for (const [userId, userHistory] of Object.entries(aumData)) {
41
- // [FIX] Validate userHistory exists
42
- if (!userHistory) continue;
43
-
44
- // [FIX] "30Day" data is an Array of daily entries.
45
- // We must iterate the array. We also handle the edge case where it might be a single object.
46
- const entries = Array.isArray(userHistory) ? userHistory : [userHistory];
47
-
48
- for (const entry of entries) {
49
- if (entry && typeof entry === 'object' && entry.date && typeof entry.aum === 'number') {
50
- const date = entry.date;
51
- // Sum the AUM for this specific date
52
- dateMap[date] = (dateMap[date] || 0) + entry.aum;
53
- }
54
- }
55
- }
56
-
57
- // Round values to 2 decimals
58
- const rounded = {};
59
- for (const [date, total] of Object.entries(dateMap)) {
60
- rounded[date] = Number(total.toFixed(2));
61
- }
62
-
63
- // [FIX] Must RETURN the result for MetaExecutor
64
- return rounded;
65
- }
66
-
67
- async getResult() {
68
- return this.results;
69
- }
70
- }
71
-
72
- module.exports = AggregateDailyAUM;
@@ -1,67 +0,0 @@
1
- /**
2
- * @fileoverview AUM Value Over Last 30 Days for Users.
3
- * Returns the AUM value for the current day.
4
- * (Historical aggregation handled by Database/API or 'isHistorical' mechanisms in Meta scripts).
5
- */
6
-
7
- class UserAUM30Day {
8
- constructor() {
9
- this.results = {};
10
- }
11
-
12
- static getMetadata() {
13
- return {
14
- type: 'standard',
15
- category: 'popular_investor',
16
- rootDataDependencies: ['portfolio', 'rankings'],
17
- userType: 'POPULAR_INVESTOR'
18
- };
19
- }
20
-
21
- static getDependencies() { return []; }
22
-
23
- static getSchema() {
24
- return {
25
- type: 'object',
26
- properties: {
27
- date: { type: 'string', format: 'date' },
28
- aum: { type: 'number' }
29
- }
30
- };
31
- }
32
-
33
- process(context) {
34
- const { RankingsExtractor, PopularInvestorExtractor } = context.math;
35
- const userId = context.user.id;
36
- const portfolio = context.user.portfolio.today;
37
- const rankEntry = context.user.rankEntry;
38
- const dateStr = context.date.today;
39
-
40
- let aum = 0;
41
-
42
- // 1. Priority: Official Rankings Data
43
- if (rankEntry) {
44
- aum = RankingsExtractor.getAUMValue(rankEntry) || 0;
45
- }
46
-
47
- // 2. Fallback: Portfolio Snapshot Data
48
- if ((!aum || aum === 0) && portfolio) {
49
- if (typeof portfolio.AUMValue === 'number') {
50
- aum = portfolio.AUMValue;
51
- } else {
52
- aum = PopularInvestorExtractor.getEquity(portfolio);
53
- }
54
- }
55
-
56
- this.results[userId] = {
57
- date: dateStr,
58
- aum: Number(aum.toFixed(2))
59
- };
60
- }
61
-
62
- async getResult() {
63
- return this.results;
64
- }
65
- }
66
-
67
- module.exports = UserAUM30Day;
@@ -1,67 +0,0 @@
1
- /**
2
- * @fileoverview AUM per Asset for Users.
3
- * Returns the AUM invested into each asset per user.
4
- */
5
-
6
- class UserAUMPerAsset {
7
- constructor() {
8
- this.results = {};
9
- }
10
-
11
- static getMetadata() {
12
- return {
13
- type: 'standard',
14
- category: 'popular_investor',
15
- rootDataDependencies: ['portfolio'],
16
- userType: 'POPULAR_INVESTOR'
17
- };
18
- }
19
-
20
- static getDependencies() { return []; }
21
-
22
- static getSchema() {
23
- return {
24
- type: 'object',
25
- additionalProperties: { type: 'number' },
26
- description: 'Map of instrumentId -> AUM value invested'
27
- };
28
- }
29
-
30
- process(context) {
31
- const { DataExtractor } = context.math;
32
- const userId = context.user.id;
33
- const portfolio = context.user.portfolio.today;
34
-
35
- if (!portfolio) {
36
- this.results[userId] = {};
37
- return;
38
- }
39
-
40
- // [FIX] Inline aggregation logic
41
- const positions = DataExtractor.getPositions(portfolio, 'POPULAR_INVESTOR');
42
- const aumMap = {};
43
-
44
- for (const pos of positions) {
45
- const instId = DataExtractor.getInstrumentId(pos);
46
- const value = DataExtractor.getPositionValue(pos); // Value in USD
47
-
48
- if (instId && value > 0) {
49
- // Ensure key is numeric or string consistent with consumption
50
- aumMap[instId] = (aumMap[instId] || 0) + value;
51
- }
52
- }
53
-
54
- // Round values
55
- for (const key in aumMap) {
56
- aumMap[key] = Number(aumMap[key].toFixed(2));
57
- }
58
-
59
- this.results[userId] = aumMap;
60
- }
61
-
62
- async getResult() {
63
- return this.results;
64
- }
65
- }
66
-
67
- module.exports = UserAUMPerAsset;