aiden-shared-calculations-unified 1.0.36 → 1.0.38

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.
@@ -2,85 +2,59 @@
2
2
  * @fileoverview Calculates "Net Crowd Flow" and "Sector Rotation"
3
3
  * *only* for the "Dumb Cohort" (Bottom 20% of Investor Scores).
4
4
  *
5
- * This calc depends on 'user-investment-profile.js' being run first for the same day.
5
+ * --- META REFACTOR ---
6
+ * This calculation is now `type: "meta"` to consume in-memory dependencies.
7
+ * It runs ONCE per day, receives the in-memory cache, and must
8
+ * perform its own user data streaming.
6
9
  */
7
10
 
8
11
  const { Firestore } = require('@google-cloud/firestore');
9
12
  const firestore = new Firestore();
10
13
  const { loadAllPriceData, getDailyPriceChange } = require('../../../utils/price_data_provider');
11
14
  const { loadInstrumentMappings, getInstrumentSectorMap } = require('../../../utils/sector_mapping_provider');
15
+ const { loadDataByRefs } = require('../../../../bulltrackers-module/functions/computation-system/utils/data_loader'); // Adjust path as needed
12
16
 
13
17
  const COHORT_PERCENTILE = 0.2; // Bottom 20%
14
18
  const PROFILE_CALC_ID = 'user-investment-profile'; // The calc to read IS scores from
15
19
 
