aiden-shared-calculations-unified 1.0.36 → 1.0.37

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;
@@ -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;
@@ -1,19 +1,17 @@
1
1
  /**
2
2
  * @fileoverview Calculates a rolling 90-day "Investor Score" (IS) for each normal user.
3
- * Heuristic engine (not an academic finance model). Outputs:
4
- * - sharded_user_profile: { <shardKey>: { profiles: { userId: [history...] }, lastUpdated } }
5
- * - daily_investor_scores: { userId: finalIS }
6
3
  *
7
- * Notes:
8
- * - NetProfit / ProfitAndLoss fields are assumed to be percent returns in decimal (e.g. 0.03 = +3%).
9
- * - The "Sharpe" used here is a cross-sectional dispersion proxy computed over position returns,
10
- * weighted by invested amounts. It's renamed/treated as a dispersionRiskProxy in comments.
4
+ * --- META REFACTOR ---
5
+ * This calculation is now `type: "meta"` to consume in-memory dependencies.
6
+ * It runs ONCE per day, receives the in-memory cache, and must
7
+ * perform its own user data streaming.
11
8
  */
12
9
 
13
10
  const { Firestore } = require('@google-cloud/firestore');
14
11
  const firestore = new Firestore();
15
- const { loadAllPriceData } = require('../../../utils/price_data_provider');
16
- const { getInstrumentSectorMap, loadInstrumentMappings } = require('../../../utils/sector_mapping_provider');
12
+ const { loadAllPriceData, getDailyPriceChange } = require('../../../utils/price_data_provider');
13
+ const { loadInstrumentMappings, getInstrumentSectorMap } = require('../../../utils/sector_mapping_provider');
14
+ const { loadDataByRefs } = require('../../../../bulltrackers-module/functions/computation-system/utils/data_loader'); // Adjust path as needed
17
15
 
18
16
  // Config
19
17
  const NUM_SHARDS = 50; // Must match the number of shards to read/write
@@ -25,88 +23,30 @@ const PNL_TRACKER_CALC_ID = 'user-profitability-tracker'; // The calc to read PN
25
23
  function getShardIndex(id) {
26
24
  const n = parseInt(id, 10);
27
25
  if (!Number.isNaN(n)) return Math.abs(n) % NUM_SHARDS;
28
- // simple deterministic string hash fallback for non-numeric IDs (UUIDs)
29
26
  let h = 0;
30
27
  for (let i = 0; i < id.length; i++) {
31
28
  h = ((h << 5) - h) + id.charCodeAt(i);
32
- h |= 0; // keep 32-bit
29
+ h |= 0;
33
30
  }
34
31
  return Math.abs(h) % NUM_SHARDS;
35
32
  }
36
33
 
