aiden-shared-calculations-unified 1.0.149 → 1.0.151

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 {
@@ -247,10 +224,11 @@ class BehavioralAnomaly {
247
224
  triggered: false,
248
225
  anomalyScore: parseFloat(distance.toFixed(2))
249
226
  };
227
+
250
228
  }
251
229
  }
252
-
253
230
  async getResult() { return this.results; }
231
+
254
232
  }
255
233
 
256
234
  module.exports = BehavioralAnomaly;
@@ -6,10 +6,8 @@ class AumLeaderboard {
6
6
  return {
7
7
  type: 'meta',
8
8
  category: 'analytics',
9
- // We only need the rankings root data
10
9
  rootDataDependencies: ['rankings'],
11
10
  mandatoryRoots: ['rankings'],
12
- // Daily schedule is standard for leaderboards
13
11
  schedule: { type: 'DAILY' }
14
12
  };
15
13
  }
@@ -42,7 +40,6 @@ class AumLeaderboard {
42
40
  return {
43
41
  username: entry.UserName || entry.username || "Unknown",
44
42
  cid: String(entry.CustomerId || entry.cid),
45
- // Use Extractor for safety
46
43
  aum: RankingsExtractor.getAUMValue(entry)
47
44
  };
48
45
  });
@@ -59,9 +56,12 @@ class AumLeaderboard {
59
56
  // 4. Output Global Result
60
57
  this.results = rankedResult;
61
58
 
62
- // CRITICAL FIX: MetaExecutor requires the data to be returned
59
+ // Critical: Return for MetaExecutor wrapping
63
60
  return this.results;
64
61
  }
62
+
63
+ // CRITICAL FIX: Interface required by ResultCommitter
64
+ async getResult() { return this.results; }
65
65
  }
66
66
 
67
67
  module.exports = AumLeaderboard;
@@ -46,6 +46,7 @@ class GlobalAumPerAsset30D {
46
46
  // CRITICAL FIX: MetaExecutor requires the data to be returned
47
47
  return this.results;
48
48
  }
49
+ async getResult() { return this.results; }
49
50
  }
50
51
 
51
52
  module.exports = GlobalAumPerAsset30D;
@@ -96,6 +96,11 @@ class PIDailyAssetAUM {
96
96
  averageMap: rollingAverage
97
97
  }
98
98
  };
99
+ return this.results;
100
+ }
101
+
102
+ async getResult() {
103
+ return this.results;
99
104
  }
100
105
  }
101
106
 
@@ -89,6 +89,7 @@ class PiAnomalyDetector {
89
89
  }
90
90
 
91
91
  this.results[userId] = alerts.length > 0 ? alerts : null;
92
+ return this.results;
92
93
  }
93
94
 
94
95
  async getResult() {
@@ -47,6 +47,7 @@ class PiAumOverTime {
47
47
  }
48
48
 
49
49
  this.results[userId] = Number(aum.toFixed(2));
50
+ return this.results;
50
51
  }
51
52
 
52
53
  async getResult() {
@@ -79,6 +79,7 @@ class PiCryptoBuyers {
79
79
  isCryptoBuyer,
80
80
  lastCryptoTrade
81
81
  };
82
+ return this.results;
82
83
  }
83
84
 
84
85
  async getResult() {
@@ -166,7 +166,11 @@ class RecommendedPopularInvestors {
166
166
  this.results = {
167
167
  [context.user.id]: recommendations.slice(0, 5)
168
168
  };
169
+ return this.results;
169
170
  }
171
+ // CRITICAL FIX: Interface required by ResultCommitter
172
+ async getResult() { return this.results; }
173
+
170
174
  }
171
175
 
172
176
  module.exports = RecommendedPopularInvestors;
@@ -55,9 +55,11 @@ class RiskLeaderboard {
55
55
 
56
56
  this.results = rankedResult;
57
57
 
58
- // CRITICAL FIX: MetaExecutor requires the data to be returned
59
58
  return this.results;
60
59
  }
60
+
61
+ // CRITICAL FIX: Interface required by ResultCommitter
62
+ async getResult() { return this.results; }
61
63
  }
62
64
 
63
65
  module.exports = RiskLeaderboard;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aiden-shared-calculations-unified",
3
- "version": "1.0.149",
3
+ "version": "1.0.151",
4
4
  "description": "Shared calculation modules for the BullTrackers Computation System.",
5
5
  "main": "index.js",
6
6
  "files": [