bulltrackers-module 1.0.766 → 1.0.769

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 (76) hide show
  1. package/functions/computation-system-v2/UserPortfolioMetrics.js +50 -0
  2. package/functions/computation-system-v2/computations/BehavioralAnomaly.js +559 -227
  3. package/functions/computation-system-v2/computations/GlobalAumPerAsset30D.js +103 -0
  4. package/functions/computation-system-v2/computations/NewSectorExposure.js +82 -35
  5. package/functions/computation-system-v2/computations/NewSocialPost.js +52 -24
  6. package/functions/computation-system-v2/computations/PIDailyAssetAUM.js +134 -0
  7. package/functions/computation-system-v2/computations/PiFeatureVectors.js +227 -0
  8. package/functions/computation-system-v2/computations/PiRecommender.js +359 -0
  9. package/functions/computation-system-v2/computations/PopularInvestorProfileMetrics.js +354 -641
  10. package/functions/computation-system-v2/computations/SignedInUserList.js +51 -0
  11. package/functions/computation-system-v2/computations/SignedInUserMirrorHistory.js +138 -0
  12. package/functions/computation-system-v2/computations/SignedInUserPIProfileMetrics.js +106 -0
  13. package/functions/computation-system-v2/computations/SignedInUserProfileMetrics.js +324 -0
  14. package/functions/computation-system-v2/config/bulltrackers.config.js +40 -126
  15. package/functions/computation-system-v2/core-api.js +17 -9
  16. package/functions/computation-system-v2/data_schema_reference.MD +108 -0
  17. package/functions/computation-system-v2/devtools/builder/builder.js +362 -0
  18. package/functions/computation-system-v2/devtools/builder/examples/user-metrics.yaml +26 -0
  19. package/functions/computation-system-v2/devtools/index.js +36 -0
  20. package/functions/computation-system-v2/devtools/shared/MockDataFactory.js +235 -0
  21. package/functions/computation-system-v2/devtools/shared/SchemaTemplates.js +475 -0
  22. package/functions/computation-system-v2/devtools/shared/SystemIntrospector.js +517 -0
  23. package/functions/computation-system-v2/devtools/shared/index.js +16 -0
  24. package/functions/computation-system-v2/devtools/simulation/DAGAnalyzer.js +243 -0
  25. package/functions/computation-system-v2/devtools/simulation/MockDataFetcher.js +306 -0
  26. package/functions/computation-system-v2/devtools/simulation/MockStorageManager.js +336 -0
  27. package/functions/computation-system-v2/devtools/simulation/SimulationEngine.js +525 -0
  28. package/functions/computation-system-v2/devtools/simulation/SimulationServer.js +581 -0
  29. package/functions/computation-system-v2/devtools/simulation/index.js +17 -0
  30. package/functions/computation-system-v2/devtools/simulation/simulate.js +324 -0
  31. package/functions/computation-system-v2/devtools/vscode-computation/package.json +90 -0
  32. package/functions/computation-system-v2/devtools/vscode-computation/snippets/computation.json +128 -0
  33. package/functions/computation-system-v2/devtools/vscode-computation/src/extension.ts +401 -0
  34. package/functions/computation-system-v2/devtools/vscode-computation/src/providers/codeActions.ts +152 -0
  35. package/functions/computation-system-v2/devtools/vscode-computation/src/providers/completions.ts +207 -0
  36. package/functions/computation-system-v2/devtools/vscode-computation/src/providers/diagnostics.ts +205 -0
  37. package/functions/computation-system-v2/devtools/vscode-computation/src/providers/hover.ts +205 -0
  38. package/functions/computation-system-v2/devtools/vscode-computation/tsconfig.json +22 -0
  39. package/functions/computation-system-v2/docs/HowToCreateComputations.MD +602 -0
  40. package/functions/computation-system-v2/framework/core/Manifest.js +9 -16
  41. package/functions/computation-system-v2/framework/core/RunAnalyzer.js +2 -1
  42. package/functions/computation-system-v2/framework/data/DataFetcher.js +330 -126
  43. package/functions/computation-system-v2/framework/data/MaterializedViewManager.js +84 -0
  44. package/functions/computation-system-v2/framework/data/QueryBuilder.js +38 -38
  45. package/functions/computation-system-v2/framework/execution/Orchestrator.js +226 -153
  46. package/functions/computation-system-v2/framework/scheduling/ScheduleValidator.js +17 -19
  47. package/functions/computation-system-v2/framework/storage/StateRepository.js +32 -2
  48. package/functions/computation-system-v2/framework/storage/StorageManager.js +111 -83
  49. package/functions/computation-system-v2/framework/testing/ComputationTester.js +161 -66
  50. package/functions/computation-system-v2/handlers/dispatcher.js +57 -29
  51. package/functions/computation-system-v2/legacy/PiAssetRecommender.js.old +115 -0
  52. package/functions/computation-system-v2/legacy/PiSimilarityMatrix.js +104 -0
  53. package/functions/computation-system-v2/legacy/PiSimilarityVector.js +71 -0
  54. package/functions/computation-system-v2/scripts/debug_aggregation.js +25 -0
  55. package/functions/computation-system-v2/scripts/test-computation-dag.js +109 -0
  56. package/functions/computation-system-v2/scripts/test-invalidation-scenarios.js +234 -0
  57. package/functions/task-engine/helpers/data_storage_helpers.js +6 -6
  58. package/package.json +1 -1
  59. package/functions/computation-system-v2/computations/PopularInvestorRiskAssessment.js +0 -176
  60. package/functions/computation-system-v2/computations/PopularInvestorRiskMetrics.js +0 -294
  61. package/functions/computation-system-v2/computations/UserPortfolioSummary.js +0 -172
  62. package/functions/computation-system-v2/scripts/migrate-sectors.js +0 -73
  63. package/functions/computation-system-v2/test/analyze-results.js +0 -238
  64. package/functions/computation-system-v2/test/other/test-dependency-cascade.js +0 -150
  65. package/functions/computation-system-v2/test/other/test-dispatcher.js +0 -317
  66. package/functions/computation-system-v2/test/other/test-framework.js +0 -500
  67. package/functions/computation-system-v2/test/other/test-real-execution.js +0 -166
  68. package/functions/computation-system-v2/test/other/test-real-integration.js +0 -194
  69. package/functions/computation-system-v2/test/other/test-refactor-e2e.js +0 -131
  70. package/functions/computation-system-v2/test/other/test-results.json +0 -31
  71. package/functions/computation-system-v2/test/other/test-risk-metrics-computation.js +0 -329
  72. package/functions/computation-system-v2/test/other/test-scheduler.js +0 -204
  73. package/functions/computation-system-v2/test/other/test-storage.js +0 -449
  74. package/functions/computation-system-v2/test/run-pipeline-test.js +0 -554
  75. package/functions/computation-system-v2/test/test-full-pipeline.js +0 -227
  76. package/functions/computation-system-v2/test/test-worker-pool.js +0 -266