37
34
  class UserInvestmentProfile {
38
35
  constructor() {
39
- // will hold today's per-user raw heuristic scores
40
- this.dailyUserScores = {}; // { userId: { score_rd, score_disc, score_time } }
41
-
42
- // cached dependencies
43
- this.priceMap = null;
44
- this.sectorMap = null;
45
- this.pnlScores = null; // { userId: dailyPnlDecimal }
46
- this.dates = {};
47
- this.dependenciesLoaded = false;
48
-
49
- // --- START MODIFICATION ---
50
- // Flag to track if dependencies loaded successfully
51
- this.dependencyLoadedSuccess = false;
52
- // --- END MODIFICATION ---
36
+ // --- META REFACTOR ---
37
+ // All state is now managed inside the `process` function.
38
+ // The constructor, getResult, and reset methods are no longer used
39
+ // by the meta-runner, but we leave them for compatibility.
40
+ // --- END REFACTOR ---
53
41
  }
54
42
 
55
- /**
56
- * Loads external dependencies once per run.
57
- */
58
- async _loadDependencies(context, dependencies) {
59
- if (this.dependenciesLoaded) return;
60
-
61
- const { db, logger } = dependencies;
62
- const { todayDateStr } = context;
63
-
64
- if (logger) logger.log('INFO', '[UserInvestmentProfile] Loading dependencies...');
65
-
66
- // load price data and sector mapping in parallel
67
- const [priceData, sectorData] = await Promise.all([
68
- loadAllPriceData(),
69
- getInstrumentSectorMap()
70
- ]);
71
- this.priceMap = priceData || {};
72
- this.sectorMap = sectorData || {};
73
-
74
- // load PNL map (daily percent returns per user) from PNL calc
75
- this.pnlScores = {};
76
- try {
77
- const pnlCalcRef = db.collection(context.config.resultsCollection).doc(todayDateStr)
78
- .collection(context.config.resultsSubcollection).doc('pnl')
79
- .collection(context.config.computationsSubcollection).doc(PNL_TRACKER_CALC_ID);
80
-
81
- const pnlSnap = await pnlCalcRef.get();
82
-
83
- // --- START MODIFICATION ---
84
- // Check for existence of the doc AND the data within it
85
- if (pnlSnap.exists && pnlSnap.data().daily_pnl_map) {
86
- this.pnlScores = pnlSnap.data().daily_pnl_map || {};
87
- if (logger) logger.log('INFO', `[UserInvestmentProfile] Loaded ${Object.keys(this.pnlScores).length} PNL scores.`);
88
- this.dependencyLoadedSuccess = true; // Set success flag
89
- } else {
90
- if (logger) logger.log('WARN', `[UserInvestmentProfile] Could not find PNL scores dependency for ${todayDateStr}. PNL score will be 0. Aborting profile calculation.`);
91
- this.dependencyLoadedSuccess = false; // Set failure flag
92
- }
93
- // --- END MODIFICATION ---
94
-
95
- } catch (e) {
96
- if (logger) logger.log('ERROR', `[UserInvestmentProfile] Failed to load PNL scores.`, { error: e.message });
97
- this.dependencyLoadedSuccess = false; // Set failure flag on error
98
- }
99
-
100
- this.dependenciesLoaded = true;
101
- if (logger) logger.log('INFO', '[UserInvestmentProfile] All dependencies loaded.');
102
- }
103
-
104
- // ... [Heuristic calculation functions _calculateRiskAndDivScore, _calculateDisciplineScore, _calculateMarketTimingScore are unchanged] ...
43
+ // ... [Heuristic calculation functions _calculateRiskAndDivScore, _calculateDisciplineScore, _calculateMarketTimingScore] ...
44
+ // These helper functions remain identical to your original file.
105
45
 
106
46
  /**
107
47
  * HEURISTIC 1: Risk & Diversification Score (0-10).
108
48
  */
109
- _calculateRiskAndDivScore(todayPortfolio) {
49
+ _calculateRiskAndDivScore(todayPortfolio, sectorMap) {
110
50
  if (!todayPortfolio.AggregatedPositions || todayPortfolio.AggregatedPositions.length === 0) {
111
51
  return 5; // neutral
112
52
  }
@@ -128,7 +68,7 @@ class UserInvestmentProfile {
128
68
  totalInvested += invested;
129
69
  if (invested > maxPosition) maxPosition = invested;
130
70
 
131
- sectors.add(this.sectorMap[pos.InstrumentID] || 'N/A');
71
+ sectors.add(sectorMap[pos.InstrumentID] || 'N/A');
132
72
  }
133
73
 
134
74
  // Weighted mean & variance of returns
@@ -215,7 +155,7 @@ class UserInvestmentProfile {
215
155
  /**
216
156
  * HEURISTIC 3: Market Timing Score (0-10).
217
157
  */
218
- _calculateMarketTimingScore(yesterdayPortfolio = {}, todayPortfolio = {}) {
158
+ _calculateMarketTimingScore(yesterdayPortfolio = {}, todayPortfolio = {}, priceMap) {
219
159
  const yIds = new Set((yesterdayPortfolio.AggregatedPositions || []).map(p => p.PositionID));
220
160
  const newPositions = (todayPortfolio.AggregatedPositions || []).filter(p => !yIds.has(p.PositionID));
221
161
 
@@ -225,7 +165,7 @@ class UserInvestmentProfile {
225
165
  let timingCount = 0;
226
166
 
227
167
  for (const tPos of newPositions) {
228
- const prices = this.priceMap[tPos.InstrumentID];
168
+ const prices = priceMap[tPos.InstrumentID];
229
169
  if (!prices) continue;
230
170
 
231
171
  // Accept prices as either array or {date:price} map; build sorted array of prices
@@ -267,98 +207,105 @@ class UserInvestmentProfile {
267
207
  const avg = (timingCount > 0) ? (timingPoints / timingCount) : 5;
268
208
  return Math.max(0, Math.min(10, avg));
269
209
  }
270
-
271
- /**
272
- * PROCESS: called per-user per-day to compute and store today's heuristics.
273
- */
274
- async process(todayPortfolio, yesterdayPortfolio, userId, context, todayInsights, yesterdayInsights, todaySocial, yesterdaySocial) {
275
- // run only for normal users with portfolios
276
- if (!todayPortfolio || !todayPortfolio.AggregatedPositions) return;
277
210
 
278
- if (!this.dependenciesLoaded) {
279
- await this._loadDependencies(context, context.dependencies);
280
- this.dates.today = context.todayDateStr;
281
- }
282
-
283
- // --- START MODIFICATION ---
284
- // If dependencies failed to load (e.g., PNL doc was missing), stop processing.
285
- if (!this.dependencyLoadedSuccess) {
286
- return;
287
- }
288
- // --- END MODIFICATION ---
289
211
 
290
- const yPort = yesterdayPortfolio || {};
212
+ /**
213
+ * PROCESS: META REFACTOR
214
+ * This now runs ONCE, loads all data, streams users, and returns one big result.
215
+ */
216
+ async process(dateStr, dependencies, config, computedDependencies) {
217
+ const { logger, db, rootData, calculationUtils } = dependencies;
218
+ const { portfolioRefs } = rootData;
291
219
 
292
- const score_rd = this._calculateRiskAndDivScore(todayPortfolio);
293
- const score_disc = this._calculateDisciplineScore(yPort, todayPortfolio);
294
- const score_time = this._calculateMarketTimingScore(yPort, todayPortfolio);
220
+ logger.log('INFO', '[UserInvestmentProfile] Starting meta-process...');
295
221
 
296
- this.dailyUserScores[userId] = {
297
- score_rd,
298
- score_disc,
299
- score_time
300
- };
301
- }
222
+ // 1. Get Pass 1 dependency from in-memory cache
223
+ const pnlTrackerResult = computedDependencies[PNL_TRACKER_CALC_ID];
224
+ if (!pnlTrackerResult || !pnlTrackerResult.daily_pnl_map) {
225
+ logger.log('WARN', `[UserInvestmentProfile] Missing in-memory dependency '${PNL_TRACKER_CALC_ID}'. Aborting.`);
226
+ return null; // Return null to signal failure
227
+ }
228
+ const pnlScores = pnlTrackerResult.daily_pnl_map;
229
+ logger.log('INFO', `[UserInvestmentProfile] Received ${Object.keys(pnlScores).length} PNL scores in-memory.`);
302
230
 
303
- /**
304
- * GETRESULT: Aggregate into rolling 90-day history, compute avg components and final IS.
305
- */
306
- async getResult() {
307
- // --- START MODIFICATION ---
308
- // If dependencies failed, return null to trigger backfill.
309
- if (!this.dependencyLoadedSuccess) {
310
- // Logger might not be available here, use console.warn
311
- console.warn('[UserInvestmentProfile] Skipping getResult as dependency (pnl-tracker) failed.');
231
+ // 2. Load external dependencies (prices, sectors)
232
+ const [priceMap, sectorMap] = await Promise.all([
233
+ loadAllPriceData(),
234
+ getInstrumentSectorMap()
235
+ ]);
236
+ if (!priceMap || !sectorMap) {
237
+ logger.log('ERROR', '[UserInvestmentProfile] Failed to load priceMap or sectorMap.');
312
238
  return null;
313
239
  }
314
240
 
315
- // If no users were processed (e.g., all were filtered out), return null.
316
- if (Object.keys(this.dailyUserScores).length === 0) {
317
- console.warn('[UserInvestmentProfile] No daily user scores were calculated. Returning null for backfill.');
318
- return null;
241
+ // 3. Load "yesterday's" portfolio data for comparison
242
+ const yesterdayDate = new Date(dateStr + 'T00:00:00Z');
243
+ yesterdayDate.setUTCDate(yesterdayDate.getUTCDate() - 1);
244
+ const yesterdayStr = yesterdayDate.toISOString().slice(0, 10);
245
+ const yesterdayRefs = await calculationUtils.getPortfolioPartRefs(config, dependencies, yesterdayStr);
246
+ const yesterdayPortfolios = await loadFullDayMap(config, dependencies, yesterdayRefs);
247
+ logger.log('INFO', `[UserInvestmentProfile] Loaded ${yesterdayRefs.length} part refs for yesterday.`);
248
+
249
+ // 4. Stream "today's" portfolio data and process
250
+ const batchSize = config.partRefBatchSize || 10;
251
+ const dailyUserScores = {}; // Local state for this run
252
+
253
+ for (let i = 0; i < portfolioRefs.length; i += batchSize) {
254
+ const batchRefs = portfolioRefs.slice(i, i + batchSize);
255
+ const todayPortfoliosChunk = await loadDataByRefs(config, dependencies, batchRefs);
256
+
257
+ for (const uid in todayPortfoliosChunk) {
258
+ const pToday = todayPortfoliosChunk[uid];
259
+ if (!pToday || !pToday.AggregatedPositions) continue; // Skip speculators or empty
260
+
261
+ const pYesterday = yesterdayPortfolios[uid] || {};
262
+
263
+ // Run the heuristic calculations
264
+ const score_rd = this._calculateRiskAndDivScore(pToday, sectorMap);
265
+ const score_disc = this._calculateDisciplineScore(pYesterday, pToday);
266
+ const score_time = this._calculateMarketTimingScore(pYesterday, pToday, priceMap);
267
+
268
+ dailyUserScores[uid] = {
269
+ score_rd,
270
+ score_disc,
271
+ score_time
272
+ };
273
+ }
319
274
  }
320
- // --- END MODIFICATION ---
321
-
322
- const todayStr = this.dates.today || (new Date()).toISOString().slice(0, 10);
275
+ logger.log('INFO', `[UserInvestmentProfile] Calculated daily scores for ${Object.keys(dailyUserScores).length} users.`);
276
+
277
+ // --- 5. GETRESULT LOGIC IS NOW INSIDE PROCESS ---
278
+ // (This is the logic from your original getResult())
323
279
 
324
- // prepare sharded output objects with profiles container (Option A)
325
280
  const shardedResults = {};
326
281
  for (let i = 0; i < NUM_SHARDS; i++) {
327
282
  const shardKey = `${SHARD_COLLECTION_NAME}_shard_${i}`;
328
- shardedResults[shardKey] = { profiles: {}, lastUpdated: todayStr };
283
+ shardedResults[shardKey] = { profiles: {}, lastUpdated: dateStr };
329
284
  }
330
285
 
331
286
  const dailyInvestorScoreMap = {};
332
287
 
333
- // fetch existing shards in parallel
288
+ // Fetch existing shards in parallel
334
289
  const shardPromises = [];
335
290
  for (let i = 0; i < NUM_SHARDS; i++) {
336
291
  const docRef = firestore.collection(SHARD_COLLECTION_NAME).doc(`${SHARD_COLLECTION_NAME}_shard_${i}`);
337
292
  shardPromises.push(docRef.get());
338
293
  }
339
294
  const shardSnapshots = await Promise.all(shardPromises);
340
-
341
- // Build existingShards map of profiles for quick access
342
- const existingShards = shardSnapshots.map((snap, idx) => {
343
- if (!snap.exists) return {}; // no profiles
344
- const data = snap.data() || {};
345
- return data.profiles || {};
346
- });
347
-
348
- // process users
349
- for (const userId of Object.keys(this.dailyUserScores)) {
295
+ const existingShards = shardSnapshots.map((snap) => (snap.exists ? snap.data().profiles : {}));
296
+
297
+ // Process users
298
+ for (const userId of Object.keys(dailyUserScores)) {
350
299
  const shardIndex = getShardIndex(userId);
351
- const scores = this.dailyUserScores[userId];
300
+ const scores = dailyUserScores[userId];
352
301
 
353
- // fetch existing history for this user (if present)
354
302
  const existingProfiles = existingShards[shardIndex] || {};
355
- // clone to avoid mutating snapshot data directly
356
- const history = (existingProfiles[userId] || []).slice();
303
+ const history = (existingProfiles[userId] || []).slice(); // clone
357
304
 
358
305
  history.push({
359
- date: todayStr,
306
+ date: dateStr,
360
307
  ...scores,
361
- pnl: (this.pnlScores && (userId in this.pnlScores)) ? this.pnlScores[userId] : 0
308
+ pnl: (pnlScores[userId] || 0)
362
309
  });
363
310
 
364
311
  const newHistory = history.slice(-ROLLING_DAYS);
@@ -377,36 +324,35 @@ class UserInvestmentProfile {
377
324
  avg_time /= N;
378
325
  avg_pnl /= N;
379
326
 
380
- // Normalize PNL: avg_pnl is decimal percent (0.005 -> 0.5%). Map to 0-10 scale:
381
- // multiply by 1000 (0.005 -> 5). Clamp to [-10, 10] to avoid outliers.
382
327
  const normalizedPnl = Math.max(-10, Math.min(10, avg_pnl * 1000));
383
-
384
- // Final IS (weights): discipline 40%, risk/div 30%, timing 20%, pnl 10%
385
328
  const finalISRaw = (avg_disc * 0.4) + (avg_rd * 0.3) + (avg_time * 0.2) + (normalizedPnl * 0.1);
386
329
  const finalIS = Math.max(0, Math.min(10, finalISRaw));
387
330
 
388
- // store in prepared shard result under 'profiles'
389
331
  const shardKey = `${SHARD_COLLECTION_NAME}_shard_${shardIndex}`;
390
332
  shardedResults[shardKey].profiles[userId] = newHistory;
391
-
392
- // also set the daily investor score
393
333
  dailyInvestorScoreMap[userId] = finalIS;
394
334
  }
395
-
335
+
336
+ logger.log('INFO', `[UserInvestmentProfile] Finalized IS scores for ${Object.keys(dailyInvestorScoreMap).length} users.`);
337
+
338
+ // Return the final result object
396
339
  return {
397
340
  sharded_user_profile: shardedResults,
398
341
  daily_investor_scores: dailyInvestorScoreMap
399
342
  };
400
343
  }
401
344
 
345
+ /**
346
+ * getResult is no longer used by the meta-runner.
347
+ */
348
+ async getResult() {
349
+ return null;
350
+ }
351
+
352
+ /**
353
+ * reset is no longer used by the meta-runner.
354
+ */
402
355
  reset() {
403
- this.dailyUserScores = {};
404
- this.dependenciesLoaded = false;
405
- this.priceMap = null;
406
- this.sectorMap = null;
407
- this.pnlScores = null;
408
- this.dates = {};
409
- this.dependencyLoadedSuccess = false; // <-- MODIFICATION
410
356
  }
411
357
  }
412
358
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aiden-shared-calculations-unified",
3
- "version": "1.0.36",
3
+ "version": "1.0.37",
4
4
  "description": "Shared calculation modules for the BullTrackers Computation System.",
5
5
  "main": "index.js",
6
6
  "files": [