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.
@@ -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;
@@ -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
- this.assetFlows = new Map();
8
- this.cohortMap = new Map();
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 tickerSchema = {
29
- "type": "object",
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 cohortData = computed['cohort-skill-definition'];
53
- if (cohortData) {
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 priceChangeMap = computed['instrument-price-change-1d'];
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 allInstrumentIds = new Set([...yPosMap.keys(), ...tPosMap.keys()]);
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
- if (yWeight > 0) {
87
- asset.total_invested_yesterday += yWeight;
88
- const ticker = mappings.instrumentToTicker[instId];
89
- const priceChange = (ticker && priceChangeMap[ticker]) ? priceChangeMap[ticker].change_1d_pct : 0;
90
- asset.price_change_yesterday += (priceChange / 100.0) * yWeight;
91
- asset.total_pos_size_yesterday += yWeight;
92
- asset.user_count_yesterday++;
93
- }
94
- if (tWeight > 0) {
95
- asset.total_invested_today += tWeight;
96
- asset.user_count_today++;
97
- asset.total_pos_size_today += tWeight;
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 finalResult = {};
105
- for (const [instId, data] of this.assetFlows.entries()) {
106
- const ticker = this.tickerMap[instId];
107
- if (!ticker) continue;
108
- // ... (Same calculation logic as before, just boilerplate math)
109
- let net_flow = 0, flow_contrib = 0;
110
- if (data.total_invested_yesterday > 0) {
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 finalResult;
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;