aiden-shared-calculations-unified 1.0.108 → 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/core/user-history-reconstructor.js +48 -49
- 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,12 +1,8 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @fileoverview GAUSS Product Line (Pass 3)
|
|
3
|
-
* REFACTORED: Implemented getResult logic and mapping caching.
|
|
4
|
-
*/
|
|
5
1
|
class CohortCapitalFlow {
|
|
6
2
|
constructor() {
|
|
7
3
|
this.cohortFlows = new Map();
|
|
8
4
|
this.cohortMap = new Map();
|
|
9
|
-
this.tickerMap = null;
|
|
5
|
+
this.tickerMap = null;
|
|
10
6
|
this.dependenciesLoaded = false;
|
|
11
7
|
}
|
|
12
8
|
|
|
@@ -14,36 +10,26 @@ class CohortCapitalFlow {
|
|
|
14
10
|
const flowSchema = {
|
|
15
11
|
"type": "object",
|
|
16
12
|
"properties": {
|
|
17
|
-
"net_flow_percentage": { "type": "number" },
|
|
18
|
-
"net_flow_contribution": { "type": "number" }
|
|
13
|
+
"net_flow_percentage": { "type": ["number", "null"] },
|
|
14
|
+
"net_flow_contribution": { "type": ["number", "null"] },
|
|
15
|
+
"total_invested_today": { "type": "number" } // Baseline
|
|
19
16
|
},
|
|
20
|
-
"required": ["net_flow_percentage", "net_flow_contribution"]
|
|
21
|
-
};
|
|
22
|
-
return {
|
|
23
|
-
"type": "object",
|
|
24
|
-
"patternProperties": { "^.*$": { "type": "object", "patternProperties": { "^.*$": flowSchema } } }
|
|
17
|
+
"required": ["net_flow_percentage", "net_flow_contribution", "total_invested_today"]
|
|
25
18
|
};
|
|
19
|
+
return { "type": "object", "patternProperties": { "^.*$": { "type": "object", "patternProperties": { "^.*$": flowSchema } } } };
|
|
26
20
|
}
|
|
27
21
|
|
|
28
22
|
static getMetadata() {
|
|
29
|
-
return {
|
|
30
|
-
type: 'standard',
|
|
31
|
-
rootDataDependencies: ['portfolio', 'history'],
|
|
32
|
-
isHistorical: true,
|
|
33
|
-
userType: 'all',
|
|
34
|
-
category: 'gauss'
|
|
35
|
-
};
|
|
23
|
+
return { type: 'standard', rootDataDependencies: ['portfolio', 'history'], isHistorical: true, userType: 'all', category: 'gauss' };
|
|
36
24
|
}
|
|
37
25
|
|
|
38
|
-
static getDependencies() {
|
|
39
|
-
return ['cohort-definer', 'instrument-price-change-1d'];
|
|
40
|
-
}
|
|
26
|
+
static getDependencies() { return ['cohort-definer', 'instrument-price-change-1d']; }
|
|
41
27
|
|
|
42
28
|
_initFlowData(cohortName, instrumentId) {
|
|
43
29
|
if (!this.cohortFlows.has(cohortName)) this.cohortFlows.set(cohortName, new Map());
|
|
44
30
|
if (!this.cohortFlows.get(cohortName).has(instrumentId)) {
|
|
45
31
|
this.cohortFlows.get(cohortName).set(instrumentId, {
|
|
46
|
-
total_invested_yesterday: 0, total_invested_today: 0, price_change_yesterday: 0
|
|
32
|
+
total_invested_yesterday: 0, total_invested_today: 0, price_change_yesterday: 0, has_yesterday: false
|
|
47
33
|
});
|
|
48
34
|
}
|
|
49
35
|
}
|
|
@@ -62,84 +48,61 @@ class CohortCapitalFlow {
|
|
|
62
48
|
process(context) {
|
|
63
49
|
const { user, computed, mappings, math } = context;
|
|
64
50
|
const { extract } = math;
|
|
65
|
-
|
|
66
|
-
// Cache mapping for getResult phase
|
|
67
51
|
if (!this.tickerMap) this.tickerMap = mappings.instrumentToTicker;
|
|
68
|
-
|
|
69
52
|
this._loadDependencies(computed);
|
|
70
|
-
|
|
71
53
|
const cohortName = this.cohortMap.get(user.id);
|
|
72
54
|
if (!cohortName) return;
|
|
73
55
|
|
|
74
56
|
const priceChangeMap = computed['instrument-price-change-1d'];
|
|
75
|
-
if (!priceChangeMap) return;
|
|
76
|
-
|
|
77
|
-
const yPos = extract.getPositions(user.portfolio.yesterday, user.type);
|
|
78
57
|
const tPos = extract.getPositions(user.portfolio.today, user.type);
|
|
58
|
+
const yPos = user.portfolio.yesterday ? extract.getPositions(user.portfolio.yesterday, user.type) : [];
|
|
79
59
|
|
|
80
60
|
const yPosMap = new Map(yPos.map(p => [extract.getInstrumentId(p), p]));
|
|
81
61
|
const tPosMap = new Map(tPos.map(p => [extract.getInstrumentId(p), p]));
|
|
82
62
|
const allInstrumentIds = new Set([...yPosMap.keys(), ...tPosMap.keys()]);
|
|
83
63
|
|
|
84
64
|
for (const instId of allInstrumentIds) {
|
|
85
|
-
if (!instId) continue;
|
|
86
|
-
|
|
87
65
|
this._initFlowData(cohortName, instId);
|
|
88
66
|
const asset = this.cohortFlows.get(cohortName).get(instId);
|
|
89
|
-
|
|
90
|
-
const yInvested = extract.getPositionWeight(yPosMap.get(instId), user.type);
|
|
91
67
|
const tInvested = extract.getPositionWeight(tPosMap.get(instId), user.type);
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
68
|
+
if (tInvested > 0) asset.total_invested_today += tInvested;
|
|
69
|
+
|
|
70
|
+
if (user.portfolio.yesterday) {
|
|
71
|
+
asset.has_yesterday = true;
|
|
72
|
+
const yInvested = extract.getPositionWeight(yPosMap.get(instId), user.type);
|
|
73
|
+
if (yInvested > 0) {
|
|
74
|
+
asset.total_invested_yesterday += yInvested;
|
|
75
|
+
const ticker = this.tickerMap[instId];
|
|
76
|
+
const yPriceChange = (ticker && priceChangeMap?.[ticker]) ? priceChangeMap[ticker].change_1d_pct : 0;
|
|
77
|
+
asset.price_change_yesterday += ((yPriceChange || 0) / 100.0) * yInvested;
|
|
78
|
+
}
|
|
103
79
|
}
|
|
104
80
|
}
|
|
105
81
|
}
|
|
106
82
|
|
|
107
83
|
async getResult() {
|
|
108
84
|
const finalResult = {};
|
|
109
|
-
|
|
110
|
-
if (!this.tickerMap) return {}; // Should be populated by process()
|
|
111
|
-
|
|
85
|
+
if (!this.tickerMap) return {};
|
|
112
86
|
for (const [cohortName, assetsMap] of this.cohortFlows.entries()) {
|
|
113
87
|
finalResult[cohortName] = {};
|
|
114
|
-
|
|
115
88
|
for (const [instId, data] of assetsMap.entries()) {
|
|
116
89
|
const ticker = this.tickerMap[instId] || `id_${instId}`;
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
const denominator = Math.max(1, (data.total_invested_yesterday + data.total_invested_today) / 2);
|
|
125
|
-
const netFlowPct = (netFlow / denominator) * 100;
|
|
126
|
-
|
|
90
|
+
let netFlow = null, netFlowPct = null;
|
|
91
|
+
if (data.has_yesterday) {
|
|
92
|
+
const expectedToday = data.total_invested_yesterday + data.price_change_yesterday;
|
|
93
|
+
netFlow = data.total_invested_today - expectedToday;
|
|
94
|
+
const denom = Math.max(1, (data.total_invested_yesterday + data.total_invested_today) / 2);
|
|
95
|
+
netFlowPct = (netFlow / denom) * 100;
|
|
96
|
+
}
|
|
127
97
|
finalResult[cohortName][ticker] = {
|
|
128
|
-
net_flow_percentage: netFlowPct,
|
|
129
|
-
net_flow_contribution: netFlow
|
|
98
|
+
net_flow_percentage: (netFlowPct !== null && isFinite(netFlowPct)) ? netFlowPct : null,
|
|
99
|
+
net_flow_contribution: (netFlow !== null && isFinite(netFlow)) ? netFlow : null,
|
|
100
|
+
total_invested_today: data.total_invested_today
|
|
130
101
|
};
|
|
131
102
|
}
|
|
132
103
|
}
|
|
133
|
-
|
|
134
104
|
return finalResult;
|
|
135
105
|
}
|
|
136
|
-
|
|
137
|
-
reset() {
|
|
138
|
-
this.cohortFlows.clear();
|
|
139
|
-
this.cohortMap.clear();
|
|
140
|
-
this.tickerMap = null;
|
|
141
|
-
this.dependenciesLoaded = false;
|
|
142
|
-
}
|
|
106
|
+
reset() { this.cohortFlows.clear(); this.cohortMap.clear(); this.tickerMap = null; this.dependenciesLoaded = false; }
|
|
143
107
|
}
|
|
144
|
-
|
|
145
|
-
module.exports = CohortCapitalFlow;
|
|
108
|
+
module.exports = CohortCapitalFlow;
|
|
@@ -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;
|