aiden-shared-calculations-unified 1.0.32 → 1.0.34

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.
@@ -121,22 +121,8 @@ class AssetCrowdFlow {
121
121
  const avg_day1_value = this.asset_values[rawInstrumentId].day1_value_sum / this.user_count;
122
122
  const avg_day2_value = this.asset_values[rawInstrumentId].day2_value_sum / this.user_count;
123
123
 
124
- let priceChangePct = null;
125
-
126
- // Check priceMap presence
127
- if (!this.priceMap || !this.priceMap[instrumentId]) {
128
- console.debug(`[AssetCrowdFlow] Missing priceMap entry for instrumentId ${instrumentId} (${ticker})`);
129
- } else {
130
- const priceDay1 = this.priceMap[instrumentId][yesterdayStr];
131
- const priceDay2 = this.priceMap[instrumentId][todayStr];
132
-
133
- if (priceDay1 == null) console.debug(`[AssetCrowdFlow] Missing price for ${instrumentId} (${ticker}) on ${yesterdayStr}`);
134
- if (priceDay2 == null) console.debug(`[AssetCrowdFlow] Missing price for ${instrumentId} (${ticker}) on ${todayStr}`);
135
-
136
- if (priceDay1 != null && priceDay2 != null && priceDay1 > 0) {
137
- priceChangePct = (priceDay2 - priceDay1) / priceDay1;
138
- }
139
- }
124
+ // FIX to detect weekends and skip them
125
+ const priceChangePct = getDailyPriceChange(instrumentId, yesterdayStr, todayStr, this.priceMap);
140
126
 
141
127
  if (priceChangePct === null) {
142
128
  // <--- MODIFICATION: We no longer add a warning object. We just skip it.
@@ -21,7 +21,10 @@ class DumbCohortFlow {
21
21
  this.todaySectorInvestment = {};
22
22
  this.yesterdaySectorInvestment = {};
23
23
 
24
- this.dumbCohortIds = null; // Set of user IDs
24
+ // --- START MODIFICATION ---
25
+ this.dumbCohortIds = null; // Set to null. Will be a Set on success.
26
+ // --- END MODIFICATION ---
27
+
25
28
  this.user_count = 0; // Number of *cohort* users
26
29
  this.priceMap = null;
27
30
  this.mappings = null;
@@ -42,11 +45,15 @@ class DumbCohortFlow {
42
45
  .collection(context.config.computationsSubcollection).doc(PROFILE_CALC_ID);
43
46
 
44
47
  const doc = await scoreMapRef.get();
45
- if (!doc.exists || !doc.data().daily_investor_scores) {
46
- logger.log('WARN', '[DumbCohortFlow] Cannot find dependency: daily_investor_scores. Cohort will be empty.');
47
- this.dumbCohortIds = new Set();
48
- return;
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
49
55
  }
56
+ // --- END MODIFICATION ---
50
57
 
51
58
  const scores = doc.data().daily_investor_scores;
52
59
  const allScores = Object.entries(scores).map(([userId, score]) => ({ userId, score }));
@@ -55,15 +62,18 @@ class DumbCohortFlow {
55
62
  const thresholdIndex = Math.floor(allScores.length * COHORT_PERCENTILE);
56
63
  const thresholdScore = allScores[thresholdIndex]?.score || 0; // Get 20th percentile score
57
64
 
65
+ // --- START MODIFICATION ---
66
+ // Successfully loaded, now create the Set
58
67
  this.dumbCohortIds = new Set(
59
68
  allScores.filter(s => s.score <= thresholdScore).map(s => s.userId) // Get users *at or below*
60
69
  );
70
+ // --- END MODIFICATION ---
61
71
 
62
72
  logger.log('INFO', `[DumbCohortFlow] Cohort built. ${this.dumbCohortIds.size} users at or below ${thresholdScore.toFixed(2)} (20th percentile).`);
63
73
 
64
74
  } catch (e) {
65
75
  logger.log('ERROR', '[DumbCohortFlow] Failed to load cohort.', { error: e.message });
66
- this.dumbCohortIds = new Set();
76
+ // Keep this.dumbCohortIds = null on error
67
77
  }
68
78
  }
69
79
 
@@ -105,9 +115,12 @@ class DumbCohortFlow {
105
115
  }
106
116
 
107
117
  // 2. Filter user
108
- if (!this.dumbCohortIds.has(userId) || !todayPortfolio || !yesterdayPortfolio || !todayPortfolio.AggregatedPositions || !yesterdayPortfolio.AggregatedPositions) {
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) {
109
121
  return;
110
122
  }
123
+ // --- END MODIFICATION ---
111
124
 
112
125
  // 3. User is in the cohort, load maps if needed
113
126
  if (!this.sectorMap) {
@@ -136,18 +149,41 @@ class DumbCohortFlow {
136
149
  * GETRESULT: Aggregates and returns the flow data for the cohort.
137
150
  */
138
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;
157
+ }
158
+
159
+ // If cohort loaded but no users were processed, also return null (or an empty object, but null is safer for backfill)
139
160
  if (this.user_count === 0 || !this.dates.today) {
140
- return { asset_flow: {}, sector_rotation: {}, user_sample_size: 0 };
161
+ console.warn('[DumbCohortFlow] No users processed for dumb cohort. Returning null.');
162
+ return null;
141
163
  }
164
+ // --- END MODIFICATION ---
142
165
 
143
166
  // 1. Load dependencies
144
167
  if (!this.priceMap || !this.mappings) {
145
- const [priceData, mappingData] = await Promise.all([
146
- loadAllPriceData(),
147
- loadInstrumentMappings()
148
- ]);
149
- this.priceMap = priceData;
150
- this.mappings = mappingData;
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 ---
151
187
  }
152
188
 
153
189
  // --- 2. Calculate Asset Flow ---
@@ -161,7 +197,7 @@ class DumbCohortFlow {
161
197
  const avg_day2_value = this.asset_values[instrumentId].day2_value_sum / this.user_count;
162
198
  const priceChangePct = getDailyPriceChange(instrumentId, yesterdayStr, todayStr, this.priceMap);
163
199
 
164
- if (priceChangePct === null) continue;
200
+ if (priceChangePct === null) continue; // Skip if price data missing
165
201
 
166
202
  const expected_day2_value = avg_day1_value * (1 + priceChangePct);
167
203
  const net_crowd_flow_pct = avg_day2_value - expected_day2_value;
@@ -181,6 +217,14 @@ class DumbCohortFlow {
181
217
  const yesterdayAmount = this.yesterdaySectorInvestment[sector] || 0;
182
218
  finalSectorRotation[sector] = todayAmount - yesterdayAmount;
183
219
  }
220
+
221
+ // --- START MODIFICATION ---
222
+ // If no asset flow was calculated (e.g., all price data missing), fail
223
+ if (Object.keys(finalAssetFlow).length === 0) {
224
+ console.warn('[DumbCohortFlow] No asset flow calculated (likely all price data missing). Returning null.');
225
+ return null;
226
+ }
227
+ // --- END MODIFICATION ---
184
228
 
185
229
  // 4. Return combined result
186
230
  return {
@@ -190,7 +234,17 @@ class DumbCohortFlow {
190
234
  };
191
235
  }
192
236
 
193
- reset() { /* ... reset all constructor properties ... */ }
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
+ }
194
248
  }
195
249
 
196
250
  module.exports = DumbCohortFlow;
@@ -21,7 +21,10 @@ class SmartCohortFlow {
21
21
  this.todaySectorInvestment = {};
22
22
  this.yesterdaySectorInvestment = {};
23
23
 
24
- this.smartCohortIds = null; // Set of user IDs
24
+ // --- START MODIFICATION ---
25
+ this.smartCohortIds = null; // Set to null. Will be a Set on success.
26
+ // --- END MODIFICATION ---
27
+
25
28
  this.user_count = 0; // Number of *cohort* users
26
29
  this.priceMap = null;
27
30
  this.mappings = null;
@@ -42,11 +45,15 @@ class SmartCohortFlow {
42
45
  .collection(context.config.computationsSubcollection).doc(PROFILE_CALC_ID);
43
46
 
44
47
  const doc = await scoreMapRef.get();
45
- if (!doc.exists || !doc.data().daily_investor_scores) {
46
- logger.log('WARN', '[SmartCohortFlow] Cannot find dependency: daily_investor_scores. Cohort will be empty.');
47
- this.smartCohortIds = new Set();
48
- return;
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
49
55
  }
56
+ // --- END MODIFICATION ---
50
57
 
51
58
  const scores = doc.data().daily_investor_scores;
52
59
  const allScores = Object.entries(scores).map(([userId, score]) => ({ userId, score }));
@@ -55,15 +62,18 @@ class SmartCohortFlow {
55
62
  const thresholdIndex = Math.floor(allScores.length * COHORT_PERCENTILE);
56
63
  const thresholdScore = allScores[thresholdIndex]?.score || 999;
57
64
 
65
+ // --- START MODIFICATION ---
66
+ // Successfully loaded, now create the Set
58
67
  this.smartCohortIds = new Set(
59
68
  allScores.filter(s => s.score >= thresholdScore).map(s => s.userId)
60
69
  );
70
+ // --- END MODIFICATION ---
61
71
 
62
72
  logger.log('INFO', `[SmartCohortFlow] Cohort built. ${this.smartCohortIds.size} users at or above ${thresholdScore.toFixed(2)} (80th percentile).`);
63
73
 
64
74
  } catch (e) {
65
75
  logger.log('ERROR', '[SmartCohortFlow] Failed to load cohort.', { error: e.message });
66
- this.smartCohortIds = new Set();
76
+ // Keep this.smartCohortIds = null on error
67
77
  }
68
78
  }
69
79
 
@@ -105,9 +115,12 @@ class SmartCohortFlow {
105
115
  }
106
116
 
107
117
  // 2. Filter user
108
- if (!this.smartCohortIds.has(userId) || !todayPortfolio || !yesterdayPortfolio || !todayPortfolio.AggregatedPositions || !yesterdayPortfolio.AggregatedPositions) {
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) {
109
121
  return;
110
122
  }
123
+ // --- END MODIFICATION ---
111
124
 
112
125
  // 3. User is in the cohort, load maps if needed
113
126
  if (!this.sectorMap) {
@@ -136,18 +149,42 @@ class SmartCohortFlow {
136
149
  * GETRESULT: Aggregates and returns the flow data for the cohort.
137
150
  */
138
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;
157
+ }
158
+
159
+ // If cohort loaded but no users were processed, also return null (or an empty object, but null is safer for backfill)
139
160
  if (this.user_count === 0 || !this.dates.today) {
140
- return { asset_flow: {}, sector_rotation: {}, user_sample_size: 0 };
161
+ console.warn('[SmartCohortFlow] No users processed for smart cohort. Returning null.');
162
+ return null;
141
163
  }
164
+ // --- END MODIFICATION ---
165
+
142
166
 
143
167
  // 1. Load dependencies
144
168
  if (!this.priceMap || !this.mappings) {
145
- const [priceData, mappingData] = await Promise.all([
146
- loadAllPriceData(),
147
- loadInstrumentMappings()
148
- ]);
149
- this.priceMap = priceData;
150
- this.mappings = mappingData;
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 ---
151
188
  }
152
189
 
153
190
  // --- 2. Calculate Asset Flow ---
@@ -182,6 +219,14 @@ class SmartCohortFlow {
182
219
  finalSectorRotation[sector] = todayAmount - yesterdayAmount; // Note: This is total $, not avg.
183
220
  }
184
221
 
222
+ // --- START MODIFICATION ---
223
+ // If no asset flow was calculated (e.g., all price data missing), fail
224
+ if (Object.keys(finalAssetFlow).length === 0) {
225
+ console.warn('[SmartCohortFlow] No asset flow calculated (likely all price data missing). Returning null.');
226
+ return null;
227
+ }
228
+ // --- END MODIFICATION ---
229
+
185
230
  // 4. Return combined result
186
231
  return {
187
232
  asset_flow: finalAssetFlow,
@@ -190,7 +235,17 @@ class SmartCohortFlow {
190
235
  };
191
236
  }
192
237
 
193
- reset() { /* ... reset all constructor properties ... */ }
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
+ }
194
249
  }
195
250
 
196
251
  module.exports = SmartCohortFlow;
@@ -1,13 +1,13 @@
1
1
  /**
2
2
  * @fileoverview Calculates a rolling 90-day "Investor Score" (IS) for each normal user.
3
3
  * Heuristic engine (not an academic finance model). Outputs:
4
- * - sharded_user_profile: { <shardKey>: { profiles: { userId: [history...] }, lastUpdated } }
5
- * - daily_investor_scores: { userId: finalIS }
4
+ * - sharded_user_profile: { <shardKey>: { profiles: { userId: [history...] }, lastUpdated } }
5
+ * - daily_investor_scores: { userId: finalIS }
6
6
  *
7
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.
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.
11
11
  */
12
12
 
13
13
  const { Firestore } = require('@google-cloud/firestore');
@@ -45,6 +45,11 @@ class UserInvestmentProfile {
45
45
  this.pnlScores = null; // { userId: dailyPnlDecimal }
46
46
  this.dates = {};
47
47
  this.dependenciesLoaded = false;
48
+
49
+ // --- START MODIFICATION ---
50
+ // Flag to track if dependencies loaded successfully
51
+ this.dependencyLoadedSuccess = false;
52
+ // --- END MODIFICATION ---
48
53
  }
49
54
 
50
55
  /**
@@ -74,27 +79,32 @@ class UserInvestmentProfile {
74
79
  .collection(context.config.computationsSubcollection).doc(PNL_TRACKER_CALC_ID);
75
80
 
76
81
  const pnlSnap = await pnlCalcRef.get();
77
- if (pnlSnap.exists) {
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) {
78
86
  this.pnlScores = pnlSnap.data().daily_pnl_map || {};
79
87
  if (logger) logger.log('INFO', `[UserInvestmentProfile] Loaded ${Object.keys(this.pnlScores).length} PNL scores.`);
88
+ this.dependencyLoadedSuccess = true; // Set success flag
80
89
  } else {
81
- if (logger) logger.log('WARN', `[UserInvestmentProfile] Could not find PNL scores dependency for ${todayDateStr}. PNL score will be 0.`);
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
82
92
  }
93
+ // --- END MODIFICATION ---
94
+
83
95
  } catch (e) {
84
96
  if (logger) logger.log('ERROR', `[UserInvestmentProfile] Failed to load PNL scores.`, { error: e.message });
97
+ this.dependencyLoadedSuccess = false; // Set failure flag on error
85
98
  }
86
99
 
87
100
  this.dependenciesLoaded = true;
88
101
  if (logger) logger.log('INFO', '[UserInvestmentProfile] All dependencies loaded.');
89
102
  }
90
103
 
104
+ // ... [Heuristic calculation functions _calculateRiskAndDivScore, _calculateDisciplineScore, _calculateMarketTimingScore are unchanged] ...
105
+
91
106
  /**
92
107
  * HEURISTIC 1: Risk & Diversification Score (0-10).
93
- *
94
- * Implementation notes:
95
- * - NetProfit is assumed to be a percent return in decimal per position (e.g. 0.03 = +3%).
96
- * - We compute a weighted mean/std of returns across positions (weights = invested amounts).
97
- * This gives a cross-sectional dispersion proxy (not a time-series Sharpe).
98
108
  */
99
109
  _calculateRiskAndDivScore(todayPortfolio) {
100
110
  if (!todayPortfolio.AggregatedPositions || todayPortfolio.AggregatedPositions.length === 0) {
@@ -158,9 +168,6 @@ class UserInvestmentProfile {
158
168
 
159
169
  /**
160
170
  * HEURISTIC 2: Discipline Score (0-10).
161
- *
162
- * Uses yesterday's positions to evaluate closes, averaging down, holding losers/winners.
163
- * Defensive: uses safe field fallbacks and guards against division by zero.
164
171
  */
165
172
  _calculateDisciplineScore(yesterdayPortfolio = {}, todayPortfolio = {}) {
166
173
  const yPositions = yesterdayPortfolio.AggregatedPositions || [];
@@ -207,9 +214,6 @@ class UserInvestmentProfile {
207
214
 
208
215
  /**
209
216
  * HEURISTIC 3: Market Timing Score (0-10).
210
- *
211
- * For new positions opened today (not present yesterday), measure proximity of openRate to
212
- * the last 30-day low/high. Uses date-sorted price history and clamps.
213
217
  */
214
218
  _calculateMarketTimingScore(yesterdayPortfolio = {}, todayPortfolio = {}) {
215
219
  const yIds = new Set((yesterdayPortfolio.AggregatedPositions || []).map(p => p.PositionID));
@@ -263,7 +267,7 @@ class UserInvestmentProfile {
263
267
  const avg = (timingCount > 0) ? (timingPoints / timingCount) : 5;
264
268
  return Math.max(0, Math.min(10, avg));
265
269
  }
266
-
270
+
267
271
  /**
268
272
  * PROCESS: called per-user per-day to compute and store today's heuristics.
269
273
  */
@@ -275,6 +279,13 @@ class UserInvestmentProfile {
275
279
  await this._loadDependencies(context, context.dependencies);
276
280
  this.dates.today = context.todayDateStr;
277
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 ---
278
289
 
279
290
  const yPort = yesterdayPortfolio || {};
280
291
 
@@ -291,14 +302,22 @@ class UserInvestmentProfile {
291
302
 
292
303
  /**
293
304
  * GETRESULT: Aggregate into rolling 90-day history, compute avg components and final IS.
294
- *
295
- * Returns a structure prepared for writing where each shardKey maps to:
296
- * { profiles: { userId: historyArray, ... }, lastUpdated: todayStr }
297
- *
298
- * This must match how existing shards are read (snap.data().profiles).
299
305
  */
300
306
  async getResult() {
301
- if (Object.keys(this.dailyUserScores).length === 0) return {};
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.');
312
+ return null;
313
+ }
314
+
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;
319
+ }
320
+ // --- END MODIFICATION ---
302
321
 
303
322
  const todayStr = this.dates.today || (new Date()).toISOString().slice(0, 10);
304
323
 
@@ -387,7 +406,8 @@ class UserInvestmentProfile {
387
406
  this.sectorMap = null;
388
407
  this.pnlScores = null;
389
408
  this.dates = {};
409
+ this.dependencyLoadedSuccess = false; // <-- MODIFICATION
390
410
  }
391
411
  }
392
412
 
393
- module.exports = UserInvestmentProfile;
413
+ module.exports = UserInvestmentProfile;
@@ -43,6 +43,8 @@ class SmartDumbDivergenceIndex {
43
43
  const dumbData = snapshots[1].exists ? snapshots[1].data() : null;
44
44
 
45
45
  // 3. Handle "day-delay"
46
+ // --- START MODIFICATION ---
47
+ // This check now catches if the dependency calcs returned null (and thus docs don't exist)
46
48
  if (!smartData || !dumbData) {
47
49
  logger.log('WARN', `[SmartDumbDivergence] Missing cohort flow data for ${dateStr}. Allowing backfill.`);
48
50
  return null; // Let backfill handle it
@@ -53,10 +55,19 @@ class SmartDumbDivergenceIndex {
53
55
  sectors: {}
54
56
  };
55
57
 
56
- const smartAssetFlow = smartData.asset_flow || {};
57
- const dumbAssetFlow = dumbData.asset_flow || {};
58
- const smartSectorFlow = smartData.sector_rotation || {};
59
- const dumbSectorFlow = dumbData.sector_rotation || {};
58
+ // Check for the asset_flow key specifically.
59
+ const smartAssetFlow = smartData.asset_flow;
60
+ const dumbAssetFlow = dumbData.asset_flow;
61
+ const smartSectorFlow = smartData.sector_rotation;
62
+ const dumbSectorFlow = dumbData.sector_rotation;
63
+
64
+ // If the docs exist but the data *inside* is missing (e.g., from an old, bad run), return null.
65
+ if (!smartAssetFlow || !dumbAssetFlow || !smartSectorFlow || !dumbSectorFlow) {
66
+ logger.log('WARN', `[SmartDumbDivergence] Dependency data for ${dateStr} is incomplete (missing asset_flow or sector_rotation). Allowing backfill.`);
67
+ return null;
68
+ }
69
+ // --- END MODIFICATION ---
70
+
60
71
 
61
72
  // 4. Correlate Assets
62
73
  const allTickers = new Set([...Object.keys(smartAssetFlow), ...Object.keys(dumbAssetFlow)]);
@@ -53,6 +53,15 @@ class UserProfitabilityTracker {
53
53
  }
54
54
 
55
55
  async getResult() {
56
+ // --- START MODIFICATION ---
57
+ // If no data was processed (e.g., no portfolios found for the day),
58
+ // return null to allow the backfill to retry.
59
+ if (Object.keys(this.dailyData).length === 0) {
60
+ console.warn('[UserProfitabilityTracker] No daily data was processed. Returning null for backfill.');
61
+ return null;
62
+ }
63
+ // --- END MODIFICATION ---
64
+
56
65
  const today = new Date().toISOString().slice(0, 10);
57
66
  const results = {}; // For sharded history
58
67
  const dailyPnlMap = {}; // For the new profile calc
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aiden-shared-calculations-unified",
3
- "version": "1.0.32",
3
+ "version": "1.0.34",
4
4
  "description": "Shared calculation modules for the BullTrackers Computation System.",
5
5
  "main": "index.js",
6
6
  "files": [