bulltrackers-module 1.0.210 → 1.0.212

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.
@@ -1,744 +1,744 @@
1
- /**
2
- * @fileoverview Math Layer - Single Source of Truth (V3 Final)
3
- * Encapsulates Schema Knowledge, Mathematical Primitives, and Signal Extractors.
4
- * * STRICT COMPLIANCE:
5
- * - Adheres to 'schema.md' definitions.
6
- * - standardizes access to P&L, Weights, and Rates.
7
- */
8
-
9
- const SCHEMAS = {
10
- USER_TYPES: { NORMAL: 'normal', SPECULATOR: 'speculator' },
11
- STYLES: { DAY_TRADER: 'Day Trader', SWING_TRADER: 'Swing Trader', INVESTOR: 'Investor' },
12
- LABELS: { SMART: 'Smart Money', DUMB: 'Dumb Money', NEUTRAL: 'Neutral' }
13
- };
14
-
15
- class InsightsExtractor {
16
- /**
17
- * Extracts the raw array of insight objects from the context.
18
- * Checks for standard context injection paths.
19
- */
20
- static getInsights(context) {
21
- // Support multiple potential injection paths depending on controller version
22
- return context.insights || context.daily_instrument_insights || [];
23
- }
24
-
25
- /**
26
- * returns the specific insight object for a given instrument ID.
27
- */
28
- static getInsightForInstrument(insights, instrumentId) {
29
- if (!insights || !Array.isArray(insights)) return null;
30
- return insights.find(i => i.instrumentId === instrumentId) || null;
31
- }
32
-
33
- // --- Standard Metrics ---
34
-
35
- static getTotalOwners(insight) {
36
- return insight ? (insight.total || 0) : 0;
37
- }
38
-
39
- static getLongPercent(insight) {
40
- return insight ? (insight.buy || 0) : 0;
41
- }
42
-
43
- static getShortPercent(insight) {
44
- return insight ? (insight.sell || 0) : 0;
45
- }
46
-
47
- static getGrowthPercent(insight) {
48
- return insight ? (insight.growth || 0) : 0;
49
- }
50
-
51
- // --- Derived Counts (Estimated) ---
52
-
53
- static getLongCount(insight) {
54
- const total = this.getTotalOwners(insight);
55
- const buyPct = this.getLongPercent(insight);
56
- return Math.floor(total * (buyPct / 100));
57
- }
58
-
59
- static getShortCount(insight) {
60
- const total = this.getTotalOwners(insight);
61
- const sellPct = this.getShortPercent(insight);
62
- return Math.floor(total * (sellPct / 100));
63
- }
64
-
65
- /**
66
- * Calculates the net change in users from yesterday based on growth %.
67
- * Formula: NetChange = Total - (Total / (1 + Growth/100))
68
- */
69
- static getNetOwnershipChange(insight) {
70
- const total = this.getTotalOwners(insight);
71
- const growth = this.getGrowthPercent(insight);
72
- if (total === 0) return 0;
73
-
74
- // Reverse engineer yesterday's count
75
- // Today = Yesterday * (1 + growth)
76
- // Yesterday = Today / (1 + growth)
77
- const prevTotal = total / (1 + (growth / 100)); // TODO: Check precision issues
78
- return Math.round(total - prevTotal);
79
- }
80
- }
81
-
82
- class UserClassifier {
83
- /**
84
- * Classifies a user as Smart/Dumb based on Win Rate and Profitability.
85
- * @param {Object} historySummary - Result from HistoryExtractor.getSummary()
86
- */
87
- static classifySmartDumb(historySummary) {
88
- if (!historySummary || historySummary.totalTrades < 5) return SCHEMAS.LABELS.NEUTRAL; // Insufficient data
89
-
90
- const { winRatio, avgProfitPct, avgLossPct } = historySummary;
91
-
92
- // 1. The "Consistent Winner" (Smart)
93
- if (winRatio > 60 && avgProfitPct > Math.abs(avgLossPct)) return SCHEMAS.LABELS.SMART;
94
-
95
- // 2. The "Sniper" (Smart - Low Win Rate but huge winners)
96
- if (winRatio > 30 && avgProfitPct > (Math.abs(avgLossPct) * 2.5)) return SCHEMAS.LABELS.SMART;
97
-
98
- // 3. The "Bagholder" (Dumb - High win rate but one loss wipes them out)
99
- if (winRatio > 80 && (Math.abs(avgLossPct) > avgProfitPct * 4)) return SCHEMAS.LABELS.DUMB;
100
-
101
- // 4. The "Gambler" (Dumb - Losing money consistently)
102
- if (winRatio < 40 && avgProfitPct < Math.abs(avgLossPct)) return SCHEMAS.LABELS.DUMB;
103
-
104
- return SCHEMAS.LABELS.NEUTRAL;
105
- }
106
-
107
- /**
108
- * Classifies trading style based on average holding time.
109
- * @param {number} avgHoldingMinutes
110
- */
111
- static classifyStyle(avgHoldingMinutes) {
112
- if (avgHoldingMinutes <= 0) return SCHEMAS.STYLES.INVESTOR; // Default
113
-
114
- if (avgHoldingMinutes < 60 * 24) return SCHEMAS.STYLES.DAY_TRADER; // < 1 Day
115
- if (avgHoldingMinutes < 60 * 24 * 30) return SCHEMAS.STYLES.SWING_TRADER; // < 1 Month
116
- return SCHEMAS.STYLES.INVESTOR; // > 1 Month
117
- }
118
- }
119
-
120
- class DataExtractor { // For generic access of data types
121
- // ========================================================================
122
- // 1. COLLECTION ACCESSORS
123
- // ========================================================================
124
-
125
- /**
126
- * Extract positions array based on User Type.
127
- * - Normal: Uses 'AggregatedPositions' (Grouped by Asset + Direction)
128
- * - Speculator: Uses 'PublicPositions' (Individual Trades)
129
- */
130
- static getPositions(portfolio, userType) {
131
- if (!portfolio) return []; // Handle empty portfolio
132
-
133
- if (userType === SCHEMAS.USER_TYPES.SPECULATOR) {
134
- return portfolio.PublicPositions || []; // SPECULATOR SCHEMA
135
- }
136
-
137
- // Default to Normal User Schema
138
- return portfolio.AggregatedPositions || [];
139
- }
140
-
141
- // ========================================================================
142
- // 2. IDENTITY & KEYS
143
- // ========================================================================
144
-
145
- /**
146
- * Extract standardized Instrument ID.
147
- */
148
- static getInstrumentId(position) {
149
- if (!position) return null; // Handle empty position data
150
- // Handle string or number variations safely
151
- return position.InstrumentID || position.instrumentId || null;
152
- }
153
-
154
- /**
155
- * Extract a unique Identifier for the position.
156
- * - Speculator: Uses 'PositionID'.
157
- * - Normal: Generates Composite Key (InstrumentID_Direction) since they lack unique Trade IDs.
158
- */
159
- static getPositionId(position) {
160
- if (!position) return null; // Handle empty position data
161
-
162
- // 1. Try Explicit ID (Speculators)
163
- if (position.PositionID) return String(position.PositionID);
164
- if (position.PositionId) return String(position.PositionId);
165
-
166
- // 2. Fallback to Composite Key (Normal Users)
167
- const instId = this.getInstrumentId(position);
168
- const dir = this.getDirection(position);
169
- if (instId) return `${instId}_${dir}`;
170
-
171
- return null;
172
- }
173
-
174
- // ========================================================================
175
- // 3. FINANCIAL METRICS (WEIGHTS & P&L)
176
- // ========================================================================
177
-
178
- /**
179
- * Extract Net Profit %.
180
- * Schema: 'NetProfit' is the percentage profit relative to invested capital.
181
- */
182
- static getNetProfit(position) {
183
- return position ? (position.NetProfit || 0) : 0;
184
- }
185
-
186
- /**
187
- * Extract Position Weight (Allocation %).
188
- * Schema:
189
- * - Normal: 'Invested' is % of initial capital.
190
- * - Speculator: 'Invested' (or 'Amount' in some contexts) is % of initial capital.
191
- */
192
- static getPositionWeight(position, userType) { // Agnostic on user type, unused.
193
- if (!position) return 0;
194
-
195
- // Both schemas use 'Invested' to represent the allocation percentage.
196
- // Speculators might optionally have 'Amount', we prioritize 'Invested' for consistency.
197
- return position.Invested || position.Amount || 0;
198
- }
199
-
200
- /**
201
- * Extract Current Equity Value %.
202
- * Schema: 'Value' is the current value as a % of total portfolio equity.
203
- */
204
- static getPositionValuePct(position) {
205
- return position ? (position.Value || 0) : 0;
206
- }
207
-
208
- /**
209
- * --- NEW PRIMITIVE ---
210
- * Derives the approximate Entry Price of a position based on Current Price and Net Profit %.
211
- * Formula: Entry = Current / (1 + (NetProfit / 100))
212
- * @param {number} currentPrice - The current market price of the asset.
213
- * @param {number} netProfitPct - The Net Profit percentage (e.g., -20.5).
214
- * @returns {number} Estimated Entry Price.
215
- */
216
- static deriveEntryPrice(currentPrice, netProfitPct) {
217
- if (!currentPrice || currentPrice <= 0) return 0;
218
- // Avoid division by zero if P&L is -100% (unlikely but possible in crypto/options)
219
- if (netProfitPct <= -100) return Number.MAX_SAFE_INTEGER; // Effectively infinite entry price (lost everything)
220
- return currentPrice / (1 + (netProfitPct / 100.0));
221
- }
222
-
223
- // ========================================================================
224
- // 4. PORTFOLIO LEVEL SUMMARY
225
- // ========================================================================
226
-
227
- /**
228
- * Calculate/Extract Daily Portfolio P&L %.
229
- */
230
- static getPortfolioDailyPnl(portfolio, userType) {
231
- if (!portfolio) return 0;
232
-
233
- // 1. Speculator (Explicit 'NetProfit' field on root)
234
- if (userType === SCHEMAS.USER_TYPES.SPECULATOR) {
235
- return portfolio.NetProfit || 0;
236
- }
237
-
238
- // 2. Normal (Aggregated Calculation)
239
- if (portfolio.AggregatedPositionsByInstrumentTypeID) {
240
- return portfolio.AggregatedPositionsByInstrumentTypeID.reduce((sum, agg) => {
241
- return sum + ((agg.Value || 0) - (agg.Invested || 0));
242
- }, 0);
243
- }
244
-
245
- return 0;
246
- }
247
-
248
- // ========================================================================
249
- // 5. TRADE DETAILS (SPECULATOR SPECIFIC)
250
- // ========================================================================
251
-
252
- static getDirection(position) {
253
- if (!position) return "Buy";
254
- if (position.Direction) return position.Direction;
255
- if (typeof position.IsBuy === 'boolean') return position.IsBuy ? "Buy" : "Sell";
256
- return "Buy"; // Default
257
- }
258
-
259
- static getLeverage(position) {
260
- return position ? (position.Leverage || 1) : 1; // Default 1 IF NOT FOUND
261
- }
262
-
263
- static getOpenRate(position) {
264
- return position ? (position.OpenRate || 0) : 0; // Default 0 IF NOT FOUND
265
- }
266
-
267
- static getCurrentRate(position) {
268
- return position ? (position.CurrentRate || 0) : 0; // Default 0 IF NOT FOUND
269
- }
270
-
271
- static getStopLossRate(position) {
272
- const rate = position ? (position.StopLossRate || 0) : 0;
273
- if (rate > 0 && rate <= 0.01) return 0; // Normalizes bug value to 0
274
- if (rate < 0) return 0;
275
- return rate;
276
- }
277
-
278
- static getTakeProfitRate(position) {
279
- const rate = position ? (position.TakeProfitRate || 0) : 0;
280
- if (rate > 0 && rate <= 0.01) return 0; // Normalizes bug value to 0
281
- return rate;
282
- }
283
-
284
- static getHasTSL(position) {
285
- return position ? (position.HasTrailingStopLoss === true) : false; // Default false IF NOT FOUND
286
- }
287
-
288
- static getOpenDateTime(position) {
289
- if (!position || !position.OpenDateTime) return null;
290
- return new Date(position.OpenDateTime);
291
- }
292
- }
293
-
294
- class priceExtractor {
295
- static getHistory(pricesContext, tickerOrId) {
296
- if (!pricesContext || !pricesContext.history) return [];
297
- let assetData = pricesContext.history[tickerOrId];
298
-
299
- if (!assetData) {
300
- const id = Object.keys(pricesContext.history).find(key => {
301
- const data = pricesContext.history[key];
302
- return data.ticker === tickerOrId;
303
- });
304
- if (id) assetData = pricesContext.history[id];
305
- }
306
-
307
- if (!assetData || !assetData.prices) return [];
308
-
309
- const priceMap = assetData.prices;
310
- const sortedDates = Object.keys(priceMap).sort((a, b) => a.localeCompare(b));
311
-
312
- return sortedDates.map(date => ({
313
- date: date,
314
- price: priceMap[date]
315
- })).filter(item => item.price > 0);
316
- }
317
-
318
- static getAllHistories(pricesContext) {
319
- if (!pricesContext || !pricesContext.history) return new Map();
320
-
321
- const results = new Map();
322
- for (const [id, data] of Object.entries(pricesContext.history)) {
323
- const ticker = data.ticker || id;
324
- const history = this.getHistory(pricesContext, id);
325
- if (history.length > 0) {
326
- results.set(ticker, history);
327
- }
328
- }
329
- return results;
330
- }
331
- }
332
-
333
- class HistoryExtractor {
334
- static getDailyHistory(user) {
335
- return user?.history?.today || null;
336
- }
337
-
338
- static getTradedAssets(historyDoc) {
339
- const trades = historyDoc?.PublicHistoryPositions || [];
340
- if (!trades.length) return [];
341
-
342
- const assetsMap = new Map();
343
-
344
- for (const t of trades) {
345
- const instId = t.InstrumentID;
346
- if (!instId) continue;
347
-
348
- if (!assetsMap.has(instId)) {
349
- assetsMap.set(instId, {
350
- instrumentId: instId,
351
- totalDuration: 0,
352
- count: 0
353
- });
354
- }
355
-
356
- const asset = assetsMap.get(instId);
357
- const open = new Date(t.OpenDateTime);
358
- const close = new Date(t.CloseDateTime);
359
- const durationMins = (close - open) / 60000;
360
-
361
- if (durationMins > 0) {
362
- asset.totalDuration += durationMins;
363
- asset.count++;
364
- }
365
- }
366
-
367
- return Array.from(assetsMap.values()).map(a => ({
368
- instrumentId: a.instrumentId,
369
- avgHoldingTimeInMinutes: a.count > 0 ? (a.totalDuration / a.count) : 0
370
- }));
371
- }
372
-
373
- static getInstrumentId(asset) {
374
- return asset ? asset.instrumentId : null;
375
- }
376
-
377
- static getAvgHoldingTimeMinutes(asset) {
378
- return asset ? (asset.avgHoldingTimeInMinutes || 0) : 0;
379
- }
380
-
381
- static getSummary(historyDoc) {
382
- const trades = historyDoc?.PublicHistoryPositions || [];
383
- if (!trades.length) return null;
384
-
385
- let totalTrades = trades.length;
386
- let wins = 0;
387
- let totalProf = 0;
388
- let totalLoss = 0;
389
- let profCount = 0;
390
- let lossCount = 0;
391
- let totalDur = 0;
392
-
393
- for (const t of trades) {
394
- if (t.NetProfit > 0) {
395
- wins++;
396
- totalProf += t.NetProfit;
397
- profCount++;
398
- } else if (t.NetProfit < 0) {
399
- totalLoss += t.NetProfit;
400
- lossCount++;
401
- }
402
-
403
- const open = new Date(t.OpenDateTime);
404
- const close = new Date(t.CloseDateTime);
405
- totalDur += (close - open) / 60000;
406
- }
407
-
408
- return {
409
- totalTrades: totalTrades,
410
- winRatio: totalTrades > 0 ? (wins / totalTrades) * 100 : 0,
411
- avgProfitPct: profCount > 0 ? totalProf / profCount : 0,
412
- avgLossPct: lossCount > 0 ? totalLoss / lossCount : 0,
413
- avgHoldingTimeInMinutes: totalTrades > 0 ? totalDur / totalTrades : 0
414
- };
415
- }
416
- }
417
-
418
- class SignalPrimitives {
419
- static getMetric(dependencies, calcName, ticker, fieldName, fallback = 0) {
420
- if (!dependencies || !dependencies[calcName]) return fallback;
421
- const tickerData = dependencies[calcName][ticker];
422
- if (!tickerData) return fallback;
423
-
424
- const val = tickerData[fieldName];
425
- return (typeof val === 'number') ? val : fallback;
426
- }
427
-
428
- static getUnionKeys(dependencies, calcNames) {
429
- const keys = new Set();
430
- if (!dependencies) return [];
431
- for (const name of calcNames) {
432
- const resultObj = dependencies[name];
433
- if (resultObj && typeof resultObj === 'object') {
434
- Object.keys(resultObj).forEach(k => keys.add(k));
435
- }
436
- }
437
- return Array.from(keys);
438
- }
439
-
440
- static normalizeTanh(value, scale = 10, sensitivity = 10.0) {
441
- if (value === 0) return 0;
442
- return Math.tanh(value / sensitivity) * scale;
443
- }
444
-
445
- static normalizeZScore(value, mean, stdDev) {
446
- if (!stdDev || stdDev === 0) return 0;
447
- return (value - mean) / stdDev;
448
- }
449
-
450
- static divergence(valueA, valueB) {
451
- return (valueA || 0) - (valueB || 0);
452
- }
453
-
454
- static getPreviousState(previousComputed, calcName, ticker, fieldName = null) {
455
- if (!previousComputed || !previousComputed[calcName]) return null;
456
- const tickerData = previousComputed[calcName][ticker];
457
- if (!tickerData) return null;
458
-
459
- if (fieldName) {
460
- return tickerData[fieldName];
461
- }
462
- return tickerData;
463
- }
464
- }
465
-
466
- class MathPrimitives {
467
- static average(values) {
468
- if (!values || !values.length) return 0;
469
- return values.reduce((a, b) => a + b, 0) / values.length;
470
- }
471
-
472
- static median(values) {
473
- if (!values || !values.length) return 0;
474
- const sorted = [...values].sort((a, b) => a - b);
475
- const mid = Math.floor(sorted.length / 2);
476
- return sorted.length % 2 === 0
477
- ? (sorted[mid - 1] + sorted[mid]) / 2
478
- : sorted[mid];
479
- }
480
-
481
- static standardDeviation(values) {
482
- if (!values || !values.length) return 0;
483
- const avg = this.average(values);
484
- const squareDiffs = values.map(val => Math.pow((val || 0) - avg, 2));
485
- return Math.sqrt(this.average(squareDiffs));
486
- }
487
-
488
- static bucketBinary(value, threshold = 0) {
489
- return value > threshold ? 'winner' : 'loser';
490
- }
491
-
492
- static calculateHitProbability(currentPrice, barrierPrice, volatility, days, drift = 0) {
493
- if (currentPrice <= 0 || barrierPrice <= 0 || volatility <= 0 || days <= 0) return 0;
494
-
495
- const t = days / 365.0;
496
- const sigma = volatility;
497
- const mu = drift;
498
- const b = Math.log(barrierPrice / currentPrice);
499
- const nu = mu - 0.5 * Math.pow(sigma, 2);
500
- const sqrtT = Math.sqrt(t);
501
- const sigmaSqrtT = sigma * sqrtT;
502
-
503
- const normCDF = (x) => {
504
- const t = 1 / (1 + 0.2316419 * Math.abs(x));
505
- const d = 0.3989423 * Math.exp(-x * x / 2);
506
- const prob = d * t * (0.3193815 + t * (-0.3565638 + t * (1.781478 + t * (-1.821256 + t * 1.330274))));
507
- return x > 0 ? 1 - prob : prob;
508
- };
509
-
510
- const term1 = (b - nu * t) / sigmaSqrtT;
511
- const term2 = (2 * nu * b) / (sigma * sigma);
512
- const term3 = (b + nu * t) / sigmaSqrtT;
513
-
514
- if ((currentPrice > barrierPrice && barrierPrice > currentPrice) ||
515
- (currentPrice < barrierPrice && barrierPrice < currentPrice)) {
516
- return 1.0;
517
- }
518
-
519
- const probability = normCDF(( -Math.abs(b) - nu * t ) / sigmaSqrtT) + Math.exp((2 * nu * Math.abs(b)) / (sigma * sigma)) * normCDF(( -Math.abs(b) + nu * t ) / sigmaSqrtT);
520
-
521
- return Math.min(Math.max(probability, 0), 1);
522
- }
523
-
524
- static simulateGBM(currentPrice, volatility, days, simulations = 1000, drift = 0) {
525
- if (currentPrice <= 0 || volatility <= 0 || days <= 0) return new Float32Array(0);
526
-
527
- const t = days / 365.0;
528
- const sigma = volatility;
529
- const mu = drift;
530
- const driftTerm = (mu - 0.5 * sigma * sigma) * t;
531
- const volTerm = sigma * Math.sqrt(t);
532
-
533
- const results = new Float32Array(simulations);
534
-
535
- for (let i = 0; i < simulations; i++) {
536
- const u1 = Math.random();
537
- const u2 = Math.random();
538
- const z = Math.sqrt(-2.0 * Math.log(u1)) * Math.cos(2.0 * Math.PI * u2);
539
- results[i] = currentPrice * Math.exp(driftTerm + volTerm * z);
540
- }
541
- return results;
542
- }
543
-
544
- static simulatePopulationBreakdown(pricePaths, userProfiles) {
545
- if (!pricePaths.length || !userProfiles.length) return 0;
546
-
547
- let totalBreakdownEvents = 0;
548
- const totalSims = pricePaths.length;
549
- const totalUsers = userProfiles.length;
550
-
551
- for (let i = 0; i < totalSims; i++) {
552
- const simPrice = pricePaths[i];
553
- let capitulatedUsersInScenario = 0;
554
-
555
- for (let j = 0; j < totalUsers; j++) {
556
- const user = userProfiles[j];
557
- const hypotheticalPnL = ((simPrice - user.entryPrice) / user.entryPrice) * 100;
558
-
559
- if (hypotheticalPnL < user.thresholdPct) {
560
- capitulatedUsersInScenario++;
561
- }
562
- }
563
-
564
- totalBreakdownEvents += (capitulatedUsersInScenario / totalUsers);
565
- }
566
-
567
- return totalBreakdownEvents / totalSims;
568
- }
569
- }
570
-
571
- class Aggregators {
572
- static bucketUsersByPnlPerAsset(usersData, tickerMap) {
573
- const buckets = new Map();
574
- for (const [userId, portfolio] of Object.entries(usersData)) {
575
- const userType = portfolio.PublicPositions ? SCHEMAS.USER_TYPES.SPECULATOR : SCHEMAS.USER_TYPES.NORMAL;
576
- const positions = DataExtractor.getPositions(portfolio, userType);
577
-
578
- for (const pos of positions) {
579
- const id = DataExtractor.getInstrumentId(pos);
580
- const pnl = DataExtractor.getNetProfit(pos);
581
- if (!id || pnl === 0) continue;
582
-
583
- const ticker = tickerMap[id];
584
- if (!ticker) continue;
585
-
586
- if (!buckets.has(ticker)) buckets.set(ticker, { winners: [], losers: [] });
587
- const b = buckets.get(ticker);
588
-
589
- if (pnl > 0) b.winners.push(userId);
590
- else b.losers.push(userId);
591
- }
592
- }
593
- return Object.fromEntries(buckets);
594
- }
595
-
596
- static getWeightedSentiment(positions) {
597
- if (!positions || positions.length === 0) return 0;
598
-
599
- let totalWeightedPnL = 0;
600
- let totalWeight = 0;
601
-
602
- for (const pos of positions) {
603
- const pnl = DataExtractor.getNetProfit(pos);
604
- const weight = DataExtractor.getPositionWeight(pos);
605
-
606
- if (weight > 0) {
607
- totalWeightedPnL += (pnl * weight);
608
- totalWeight += weight;
609
- }
610
- }
611
-
612
- if (totalWeight === 0) return 0;
613
- return totalWeightedPnL / totalWeight;
614
- }
615
- }
616
-
617
- class Validators {
618
- static validatePortfolio(portfolio, userType) {
619
- if (!portfolio) return { valid: false, errors: ['Portfolio is null'] };
620
-
621
- if (userType === SCHEMAS.USER_TYPES.SPECULATOR) {
622
- if (!portfolio.PublicPositions) return { valid: false, errors: ['Missing PublicPositions'] };
623
- } else {
624
- if (!portfolio.AggregatedPositions) return { valid: false, errors: ['Missing AggregatedPositions'] };
625
- }
626
-
627
- return { valid: true, errors: [] };
628
- }
629
- }
630
-
631
- class TimeSeries {
632
- static updateEMAState(value, state, alpha = 0.1) {
633
- const mean = state ? (state.mean || 0) : 0;
634
- const variance = state ? (state.variance || 1) : 1;
635
-
636
- if (value === undefined || value === null || isNaN(value)) {
637
- return { mean, variance };
638
- }
639
-
640
- const diff = value - mean;
641
- const newMean = mean + (alpha * diff);
642
- const newVariance = (1 - alpha) * (variance + (alpha * diff * diff));
643
-
644
- return { mean: newMean, variance: newVariance };
645
- }
646
-
647
- static pearsonCorrelation(x, y) {
648
- if (!x || !y || x.length !== y.length || x.length === 0) return 0;
649
-
650
- const n = x.length;
651
- let sumX = 0, sumY = 0, sumXY = 0, sumX2 = 0, sumY2 = 0;
652
-
653
- for (let i = 0; i < n; i++) {
654
- sumX += x[i];
655
- sumY += y[i];
656
- sumXY += x[i] * y[i];
657
- sumX2 += x[i] * x[i];
658
- sumY2 += y[i] * y[i];
659
- }
660
-
661
- const numerator = (n * sumXY) - (sumX * sumY);
662
- const denominator = Math.sqrt(((n * sumX2) - (sumX * sumX)) * ((n * sumY2) - (sumY * sumY)));
663
-
664
- return (denominator === 0) ? 0 : numerator / denominator;
665
- }
666
- }
667
-
668
-
669
- class DistributionAnalytics {
670
- static computeKDE(data, bandwidth, steps = 60) {
671
- if (!data || data.length === 0) return [];
672
-
673
- let min = Infinity, max = -Infinity;
674
- for (const p of data) {
675
- if (p.value < min) min = p.value;
676
- if (p.value > max) max = p.value;
677
- }
678
-
679
- min -= bandwidth * 3;
680
- max += bandwidth * 3;
681
- const stepSize = (max - min) / steps;
682
- const curve = [];
683
-
684
- for (let i = 0; i <= steps; i++) {
685
- const x = min + (i * stepSize);
686
- let density = 0;
687
- for (const p of data) {
688
- const diff = (x - p.value);
689
- if (Math.abs(diff) > bandwidth * 3) continue;
690
-
691
- const u = diff / bandwidth;
692
- const k = 0.39894228 * Math.exp(-0.5 * u * u);
693
- density += (p.weight * k) / bandwidth;
694
- }
695
- if (density > 0) curve.push({ price: x, density });
696
- }
697
- return curve;
698
- }
699
-
700
- static integrateProfile(curve, startPrice, endPrice) {
701
- if (!curve || !Array.isArray(curve)) return 0; // Fix for potential crash
702
- let sum = 0;
703
- for (let i = 0; i < curve.length - 1; i++) {
704
- const p1 = curve[i];
705
- const p2 = curve[i+1];
706
- if (p1.price >= startPrice && p2.price <= endPrice) {
707
- sum += (p2.price - p1.price) * ((p1.density + p2.density) / 2);
708
- }
709
- }
710
- return sum;
711
- }
712
-
713
- static linearRegression(xValues, yValues) {
714
- const n = xValues.length;
715
- if (n !== yValues.length || n < 2) return { slope: 0, r2: 0 };
716
-
717
- let sumX = 0, sumY = 0, sumXY = 0, sumXX = 0, sumYY = 0;
718
- for (let i = 0; i < n; i++) {
719
- sumX += xValues[i];
720
- sumY += yValues[i];
721
- sumXY += xValues[i] * yValues[i];
722
- sumXX += xValues[i] * xValues[i];
723
- sumYY += yValues[i] * yValues[i];
724
- }
725
-
726
- const slope = (n * sumXY - sumX * sumY) / (n * sumXX - sumX * sumX);
727
- return { slope, n };
728
- }
729
- }
730
-
731
- module.exports = {
732
- Aggregators,
733
- DataExtractor,
734
- DistributionAnalytics,
735
- HistoryExtractor,
736
- InsightsExtractor, // Exported
737
- MathPrimitives,
738
- SCHEMAS,
739
- SignalPrimitives,
740
- TimeSeries,
741
- UserClassifier, // Exported
742
- Validators,
743
- priceExtractor
1
+ /**
2
+ * @fileoverview Math Layer - Single Source of Truth (V3 Final)
3
+ * Encapsulates Schema Knowledge, Mathematical Primitives, and Signal Extractors.
4
+ * * STRICT COMPLIANCE:
5
+ * - Adheres to 'schema.md' definitions.
6
+ * - standardizes access to P&L, Weights, and Rates.
7
+ */
8
+
9
+ const SCHEMAS = {
10
+ USER_TYPES: { NORMAL: 'normal', SPECULATOR: 'speculator' },
11
+ STYLES: { DAY_TRADER: 'Day Trader', SWING_TRADER: 'Swing Trader', INVESTOR: 'Investor' },
12
+ LABELS: { SMART: 'Smart Money', DUMB: 'Dumb Money', NEUTRAL: 'Neutral' }
13
+ };
14
+
15
+ class InsightsExtractor {
16
+ /**
17
+ * Extracts the raw array of insight objects from the context.
18
+ * Checks for standard context injection paths.
19
+ */
20
+ static getInsights(context) {
21
+ // Support multiple potential injection paths depending on controller version
22
+ return context.insights || context.daily_instrument_insights || [];
23
+ }
24
+
25
+ /**
26
+ * returns the specific insight object for a given instrument ID.
27
+ */
28
+ static getInsightForInstrument(insights, instrumentId) {
29
+ if (!insights || !Array.isArray(insights)) return null;
30
+ return insights.find(i => i.instrumentId === instrumentId) || null;
31
+ }
32
+
33
+ // --- Standard Metrics ---
34
+
35
+ static getTotalOwners(insight) {
36
+ return insight ? (insight.total || 0) : 0;
37
+ }
38
+
39
+ static getLongPercent(insight) {
40
+ return insight ? (insight.buy || 0) : 0;
41
+ }
42
+
43
+ static getShortPercent(insight) {
44
+ return insight ? (insight.sell || 0) : 0;
45
+ }
46
+
47
+ static getGrowthPercent(insight) {
48
+ return insight ? (insight.growth || 0) : 0;
49
+ }
50
+
51
+ // --- Derived Counts (Estimated) ---
52
+
53
+ static getLongCount(insight) {
54
+ const total = this.getTotalOwners(insight);
55
+ const buyPct = this.getLongPercent(insight);
56
+ return Math.floor(total * (buyPct / 100));
57
+ }
58
+
59
+ static getShortCount(insight) {
60
+ const total = this.getTotalOwners(insight);
61
+ const sellPct = this.getShortPercent(insight);
62
+ return Math.floor(total * (sellPct / 100));
63
+ }
64
+
65
+ /**
66
+ * Calculates the net change in users from yesterday based on growth %.
67
+ * Formula: NetChange = Total - (Total / (1 + Growth/100))
68
+ */
69
+ static getNetOwnershipChange(insight) {
70
+ const total = this.getTotalOwners(insight);
71
+ const growth = this.getGrowthPercent(insight);
72
+ if (total === 0) return 0;
73
+
74
+ // Reverse engineer yesterday's count
75
+ // Today = Yesterday * (1 + growth)
76
+ // Yesterday = Today / (1 + growth)
77
+ const prevTotal = total / (1 + (growth / 100)); // TODO: Check precision issues
78
+ return Math.round(total - prevTotal);
79
+ }
80
+ }
81
+
82
+ class UserClassifier {
83
+ /**
84
+ * Classifies a user as Smart/Dumb based on Win Rate and Profitability.
85
+ * @param {Object} historySummary - Result from HistoryExtractor.getSummary()
86
+ */
87
+ static classifySmartDumb(historySummary) {
88
+ if (!historySummary || historySummary.totalTrades < 5) return SCHEMAS.LABELS.NEUTRAL; // Insufficient data
89
+
90
+ const { winRatio, avgProfitPct, avgLossPct } = historySummary;
91
+
92
+ // 1. The "Consistent Winner" (Smart)
93
+ if (winRatio > 60 && avgProfitPct > Math.abs(avgLossPct)) return SCHEMAS.LABELS.SMART;
94
+
95
+ // 2. The "Sniper" (Smart - Low Win Rate but huge winners)
96
+ if (winRatio > 30 && avgProfitPct > (Math.abs(avgLossPct) * 2.5)) return SCHEMAS.LABELS.SMART;
97
+
98
+ // 3. The "Bagholder" (Dumb - High win rate but one loss wipes them out)
99
+ if (winRatio > 80 && (Math.abs(avgLossPct) > avgProfitPct * 4)) return SCHEMAS.LABELS.DUMB;
100
+
101
+ // 4. The "Gambler" (Dumb - Losing money consistently)
102
+ if (winRatio < 40 && avgProfitPct < Math.abs(avgLossPct)) return SCHEMAS.LABELS.DUMB;
103
+
104
+ return SCHEMAS.LABELS.NEUTRAL;
105
+ }
106
+
107
+ /**
108
+ * Classifies trading style based on average holding time.
109
+ * @param {number} avgHoldingMinutes
110
+ */
111
+ static classifyStyle(avgHoldingMinutes) {
112
+ if (avgHoldingMinutes <= 0) return SCHEMAS.STYLES.INVESTOR; // Default
113
+
114
+ if (avgHoldingMinutes < 60 * 24) return SCHEMAS.STYLES.DAY_TRADER; // < 1 Day
115
+ if (avgHoldingMinutes < 60 * 24 * 30) return SCHEMAS.STYLES.SWING_TRADER; // < 1 Month
116
+ return SCHEMAS.STYLES.INVESTOR; // > 1 Month
117
+ }
118
+ }
119
+
120
+ class DataExtractor { // For generic access of data types
121
+ // ========================================================================
122
+ // 1. COLLECTION ACCESSORS
123
+ // ========================================================================
124
+
125
+ /**
126
+ * Extract positions array based on User Type.
127
+ * - Normal: Uses 'AggregatedPositions' (Grouped by Asset + Direction)
128
+ * - Speculator: Uses 'PublicPositions' (Individual Trades)
129
+ */
130
+ static getPositions(portfolio, userType) {
131
+ if (!portfolio) return []; // Handle empty portfolio
132
+
133
+ if (userType === SCHEMAS.USER_TYPES.SPECULATOR) {
134
+ return portfolio.PublicPositions || []; // SPECULATOR SCHEMA
135
+ }
136
+
137
+ // Default to Normal User Schema
138
+ return portfolio.AggregatedPositions || [];
139
+ }
140
+
141
+ // ========================================================================
142
+ // 2. IDENTITY & KEYS
143
+ // ========================================================================
144
+
145
+ /**
146
+ * Extract standardized Instrument ID.
147
+ */
148
+ static getInstrumentId(position) {
149
+ if (!position) return null; // Handle empty position data
150
+ // Handle string or number variations safely
151
+ return position.InstrumentID || position.instrumentId || null;
152
+ }
153
+
154
+ /**
155
+ * Extract a unique Identifier for the position.
156
+ * - Speculator: Uses 'PositionID'.
157
+ * - Normal: Generates Composite Key (InstrumentID_Direction) since they lack unique Trade IDs.
158
+ */
159
+ static getPositionId(position) {
160
+ if (!position) return null; // Handle empty position data
161
+
162
+ // 1. Try Explicit ID (Speculators)
163
+ if (position.PositionID) return String(position.PositionID);
164
+ if (position.PositionId) return String(position.PositionId);
165
+
166
+ // 2. Fallback to Composite Key (Normal Users)
167
+ const instId = this.getInstrumentId(position);
168
+ const dir = this.getDirection(position);
169
+ if (instId) return `${instId}_${dir}`;
170
+
171
+ return null;
172
+ }
173
+
174
+ // ========================================================================
175
+ // 3. FINANCIAL METRICS (WEIGHTS & P&L)
176
+ // ========================================================================
177
+
178
+ /**
179
+ * Extract Net Profit %.
180
+ * Schema: 'NetProfit' is the percentage profit relative to invested capital.
181
+ */
182
+ static getNetProfit(position) {
183
+ return position ? (position.NetProfit || 0) : 0;
184
+ }
185
+
186
+ /**
187
+ * Extract Position Weight (Allocation %).
188
+ * Schema:
189
+ * - Normal: 'Invested' is % of initial capital.
190
+ * - Speculator: 'Invested' (or 'Amount' in some contexts) is % of initial capital.
191
+ */
192
+ static getPositionWeight(position, userType) { // Agnostic on user type, unused.
193
+ if (!position) return 0;
194
+
195
+ // Both schemas use 'Invested' to represent the allocation percentage.
196
+ // Speculators might optionally have 'Amount', we prioritize 'Invested' for consistency.
197
+ return position.Invested || position.Amount || 0;
198
+ }
199
+
200
+ /**
201
+ * Extract Current Equity Value %.
202
+ * Schema: 'Value' is the current value as a % of total portfolio equity.
203
+ */
204
+ static getPositionValuePct(position) {
205
+ return position ? (position.Value || 0) : 0;
206
+ }
207
+
208
+ /**
209
+ * --- NEW PRIMITIVE ---
210
+ * Derives the approximate Entry Price of a position based on Current Price and Net Profit %.
211
+ * Formula: Entry = Current / (1 + (NetProfit / 100))
212
+ * @param {number} currentPrice - The current market price of the asset.
213
+ * @param {number} netProfitPct - The Net Profit percentage (e.g., -20.5).
214
+ * @returns {number} Estimated Entry Price.
215
+ */
216
+ static deriveEntryPrice(currentPrice, netProfitPct) {
217
+ if (!currentPrice || currentPrice <= 0) return 0;
218
+ // Avoid division by zero if P&L is -100% (unlikely but possible in crypto/options)
219
+ if (netProfitPct <= -100) return Number.MAX_SAFE_INTEGER; // Effectively infinite entry price (lost everything)
220
+ return currentPrice / (1 + (netProfitPct / 100.0));
221
+ }
222
+
223
+ // ========================================================================
224
+ // 4. PORTFOLIO LEVEL SUMMARY
225
+ // ========================================================================
226
+
227
+ /**
228
+ * Calculate/Extract Daily Portfolio P&L %.
229
+ */
230
+ static getPortfolioDailyPnl(portfolio, userType) {
231
+ if (!portfolio) return 0;
232
+
233
+ // 1. Speculator (Explicit 'NetProfit' field on root)
234
+ if (userType === SCHEMAS.USER_TYPES.SPECULATOR) {
235
+ return portfolio.NetProfit || 0;
236
+ }
237
+
238
+ // 2. Normal (Aggregated Calculation)
239
+ if (portfolio.AggregatedPositionsByInstrumentTypeID) {
240
+ return portfolio.AggregatedPositionsByInstrumentTypeID.reduce((sum, agg) => {
241
+ return sum + ((agg.Value || 0) - (agg.Invested || 0));
242
+ }, 0);
243
+ }
244
+
245
+ return 0;
246
+ }
247
+
248
+ // ========================================================================
249
+ // 5. TRADE DETAILS (SPECULATOR SPECIFIC)
250
+ // ========================================================================
251
+
252
+ static getDirection(position) {
253
+ if (!position) return "Buy";
254
+ if (position.Direction) return position.Direction;
255
+ if (typeof position.IsBuy === 'boolean') return position.IsBuy ? "Buy" : "Sell";
256
+ return "Buy"; // Default
257
+ }
258
+
259
+ static getLeverage(position) {
260
+ return position ? (position.Leverage || 1) : 1; // Default 1 IF NOT FOUND
261
+ }
262
+
263
+ static getOpenRate(position) {
264
+ return position ? (position.OpenRate || 0) : 0; // Default 0 IF NOT FOUND
265
+ }
266
+
267
+ static getCurrentRate(position) {
268
+ return position ? (position.CurrentRate || 0) : 0; // Default 0 IF NOT FOUND
269
+ }
270
+
271
+ static getStopLossRate(position) {
272
+ const rate = position ? (position.StopLossRate || 0) : 0;
273
+ if (rate > 0 && rate <= 0.01) return 0; // Normalizes bug value to 0
274
+ if (rate < 0) return 0;
275
+ return rate;
276
+ }
277
+
278
+ static getTakeProfitRate(position) {
279
+ const rate = position ? (position.TakeProfitRate || 0) : 0;
280
+ if (rate > 0 && rate <= 0.01) return 0; // Normalizes bug value to 0
281
+ return rate;
282
+ }
283
+
284
+ static getHasTSL(position) {
285
+ return position ? (position.HasTrailingStopLoss === true) : false; // Default false IF NOT FOUND
286
+ }
287
+
288
+ static getOpenDateTime(position) {
289
+ if (!position || !position.OpenDateTime) return null;
290
+ return new Date(position.OpenDateTime);
291
+ }
292
+ }
293
+
294
+ class priceExtractor {
295
+ static getHistory(pricesContext, tickerOrId) {
296
+ if (!pricesContext || !pricesContext.history) return [];
297
+ let assetData = pricesContext.history[tickerOrId];
298
+
299
+ if (!assetData) {
300
+ const id = Object.keys(pricesContext.history).find(key => {
301
+ const data = pricesContext.history[key];
302
+ return data.ticker === tickerOrId;
303
+ });
304
+ if (id) assetData = pricesContext.history[id];
305
+ }
306
+
307
+ if (!assetData || !assetData.prices) return [];
308
+
309
+ const priceMap = assetData.prices;
310
+ const sortedDates = Object.keys(priceMap).sort((a, b) => a.localeCompare(b));
311
+
312
+ return sortedDates.map(date => ({
313
+ date: date,
314
+ price: priceMap[date]
315
+ })).filter(item => item.price > 0);
316
+ }
317
+
318
+ static getAllHistories(pricesContext) {
319
+ if (!pricesContext || !pricesContext.history) return new Map();
320
+
321
+ const results = new Map();
322
+ for (const [id, data] of Object.entries(pricesContext.history)) {
323
+ const ticker = data.ticker || id;
324
+ const history = this.getHistory(pricesContext, id);
325
+ if (history.length > 0) {
326
+ results.set(ticker, history);
327
+ }
328
+ }
329
+ return results;
330
+ }
331
+ }
332
+
333
+ class HistoryExtractor {
334
+ static getDailyHistory(user) {
335
+ return user?.history?.today || null;
336
+ }
337
+
338
+ static getTradedAssets(historyDoc) {
339
+ const trades = historyDoc?.PublicHistoryPositions || [];
340
+ if (!trades.length) return [];
341
+
342
+ const assetsMap = new Map();
343
+
344
+ for (const t of trades) {
345
+ const instId = t.InstrumentID;
346
+ if (!instId) continue;
347
+
348
+ if (!assetsMap.has(instId)) {
349
+ assetsMap.set(instId, {
350
+ instrumentId: instId,
351
+ totalDuration: 0,
352
+ count: 0
353
+ });
354
+ }
355
+
356
+ const asset = assetsMap.get(instId);
357
+ const open = new Date(t.OpenDateTime);
358
+ const close = new Date(t.CloseDateTime);
359
+ const durationMins = (close - open) / 60000;
360
+
361
+ if (durationMins > 0) {
362
+ asset.totalDuration += durationMins;
363
+ asset.count++;
364
+ }
365
+ }
366
+
367
+ return Array.from(assetsMap.values()).map(a => ({
368
+ instrumentId: a.instrumentId,
369
+ avgHoldingTimeInMinutes: a.count > 0 ? (a.totalDuration / a.count) : 0
370
+ }));
371
+ }
372
+
373
+ static getInstrumentId(asset) {
374
+ return asset ? asset.instrumentId : null;
375
+ }
376
+
377
+ static getAvgHoldingTimeMinutes(asset) {
378
+ return asset ? (asset.avgHoldingTimeInMinutes || 0) : 0;
379
+ }
380
+
381
+ static getSummary(historyDoc) {
382
+ const trades = historyDoc?.PublicHistoryPositions || [];
383
+ if (!trades.length) return null;
384
+
385
+ let totalTrades = trades.length;
386
+ let wins = 0;
387
+ let totalProf = 0;
388
+ let totalLoss = 0;
389
+ let profCount = 0;
390
+ let lossCount = 0;
391
+ let totalDur = 0;
392
+
393
+ for (const t of trades) {
394
+ if (t.NetProfit > 0) {
395
+ wins++;
396
+ totalProf += t.NetProfit;
397
+ profCount++;
398
+ } else if (t.NetProfit < 0) {
399
+ totalLoss += t.NetProfit;
400
+ lossCount++;
401
+ }
402
+
403
+ const open = new Date(t.OpenDateTime);
404
+ const close = new Date(t.CloseDateTime);
405
+ totalDur += (close - open) / 60000;
406
+ }
407
+
408
+ return {
409
+ totalTrades: totalTrades,
410
+ winRatio: totalTrades > 0 ? (wins / totalTrades) * 100 : 0,
411
+ avgProfitPct: profCount > 0 ? totalProf / profCount : 0,
412
+ avgLossPct: lossCount > 0 ? totalLoss / lossCount : 0,
413
+ avgHoldingTimeInMinutes: totalTrades > 0 ? totalDur / totalTrades : 0
414
+ };
415
+ }
416
+ }
417
+
418
+ class SignalPrimitives {
419
+ static getMetric(dependencies, calcName, ticker, fieldName, fallback = 0) {
420
+ if (!dependencies || !dependencies[calcName]) return fallback;
421
+ const tickerData = dependencies[calcName][ticker];
422
+ if (!tickerData) return fallback;
423
+
424
+ const val = tickerData[fieldName];
425
+ return (typeof val === 'number') ? val : fallback;
426
+ }
427
+
428
+ static getUnionKeys(dependencies, calcNames) {
429
+ const keys = new Set();
430
+ if (!dependencies) return [];
431
+ for (const name of calcNames) {
432
+ const resultObj = dependencies[name];
433
+ if (resultObj && typeof resultObj === 'object') {
434
+ Object.keys(resultObj).forEach(k => keys.add(k));
435
+ }
436
+ }
437
+ return Array.from(keys);
438
+ }
439
+
440
+ static normalizeTanh(value, scale = 10, sensitivity = 10.0) {
441
+ if (value === 0) return 0;
442
+ return Math.tanh(value / sensitivity) * scale;
443
+ }
444
+
445
+ static normalizeZScore(value, mean, stdDev) {
446
+ if (!stdDev || stdDev === 0) return 0;
447
+ return (value - mean) / stdDev;
448
+ }
449
+
450
+ static divergence(valueA, valueB) {
451
+ return (valueA || 0) - (valueB || 0);
452
+ }
453
+
454
+ static getPreviousState(previousComputed, calcName, ticker, fieldName = null) {
455
+ if (!previousComputed || !previousComputed[calcName]) return null;
456
+ const tickerData = previousComputed[calcName][ticker];
457
+ if (!tickerData) return null;
458
+
459
+ if (fieldName) {
460
+ return tickerData[fieldName];
461
+ }
462
+ return tickerData;
463
+ }
464
+ }
465
+
466
+ class MathPrimitives {
467
+ static average(values) {
468
+ if (!values || !values.length) return 0;
469
+ return values.reduce((a, b) => a + b, 0) / values.length;
470
+ }
471
+
472
+ static median(values) {
473
+ if (!values || !values.length) return 0;
474
+ const sorted = [...values].sort((a, b) => a - b);
475
+ const mid = Math.floor(sorted.length / 2);
476
+ return sorted.length % 2 === 0
477
+ ? (sorted[mid - 1] + sorted[mid]) / 2
478
+ : sorted[mid];
479
+ }
480
+
481
+ static standardDeviation(values) {
482
+ if (!values || !values.length) return 0;
483
+ const avg = this.average(values);
484
+ const squareDiffs = values.map(val => Math.pow((val || 0) - avg, 2));
485
+ return Math.sqrt(this.average(squareDiffs));
486
+ }
487
+
488
+ static bucketBinary(value, threshold = 0) {
489
+ return value > threshold ? 'winner' : 'loser';
490
+ }
491
+
492
+ static calculateHitProbability(currentPrice, barrierPrice, volatility, days, drift = 0) {
493
+ if (currentPrice <= 0 || barrierPrice <= 0 || volatility <= 0 || days <= 0) return 0;
494
+
495
+ const t = days / 365.0;
496
+ const sigma = volatility;
497
+ const mu = drift;
498
+ const b = Math.log(barrierPrice / currentPrice);
499
+ const nu = mu - 0.5 * Math.pow(sigma, 2);
500
+ const sqrtT = Math.sqrt(t);
501
+ const sigmaSqrtT = sigma * sqrtT;
502
+
503
+ const normCDF = (x) => {
504
+ const t = 1 / (1 + 0.2316419 * Math.abs(x));
505
+ const d = 0.3989423 * Math.exp(-x * x / 2);
506
+ const prob = d * t * (0.3193815 + t * (-0.3565638 + t * (1.781478 + t * (-1.821256 + t * 1.330274))));
507
+ return x > 0 ? 1 - prob : prob;
508
+ };
509
+
510
+ const term1 = (b - nu * t) / sigmaSqrtT;
511
+ const term2 = (2 * nu * b) / (sigma * sigma);
512
+ const term3 = (b + nu * t) / sigmaSqrtT;
513
+
514
+ if ((currentPrice > barrierPrice && barrierPrice > currentPrice) ||
515
+ (currentPrice < barrierPrice && barrierPrice < currentPrice)) {
516
+ return 1.0;
517
+ }
518
+
519
+ const probability = normCDF(( -Math.abs(b) - nu * t ) / sigmaSqrtT) + Math.exp((2 * nu * Math.abs(b)) / (sigma * sigma)) * normCDF(( -Math.abs(b) + nu * t ) / sigmaSqrtT);
520
+
521
+ return Math.min(Math.max(probability, 0), 1);
522
+ }
523
+
524
+ static simulateGBM(currentPrice, volatility, days, simulations = 1000, drift = 0) {
525
+ if (currentPrice <= 0 || volatility <= 0 || days <= 0) return new Float32Array(0);
526
+
527
+ const t = days / 365.0;
528
+ const sigma = volatility;
529
+ const mu = drift;
530
+ const driftTerm = (mu - 0.5 * sigma * sigma) * t;
531
+ const volTerm = sigma * Math.sqrt(t);
532
+
533
+ const results = new Float32Array(simulations);
534
+
535
+ for (let i = 0; i < simulations; i++) {
536
+ const u1 = Math.random();
537
+ const u2 = Math.random();
538
+ const z = Math.sqrt(-2.0 * Math.log(u1)) * Math.cos(2.0 * Math.PI * u2);
539
+ results[i] = currentPrice * Math.exp(driftTerm + volTerm * z);
540
+ }
541
+ return results;
542
+ }
543
+
544
+ static simulatePopulationBreakdown(pricePaths, userProfiles) {
545
+ if (!pricePaths.length || !userProfiles.length) return 0;
546
+
547
+ let totalBreakdownEvents = 0;
548
+ const totalSims = pricePaths.length;
549
+ const totalUsers = userProfiles.length;
550
+
551
+ for (let i = 0; i < totalSims; i++) {
552
+ const simPrice = pricePaths[i];
553
+ let capitulatedUsersInScenario = 0;
554
+
555
+ for (let j = 0; j < totalUsers; j++) {
556
+ const user = userProfiles[j];
557
+ const hypotheticalPnL = ((simPrice - user.entryPrice) / user.entryPrice) * 100;
558
+
559
+ if (hypotheticalPnL < user.thresholdPct) {
560
+ capitulatedUsersInScenario++;
561
+ }
562
+ }
563
+
564
+ totalBreakdownEvents += (capitulatedUsersInScenario / totalUsers);
565
+ }
566
+
567
+ return totalBreakdownEvents / totalSims;
568
+ }
569
+ }
570
+
571
+ class Aggregators {
572
+ static bucketUsersByPnlPerAsset(usersData, tickerMap) {
573
+ const buckets = new Map();
574
+ for (const [userId, portfolio] of Object.entries(usersData)) {
575
+ const userType = portfolio.PublicPositions ? SCHEMAS.USER_TYPES.SPECULATOR : SCHEMAS.USER_TYPES.NORMAL;
576
+ const positions = DataExtractor.getPositions(portfolio, userType);
577
+
578
+ for (const pos of positions) {
579
+ const id = DataExtractor.getInstrumentId(pos);
580
+ const pnl = DataExtractor.getNetProfit(pos);
581
+ if (!id || pnl === 0) continue;
582
+
583
+ const ticker = tickerMap[id];
584
+ if (!ticker) continue;
585
+
586
+ if (!buckets.has(ticker)) buckets.set(ticker, { winners: [], losers: [] });
587
+ const b = buckets.get(ticker);
588
+
589
+ if (pnl > 0) b.winners.push(userId);
590
+ else b.losers.push(userId);
591
+ }
592
+ }
593
+ return Object.fromEntries(buckets);
594
+ }
595
+
596
+ static getWeightedSentiment(positions) {
597
+ if (!positions || positions.length === 0) return 0;
598
+
599
+ let totalWeightedPnL = 0;
600
+ let totalWeight = 0;
601
+
602
+ for (const pos of positions) {
603
+ const pnl = DataExtractor.getNetProfit(pos);
604
+ const weight = DataExtractor.getPositionWeight(pos);
605
+
606
+ if (weight > 0) {
607
+ totalWeightedPnL += (pnl * weight);
608
+ totalWeight += weight;
609
+ }
610
+ }
611
+
612
+ if (totalWeight === 0) return 0;
613
+ return totalWeightedPnL / totalWeight;
614
+ }
615
+ }
616
+
617
+ class Validators {
618
+ static validatePortfolio(portfolio, userType) {
619
+ if (!portfolio) return { valid: false, errors: ['Portfolio is null'] };
620
+
621
+ if (userType === SCHEMAS.USER_TYPES.SPECULATOR) {
622
+ if (!portfolio.PublicPositions) return { valid: false, errors: ['Missing PublicPositions'] };
623
+ } else {
624
+ if (!portfolio.AggregatedPositions) return { valid: false, errors: ['Missing AggregatedPositions'] };
625
+ }
626
+
627
+ return { valid: true, errors: [] };
628
+ }
629
+ }
630
+
631
+ class TimeSeries {
632
+ static updateEMAState(value, state, alpha = 0.1) {
633
+ const mean = state ? (state.mean || 0) : 0;
634
+ const variance = state ? (state.variance || 1) : 1;
635
+
636
+ if (value === undefined || value === null || isNaN(value)) {
637
+ return { mean, variance };
638
+ }
639
+
640
+ const diff = value - mean;
641
+ const newMean = mean + (alpha * diff);
642
+ const newVariance = (1 - alpha) * (variance + (alpha * diff * diff));
643
+
644
+ return { mean: newMean, variance: newVariance };
645
+ }
646
+
647
+ static pearsonCorrelation(x, y) {
648
+ if (!x || !y || x.length !== y.length || x.length === 0) return 0;
649
+
650
+ const n = x.length;
651
+ let sumX = 0, sumY = 0, sumXY = 0, sumX2 = 0, sumY2 = 0;
652
+
653
+ for (let i = 0; i < n; i++) {
654
+ sumX += x[i];
655
+ sumY += y[i];
656
+ sumXY += x[i] * y[i];
657
+ sumX2 += x[i] * x[i];
658
+ sumY2 += y[i] * y[i];
659
+ }
660
+
661
+ const numerator = (n * sumXY) - (sumX * sumY);
662
+ const denominator = Math.sqrt(((n * sumX2) - (sumX * sumX)) * ((n * sumY2) - (sumY * sumY)));
663
+
664
+ return (denominator === 0) ? 0 : numerator / denominator;
665
+ }
666
+ }
667
+
668
+
669
+ class DistributionAnalytics {
670
+ static computeKDE(data, bandwidth, steps = 60) {
671
+ if (!data || data.length === 0) return [];
672
+
673
+ let min = Infinity, max = -Infinity;
674
+ for (const p of data) {
675
+ if (p.value < min) min = p.value;
676
+ if (p.value > max) max = p.value;
677
+ }
678
+
679
+ min -= bandwidth * 3;
680
+ max += bandwidth * 3;
681
+ const stepSize = (max - min) / steps;
682
+ const curve = [];
683
+
684
+ for (let i = 0; i <= steps; i++) {
685
+ const x = min + (i * stepSize);
686
+ let density = 0;
687
+ for (const p of data) {
688
+ const diff = (x - p.value);
689
+ if (Math.abs(diff) > bandwidth * 3) continue;
690
+
691
+ const u = diff / bandwidth;
692
+ const k = 0.39894228 * Math.exp(-0.5 * u * u);
693
+ density += (p.weight * k) / bandwidth;
694
+ }
695
+ if (density > 0) curve.push({ price: x, density });
696
+ }
697
+ return curve;
698
+ }
699
+
700
+ static integrateProfile(curve, startPrice, endPrice) {
701
+ if (!curve || !Array.isArray(curve)) return 0; // Fix for potential crash
702
+ let sum = 0;
703
+ for (let i = 0; i < curve.length - 1; i++) {
704
+ const p1 = curve[i];
705
+ const p2 = curve[i+1];
706
+ if (p1.price >= startPrice && p2.price <= endPrice) {
707
+ sum += (p2.price - p1.price) * ((p1.density + p2.density) / 2);
708
+ }
709
+ }
710
+ return sum;
711
+ }
712
+
713
+ static linearRegression(xValues, yValues) {
714
+ const n = xValues.length;
715
+ if (n !== yValues.length || n < 2) return { slope: 0, r2: 0 };
716
+
717
+ let sumX = 0, sumY = 0, sumXY = 0, sumXX = 0, sumYY = 0;
718
+ for (let i = 0; i < n; i++) {
719
+ sumX += xValues[i];
720
+ sumY += yValues[i];
721
+ sumXY += xValues[i] * yValues[i];
722
+ sumXX += xValues[i] * xValues[i];
723
+ sumYY += yValues[i] * yValues[i];
724
+ }
725
+
726
+ const slope = (n * sumXY - sumX * sumY) / (n * sumXX - sumX * sumX);
727
+ return { slope, n };
728
+ }
729
+ }
730
+
731
+ module.exports = {
732
+ Aggregators,
733
+ DataExtractor,
734
+ DistributionAnalytics,
735
+ HistoryExtractor,
736
+ InsightsExtractor, // Exported
737
+ MathPrimitives,
738
+ SCHEMAS,
739
+ SignalPrimitives,
740
+ TimeSeries,
741
+ UserClassifier, // Exported
742
+ Validators,
743
+ priceExtractor
744
744
  };