aiden-shared-calculations-unified 1.0.148 → 1.0.150
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.
- package/calculations/alerts/Behaviour.js +41 -66
- package/calculations/popular-investor/AumLeaderboard.js +3 -4
- package/calculations/popular-investor/GlobalAumPerAsset30D.js +26 -17
- package/calculations/popular-investor/PIDailyAssetAUM.js +37 -36
- package/calculations/popular-investor/RecommendedPopularInvestors.js +85 -45
- package/calculations/popular-investor/RiskLeaderboard.js +2 -3
- package/package.json +1 -1
- package/calculations/popular-investor/AggregateAssetAUM.js +0 -70
- package/calculations/popular-investor/AggregateDailyAUM.js +0 -72
- package/calculations/popular-investor/UserAUM30Day.js +0 -67
- package/calculations/popular-investor/UserAUMPerAsset.js +0 -67
|
@@ -3,8 +3,6 @@
|
|
|
3
3
|
* Uses Mahalanobis Distance to detect complex deviations in investor behavior.
|
|
4
4
|
* Features: Concentration (HHI), Martingale Propensity, Capacity Strain, Risk Score.
|
|
5
5
|
*/
|
|
6
|
-
|
|
7
|
-
|
|
8
6
|
class BehavioralAnomaly {
|
|
9
7
|
constructor() {
|
|
10
8
|
this.results = {};
|
|
@@ -17,9 +15,8 @@ class BehavioralAnomaly {
|
|
|
17
15
|
type: 'standard',
|
|
18
16
|
isHistorical: true,
|
|
19
17
|
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
// and only fetches dates that actually exist, saving reads automatically.
|
|
18
|
+
// RESTORED: Request full 60-day history for both Portfolio and Rankings.
|
|
19
|
+
// CachedDataLoader handles this efficiently via reference pointers.
|
|
23
20
|
rootDataSeries: {
|
|
24
21
|
portfolio: 60,
|
|
25
22
|
rankings: 60
|
|
@@ -28,7 +25,6 @@ class BehavioralAnomaly {
|
|
|
28
25
|
rootDataDependencies: ['portfolio', 'rankings', 'history'],
|
|
29
26
|
userType: 'POPULAR_INVESTOR',
|
|
30
27
|
|
|
31
|
-
// Allows the computation to run even if today's specific root data is missing
|
|
32
28
|
canHaveMissingRoots: true,
|
|
33
29
|
isAlertComputation: true,
|
|
34
30
|
mandatoryRoots: ['portfolio', 'history'],
|
|
@@ -39,14 +35,21 @@ class BehavioralAnomaly {
|
|
|
39
35
|
|
|
40
36
|
// --- Helpers ---
|
|
41
37
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
38
|
+
findClosestData(dateStr, seriesMap, maxLookback = 5) {
|
|
39
|
+
if (!seriesMap) return null;
|
|
40
|
+
if (seriesMap[dateStr]) return seriesMap[dateStr];
|
|
41
|
+
|
|
42
|
+
let current = new Date(dateStr);
|
|
43
|
+
for (let i = 0; i < maxLookback; i++) {
|
|
44
|
+
current.setUTCDate(current.getUTCDate() - 1);
|
|
45
|
+
const dString = current.toISOString().slice(0, 10);
|
|
46
|
+
if (seriesMap[dString]) return seriesMap[dString];
|
|
47
|
+
}
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
|
|
46
51
|
calculateHHI(portfolio, math) {
|
|
47
52
|
if (!portfolio) return 0;
|
|
48
|
-
|
|
49
|
-
// [FIX] DataExtractor is available directly on the flattened math object
|
|
50
53
|
const { DataExtractor } = math;
|
|
51
54
|
const positions = DataExtractor.getPositions(portfolio, 'POPULAR_INVESTOR');
|
|
52
55
|
|
|
@@ -55,29 +58,22 @@ class BehavioralAnomaly {
|
|
|
55
58
|
let sumSquares = 0;
|
|
56
59
|
let totalValue = 0;
|
|
57
60
|
|
|
58
|
-
positions.forEach(p =>
|
|
59
|
-
const val = DataExtractor.getPositionValue(p);
|
|
60
|
-
totalValue += val;
|
|
61
|
-
});
|
|
61
|
+
positions.forEach(p => totalValue += DataExtractor.getPositionValue(p));
|
|
62
62
|
|
|
63
63
|
if (totalValue === 0) return 0;
|
|
64
64
|
|
|
65
65
|
positions.forEach(p => {
|
|
66
|
-
const
|
|
67
|
-
const weight = val / totalValue;
|
|
66
|
+
const weight = DataExtractor.getPositionValue(p) / totalValue;
|
|
68
67
|
sumSquares += (weight * weight);
|
|
69
68
|
});
|
|
70
69
|
|
|
71
70
|
return sumSquares;
|
|
72
71
|
}
|
|
73
72
|
|
|
74
|
-
/**
|
|
75
|
-
* Helper: Calculates Martingale Score (Doubling down after loss)
|
|
76
|
-
*/
|
|
77
73
|
calculateMartingaleScore(tradeHistory) {
|
|
78
74
|
if (!tradeHistory || tradeHistory.length < 2) return 0;
|
|
79
75
|
|
|
80
|
-
//
|
|
76
|
+
// Trades are already filtered by date in the main loop logic
|
|
81
77
|
const sorted = [...tradeHistory].sort((a, b) => new Date(a.CloseDateTime) - new Date(b.CloseDateTime));
|
|
82
78
|
const recentTrades = sorted.slice(-30);
|
|
83
79
|
|
|
@@ -100,27 +96,7 @@ class BehavioralAnomaly {
|
|
|
100
96
|
return lossEvents > 0 ? (martingaleResponses / lossEvents) : 0;
|
|
101
97
|
}
|
|
102
98
|
|
|
103
|
-
/**
|
|
104
|
-
* "Use Closest" Logic: Looks for recent valid data point
|
|
105
|
-
*/
|
|
106
|
-
findClosestData(dateStr, seriesMap, maxLookback = 5) {
|
|
107
|
-
if (!seriesMap) return null;
|
|
108
|
-
if (seriesMap[dateStr]) return seriesMap[dateStr];
|
|
109
|
-
|
|
110
|
-
let current = new Date(dateStr);
|
|
111
|
-
for (let i = 0; i < maxLookback; i++) {
|
|
112
|
-
current.setUTCDate(current.getUTCDate() - 1);
|
|
113
|
-
const dString = current.toISOString().slice(0, 10);
|
|
114
|
-
if (seriesMap[dString]) return seriesMap[dString];
|
|
115
|
-
}
|
|
116
|
-
return null;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
/**
|
|
120
|
-
* [FIX] Updated to accept 'math' context instead of 'layers'
|
|
121
|
-
*/
|
|
122
99
|
getDailyVector(portfolio, rankings, history, math) {
|
|
123
|
-
// [FIX] RankingsExtractor is available directly on the flattened math object
|
|
124
100
|
const { RankingsExtractor } = math;
|
|
125
101
|
|
|
126
102
|
const hhi = this.calculateHHI(portfolio, math);
|
|
@@ -140,79 +116,79 @@ class BehavioralAnomaly {
|
|
|
140
116
|
}
|
|
141
117
|
|
|
142
118
|
process(context) {
|
|
143
|
-
// [FIX] Use 'math' from context (ContextFactory flattens layers into 'math')
|
|
144
|
-
// [FIX] Access 'globalData' to retrieve series
|
|
145
119
|
const { math, globalData } = context;
|
|
146
120
|
const { LinearAlgebra } = math;
|
|
147
121
|
const userId = context.user.id;
|
|
148
122
|
|
|
149
|
-
// [FIX] Series data is located in globalData.series
|
|
150
123
|
const seriesData = globalData?.series?.root || {};
|
|
151
124
|
const portfolioSeries = seriesData.portfolio || {};
|
|
152
125
|
const rankingsSeries = seriesData.rankings || {};
|
|
153
126
|
|
|
154
|
-
//
|
|
155
|
-
|
|
127
|
+
// Full Trade History (Contains ALL time, so we must filter it)
|
|
128
|
+
const fullHistory = context.user.history || [];
|
|
129
|
+
|
|
156
130
|
const availableDates = Object.keys(portfolioSeries).sort();
|
|
157
131
|
const trainingVectors = [];
|
|
158
132
|
|
|
159
|
-
// 1. Build Baseline
|
|
133
|
+
// 1. Build Baseline
|
|
160
134
|
for (const date of availableDates) {
|
|
161
135
|
const datePortfolio = portfolioSeries[date][userId];
|
|
162
136
|
if (!datePortfolio) continue;
|
|
163
137
|
|
|
138
|
+
// CRITICAL FIX: Filter History to avoid "Time Travel"
|
|
139
|
+
// We only look at trades that had closed ON or BEFORE this specific loop date.
|
|
140
|
+
const loopDateObj = new Date(date);
|
|
141
|
+
const historyAsOfDate = fullHistory.filter(t => new Date(t.CloseDateTime) <= loopDateObj);
|
|
142
|
+
|
|
143
|
+
// Robust Lookup: Find ranking for this date (or closest recent date)
|
|
164
144
|
const rankingsSnapshot = this.findClosestData(date, rankingsSeries);
|
|
165
145
|
const userRanking = rankingsSnapshot ? rankingsSnapshot.find(r => String(r.CustomerId) === String(userId)) : null;
|
|
166
146
|
|
|
167
147
|
const vec = this.getDailyVector(
|
|
168
148
|
datePortfolio,
|
|
169
149
|
userRanking,
|
|
170
|
-
|
|
171
|
-
math
|
|
150
|
+
historyAsOfDate, // Pass filtered history
|
|
151
|
+
math
|
|
172
152
|
);
|
|
173
153
|
trainingVectors.push(vec);
|
|
174
154
|
}
|
|
175
155
|
|
|
176
|
-
// 2. Minimum Viable Baseline
|
|
177
|
-
// We requested 60 days, but we accept as few as 15 (approx 3 weeks)
|
|
178
|
-
// to avoid "2 month" delay for new users while ensuring statistical fairness.
|
|
156
|
+
// 2. Minimum Viable Baseline
|
|
179
157
|
const MIN_DATAPOINTS = 15;
|
|
180
|
-
|
|
181
158
|
if (trainingVectors.length < MIN_DATAPOINTS) {
|
|
182
159
|
this.results[userId] = {
|
|
183
160
|
status: 'INSUFFICIENT_BASELINE',
|
|
184
|
-
dataPoints: trainingVectors.length
|
|
185
|
-
required: MIN_DATAPOINTS,
|
|
186
|
-
info: 'Waiting for more history to generate fair alerts'
|
|
161
|
+
dataPoints: trainingVectors.length
|
|
187
162
|
};
|
|
188
163
|
return;
|
|
189
164
|
}
|
|
190
165
|
|
|
191
|
-
// 3. Compute Statistics
|
|
166
|
+
// 3. Compute Statistics (Covariance Matrix)
|
|
192
167
|
const stats = LinearAlgebra.covarianceMatrix(trainingVectors);
|
|
193
168
|
const inverseCov = LinearAlgebra.invertMatrix(stats.matrix);
|
|
194
169
|
|
|
195
|
-
// Singular Matrix Check
|
|
170
|
+
// Singular Matrix Check (If behavior is perfectly identical every day)
|
|
196
171
|
if (!inverseCov) {
|
|
197
|
-
this.results[userId] = { status: 'STABLE_STATE', info: 'Variance too low
|
|
172
|
+
this.results[userId] = { status: 'STABLE_STATE', info: 'Variance too low' };
|
|
198
173
|
return;
|
|
199
174
|
}
|
|
200
175
|
|
|
201
176
|
// 4. Compute Today's Vector
|
|
177
|
+
// We use the explicit 'today' injection, or fallback to the series
|
|
202
178
|
const todayRanking = context.user.rankEntry ||
|
|
203
179
|
(this.findClosestData(context.date.today, rankingsSeries) || [])
|
|
204
180
|
.find(r => String(r.CustomerId) === String(userId));
|
|
205
181
|
|
|
206
182
|
const todayVector = this.getDailyVector(
|
|
207
|
-
context.user.portfolio,
|
|
183
|
+
context.user.portfolio.today,
|
|
208
184
|
todayRanking,
|
|
209
|
-
|
|
210
|
-
math
|
|
185
|
+
fullHistory, // Today includes all history
|
|
186
|
+
math
|
|
211
187
|
);
|
|
212
188
|
|
|
213
189
|
// 5. Calculate Distance
|
|
214
190
|
const distance = LinearAlgebra.mahalanobisDistance(todayVector, stats.means, inverseCov);
|
|
215
|
-
const IS_ANOMALY = distance > 3.5;
|
|
191
|
+
const IS_ANOMALY = distance > 3.5;
|
|
216
192
|
|
|
217
193
|
if (IS_ANOMALY) {
|
|
218
194
|
const featureNames = ['Concentration (HHI)', 'Martingale Behavior', 'Capacity Strain', 'Risk Score'];
|
|
@@ -240,6 +216,7 @@ class BehavioralAnomaly {
|
|
|
240
216
|
}
|
|
241
217
|
};
|
|
242
218
|
|
|
219
|
+
// Add CID to index for downstream alert handlers
|
|
243
220
|
if (!this.results.cids) this.results.cids = [];
|
|
244
221
|
this.results.cids.push(userId);
|
|
245
222
|
} else {
|
|
@@ -249,8 +226,6 @@ class BehavioralAnomaly {
|
|
|
249
226
|
};
|
|
250
227
|
}
|
|
251
228
|
}
|
|
252
|
-
|
|
253
|
-
async getResult() { return this.results; }
|
|
254
229
|
}
|
|
255
230
|
|
|
256
231
|
module.exports = BehavioralAnomaly;
|
|
@@ -5,7 +5,7 @@ class AumLeaderboard {
|
|
|
5
5
|
static getMetadata() {
|
|
6
6
|
return {
|
|
7
7
|
type: 'meta',
|
|
8
|
-
category: '
|
|
8
|
+
category: 'analytics',
|
|
9
9
|
// We only need the rankings root data
|
|
10
10
|
rootDataDependencies: ['rankings'],
|
|
11
11
|
mandatoryRoots: ['rankings'],
|
|
@@ -58,9 +58,8 @@ class AumLeaderboard {
|
|
|
58
58
|
|
|
59
59
|
// 4. Output Global Result
|
|
60
60
|
this.results = rankedResult;
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
async getResult() {
|
|
61
|
+
|
|
62
|
+
// CRITICAL FIX: MetaExecutor requires the data to be returned
|
|
64
63
|
return this.results;
|
|
65
64
|
}
|
|
66
65
|
}
|
|
@@ -1,42 +1,51 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Aggregates PI Asset AUM over the last 30 days.
|
|
3
|
+
*/
|
|
4
|
+
class GlobalAumPerAsset30D {
|
|
2
5
|
static getMetadata() {
|
|
3
6
|
return {
|
|
4
7
|
type: 'meta',
|
|
5
8
|
category: 'analytics',
|
|
6
9
|
rootDataDependencies: [],
|
|
7
|
-
// No dependencySeries needed! We just need Today's results.
|
|
10
|
+
// No dependencySeries needed! We just need Today's results via getDependencies.
|
|
8
11
|
schedule: { type: 'DAILY' }
|
|
9
12
|
};
|
|
10
13
|
}
|
|
11
14
|
|
|
12
15
|
static getDependencies() {
|
|
13
16
|
// Force running AFTER the Standard comp
|
|
14
|
-
|
|
17
|
+
// CRITICAL: Must match the File Name of the standard computation
|
|
18
|
+
return ['PIDailyAssetAUM'];
|
|
15
19
|
}
|
|
16
20
|
|
|
17
21
|
async process(context) {
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
const allUserResults = context.computed['
|
|
22
|
+
// Access the results injected by getDependencies()
|
|
23
|
+
// CRITICAL: Must match the File Name
|
|
24
|
+
const allUserResults = context.computed['PIDailyAssetAUM'];
|
|
21
25
|
|
|
22
26
|
const globalAverages = {};
|
|
23
27
|
let userCount = 0;
|
|
24
28
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
+
if (allUserResults) {
|
|
30
|
+
Object.values(allUserResults).forEach(userOutput => {
|
|
31
|
+
// userOutput is: { dailyMap, averageMap }
|
|
32
|
+
const avgMap = userOutput.averageMap;
|
|
33
|
+
if (!avgMap) return;
|
|
29
34
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
35
|
+
userCount++;
|
|
36
|
+
Object.entries(avgMap).forEach(([ticker, avgVal]) => {
|
|
37
|
+
if (!globalAverages[ticker]) globalAverages[ticker] = 0;
|
|
38
|
+
globalAverages[ticker] += avgVal;
|
|
39
|
+
});
|
|
34
40
|
});
|
|
35
|
-
}
|
|
41
|
+
}
|
|
36
42
|
|
|
37
43
|
// Result: The Sum of everyone's 30-Day Average Allocations
|
|
38
44
|
this.results = globalAverages;
|
|
45
|
+
|
|
46
|
+
// CRITICAL FIX: MetaExecutor requires the data to be returned
|
|
47
|
+
return this.results;
|
|
39
48
|
}
|
|
49
|
+
}
|
|
40
50
|
|
|
41
|
-
|
|
42
|
-
}
|
|
51
|
+
module.exports = GlobalAumPerAsset30D;
|
|
@@ -1,26 +1,30 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Calculates the $ AUM allocated to each asset for a single PI using a Rolling 30-Day Window.
|
|
3
|
+
*/
|
|
4
|
+
class PIDailyAssetAUM {
|
|
2
5
|
static getMetadata() {
|
|
3
6
|
return {
|
|
4
7
|
type: 'standard',
|
|
5
8
|
category: 'analytics',
|
|
6
9
|
userType: 'POPULAR_INVESTOR',
|
|
7
10
|
|
|
8
|
-
// 1.
|
|
11
|
+
// 1. Core Data
|
|
9
12
|
rootDataDependencies: ['portfolio', 'rankings'],
|
|
10
13
|
mandatoryRoots: ['portfolio', 'rankings'],
|
|
11
14
|
|
|
12
|
-
// 2. Load MY OWN
|
|
13
|
-
//
|
|
15
|
+
// 2. History: Load MY OWN results from the last 29 days
|
|
16
|
+
// CRITICAL: Name must match the File Name (PIDailyAssetAUM)
|
|
14
17
|
dependencySeries: {
|
|
15
|
-
'
|
|
16
|
-
}
|
|
18
|
+
'PIDailyAssetAUM': 29
|
|
19
|
+
},
|
|
20
|
+
|
|
21
|
+
// 3. Bootstrap Flag: Allows running even if history is empty (First Run)
|
|
22
|
+
canHaveMissingSeries: true
|
|
17
23
|
};
|
|
18
24
|
}
|
|
19
25
|
|
|
20
26
|
static getDependencies() { return []; }
|
|
21
27
|
|
|
22
|
-
//
|
|
23
|
-
|
|
24
28
|
async process(context) {
|
|
25
29
|
const { extract, RankingsExtractor } = context.math;
|
|
26
30
|
const userId = context.user.id;
|
|
@@ -47,55 +51,52 @@ class PIDailyRollingAUM {
|
|
|
47
51
|
}
|
|
48
52
|
|
|
49
53
|
// --- STEP B: Load History (T-1 to T-29) ---
|
|
50
|
-
//
|
|
51
|
-
const historyMap = context.globalData.series?.results?.['
|
|
54
|
+
// Access using the FILE NAME key
|
|
55
|
+
const historyMap = context.globalData.series?.results?.['PIDailyAssetAUM'] || {};
|
|
52
56
|
|
|
53
|
-
// Collect all daily maps (History + Today)
|
|
54
57
|
const rollingWindow = [];
|
|
55
58
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
});
|
|
59
|
+
if (historyMap) {
|
|
60
|
+
Object.values(historyMap).forEach(dayResult => {
|
|
61
|
+
const userResult = dayResult[userId];
|
|
62
|
+
if (userResult && userResult.dailyMap) {
|
|
63
|
+
rollingWindow.push(userResult.dailyMap);
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
}
|
|
65
67
|
|
|
66
|
-
//
|
|
68
|
+
// Always add Today
|
|
67
69
|
rollingWindow.push(todayAllocation);
|
|
68
70
|
|
|
69
71
|
// --- STEP C: Compute Rolling Average ---
|
|
70
|
-
const summedStats = {};
|
|
72
|
+
const summedStats = {};
|
|
71
73
|
|
|
72
74
|
rollingWindow.forEach(dayAllocations => {
|
|
73
75
|
Object.entries(dayAllocations).forEach(([ticker, val]) => {
|
|
74
76
|
if (!summedStats[ticker]) summedStats[ticker] = { total: 0, count: 0 };
|
|
75
77
|
summedStats[ticker].total += val;
|
|
76
|
-
summedStats[ticker].count += 1;
|
|
78
|
+
summedStats[ticker].count += 1;
|
|
77
79
|
});
|
|
78
80
|
});
|
|
79
81
|
|
|
80
82
|
const rollingAverage = {};
|
|
81
|
-
const validDays = rollingWindow.length;
|
|
83
|
+
const validDays = rollingWindow.length;
|
|
82
84
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
});
|
|
85
|
+
if (validDays > 0) {
|
|
86
|
+
Object.entries(summedStats).forEach(([ticker, stat]) => {
|
|
87
|
+
rollingAverage[ticker] = stat.total / validDays;
|
|
88
|
+
});
|
|
89
|
+
}
|
|
89
90
|
|
|
90
|
-
// --- STEP D:
|
|
91
|
-
//
|
|
91
|
+
// --- STEP D: Output Results ---
|
|
92
|
+
// StandardExecutor reads 'this.results', it does NOT use the return value.
|
|
92
93
|
this.results = {
|
|
93
94
|
[userId]: {
|
|
94
|
-
dailyMap: todayAllocation,
|
|
95
|
-
averageMap: rollingAverage
|
|
95
|
+
dailyMap: todayAllocation,
|
|
96
|
+
averageMap: rollingAverage
|
|
96
97
|
}
|
|
97
98
|
};
|
|
98
99
|
}
|
|
100
|
+
}
|
|
99
101
|
|
|
100
|
-
|
|
101
|
-
}
|
|
102
|
+
module.exports = PIDailyAssetAUM;
|
|
@@ -1,19 +1,25 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @fileoverview Recommends PIs to Signed-In Users
|
|
2
|
+
* @fileoverview Recommends PIs to Signed-In Users using concrete ID matching.
|
|
3
|
+
* MATCHING STRATEGY:
|
|
4
|
+
* 1. Exclusion: Filter PIs the user already copies (ParentCID).
|
|
5
|
+
* 2. Interest: Page Views (30D History) using 'viewsByUser' schema.
|
|
6
|
+
* 3. Asset Class: Match User's holdings (InstrumentTypeID) vs PI's TopTradedAssetClassId.
|
|
7
|
+
* 4. Instrument: Match User's holdings (InstrumentID) vs PI's TopTradedInstrumentId.
|
|
3
8
|
*/
|
|
4
9
|
class RecommendedPopularInvestors {
|
|
5
10
|
static getMetadata() {
|
|
6
11
|
return {
|
|
7
12
|
type: 'standard',
|
|
8
13
|
category: 'recommendations',
|
|
9
|
-
userType: 'SIGNED_IN_USER',
|
|
14
|
+
userType: 'SIGNED_IN_USER',
|
|
10
15
|
|
|
11
|
-
// We need Portfolio (
|
|
16
|
+
// We need User Portfolio (Deep) and Global Rankings (Summary)
|
|
12
17
|
rootDataDependencies: ['portfolio', 'rankings'],
|
|
13
18
|
mandatoryRoots: ['portfolio', 'rankings'],
|
|
14
|
-
canHaveMissingRoots: true, // Allow running even if pageViews
|
|
19
|
+
canHaveMissingRoots: true, // Allow running even if pageViews/ratings missing
|
|
15
20
|
|
|
16
21
|
// 30-Day History of Page Views for Interest Graph
|
|
22
|
+
// System loads the global daily docs: { [PiCID]: { viewsByUser: { [UserCID]: { viewCount: N } } } }
|
|
17
23
|
rootDataSeries: {
|
|
18
24
|
pageViews: 30
|
|
19
25
|
}
|
|
@@ -26,7 +32,7 @@ class RecommendedPopularInvestors {
|
|
|
26
32
|
return {
|
|
27
33
|
type: 'object',
|
|
28
34
|
patternProperties: {
|
|
29
|
-
'^[0-9]+$': {
|
|
35
|
+
'^[0-9]+$': {
|
|
30
36
|
type: 'array',
|
|
31
37
|
items: {
|
|
32
38
|
type: 'object',
|
|
@@ -43,90 +49,124 @@ class RecommendedPopularInvestors {
|
|
|
43
49
|
}
|
|
44
50
|
|
|
45
51
|
async process(context) {
|
|
46
|
-
const {
|
|
52
|
+
const { RankingsExtractor, CopyTradingExtractor, PageViewsExtractor, DataExtractor } = context.math;
|
|
47
53
|
const userPortfolio = context.user.portfolio.today;
|
|
48
54
|
const allRankings = context.globalData.rankings || [];
|
|
49
55
|
const pageViewSeries = context.globalData.series.root.pageViews || {};
|
|
50
56
|
|
|
51
|
-
//
|
|
57
|
+
// --- STEP 1: USER PROFILE EXTRACTION ---
|
|
58
|
+
|
|
59
|
+
// A. Who do I copy? (Exclusion List)
|
|
60
|
+
// Schema: AggregatedMirrors -> ParentCID (e.g., 5125148)
|
|
52
61
|
const mirrors = CopyTradingExtractor.getAggregatedMirrors(userPortfolio);
|
|
53
62
|
const copiedCids = new Set(mirrors.map(m => String(CopyTradingExtractor.getParentCID(m))));
|
|
54
63
|
copiedCids.add(String(context.user.id)); // Exclude self
|
|
55
64
|
|
|
56
|
-
//
|
|
57
|
-
//
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
const current = piInterestScore.get(piCid) || 0;
|
|
73
|
-
piInterestScore.set(piCid, current + (userViews * 2)); // 2 points per view
|
|
74
|
-
}
|
|
75
|
-
}
|
|
65
|
+
// B. What Asset Classes do I hold?
|
|
66
|
+
// Schema: AggregatedPositionsByInstrumentTypeID -> InstrumentTypeID (e.g., 5=Crypto, 4=Stocks)
|
|
67
|
+
const userAssetClasses = new Set();
|
|
68
|
+
if (userPortfolio && Array.isArray(userPortfolio.AggregatedPositionsByInstrumentTypeID)) {
|
|
69
|
+
userPortfolio.AggregatedPositionsByInstrumentTypeID.forEach(grp => {
|
|
70
|
+
if (grp.InstrumentTypeID) userAssetClasses.add(Number(grp.InstrumentTypeID));
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// C. What Instruments do I hold?
|
|
75
|
+
// Schema: AggregatedPositions -> InstrumentID (e.g., 1353)
|
|
76
|
+
const userInstruments = new Set();
|
|
77
|
+
const positions = DataExtractor.getPositions(userPortfolio, context.user.type);
|
|
78
|
+
positions.forEach(pos => {
|
|
79
|
+
const id = DataExtractor.getInstrumentId(pos);
|
|
80
|
+
if (id) userInstruments.add(Number(id));
|
|
76
81
|
});
|
|
77
82
|
|
|
78
|
-
//
|
|
79
|
-
|
|
83
|
+
// --- STEP 2: INTEREST GRAPH (Page Views) ---
|
|
84
|
+
// Iterate through 30 days of global page view maps
|
|
85
|
+
const piInterestScore = new Map();
|
|
86
|
+
if (pageViewSeries) {
|
|
87
|
+
Object.values(pageViewSeries).forEach(dailyGlobalData => {
|
|
88
|
+
// dailyGlobalData matches your schema: { "31075566": { viewsByUser: { ... } } }
|
|
89
|
+
|
|
90
|
+
// Extractor handles filtering 'lastUpdated' and iterating PIs
|
|
91
|
+
const allPiCids = PageViewsExtractor.getAllPIs(dailyGlobalData);
|
|
92
|
+
|
|
93
|
+
for (const piCid of allPiCids) {
|
|
94
|
+
if (copiedCids.has(piCid)) continue;
|
|
95
|
+
|
|
96
|
+
// Extractor looks inside 'viewsByUser' -> [UserCID] -> 'viewCount'
|
|
97
|
+
const userViews = PageViewsExtractor.getUserViewCount(dailyGlobalData, piCid, context.user.id);
|
|
98
|
+
|
|
99
|
+
if (userViews > 0) {
|
|
100
|
+
const current = piInterestScore.get(piCid) || 0;
|
|
101
|
+
// Score logic: 2 points per view
|
|
102
|
+
piInterestScore.set(piCid, current + (userViews * 2));
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
}
|
|
80
107
|
|
|
81
|
-
//
|
|
108
|
+
// --- STEP 3: MATCHING LOGIC ---
|
|
82
109
|
const recommendations = [];
|
|
83
110
|
|
|
84
111
|
for (const piEntry of allRankings) {
|
|
85
112
|
const piCid = String(piEntry.CustomerId || piEntry.cid);
|
|
113
|
+
|
|
114
|
+
// 1. Exclusion
|
|
86
115
|
if (copiedCids.has(piCid)) continue;
|
|
87
116
|
|
|
88
117
|
let score = 0;
|
|
89
118
|
const reasons = [];
|
|
90
119
|
|
|
91
|
-
//
|
|
120
|
+
// 2. Interest (Page Views)
|
|
92
121
|
const viewScore = piInterestScore.get(piCid) || 0;
|
|
93
122
|
if (viewScore > 0) {
|
|
94
|
-
score += Math.min(20, viewScore);
|
|
123
|
+
score += Math.min(20, viewScore);
|
|
95
124
|
reasons.push('Recently Viewed');
|
|
96
125
|
}
|
|
97
126
|
|
|
98
|
-
//
|
|
99
|
-
|
|
100
|
-
const
|
|
101
|
-
if (
|
|
127
|
+
// 3. Asset Class Match (Hard ID Match)
|
|
128
|
+
// Schema: Rankings -> TopTradedAssetClassId (e.g., 5)
|
|
129
|
+
const piAssetClass = piEntry.TopTradedAssetClassId;
|
|
130
|
+
if (piAssetClass && userAssetClasses.has(piAssetClass)) {
|
|
102
131
|
score += 15;
|
|
103
|
-
reasons.push('
|
|
132
|
+
reasons.push('Trades Your Asset Classes');
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// 4. Top Instrument Match (Hard ID Match)
|
|
136
|
+
// Schema: Rankings -> TopTradedInstrumentId (e.g., 1155)
|
|
137
|
+
const piTopInst = piEntry.TopTradedInstrumentId;
|
|
138
|
+
if (piTopInst && userInstruments.has(piTopInst)) {
|
|
139
|
+
score += 10;
|
|
140
|
+
reasons.push('Trades Your Top Assets');
|
|
104
141
|
}
|
|
105
142
|
|
|
106
|
-
//
|
|
143
|
+
// 5. Risk Filter (Baseline Quality)
|
|
144
|
+
// Schema: Rankings -> RiskScore
|
|
107
145
|
const risk = RankingsExtractor.getRiskScore(piEntry);
|
|
108
|
-
if (risk <= 6)
|
|
109
|
-
|
|
110
|
-
|
|
146
|
+
if (risk <= 6) {
|
|
147
|
+
score += 5;
|
|
148
|
+
reasons.push('Stable Risk Score');
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Push if there is a signal
|
|
111
152
|
if (score > 0) {
|
|
112
153
|
recommendations.push({
|
|
113
154
|
cid: piCid,
|
|
114
155
|
username: piEntry.UserName || "Unknown",
|
|
115
156
|
matchScore: score,
|
|
116
|
-
reason
|
|
157
|
+
// Ensure a default reason exists
|
|
158
|
+
reason: reasons[0] || 'Recommended Strategy'
|
|
117
159
|
});
|
|
118
160
|
}
|
|
119
161
|
}
|
|
120
162
|
|
|
121
|
-
//
|
|
163
|
+
// --- STEP 4: SORT & LIMIT ---
|
|
122
164
|
recommendations.sort((a, b) => b.matchScore - a.matchScore);
|
|
123
165
|
|
|
124
166
|
this.results = {
|
|
125
167
|
[context.user.id]: recommendations.slice(0, 5)
|
|
126
168
|
};
|
|
127
169
|
}
|
|
128
|
-
|
|
129
|
-
async getResult() { return this.results; }
|
|
130
170
|
}
|
|
131
171
|
|
|
132
172
|
module.exports = RecommendedPopularInvestors;
|
package/package.json
CHANGED
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @fileoverview Aggregate AUM per Asset Across All Users.
|
|
3
|
-
* Returns the AUM invested into each asset total across all users.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
class AggregateAssetAUM {
|
|
7
|
-
constructor() {
|
|
8
|
-
this.results = {};
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
static getMetadata() {
|
|
12
|
-
return {
|
|
13
|
-
type: 'meta',
|
|
14
|
-
category: 'popular_investor',
|
|
15
|
-
userType: 'POPULAR_INVESTOR',
|
|
16
|
-
// [FIX] Enforce strict availability check.
|
|
17
|
-
// Prevents execution if PI data is missing, even if upstream calc exists.
|
|
18
|
-
rootDataDependencies: ['portfolio']
|
|
19
|
-
};
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
static getDependencies() { return ['UserAUMPerAsset']; }
|
|
23
|
-
|
|
24
|
-
static getSchema() {
|
|
25
|
-
return {
|
|
26
|
-
type: 'object',
|
|
27
|
-
additionalProperties: { type: 'number' },
|
|
28
|
-
description: 'Map of instrumentId -> total AUM across all users'
|
|
29
|
-
};
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
process(context) {
|
|
33
|
-
// Input: Map of { userId: { instrumentId: aum } }
|
|
34
|
-
const aumPerAssetData = context.computed['UserAUMPerAsset'];
|
|
35
|
-
|
|
36
|
-
if (!aumPerAssetData) {
|
|
37
|
-
return {};
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// Aggregate AUM per instrument across all users
|
|
41
|
-
const instrumentAUM = new Map();
|
|
42
|
-
|
|
43
|
-
for (const [userId, userAssets] of Object.entries(aumPerAssetData)) {
|
|
44
|
-
if (!userAssets || typeof userAssets !== 'object') continue;
|
|
45
|
-
|
|
46
|
-
for (const [instrumentIdStr, aum] of Object.entries(userAssets)) {
|
|
47
|
-
const instrumentId = Number(instrumentIdStr);
|
|
48
|
-
// Validate AUM is a number and valid instrument ID
|
|
49
|
-
if (isNaN(instrumentId) || typeof aum !== 'number' || aum <= 0) continue;
|
|
50
|
-
|
|
51
|
-
const current = instrumentAUM.get(instrumentId) || 0;
|
|
52
|
-
instrumentAUM.set(instrumentId, current + aum);
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
// Convert Map to output Object
|
|
57
|
-
const result = {};
|
|
58
|
-
for (const [instrumentId, totalAUM] of instrumentAUM.entries()) {
|
|
59
|
-
result[instrumentId] = Number(totalAUM.toFixed(2));
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
return result;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
async getResult() {
|
|
66
|
-
return this.results;
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
module.exports = AggregateAssetAUM;
|
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @fileoverview Aggregate Daily AUM Across All Users.
|
|
3
|
-
* Returns the overall AUM values each day total across all users.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
class AggregateDailyAUM {
|
|
7
|
-
constructor() {
|
|
8
|
-
this.results = {};
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
static getMetadata() {
|
|
12
|
-
return {
|
|
13
|
-
type: 'meta',
|
|
14
|
-
category: 'popular_investor',
|
|
15
|
-
userType: 'POPULAR_INVESTOR'
|
|
16
|
-
};
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
static getDependencies() { return ['UserAUM30Day']; }
|
|
20
|
-
|
|
21
|
-
static getSchema() {
|
|
22
|
-
return {
|
|
23
|
-
type: 'object',
|
|
24
|
-
additionalProperties: { type: 'number' },
|
|
25
|
-
description: 'Map of date -> total AUM across all users'
|
|
26
|
-
};
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
process(context) {
|
|
30
|
-
const aumData = context.computed['UserAUM30Day'];
|
|
31
|
-
|
|
32
|
-
// [FIX] Defensive check
|
|
33
|
-
if (!aumData) {
|
|
34
|
-
return {};
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
// Aggregate AUM by date
|
|
38
|
-
const dateMap = {};
|
|
39
|
-
|
|
40
|
-
for (const [userId, userHistory] of Object.entries(aumData)) {
|
|
41
|
-
// [FIX] Validate userHistory exists
|
|
42
|
-
if (!userHistory) continue;
|
|
43
|
-
|
|
44
|
-
// [FIX] "30Day" data is an Array of daily entries.
|
|
45
|
-
// We must iterate the array. We also handle the edge case where it might be a single object.
|
|
46
|
-
const entries = Array.isArray(userHistory) ? userHistory : [userHistory];
|
|
47
|
-
|
|
48
|
-
for (const entry of entries) {
|
|
49
|
-
if (entry && typeof entry === 'object' && entry.date && typeof entry.aum === 'number') {
|
|
50
|
-
const date = entry.date;
|
|
51
|
-
// Sum the AUM for this specific date
|
|
52
|
-
dateMap[date] = (dateMap[date] || 0) + entry.aum;
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// Round values to 2 decimals
|
|
58
|
-
const rounded = {};
|
|
59
|
-
for (const [date, total] of Object.entries(dateMap)) {
|
|
60
|
-
rounded[date] = Number(total.toFixed(2));
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// [FIX] Must RETURN the result for MetaExecutor
|
|
64
|
-
return rounded;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
async getResult() {
|
|
68
|
-
return this.results;
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
module.exports = AggregateDailyAUM;
|
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @fileoverview AUM Value Over Last 30 Days for Users.
|
|
3
|
-
* Returns the AUM value for the current day.
|
|
4
|
-
* (Historical aggregation handled by Database/API or 'isHistorical' mechanisms in Meta scripts).
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
class UserAUM30Day {
|
|
8
|
-
constructor() {
|
|
9
|
-
this.results = {};
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
static getMetadata() {
|
|
13
|
-
return {
|
|
14
|
-
type: 'standard',
|
|
15
|
-
category: 'popular_investor',
|
|
16
|
-
rootDataDependencies: ['portfolio', 'rankings'],
|
|
17
|
-
userType: 'POPULAR_INVESTOR'
|
|
18
|
-
};
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
static getDependencies() { return []; }
|
|
22
|
-
|
|
23
|
-
static getSchema() {
|
|
24
|
-
return {
|
|
25
|
-
type: 'object',
|
|
26
|
-
properties: {
|
|
27
|
-
date: { type: 'string', format: 'date' },
|
|
28
|
-
aum: { type: 'number' }
|
|
29
|
-
}
|
|
30
|
-
};
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
process(context) {
|
|
34
|
-
const { RankingsExtractor, PopularInvestorExtractor } = context.math;
|
|
35
|
-
const userId = context.user.id;
|
|
36
|
-
const portfolio = context.user.portfolio.today;
|
|
37
|
-
const rankEntry = context.user.rankEntry;
|
|
38
|
-
const dateStr = context.date.today;
|
|
39
|
-
|
|
40
|
-
let aum = 0;
|
|
41
|
-
|
|
42
|
-
// 1. Priority: Official Rankings Data
|
|
43
|
-
if (rankEntry) {
|
|
44
|
-
aum = RankingsExtractor.getAUMValue(rankEntry) || 0;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
// 2. Fallback: Portfolio Snapshot Data
|
|
48
|
-
if ((!aum || aum === 0) && portfolio) {
|
|
49
|
-
if (typeof portfolio.AUMValue === 'number') {
|
|
50
|
-
aum = portfolio.AUMValue;
|
|
51
|
-
} else {
|
|
52
|
-
aum = PopularInvestorExtractor.getEquity(portfolio);
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
this.results[userId] = {
|
|
57
|
-
date: dateStr,
|
|
58
|
-
aum: Number(aum.toFixed(2))
|
|
59
|
-
};
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
async getResult() {
|
|
63
|
-
return this.results;
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
module.exports = UserAUM30Day;
|
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @fileoverview AUM per Asset for Users.
|
|
3
|
-
* Returns the AUM invested into each asset per user.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
class UserAUMPerAsset {
|
|
7
|
-
constructor() {
|
|
8
|
-
this.results = {};
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
static getMetadata() {
|
|
12
|
-
return {
|
|
13
|
-
type: 'standard',
|
|
14
|
-
category: 'popular_investor',
|
|
15
|
-
rootDataDependencies: ['portfolio'],
|
|
16
|
-
userType: 'POPULAR_INVESTOR'
|
|
17
|
-
};
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
static getDependencies() { return []; }
|
|
21
|
-
|
|
22
|
-
static getSchema() {
|
|
23
|
-
return {
|
|
24
|
-
type: 'object',
|
|
25
|
-
additionalProperties: { type: 'number' },
|
|
26
|
-
description: 'Map of instrumentId -> AUM value invested'
|
|
27
|
-
};
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
process(context) {
|
|
31
|
-
const { DataExtractor } = context.math;
|
|
32
|
-
const userId = context.user.id;
|
|
33
|
-
const portfolio = context.user.portfolio.today;
|
|
34
|
-
|
|
35
|
-
if (!portfolio) {
|
|
36
|
-
this.results[userId] = {};
|
|
37
|
-
return;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// [FIX] Inline aggregation logic
|
|
41
|
-
const positions = DataExtractor.getPositions(portfolio, 'POPULAR_INVESTOR');
|
|
42
|
-
const aumMap = {};
|
|
43
|
-
|
|
44
|
-
for (const pos of positions) {
|
|
45
|
-
const instId = DataExtractor.getInstrumentId(pos);
|
|
46
|
-
const value = DataExtractor.getPositionValue(pos); // Value in USD
|
|
47
|
-
|
|
48
|
-
if (instId && value > 0) {
|
|
49
|
-
// Ensure key is numeric or string consistent with consumption
|
|
50
|
-
aumMap[instId] = (aumMap[instId] || 0) + value;
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
// Round values
|
|
55
|
-
for (const key in aumMap) {
|
|
56
|
-
aumMap[key] = Number(aumMap[key].toFixed(2));
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
this.results[userId] = aumMap;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
async getResult() {
|
|
63
|
-
return this.results;
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
module.exports = UserAUMPerAsset;
|