@@ -1,5 +1,455 @@
1
+ /**
2
+ * @fileoverview Behavioral Anomaly Detection V4.1 - Predictive Risk Intelligence
3
+ * * ARCHITECTURE:
4
+ * - Hybrid Math: Uses 6-Dim Core for Stability + 18-Dim Full Spectrum for Detail.
5
+ * - Cost Control: Strict 30-day lookback enforced (compatible with DAG constraints).
6
+ * - Logic Fix: "Primary Driver" uses Statistical Z-Scores, not raw values.
7
+ * - Robustness: Dynamic Z-Scoring removes dependency on hardcoded divisors.
8
+ */
9
+
1
10
  const { Computation } = require('../framework');
2
11
 
12
+ // =============================================================================
13
+ // ADVANCED MATH KERNEL
14
+ // =============================================================================
15
+ const AdvancedMath = {
16
+ entropy: (distribution) => {
17
+ const total = distribution.reduce((sum, val) => sum + val, 0);
18
+ if (total === 0) return 0;
19
+ let entropy = 0;
20
+ for (const val of distribution) {
21
+ if (val > 0) {
22
+ const p = val / total;
23
+ entropy -= p * Math.log2(p);
24
+ }
25
+ }
26
+ return entropy;
27
+ },
28
+
29
+ hhi: (shares) => {
30
+ const total = shares.reduce((sum, val) => sum + val, 0);
31
+ if (total === 0) return 0;
32
+ let hhi = 0;
33
+ for (const share of shares) {
34
+ const marketShare = share / total;
35
+ hhi += marketShare * marketShare;
36
+ }
37
+ return hhi * 10000;
38
+ },
39
+
40
+ velocity: (current, previous, timeDelta = 1) => {
41
+ if (timeDelta === 0) return 0;
42
+ return (current - previous) / timeDelta;
43
+ },
44
+
45
+ acceleration: (current, previous, previousPrevious, timeDelta = 1) => {
46
+ const v1 = AdvancedMath.velocity(current, previous, timeDelta);
47
+ const v0 = AdvancedMath.velocity(previous, previousPrevious, timeDelta);
48
+ return AdvancedMath.velocity(v1, v0, timeDelta);
49
+ },
50
+
51
+ percentileRank: (value, array) => {
52
+ if (!array.length) return 0.5;
53
+ let rank = 0;
54
+ for (const val of array) {
55
+ if (val < value) rank++;
56
+ }
57
+ return rank / array.length;
58
+ },
59
+
60
+ detectRegime: (recentVectors) => {
61
+ if (recentVectors.length < 3) return 'unknown';
62
+ // Calculate aggregate volatility across the CORE features (indices 0-5)
63
+ const coreIndices = [0, 1, 2, 3, 4, 5];
64
+ const volatilities = [];
65
+
66
+ for (const f of coreIndices) {
67
+ const values = recentVectors.map(v => v[f]);
68
+ const mean = values.reduce((sum, v) => sum + v, 0) / values.length;
69
+ const variance = values.reduce((sum, v) => sum + Math.pow(v - mean, 2), 0) / values.length;
70
+ volatilities.push(Math.sqrt(variance));
71
+ }
72
+ const avgVolatility = volatilities.reduce((sum, v) => sum + v, 0) / volatilities.length;
73
+
74
+ if (avgVolatility < 0.3) return 'calm';
75
+ if (avgVolatility < 0.8) return 'active';
76
+ return 'stressed';
77
+ }
78
+ };
79
+
80
+ // =============================================================================
81
+ // FEATURE ENGINEERING
82
+ // =============================================================================
83
+ const FeatureExtractor = {
84
+ extract: (day, prevDay, prevPrevDay, historyBlob, rankings, rules, maps) => {
85
+ const { portfolio } = day;
86
+ const positions = rules.portfolio.extractPositions(portfolio) || [];
87
+ const rankingsData = day.rankings || {};
88
+ const { sectorMap, tickerMap } = maps;
89
+
90
+ // Resolve Sector
91
+ const getSector = (instId) => {
92
+ const ticker = tickerMap.get(String(instId));
93
+ if (!ticker) return 'Unknown';
94
+ return sectorMap.get(ticker) || 'Unknown';
95
+ };
96
+
97
+ // 1. Sector Concentration (HHI)
98
+ const sectorExposure = {};
99
+ let totalInvestedPct = 0;
100
+ positions.forEach(p => {
101
+ const sector = getSector(p.InstrumentID);
102
+ const val = p.Invested || 0;
103
+ sectorExposure[sector] = (sectorExposure[sector] || 0) + val;
104
+ totalInvestedPct += val;
105
+ });
106
+ const sectorHHI = AdvancedMath.hhi(Object.values(sectorExposure));
107
+
108
+ // 2. Martingale/Distress Score
109
+ let martingaleScore = 0;
110
+ let martingaleCount = 0;
111
+ positions.forEach(p => {
112
+ const pnl = p.NetProfit || 0;
113
+ const invested = p.Invested || 0;
114
+ if (pnl < -4 && invested > 4) {
115
+ martingaleScore += Math.abs(pnl) * invested;
116
+ martingaleCount++;
117
+ }
118
+ });
119
+
120
+ // 3. Leverage Profile (Lazy Loaded)
121
+ const { avgLeverage, highLevCount } = FeatureExtractor._extractLeverageProfile(day.date, historyBlob);
122
+
123
+ // 4. Risk Score
124
+ const riskScore = rankingsData.RiskScore || 1;
125
+
126
+ // 5. Complexity
127
+ const complexity = positions.length;
128
+
129
+ // 6. Exposure
130
+ const exposure = totalInvestedPct;
131
+
132
+ // --- EXTENDED FEATURES (Full Spectrum) ---
133
+
134
+ // 7. Entropy
135
+ const positionSizes = positions.map(p => p.Invested || 0);
136
+ const portfolioEntropy = AdvancedMath.entropy(positionSizes);
137
+
138
+ // 8. Drawdown
139
+ const drawdownSeverity = Math.abs(rankingsData.PeakToValley || 0);
140
+
141
+ // 9. Win Rate Deviation
142
+ const winRateDeviation = Math.abs((rankingsData.WinRatio || 50) - 50);
143
+
144
+ // 10. Skewness
145
+ const sortedSizes = [...positionSizes].sort((a, b) => b - a);
146
+ const top3Share = sortedSizes.slice(0, 3).reduce((sum, v) => sum + v, 0);
147
+ const positionSkewness = totalInvestedPct > 0 ? top3Share / totalInvestedPct : 0;
148
+
149
+ // 11. Stress Ratio
150
+ const losingCount = positions.filter(p => (p.NetProfit || 0) < 0).length;
151
+ const stressRatio = positions.length > 0 ? losingCount / positions.length : 0;
152
+
153
+ // 12. Credit Imbalance
154
+ const realizedCredit = portfolio?.CreditByRealizedEquity || 0;
155
+ const unrealizedCredit = portfolio?.CreditByUnrealizedEquity || 0;
156
+ const creditImbalance = Math.abs(realizedCredit - unrealizedCredit);
157
+
158
+ // 13. High Lev Freq
159
+ const highLevFrequency = highLevCount / Math.max(1, positions.length);
160
+
161
+ // 14. Copier Momentum
162
+ const copiers = rankingsData.Copiers || 0;
163
+ const baselineCopiers = rankingsData.BaseLineCopiers || copiers;
164
+ const copierMomentum = baselineCopiers > 0 ? (copiers - baselineCopiers) / baselineCopiers : 0;
165
+
166
+ // 15. AUM Tier
167
+ const aumTier = (rankingsData.AUMTier || 0);
168
+
169
+ // 16. Exposure Velocity
170
+ let exposureVelocity = 0;
171
+ if (prevDay && prevDay.portfolio) {
172
+ const prevPositions = rules.portfolio.extractPositions(prevDay.portfolio) || [];
173
+ const prevExposure = prevPositions.reduce((sum, p) => sum + (p.Invested || 0), 0);
174
+ exposureVelocity = exposure - prevExposure;
175
+ }
176
+
177
+ // 17. Risk Acceleration
178
+ let riskAcceleration = 0;
179
+ if (prevDay && prevDay.rankings && prevPrevDay && prevPrevDay.rankings) {
180
+ const r0 = prevPrevDay.rankings.RiskScore || 0;
181
+ const r1 = prevDay.rankings.RiskScore || 0;
182
+ const r2 = riskScore;
183
+ riskAcceleration = AdvancedMath.acceleration(r2, r1, r0, 1);
184
+ }
185
+
186
+ // 18. Behavioral Momentum
187
+ const behavioralMomentum = Math.abs(exposureVelocity) + Math.abs(riskAcceleration);
188
+
189
+ return {
190
+ vector: [
191
+ // CORE (0-5)
192
+ sectorHHI,
193
+ Math.log(martingaleScore + 1),
194
+ avgLeverage,
195
+ riskScore,
196
+ complexity,
197
+ exposure,
198
+
199
+ // EXTENDED (6-17)
200
+ portfolioEntropy,
201
+ drawdownSeverity,
202
+ winRateDeviation,
203
+ positionSkewness,
204
+ stressRatio,
205
+ creditImbalance,
206
+ highLevFrequency,
207
+ copierMomentum,
208
+ aumTier,
209
+ exposureVelocity,
210
+ riskAcceleration,
211
+ behavioralMomentum
212
+ ],
213
+ metadata: {
214
+ martingaleCount,
215
+ losingCount,
216
+ totalPositions: positions.length,
217
+ exposure,
218
+ regime: 'unknown'
219
+ }
220
+ };
221
+ },
222
+
223
+ _extractLeverageProfile: (dateStr, historyBlob) => {
224
+ const targetDate = new Date(dateStr);
225
+ const lookbackWindow = new Date(targetDate);
226
+ lookbackWindow.setDate(lookbackWindow.getDate() - 90);
227
+
228
+ let sumLev = 0;
229
+ let tradeCount = 0;
230
+ let highLevCount = 0;
231
+
232
+ for (const t of historyBlob) {
233
+ const closeDate = new Date(t.CloseDateTime);
234
+ if (closeDate <= targetDate && closeDate >= lookbackWindow) {
235
+ const lev = t.Leverage || 1;
236
+ sumLev += lev;
237
+ tradeCount++;
238
+ if (lev > 2) highLevCount++;
239
+ }
240
+ }
241
+
242
+ return {
243
+ avgLeverage: tradeCount > 0 ? sumLev / tradeCount : 1,
244
+ highLevCount
245
+ };
246
+ }
247
+ };
248
+
249
+ // =============================================================================
250
+ // ANOMALY DETECTION ENGINE
251
+ // =============================================================================
252
+ const AnomalyEngine = {
253
+ score: (todayFeatures, historicalFeatures, regime) => {
254
+ // 1. DYNAMIC STANDARDIZATION (Z-Scoring)
255
+ // We capture zVector here because it represents the "True Statistical Deviation"
256
+ const { zVector, histZVectors } = AnomalyEngine._standardize(todayFeatures.vector, historicalFeatures);
257
+
258
+ // 2. HYBRID MAHALANOBIS (Core Features Only)
259
+ // Slicing indices 0-5 (Core) for structural stability on 30-day lookback
260
+ const coreZVector = zVector.slice(0, 6);
261
+ const coreHistZVectors = histZVectors.map(v => v.slice(0, 6));
262
+ const mahalanobisScore = AnomalyEngine._mahalanobisDistance(coreZVector, coreHistZVectors);
263
+
264
+ // 3. FULL SPECTRUM PERCENTILE (All 18 Features)
265
+ const percentileScore = AnomalyEngine._percentileScore(todayFeatures.vector, historicalFeatures);
266
+
267
+ // 4. REGIME CONDITIONAL
268
+ const regimeScore = AnomalyEngine._regimeScore(todayFeatures, historicalFeatures, regime);
269
+
270
+ // 5. VELOCITY
271
+ const velocityScore = (Math.abs(zVector[15]) + Math.abs(zVector[16]) + Math.abs(zVector[17])) / 3;
272
+
273
+ const ensembleScore =
274
+ 0.40 * mahalanobisScore +
275
+ 0.30 * percentileScore +
276
+ 0.15 * regimeScore +
277
+ 0.15 * velocityScore;
278
+
279
+ return {
280
+ overall: ensembleScore,
281
+ components: {
282
+ mahalanobis: mahalanobisScore,
283
+ percentile: percentileScore,
284
+ regime: regimeScore,
285
+ velocity: velocityScore
286
+ },
287
+ // EXPORT Z-SCORES FOR INTERPRETER
288
+ zScores: zVector
289
+ };
290
+ },
291
+
292
+ _standardize: (todayVec, history) => {
293
+ const dim = todayVec.length;
294
+ const n = history.length;
295
+ const means = new Array(dim).fill(0);
296
+ const stdDevs = new Array(dim).fill(0);
297
+
298
+ const vectors = history.map(h => h.vector);
299
+ for (const v of vectors) for (let i = 0; i < dim; i++) means[i] += v[i];
300
+ for (let i = 0; i < dim; i++) means[i] /= n;
301
+
302
+ for (const v of vectors) for (let i = 0; i < dim; i++) stdDevs[i] += Math.pow(v[i] - means[i], 2);
303
+ for (let i = 0; i < dim; i++) {
304
+ stdDevs[i] = Math.sqrt(stdDevs[i] / n);
305
+ if (stdDevs[i] === 0) stdDevs[i] = 1;
306
+ }
307
+
308
+ const zVector = todayVec.map((v, i) => (v - means[i]) / stdDevs[i]);
309
+ const histZVectors = vectors.map(v => v.map((val, i) => (val - means[i]) / stdDevs[i]));
310
+
311
+ return { zVector, histZVectors };
312
+ },
313
+
314
+ _mahalanobisDistance: (todayZVec, histZVectors) => {
315
+ const means = new Array(todayZVec.length).fill(0);
316
+ const cov = AnomalyEngine._covariance(histZVectors, means);
317
+ const invCov = AnomalyEngine._invert(cov);
318
+
319
+ if (!invCov) return 0;
320
+
321
+ let sum = 0;
322
+ for (let i = 0; i < todayZVec.length; i++) {
323
+ for (let j = 0; j < todayZVec.length; j++) {
324
+ sum += todayZVec[i] * invCov[i][j] * todayZVec[j];
325
+ }
326
+ }
327
+ return Math.sqrt(Math.max(0, sum));
328
+ },
329
+
330
+ _percentileScore: (todayVec, historicalFeatures) => {
331
+ let totalDeviation = 0;
332
+ const featureCount = todayVec.length;
333
+ for (let i = 0; i < featureCount; i++) {
334
+ const historicalValues = historicalFeatures.map(f => f.vector[i]);
335
+ const percentile = AdvancedMath.percentileRank(todayVec[i], historicalValues);
336
+ totalDeviation += Math.abs(percentile - 0.5) * 2;
337
+ }
338
+ return totalDeviation / featureCount;
339
+ },
340
+
341
+ _regimeScore: (todayFeatures, historicalFeatures, regime) => {
342
+ const sameRegimeFeatures = historicalFeatures.filter(f => f.metadata.regime === regime);
343
+ const comparisonSet = sameRegimeFeatures.length < 5 ? historicalFeatures : sameRegimeFeatures;
344
+ return AnomalyEngine._percentileScore(todayFeatures.vector, comparisonSet);
345
+ },
346
+
347
+ _covariance: (vectors, means) => {
348
+ const dim = vectors[0].length;
349
+ const n = vectors.length;
350
+ const matrix = Array(dim).fill(0).map(() => Array(dim).fill(0));
351
+ for (const v of vectors) {
352
+ for (let i = 0; i < dim; i++) {
353
+ for (let j = 0; j < dim; j++) {
354
+ matrix[i][j] += (v[i] - means[i]) * (v[j] - means[j]);
355
+ }
356
+ }
357
+ }
358
+ // Ridge Regularization
359
+ return matrix.map((row, i) => row.map((val, j) => (val / (n - 1)) + (i === j ? 1e-3 : 0)));
360
+ },
361
+
362
+ _invert: (M) => {
363
+ try {
364
+ const n = M.length;
365
+ const A = M.map(row => [...row]);
366
+ const I = M.map((_, i) => M.map((__, j) => (i === j ? 1 : 0)));
367
+ for (let i = 0; i < n; i++) {
368
+ let pivot = A[i][i];
369
+ if (Math.abs(pivot) < 1e-9) pivot = 1e-9;
370
+ for (let j = 0; j < n; j++) { A[i][j] /= pivot; I[i][j] /= pivot; }
371
+ for (let k = 0; k < n; k++) {
372
+ if (k !== i) {
373
+ const f = A[k][i];
374
+ for (let j = 0; j < n; j++) { A[k][j] -= f * A[i][j]; I[k][j] -= f * I[i][j]; }
375
+ }
376
+ }
377
+ }
378
+ return I;
379
+ } catch (e) { return null; }
380
+ }
381
+ };
382
+
383
+ // =============================================================================
384
+ // PREDICTIVE ENGINE & INTERPRETER
385
+ // =============================================================================
386
+ const PredictiveEngine = {
387
+ forecastRisk: (todayFeatures, historicalFeatures, anomalyScore) => {
388
+ const momentum = todayFeatures.vector[17];
389
+ const riskAccel = todayFeatures.vector[16];
390
+
391
+ let baseProbability = 1 / (1 + Math.exp(-(anomalyScore - 3)));
392
+
393
+ if (momentum > 1.0) baseProbability = Math.min(0.99, baseProbability * 1.2);
394
+ if (riskAccel > 1.0) baseProbability = Math.min(0.99, baseProbability * 1.15);
395
+
396
+ return {
397
+ probability7d: baseProbability,
398
+ confidence: historicalFeatures.length >= 25 ? 'high' : 'medium',
399
+ trajectory: momentum > 0 ? 'escalating' : 'stable'
400
+ };
401
+ }
402
+ };
403
+
404
+ const SemanticInterpreter = {
405
+ interpret: (scores, features, prediction, regime, featureNames) => {
406
+ // ROBUST LOGIC: Use the Z-Scores passed from AnomalyEngine.
407
+ const zScores = scores.zScores || [];
408
+
409
+ let maxIdx = 0;
410
+ let maxZ = 0;
411
+
412
+ // Find feature with highest ABSOLUTE Z-score (Statistically most significant)
413
+ zScores.forEach((z, i) => {
414
+ if (Math.abs(z) > maxZ) {
415
+ maxZ = Math.abs(z);
416
+ maxIdx = i;
417
+ }
418
+ });
419
+
420
+ const primaryDriver = featureNames[maxIdx];
421
+ const driverZ = zScores[maxIdx];
422
+ const rawValue = features.vector[maxIdx];
423
+
424
+ let description = '';
425
+ let severity = 'low';
426
+
427
+ if (scores.overall > 5.0) { severity = 'critical'; description = `🚨 CRITICAL: Severe anomaly in ${regime} regime. `; }
428
+ else if (scores.overall > 4.0) { severity = 'high'; description = `⚠️ HIGH RISK: Significant deviation. `; }
429
+ else if (scores.overall > 3.0) { severity = 'medium'; description = `⚡ MODERATE: Unusual pattern. `; }
430
+ else { description = `ℹ️ NOTICE: Minor shift. `; }
431
+
432
+ const direction = driverZ > 0 ? "increased" : "decreased";
433
+
434
+ description += `Driven by ${primaryDriver}. `;
435
+ description += `Value ${direction} to ${rawValue.toFixed(2)} (${driverZ > 0 ? '+' : ''}${maxZ.toFixed(1)}σ). `;
436
+
437
+ if (prediction.probability7d > 0.7) description += `High escalation risk (${(prediction.probability7d*100).toFixed(0)}%).`;
438
+
439
+ return {
440
+ description,
441
+ severity,
442
+ primaryDriver,
443
+ driverValue: rawValue.toFixed(2),
444
+ driverSigma: (driverZ > 0 ? '+' : '-') + maxZ.toFixed(1) + 'σ',
445
+ regime
446
+ };
447
+ }
448
+ };
449
+
450
+ // =============================================================================
451
+ // MAIN COMPUTATION
452
+ // =============================================================================
3
453
  class BehavioralAnomaly extends Computation {
4
454
 
5
455
  static getConfig() {
@@ -10,271 +460,153 @@ class BehavioralAnomaly extends Computation {
10
460
  isHistorical: true,
11
461
 
12
462
  requires: {
463
+ // COST CONTROL: 30-day limit
13
464
  'portfolio_snapshots': {
14
- lookback: 60,
465
+ lookback: 30,
15
466
  mandatory: true,
16
467
  fields: ['user_id', 'portfolio_data', 'date']
17
468
  },
18
469
  'pi_rankings': {
19
- lookback: 60,
20
- mandatory: true,
470
+ lookback: 30,
471
+ mandatory: true,
21
472
  fields: ['pi_id', 'rankings_data', 'date']
22
473
  },
23
474
  'trade_history_snapshots': {
24
- lookback: 60,
25
- mandatory: false, // Not all users have trade history every day
475
+ lookback: 0,
476
+ mandatory: false,
26
477
  fields: ['user_id', 'history_data', 'date']
27
- }
478
+ },
479
+ 'ticker_mappings': { mandatory: false, fields: ['instrument_id', 'ticker'] },
480
+ 'sector_mappings': { mandatory: false, fields: ['symbol', 'sector'] },
481
+ 'pi_master_list': { mandatory: false, fields: ['cid', 'username'] }
28
482
  },
29
483
 
30
484
  storage: {
31
485
  bigquery: true,
32
- firestore: {
33
- enabled: true,
34
- path: 'alerts/{date}/BehavioralAnomaly/{entityId}',
35
- merge: true
486
+ firestore: {
487
+ enabled: true,
488
+ path: 'alerts/{date}/BehavioralAnomaly/{entityId}',
489
+ merge: true
36
490
  }
37
491
  },
38
-
39
- // LEGACY METADATA PRESERVED
492
+
40
493
  userType: 'POPULAR_INVESTOR',
41
494
  alert: {
42
- id: 'behavioralAnomaly',
43
- frontendName: 'Behavioral Anomaly',
44
- description: 'Alert when a Popular Investor deviates significantly from their baseline behavior',
45
- messageTemplate: 'Behavioral Alert for {piUsername}: {primaryDriver} Deviation ({driverSignificance}) detected.',
46
- severity: 'high',
47
- configKey: 'behavioralAnomaly',
48
- isDynamic: true,
49
- thresholds: [
50
- {
51
- key: 'anomalyScoreThreshold',
52
- type: 'number',
53
- label: 'Anomaly Sensitivity',
54
- description: 'Alert when anomaly score exceeds this threshold (higher = less sensitive)',
55
- default: 3.5,
56
- min: 2.0,
57
- max: 5.0,
58
- step: 0.5,
59
- unit: 'σ (standard deviations)'
60
- }
61
- ],
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']
495
+ id: 'behavioral_anomaly_v4',
496
+ frontendName: 'Behavioral Risk Intelligence',
497
+ severity: 'high',
498
+ isDynamic: true
78
499
  }
79
500
  };
80
501
  }
81
502
 
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
- }
142
-
143
- calculateMartingaleScore(historyData) {
144
- const trades = this.rules.trades.extractTrades(historyData);
145
- if (!trades || trades.length < 2) return 0;
146
-
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];
503
+ async process(context) {
504
+ const { data, entityId, date, rules, references } = context;
159
505
 
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++;
165
- }
506
+ // 1. Map Building
507
+ const tickerMap = new Map();
508
+ if (references.ticker_mappings) {
509
+ Object.values(references.ticker_mappings).forEach(r => tickerMap.set(String(r.instrument_id), r.ticker));
166
510
  }
