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.
Files changed (24) hide show
  1. package/calculations/core/insights-daily-bought-vs-sold-count.js +20 -15
  2. package/calculations/core/insights-daily-ownership-delta.js +12 -10
  3. package/calculations/core/instrument-price-change-1d.js +16 -31
  4. package/calculations/core/instrument-price-momentum-20d.js +16 -26
  5. package/calculations/core/ownership-vs-performance-ytd.js +15 -52
  6. package/calculations/core/ownership-vs-volatility.js +27 -36
  7. package/calculations/core/platform-daily-bought-vs-sold-count.js +28 -26
  8. package/calculations/core/platform-daily-ownership-delta.js +28 -31
  9. package/calculations/core/price-metrics.js +15 -54
  10. package/calculations/core/short-interest-growth.js +6 -13
  11. package/calculations/core/trending-ownership-momentum.js +16 -28
  12. package/calculations/core/user-history-reconstructor.js +48 -49
  13. package/calculations/gauss/cohort-capital-flow.js +34 -71
  14. package/calculations/gauss/cohort-definer.js +61 -142
  15. package/calculations/gem/cohort-momentum-state.js +27 -77
  16. package/calculations/gem/skilled-cohort-flow.js +36 -114
  17. package/calculations/gem/unskilled-cohort-flow.js +36 -112
  18. package/calculations/ghost-book/retail-gamma-exposure.js +14 -61
  19. package/calculations/helix/herd-consensus-score.js +27 -90
  20. package/calculations/helix/winner-loser-flow.js +21 -90
  21. package/calculations/predicative-alpha/cognitive-dissonance.js +25 -91
  22. package/calculations/predicative-alpha/diamond-hand-fracture.js +17 -72
  23. package/calculations/predicative-alpha/mimetic-latency.js +21 -100
  24. 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; // Cache mappings
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
- if (yInvested > 0) {
94
- asset.total_invested_yesterday += yInvested;
95
-
96
- const ticker = this.tickerMap[instId];
97
- const yPriceChange_pct = (ticker && priceChangeMap[ticker]) ? priceChangeMap[ticker].change_1d_pct : 0;
98
-
99
- asset.price_change_yesterday += (yPriceChange_pct / 100.0) * yInvested;
100
- }
101
- if (tInvested > 0) {
102
- asset.total_invested_today += tInvested;
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
- // Logic: Flow = Today - (Yesterday * (1 + PriceChange))
119
- // This isolates the capital movement from the price action.
120
- const expectedToday = data.total_invested_yesterday + data.price_change_yesterday;
121
- const netFlow = data.total_invested_today - expectedToday;
122
-
123
- // Avoid divide by zero or noise for very small numbers
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
- return {
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 cohortList = {
28
- "type": "array",
29
- "items": { "type": "string" },
30
- "description": "List of User IDs belonging to this behavioral cohort"
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 dnaFilterData = computed['daily-dna-filter'];
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
- if (!summary) return;
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
- const st_fomo = this._getFomoScore(extract, mappings, user.portfolio.today, user.portfolio.yesterday, momentumData, user.type);
136
- const st_bagholder = this._getBagholderScore(extract, user.portfolio.today, user.type);
137
-
138
- const vector = { userId: user.id, skill: lt_skill, time: lt_time, fomo: st_fomo, bagholder: st_bagholder };
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
- if (isSmart) this.smartVectors.push(vector);
141
- else this.dumbVectors.push(vector);
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
- if (vectors.length === 0) return 0;
146
- const sorted = vectors.map(v => v[key]).sort((a, b) => a - b);
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 assignedSmart = new Set(), assignedDumb = new Set();
154
-
155
- const smart_time = this._getMedian(this.smartVectors, 'time');
156
- cohorts['smart_investors'] = this.smartVectors.filter(u => u.time >= smart_time).map(u => { assignedSmart.add(u.userId); return u.userId; });
157
- cohorts['smart_scalpers'] = this.smartVectors.filter(u => u.time < smart_time).map(u => { assignedSmart.add(u.userId); return u.userId; });
158
- cohorts['uncategorized_smart'] = this.smartVectors.filter(u => !assignedSmart.has(u.userId)).map(u => u.userId);
159
-
160
- const dumb_fomo = this._getMedian(this.dumbVectors, 'fomo');
161
- const dumb_bag = this._getMedian(this.dumbVectors, 'bagholder');
162
-
163
- cohorts['fomo_chasers'] = this.dumbVectors.filter(u => u.fomo >= dumb_fomo && u.bagholder < dumb_bag).map(u => { assignedDumb.add(u.userId); return u.userId; });
164
- cohorts['patient_losers'] = this.dumbVectors.filter(u => u.fomo < dumb_fomo && u.bagholder >= dumb_bag).map(u => { assignedDumb.add(u.userId); return u.userId; });
165
- cohorts['fomo_bagholders'] = this.dumbVectors.filter(u => u.fomo >= dumb_fomo && u.bagholder >= dumb_bag).map(u => { assignedDumb.add(u.userId); return u.userId; });
166
- cohorts['uncategorized_dumb'] = this.dumbVectors.filter(u => !assignedDumb.has(u.userId)).map(u => u.userId);
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
- this.cohortMomentum = new Map();
8
- this.cohortMap = new Map();
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 cohortSchema = {
29
- "type": "object",
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 cohortData = computed['cohort-skill-definition'];
42
- if (cohortData) {
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
- if (!this.tickerMap) this.tickerMap = mappings.instrumentToTicker;
59
-
60
- const cohortName = this.cohortMap.get(user.id);
61
- if (!cohortName) return;
62
-
63
- const momentumData = computed['instrument-price-momentum-20d'];
64
- if (!momentumData) return;
65
-
66
- const yIds = new Set(extract.getPositions(user.portfolio.yesterday, user.type).map(p => extract.getInstrumentId(p)));
67
- const newPositions = extract.getPositions(user.portfolio.today, user.type).filter(p => !yIds.has(extract.getInstrumentId(p)));
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
- for (const pos of newPositions) {
75
- const ticker = mappings.instrumentToTicker[extract.getInstrumentId(pos)];
76
- if (ticker && momentumData[ticker]) {
77
- asset.momentum_sum += momentumData[ticker].momentum_20d_pct || 0;
78
- asset.count++;
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 finalResult = {};
85
- for (const [cohortName, data] of this.cohortMomentum.entries()) {
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 finalResult;
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;