bulltrackers-module 1.0.766 → 1.0.768

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.
Files changed (32) hide show
  1. package/functions/computation-system-v2/computations/BehavioralAnomaly.js +298 -186
  2. package/functions/computation-system-v2/computations/NewSectorExposure.js +82 -35
  3. package/functions/computation-system-v2/computations/NewSocialPost.js +52 -24
  4. package/functions/computation-system-v2/computations/PopularInvestorProfileMetrics.js +354 -641
  5. package/functions/computation-system-v2/config/bulltrackers.config.js +26 -14
  6. package/functions/computation-system-v2/framework/core/Manifest.js +9 -16
  7. package/functions/computation-system-v2/framework/core/RunAnalyzer.js +2 -1
  8. package/functions/computation-system-v2/framework/data/DataFetcher.js +142 -4
  9. package/functions/computation-system-v2/framework/execution/Orchestrator.js +18 -31
  10. package/functions/computation-system-v2/framework/storage/StorageManager.js +7 -17
  11. package/functions/computation-system-v2/framework/testing/ComputationTester.js +155 -66
  12. package/functions/computation-system-v2/scripts/test-computation-dag.js +109 -0
  13. package/functions/task-engine/helpers/data_storage_helpers.js +6 -6
  14. package/package.json +1 -1
  15. package/functions/computation-system-v2/computations/PopularInvestorRiskAssessment.js +0 -176
  16. package/functions/computation-system-v2/computations/PopularInvestorRiskMetrics.js +0 -294
  17. package/functions/computation-system-v2/computations/UserPortfolioSummary.js +0 -172
  18. package/functions/computation-system-v2/scripts/migrate-sectors.js +0 -73
  19. package/functions/computation-system-v2/test/analyze-results.js +0 -238
  20. package/functions/computation-system-v2/test/other/test-dependency-cascade.js +0 -150
  21. package/functions/computation-system-v2/test/other/test-dispatcher.js +0 -317
  22. package/functions/computation-system-v2/test/other/test-framework.js +0 -500
  23. package/functions/computation-system-v2/test/other/test-real-execution.js +0 -166
  24. package/functions/computation-system-v2/test/other/test-real-integration.js +0 -194
  25. package/functions/computation-system-v2/test/other/test-refactor-e2e.js +0 -131
  26. package/functions/computation-system-v2/test/other/test-results.json +0 -31
  27. package/functions/computation-system-v2/test/other/test-risk-metrics-computation.js +0 -329
  28. package/functions/computation-system-v2/test/other/test-scheduler.js +0 -204
  29. package/functions/computation-system-v2/test/other/test-storage.js +0 -449
  30. package/functions/computation-system-v2/test/run-pipeline-test.js +0 -554
  31. package/functions/computation-system-v2/test/test-full-pipeline.js +0 -227
  32. package/functions/computation-system-v2/test/test-worker-pool.js +0 -266
@@ -1,3 +1,13 @@
1
+ /**
2
+ * @fileoverview Behavioral Anomaly Detection (V2)
3
+ * Detects significant deviations in investor behavior compared to their 60-day baseline.
4
+ * * MATH: Uses Mahalanobis Distance to detect outliers in multi-dimensional space.
5
+ * FEATURES:
6
+ * 1. Concentration (HHI): Are they suddenly putting all eggs in one basket?
7
+ * 2. Martingale (Loss Chasing): Are they increasing leverage after losses?
8
+ * 3. Capacity Strain: Is their AUM growing faster than their copy count implies (risk of slippage)?
9
+ * 4. Risk Score: Sudden spikes in risk.
10
+ */
1
11
  const { Computation } = require('../framework');
2
12
 
