aiden-shared-calculations-unified 1.0.155 → 1.0.157

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.
@@ -5,7 +5,7 @@ class AumLeaderboard {
5
5
  static getMetadata() {
6
6
  return {
7
7
  type: 'meta',
8
- category: 'analytics',
8
+ category: 'popular-investor',
9
9
  rootDataDependencies: ['rankings'],
10
10
  mandatoryRoots: ['rankings'],
11
11
  schedule: { type: 'DAILY' }
@@ -6,7 +6,7 @@ class GlobalAumPerAsset30D {
6
6
  static getMetadata() {
7
7
  return {
8
8
  type: 'meta',
9
- category: 'analytics',
9
+ category: 'popular-investor',
10
10
  rootDataDependencies: [],
11
11
  schedule: { type: 'DAILY' }
12
12
  };
@@ -6,7 +6,7 @@ class PIDailyAssetAUM {
6
6
  static getMetadata() {
7
7
  return {
8
8
  type: 'standard',
9
- category: 'analytics',
9
+ category: 'popular-investor',
10
10
  userType: 'POPULAR_INVESTOR', // Ensures we only run for PIs
11
11
 
12
12
  // 1. Core Data
@@ -1,6 +1,15 @@
1
1
  /**
2
- * @fileoverview Popular Investors Who Bought Crypto.
2
+ * @fileoverview Popular Investors Who Bought Crypto - OPTIMIZED & DETERMINISTIC
3
3
  * Flags if the specific PI has bought crypto recently.
4
+ * * ALGORITHM: Domain-Aware Hybrid Integer Subset Sum Solver
5
+ * 0. DOMAIN FILTER: Crypto assets have 6-digit InstrumentIDs (100000+)
6
+ * 1. Integerization: Scaled integers (x100,000) to eliminate FP drift.
7
+ * 2. Grouping: Bounded-choice branching for identical weights.
8
+ * 3. Strategy:
9
+ * - N <= 40: DFS with Suffix Max, Greedy Bounding, Early Termination
10
+ * - N > 40: Meet-in-the-Middle with immediate left-side checks.
11
+ * 4. Determinism: Strictly prefers solutions with fewer assets.
12
+ * 5. Tolerance: 0.01% rounding tolerance for financial data
4
13
  */
5
14
 
6
15
  class PiCryptoBuyers {
@@ -10,7 +19,7 @@ class PiCryptoBuyers {
10
19
 
11
20
  static getMetadata() {
12
21
  return {
13
- type: 'standard', // [FIX] Changed to standard
22
+ type: 'standard',
14
23
  category: 'popular_investor',
15
24
  rootDataDependencies: ['portfolio', 'history'],
16
25
  userType: 'POPULAR_INVESTOR'
@@ -24,47 +33,84 @@ class PiCryptoBuyers {
24
33
  type: 'object',
25
34
  properties: {
26
35
  isCryptoBuyer: { type: 'boolean' },
27
- lastCryptoTrade: { type: 'string' }
36
+ lastCryptoTrade: { type: 'string' },
37
+ cryptoAssets: { type: 'array', items: { type: 'number' } }
28
38
  }
29
39
  };
30
40
  }
31
41
 
32
42
  process(context) {
33
- const { DataExtractor, HistoryExtractor } = context.math;
43
+ // 1. Dependency Injection & Safety
44
+ // HistoryExtractor is injected into context.math by ContextFactory -> layers/index.js
45
+ const { HistoryExtractor } = context.math;
46
+ if (!HistoryExtractor) throw new Error("HistoryExtractor not found in context.math");
47
+
34
48
  const userId = context.user.id;
35
49
  const portfolio = context.user.portfolio.today;
36
50
  const historyDoc = context.user.history.today;
51
+
52
+ // 2. Deterministic Date Handling (CRITICAL FIX)
53
+ // Use the computation date (context.date.today), NOT new Date() (server time).
54
+ const runDateString = context.date.today;
55
+ const runDate = new Date(runDateString);
37
56
 
38
- // Default time window: 7 days
39
57
  const TIME_WINDOW_DAYS = 7;
40
- const cutoffDate = new Date();
58
+ const cutoffDate = new Date(runDate);
41
59
  cutoffDate.setDate(cutoffDate.getDate() - TIME_WINDOW_DAYS);
42
-
43
- const CRYPTO_INSTRUMENT_TYPES = [28, 100028];
44
-
60
+
61
+ // ID 10 is Cryptocurrencies
62
+ const CRYPTO_INSTRUMENT_TYPES = [10, 28, 100028];
63
+
45
64
  let isCryptoBuyer = false;
46
65
  let lastCryptoTrade = null;
47
-
48
- // 1. Check current portfolio
66
+ let identifiedCryptoInstrumentIDs = [];
67
+
68
+ // 3. Check current portfolio
49
69
  if (portfolio) {
50
- const positions = DataExtractor.getPositions(portfolio, 'POPULAR_INVESTOR');
51
- for (const pos of positions) {
52
- const instrumentType = pos.InstrumentTypeID || 0;
53
- if (CRYPTO_INSTRUMENT_TYPES.includes(instrumentType)) {
54
- isCryptoBuyer = true;
55
- lastCryptoTrade = new Date().toISOString();
56
- break;
70
+ const aggregatedTypes = portfolio.AggregatedPositionsByInstrumentTypeID || [];
71
+ const cryptoAggregate = aggregatedTypes.find(type => type.InstrumentTypeID === 10);
72
+
73
+ if (cryptoAggregate && cryptoAggregate.Invested > 0) {
74
+ isCryptoBuyer = true;
75
+ // Use the deterministic run date for the "last seen" timestamp
76
+ lastCryptoTrade = runDate.toISOString();
77
+
78
+ const allPositions = portfolio.AggregatedPositions || [];
79
+
80
+ // DOMAIN OPTIMIZATION: Pre-filter to crypto assets only (6-digit IDs >= 100000)
81
+ // This reduces candidate pool from 50+ to ~5-15 assets
82
+ const cryptoCandidates = allPositions
83
+ .filter(p => p.Invested > 0 && p.InstrumentID >= 100000)
84
+ .map(p => ({
85
+ id: p.InstrumentID,
86
+ val: p.Invested
87
+ }));
88
+
89
+ // Fast path: If crypto candidates sum exactly matches, we're done (O(N))
90
+ const cryptoSum = cryptoCandidates.reduce((sum, c) => sum + c.val, 0);
91
+ const tolerance = 0.0001; // 0.01% tolerance
92
+
93
+ if (Math.abs(cryptoSum - cryptoAggregate.Invested) <= tolerance) {
94
+ // Perfect match - all 6-digit IDs are the crypto assets
95
+ identifiedCryptoInstrumentIDs = cryptoCandidates.map(c => c.id);
96
+ } else {
97
+ // Fallback to subset sum (now with much smaller input)
98
+ identifiedCryptoInstrumentIDs = this.solveSubsetSum(
99
+ cryptoAggregate.Invested,
100
+ cryptoCandidates
101
+ );
57
102
  }
58
103
  }
59
104
  }
60
-
61
- // 2. Check history
105
+
106
+ // 4. Check history (Fallback)
62
107
  if (!isCryptoBuyer && historyDoc) {
63
108
  const trades = HistoryExtractor.getTrades(historyDoc);
64
109
  for (const trade of trades) {
65
110
  const openDate = trade.OpenDateTime ? new Date(trade.OpenDateTime) : null;
111
+ // Filter against the deterministic cutoff date
66
112
  if (!openDate || openDate < cutoffDate) continue;
67
-
113
+
68
114
  const instrumentType = trade.InstrumentTypeID || 0;
69
115
  if (CRYPTO_INSTRUMENT_TYPES.includes(instrumentType)) {
70
116
  isCryptoBuyer = true;
@@ -74,17 +120,242 @@ class PiCryptoBuyers {
74
120
  }
75
121
  }
76
122
  }
77
-
123
+
124
+ // 5. Store Result
125
+ // StandardExecutor reads directly from this.results[userId]
78
126
  this.results[userId] = {
79
127
  isCryptoBuyer,
80
- lastCryptoTrade
128
+ lastCryptoTrade,
129
+ cryptoAssets: identifiedCryptoInstrumentIDs
81
130
  };
131
+
82
132
  return this.results;
83
133
  }
84
134
 
85
135
  async getResult() {
86
136
  return this.results;
87
137
  }
138
+
139
+ /**
140
+ * Master Solver Orchestrator
141
+ */
142
+ solveSubsetSum(targetFloat, candidates) {
143
+ const SCALE = 100000;
144
+ const TOLERANCE = Math.round(0.0001 * SCALE); // 0.01% tolerance for rounding
145
+ const target = Math.round(targetFloat * SCALE);
146
+
147
+ // Canonicalize & Filter
148
+ const items = candidates
149
+ .map(c => ({ id: c.id, w: Math.round(c.val * SCALE) }))
150
+ .filter(c => c.w > 0 && c.w <= target + TOLERANCE);
151
+
152
+ // Early exit: no valid candidates
153
+ if (items.length === 0) return [];
154
+
155
+ // Domain Rule: Single Dominant Asset Check (with tolerance)
156
+ const exactMatch = items.find(i => Math.abs(i.w - target) <= TOLERANCE);
157
+ if (exactMatch) return [exactMatch.id];
158
+
159
+ // Complement Logic (60% threshold for better performance)
160
+ const totalSum = items.reduce((sum, item) => sum + item.w, 0);
161
+ let effectiveTarget = target;
162
+ let solvingForComplement = false;
163
+
164
+ if (target > (totalSum * 0.6)) {
165
+ effectiveTarget = totalSum - target;
166
+ solvingForComplement = true;
167
+ }
168
+
169
+ // Strategy Selection
170
+ let resultIds = [];
171
+ if (items.length <= 40) {
172
+ resultIds = this.solveBoundedDFS(effectiveTarget, items, TOLERANCE);
173
+ } else {
174
+ resultIds = this.solveMeetInTheMiddle(effectiveTarget, items, TOLERANCE);
175
+ }
176
+
177
+ // Return Result
178
+ if (!solvingForComplement) {
179
+ return resultIds;
180
+ } else {
181
+ const complementSet = new Set(resultIds);
182
+ return items.filter(i => !complementSet.has(i.id)).map(i => i.id);
183
+ }
184
+ }
185
+
186
+ /**
187
+ * STRATEGY A: Bounded-Choice DFS (N <= 40)
188
+ * Features: Suffix Max, Greedy Bounds, Early Termination, Tolerance
189
+ */
190
+ solveBoundedDFS(target, items, tolerance) {
191
+ // 1. Group by Weight
192
+ const groupsMap = new Map();
193
+ for (const item of items) {
194
+ if (!groupsMap.has(item.w)) {
195
+ groupsMap.set(item.w, { weight: item.w, ids: [] });
196
+ }
197
+ groupsMap.get(item.w).ids.push(item.id);
198
+ }
199
+
200
+ // 2. Sort Groups Descending (Greedy)
201
+ const groups = Array.from(groupsMap.values()).sort((a, b) => b.weight - a.weight);
202
+ groups.forEach(g => g.ids.sort()); // Deterministic ID order
203
+
204
+ // 3. Global GCD Pruning (skip if tolerance is active)
205
+ if (tolerance === 0 && groups.length > 0) {
206
+ let overallGCD = groups[0].weight;
207
+ for(let i=1; i<groups.length; i++) overallGCD = this.gcd(overallGCD, groups[i].weight);
208
+ if (target % overallGCD !== 0) return [];
209
+ }
210
+
211
+ // 4. Precompute Suffix Max
212
+ const suffixMax = new Array(groups.length).fill(0);
213
+ for (let i = groups.length - 1; i >= 0; i--) {
214
+ const groupMax = groups[i].weight * groups[i].ids.length;
215
+ suffixMax[i] = groupMax + (i < groups.length - 1 ? suffixMax[i+1] : 0);
216
+ }
217
+
218
+ const failedStates = new Set();
219
+ let bestSolution = null;
220
+
221
+ const dfs = (index, currentSum, currentSelection) => {
222
+ // Success (with tolerance)
223
+ if (Math.abs(currentSum - target) <= tolerance) {
224
+ if (!bestSolution || currentSelection.length < bestSolution.length) {
225
+ bestSolution = [...currentSelection];
226
+ }
227
+ return true; // Signal early termination
228
+ }
229
+
230
+ // Pruning 1: Overshot or End
231
+ if (currentSum > target + tolerance || index >= groups.length) return false;
232
+
233
+ // Pruning 2: Best-Path Pruning
234
+ if (bestSolution && currentSelection.length >= bestSolution.length) return false;
235
+
236
+ // Pruning 3: Memoization
237
+ const stateKey = `${index}:${currentSum}`;
238
+ if (failedStates.has(stateKey)) return false;
239
+
240
+ // Pruning 4: Suffix Max
241
+ if (currentSum + suffixMax[index] < target - tolerance) {
242
+ failedStates.add(stateKey);
243
+ return false;
244
+ }
245
+
246
+ const group = groups[index];
247
+ const weight = group.weight;
248
+ const availableCount = group.ids.length;
249
+ const remainingSpace = target - currentSum + tolerance;
250
+ const maxCalc = Math.floor(remainingSpace / weight);
251
+ const maxToTake = Math.min(availableCount, maxCalc);
252
+
253
+ // Try taking k items (Greedy: largest k first)
254
+ for (let k = maxToTake; k >= 0; k--) {
255
+ const newSelection = [...currentSelection];
256
+ for(let i=0; i<k; i++) newSelection.push(group.ids[i]);
257
+
258
+ const foundSolution = dfs(index + 1, currentSum + (k * weight), newSelection);
259
+
260
+ // Early termination: if we found optimal solution, stop
261
+ if (foundSolution && bestSolution && bestSolution.length === 1) {
262
+ return true;
263
+ }
264
+ }
265
+
266
+ if (!bestSolution) failedStates.add(stateKey);
267
+ return bestSolution !== null;
268
+ };
269
+
270
+ dfs(0, 0, []);
271
+ return bestSolution || [];
272
+ }
273
+
274
+ /**
275
+ * STRATEGY B: Meet-in-the-Middle (N > 40)
276
+ * Features: Early Exit, Tolerance, O(1) Lookup
277
+ */
278
+ solveMeetInTheMiddle(target, items, tolerance) {
279
+ const n = items.length;
280
+ const mid = Math.floor(n / 2);
281
+ const left = items.slice(0, mid);
282
+ const right = items.slice(mid);
283
+
284
+ // Generate Left Sums
285
+ const leftMap = new Map();
286
+
287
+ const generateLeft = (pool) => {
288
+ const limit = 1 << pool.length;
289
+ for (let i = 0; i < limit; i++) {
290
+ let s = 0;
291
+ let ids = [];
292
+ for (let j = 0; j < pool.length; j++) {
293
+ if ((i >> j) & 1) {
294
+ s += pool[j].w;
295
+ ids.push(pool[j].id);
296
+ }
297
+ }
298
+ if (s <= target + tolerance) {
299
+ // Early Exit on Left Side (with tolerance)
300
+ if (Math.abs(s - target) <= tolerance) return { earlyExit: ids };
301
+
302
+ // Store shortest solution for each sum
303
+ if (!leftMap.has(s) || ids.length < leftMap.get(s).length) {
304
+ leftMap.set(s, ids);
305
+ }
306
+ }
307
+ }
308
+ return null;
309
+ };
310
+
311
+ const early = generateLeft(left);
312
+ if (early && early.earlyExit) return early.earlyExit;
313
+
314
+ // Iterate Right Side
315
+ const rightLimit = 1 << right.length;
316
+ let bestSolution = null;
317
+
318
+ for (let i = 0; i < rightLimit; i++) {
319
+ let s = 0;
320
+ let rightIds = [];
321
+ for (let j = 0; j < right.length; j++) {
322
+ if ((i >> j) & 1) {
323
+ s += right[j].w;
324
+ rightIds.push(right[j].id);
325
+ }
326
+ }
327
+
328
+ // Early Exit Right Side (with tolerance)
329
+ if (Math.abs(s - target) <= tolerance) {
330
+ if (!bestSolution || rightIds.length < bestSolution.length) {
331
+ bestSolution = rightIds;
332
+ }
333
+ continue;
334
+ }
335
+
336
+ if (s > target + tolerance) continue;
337
+
338
+ // Check for complement in left map (with tolerance range)
339
+ const remainder = target - s;
340
+
341
+ // Search within tolerance range
342
+ for (let delta = -tolerance; delta <= tolerance; delta++) {
343
+ const lookupSum = remainder + delta;
344
+ if (leftMap.has(lookupSum)) {
345
+ const combined = [...leftMap.get(lookupSum), ...rightIds];
346
+ if (!bestSolution || combined.length < bestSolution.length) {
347
+ bestSolution = combined;
348
+ }
349
+ }
350
+ }
351
+ }
352
+
353
+ return bestSolution || [];
354
+ }
355
+
356
+ gcd(a, b) {
357
+ return b === 0 ? a : this.gcd(b, a % b);
358
+ }
88
359
  }
89
360
 
90
361
  module.exports = PiCryptoBuyers;
@@ -5,7 +5,7 @@ class RiskLeaderboard {
5
5
  static getMetadata() {
6
6
  return {
7
7
  type: 'meta',
8
- category: 'analytics',
8
+ category: 'popular-investor',
9
9
  rootDataDependencies: ['rankings'],
10
10
  mandatoryRoots: ['rankings'],
11
11
  schedule: { type: 'DAILY' }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aiden-shared-calculations-unified",
3
- "version": "1.0.155",
3
+ "version": "1.0.157",
4
4
  "description": "Shared calculation modules for the BullTrackers Computation System.",
5
5
  "main": "index.js",
6
6
  "files": [