bulltrackers-module 1.0.171 → 1.0.172

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.
@@ -0,0 +1,346 @@
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 for Normal vs Speculator portfolios.
6
+ * - standardizes access to P&L, Weights, and Rates.
7
+ * - Provides safe fallbacks for all fields.
8
+ */
9
+
10
+ const SCHEMAS = {
11
+ USER_TYPES: { NORMAL: 'normal', SPECULATOR: 'speculator' }
12
+ };
13
+
14
+ class DataExtractor {
15
+ // ========================================================================
16
+ // 1. COLLECTION ACCESSORS
17
+ // ========================================================================
18
+
19
+ /**
20
+ * Extract positions array based on User Type.
21
+ * - Normal: Uses 'AggregatedPositions' (Grouped by Asset + Direction)
22
+ * - Speculator: Uses 'PublicPositions' (Individual Trades)
23
+ */
24
+ static getPositions(portfolio, userType) {
25
+ if (!portfolio) return [];
26
+
27
+ if (userType === SCHEMAS.USER_TYPES.SPECULATOR) {
28
+ return portfolio.PublicPositions || [];
29
+ }
30
+
31
+ // Default to Normal User Schema
32
+ return portfolio.AggregatedPositions || [];
33
+ }
34
+
35
+ // ========================================================================
36
+ // 2. IDENTITY & KEYS
37
+ // ========================================================================
38
+
39
+ /**
40
+ * Extract standardized Instrument ID.
41
+ */
42
+ static getInstrumentId(position) {
43
+ if (!position) return null;
44
+ // Handle string or number variations safely
45
+ return position.InstrumentID || position.instrumentId || null;
46
+ }
47
+
48
+ /**
49
+ * Extract a unique Identifier for the position.
50
+ * - Speculator: Uses 'PositionID'.
51
+ * - Normal: Generates Composite Key (InstrumentID_Direction) since they lack unique Trade IDs.
52
+ */
53
+ static getPositionId(position) {
54
+ if (!position) return null;
55
+
56
+ // 1. Try Explicit ID (Speculators)
57
+ if (position.PositionID) return String(position.PositionID);
58
+ if (position.PositionId) return String(position.PositionId);
59
+
60
+ // 2. Fallback to Composite Key (Normal Users)
61
+ const instId = this.getInstrumentId(position);
62
+ const dir = this.getDirection(position);
63
+ if (instId) return `${instId}_${dir}`;
64
+
65
+ return null;
66
+ }
67
+
68
+ // ========================================================================
69
+ // 3. FINANCIAL METRICS (WEIGHTS & P&L)
70
+ // ========================================================================
71
+
72
+ /**
73
+ * Extract Net Profit %.
74
+ * Schema: 'NetProfit' is the percentage profit relative to invested capital.
75
+ */
76
+ static getNetProfit(position) {
77
+ return position ? (position.NetProfit || 0) : 0;
78
+ }
79
+
80
+ /**
81
+ * Extract Position Weight (Allocation %).
82
+ * Schema:
83
+ * - Normal: 'Invested' is % of initial capital.
84
+ * - Speculator: 'Invested' (or 'Amount' in some contexts) is % of initial capital.
85
+ */
86
+ static getPositionWeight(position, userType) {
87
+ if (!position) return 0;
88
+
89
+ // Both schemas use 'Invested' to represent the allocation percentage.
90
+ // Speculators might optionally have 'Amount', we prioritize 'Invested' for consistency.
91
+ return position.Invested || position.Amount || 0;
92
+ }
93
+
94
+ /**
95
+ * Extract Current Equity Value %.
96
+ * Schema: 'Value' is the current value as a % of total portfolio equity.
97
+ */
98
+ static getPositionValuePct(position) {
99
+ return position ? (position.Value || 0) : 0;
100
+ }
101
+
102
+ // ========================================================================
103
+ // 4. PORTFOLIO LEVEL SUMMARY
104
+ // ========================================================================
105
+
106
+ /**
107
+ * Calculate/Extract Daily Portfolio P&L %.
108
+ */
109
+ static getPortfolioDailyPnl(portfolio, userType) {
110
+ if (!portfolio) return 0;
111
+
112
+ // 1. Speculator (Explicit 'NetProfit' field on root)
113
+ if (userType === SCHEMAS.USER_TYPES.SPECULATOR) {
114
+ return portfolio.NetProfit || 0;
115
+ }
116
+
117
+ // 2. Normal (Aggregated Calculation)
118
+ // Logic: Sum(Value - Invested) across the 'InstrumentType' breakdown
119
+ // This gives the net performance change for the day relative to the portfolio.
120
+ if (portfolio.AggregatedPositionsByInstrumentTypeID) {
121
+ return portfolio.AggregatedPositionsByInstrumentTypeID.reduce((sum, agg) => {
122
+ return sum + ((agg.Value || 0) - (agg.Invested || 0));
123
+ }, 0);
124
+ }
125
+
126
+ return 0;
127
+ }
128
+
129
+ // ========================================================================
130
+ // 5. TRADE DETAILS (SPECULATOR SPECIFIC)
131
+ // ========================================================================
132
+
133
+ static getDirection(position) {
134
+ if (!position) return "Buy";
135
+ if (position.Direction) return position.Direction;
136
+ if (typeof position.IsBuy === 'boolean') return position.IsBuy ? "Buy" : "Sell";
137
+ return "Buy"; // Default
138
+ }
139
+
140
+ static getLeverage(position) {
141
+ return position ? (position.Leverage || 1) : 1;
142
+ }
143
+
144
+ static getOpenRate(position) {
145
+ return position ? (position.OpenRate || 0) : 0;
146
+ }
147
+
148
+ static getCurrentRate(position) {
149
+ return position ? (position.CurrentRate || 0) : 0;
150
+ }
151
+
152
+ static getStopLossRate(position) {
153
+ return position ? (position.StopLossRate || 0) : 0;
154
+ }
155
+
156
+ static getTakeProfitRate(position) {
157
+ return position ? (position.TakeProfitRate || 0) : 0;
158
+ }
159
+
160
+ static getHasTSL(position) {
161
+ return position ? (position.HasTrailingStopLoss === true) : false;
162
+ }
163
+
164
+ /**
165
+ * Extract Open Date Time.
166
+ * Used for bagholder calculations.
167
+ */
168
+ static getOpenDateTime(position) {
169
+ if (!position || !position.OpenDateTime) return null;
170
+ return new Date(position.OpenDateTime);
171
+ }
172
+ }
173
+
174
+ class HistoryExtractor {
175
+ // --- Schema Accessor (NEW) ---
176
+ /**
177
+ * Extracts the daily history snapshot from the User object.
178
+ * This decouples the computation from knowing 'user.history.today'.
179
+ */
180
+ static getDailyHistory(user) {
181
+ return user?.history?.today || null;
182
+ }
183
+
184
+ // --- Data Extractors ---
185
+ static getTradedAssets(historyDoc) {
186
+ if (!historyDoc || !Array.isArray(historyDoc.assets)) return [];
187
+ return historyDoc.assets;
188
+ }
189
+
190
+ static getInstrumentId(asset) {
191
+ return asset ? asset.instrumentId : null;
192
+ }
193
+
194
+ static getAvgHoldingTimeMinutes(asset) {
195
+ return asset ? (asset.avgHoldingTimeInMinutes || 0) : 0;
196
+ }
197
+
198
+ static getSummary(historyDoc) {
199
+ const all = historyDoc?.all;
200
+ if (!all) return null;
201
+
202
+ return {
203
+ totalTrades: all.totalTrades || 0,
204
+ winRatio: all.winRatio || 0,
205
+ avgProfitPct: all.avgProfitPct || 0,
206
+ avgLossPct: all.avgLossPct || 0,
207
+ avgHoldingTimeInMinutes: all.avgHoldingTimeInMinutes || 0
208
+ };
209
+ }
210
+ }
211
+
212
+ class SignalPrimitives {
213
+ /**
214
+ * Safely extracts a specific numeric field for a specific ticker from a dependency.
215
+ */
216
+ static getMetric(dependencies, calcName, ticker, fieldName, fallback = 0) {
217
+ if (!dependencies || !dependencies[calcName]) return fallback;
218
+ const tickerData = dependencies[calcName][ticker];
219
+ if (!tickerData) return fallback;
220
+
221
+ const val = tickerData[fieldName];
222
+ return (typeof val === 'number') ? val : fallback;
223
+ }
224
+
225
+ /**
226
+ * Creates a unified Set of all keys (tickers) present across multiple dependency results.
227
+ */
228
+ static getUnionKeys(dependencies, calcNames) {
229
+ const keys = new Set();
230
+ if (!dependencies) return [];
231
+
232
+ for (const name of calcNames) {
233
+ const resultObj = dependencies[name];
234
+ if (resultObj && typeof resultObj === 'object') {
235
+ Object.keys(resultObj).forEach(k => keys.add(k));
236
+ }
237
+ }
238
+ return Array.from(keys);
239
+ }
240
+
241
+ /**
242
+ * Hyperbolic Tangent Normalization.
243
+ * Maps inputs to a strict -Scale to +Scale range.
244
+ */
245
+ static normalizeTanh(value, scale = 10, sensitivity = 10.0) {
246
+ if (value === 0) return 0;
247
+ return Math.tanh(value / sensitivity) * scale;
248
+ }
249
+
250
+ /**
251
+ * Standard Z-Score normalization.
252
+ */
253
+ static normalizeZScore(value, mean, stdDev) {
254
+ if (!stdDev || stdDev === 0) return 0;
255
+ return (value - mean) / stdDev;
256
+ }
257
+
258
+ /**
259
+ * Simple Divergence (A - B).
260
+ */
261
+ static divergence(valueA, valueB) {
262
+ return (valueA || 0) - (valueB || 0);
263
+ }
264
+ }
265
+
266
+ class MathPrimitives {
267
+ static average(values) {
268
+ if (!values || !values.length) return 0;
269
+ return values.reduce((a, b) => a + b, 0) / values.length;
270
+ }
271
+
272
+ static median(values) {
273
+ if (!values || !values.length) return 0;
274
+ const sorted = [...values].sort((a, b) => a - b);
275
+ const mid = Math.floor(sorted.length / 2);
276
+ return sorted.length % 2 === 0
277
+ ? (sorted[mid - 1] + sorted[mid]) / 2
278
+ : sorted[mid];
279
+ }
280
+
281
+ static standardDeviation(values) {
282
+ if (!values || !values.length) return 0;
283
+ const avg = this.average(values);
284
+ const squareDiffs = values.map(val => Math.pow((val || 0) - avg, 2));
285
+ return Math.sqrt(this.average(squareDiffs));
286
+ }
287
+
288
+ static bucketBinary(value, threshold = 0) {
289
+ return value > threshold ? 'winner' : 'loser';
290
+ }
291
+ }
292
+
293
+ class Aggregators {
294
+ /**
295
+ * Helper to bucket users by P&L Status.
296
+ * Used by legacy systems or specific aggregators.
297
+ */
298
+ static bucketUsersByPnlPerAsset(usersData, tickerMap) {
299
+ const buckets = new Map();
300
+ 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;
303
+ const positions = DataExtractor.getPositions(portfolio, userType);
304
+
305
+ for (const pos of positions) {
306
+ const id = DataExtractor.getInstrumentId(pos);
307
+ const pnl = DataExtractor.getNetProfit(pos);
308
+ if (!id || pnl === 0) continue;
309
+
310
+ const ticker = tickerMap[id];
311
+ if (!ticker) continue;
312
+
313
+ if (!buckets.has(ticker)) buckets.set(ticker, { winners: [], losers: [] });
314
+ const b = buckets.get(ticker);
315
+
316
+ if (pnl > 0) b.winners.push(userId);
317
+ else b.losers.push(userId);
318
+ }
319
+ }
320
+ return Object.fromEntries(buckets);
321
+ }
322
+ }
323
+
324
+ class Validators {
325
+ static validatePortfolio(portfolio, userType) {
326
+ if (!portfolio) return { valid: false, errors: ['Portfolio is null'] };
327
+
328
+ if (userType === SCHEMAS.USER_TYPES.SPECULATOR) {
329
+ if (!portfolio.PublicPositions) return { valid: false, errors: ['Missing PublicPositions'] };
330
+ } else {
331
+ if (!portfolio.AggregatedPositions) return { valid: false, errors: ['Missing AggregatedPositions'] };
332
+ }
333
+
334
+ return { valid: true, errors: [] };
335
+ }
336
+ }
337
+
338
+ module.exports = {
339
+ SCHEMAS,
340
+ DataExtractor,
341
+ HistoryExtractor,
342
+ MathPrimitives,
343
+ Aggregators,
344
+ Validators,
345
+ SignalPrimitives
346
+ };
@@ -15,7 +15,7 @@ async function lookupUsernames(cids, { logger, headerManager, proxyManager }, co
15
15
  if (!cids?.length) return [];
16
16
  logger.log('INFO', `[lookupUsernames] Looking up usernames for ${cids.length} CIDs.`);
17
17
 
18
- // --- Set concurrency to 1 because appscript gets really fuckmed up with undocumented rate limits if we try spam it concurrently, a shame but that's life. DO NOT CHANGE THIS
18
+ // --- Set concurrency to 1 because appscript gets really fucked up with undocumented rate limits if we try spam it concurrently, a shame but that's life. DO NOT CHANGE THIS
19
19
  const limit = pLimit(1);
20
20
  const { USERNAME_LOOKUP_BATCH_SIZE, ETORO_API_RANKINGS_URL } = config;
21
21
  const batches = [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bulltrackers-module",
3
- "version": "1.0.171",
3
+ "version": "1.0.172",
4
4
  "description": "Helper Functions for Bulltrackers.",
5
5
  "main": "index.js",
6
6
  "files": [