167
- return lossEvents > 0 ? (martingaleResponses / lossEvents) : 0;
168
- }
169
-
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);
511
+ const sectorMap = new Map();
512
+ if (references.sector_mappings) {
513
+ Object.values(references.sector_mappings).forEach(r => sectorMap.set(r.symbol, r.sector));
182
514
  }
515
+ const maps = { tickerMap, sectorMap };
516
+
517
+ // 2. Data Alignment
518
+ const toDateStr = (d) => d && d.value ? d.value : (d instanceof Date ? d.toISOString().slice(0, 10) : String(d));
519
+ const getRows = (dataset) => {
520
+ if (!dataset) return [];
521
+ if (dataset[entityId]) return Array.isArray(dataset[entityId]) ? dataset[entityId] : [dataset[entityId]];
522
+ if (Array.isArray(dataset)) return dataset.filter(r => String(r.user_id || r.pi_id || r.cid) === String(entityId));
523
+ return [];
524
+ };
183
525
 
184
- return [hhi, mScore, strain, risk];
185
- }
526
+ const portfolios = getRows(data.portfolio_snapshots);
527
+ const rankings = getRows(data.pi_rankings);
528
+ const historyRows = getRows(data.trade_history_snapshots);
529
+ const currentHistoryBlob = historyRows.length > 0 ? (historyRows[0].history_data?.PublicHistoryPositions || []) : [];
530
+ const identityRows = getRows(data.pi_master_list);
531
+ const username = identityRows.length > 0 ? identityRows[0].username : "Unknown";
532
+
533
+ const dailyData = new Map();
534
+ portfolios.forEach(p => {
535
+ const d = toDateStr(p.date);
536
+ if (!dailyData.has(d)) dailyData.set(d, {});
537
+ dailyData.get(d).portfolio = p.portfolio_data;
538
+ dailyData.get(d).date = d;
539
+ });
540
+ rankings.forEach(r => {
541
+ const d = toDateStr(r.date);
542
+ if (dailyData.has(d)) dailyData.get(d).rankings = r.rankings_data;
543
+ });
186
544
 
