aiden-shared-calculations-unified 1.0.109 → 1.0.110
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/core/insights-daily-bought-vs-sold-count.js +20 -15
- package/calculations/core/insights-daily-ownership-delta.js +12 -10
- package/calculations/core/instrument-price-change-1d.js +16 -31
- package/calculations/core/instrument-price-momentum-20d.js +16 -26
- package/calculations/core/ownership-vs-performance-ytd.js +15 -52
- package/calculations/core/ownership-vs-volatility.js +27 -36
- package/calculations/core/platform-daily-bought-vs-sold-count.js +28 -26
- package/calculations/core/platform-daily-ownership-delta.js +28 -31
- package/calculations/core/price-metrics.js +15 -54
- package/calculations/core/short-interest-growth.js +6 -13
- package/calculations/core/trending-ownership-momentum.js +16 -28
- package/calculations/gauss/cohort-capital-flow.js +34 -71
- package/calculations/gauss/cohort-definer.js +61 -142
- package/calculations/gem/cohort-momentum-state.js +27 -77
- package/calculations/gem/skilled-cohort-flow.js +36 -114
- package/calculations/gem/unskilled-cohort-flow.js +36 -112
- package/calculations/ghost-book/retail-gamma-exposure.js +14 -61
- package/calculations/helix/herd-consensus-score.js +27 -90
- package/calculations/helix/winner-loser-flow.js +21 -90
- package/calculations/predicative-alpha/cognitive-dissonance.js +25 -91
- package/calculations/predicative-alpha/diamond-hand-fracture.js +17 -72
- package/calculations/predicative-alpha/mimetic-latency.js +21 -100
- package/package.json +1 -1
|
@@ -1,177 +1,96 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @fileoverview GAUSS Product Line (Pass 2)
|
|
3
|
-
* REFACTORED: Uses context.computed and context.math.
|
|
4
|
-
*/
|
|
5
1
|
class CohortDefiner {
|
|
6
|
-
constructor() {
|
|
7
|
-
this.smartVectors = [];
|
|
8
|
-
this.dumbVectors = [];
|
|
9
|
-
this.cohortIdSets = null;
|
|
10
|
-
}
|
|
2
|
+
constructor() { this.smartVectors = []; this.dumbVectors = []; this.cohortIdSets = null; }
|
|
11
3
|
|
|
12
|
-
static getMetadata() {
|
|
13
|
-
|
|
14
|
-
type: 'standard',
|
|
15
|
-
rootDataDependencies: ['portfolio', 'history'],
|
|
16
|
-
isHistorical: true,
|
|
17
|
-
userType: 'all',
|
|
18
|
-
category: 'gauss'
|
|
19
|
-
};
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
static getDependencies() {
|
|
23
|
-
return ['daily-dna-filter', 'instrument-price-momentum-20d'];
|
|
24
|
-
}
|
|
4
|
+
static getMetadata() { return { type: 'standard', rootDataDependencies: ['portfolio', 'history'], isHistorical: true, userType: 'all', category: 'gauss' }; }
|
|
5
|
+
static getDependencies() { return ['daily-dna-filter', 'instrument-price-momentum-20d']; }
|
|
25
6
|
|
|
26
7
|
static getSchema() {
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
"
|
|
30
|
-
"
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
return {
|
|
34
|
-
"type": "object",
|
|
35
|
-
"required": [
|
|
36
|
-
"smart_investors",
|
|
37
|
-
"smart_scalpers",
|
|
38
|
-
"uncategorized_smart",
|
|
39
|
-
"fomo_chasers",
|
|
40
|
-
"patient_losers",
|
|
41
|
-
"fomo_bagholders",
|
|
42
|
-
"uncategorized_dumb"
|
|
43
|
-
],
|
|
44
|
-
"properties": {
|
|
45
|
-
"smart_investors": cohortList,
|
|
46
|
-
"smart_scalpers": cohortList,
|
|
47
|
-
"uncategorized_smart": cohortList,
|
|
48
|
-
"fomo_chasers": cohortList,
|
|
49
|
-
"patient_losers": cohortList,
|
|
50
|
-
"fomo_bagholders": cohortList,
|
|
51
|
-
"uncategorized_dumb": cohortList
|
|
52
|
-
}
|
|
53
|
-
};
|
|
8
|
+
const list = { "type": "array", "items": { "type": "string" } };
|
|
9
|
+
return { "type": "object", "properties": {
|
|
10
|
+
"smart_investors": list, "smart_scalpers": list, "uncategorized_smart": list,
|
|
11
|
+
"fomo_chasers": list, "patient_losers": list, "fomo_bagholders": list, "uncategorized_dumb": list
|
|
12
|
+
}, "required": ["smart_investors", "smart_scalpers", "uncategorized_smart", "fomo_chasers", "patient_losers", "fomo_bagholders", "uncategorized_dumb"] };
|
|
54
13
|
}
|
|
55
14
|
|
|
56
15
|
_loadDependencies(computed) {
|
|
57
16
|
if (this.cohortIdSets) return;
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
this.cohortIdSets = {
|
|
61
|
-
smart: new Set(dnaFilterData?.smart_cohort_ids || []),
|
|
62
|
-
dumb: new Set(dnaFilterData?.dumb_cohort_ids || [])
|
|
63
|
-
};
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
_getFomoScore(extract, mappings, today, yesterday, momentumData, userType) {
|
|
67
|
-
if (!momentumData) return 0;
|
|
68
|
-
|
|
69
|
-
const tPos = extract.getPositions(today, userType);
|
|
70
|
-
const yPos = extract.getPositions(yesterday, userType);
|
|
71
|
-
|
|
72
|
-
const yIds = new Set(yPos.map(p => extract.getInstrumentId(p)));
|
|
73
|
-
const newPositions = tPos.filter(p => !yIds.has(extract.getInstrumentId(p)));
|
|
74
|
-
|
|
75
|
-
if (newPositions.length === 0) return 0;
|
|
76
|
-
|
|
77
|
-
let fomoSum = 0, count = 0;
|
|
78
|
-
for (const pos of newPositions) {
|
|
79
|
-
const instId = extract.getInstrumentId(pos);
|
|
80
|
-
const ticker = mappings.instrumentToTicker[instId];
|
|
81
|
-
if (ticker && momentumData[ticker]) {
|
|
82
|
-
fomoSum += momentumData[ticker].momentum_20d_pct || 0;
|
|
83
|
-
count++;
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
return count > 0 ? fomoSum / count : 0;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
_getBagholderScore(extract, today, userType) {
|
|
90
|
-
const positions = extract.getPositions(today, userType);
|
|
91
|
-
if (positions.length === 0) return 0;
|
|
92
|
-
|
|
93
|
-
let durationSum = 0, count = 0;
|
|
94
|
-
const now = new Date();
|
|
95
|
-
|
|
96
|
-
for (const pos of positions) {
|
|
97
|
-
if (extract.getNetProfit(pos) < -20) {
|
|
98
|
-
const openDate = extract.getOpenDateTime(pos);
|
|
99
|
-
if (openDate) {
|
|
100
|
-
try {
|
|
101
|
-
const durationDays = (now - openDate) / (86400000);
|
|
102
|
-
durationSum += durationDays;
|
|
103
|
-
count++;
|
|
104
|
-
} catch (e) {}
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
return count > 0 ? durationSum / count : 0;
|
|
17
|
+
const dna = computed['daily-dna-filter'];
|
|
18
|
+
this.cohortIdSets = { smart: new Set(dna?.smart_cohort_ids || []), dumb: new Set(dna?.dumb_cohort_ids || []) };
|
|
109
19
|
}
|
|
110
20
|
|
|
111
21
|
process(context) {
|
|
112
22
|
const { user, computed, mappings, math } = context;
|
|
113
23
|
const { extract, history } = math;
|
|
114
|
-
|
|
115
24
|
this._loadDependencies(computed);
|
|
116
|
-
|
|
117
|
-
const isSmart = this.cohortIdSets.smart.has(user.id);
|
|
118
|
-
const isDumb = this.cohortIdSets.dumb.has(user.id);
|
|
119
|
-
|
|
25
|
+
const isSmart = this.cohortIdSets.smart.has(user.id), isDumb = this.cohortIdSets.dumb.has(user.id);
|
|
120
26
|
if (!isSmart && !isDumb) return;
|
|
121
27
|
|
|
122
|
-
// 1. Strict History Access
|
|
123
28
|
const historyDoc = history.getDailyHistory(user);
|
|
124
29
|
const summary = history.getSummary(historyDoc);
|
|
30
|
+
if (!summary) {
|
|
31
|
+
// No history? Keep them for aggregate count as uncategorized
|
|
32
|
+
const v = { userId: user.id, skill: 0, time: 0, fomo: 0, bagholder: 0, coldStart: true };
|
|
33
|
+
if (isSmart) this.smartVectors.push(v); else this.dumbVectors.push(v);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
125
36
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
// 2. Valid usage of Summary DTO properties
|
|
129
|
-
const winRate = summary.winRatio / 100.0;
|
|
130
|
-
const lossRate = 1.0 - winRate;
|
|
37
|
+
const winRate = summary.winRatio / 100.0, lossRate = 1.0 - winRate;
|
|
131
38
|
const lt_skill = (winRate * summary.avgProfitPct) - (lossRate * Math.abs(summary.avgLossPct));
|
|
132
39
|
const lt_time = summary.avgHoldingTimeInMinutes;
|
|
133
|
-
|
|
134
40
|
const momentumData = computed['instrument-price-momentum-20d'];
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
const
|
|
41
|
+
|
|
42
|
+
// st_fomo logic handles user.portfolio.yesterday internally via extract calls
|
|
43
|
+
const tPos = extract.getPositions(user.portfolio.today, user.type);
|
|
44
|
+
const yPos = user.portfolio.yesterday ? extract.getPositions(user.portfolio.yesterday, user.type) : [];
|
|
45
|
+
const yIds = new Set(yPos.map(p => extract.getInstrumentId(p)));
|
|
46
|
+
const newPos = tPos.filter(p => !yIds.has(extract.getInstrumentId(p)));
|
|
47
|
+
let fomo = 0; if (newPos.length > 0 && momentumData) {
|
|
48
|
+
let sum = 0, count = 0;
|
|
49
|
+
for (const p of newPos) {
|
|
50
|
+
const tick = mappings.instrumentToTicker[extract.getInstrumentId(p)];
|
|
51
|
+
if (tick && momentumData[tick]) { sum += momentumData[tick].momentum_20d_pct || 0; count++; }
|
|
52
|
+
}
|
|
53
|
+
fomo = count > 0 ? sum / count : 0;
|
|
54
|
+
}
|
|
139
55
|
|
|
140
|
-
|
|
141
|
-
|
|
56
|
+
let bag = 0, bCount = 0; for (const p of tPos) {
|
|
57
|
+
if (extract.getNetProfit(p) < -20) {
|
|
58
|
+
const open = extract.getOpenDateTime(p);
|
|
59
|
+
if (open) { bag += (new Date() - open) / 86400000; bCount++; }
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
const st_bag = bCount > 0 ? bag / bCount : 0;
|
|
63
|
+
const vector = { userId: user.id, skill: lt_skill, time: lt_time, fomo: fomo, bagholder: st_bag, coldStart: false };
|
|
64
|
+
if (isSmart) this.smartVectors.push(vector); else this.dumbVectors.push(vector);
|
|
142
65
|
}
|
|
143
66
|
|
|
144
67
|
_getMedian(vectors, key) {
|
|
145
|
-
|
|
146
|
-
|
|
68
|
+
const filtered = vectors.filter(v => !v.coldStart);
|
|
69
|
+
if (filtered.length === 0) return 0;
|
|
70
|
+
const sorted = filtered.map(v => v[key]).sort((a, b) => a - b);
|
|
147
71
|
const mid = Math.floor(sorted.length / 2);
|
|
148
72
|
return sorted.length % 2 === 0 ? (sorted[mid - 1] + sorted[mid]) / 2 : sorted[mid];
|
|
149
73
|
}
|
|
150
74
|
|
|
151
75
|
async getResult() {
|
|
152
|
-
const cohorts = {};
|
|
153
|
-
const
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
const
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
76
|
+
const cohorts = { smart_investors: [], smart_scalpers: [], uncategorized_smart: [], fomo_chasers: [], patient_losers: [], fomo_bagholders: [], uncategorized_dumb: [] };
|
|
77
|
+
const assSmart = new Set(), assDumb = new Set();
|
|
78
|
+
const sTime = this._getMedian(this.smartVectors, 'time');
|
|
79
|
+
this.smartVectors.forEach(u => {
|
|
80
|
+
if (u.coldStart) { cohorts.uncategorized_smart.push(u.userId); return; }
|
|
81
|
+
if (u.time >= sTime) { cohorts.smart_investors.push(u.userId); assSmart.add(u.userId); }
|
|
82
|
+
else { cohorts.smart_scalpers.push(u.userId); assSmart.add(u.userId); }
|
|
83
|
+
});
|
|
84
|
+
const dFomo = this._getMedian(this.dumbVectors, 'fomo'), dBag = this._getMedian(this.dumbVectors, 'bagholder');
|
|
85
|
+
this.dumbVectors.forEach(u => {
|
|
86
|
+
if (u.coldStart) { cohorts.uncategorized_dumb.push(u.userId); return; }
|
|
87
|
+
if (u.fomo >= dFomo && u.bagholder < dBag) cohorts.fomo_chasers.push(u.userId);
|
|
88
|
+
else if (u.fomo < dFomo && u.bagholder >= dBag) cohorts.patient_losers.push(u.userId);
|
|
89
|
+
else if (u.fomo >= dFomo && u.bagholder >= dBag) cohorts.fomo_bagholders.push(u.userId);
|
|
90
|
+
else cohorts.uncategorized_dumb.push(u.userId);
|
|
91
|
+
});
|
|
168
92
|
return cohorts;
|
|
169
93
|
}
|
|
170
|
-
|
|
171
|
-
reset() {
|
|
172
|
-
this.smartVectors = [];
|
|
173
|
-
this.dumbVectors = [];
|
|
174
|
-
this.cohortIdSets = null;
|
|
175
|
-
}
|
|
94
|
+
reset() { this.smartVectors = []; this.dumbVectors = []; this.cohortIdSets = null; }
|
|
176
95
|
}
|
|
177
96
|
module.exports = CohortDefiner;
|
|
@@ -1,101 +1,51 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @fileoverview GEM Product Line (Pass 2)
|
|
3
|
-
* REFACTORED: Uses context.computed and context.math.
|
|
4
|
-
*/
|
|
5
1
|
class CohortMomentumState {
|
|
6
|
-
constructor() {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
this.dependenciesLoaded = false;
|
|
10
|
-
this.tickerMap = null; // Need to store this
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
static getMetadata() {
|
|
14
|
-
return {
|
|
15
|
-
type: 'standard',
|
|
16
|
-
rootDataDependencies: ['portfolio'],
|
|
17
|
-
isHistorical: true,
|
|
18
|
-
userType: 'all',
|
|
19
|
-
category: 'gem'
|
|
20
|
-
};
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
static getDependencies() {
|
|
24
|
-
return ['cohort-skill-definition', 'instrument-price-momentum-20d'];
|
|
25
|
-
}
|
|
2
|
+
constructor() { this.cohortMomentum = new Map(); this.cohortMap = new Map(); this.dependenciesLoaded = false; this.tickerMap = null; }
|
|
3
|
+
static getMetadata() { return { type: 'standard', rootDataDependencies: ['portfolio'], isHistorical: true, userType: 'all', category: 'gem' }; }
|
|
4
|
+
static getDependencies() { return ['cohort-skill-definition', 'instrument-price-momentum-20d']; }
|
|
26
5
|
|
|
27
6
|
static getSchema() {
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
"properties": {
|
|
31
|
-
"average_momentum_exposure_pct": { "type": "number" },
|
|
32
|
-
"trade_count": { "type": "number" }
|
|
33
|
-
},
|
|
34
|
-
"required": ["average_momentum_exposure_pct", "trade_count"]
|
|
35
|
-
};
|
|
36
|
-
return { "type": "object", "properties": { "skilled": cohortSchema, "unskilled": cohortSchema } };
|
|
7
|
+
const s = { "type": "object", "properties": { "average_momentum_exposure_pct": { "type": ["number", "null"] }, "trade_count": { "type": "number" }, "total_exposure_today": { "type": "number" } }, "required": ["average_momentum_exposure_pct", "trade_count", "total_exposure_today"] };
|
|
8
|
+
return { "type": "object", "properties": { "skilled": s, "unskilled": s } };
|
|
37
9
|
}
|
|
38
10
|
|
|
39
11
|
_loadDependencies(computed) {
|
|
40
12
|
if (this.dependenciesLoaded) return;
|
|
41
|
-
const
|
|
42
|
-
if (
|
|
43
|
-
(cohortData.skilled_user_ids || []).forEach(uid => this.cohortMap.set(String(uid), 'skilled'));
|
|
44
|
-
(cohortData.unskilled_user_ids || []).forEach(uid => this.cohortMap.set(String(uid), 'unskilled'));
|
|
45
|
-
}
|
|
13
|
+
const d = computed['cohort-skill-definition'];
|
|
14
|
+
if (d) { (d.skilled_user_ids || []).forEach(uid => this.cohortMap.set(String(uid), 'skilled')); (d.unskilled_user_ids || []).forEach(uid => this.cohortMap.set(String(uid), 'unskilled')); }
|
|
46
15
|
this.dependenciesLoaded = true;
|
|
47
16
|
}
|
|
48
17
|
|
|
49
|
-
_initCohort(cohortName) {
|
|
50
|
-
if (!this.cohortMomentum.has(cohortName)) this.cohortMomentum.set(cohortName, { momentum_sum: 0, count: 0 });
|
|
51
|
-
}
|
|
52
|
-
|
|
53
18
|
process(context) {
|
|
54
19
|
const { user, computed, mappings, math } = context;
|
|
55
20
|
const { extract } = math;
|
|
56
|
-
|
|
57
21
|
this._loadDependencies(computed);
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
if (newPositions.length === 0) return;
|
|
70
|
-
|
|
71
|
-
this._initCohort(cohortName);
|
|
72
|
-
const asset = this.cohortMomentum.get(cohortName);
|
|
22
|
+
const cohort = this.cohortMap.get(user.id); if (!cohort) return;
|
|
23
|
+
if (!this.cohortMomentum.has(cohort)) this.cohortMomentum.set(cohort, { sum: 0, count: 0, total_abs: 0 });
|
|
24
|
+
const asset = this.cohortMomentum.get(cohort);
|
|
25
|
+
|
|
26
|
+
const mom = computed['instrument-price-momentum-20d'];
|
|
27
|
+
const tPos = extract.getPositions(user.portfolio.today, user.type);
|
|
28
|
+
for (const p of tPos) {
|
|
29
|
+
const tick = mappings.instrumentToTicker[extract.getInstrumentId(p)];
|
|
30
|
+
if (tick && mom?.[tick]) asset.total_abs += Math.abs(mom[tick].momentum_20d_pct || 0);
|
|
31
|
+
}
|
|
73
32
|
|
|
74
|
-
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
33
|
+
if (user.portfolio.yesterday) {
|
|
34
|
+
const yIds = new Set(extract.getPositions(user.portfolio.yesterday, user.type).map(p => extract.getInstrumentId(p)));
|
|
35
|
+
const newPos = tPos.filter(p => !yIds.has(extract.getInstrumentId(p)));
|
|
36
|
+
for (const p of newPos) {
|
|
37
|
+
const tick = mappings.instrumentToTicker[extract.getInstrumentId(p)];
|
|
38
|
+
if (tick && mom?.[tick]) { asset.sum += mom[tick].momentum_20d_pct || 0; asset.count++; }
|
|
79
39
|
}
|
|
80
40
|
}
|
|
81
41
|
}
|
|
82
42
|
|
|
83
43
|
async getResult() {
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
finalResult[cohortName] = {
|
|
87
|
-
average_momentum_exposure_pct: (data.count > 0) ? data.momentum_sum / data.count : 0,
|
|
88
|
-
trade_count: data.count
|
|
89
|
-
};
|
|
44
|
+
const res = {}; for (const [c, d] of this.cohortMomentum.entries()) {
|
|
45
|
+
res[c] = { average_momentum_exposure_pct: (d.count > 0) ? d.sum / d.count : null, trade_count: d.count, total_exposure_today: d.total_abs };
|
|
90
46
|
}
|
|
91
|
-
return
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
reset() {
|
|
95
|
-
this.cohortMomentum.clear();
|
|
96
|
-
this.cohortMap.clear();
|
|
97
|
-
this.dependenciesLoaded = false;
|
|
98
|
-
this.tickerMap = null;
|
|
47
|
+
return res;
|
|
99
48
|
}
|
|
49
|
+
reset() { this.cohortMomentum.clear(); this.cohortMap.clear(); this.dependenciesLoaded = false; this.tickerMap = null; }
|
|
100
50
|
}
|
|
101
51
|
module.exports = CohortMomentumState;
|
|
@@ -1,144 +1,66 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @fileoverview GEM Product Line (Pass 2)
|
|
3
|
-
* REFACTORED: Uses context.computed and context.math.extract.
|
|
4
|
-
*/
|
|
5
1
|
class SkilledCohortFlow {
|
|
6
|
-
constructor() {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
this.dependenciesLoaded = false;
|
|
10
|
-
this.tickerMap = null;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
static getMetadata() {
|
|
14
|
-
return {
|
|
15
|
-
type: 'standard',
|
|
16
|
-
rootDataDependencies: ['portfolio'],
|
|
17
|
-
isHistorical: true,
|
|
18
|
-
userType: 'all',
|
|
19
|
-
category: 'gem'
|
|
20
|
-
};
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
static getDependencies() {
|
|
24
|
-
return ['cohort-skill-definition', 'instrument-price-change-1d'];
|
|
25
|
-
}
|
|
2
|
+
constructor() { this.assetFlows = new Map(); this.cohortMap = new Map(); this.dependenciesLoaded = false; this.tickerMap = null; }
|
|
3
|
+
static getMetadata() { return { type: 'standard', rootDataDependencies: ['portfolio'], isHistorical: true, userType: 'all', category: 'gem' }; }
|
|
4
|
+
static getDependencies() { return ['cohort-skill-definition', 'instrument-price-change-1d']; }
|
|
26
5
|
|
|
27
6
|
static getSchema() {
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
"properties": {
|
|
31
|
-
"net_flow_pct": { "type": "number" },
|
|
32
|
-
"net_flow_contribution": { "type": "number" },
|
|
33
|
-
"avg_position_change_pct": { "type": "number" },
|
|
34
|
-
"user_count": { "type": "number" }
|
|
35
|
-
},
|
|
36
|
-
"required": ["net_flow_pct", "net_flow_contribution", "avg_position_change_pct", "user_count"]
|
|
37
|
-
};
|
|
38
|
-
return { "type": "object", "patternProperties": { "^.*$": tickerSchema } };
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
_initFlowData(instrumentId) {
|
|
42
|
-
if (!this.assetFlows.has(instrumentId)) {
|
|
43
|
-
this.assetFlows.set(instrumentId, {
|
|
44
|
-
total_invested_yesterday: 0, total_invested_today: 0, price_change_yesterday: 0,
|
|
45
|
-
total_pos_size_yesterday: 0, total_pos_size_today: 0, user_count_yesterday: 0, user_count_today: 0
|
|
46
|
-
});
|
|
47
|
-
}
|
|
7
|
+
const s = { "type": "object", "properties": { "net_flow_pct": { "type": ["number", "null"] }, "net_flow_contribution": { "type": ["number", "null"] }, "avg_position_change_pct": { "type": ["number", "null"] }, "user_count": { "type": "number" }, "total_invested_today": { "type": "number" } }, "required": ["net_flow_pct", "net_flow_contribution", "avg_position_change_pct", "user_count", "total_invested_today"] };
|
|
8
|
+
return { "type": "object", "patternProperties": { "^.*$": s } };
|
|
48
9
|
}
|
|
49
10
|
|
|
50
11
|
_loadDependencies(computed) {
|
|
51
12
|
if (this.dependenciesLoaded) return;
|
|
52
|
-
const
|
|
53
|
-
if (
|
|
54
|
-
(cohortData.skilled_user_ids || []).forEach(uid => this.cohortMap.set(String(uid), 'skilled'));
|
|
55
|
-
}
|
|
13
|
+
const d = computed['cohort-skill-definition'];
|
|
14
|
+
if (d) (d.skilled_user_ids || []).forEach(uid => this.cohortMap.set(String(uid), 'skilled'));
|
|
56
15
|
this.dependenciesLoaded = true;
|
|
57
16
|
}
|
|
58
17
|
|
|
59
18
|
process(context) {
|
|
60
19
|
const { user, computed, mappings, math } = context;
|
|
61
20
|
const { extract } = math;
|
|
62
|
-
|
|
63
21
|
this._loadDependencies(computed);
|
|
64
|
-
if (!this.tickerMap) this.tickerMap = mappings.instrumentToTicker;
|
|
65
|
-
|
|
66
22
|
if (this.cohortMap.get(user.id) !== 'skilled') return;
|
|
67
|
-
|
|
68
|
-
const
|
|
69
|
-
if (!priceChangeMap) return;
|
|
23
|
+
if (!this.tickerMap) this.tickerMap = mappings.instrumentToTicker;
|
|
24
|
+
const priceMap = computed['instrument-price-change-1d'];
|
|
70
25
|
|
|
71
|
-
const yPos = extract.getPositions(user.portfolio.yesterday, user.type);
|
|
72
26
|
const tPos = extract.getPositions(user.portfolio.today, user.type);
|
|
73
|
-
|
|
27
|
+
const yPos = user.portfolio.yesterday ? extract.getPositions(user.portfolio.yesterday, user.type) : [];
|
|
74
28
|
const yPosMap = new Map(yPos.map(p => [extract.getInstrumentId(p), p]));
|
|
75
29
|
const tPosMap = new Map(tPos.map(p => [extract.getInstrumentId(p), p]));
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
for (const instId of allInstrumentIds) {
|
|
79
|
-
if (!instId) continue;
|
|
80
|
-
this._initFlowData(instId);
|
|
81
|
-
const asset = this.assetFlows.get(instId);
|
|
82
|
-
|
|
83
|
-
const yWeight = extract.getPositionWeight(yPosMap.get(instId), user.type);
|
|
84
|
-
const tWeight = extract.getPositionWeight(tPosMap.get(instId), user.type);
|
|
30
|
+
const ids = new Set([...yPosMap.keys(), ...tPosMap.keys()]);
|
|
85
31
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
asset.
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
32
|
+
for (const id of ids) {
|
|
33
|
+
if (!this.assetFlows.has(id)) this.assetFlows.set(id, { yInv: 0, tInv: 0, pChg: 0, ySize: 0, tSize: 0, yCount: 0, tCount: 0, hasY: false });
|
|
34
|
+
const asset = this.assetFlows.get(id);
|
|
35
|
+
const tW = extract.getPositionWeight(tPosMap.get(id), user.type);
|
|
36
|
+
if (tW > 0) { asset.tInv += tW; asset.tCount++; asset.tSize += tW; }
|
|
37
|
+
if (user.portfolio.yesterday) {
|
|
38
|
+
asset.hasY = true;
|
|
39
|
+
const yW = extract.getPositionWeight(yPosMap.get(id), user.type);
|
|
40
|
+
if (yW > 0) {
|
|
41
|
+
asset.yInv += yW; asset.yCount++; asset.ySize += yW;
|
|
42
|
+
const tick = mappings.instrumentToTicker[id];
|
|
43
|
+
const chg = (tick && priceMap?.[tick]) ? priceMap[tick].change_1d_pct : 0;
|
|
44
|
+
asset.pChg += ((chg || 0) / 100.0) * yW;
|
|
45
|
+
}
|
|
98
46
|
}
|
|
99
47
|
}
|
|
100
48
|
}
|
|
101
49
|
|
|
102
50
|
async getResult() {
|
|
103
|
-
if (!this.tickerMap) return {};
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
if (
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
const avg_change = data.price_change_yesterday / data.total_invested_yesterday;
|
|
112
|
-
const adjusted_y = data.total_invested_yesterday * (1 + avg_change);
|
|
113
|
-
flow_contrib = data.total_invested_today - adjusted_y;
|
|
114
|
-
net_flow = (flow_contrib / data.total_invested_yesterday) * 100;
|
|
115
|
-
} else if (data.total_invested_today > 0) {
|
|
116
|
-
flow_contrib = data.total_invested_today;
|
|
117
|
-
net_flow = Infinity;
|
|
51
|
+
if (!this.tickerMap) return {}; const res = {};
|
|
52
|
+
for (const [id, d] of this.assetFlows.entries()) {
|
|
53
|
+
const tick = this.tickerMap[id]; if (!tick) continue;
|
|
54
|
+
let net = null, contrib = null, avgPos = null;
|
|
55
|
+
if (d.hasY) {
|
|
56
|
+
const adjY = d.yInv + d.pChg; contrib = d.tInv - adjY;
|
|
57
|
+
net = d.yInv > 0 ? (contrib / d.yInv) * 100 : (d.tInv > 0 ? Infinity : 0);
|
|
58
|
+
if (d.yCount > 0 && d.tCount > 0) avgPos = (((d.tSize / d.tCount) - (d.ySize / d.yCount)) / (d.ySize / d.yCount)) * 100;
|
|
118
59
|
}
|
|
119
|
-
|
|
120
|
-
let avg_pos_change = 0;
|
|
121
|
-
if (data.user_count_yesterday > 0 && data.user_count_today > 0) {
|
|
122
|
-
const avg_y = data.total_pos_size_yesterday / data.user_count_yesterday;
|
|
123
|
-
const avg_t = data.total_pos_size_today / data.user_count_today;
|
|
124
|
-
if (avg_y > 0) avg_pos_change = ((avg_t - avg_y) / avg_y) * 100;
|
|
125
|
-
} else if (data.user_count_today > 0) avg_pos_change = Infinity;
|
|
126
|
-
|
|
127
|
-
finalResult[ticker] = {
|
|
128
|
-
net_flow_pct: isFinite(net_flow) ? net_flow : 0,
|
|
129
|
-
net_flow_contribution: isFinite(flow_contrib) ? flow_contrib : 0,
|
|
130
|
-
avg_position_change_pct: isFinite(avg_pos_change) ? avg_pos_change : 0,
|
|
131
|
-
user_count: data.user_count_today
|
|
132
|
-
};
|
|
60
|
+
res[tick] = { net_flow_pct: isFinite(net) ? net : null, net_flow_contribution: isFinite(contrib) ? contrib : null, avg_position_change_pct: isFinite(avgPos) ? avgPos : null, user_count: d.tCount, total_invested_today: d.tInv };
|
|
133
61
|
}
|
|
134
|
-
return
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
reset() {
|
|
138
|
-
this.assetFlows.clear();
|
|
139
|
-
this.cohortMap.clear();
|
|
140
|
-
this.dependenciesLoaded = false;
|
|
141
|
-
this.tickerMap = null;
|
|
62
|
+
return res;
|
|
142
63
|
}
|
|
64
|
+
reset() { this.assetFlows.clear(); this.cohortMap.clear(); this.dependenciesLoaded = false; this.tickerMap = null; }
|
|
143
65
|
}
|
|
144
66
|
module.exports = SkilledCohortFlow;
|