aiden-shared-calculations-unified 1.0.113 → 1.0.115
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/popular-investor/AggregateAssetAUM.js +66 -0
- package/calculations/popular-investor/AggregateDailyAUM.js +60 -0
- package/calculations/popular-investor/AggregateSectorRanking.js +79 -0
- package/calculations/popular-investor/AggregateStockRanking.js +82 -0
- package/calculations/popular-investor/PiCryptoBuyers.js +119 -0
- package/calculations/popular-investor/PiHighVolatility.js +133 -0
- package/calculations/popular-investor/PiRiskIncrease.js +132 -0
- package/calculations/popular-investor/SignedInUserCopiedPIs.js +89 -0
- package/calculations/popular-investor/SignedInUserProfileMetrics.js +479 -0
- package/calculations/popular-investor/UserAUM30Day.js +72 -0
- package/calculations/popular-investor/UserAUMPerAsset.js +52 -0
- package/calculations/popular-investor/UserCurrentSectors.js +52 -0
- package/calculations/popular-investor/UserHistoricalSectors.js +51 -0
- package/calculations/popular-investor/UserRiskScore.js +62 -0
- package/package.json +1 -1
|
@@ -0,0 +1,66 @@
|
|
|
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_meta'
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
static getDependencies() { return ['UserAUMPerAsset']; }
|
|
19
|
+
|
|
20
|
+
static getSchema() {
|
|
21
|
+
return {
|
|
22
|
+
type: 'object',
|
|
23
|
+
additionalProperties: { type: 'number' },
|
|
24
|
+
description: 'Map of instrumentId -> total AUM across all users'
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
process(context) {
|
|
29
|
+
const aumPerAssetData = context.computed['UserAUMPerAsset'];
|
|
30
|
+
|
|
31
|
+
if (!aumPerAssetData) {
|
|
32
|
+
this.results['meta'] = {};
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Aggregate AUM per instrument across all users
|
|
37
|
+
const instrumentAUM = new Map();
|
|
38
|
+
|
|
39
|
+
for (const [userId, userAssets] of Object.entries(aumPerAssetData)) {
|
|
40
|
+
if (!userAssets || typeof userAssets !== 'object') continue;
|
|
41
|
+
|
|
42
|
+
for (const [instrumentIdStr, aum] of Object.entries(userAssets)) {
|
|
43
|
+
const instrumentId = Number(instrumentIdStr);
|
|
44
|
+
if (isNaN(instrumentId) || !aum) continue;
|
|
45
|
+
|
|
46
|
+
const current = instrumentAUM.get(instrumentId) || 0;
|
|
47
|
+
instrumentAUM.set(instrumentId, current + aum);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Convert to object
|
|
52
|
+
const result = {};
|
|
53
|
+
for (const [instrumentId, totalAUM] of instrumentAUM.entries()) {
|
|
54
|
+
result[instrumentId] = Number(totalAUM.toFixed(2));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
this.results['meta'] = result;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async getResult() {
|
|
61
|
+
return this.results;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
module.exports = AggregateAssetAUM;
|
|
66
|
+
|
|
@@ -0,0 +1,60 @@
|
|
|
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_meta'
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
static getDependencies() { return ['UserAUM30Day']; }
|
|
19
|
+
|
|
20
|
+
static getSchema() {
|
|
21
|
+
return {
|
|
22
|
+
type: 'object',
|
|
23
|
+
additionalProperties: { type: 'number' },
|
|
24
|
+
description: 'Map of date -> total AUM across all users'
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
process(context) {
|
|
29
|
+
const aumData = context.computed['UserAUM30Day'];
|
|
30
|
+
if (!aumData) {
|
|
31
|
+
this.results['meta'] = {};
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Aggregate AUM by date
|
|
36
|
+
const dateMap = {};
|
|
37
|
+
|
|
38
|
+
for (const [userId, userData] of Object.entries(aumData)) {
|
|
39
|
+
if (userData && typeof userData === 'object' && userData.date && typeof userData.aum === 'number') {
|
|
40
|
+
const date = userData.date;
|
|
41
|
+
dateMap[date] = (dateMap[date] || 0) + userData.aum;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Round values
|
|
46
|
+
const rounded = {};
|
|
47
|
+
for (const [date, total] of Object.entries(dateMap)) {
|
|
48
|
+
rounded[date] = Number(total.toFixed(2));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
this.results['meta'] = rounded;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async getResult() {
|
|
55
|
+
return this.results;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
module.exports = AggregateDailyAUM;
|
|
60
|
+
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Aggregate Sector Ranking by AUM.
|
|
3
|
+
* Returns the ranked order of sectors invested in each day across all users (based on AUM).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
class AggregateSectorRanking {
|
|
7
|
+
constructor() {
|
|
8
|
+
this.results = {};
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
static getMetadata() {
|
|
12
|
+
return {
|
|
13
|
+
type: 'meta',
|
|
14
|
+
category: 'popular_investor_meta'
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
static getDependencies() { return ['UserAUMPerAsset']; }
|
|
19
|
+
|
|
20
|
+
static getSchema() {
|
|
21
|
+
return {
|
|
22
|
+
type: 'array',
|
|
23
|
+
items: {
|
|
24
|
+
type: 'object',
|
|
25
|
+
properties: {
|
|
26
|
+
sector: { type: 'string' },
|
|
27
|
+
aum: { type: 'number' },
|
|
28
|
+
rank: { type: 'number' }
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
description: 'Ranked list of sectors by total AUM invested'
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
process(context) {
|
|
36
|
+
const aumPerAssetData = context.computed['UserAUMPerAsset'];
|
|
37
|
+
const mappings = context.mappings;
|
|
38
|
+
|
|
39
|
+
if (!aumPerAssetData || !mappings?.instrumentToSector) {
|
|
40
|
+
this.results['meta'] = [];
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Aggregate AUM per sector across all users
|
|
45
|
+
// Use instrument-to-sector mapping to calculate accurate sector AUM
|
|
46
|
+
const sectorAUM = new Map();
|
|
47
|
+
|
|
48
|
+
for (const [userId, userAssets] of Object.entries(aumPerAssetData)) {
|
|
49
|
+
if (!userAssets || typeof userAssets !== 'object') continue;
|
|
50
|
+
|
|
51
|
+
for (const [instrumentIdStr, aum] of Object.entries(userAssets)) {
|
|
52
|
+
const instrumentId = Number(instrumentIdStr);
|
|
53
|
+
if (isNaN(instrumentId) || !aum) continue;
|
|
54
|
+
|
|
55
|
+
// Map instrument to sector
|
|
56
|
+
const sector = mappings.instrumentToSector[instrumentId] || 'Unknown';
|
|
57
|
+
|
|
58
|
+
// Add AUM to sector total
|
|
59
|
+
const current = sectorAUM.get(sector) || 0;
|
|
60
|
+
sectorAUM.set(sector, current + aum);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Convert to array and rank
|
|
65
|
+
const ranked = Array.from(sectorAUM.entries())
|
|
66
|
+
.map(([sector, aum]) => ({ sector, aum: Number(aum.toFixed(2)) }))
|
|
67
|
+
.sort((a, b) => b.aum - a.aum)
|
|
68
|
+
.map((item, index) => ({ ...item, rank: index + 1 }));
|
|
69
|
+
|
|
70
|
+
this.results['meta'] = ranked;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async getResult() {
|
|
74
|
+
return this.results;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
module.exports = AggregateSectorRanking;
|
|
79
|
+
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Aggregate Stock Ranking by AUM.
|
|
3
|
+
* Returns the ranked order of most invested in stocks each day across all users (based on AUM).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
class AggregateStockRanking {
|
|
7
|
+
constructor() {
|
|
8
|
+
this.results = {};
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
static getMetadata() {
|
|
12
|
+
return {
|
|
13
|
+
type: 'meta',
|
|
14
|
+
category: 'popular_investor_meta'
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
static getDependencies() { return ['UserAUMPerAsset']; }
|
|
19
|
+
|
|
20
|
+
static getSchema() {
|
|
21
|
+
return {
|
|
22
|
+
type: 'array',
|
|
23
|
+
items: {
|
|
24
|
+
type: 'object',
|
|
25
|
+
properties: {
|
|
26
|
+
instrumentId: { type: 'number' },
|
|
27
|
+
ticker: { type: 'string' },
|
|
28
|
+
aum: { type: 'number' },
|
|
29
|
+
rank: { type: 'number' }
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
description: 'Ranked list of stocks by total AUM invested'
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
process(context) {
|
|
37
|
+
const aumPerAssetData = context.computed['UserAUMPerAsset'];
|
|
38
|
+
const mappings = context.mappings;
|
|
39
|
+
|
|
40
|
+
if (!aumPerAssetData) {
|
|
41
|
+
this.results['meta'] = [];
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Aggregate AUM per instrument across all users
|
|
46
|
+
const instrumentAUM = new Map();
|
|
47
|
+
|
|
48
|
+
for (const [userId, userAssets] of Object.entries(aumPerAssetData)) {
|
|
49
|
+
if (!userAssets || typeof userAssets !== 'object') continue;
|
|
50
|
+
|
|
51
|
+
for (const [instrumentIdStr, aum] of Object.entries(userAssets)) {
|
|
52
|
+
const instrumentId = Number(instrumentIdStr);
|
|
53
|
+
if (isNaN(instrumentId) || !aum) continue;
|
|
54
|
+
|
|
55
|
+
const current = instrumentAUM.get(instrumentId) || 0;
|
|
56
|
+
instrumentAUM.set(instrumentId, current + aum);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Convert to array, add ticker, and rank
|
|
61
|
+
const ranked = Array.from(instrumentAUM.entries())
|
|
62
|
+
.map(([instrumentId, aum]) => {
|
|
63
|
+
const ticker = mappings?.instrumentToTicker?.[instrumentId] || `ID${instrumentId}`;
|
|
64
|
+
return {
|
|
65
|
+
instrumentId,
|
|
66
|
+
ticker,
|
|
67
|
+
aum: Number(aum.toFixed(2))
|
|
68
|
+
};
|
|
69
|
+
})
|
|
70
|
+
.sort((a, b) => b.aum - a.aum)
|
|
71
|
+
.map((item, index) => ({ ...item, rank: index + 1 }));
|
|
72
|
+
|
|
73
|
+
this.results['meta'] = ranked;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async getResult() {
|
|
77
|
+
return this.results;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
module.exports = AggregateStockRanking;
|
|
82
|
+
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Popular Investors Who Bought Crypto
|
|
3
|
+
* Returns a list of PI CIDs who bought crypto instruments in the last N days.
|
|
4
|
+
* Used for dynamic watchlists and alerts.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
class PiCryptoBuyers {
|
|
8
|
+
constructor() {
|
|
9
|
+
this.results = {};
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
static getMetadata() {
|
|
13
|
+
return {
|
|
14
|
+
type: 'meta',
|
|
15
|
+
category: 'popular_investor',
|
|
16
|
+
rootDataDependencies: ['portfolio', 'history'],
|
|
17
|
+
userType: 'POPULAR_INVESTOR'
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
static getDependencies() { return []; }
|
|
22
|
+
|
|
23
|
+
static getSchema() {
|
|
24
|
+
return {
|
|
25
|
+
type: 'object',
|
|
26
|
+
properties: {
|
|
27
|
+
cids: {
|
|
28
|
+
type: 'array',
|
|
29
|
+
items: { type: 'number' },
|
|
30
|
+
description: 'List of PI CIDs who bought crypto'
|
|
31
|
+
},
|
|
32
|
+
metadata: {
|
|
33
|
+
type: 'object',
|
|
34
|
+
properties: {
|
|
35
|
+
timeWindow: { type: 'number' },
|
|
36
|
+
count: { type: 'number' },
|
|
37
|
+
timestamp: { type: 'string' }
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
description: 'PIs who bought crypto in the last N days'
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
process(context) {
|
|
46
|
+
const { DataExtractor, HistoryExtractor } = context.math;
|
|
47
|
+
const portfolio = context.user?.portfolio?.today;
|
|
48
|
+
const historyDoc = context.user?.history?.today;
|
|
49
|
+
const mappings = context.mappings;
|
|
50
|
+
|
|
51
|
+
// Default time window: 7 days
|
|
52
|
+
const TIME_WINDOW_DAYS = 7;
|
|
53
|
+
const cutoffDate = new Date();
|
|
54
|
+
cutoffDate.setDate(cutoffDate.getDate() - TIME_WINDOW_DAYS);
|
|
55
|
+
|
|
56
|
+
// Crypto instrument type IDs (common crypto types in eToro)
|
|
57
|
+
// InstrumentTypeID 28 = Crypto
|
|
58
|
+
const CRYPTO_INSTRUMENT_TYPES = [28]; // Can be expanded
|
|
59
|
+
|
|
60
|
+
const cryptoBuyerCIDs = new Set();
|
|
61
|
+
|
|
62
|
+
// Check current portfolio for crypto positions
|
|
63
|
+
if (portfolio) {
|
|
64
|
+
const positions = DataExtractor.getPositions(portfolio, 'POPULAR_INVESTOR');
|
|
65
|
+
|
|
66
|
+
for (const pos of positions) {
|
|
67
|
+
const instrumentType = DataExtractor.getInstrumentTypeId(pos);
|
|
68
|
+
|
|
69
|
+
if (CRYPTO_INSTRUMENT_TYPES.includes(instrumentType)) {
|
|
70
|
+
const userId = context.user?.id;
|
|
71
|
+
if (userId) {
|
|
72
|
+
cryptoBuyerCIDs.add(Number(userId));
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Check trade history for recent crypto purchases
|
|
79
|
+
if (historyDoc) {
|
|
80
|
+
const historyPositions = HistoryExtractor.getHistoryPositions(historyDoc);
|
|
81
|
+
|
|
82
|
+
for (const trade of historyPositions) {
|
|
83
|
+
const openDate = trade.OpenDateTime ? new Date(trade.OpenDateTime) : null;
|
|
84
|
+
|
|
85
|
+
if (!openDate || openDate < cutoffDate) {
|
|
86
|
+
continue; // Too old
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Check if this is a crypto instrument
|
|
90
|
+
// For now, we'll check if the instrument type is crypto
|
|
91
|
+
// In a full implementation, we'd use instrument type mapping
|
|
92
|
+
const instrumentType = trade.InstrumentTypeID || 0;
|
|
93
|
+
|
|
94
|
+
if (CRYPTO_INSTRUMENT_TYPES.includes(instrumentType)) {
|
|
95
|
+
const userId = context.user?.id;
|
|
96
|
+
if (userId) {
|
|
97
|
+
cryptoBuyerCIDs.add(Number(userId));
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
this.results['meta'] = {
|
|
104
|
+
cids: Array.from(cryptoBuyerCIDs),
|
|
105
|
+
metadata: {
|
|
106
|
+
timeWindow: TIME_WINDOW_DAYS,
|
|
107
|
+
count: cryptoBuyerCIDs.size,
|
|
108
|
+
timestamp: new Date().toISOString()
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async getResult() {
|
|
114
|
+
return this.results;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
module.exports = PiCryptoBuyers;
|
|
119
|
+
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Popular Investors with High Volatility Changes
|
|
3
|
+
* Returns a list of PI CIDs with significant volatility changes.
|
|
4
|
+
* Used for dynamic watchlists and alerts.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
class PiHighVolatility {
|
|
8
|
+
constructor() {
|
|
9
|
+
this.results = {};
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
static getMetadata() {
|
|
13
|
+
return {
|
|
14
|
+
type: 'meta',
|
|
15
|
+
category: 'popular_investor',
|
|
16
|
+
rootDataDependencies: ['rankings', 'portfolio'],
|
|
17
|
+
userType: 'POPULAR_INVESTOR'
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
static getDependencies() { return []; }
|
|
22
|
+
|
|
23
|
+
static getSchema() {
|
|
24
|
+
return {
|
|
25
|
+
type: 'object',
|
|
26
|
+
properties: {
|
|
27
|
+
cids: {
|
|
28
|
+
type: 'array',
|
|
29
|
+
items: { type: 'number' },
|
|
30
|
+
description: 'List of PI CIDs with high volatility changes'
|
|
31
|
+
},
|
|
32
|
+
metadata: {
|
|
33
|
+
type: 'object',
|
|
34
|
+
properties: {
|
|
35
|
+
volatilityThreshold: { type: 'number' },
|
|
36
|
+
timeWindow: { type: 'number' },
|
|
37
|
+
count: { type: 'number' },
|
|
38
|
+
timestamp: { type: 'string' }
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
description: 'PIs with high volatility changes'
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
process(context) {
|
|
47
|
+
const { RankingsExtractor, DataExtractor } = context.math;
|
|
48
|
+
|
|
49
|
+
// Default threshold: 50% volatility change
|
|
50
|
+
const VOLATILITY_THRESHOLD = 0.5; // 50%
|
|
51
|
+
|
|
52
|
+
const rankings = context.user?.rankings;
|
|
53
|
+
const portfolio = context.user?.portfolio?.today;
|
|
54
|
+
|
|
55
|
+
if (!rankings || !portfolio) {
|
|
56
|
+
this.results['meta'] = {
|
|
57
|
+
cids: [],
|
|
58
|
+
metadata: {
|
|
59
|
+
volatilityThreshold: VOLATILITY_THRESHOLD,
|
|
60
|
+
timeWindow: 1,
|
|
61
|
+
count: 0,
|
|
62
|
+
timestamp: new Date().toISOString()
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const todayRankings = rankings.today || {};
|
|
69
|
+
const yesterdayRankings = rankings.yesterday || {};
|
|
70
|
+
|
|
71
|
+
const todayItems = RankingsExtractor.getItems(todayRankings);
|
|
72
|
+
const yesterdayItems = RankingsExtractor.getItems(yesterdayRankings);
|
|
73
|
+
|
|
74
|
+
// Create maps for quick lookup
|
|
75
|
+
const todayMap = new Map();
|
|
76
|
+
for (const item of todayItems) {
|
|
77
|
+
if (item.CustomerId) {
|
|
78
|
+
todayMap.set(String(item.CustomerId), item);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const yesterdayMap = new Map();
|
|
83
|
+
for (const item of yesterdayItems) {
|
|
84
|
+
if (item.CustomerId) {
|
|
85
|
+
yesterdayMap.set(String(item.CustomerId), item);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const highVolatilityCIDs = [];
|
|
90
|
+
|
|
91
|
+
// Check each PI for volatility changes
|
|
92
|
+
for (const [cid, todayItem] of todayMap.entries()) {
|
|
93
|
+
const yesterdayItem = yesterdayMap.get(cid);
|
|
94
|
+
|
|
95
|
+
if (!yesterdayItem) {
|
|
96
|
+
continue; // New PI, skip
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Calculate volatility from DailyDD (Daily Drawdown) or WeeklyDD
|
|
100
|
+
// Using DailyDD as a proxy for volatility
|
|
101
|
+
const todayDD = todayItem.DailyDD || 0;
|
|
102
|
+
const yesterdayDD = yesterdayItem.DailyDD || 0;
|
|
103
|
+
|
|
104
|
+
if (yesterdayDD === 0) {
|
|
105
|
+
continue; // Can't calculate change from zero
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Calculate percentage change in drawdown (volatility proxy)
|
|
109
|
+
const volatilityChange = Math.abs((todayDD - yesterdayDD) / Math.abs(yesterdayDD));
|
|
110
|
+
|
|
111
|
+
if (volatilityChange >= VOLATILITY_THRESHOLD) {
|
|
112
|
+
highVolatilityCIDs.push(Number(cid));
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
this.results['meta'] = {
|
|
117
|
+
cids: highVolatilityCIDs,
|
|
118
|
+
metadata: {
|
|
119
|
+
volatilityThreshold: VOLATILITY_THRESHOLD,
|
|
120
|
+
timeWindow: 1,
|
|
121
|
+
count: highVolatilityCIDs.length,
|
|
122
|
+
timestamp: new Date().toISOString()
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async getResult() {
|
|
128
|
+
return this.results;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
module.exports = PiHighVolatility;
|
|
133
|
+
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Popular Investors with Risk Score Increase
|
|
3
|
+
* Returns a list of PI CIDs whose risk score increased by a threshold amount.
|
|
4
|
+
* Used for dynamic watchlists and alerts.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
class PiRiskIncrease {
|
|
8
|
+
constructor() {
|
|
9
|
+
this.results = {};
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
static getMetadata() {
|
|
13
|
+
return {
|
|
14
|
+
type: 'meta',
|
|
15
|
+
category: 'popular_investor',
|
|
16
|
+
rootDataDependencies: ['rankings'],
|
|
17
|
+
userType: 'POPULAR_INVESTOR'
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
static getDependencies() { return []; }
|
|
22
|
+
|
|
23
|
+
static getSchema() {
|
|
24
|
+
return {
|
|
25
|
+
type: 'object',
|
|
26
|
+
properties: {
|
|
27
|
+
cids: {
|
|
28
|
+
type: 'array',
|
|
29
|
+
items: { type: 'number' },
|
|
30
|
+
description: 'List of PI CIDs whose risk score increased'
|
|
31
|
+
},
|
|
32
|
+
metadata: {
|
|
33
|
+
type: 'object',
|
|
34
|
+
properties: {
|
|
35
|
+
threshold: { type: 'number' },
|
|
36
|
+
timeWindow: { type: 'number' },
|
|
37
|
+
count: { type: 'number' },
|
|
38
|
+
timestamp: { type: 'string' }
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
description: 'PIs with risk score increase meeting threshold'
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
process(context) {
|
|
47
|
+
const { RankingsExtractor } = context.math;
|
|
48
|
+
|
|
49
|
+
// Default threshold: 2 points increase
|
|
50
|
+
// This can be parameterized later via computation parameters
|
|
51
|
+
const RISK_INCREASE_THRESHOLD = 2;
|
|
52
|
+
|
|
53
|
+
// Meta executors process all users, but we need rankings data
|
|
54
|
+
// For now, we'll access rankings from context.user.rankings
|
|
55
|
+
// In a full implementation, this would iterate through all PIs
|
|
56
|
+
const rankings = context.user?.rankings;
|
|
57
|
+
|
|
58
|
+
if (!rankings) {
|
|
59
|
+
this.results['meta'] = {
|
|
60
|
+
cids: [],
|
|
61
|
+
metadata: {
|
|
62
|
+
threshold: RISK_INCREASE_THRESHOLD,
|
|
63
|
+
timeWindow: 1,
|
|
64
|
+
count: 0,
|
|
65
|
+
timestamp: new Date().toISOString()
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const todayRankings = rankings.today || {};
|
|
72
|
+
const yesterdayRankings = rankings.yesterday || {};
|
|
73
|
+
|
|
74
|
+
const todayItems = RankingsExtractor.getItems(todayRankings);
|
|
75
|
+
const yesterdayItems = RankingsExtractor.getItems(yesterdayRankings);
|
|
76
|
+
|
|
77
|
+
// Create maps for quick lookup
|
|
78
|
+
const todayMap = new Map();
|
|
79
|
+
for (const item of todayItems) {
|
|
80
|
+
if (item.CustomerId) {
|
|
81
|
+
todayMap.set(String(item.CustomerId), item);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const yesterdayMap = new Map();
|
|
86
|
+
for (const item of yesterdayItems) {
|
|
87
|
+
if (item.CustomerId) {
|
|
88
|
+
yesterdayMap.set(String(item.CustomerId), item);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const increasedRiskCIDs = [];
|
|
93
|
+
|
|
94
|
+
// Check each PI in today's rankings
|
|
95
|
+
for (const [cid, todayItem] of todayMap.entries()) {
|
|
96
|
+
const yesterdayItem = yesterdayMap.get(cid);
|
|
97
|
+
|
|
98
|
+
if (!yesterdayItem) {
|
|
99
|
+
// New PI in rankings, skip (no previous data)
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const todayRisk = RankingsExtractor.getRiskScore(todayItem);
|
|
104
|
+
const yesterdayRisk = RankingsExtractor.getRiskScore(yesterdayItem);
|
|
105
|
+
|
|
106
|
+
if (todayRisk !== null && yesterdayRisk !== null) {
|
|
107
|
+
const riskIncrease = todayRisk - yesterdayRisk;
|
|
108
|
+
|
|
109
|
+
if (riskIncrease >= RISK_INCREASE_THRESHOLD) {
|
|
110
|
+
increasedRiskCIDs.push(Number(cid));
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
this.results['meta'] = {
|
|
116
|
+
cids: increasedRiskCIDs,
|
|
117
|
+
metadata: {
|
|
118
|
+
threshold: RISK_INCREASE_THRESHOLD,
|
|
119
|
+
timeWindow: 1, // 1 day comparison
|
|
120
|
+
count: increasedRiskCIDs.length,
|
|
121
|
+
timestamp: new Date().toISOString()
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async getResult() {
|
|
127
|
+
return this.results;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
module.exports = PiRiskIncrease;
|
|
132
|
+
|