bulltrackers-module 1.0.172 → 1.0.174

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.
@@ -11,7 +11,7 @@ const SCHEMAS = {
11
11
  USER_TYPES: { NORMAL: 'normal', SPECULATOR: 'speculator' }
12
12
  };
13
13
 
14
- class DataExtractor {
14
+ class DataExtractor { // For generic access of data types
15
15
  // ========================================================================
16
16
  // 1. COLLECTION ACCESSORS
17
17
  // ========================================================================
@@ -22,10 +22,10 @@ class DataExtractor {
22
22
  * - Speculator: Uses 'PublicPositions' (Individual Trades)
23
23
  */
24
24
  static getPositions(portfolio, userType) {
25
- if (!portfolio) return [];
25
+ if (!portfolio) return []; // Handle empty portfolio
26
26
 
27
- if (userType === SCHEMAS.USER_TYPES.SPECULATOR) {
28
- return portfolio.PublicPositions || [];
27
+ if (userType === SCHEMAS.USER_TYPES.SPECULATOR) {
28
+ return portfolio.PublicPositions || []; // SPECULATOR SCHEMA
29
29
  }
30
30
 
31
31
  // Default to Normal User Schema
@@ -40,7 +40,7 @@ class DataExtractor {
40
40
  * Extract standardized Instrument ID.
41
41
  */
42
42
  static getInstrumentId(position) {
43
- if (!position) return null;
43
+ if (!position) return null; // Handle empty position data
44
44
  // Handle string or number variations safely
45
45
  return position.InstrumentID || position.instrumentId || null;
46
46
  }
@@ -51,7 +51,7 @@ class DataExtractor {
51
51
  * - Normal: Generates Composite Key (InstrumentID_Direction) since they lack unique Trade IDs.
52
52
  */
53
53
  static getPositionId(position) {
54
- if (!position) return null;
54
+ if (!position) return null; // Handle empty position data
55
55
 
56
56
  // 1. Try Explicit ID (Speculators)
57
57
  if (position.PositionID) return String(position.PositionID);
@@ -59,7 +59,7 @@ class DataExtractor {
59
59
 
60
60
  // 2. Fallback to Composite Key (Normal Users)
61
61
  const instId = this.getInstrumentId(position);
62
- const dir = this.getDirection(position);
62
+ const dir = this.getDirection(position);
63
63
  if (instId) return `${instId}_${dir}`;
64
64
 
65
65
  return null;
@@ -73,7 +73,7 @@ class DataExtractor {
73
73
  * Extract Net Profit %.
74
74
  * Schema: 'NetProfit' is the percentage profit relative to invested capital.
75
75
  */
76
- static getNetProfit(position) {
76
+ static getNetProfit(position) {
77
77
  return position ? (position.NetProfit || 0) : 0;
78
78
  }
79
79
 
@@ -83,7 +83,7 @@ class DataExtractor {
83
83
  * - Normal: 'Invested' is % of initial capital.
84
84
  * - Speculator: 'Invested' (or 'Amount' in some contexts) is % of initial capital.
85
85
  */
86
- static getPositionWeight(position, userType) {
86
+ static getPositionWeight(position, userType) { // Agnostic on user type, unused.
87
87
  if (!position) return 0;
88
88
 
89
89
  // Both schemas use 'Invested' to represent the allocation percentage.
@@ -95,8 +95,23 @@ class DataExtractor {
95
95
  * Extract Current Equity Value %.
96
96
  * Schema: 'Value' is the current value as a % of total portfolio equity.
97
97
  */
98
- static getPositionValuePct(position) {
99
- return position ? (position.Value || 0) : 0;
98
+ static getPositionValuePct(position) { // TODO - VERIFY THIS WORKS FOR SPECULATORS,
99
+ return position ? (position.Value || 0) : 0; // IS VALUE ACTUALLY THE VALUE OF POSITION AS A % OF TOTAL PORTFOLIO EQUITY? IS IT THE SAME FOR NORMAL USERS?
100
+ }
101
+
102
+ /**
103
+ * --- NEW PRIMITIVE ---
104
+ * Derives the approximate Entry Price of a position based on Current Price and Net Profit %.
105
+ * Formula: Entry = Current / (1 + (NetProfit / 100))
106
+ * @param {number} currentPrice - The current market price of the asset.
107
+ * @param {number} netProfitPct - The Net Profit percentage (e.g., -20.5).
108
+ * @returns {number} Estimated Entry Price.
109
+ */
110
+ static deriveEntryPrice(currentPrice, netProfitPct) {
111
+ if (!currentPrice || currentPrice <= 0) return 0;
112
+ // Avoid division by zero if P&L is -100% (unlikely but possible in crypto/options)
113
+ if (netProfitPct <= -100) return Number.MAX_SAFE_INTEGER; // Effectively infinite entry price (lost everything)
114
+ return currentPrice / (1 + (netProfitPct / 100.0));
100
115
  }
101
116
 
102
117
  // ========================================================================
@@ -109,12 +124,13 @@ class DataExtractor {
109
124
  static getPortfolioDailyPnl(portfolio, userType) {
110
125
  if (!portfolio) return 0;
111
126
 
112
- // 1. Speculator (Explicit 'NetProfit' field on root)
113
- if (userType === SCHEMAS.USER_TYPES.SPECULATOR) {
114
- return portfolio.NetProfit || 0;
127
+ // 1. Speculator (Explicit 'NetProfit' field on root) ---> TODO : THIS IS FLAWED,
128
+ if (userType === SCHEMAS.USER_TYPES.SPECULATOR) { // SPECULATOR DATA DOES NOT CONTAIN THE ENTIRE PORTFOLIO DATA FOR THAT USER, IT CONTAINS A SNAPSHOT OF THE USERS' SPECIFIC ASSET WE REQUESTED FROM THE TASK ENGINE
129
+ // You cannot ever infer the entire speculator portfolio daily / weekly / monthly or any time period value based on the data we have for speculators
130
+ return portfolio.NetProfit || 0; // Data for speculators only works when we are looking at the ticker-specific data, or the trade history data which is agnostic on user type
115
131
  }
116
132
 
117
- // 2. Normal (Aggregated Calculation)
133
+ // 2. Normal (Aggregated Calculation) ---> TODO : VERIFY THIS LOGIC, IT SHOULD BE OK
118
134
  // Logic: Sum(Value - Invested) across the 'InstrumentType' breakdown
119
135
  // This gives the net performance change for the day relative to the portfolio.
120
136
  if (portfolio.AggregatedPositionsByInstrumentTypeID) {
@@ -138,27 +154,36 @@ class DataExtractor {
138
154
  }
139
155
 
140
156
  static getLeverage(position) {
141
- return position ? (position.Leverage || 1) : 1;
157
+ return position ? (position.Leverage || 1) : 1; // Default 1 IF NOT FOUND
142
158
  }
143
159
 
144
160
  static getOpenRate(position) {
145
- return position ? (position.OpenRate || 0) : 0;
161
+ return position ? (position.OpenRate || 0) : 0; // Default 0 IF NOT FOUND
146
162
  }
147
163
 
148
164
  static getCurrentRate(position) {
149
- return position ? (position.CurrentRate || 0) : 0;
165
+ return position ? (position.CurrentRate || 0) : 0; // Default 0 IF NOT FOUND
150
166
  }
151
167
 
152
168
  static getStopLossRate(position) {
153
- return position ? (position.StopLossRate || 0) : 0;
154
- }
169
+ const rate = position ? (position.StopLossRate || 0) : 0;
170
+ // Fix for eToro bug: SL disabled positions can return 0.01 or similar small values.
171
+ // If the rate is positive but extremely small (<= 0.01), treat as 0 (disabled).
172
+ if (rate > 0 && rate <= 0.01) return 0; // Normalizes bug value to 0
173
+ if (rate < 0) return 0;
174
+ return rate;
175
+ }
155
176
 
156
177
  static getTakeProfitRate(position) {
157
- return position ? (position.TakeProfitRate || 0) : 0;
178
+ const rate = position ? (position.TakeProfitRate || 0) : 0;
179
+ // Fix for eToro bug: TP disabled positions can return INF or small values.
180
+ // If the rate is positive but extremely small (<= 0.01), treat as 0 (disabled).
181
+ if (rate > 0 && rate <= 0.01) return 0; // Normalizes bug value to 0
182
+ return rate;
158
183
  }
159
184
 
160
185
  static getHasTSL(position) {
161
- return position ? (position.HasTrailingStopLoss === true) : false;
186
+ return position ? (position.HasTrailingStopLoss === true) : false; // Default false IF NOT FOUND
162
187
  }
163
188
 
164
189
  /**
@@ -166,11 +191,76 @@ class DataExtractor {
166
191
  * Used for bagholder calculations.
167
192
  */
168
193
  static getOpenDateTime(position) {
169
- if (!position || !position.OpenDateTime) return null;
194
+ if (!position || !position.OpenDateTime) return null;
170
195
  return new Date(position.OpenDateTime);
171
196
  }
172
197
  }
173
198
 
199
+ /**
200
+ * --- NEW CLASS: priceExtractor ---
201
+ * Handles schema-aware extraction of asset price history.
202
+ * Mitigates typo risk by centralizing access to context.prices.
203
+ */
204
+ class priceExtractor {
205
+ /**
206
+ * Retrieves the sorted price history for a specific instrument.
207
+ * @param {object} pricesContext - The global context.prices object.
208
+ * @param {string|number} tickerOrId - The Ticker or InstrumentID to fetch.
209
+ * @returns {Array<{date: string, price: number}>} Sorted array of price objects.
210
+ */
211
+ static getHistory(pricesContext, tickerOrId) {
212
+ if (!pricesContext || !pricesContext.history) return [];
213
+
214
+ // The history map is keyed by InstrumentID (e.g., "10000")
215
+ // We iterate to find the entry matching the request (by ID or Ticker)
216
+ // Optimization: If input is ID, direct lookup. If ticker, search.
217
+
218
+ let assetData = pricesContext.history[tickerOrId];
219
+
220
+ // Fallback: If direct lookup failed, maybe it's a ticker symbol?
221
+ if (!assetData) {
222
+ const id = Object.keys(pricesContext.history).find(key => {
223
+ const data = pricesContext.history[key];
224
+ return data.ticker === tickerOrId;
225
+ });
226
+ if (id) assetData = pricesContext.history[id];
227
+ }
228
+
229
+ if (!assetData || !assetData.prices) return [];
230
+
231
+ // Convert Map<Date, Price> to Array<{date, price}> and sort
232
+ const priceMap = assetData.prices;
233
+ const sortedDates = Object.keys(priceMap).sort((a, b) => a.localeCompare(b));
234
+
235
+ return sortedDates.map(date => ({
236
+ date: date,
237
+ price: priceMap[date]
238
+ })).filter(item => item.price > 0);
239
+ }
240
+
241
+ /**
242
+ * Returns all available assets with their histories.
243
+ * Useful for computations iterating over the entire market.
244
+ * @param {object} pricesContext - The global context.prices object.
245
+ * @returns {Map<string, Array<{date: string, price: number}>>} Map<Ticker, HistoryArray>
246
+ */
247
+ static getAllHistories(pricesContext) {
248
+ if (!pricesContext || !pricesContext.history) return new Map();
249
+
250
+ const results = new Map();
251
+ for (const [id, data] of Object.entries(pricesContext.history)) {
252
+ const ticker = data.ticker || id;
253
+ // Reuse the single-asset logic for consistency, though slightly less efficient
254
+ // than inlining the sort. For safety/DRY, we reuse.
255
+ const history = this.getHistory(pricesContext, id);
256
+ if (history.length > 0) {
257
+ results.set(ticker, history);
258
+ }
259
+ }
260
+ return results;
261
+ }
262
+ }
263
+
174
264
  class HistoryExtractor {
175
265
  // --- Schema Accessor (NEW) ---
176
266
  /**
@@ -187,23 +277,23 @@ class HistoryExtractor {
187
277
  return historyDoc.assets;
188
278
  }
189
279
 
190
- static getInstrumentId(asset) {
280
+ static getInstrumentId(asset) {
191
281
  return asset ? asset.instrumentId : null;
192
282
  }
193
283
 
194
- static getAvgHoldingTimeMinutes(asset) {
284
+ static getAvgHoldingTimeMinutes(asset) { // Note, in minutes, we could convert values here into hours or days but we leave as-is for now.
195
285
  return asset ? (asset.avgHoldingTimeInMinutes || 0) : 0;
196
286
  }
197
287
 
198
- static getSummary(historyDoc) {
288
+ static getSummary(historyDoc) { // This returns the top-level summary of trade history
199
289
  const all = historyDoc?.all;
200
290
  if (!all) return null;
201
291
 
202
- return {
203
- totalTrades: all.totalTrades || 0,
204
- winRatio: all.winRatio || 0,
205
- avgProfitPct: all.avgProfitPct || 0,
206
- avgLossPct: all.avgLossPct || 0,
292
+ return { // The all object contains instrumentid of -1 value, we do not include this, it's a junk backend-eToro placeholder.
293
+ totalTrades: all.totalTrades || 0,
294
+ winRatio: all.winRatio || 0,
295
+ avgProfitPct: all.avgProfitPct || 0,
296
+ avgLossPct: all.avgLossPct || 0,
207
297
  avgHoldingTimeInMinutes: all.avgHoldingTimeInMinutes || 0
208
298
  };
209
299
  }
@@ -242,7 +332,7 @@ class SignalPrimitives {
242
332
  * Hyperbolic Tangent Normalization.
243
333
  * Maps inputs to a strict -Scale to +Scale range.
244
334
  */
245
- static normalizeTanh(value, scale = 10, sensitivity = 10.0) {
335
+ static normalizeTanh(value, scale = 10, sensitivity = 10.0) { // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/tanh
246
336
  if (value === 0) return 0;
247
337
  return Math.tanh(value / sensitivity) * scale;
248
338
  }
@@ -250,7 +340,7 @@ class SignalPrimitives {
250
340
  /**
251
341
  * Standard Z-Score normalization.
252
342
  */
253
- static normalizeZScore(value, mean, stdDev) {
343
+ static normalizeZScore(value, mean, stdDev) { // https://gist.github.com/Restuta/5cbf1d186f17febe5e899febb63e1b86
254
344
  if (!stdDev || stdDev === 0) return 0;
255
345
  return (value - mean) / stdDev;
256
346
  }
@@ -261,6 +351,18 @@ class SignalPrimitives {
261
351
  static divergence(valueA, valueB) {
262
352
  return (valueA || 0) - (valueB || 0);
263
353
  }
354
+
355
+ static getPreviousState(previousComputed, calcName, ticker, fieldName = null) { // This is used for either fetching computations listed in getdependencies() OR self-history
356
+ if (!previousComputed || !previousComputed[calcName]) return null; // Using this for self-history DOES NOT cause a circular dependency because we assign a special rule in orchestration_helpers
357
+ // Which handles the self-reference, see 2. SMART SELF-FETCH in orchestration_helpers
358
+ const tickerData = previousComputed[calcName][ticker];
359
+ if (!tickerData) return null;
360
+
361
+ if (fieldName) {
362
+ return tickerData[fieldName];
363
+ }
364
+ return tickerData; // Return whole object (useful for _state)
365
+ }
264
366
  }
265
367
 
266
368
  class MathPrimitives {
@@ -288,6 +390,146 @@ class MathPrimitives {
288
390
  static bucketBinary(value, threshold = 0) {
289
391
  return value > threshold ? 'winner' : 'loser';
290
392
  }
393
+
394
+ /**
395
+ * Calculates the probability of an asset hitting a specific price barrier (Stop Loss/Take Profit)
396
+ * within a given timeframe using the First Passage Time of Geometric Brownian Motion.
397
+ * * Formula: P(T < t) = Φ((b - vt) / σ√t) + exp(2vb/σ²) * Φ((b + vt) / σ√t)
398
+ * Where:
399
+ * b = ln(Barrier/Price)
400
+ * v = drift - 0.5 * volatility^2
401
+ * * @param {number} currentPrice - The current price of the asset
402
+ * @param {number} barrierPrice - The target price (SL or TP)
403
+ * @param {number} volatility - Annualized volatility (e.g., 0.40 for 40%)
404
+ * @param {number} days - Number of days to forecast (e.g., 3)
405
+ * @param {number} drift - (Optional) Annualized drift. Default 0 (Risk Neutral).
406
+ * @returns {number} Probability (0.0 to 1.0)
407
+ */
408
+ static calculateHitProbability(currentPrice, barrierPrice, volatility, days, drift = 0) { // https://www.ma.ic.ac.uk/~bin06/M3A22/m3f22chVII.pdf
409
+ if (currentPrice <= 0 || barrierPrice <= 0 || volatility <= 0 || days <= 0) return 0;
410
+
411
+ const t = days / 365.0; // Convert days to years
412
+ const sigma = volatility;
413
+ const mu = drift;
414
+
415
+ // The barrier in log-space
416
+ const b = Math.log(barrierPrice / currentPrice);
417
+
418
+ // Adjusted drift (nu)
419
+ const nu = mu - 0.5 * Math.pow(sigma, 2);
420
+
421
+ const sqrtT = Math.sqrt(t);
422
+ const sigmaSqrtT = sigma * sqrtT;
423
+
424
+ // Helper for Standard Normal CDF (Φ)
425
+ const normCDF = (x) => {
426
+ const t = 1 / (1 + 0.2316419 * Math.abs(x));
427
+ const d = 0.3989423 * Math.exp(-x * x / 2);
428
+ const prob = d * t * (0.3193815 + t * (-0.3565638 + t * (1.781478 + t * (-1.821256 + t * 1.330274))));
429
+ return x > 0 ? 1 - prob : prob;
430
+ };
431
+
432
+ // Standard First Passage Time Formula parts
433
+ const term1 = (b - nu * t) / sigmaSqrtT;
434
+ const term2 = (2 * nu * b) / (sigma * sigma);
435
+ const term3 = (b + nu * t) / sigmaSqrtT;
436
+
437
+ // If barrier is below price (Stop Loss for Long), b is negative.
438
+ // If barrier is above price (Take Profit for Long), we flip the logic essentially,
439
+ // but the formula works for the distance.
440
+ // However, for strict GBM hitting time, we usually treat 'b' as the distance.
441
+ // For this implementation, we check direction relative to barrier.
442
+
443
+ // If we are already at or past the barrier, probability is 100%
444
+ if ((currentPrice > barrierPrice && barrierPrice > currentPrice) ||
445
+ (currentPrice < barrierPrice && barrierPrice < currentPrice)) {
446
+ return 1.0;
447
+ }
448
+
449
+ // Calculate Probability
450
+ // Note: If nu is 0, the second term simplifies significantly, but we keep full form.
451
+ const probability = normCDF(( -Math.abs(b) - nu * t ) / sigmaSqrtT) +
452
+ Math.exp((2 * nu * Math.abs(b)) / (sigma * sigma)) * normCDF(( -Math.abs(b) + nu * t ) / sigmaSqrtT);
453
+
454
+ return Math.min(Math.max(probability, 0), 1);
455
+ }
456
+
457
+ /**
458
+ * --- NEW PRIMITIVE ---
459
+ * Simulates future price paths using Geometric Brownian Motion (Monte Carlo).
460
+ * Used for testing portfolio resilience against potential market moves.
461
+ * @param {number} currentPrice - S0
462
+ * @param {number} volatility - Annualized volatility (sigma)
463
+ * @param {number} days - Time horizon in days (t)
464
+ * @param {number} simulations - Number of paths to generate (e.g., 1000)
465
+ * @param {number} drift - Annualized drift (mu), default 0
466
+ * @returns {Float32Array} Array of simulated end prices
467
+ */
468
+ static simulateGBM(currentPrice, volatility, days, simulations = 1000, drift = 0) {
469
+ if (currentPrice <= 0 || volatility <= 0 || days <= 0) return new Float32Array(0);
470
+
471
+ const t = days / 365.0;
472
+ const sigma = volatility;
473
+ const mu = drift;
474
+ const driftTerm = (mu - 0.5 * sigma * sigma) * t;
475
+ const volTerm = sigma * Math.sqrt(t);
476
+
477
+ // Use Float32Array for memory efficiency with large simulation counts
478
+ const results = new Float32Array(simulations);
479
+
480
+ for (let i = 0; i < simulations; i++) {
481
+ // Box-Muller transform for efficient standard normal distribution generation
482
+ const u1 = Math.random();
483
+ const u2 = Math.random();
484
+ const z = Math.sqrt(-2.0 * Math.log(u1)) * Math.cos(2.0 * Math.PI * u2);
485
+
486
+ // GBM Formula: St = S0 * exp((mu - 0.5*sigma^2)t + sigma*Wt)
487
+ results[i] = currentPrice * Math.exp(driftTerm + volTerm * z);
488
+ }
489
+ return results;
490
+ }
491
+
492
+ /**
493
+ * --- NEW PRIMITIVE ---
494
+ * Simulates "Population Breakdown" (Capitulation) Risk.
495
+ * Correlates simulated price drops with user pain thresholds.
496
+ * @param {Float32Array} pricePaths - Array of simulated prices (from simulateGBM).
497
+ * @param {Array<{entryPrice: number, thresholdPct: number}>} userProfiles - Array of user states.
498
+ * @returns {number} Expected % of population that capitulates (0.0 - 1.0).
499
+ */
500
+ static simulatePopulationBreakdown(pricePaths, userProfiles) {
501
+ if (!pricePaths.length || !userProfiles.length) return 0;
502
+
503
+ let totalBreakdownEvents = 0;
504
+ const totalSims = pricePaths.length;
505
+ const totalUsers = userProfiles.length;
506
+
507
+ // For each simulated future price scenario...
508
+ for (let i = 0; i < totalSims; i++) {
509
+ const simPrice = pricePaths[i];
510
+ let capitulatedUsersInScenario = 0;
511
+
512
+ // ...check every user to see if they survive
513
+ for (let j = 0; j < totalUsers; j++) {
514
+ const user = userProfiles[j];
515
+ // Calculate hypothetical P&L for this user in this scenario
516
+ // P&L% = (CurrentValue - EntryValue) / EntryValue
517
+ const hypotheticalPnL = ((simPrice - user.entryPrice) / user.entryPrice) * 100;
518
+
519
+ // If hypothetical P&L is worse (lower) than their historical pain threshold, they capitulate.
520
+ // Note: thresholdPct is typically negative (e.g., -15.0)
521
+ if (hypotheticalPnL < user.thresholdPct) {
522
+ capitulatedUsersInScenario++;
523
+ }
524
+ }
525
+
526
+ // Add the % of users who broke in this scenario to the accumulator
527
+ totalBreakdownEvents += (capitulatedUsersInScenario / totalUsers);
528
+ }
529
+
530
+ // Return the average capitulation rate across all simulations
531
+ return totalBreakdownEvents / totalSims;
532
+ }
291
533
  }
292
534
 
293
535
  class Aggregators {
@@ -295,11 +537,11 @@ class Aggregators {
295
537
  * Helper to bucket users by P&L Status.
296
538
  * Used by legacy systems or specific aggregators.
297
539
  */
298
- static bucketUsersByPnlPerAsset(usersData, tickerMap) {
540
+ static bucketUsersByPnlPerAsset(usersData, tickerMap) { // https://www.geeksforgeeks.org/javascript/bucket-sort-visualization-using-javascript/
299
541
  const buckets = new Map();
300
542
  for (const [userId, portfolio] of Object.entries(usersData)) {
301
- // Auto-detect type if not provided (Legacy compatibility)
302
- const userType = portfolio.PublicPositions ? SCHEMAS.USER_TYPES.SPECULATOR : SCHEMAS.USER_TYPES.NORMAL;
543
+ // Auto-detect type if not provided (Legacy compatibility) TODO : We do not need legacy compatability, legacy computations do not run.
544
+ const userType = portfolio.PublicPositions ? SCHEMAS.USER_TYPES.SPECULATOR : SCHEMAS.USER_TYPES.NORMAL;
303
545
  const positions = DataExtractor.getPositions(portfolio, userType);
304
546
 
305
547
  for (const pos of positions) {
@@ -319,12 +561,42 @@ class Aggregators {
319
561
  }
320
562
  return Object.fromEntries(buckets);
321
563
  }
564
+
565
+ /**
566
+ * --- NEW PRIMITIVE ---
567
+ * Calculates Weighted Sentiment (Avg P&L) for a set of positions.
568
+ * Solves "Dirty Data" / Variable N issue by weighting by investment size.
569
+ * @param {Array} positions - Array of raw position objects.
570
+ * @returns {number} Weighted Average NetProfit %.
571
+ */
572
+ static getWeightedSentiment(positions) {
573
+ if (!positions || positions.length === 0) return 0;
574
+
575
+ let totalWeightedPnL = 0;
576
+ let totalWeight = 0;
577
+
578
+ for (const pos of positions) {
579
+ // Use DataExtractor to be safe and schema-agnostic
580
+ const pnl = DataExtractor.getNetProfit(pos);
581
+ const weight = DataExtractor.getPositionWeight(pos); // 'Invested' or 'Amount'
582
+
583
+ if (weight > 0) {
584
+ totalWeightedPnL += (pnl * weight);
585
+ totalWeight += weight;
586
+ }
587
+ }
588
+
589
+ if (totalWeight === 0) return 0;
590
+ return totalWeightedPnL / totalWeight;
591
+ }
322
592
  }
323
593
 
594
+ // Validation layer -- Used to validate the data incoming
324
595
  class Validators {
325
596
  static validatePortfolio(portfolio, userType) {
326
597
  if (!portfolio) return { valid: false, errors: ['Portfolio is null'] };
327
598
 
599
+ // Handle both types of schema
328
600
  if (userType === SCHEMAS.USER_TYPES.SPECULATOR) {
329
601
  if (!portfolio.PublicPositions) return { valid: false, errors: ['Missing PublicPositions'] };
330
602
  } else {
@@ -335,12 +607,141 @@ class Validators {
335
607
  }
336
608
  }
337
609
 
338
- module.exports = {
339
- SCHEMAS,
340
- DataExtractor,
341
- HistoryExtractor,
342
- MathPrimitives,
343
- Aggregators,
344
- Validators,
345
- SignalPrimitives
610
+ class TimeSeries {
611
+ /**
612
+ * Updates a Rolling Mean and Variance using Welford's Online Algorithm (EMA variant). // https://jonisalonen.com/2013/deriving-welfords-method-for-computing-variance/
613
+ * @param {number} value - New data point.
614
+ * @param {Object} state - { mean: number, variance: number }
615
+ * @param {number} alpha - Decay factor (e.g., 0.1 for ~20 days).
616
+ */
617
+ static updateEMAState(value, state, alpha = 0.1) {
618
+ const mean = state ? (state.mean || 0) : 0;
619
+ const variance = state ? (state.variance || 1) : 1; // Default variance 1 to avoid div/0
620
+
621
+ if (value === undefined || value === null || isNaN(value)) {
622
+ return { mean, variance };
623
+ }
624
+
625
+ // EMA Update for Mean
626
+ const diff = value - mean;
627
+ const newMean = mean + (alpha * diff);
628
+
629
+ // EMA Update for Variance
630
+ const newVariance = (1 - alpha) * (variance + (alpha * diff * diff));
631
+
632
+ return { mean: newMean, variance: newVariance };
633
+ }
634
+
635
+ /**
636
+ * Calculates Pearson Correlation between two arrays. https://gist.github.com/matt-west/6500993
637
+ */
638
+ static pearsonCorrelation(x, y) {
639
+ if (!x || !y || x.length !== y.length || x.length === 0) return 0;
640
+
641
+ const n = x.length;
642
+ // Simple sums
643
+ let sumX = 0, sumY = 0, sumXY = 0, sumX2 = 0, sumY2 = 0;
644
+
645
+ for (let i = 0; i < n; i++) {
646
+ sumX += x[i];
647
+ sumY += y[i];
648
+ sumXY += x[i] * y[i];
649
+ sumX2 += x[i] * x[i];
650
+ sumY2 += y[i] * y[i];
651
+ }
652
+
653
+ const numerator = (n * sumXY) - (sumX * sumY);
654
+ const denominator = Math.sqrt(((n * sumX2) - (sumX * sumX)) * ((n * sumY2) - (sumY * sumY)));
655
+
656
+ return (denominator === 0) ? 0 : numerator / denominator;
657
+ }
658
+ }
659
+
660
+
661
+ class DistributionAnalytics {
662
+ /**
663
+ * Gaussian Kernel Density Estimation (KDE)
664
+ * Converts discrete price points into a continuous probability density curve.
665
+ * Optimized for memory: accepts pre-binned data.
666
+ * @param {Array<{value: number, weight: number}>} data - Points or Bins.
667
+ * @param {number} bandwidth - Smoothing factor (h).
668
+ * @param {number} steps - Resolution of the output curve.
669
+ */
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
+ // Pad range to capture tails
680
+ min -= bandwidth * 3;
681
+ max += bandwidth * 3;
682
+ const stepSize = (max - min) / steps;
683
+ const curve = [];
684
+
685
+ for (let i = 0; i <= steps; i++) {
686
+ const x = min + (i * stepSize);
687
+ let density = 0;
688
+ // Vectorized-like summation https://cvw.cac.cornell.edu/vector/intro/how-vector-works#:~:text=Vectorization%20is%20a%20process%20by,performance%20increases%20obtained%20by%20vectorization.
689
+ for (const p of data) {
690
+ const diff = (x - p.value);
691
+ // Optimization: Skip calculation for points too far away (> 3 std devs)
692
+ if (Math.abs(diff) > bandwidth * 3) continue;
693
+
694
+ const u = diff / bandwidth;
695
+ const k = 0.39894228 * Math.exp(-0.5 * u * u); // Standard Normal PDF
696
+ density += (p.weight * k) / bandwidth;
697
+ }
698
+ if (density > 0) curve.push({ price: x, density });
699
+ }
700
+ return curve;
701
+ }
702
+
703
+ static integrateProfile(curve, startPrice, endPrice) {
704
+ let sum = 0;
705
+ for (let i = 0; i < curve.length - 1; i++) {
706
+ const p1 = curve[i];
707
+ const p2 = curve[i+1];
708
+ if (p1.price >= startPrice && p2.price <= endPrice) {
709
+ // Trapezoidal Rule https://www.khanacademy.org/math/ap-calculus-ab/ab-integration-new/ab-6-2/a/understanding-the-trapezoid-rule
710
+ sum += (p2.price - p1.price) * ((p1.density + p2.density) / 2);
711
+ }
712
+ }
713
+ return sum;
714
+ }
715
+
716
+ static linearRegression(xValues, yValues) { // https://hazlo.medium.com/linear-regression-from-scratch-in-js-first-foray-into-ml-for-web-developers-867cfcae8fde
717
+ const n = xValues.length;
718
+ if (n !== yValues.length || n < 2) return { slope: 0, r2: 0 };
719
+
720
+ let sumX = 0, sumY = 0, sumXY = 0, sumXX = 0, sumYY = 0;
721
+ for (let i = 0; i < n; i++) {
722
+ sumX += xValues[i];
723
+ sumY += yValues[i];
724
+ sumXY += xValues[i] * yValues[i];
725
+ sumXX += xValues[i] * xValues[i];
726
+ sumYY += yValues[i] * yValues[i];
727
+ }
728
+
729
+ const slope = (n * sumXY - sumX * sumY) / (n * sumXX - sumX * sumX);
730
+ return { slope, n };
731
+ }
732
+ }
733
+
734
+
735
+ // This block is dynamically generated. Do not edit manually.
736
+ module.exports = {
737
+ Aggregators,
738
+ DataExtractor,
739
+ DistributionAnalytics,
740
+ HistoryExtractor,
741
+ MathPrimitives,
742
+ SCHEMAS,
743
+ SignalPrimitives,
744
+ TimeSeries,
745
+ Validators,
746
+ priceExtractor
346
747
  };
package/index.js CHANGED
@@ -36,7 +36,7 @@ const { build: buildManifestFunc } = require('./functions/computation-system/hel
36
36
  const computationSystem = { runComputationPass : require('./functions/computation-system/helpers/computation_pass_runner') .runComputationPass,
37
37
  dataLoader : require('./functions/computation-system/utils/data_loader'),
38
38
  computationUtils : require('./functions/computation-system/utils/utils'),
39
- buildManifest : buildManifestFunc // <--- NEW PIPE EXPORT
39
+ buildManifest : buildManifestFunc
40
40
  };
41
41
 
42
42
  // API
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bulltrackers-module",
3
- "version": "1.0.172",
3
+ "version": "1.0.174",
4
4
  "description": "Helper Functions for Bulltrackers.",
5
5
  "main": "index.js",
6
6
  "files": [