187
- async process(context) {
188
- const { data, entityId, rules } = context;
189
- this.rules = rules; // Attach rules for helper access
545
+ if (!dailyData.has(date)) return;
190
546
 
191
- const portfolios = data['portfolio_snapshots'] || [];
192
- const rankings = data['pi_rankings'] || [];
193
- const histories = data['trade_history_snapshots'] || [];
547
+ // 3. Historical Extraction (30 days)
548
+ const historicalFeatures = [];
549
+ const lookbackDate = new Date(date);
550
+ lookbackDate.setDate(lookbackDate.getDate() - 30);
194
551
 
195
- // Sort data by date ascending for baseline building
196
- portfolios.sort((a, b) => new Date(a.date) - new Date(b.date));
552
+ let prevDay = null;
553
+ let prevPrevDay = null;
197
554
 
198
- const trainingVectors = [];
199
- const MIN_DATAPOINTS = 15;
200
-
201
- // Build Baseline
202
- for (const portRow of portfolios) {
203
- const date = portRow.date;
555
+ for (let d = new Date(lookbackDate); d < new Date(date); d.setDate(d.getDate() + 1)) {
556
+ const dStr = d.toISOString().slice(0, 10);
557
+ const dayData = dailyData.get(dStr);
204
558
 
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);
208
-
209
- const portData = rules.portfolio.extractPortfolioData(portRow);
210
- const rankData = rules.rankings.extractRankingsData(rankRow);
211
-
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;
215
-
216
- if (portData) {
217
- const vec = this.getDailyVector(portData, rankData, histData);
218
- trainingVectors.push(vec);
559
+ if (dayData && dayData.portfolio) {
560
+ const features = FeatureExtractor.extract(dayData, prevDay, prevPrevDay, currentHistoryBlob, dayData.rankings, rules, maps);
561
+ historicalFeatures.push(features);
562
+ prevPrevDay = prevDay;
563
+ prevDay = dayData;
219
564
  }
220
565
  }
221
566
 
222
- if (trainingVectors.length < MIN_DATAPOINTS) {
223
- this.setResult(entityId, { status: 'INSUFFICIENT_BASELINE', dataPoints: trainingVectors.length });
224
- return;
567
+ if (historicalFeatures.length < 15) {
568
+ this.setResult(entityId, { triggered: false, status: 'INSUFFICIENT_HISTORY' });
569
+ return;
225
570
  }
226
571
 
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);
232
-
233
- if (!inverseCov) {
234
- this.setResult(entityId, { status: 'STABLE_STATE', info: 'Variance too low' });
235
- return;
236
- }
572
+ // 4. Today's Features
573
+ const todayData = dailyData.get(date);
574
+ const todayFeatures = FeatureExtractor.extract(todayData, prevDay, prevPrevDay, currentHistoryBlob, todayData.rankings, rules, maps);
237
575
 
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);
242
-
243
- const todayVector = this.getDailyVector(
244
- rules.portfolio.extractPortfolioData(todayPortRow),
245
- rules.rankings.extractRankingsData(todayRankRow),
246
- todayHistRow
247
- );
248
-
249
- const distance = BehavioralAnomaly.LinearAlgebra.mahalanobisDistance(todayVector, stats.means, inverseCov);
250
- const IS_ANOMALY = distance > 3.5;
251
-
252
- const result = {
253
- triggered: IS_ANOMALY,
254
- anomalyScore: parseFloat(distance.toFixed(2)),
255
- baselineDays: trainingVectors.length
256
- };
257
-
258
- if (IS_ANOMALY) {
259
- const featureNames = ['Concentration (HHI)', 'Martingale Behavior', 'Capacity Strain', 'Risk Score'];
260
- let maxZ = 0;
261
- let primaryDriver = 'Unknown';
262
-
263
- 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;
266
- if (z > maxZ) {
267
- maxZ = z;
268
- primaryDriver = featureNames[i];
269
- }
270
- });
576
+ // 5. Detection & Scoring
577
+ const recentVectors = historicalFeatures.slice(-5).map(f => f.vector);
578
+ const regime = AdvancedMath.detectRegime(recentVectors);
579
+ historicalFeatures.forEach(f => f.metadata.regime = regime);
580
+
581
+ const scores = AnomalyEngine.score(todayFeatures, historicalFeatures, regime);
582
+ const prediction = PredictiveEngine.forecastRisk(todayFeatures, historicalFeatures, scores.overall);
271
583
 