16
20
  class DumbCohortFlow {
17
21
  constructor() {
18
- // Asset Flow
19
- this.asset_values = {}; // { instrumentId: { day1_value_sum: 0, day2_value_sum: 0 } }
20
- // Sector Rotation
21
- this.todaySectorInvestment = {};
22
- this.yesterdaySectorInvestment = {};
23
-
24
- // --- START MODIFICATION ---
25
- this.dumbCohortIds = null; // Set to null. Will be a Set on success.
26
- // --- END MODIFICATION ---
27
-
28
- this.user_count = 0; // Number of *cohort* users
29
- this.priceMap = null;
30
- this.mappings = null;
31
- this.sectorMap = null;
32
- this.dates = {};
22
+ // Meta-calc, no constructor state needed
33
23
  }
34
24
 
35
25
  /**
36
26
  * Loads the Investor Scores, calculates the cohort threshold, and builds the Set of user IDs.
27
+ * --- MODIFIED: Reads from in-memory 'computedDependencies' ---
37
28
  */
38
- async _loadCohort(context, dependencies) {
39
- const { db, logger } = dependencies;
40
- logger.log('INFO', '[DumbCohortFlow] Loading Investor Scores to build cohort...');
29
+ _loadCohort(logger, computedDependencies) {
30
+ logger.log('INFO', '[DumbCohortFlow] Loading Investor Scores from in-memory cache...');
41
31
 
42
- try {
43
- const scoreMapRef = db.collection(context.config.resultsCollection).doc(context.todayDateStr)
44
- .collection(context.config.resultsSubcollection).doc('behavioural')
45
- .collection(context.config.computationsSubcollection).doc(PROFILE_CALC_ID);
46
-
47
- const doc = await scoreMapRef.get();
48
-
49
- // --- START MODIFICATION ---
50
- // Check for doc, data, and that the scores map isn't empty
51
- if (!doc.exists || !doc.data().daily_investor_scores || Object.keys(doc.data().daily_investor_scores).length === 0) {
52
- logger.log('WARN', '[DumbCohortFlow] Cannot find dependency: daily_investor_scores. Cohort will not be built. Returning null on getResult.');
53
- // Keep this.dumbCohortIds = null
54
- return; // Abort
55
- }
56
- // --- END MODIFICATION ---
57
-
58
- const scores = doc.data().daily_investor_scores;
59
- const allScores = Object.entries(scores).map(([userId, score]) => ({ userId, score }));
60
- allScores.sort((a, b) => a.score - b.score);
61
-
62
- const thresholdIndex = Math.floor(allScores.length * COHORT_PERCENTILE);
63
- const thresholdScore = allScores[thresholdIndex]?.score || 0; // Get 20th percentile score
64
-
65
- // --- START MODIFICATION ---
66
- // Successfully loaded, now create the Set
67
- this.dumbCohortIds = new Set(
68
- allScores.filter(s => s.score <= thresholdScore).map(s => s.userId) // Get users *at or below*
69
- );
70
- // --- END MODIFICATION ---
71
-
72
- logger.log('INFO', `[DumbCohortFlow] Cohort built. ${this.dumbCohortIds.size} users at or below ${thresholdScore.toFixed(2)} (20th percentile).`);
73
-
74
- } catch (e) {
75
- logger.log('ERROR', '[DumbCohortFlow] Failed to load cohort.', { error: e.message });
76
- // Keep this.dumbCohortIds = null on error
32
+ const profileData = computedDependencies[PROFILE_CALC_ID];
33
+
34
+ if (!profileData || !profileData.daily_investor_scores || Object.keys(profileData.daily_investor_scores).length === 0) {
35
+ logger.log('WARN', `[DumbCohortFlow] Cannot find dependency in-memory: ${PROFILE_CALC_ID}. Cohort will not be built.`);
36
+ return null; // Return null to signal failure
77
37
  }
38
+
39
+ const scores = profileData.daily_investor_scores;
40
+ const allScores = Object.entries(scores).map(([userId, score]) => ({ userId, score }));
41
+ allScores.sort((a, b) => a.score - b.score);
42
+
43
+ const thresholdIndex = Math.floor(allScores.length * COHORT_PERCENTILE);
44
+ const thresholdScore = allScores[thresholdIndex]?.score || 0; // Get 20th percentile score
45
+
46
+ const dumbCohortIds = new Set(
47
+ allScores.filter(s => s.score <= thresholdScore).map(s => s.userId) // Get users *at or below*
48
+ );
49
+
50
+ logger.log('INFO', `[DumbCohortFlow] Cohort built. ${dumbCohortIds.size} users at or below ${thresholdScore.toFixed(2)} (20th percentile).`);
51
+ return dumbCohortIds;
78
52
  }
79
53
 
80
- // --- Asset Flow Helpers ---
81
- _initAsset(instrumentId) {
82
- if (!this.asset_values[instrumentId]) {
83
- this.asset_values[instrumentId] = { day1_value_sum: 0, day2_value_sum: 0 };
54
+ // --- Asset Flow Helpers (unchanged) ---
55
+ _initAsset(asset_values, instrumentId) {
56
+ if (!asset_values[instrumentId]) {
57
+ asset_values[instrumentId] = { day1_value_sum: 0, day2_value_sum: 0 };
84
58
  }
85
59
  }
86
60
  _sumAssetValue(positions) {
@@ -93,111 +67,117 @@ class DumbCohortFlow {
93
67
  }
94
68
  return valueMap;
95
69
  }
96
- // --- Sector Rotation Helper ---
97
- _accumulateSectorInvestment(portfolio, target) {
70
+ // --- Sector Rotation Helper (unchanged) ---
71
+ _accumulateSectorInvestment(portfolio, target, sectorMap) {
98
72
  if (portfolio && portfolio.AggregatedPositions) {
99
73
  for (const pos of portfolio.AggregatedPositions) {
100
- const sector = this.sectorMap[pos.InstrumentID] || 'N/A';
74
+ const sector = sectorMap[pos.InstrumentID] || 'N/A';
101
75
  target[sector] = (target[sector] || 0) + (pos.Invested || pos.Amount || 0);
102
76
  }
103
77
  }
104
78
  }
105
79
 
106
80
  /**
107
- * PROCESS: Runs daily for each user.
81
+ * PROCESS: META REFACTOR
82
+ * This now runs ONCE, loads all data, streams users, and returns one big result.
108
83
  */
109
- async process(todayPortfolio, yesterdayPortfolio, userId, context) {
110
- // 1. Load cohort on first run
111
- if (!this.dumbCohortIds) {
112
- await this._loadCohort(context, context.dependencies);
113
- this.dates.today = context.todayDateStr;
114
- this.dates.yesterday = context.yesterdayDateStr;
115
- }
116
-
117
- // 2. Filter user
118
- // --- START MODIFICATION ---
119
- // If cohort failed to load, this.dumbCohortIds will be null, and this check will fail correctly.
120
- if (!this.dumbCohortIds || !this.dumbCohortIds.has(userId) || !todayPortfolio || !yesterdayPortfolio || !todayPortfolio.AggregatedPositions || !yesterdayPortfolio.AggregatedPositions) {
121
- return;
122
- }
123
- // --- END MODIFICATION ---
84
+ async process(dateStr, dependencies, config, computedDependencies) {
85
+ const { logger, db, rootData, calculationUtils } = dependencies;
86
+ const { portfolioRefs } = rootData;
87
+ logger.log('INFO', '[DumbCohortFlow] Starting meta-process...');
124
88
 
125
- // 3. User is in the cohort, load maps if needed
126
- if (!this.sectorMap) {
127
- this.sectorMap = await getInstrumentSectorMap();
89
+ // 1. Load Cohort from in-memory dependency
90
+ const dumbCohortIds = this._loadCohort(logger, computedDependencies);
91
+ if (!dumbCohortIds) {
92
+ return null; // Dependency failed
128
93
  }
129
94
 
130
- // --- 4. RUN ASSET FLOW LOGIC ---
131
- const yesterdayValues = this._sumAssetValue(yesterdayPortfolio.AggregatedPositions);
132
- const todayValues = this._sumAssetValue(todayPortfolio.AggregatedPositions);
133
- const allInstrumentIds = new Set([...Object.keys(yesterdayValues), ...Object.keys(todayValues)]);
134
-
135
- for (const instrumentId of allInstrumentIds) {
136
- this._initAsset(instrumentId);
137
- this.asset_values[instrumentId].day1_value_sum += (yesterdayValues[instrumentId] || 0);
138
- this.asset_values[instrumentId].day2_value_sum += (todayValues[instrumentId] || 0);
95
+ // 2. Load external dependencies (prices, sectors)
96
+ const [priceMap, mappings, sectorMap] = await Promise.all([
97
+ loadAllPriceData(),
98
+ loadInstrumentMappings(),
99
+ getInstrumentSectorMap()
100
+ ]);
101
+ if (!priceMap || !mappings || !sectorMap || Object.keys(priceMap).length === 0) {
102
+ logger.log('ERROR', '[DumbCohortFlow] Failed to load critical price/mapping/sector data. Aborting.');
103
+ return null; // Return null to trigger backfill
139
104
  }
140
105
 
141
- // --- 5. RUN SECTOR ROTATION LOGIC ---
142
- this._accumulateSectorInvestment(todayPortfolio, this.todaySectorInvestment);
143
- this._accumulateSectorInvestment(yesterdayPortfolio, this.yesterdaySectorInvestment);
106
+ // 3. Load "yesterday's" portfolio data for comparison
107
+ const yesterdayDate = new Date(dateStr + 'T00:00:00Z');
108
+ yesterdayDate.setUTCDate(yesterdayDate.getUTCDate() - 1);
109
+ const yesterdayStr = yesterdayDate.toISOString().slice(0, 10);
110
+ const yesterdayRefs = await calculationUtils.getPortfolioPartRefs(config, dependencies, yesterdayStr);
111
+ const yesterdayPortfolios = await loadFullDayMap(config, dependencies, yesterdayRefs);
112
+ logger.log('INFO', `[DumbCohortFlow] Loaded ${yesterdayRefs.length} part refs for yesterday.`);
144
113
 
145
- this.user_count++;
146
- }
114
+ // 4. Stream "today's" portfolio data and process
115
+
116
+ // --- Local state for this run ---
117
+ const asset_values = {};
118
+ const todaySectorInvestment = {};
119
+ const yesterdaySectorInvestment = {};
120
+ let user_count = 0;
121
+ // --- End Local state ---
122
+
123
+ const batchSize = config.partRefBatchSize || 10;
124
+ for (let i = 0; i < portfolioRefs.length; i += batchSize) {
125
+ const batchRefs = portfolioRefs.slice(i, i + batchSize);
126
+ const todayPortfoliosChunk = await loadDataByRefs(config, dependencies, batchRefs);
127
+
128
+ for (const uid in todayPortfoliosChunk) {
129
+
130
+ // --- Filter user ---
131
+ if (!dumbCohortIds.has(uid)) {
132
+ continue;
133
+ }
134
+
135
+ const pToday = todayPortfoliosChunk[uid];
136
+ const pYesterday = yesterdayPortfolios[uid];
137
+
138
+ if (!pToday || !pYesterday || !pToday.AggregatedPositions || !pYesterday.AggregatedPositions) {
139
+ continue;
140
+ }
141
+
142
+ // --- User is in cohort, run logic ---
143
+
144
+ // 4a. RUN ASSET FLOW LOGIC
145
+ const yesterdayValues = this._sumAssetValue(pYesterday.AggregatedPositions);
146
+ const todayValues = this._sumAssetValue(pToday.AggregatedPositions);
147
+ const allInstrumentIds = new Set([...Object.keys(yesterdayValues), ...Object.keys(todayValues)]);
148
+
149
+ for (const instrumentId of allInstrumentIds) {
150
+ this._initAsset(asset_values, instrumentId);
151
+ asset_values[instrumentId].day1_value_sum += (yesterdayValues[instrumentId] || 0);
152
+ asset_values[instrumentId].day2_value_sum += (todayValues[instrumentId] || 0);
153
+ }
147
154
 
148
- /**
149
- * GETRESULT: Aggregates and returns the flow data for the cohort.
150
- */
151
- async getResult() {
152
- // --- START MODIFICATION ---
153
- // If cohort IDs were never loaded due to dependency failure, return null.
154
- if (this.dumbCohortIds === null) {
155
- console.warn('[DumbCohortFlow] Skipping getResult because dependency (user-investment-profile) failed to load.');
156
- return null;
155
+ // 4b. RUN SECTOR ROTATION LOGIC
156
+ this._accumulateSectorInvestment(pToday, todaySectorInvestment, sectorMap);
157
+ this._accumulateSectorInvestment(pYesterday, yesterdaySectorInvestment, sectorMap);
158
+
159
+ user_count++;
160
+ }
157
161
  }
158
162
 
159
- // If cohort loaded but no users were processed, also return null (or an empty object, but null is safer for backfill)
160
- if (this.user_count === 0 || !this.dates.today) {
161
- console.warn('[DumbCohortFlow] No users processed for dumb cohort. Returning null.');
163
+ logger.log('INFO', `[DumbCohortFlow] Processed ${user_count} users in cohort.`);
164
+
165
+ // --- 5. GETRESULT LOGIC IS NOW INSIDE PROCESS ---
166
+
167
+ if (user_count === 0) {
168
+ logger.warn('[DumbCohortFlow] No users processed for dumb cohort. Returning null.');
162
169
  return null;
163
170
  }
164
- // --- END MODIFICATION ---
165
-
166
- // 1. Load dependencies
167
- if (!this.priceMap || !this.mappings) {
168
- // --- START MODIFICATION ---
169
- // Add error handling for this load, and check for empty priceMap
170
- try {
171
- const [priceData, mappingData] = await Promise.all([
172
- loadAllPriceData(),
173
- loadInstrumentMappings()
174
- ]);
175
- this.priceMap = priceData;
176
- this.mappings = mappingData;
177
-
178
- if (!this.priceMap || Object.keys(this.priceMap).length === 0) {
179
- console.error('[DumbCohortFlow] CRITICAL: Price map is empty or failed to load. Aborting calculation to allow backfill.');
180
- return null; // Return null to trigger backfill
181
- }
182
- } catch (e) {
183
- console.error('[DumbCohortFlow] Failed to load price/mapping dependencies:', e);
184
- return null;
185
- }
186
- // --- END MODIFICATION ---
187
- }
188
171
 
189
- // --- 2. Calculate Asset Flow ---
172
+ // 5a. Calculate Asset Flow
190
173
  const finalAssetFlow = {};
191
- const todayStr = this.dates.today;
192
- const yesterdayStr = this.dates.yesterday;
193
-
194
- for (const instrumentId in this.asset_values) {
195
- const ticker = this.mappings.instrumentToTicker[instrumentId] || `id_${instrumentId}`;
196
- const avg_day1_value = this.asset_values[instrumentId].day1_value_sum / this.user_count;
197
- const avg_day2_value = this.asset_values[instrumentId].day2_value_sum / this.user_count;
198
- const priceChangePct = getDailyPriceChange(instrumentId, yesterdayStr, todayStr, this.priceMap);
174
+ for (const instrumentId in asset_values) {
175
+ const ticker = mappings.instrumentToTicker[instrumentId] || `id_${instrumentId}`;
176
+ const avg_day1_value = asset_values[instrumentId].day1_value_sum / user_count;
177
+ const avg_day2_value = asset_values[instrumentId].day2_value_sum / user_count;
178
+ const priceChangePct = getDailyPriceChange(instrumentId, yesterdayStr, dateStr, priceMap);
199
179
 
200
- if (priceChangePct === null) continue; // Skip if price data missing
180
+ if (priceChangePct === null) continue;
201
181
 
202
182
  const expected_day2_value = avg_day1_value * (1 + priceChangePct);
203
183
  const net_crowd_flow_pct = avg_day2_value - expected_day2_value;
@@ -209,42 +189,30 @@ class DumbCohortFlow {
209
189
  };
210
190
  }
211
191
 
212
- // --- 3. Calculate Sector Rotation ---
192
+ // 5b. Calculate Sector Rotation
213
193
  const finalSectorRotation = {};
214
- const allSectors = new Set([...Object.keys(this.todaySectorInvestment), ...Object.keys(this.yesterdaySectorInvestment)]);
194
+ const allSectors = new Set([...Object.keys(todaySectorInvestment), ...Object.keys(yesterdaySectorInvestment)]);
215
195
  for (const sector of allSectors) {
216
- const todayAmount = this.todaySectorInvestment[sector] || 0;
217
- const yesterdayAmount = this.yesterdaySectorInvestment[sector] || 0;
196
+ const todayAmount = todaySectorInvestment[sector] || 0;
197
+ const yesterdayAmount = yesterdaySectorInvestment[sector] || 0;
218
198
  finalSectorRotation[sector] = todayAmount - yesterdayAmount;
219
199
  }
220
-
221
- // --- START MODIFICATION ---
222
- // If no asset flow was calculated (e.g., all price data missing), fail
200
+
223
201
  if (Object.keys(finalAssetFlow).length === 0) {
224
- console.warn('[DumbCohortFlow] No asset flow calculated (likely all price data missing). Returning null.');
202
+ logger.warn('[DumbCohortFlow] No asset flow calculated (likely all price data missing). Returning null.');
225
203
  return null;
226
204
  }
227
- // --- END MODIFICATION ---
228
205
 
229
- // 4. Return combined result
206
+ // 6. Return combined result
230
207
  return {
231
208
  asset_flow: finalAssetFlow,
232
209
  sector_rotation: finalSectorRotation,
233
- user_sample_size: this.user_count
210
+ user_sample_size: user_count
234
211
  };
235
212
  }
236
213
 
237
- reset() {
238
- this.asset_values = {};
239
- this.todaySectorInvestment = {};
240
- this.yesterdaySectorInvestment = {};
241
- this.dumbCohortIds = null;
242
- this.user_count = 0;
243
- this.priceMap = null;
244
- this.mappings = null;
245
- this.sectorMap = null;
246
- this.dates = {};
247
- }
214
+ async getResult() { return null; }
215
+ reset() { }
248
216
  }
249
217
 
250
218
  module.exports = DumbCohortFlow;