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.
- package/calculations/behavioural/historical/asset_crowd_flow.js +2 -16
- package/calculations/behavioural/historical/dumb-cohort-flow.js +70 -16
- package/calculations/behavioural/historical/smart-cohort-flow.js +70 -15
- package/calculations/behavioural/historical/user-investment-profile.js +46 -26
- package/calculations/meta/smart-dumb-divergence-index.js +15 -4
- package/calculations/pnl/historical/user_profitability_tracker.js +9 -0
- package/package.json +1 -1
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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() {
|
|
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
|
-
|
|
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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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() {
|
|
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
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* - sharded_user_profile: { <shardKey>: { profiles: { userId: [history...] }, lastUpdated } }
|
|
5
|
+
* - daily_investor_scores: { userId: finalIS }
|
|
6
6
|
*
|
|
7
7
|
* Notes:
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
57
|
-
const
|
|
58
|
-
const
|
|
59
|
-
const
|
|
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
|