272
- result.primaryDriver = primaryDriver;
273
- result.driverSignificance = `${maxZ.toFixed(1)}σ`;
274
- result.vectors = { current: todayVector, baselineMeans: stats.means };
275
- }
584
+ const featureNames = [
585
+ 'Sector HHI', 'Martingale', 'Avg Leverage', 'Risk Score', 'Complexity', 'Exposure',
586
+ 'Entropy', 'Drawdown', 'Win Rate Dev', 'Skewness', 'Stress Ratio', 'Credit Imbal',
587
+ 'High Lev Freq', 'Copier Mom', 'AUM Tier', 'Exp Velocity', 'Risk Accel', 'Behav Mom'
588
+ ];
276
589
 
277
- this.setResult(entityId, result);
590
+ const interpretation = SemanticInterpreter.interpret(scores, todayFeatures, prediction, regime, featureNames);
591
+
592
+ const THRESHOLD = 3.5;
593
+ const triggered = scores.overall > THRESHOLD || prediction.probability7d > 0.75;
594
+
595
+ this.setResult(entityId, {
596
+ triggered,
597
+ score: Number(scores.overall.toFixed(2)),
598
+ severity: interpretation.severity,
599
+ regime,
600
+ description: interpretation.description,
601
+ username,
602
+ driver: interpretation.primaryDriver,
603
+ driverValue: interpretation.driverValue,
604
+ prediction: {
605
+ probability: (prediction.probability7d * 100).toFixed(1) + '%',
606
+ trajectory: prediction.trajectory
607
+ },
608
+ features: todayFeatures.vector.map(v => Number(v.toFixed(3)))
609
+ });
278
610
  }
279
611
  }
280
612