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 "Smart Cohort" (Top 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.8; // Top 20%
14
18
  const PROFILE_CALC_ID = 'user-investment-profile'; // The calc to read IS scores from
15
19
 
16
20
  class SmartCohortFlow {
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.smartCohortIds = 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', '[SmartCohortFlow] Loading Investor Scores to build cohort...');
29
+ _loadCohort(logger, computedDependencies) {
30
+ logger.log('INFO', '[SmartCohortFlow] 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', '[SmartCohortFlow] Cannot find dependency: daily_investor_scores. Cohort will not be built. Returning null on getResult.');
53
- // Keep this.smartCohortIds = 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 || 999;
64
-
65
- // --- START MODIFICATION ---
66
- // Successfully loaded, now create the Set
67
- this.smartCohortIds = new Set(
68
- allScores.filter(s => s.score >= thresholdScore).map(s => s.userId)
69
- );
70
- // --- END MODIFICATION ---
71
-
72
- logger.log('INFO', `[SmartCohortFlow] Cohort built. ${this.smartCohortIds.size} users at or above ${thresholdScore.toFixed(2)} (80th percentile).`);
73
-
74
- } catch (e) {
75
- logger.log('ERROR', '[SmartCohortFlow] Failed to load cohort.', { error: e.message });
76
- // Keep this.smartCohortIds = 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', `[SmartCohortFlow] 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 || 999;
45
+
46
+ const smartCohortIds = new Set(
47
+ allScores.filter(s => s.score >= thresholdScore).map(s => s.userId)
48
+ );
49
+
50
+ logger.log('INFO', `[SmartCohortFlow] Cohort built. ${smartCohortIds.size} users at or above ${thresholdScore.toFixed(2)} (80th percentile).`);
51
+ return smartCohortIds;
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,110 +67,115 @@ class SmartCohortFlow {
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.smartCohortIds) {
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.smartCohortIds will be null, and this check will fail correctly.
120
- if (!this.smartCohortIds || !this.smartCohortIds.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', '[SmartCohortFlow] 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 smartCohortIds = this._loadCohort(logger, computedDependencies);
91
+ if (!smartCohortIds) {
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', '[SmartCohortFlow] 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', `[SmartCohortFlow] 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 (!smartCohortIds.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.smartCohortIds === null) {
155
- console.warn('[SmartCohortFlow] 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('[SmartCohortFlow] No users processed for smart cohort. Returning null.');
163
+ logger.log('INFO', `[SmartCohortFlow] Processed ${user_count} users in cohort.`);
164
+
165
+ // --- 5. GETRESULT LOGIC IS NOW INSIDE PROCESS ---
166
+
167
+ if (user_count === 0) {
168
+ logger.warn('[SmartCohortFlow] No users processed for smart cohort. Returning null.');
162
169
  return null;
163
170
  }
164
- // --- END MODIFICATION ---
165
-
166
-
167
- // 1. Load dependencies
168
- if (!this.priceMap || !this.mappings) {
169
- // --- START MODIFICATION ---
170
- // Add error handling for this load, and check for empty priceMap
171
- try {
172
- const [priceData, mappingData] = await Promise.all([
173
- loadAllPriceData(),
174
- loadInstrumentMappings()
175
- ]);
176
- this.priceMap = priceData;
177
- this.mappings = mappingData;
178
-
179
- if (!this.priceMap || Object.keys(this.priceMap).length === 0) {
180
- console.error('[SmartCohortFlow] CRITICAL: Price map is empty or failed to load. Aborting calculation to allow backfill.');
181
- return null; // Return null to trigger backfill
182
- }
183
- } catch (e) {
184
- console.error('[SmartCohortFlow] Failed to load price/mapping dependencies:', e);
185
- return null;
186
- }
187
- // --- END MODIFICATION ---
188
- }
189
171
 
190
- // --- 2. Calculate Asset Flow ---
172
+ // 5a. Calculate Asset Flow
191
173
  const finalAssetFlow = {};
192
- const todayStr = this.dates.today;
193
- const yesterdayStr = this.dates.yesterday;
194
-
195
- for (const instrumentId in this.asset_values) {
196
- const ticker = this.mappings.instrumentToTicker[instrumentId] || `id_${instrumentId}`;
197
- const avg_day1_value = this.asset_values[instrumentId].day1_value_sum / this.user_count;
198
- const avg_day2_value = this.asset_values[instrumentId].day2_value_sum / this.user_count;
199
- 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);
200
179
 
201
180
  if (priceChangePct === null) continue;
202
181
 
@@ -210,42 +189,30 @@ class SmartCohortFlow {
210
189
  };
211
190
  }
212
191
 
213
- // --- 3. Calculate Sector Rotation ---
192
+ // 5b. Calculate Sector Rotation
214
193
  const finalSectorRotation = {};
215
- const allSectors = new Set([...Object.keys(this.todaySectorInvestment), ...Object.keys(this.yesterdaySectorInvestment)]);
194
+ const allSectors = new Set([...Object.keys(todaySectorInvestment), ...Object.keys(yesterdaySectorInvestment)]);
216
195
  for (const sector of allSectors) {
217
- const todayAmount = this.todaySectorInvestment[sector] || 0;
218
- const yesterdayAmount = this.yesterdaySectorInvestment[sector] || 0;
219
- finalSectorRotation[sector] = todayAmount - yesterdayAmount; // Note: This is total $, not avg.
196
+ const todayAmount = todaySectorInvestment[sector] || 0;
197
+ const yesterdayAmount = yesterdaySectorInvestment[sector] || 0;
198
+ finalSectorRotation[sector] = todayAmount - yesterdayAmount;
220
199
  }
221
200
 
222
- // --- START MODIFICATION ---
223
- // If no asset flow was calculated (e.g., all price data missing), fail
224
201
  if (Object.keys(finalAssetFlow).length === 0) {
225
- console.warn('[SmartCohortFlow] No asset flow calculated (likely all price data missing). Returning null.');
202
+ logger.warn('[SmartCohortFlow] No asset flow calculated (likely all price data missing). Returning null.');
226
203
  return null;
227
204
  }
228
- // --- END MODIFICATION ---
229
205
 
230
- // 4. Return combined result
206
+ // 6. Return combined result
231
207
  return {
232
208
  asset_flow: finalAssetFlow,
233
209
  sector_rotation: finalSectorRotation,
234
- user_sample_size: this.user_count
210
+ user_sample_size: user_count
235
211
  };
236
212
  }
237
213
 
238
- reset() {
239
- this.asset_values = {};
240
- this.todaySectorInvestment = {};
241
- this.yesterdaySectorInvestment = {};
242
- this.smartCohortIds = null;
243
- this.user_count = 0;
244
- this.priceMap = null;
245
- this.mappings = null;
246
- this.sectorMap = null;
247
- this.dates = {};
248
- }
214
+ async getResult() { return null; }
215
+ reset() { }
249
216
  }
250
217
 
251
218
  module.exports = SmartCohortFlow;