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.
- package/functions/computation-system-v2/computations/BehavioralAnomaly.js +298 -186
- package/functions/computation-system-v2/computations/NewSectorExposure.js +82 -35
- package/functions/computation-system-v2/computations/NewSocialPost.js +52 -24
- package/functions/computation-system-v2/computations/PopularInvestorProfileMetrics.js +354 -641
- package/functions/computation-system-v2/config/bulltrackers.config.js +26 -14
- package/functions/computation-system-v2/framework/core/Manifest.js +9 -16
- package/functions/computation-system-v2/framework/core/RunAnalyzer.js +2 -1
- package/functions/computation-system-v2/framework/data/DataFetcher.js +142 -4
- package/functions/computation-system-v2/framework/execution/Orchestrator.js +18 -31
- package/functions/computation-system-v2/framework/storage/StorageManager.js +7 -17
- package/functions/computation-system-v2/framework/testing/ComputationTester.js +155 -66
- package/functions/computation-system-v2/scripts/test-computation-dag.js +109 -0
- package/functions/task-engine/helpers/data_storage_helpers.js +6 -6
- package/package.json +1 -1
- package/functions/computation-system-v2/computations/PopularInvestorRiskAssessment.js +0 -176
- package/functions/computation-system-v2/computations/PopularInvestorRiskMetrics.js +0 -294
- package/functions/computation-system-v2/computations/UserPortfolioSummary.js +0 -172
- package/functions/computation-system-v2/scripts/migrate-sectors.js +0 -73
- package/functions/computation-system-v2/test/analyze-results.js +0 -238
- package/functions/computation-system-v2/test/other/test-dependency-cascade.js +0 -150
- package/functions/computation-system-v2/test/other/test-dispatcher.js +0 -317
- package/functions/computation-system-v2/test/other/test-framework.js +0 -500
- package/functions/computation-system-v2/test/other/test-real-execution.js +0 -166
- package/functions/computation-system-v2/test/other/test-real-integration.js +0 -194
- package/functions/computation-system-v2/test/other/test-refactor-e2e.js +0 -131
- package/functions/computation-system-v2/test/other/test-results.json +0 -31
- package/functions/computation-system-v2/test/other/test-risk-metrics-computation.js +0 -329
- package/functions/computation-system-v2/test/other/test-scheduler.js +0 -204
- package/functions/computation-system-v2/test/other/test-storage.js +0 -449
- package/functions/computation-system-v2/test/run-pipeline-test.js +0 -554
- package/functions/computation-system-v2/test/test-full-pipeline.js +0 -227
- 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 {
|
|
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: '
|
|
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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
83
|
-
|
|
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
|
-
|
|
144
|
-
|
|
145
|
-
|
|
79
|
+
// =====================================================================
|
|
80
|
+
// 1. DATA PREPARATION
|
|
81
|
+
// =====================================================================
|
|
146
82
|
|
|
147
|
-
//
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
-
|
|
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
|
-
|
|
171
|
-
const
|
|
172
|
-
const
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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
|
-
|
|
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
|
-
|
|
188
|
-
|
|
189
|
-
this.rules = rules; // Attach rules for helper access
|
|
137
|
+
let sumSquares = 0;
|
|
138
|
+
let totalInvested = 0;
|
|
190
139
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
const
|
|
207
|
-
|
|
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
|
-
|
|
210
|
-
|
|
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
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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
|
-
|
|
217
|
-
|
|
218
|
-
|
|
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
|
-
|
|
223
|
-
|
|
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
|
-
//
|
|
228
|
-
|
|
229
|
-
//
|
|
230
|
-
|
|
231
|
-
const
|
|
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
|
-
|
|
234
|
-
|
|
235
|
-
|
|
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
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
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
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
todayHistRow
|
|
247
|
-
);
|
|
339
|
+
// =====================================================================
|
|
340
|
+
// 5. ANOMALY DETECTION
|
|
341
|
+
// =====================================================================
|
|
248
342
|
|
|
249
|
-
const
|
|
250
|
-
const
|
|
343
|
+
const means = MathLib.mean(trainingVectors);
|
|
344
|
+
const covMatrix = MathLib.covariance(trainingVectors, means);
|
|
345
|
+
const invCov = MathLib.invert(covMatrix);
|
|
251
346
|
|
|
252
|
-
|
|
253
|
-
triggered:
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
};
|
|
347
|
+
if (!invCov) {
|
|
348
|
+
this.setResult(entityId, { triggered: false, status: 'SINGULAR_MATRIX_ERROR' });
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
257
351
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
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(
|
|
265
|
-
const z = stdDev > 0 ? Math.abs((val -
|
|
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
|
}
|