aiden-shared-calculations-unified 1.0.156 → 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.
|
@@ -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',
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
66
|
+
let identifiedCryptoInstrumentIDs = [];
|
|
67
|
+
|
|
68
|
+
// 3. Check current portfolio
|
|
49
69
|
if (portfolio) {
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
//
|
|
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;
|