aiden-shared-calculations-unified 1.0.36 → 1.0.38
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/calculations/behavioural/historical/dumb-cohort-flow.js +130 -162
- package/calculations/behavioural/historical/smart-cohort-flow.js +129 -162
- package/calculations/behavioural/historical/user-investment-profile.js +103 -157
- package/calculations/capital_flow/historical/reallocation_increase_percentage.js +63 -0
- package/calculations/speculators/stop_loss_distance_by_sector_short_long_breakdown.js +91 -0
- package/calculations/speculators/stop_loss_distance_by_ticker_short_long_breakdown.js +73 -0
- package/package.json +1 -1
|
@@ -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
|
-
*
|
|
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
|
-
//
|
|
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
|
-
|
|
39
|
-
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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 (!
|
|
83
|
-
|
|
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 =
|
|
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:
|
|
81
|
+
* PROCESS: META REFACTOR
|
|
82
|
+
* This now runs ONCE, loads all data, streams users, and returns one big result.
|
|
108
83
|
*/
|
|
109
|
-
async process(
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
//
|
|
126
|
-
|
|
127
|
-
|
|
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
|
-
//
|
|
131
|
-
const
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
-
//
|
|
142
|
-
|
|
143
|
-
|
|
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
|
-
|
|
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
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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
|
-
//
|
|
172
|
+
// 5a. Calculate Asset Flow
|
|
190
173
|
const finalAssetFlow = {};
|
|
191
|
-
const
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
const
|
|
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;
|
|
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
|
-
//
|
|
192
|
+
// 5b. Calculate Sector Rotation
|
|
213
193
|
const finalSectorRotation = {};
|
|
214
|
-
const allSectors = new Set([...Object.keys(
|
|
194
|
+
const allSectors = new Set([...Object.keys(todaySectorInvestment), ...Object.keys(yesterdaySectorInvestment)]);
|
|
215
195
|
for (const sector of allSectors) {
|
|
216
|
-
const todayAmount =
|
|
217
|
-
const yesterdayAmount =
|
|
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
|
-
|
|
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
|
-
//
|
|
206
|
+
// 6. Return combined result
|
|
230
207
|
return {
|
|
231
208
|
asset_flow: finalAssetFlow,
|
|
232
209
|
sector_rotation: finalSectorRotation,
|
|
233
|
-
user_sample_size:
|
|
210
|
+
user_sample_size: user_count
|
|
234
211
|
};
|
|
235
212
|
}
|
|
236
213
|
|
|
237
|
-
|
|
238
|
-
|
|
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;
|