3
13
  class BehavioralAnomaly extends Computation {
@@ -10,6 +20,7 @@ class BehavioralAnomaly extends Computation {
10
20
  isHistorical: true,
11
21
 
12
22
  requires: {
23
+ // We need 60 days of history to build a statistical baseline
13
24
  'portfolio_snapshots': {
14
25
  lookback: 60,
15
26
  mandatory: true,
@@ -36,13 +47,12 @@ class BehavioralAnomaly extends Computation {
36
47
  }
37
48
  },
38
49
 
39
- // LEGACY METADATA PRESERVED
40
50
  userType: 'POPULAR_INVESTOR',
41
51
  alert: {
42
52
  id: 'behavioralAnomaly',
43
53
  frontendName: 'Behavioral Anomaly',
44
54
  description: 'Alert when a Popular Investor deviates significantly from their baseline behavior',
45
- messageTemplate: 'Behavioral Alert for {piUsername}: {primaryDriver} Deviation ({driverSignificance}) detected.',
55
+ messageTemplate: 'Behavioral Alert for {username}: {primaryDriver} Deviation ({driverSignificance}) detected.',
46
56
  severity: 'high',
47
57
  configKey: 'behavioralAnomaly',
48
58
  isDynamic: true,
@@ -50,230 +60,332 @@ class BehavioralAnomaly extends Computation {
50
60
  {
51
61
  key: 'anomalyScoreThreshold',
52
62
  type: 'number',
53
- label: 'Anomaly Sensitivity',
54
- description: 'Alert when anomaly score exceeds this threshold (higher = less sensitive)',
63
+ label: 'Sensitivity Threshold',
55
64
  default: 3.5,
56
- min: 2.0,
57
- max: 5.0,
58
- step: 0.5,
59
- unit: 'σ (standard deviations)'
65
+ min: 2.0, max: 10.0, step: 0.1
60
66
  }
61
67
  ],
62
- conditions: [
63
- {
64
- key: 'watchedDrivers',
65
- type: 'array',
66
- label: 'Watched Behavior Drivers',
67
- description: 'Only alert for these specific behavioral factors (empty = all)',
68
- default: [],
69
- options: [
70
- { value: 'Concentration (HHI)', label: 'Portfolio Concentration' },
71
- { value: 'Martingale Behavior', label: 'Martingale Behavior' },
72
- { value: 'Capacity Strain', label: 'Capacity Strain' },
73
- { value: 'Risk Score', label: 'Risk Score Changes' }
74
- ]
75
- }
76
- ],
77
- resultKeys: ['triggered', 'anomalyScore', 'primaryDriver', 'driverSignificance', 'baselineDays']
68
+ resultFields: {
69
+ driver: 'primaryDriver',
70
+ score: 'driverSignificance'
71
+ }
78
72
  }
79
73
  };
80
74
  }
81
75
 
82
- // --- Math Helper (Self-Contained) ---
83
- static LinearAlgebra = {
84
- mean(data) {
85
- const sum = data.reduce((a, b) => a + b, 0);
86
- return sum / data.length;
87
- },
88
- covarianceMatrix(vectors) {
89
- const n = vectors.length;
90
- const dim = vectors[0].length;
91
- const means = Array(dim).fill(0).map((_, i) => this.mean(vectors.map(v => v[i])));
92
- const matrix = Array(dim).fill(0).map(() => Array(dim).fill(0));
93
-
94
- for (let i = 0; i < dim; i++) {
95
- for (let j = 0; j < dim; j++) {
96
- let sum = 0;
97
- for (let k = 0; k < n; k++) {
98
- sum += (vectors[k][i] - means[i]) * (vectors[k][j] - means[j]);
99
- }
100
- matrix[i][j] = sum / (n - 1);
101
- }
102
- }
103
- return { matrix, means };
104
- },
105
- invertMatrix(M) {
106
- // Simple 4x4 inversion or generic Gaussian elimination
107
- // For brevity, assuming 4x4 or small dimensions suitable for JS
108
- // (Implementation omitted for brevity, essentially standard Gaussian elimination)
109
- // Simplified check for diagonal/stability
110
- if(M.length === 0) return null;
111
- // Placeholder for full inversion logic - in production use a library
112
- // For this migration, we check for zero variance on diagonal
113
- for(let i=0; i<M.length; i++) if(M[i][i] === 0) return null;
114
- return M; // Mock return: In real usage, implement numeric.js or similar
115
- },
116
- mahalanobisDistance(vector, means, inverseCov) {
117
- // D^2 = (x - u)^T * S^-1 * (x - u)
118
- // Simplified Euclidean for fallback if inversion fails, or full calc
119
- // For this output, we assume full calculation logic exists here.
120
- let diff = vector.map((v, i) => v - means[i]);
121
- let sum = 0;
122
- // Mock calculation
123
- return Math.sqrt(diff.reduce((acc, val) => acc + (val * val), 0));
124
- }
125
- };
126
-
127
- calculateHHI(portfolioData) {
128
- const positions = this.rules.portfolio.extractPositions(portfolioData);
129
- if (!positions || positions.length === 0) return 0;
130
-
131
- let sumSquares = 0;
132
- const totalValue = this.rules.portfolio.calculateTotalValue(positions);
133
-
134
- positions.forEach(p => {
135
- // Calculate actual % weight based on Value
136
- const val = this.rules.portfolio.getValue(p);
137
- const weight = totalValue > 0 ? (val / totalValue) * 100 : 0;
138
- sumSquares += (weight * weight);
139
- });
140
- return sumSquares;
141
- }
76
+ async process(context) {
77
+ const { data, entityId, date, rules } = context;
142
78
 
143
- calculateMartingaleScore(historyData) {
144
- const trades = this.rules.trades.extractTrades(historyData);
145
- if (!trades || trades.length < 2) return 0;
79
+ // =====================================================================
80
+ // 1. DATA PREPARATION
81
+ // =====================================================================
146
82
 
147
- // Sort by close date
148
- const sorted = [...trades].sort((a, b) =>
149
- (this.rules.trades.getCloseDate(a) || 0) - (this.rules.trades.getCloseDate(b) || 0)
150
- );
151
- const recent = sorted.slice(-30);
152
-
153
- let lossEvents = 0;
154
- let martingaleResponses = 0;
155
-
156
- for (let i = 0; i < recent.length - 1; i++) {
157
- const current = recent[i];
158
- const next = recent[i+1];
159
-
160
- if (this.rules.trades.getNetProfit(current) < 0) {
161
- lossEvents++;
162
- const currentLev = this.rules.trades.getLeverage(current);
163
- const nextLev = this.rules.trades.getLeverage(next);
164
- if (nextLev > currentLev) martingaleResponses++;
83
+ // Helper: Safe Date String
84
+ const toDateStr = (d) => {
85
+ if (!d) return "";
86
+ if (d.value) return d.value;
87
+ return d instanceof Date ? d.toISOString().slice(0, 10) : String(d);
88
+ };
89
+
90
+ // Helper: Access Data Map or Array safely (V2 Pattern)
91
+ const getEntityRows = (dataset) => {
92
+ if (!dataset) return [];
93
+ if (dataset[entityId]) {
94
+ return Array.isArray(dataset[entityId]) ? dataset[entityId] : [dataset[entityId]];
165
95
  }
166
- }
167
- return lossEvents > 0 ? (martingaleResponses / lossEvents) : 0;
168
- }
96
+ if (Array.isArray(dataset)) {
97
+ return dataset.filter(r => String(r.pi_id || r.user_id || r.cid) === String(entityId));
98
+ }
99
+ return [];
100
+ };
169
101
 
170
- getDailyVector(portfolioData, rankingsData, historyData) {
171
- const hhi = this.calculateHHI(portfolioData);
172
- const mScore = this.calculateMartingaleScore(historyData);
173
-
174
- let strain = 0;
175
- let risk = 1;
176
-
177
- if (rankingsData) {
178
- const copiers = this.rules.rankings.getCopiers(rankingsData);
179
- const aum = this.rules.rankings.getAUM(rankingsData);
180
- strain = aum > 0 ? (copiers / (aum / 1000)) : 0;
181
- risk = this.rules.rankings.getRiskScore(rankingsData);
102
+ // Fetch Data
103
+ const portfolios = getEntityRows(data['portfolio_snapshots']);
104
+ const rankings = getEntityRows(data['pi_rankings']);
105
+ const history = getEntityRows(data['trade_history_snapshots']);
106
+
107
+ // Index by Date for fast alignment
108
+ // { "2026-01-28": row }
109
+ const portMap = new Map(portfolios.map(r => [toDateStr(r.date), r]));
110
+ const rankMap = new Map(rankings.map(r => [toDateStr(r.date), r]));
111
+ const histMap = new Map(history.map(r => [toDateStr(r.date), r]));
112
+
113
+ // Identify "Today" (Execution Date)
114
+ const todayPort = portMap.get(date);
115
+ const todayRank = rankMap.get(date);
116
+ const todayHist = histMap.get(date);
117
+
118
+ if (!todayPort || !todayRank) {
119
+ // Cannot run anomaly detection without today's data
120
+ return;
182
121
  }
183
122
 
184
- return [hhi, mScore, strain, risk];
185
- }
123
+ // =====================================================================
124
+ // 2. FEATURE ENGINEERING
125
+ // =====================================================================
126
+
127
+ /**
128
+ * 1. Concentration (HHI)
129
+ * Sum of squared weights. High HHI = Low Diversification.
130
+ */
131
+ const calculateHHI = (portRow) => {
132
+ const pData = rules.portfolio.extractPortfolioData(portRow);
133
+ const positions = rules.portfolio.extractPositions(pData);
134
+
135
+ if (!positions || positions.length === 0) return 0;
186
136
 
187
- async process(context) {
188
- const { data, entityId, rules } = context;
189
- this.rules = rules; // Attach rules for helper access
137
+ let sumSquares = 0;
138
+ let totalInvested = 0;
190
139
 
191
- const portfolios = data['portfolio_snapshots'] || [];
192
- const rankings = data['pi_rankings'] || [];
193
- const histories = data['trade_history_snapshots'] || [];
140
+ positions.forEach(p => {
141
+ // Invested is typically a %. If it's absolute $, we normalize later.
142
+ // Assuming standard eToro data where Invested is a % (0-100 or 0-1)
143
+ const val = rules.portfolio.getInvested(p) || 0;
144
+ totalInvested += val;
145
+ sumSquares += (val * val);
146
+ });
194
147
 
195
- // Sort data by date ascending for baseline building
196
- portfolios.sort((a, b) => new Date(a.date) - new Date(b.date));
197
-
198
- const trainingVectors = [];
199
- const MIN_DATAPOINTS = 15;
148
+ // Normalize if total > 0 (Standard HHI ranges 0 to 10,000)
149
+ // If weights sum to 100, HHI is sum(w^2).
150
+ return sumSquares;
151
+ };
200
152
 
201
- // Build Baseline
202
- for (const portRow of portfolios) {
203
- const date = portRow.date;
204
-
205
- // Match ranking and history for this date
206
- const rankRow = rankings.find(r => r.date === date);
207
- const histRow = histories.find(h => h.date === date);
153
+ /**
154
+ * 2. Martingale Score
155
+ * Checks if leverage increases after a loss (Loss Chasing).
156
+ */
157
+ const calculateMartingale = (histRow) => {
158
+ const trades = rules.trades.extractTrades(histRow);
159
+ if (!trades || trades.length < 2) return 0;
160
+
161
+ // Sort by close date (Oldest -> Newest)
162
+ trades.sort((a, b) => {
163
+ const dA = rules.trades.getCloseDate(a);
164
+ const dB = rules.trades.getCloseDate(b);
165
+ return (dA || 0) - (dB || 0);
166
+ });
167
+
168
+ // Look at recent behavior (last 30 trades in this snapshot)
169
+ const recent = trades.slice(-30);
170
+ let lossEvents = 0;
171
+ let martingaleResponses = 0;
172
+
173
+ for (let i = 0; i < recent.length - 1; i++) {
174
+ const current = recent[i];
175
+ const next = recent[i+1];
176
+
177
+ const profit = rules.trades.getNetProfit(current);
178
+ if (profit < 0) {
179
+ lossEvents++;
180
+ const curLev = rules.trades.getLeverage(current) || 1;
181
+ const nextLev = rules.trades.getLeverage(next) || 1;
182
+
183
+ if (nextLev > curLev) {
184
+ martingaleResponses++;
185
+ }
186
+ }
187
+ }
208
188
 
209
- const portData = rules.portfolio.extractPortfolioData(portRow);
210
- const rankData = rules.rankings.extractRankingsData(rankRow);
189
+ return lossEvents > 0 ? (martingaleResponses / lossEvents) : 0;
190
+ };
191
+
192
+ /**
193
+ * 3. Capacity Strain
194
+ * Copiers / AUM. If copiers grow but AUM doesn't, efficiency drops.
195
+ */
196
+ const calculateStrain = (rankRow) => {
197
+ const rData = rules.rankings.extractRankingsData(rankRow);
198
+ if (!rData) return 0;
199
+
200
+ const copiers = rules.rankings.getCopiers(rData) || 0;
201
+ // AUM Tier is usually an int (1-6). We need approximate value or raw AUM if available.
202
+ // Using raw AUMValue if available in JSON, else estimate from tier.
203
+ const aum = rData.AUMValue || (rules.rankings.getAUMTier(rData) * 10000) || 1;
211
204
 
212
- // Note: History logic simplified; V1 filtered full history,
213
- // here we rely on the daily snapshots which usually contain accumulated history
214
- const histData = histRow;
205
+ return (copiers / (aum / 1000)); // Normalized ratio
206
+ };
207
+
208
+ /**
209
+ * 4. Risk Score
210
+ */
211
+ const getRisk = (rankRow) => {
212
+ const rData = rules.rankings.extractRankingsData(rankRow);
213
+ return rules.rankings.getRiskScore(rData) || 1;
214
+ };
215
+
216
+ // --- Build Vectors ---
217
+ const getDailyVector = (pRow, rRow, hRow) => {
218
+ if (!pRow || !rRow) return null;
219
+ return [
220
+ calculateHHI(pRow),
221
+ hRow ? calculateMartingale(hRow) : 0,
222
+ calculateStrain(rRow),
223
+ getRisk(rRow)
224
+ ];
225
+ };
215
226
 
216
- if (portData) {
217
- const vec = this.getDailyVector(portData, rankData, histData);
218
- trainingVectors.push(vec);
227
+ // =====================================================================
228
+ // 3. BASELINE CONSTRUCTION
229
+ // =====================================================================
230
+
231
+ const trainingVectors = [];
232
+
233
+ // Loop through last 60 days (excluding today)
234
+ const sortedDates = Array.from(portMap.keys()).sort();
235
+
236
+ for (const d of sortedDates) {
237
+ if (d === date) continue; // Don't include today in the baseline training
238
+
239
+ const pRow = portMap.get(d);
240
+ const rRow = rankMap.get(d); // Matches date?
241
+ const hRow = histMap.get(d); // History available for this date?
242
+
243
+ // We need intersection of Portfolio and Ranking to form a valid vector point
244
+ if (pRow && rRow) {
245
+ const vec = getDailyVector(pRow, rRow, hRow);
246
+ if (vec) trainingVectors.push(vec);
219
247
  }
220
248
  }
221
249
 
222
- if (trainingVectors.length < MIN_DATAPOINTS) {
223
- this.setResult(entityId, { status: 'INSUFFICIENT_BASELINE', dataPoints: trainingVectors.length });
250
+ // Need minimum data to be statistically significant
251
+ if (trainingVectors.length < 15) {
252
+ this.setResult(entityId, {
253
+ triggered: false,
254
+ status: 'INSUFFICIENT_BASELINE',
255
+ dataPoints: trainingVectors.length
256
+ });
224
257
  return;
225
258
  }
226
259
 
227
- // Compute Stats
228
- const stats = BehavioralAnomaly.LinearAlgebra.covarianceMatrix(trainingVectors);
229
- // Note: Real implementation needs a robust inversion.
230
- // If variance is 0, inversion fails.
231
- const inverseCov = BehavioralAnomaly.LinearAlgebra.invertMatrix(stats.matrix);
260
+ // =====================================================================
261
+ // 4. LINEAR ALGEBRA ENGINE
262
+ // =====================================================================
263
+
264
+ const MathLib = {
265
+ // Mean of each column
266
+ mean: (vectors) => {
267
+ const dim = vectors[0].length;
268
+ const means = new Array(dim).fill(0);
269
+ for (const v of vectors) {
270
+ for (let i = 0; i < dim; i++) means[i] += v[i];
271
+ }
272
+ return means.map(x => x / vectors.length);
273
+ },
274
+
275
+ // Covariance Matrix
276
+ covariance: (vectors, means) => {
277
+ const dim = vectors[0].length;
278
+ const n = vectors.length;
279
+ const matrix = Array(dim).fill(0).map(() => Array(dim).fill(0));
280
+
281
+ for (const v of vectors) {
282
+ for (let i = 0; i < dim; i++) {
283
+ for (let j = 0; j < dim; j++) {
284
+ matrix[i][j] += (v[i] - means[i]) * (v[j] - means[j]);
285
+ }
286
+ }
287
+ }
288
+ return matrix.map(row => row.map(val => val / (n - 1))); // Sample Covariance
289
+ },
232
290
 
233
- if (!inverseCov) {
234
- this.setResult(entityId, { status: 'STABLE_STATE', info: 'Variance too low' });
235
- return;
236
- }
291
+ // Matrix Inversion (Gauss-Jordan)
292
+ invert: (M) => {
293
+ // Simplified for 4x4. Returns null if singular.
294
+ try {
295
+ const n = M.length;
296
+ const A = M.map(row => [...row]); // Clone
297
+ const I = M.map((row, i) => M.map((_, j) => (i === j ? 1 : 0))); // Identity
298
+
299
+ for (let i = 0; i < n; i++) {
300
+ let pivot = A[i][i];
301
+ if (Math.abs(pivot) < 1e-8) return null; // Singular
302
+
303
+ for (let j = 0; j < n; j++) {
304
+ A[i][j] /= pivot;
305
+ I[i][j] /= pivot;
306
+ }
307
+
308
+ for (let k = 0; k < n; k++) {
309
+ if (k !== i) {
310
+ const factor = A[k][i];
311
+ for (let j = 0; j < n; j++) {
312
+ A[k][j] -= factor * A[i][j];
313
+ I[k][j] -= factor * I[i][j];
314
+ }
315
+ }
316
+ }
317
+ }
318
+ return I;
319
+ } catch (e) { return null; }
320
+ },
237
321
 
238
- // Today's Vector (Last element in sorted array)
239
- const todayPortRow = portfolios[portfolios.length - 1];
240
- const todayRankRow = rankings.find(r => r.date === todayPortRow.date);
241
- const todayHistRow = histories.find(h => h.date === todayPortRow.date);
322
+ // Mahalanobis Distance
323
+ distance: (v, means, invCov) => {
324
+ const diff = v.map((val, i) => val - means[i]);
325
+ const dim = diff.length;
326
+ let sum = 0;
327
+
328
+ for (let i = 0; i < dim; i++) {
329
+ let temp = 0;
330
+ for (let j = 0; j < dim; j++) {
331
+ temp += diff[j] * invCov[j][i];
332
+ }
333
+ sum += temp * diff[i];
334
+ }
335
+ return Math.sqrt(Math.max(0, sum));
336
+ }
337
+ };
242
338
 
243
- const todayVector = this.getDailyVector(
244
- rules.portfolio.extractPortfolioData(todayPortRow),
245
- rules.rankings.extractRankingsData(todayRankRow),
246
- todayHistRow
247
- );
339
+ // =====================================================================
340
+ // 5. ANOMALY DETECTION
341
+ // =====================================================================
248
342
 
249
- const distance = BehavioralAnomaly.LinearAlgebra.mahalanobisDistance(todayVector, stats.means, inverseCov);
250
- const IS_ANOMALY = distance > 3.5;
343
+ const means = MathLib.mean(trainingVectors);
344
+ const covMatrix = MathLib.covariance(trainingVectors, means);
345
+ const invCov = MathLib.invert(covMatrix);
251
346
 
252
- const result = {
253
- triggered: IS_ANOMALY,
254
- anomalyScore: parseFloat(distance.toFixed(2)),
255
- baselineDays: trainingVectors.length
256
- };
347
+ if (!invCov) {
348
+ this.setResult(entityId, { triggered: false, status: 'SINGULAR_MATRIX_ERROR' });
349
+ return;
350
+ }
257
351
 
258
- if (IS_ANOMALY) {
259
- const featureNames = ['Concentration (HHI)', 'Martingale Behavior', 'Capacity Strain', 'Risk Score'];
260
- let maxZ = 0;
261
- let primaryDriver = 'Unknown';
262
-
352
+ const todayVector = getDailyVector(todayPort, todayRank, todayHist);
353
+ const distance = MathLib.distance(todayVector, means, invCov);
354
+
355
+ // Threshold from Alert Config (or default 3.5 sigma)
356
+ const threshold = 3.5;
357
+ const isAnomaly = distance > threshold;
358
+
359
+ const featureNames = ['Concentration (HHI)', 'Martingale Behavior', 'Capacity Strain', 'Risk Score'];
360
+
361
+ let primaryDriver = 'Unknown';
362
+ let maxZ = 0;
363
+
364
+ // Determine *Why* it's an anomaly (Z-Score contribution)
365
+ if (isAnomaly) {
263
366
  todayVector.forEach((val, i) => {
264
- const stdDev = Math.sqrt(stats.matrix[i][i]);
265
- const z = stdDev > 0 ? Math.abs((val - stats.means[i]) / stdDev) : 0;
367
+ const stdDev = Math.sqrt(covMatrix[i][i]);
368
+ const z = stdDev > 0 ? Math.abs((val - means[i]) / stdDev) : 0;
266
369
  if (z > maxZ) {
267
370
  maxZ = z;
268
371
  primaryDriver = featureNames[i];
269
372
  }
270
373
  });
271
-
272
- result.primaryDriver = primaryDriver;
273
- result.driverSignificance = `${maxZ.toFixed(1)}σ`;
274
- result.vectors = { current: todayVector, baselineMeans: stats.means };
275
374
  }
276
375
 
376
+ const result = {
377
+ triggered: isAnomaly,
378
+ anomalyScore: Number(distance.toFixed(2)),
379
+ primaryDriver: isAnomaly ? primaryDriver : null,
380
+ driverSignificance: isAnomaly ? `${maxZ.toFixed(1)}σ` : null,
381
+ baselineDays: trainingVectors.length,
382
+ username: rules.rankings.extractRankingsData(todayRank)?.UserName || "Unknown",
383
+ _debug: {
384
+ currentVector: todayVector,
385
+ baselineMeans: means
386
+ }
387
+ };
388
+
277
389
  this.setResult(entityId, result);
278
390
  }
279
391
  }