aiden-shared-calculations-unified 1.0.35 → 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.
Files changed (58) hide show
  1. package/README.MD +77 -77
  2. package/calculations/activity/historical/activity_by_pnl_status.js +85 -85
  3. package/calculations/activity/historical/daily_asset_activity.js +85 -85
  4. package/calculations/activity/historical/daily_user_activity_tracker.js +144 -144
  5. package/calculations/activity/historical/speculator_adjustment_activity.js +76 -76
  6. package/calculations/asset_metrics/asset_position_size.js +57 -57
  7. package/calculations/backtests/strategy-performance.js +229 -245
  8. package/calculations/behavioural/historical/asset_crowd_flow.js +165 -165
  9. package/calculations/behavioural/historical/drawdown_response.js +58 -58
  10. package/calculations/behavioural/historical/dumb-cohort-flow.js +217 -249
  11. package/calculations/behavioural/historical/gain_response.js +57 -57
  12. package/calculations/behavioural/historical/in_loss_asset_crowd_flow.js +98 -98
  13. package/calculations/behavioural/historical/in_profit_asset_crowd_flow.js +99 -99
  14. package/calculations/behavioural/historical/paper_vs_diamond_hands.js +39 -39
  15. package/calculations/behavioural/historical/position_count_pnl.js +67 -67
  16. package/calculations/behavioural/historical/smart-cohort-flow.js +217 -250
  17. package/calculations/behavioural/historical/smart_money_flow.js +165 -165
  18. package/calculations/behavioural/historical/user-investment-profile.js +358 -412
  19. package/calculations/capital_flow/historical/crowd-cash-flow-proxy.js +121 -121
  20. package/calculations/capital_flow/historical/deposit_withdrawal_percentage.js +117 -117
  21. package/calculations/capital_flow/historical/new_allocation_percentage.js +49 -49
  22. package/calculations/insights/daily_bought_vs_sold_count.js +55 -55
  23. package/calculations/insights/daily_buy_sell_sentiment_count.js +49 -49
  24. package/calculations/insights/daily_ownership_delta.js +55 -55
  25. package/calculations/insights/daily_total_positions_held.js +39 -39
  26. package/calculations/meta/capital_deployment_strategy.js +129 -137
  27. package/calculations/meta/capital_liquidation_performance.js +121 -163
  28. package/calculations/meta/capital_vintage_performance.js +121 -158
  29. package/calculations/meta/cash-flow-deployment.js +110 -124
  30. package/calculations/meta/cash-flow-liquidation.js +126 -142
  31. package/calculations/meta/crowd_sharpe_ratio_proxy.js +83 -91
  32. package/calculations/meta/profit_cohort_divergence.js +77 -91
  33. package/calculations/meta/smart-dumb-divergence-index.js +116 -138
  34. package/calculations/meta/social_flow_correlation.js +99 -125
  35. package/calculations/pnl/asset_pnl_status.js +46 -46
  36. package/calculations/pnl/historical/profitability_migration.js +57 -57
  37. package/calculations/pnl/historical/user_profitability_tracker.js +117 -117
  38. package/calculations/pnl/profitable_and_unprofitable_status.js +64 -64
  39. package/calculations/sectors/historical/diversification_pnl.js +76 -76
  40. package/calculations/sectors/historical/sector_rotation.js +67 -67
  41. package/calculations/sentiment/historical/crowd_conviction_score.js +80 -80
  42. package/calculations/socialPosts/social-asset-posts-trend.js +52 -52
  43. package/calculations/socialPosts/social-top-mentioned-words.js +102 -102
  44. package/calculations/socialPosts/social-topic-interest-evolution.js +53 -53
  45. package/calculations/socialPosts/social-word-mentions-trend.js +62 -62
  46. package/calculations/socialPosts/social_activity_aggregation.js +103 -103
  47. package/calculations/socialPosts/social_event_correlation.js +121 -121
  48. package/calculations/socialPosts/social_sentiment_aggregation.js +114 -114
  49. package/calculations/speculators/historical/risk_appetite_change.js +54 -54
  50. package/calculations/speculators/historical/tsl_effectiveness.js +74 -74
  51. package/index.js +33 -33
  52. package/package.json +32 -32
  53. package/utils/firestore_utils.js +76 -76
  54. package/utils/price_data_provider.js +142 -142
  55. package/utils/sector_mapping_provider.js +74 -74
  56. package/calculations/capital_flow/historical/reallocation_increase_percentage.js +0 -63
  57. package/calculations/speculators/stop_loss_distance_by_sector_short_long_breakdown.js +0 -91
  58. package/calculations/speculators/stop_loss_distance_by_ticker_short_long_breakdown.js +0 -73
@@ -1,251 +1,218 @@
1
- /**
2
- * @fileoverview Calculates "Net Crowd Flow" and "Sector Rotation"
3
- * *only* for the "Smart Cohort" (Top 20% of Investor Scores).
4
- *
5
- * This calc depends on 'user-investment-profile.js' being run first for the same day.
6
- */
7
-
8
- const { Firestore } = require('@google-cloud/firestore');
9
- const firestore = new Firestore();
10
- const { loadAllPriceData, getDailyPriceChange } = require('../../../utils/price_data_provider');
11
- const { loadInstrumentMappings, getInstrumentSectorMap } = require('../../../utils/sector_mapping_provider');
12
-
13
- const COHORT_PERCENTILE = 0.8; // Top 20%
14
- const PROFILE_CALC_ID = 'user-investment-profile'; // The calc to read IS scores from
15
-
16
- class SmartCohortFlow {
17
- 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 = {};
33
- }
34
-
35
- /**
36
- * Loads the Investor Scores, calculates the cohort threshold, and builds the Set of user IDs.
37
- */
38
- async _loadCohort(context, dependencies) {
39
- const { db, logger } = dependencies;
40
- logger.log('INFO', '[SmartCohortFlow] Loading Investor Scores to build cohort...');
41
-
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
77
- }
78
- }
79
-
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 };
84
- }
85
- }
86
- _sumAssetValue(positions) {
87
- const valueMap = {};
88
- if (!positions || !Array.isArray(positions)) return valueMap;
89
- for (const pos of positions) {
90
- if (pos && pos.InstrumentID && pos.Value) {
91
- valueMap[pos.InstrumentID] = (valueMap[pos.InstrumentID] || 0) + pos.Value;
92
- }
93
- }
94
- return valueMap;
95
- }
96
- // --- Sector Rotation Helper ---
97
- _accumulateSectorInvestment(portfolio, target) {
98
- if (portfolio && portfolio.AggregatedPositions) {
99
- for (const pos of portfolio.AggregatedPositions) {
100
- const sector = this.sectorMap[pos.InstrumentID] || 'N/A';
101
- target[sector] = (target[sector] || 0) + (pos.Invested || pos.Amount || 0);
102
- }
103
- }
104
- }
105
-
106
- /**
107
- * PROCESS: Runs daily for each user.
108
- */
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 ---
124
-
125
- // 3. User is in the cohort, load maps if needed
126
- if (!this.sectorMap) {
127
- this.sectorMap = await getInstrumentSectorMap();
128
- }
129
-
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);
139
- }
140
-
141
- // --- 5. RUN SECTOR ROTATION LOGIC ---
142
- this._accumulateSectorInvestment(todayPortfolio, this.todaySectorInvestment);
143
- this._accumulateSectorInvestment(yesterdayPortfolio, this.yesterdaySectorInvestment);
144
-
145
- this.user_count++;
146
- }
147
-
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;
157
- }
158
-
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.');
162
- return null;
163
- }
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
-
190
- // --- 2. Calculate Asset Flow ---
191
- 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);
200
-
201
- if (priceChangePct === null) continue;
202
-
203
- const expected_day2_value = avg_day1_value * (1 + priceChangePct);
204
- const net_crowd_flow_pct = avg_day2_value - expected_day2_value;
205
-
206
- finalAssetFlow[ticker] = {
207
- net_crowd_flow_pct: net_crowd_flow_pct,
208
- avg_value_day1_pct: avg_day1_value,
209
- avg_value_day2_pct: avg_day2_value
210
- };
211
- }
212
-
213
- // --- 3. Calculate Sector Rotation ---
214
- const finalSectorRotation = {};
215
- const allSectors = new Set([...Object.keys(this.todaySectorInvestment), ...Object.keys(this.yesterdaySectorInvestment)]);
216
- 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.
220
- }
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
-
230
- // 4. Return combined result
231
- return {
232
- asset_flow: finalAssetFlow,
233
- sector_rotation: finalSectorRotation,
234
- user_sample_size: this.user_count
235
- };
236
- }
237
-
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
- }
249
- }
250
-
1
+ /**
2
+ * @fileoverview Calculates "Net Crowd Flow" and "Sector Rotation"
3
+ * *only* for the "Smart Cohort" (Top 20% of Investor Scores).
4
+ *
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.
9
+ */
10
+
11
+ const { Firestore } = require('@google-cloud/firestore');
12
+ const firestore = new Firestore();
13
+ const { loadAllPriceData, getDailyPriceChange } = require('../../../utils/price_data_provider');
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
16
+
17
+ const COHORT_PERCENTILE = 0.8; // Top 20%
18
+ const PROFILE_CALC_ID = 'user-investment-profile'; // The calc to read IS scores from
19
+
20
+ class SmartCohortFlow {
21
+ constructor() {
22
+ // Meta-calc, no constructor state needed
23
+ }
24
+
25
+ /**
26
+ * Loads the Investor Scores, calculates the cohort threshold, and builds the Set of user IDs.
27
+ * --- MODIFIED: Reads from in-memory 'computedDependencies' ---
28
+ */
29
+ _loadCohort(logger, computedDependencies) {
30
+ logger.log('INFO', '[SmartCohortFlow] Loading Investor Scores from in-memory cache...');
31
+
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
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;
52
+ }
53
+
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 };
58
+ }
59
+ }
60
+ _sumAssetValue(positions) {
61
+ const valueMap = {};
62
+ if (!positions || !Array.isArray(positions)) return valueMap;
63
+ for (const pos of positions) {
64
+ if (pos && pos.InstrumentID && pos.Value) {
65
+ valueMap[pos.InstrumentID] = (valueMap[pos.InstrumentID] || 0) + pos.Value;
66
+ }
67
+ }
68
+ return valueMap;
69
+ }
70
+ // --- Sector Rotation Helper (unchanged) ---
71
+ _accumulateSectorInvestment(portfolio, target, sectorMap) {
72
+ if (portfolio && portfolio.AggregatedPositions) {
73
+ for (const pos of portfolio.AggregatedPositions) {
74
+ const sector = sectorMap[pos.InstrumentID] || 'N/A';
75
+ target[sector] = (target[sector] || 0) + (pos.Invested || pos.Amount || 0);
76
+ }
77
+ }
78
+ }
79
+
80
+ /**
81
+ * PROCESS: META REFACTOR
82
+ * This now runs ONCE, loads all data, streams users, and returns one big result.
83
+ */
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...');
88
+
89
+ // 1. Load Cohort from in-memory dependency
90
+ const smartCohortIds = this._loadCohort(logger, computedDependencies);
91
+ if (!smartCohortIds) {
92
+ return null; // Dependency failed
93
+ }
94
+
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
104
+ }
105
+
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.`);
113
+
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
+ }
154
+
155
+ // 4b. RUN SECTOR ROTATION LOGIC
156
+ this._accumulateSectorInvestment(pToday, todaySectorInvestment, sectorMap);
157
+ this._accumulateSectorInvestment(pYesterday, yesterdaySectorInvestment, sectorMap);
158
+
159
+ user_count++;
160
+ }
161
+ }
162
+
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.');
169
+ return null;
170
+ }
171
+
172
+ // 5a. Calculate Asset Flow
173
+ const finalAssetFlow = {};
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);
179
+
180
+ if (priceChangePct === null) continue;
181
+
182
+ const expected_day2_value = avg_day1_value * (1 + priceChangePct);
183
+ const net_crowd_flow_pct = avg_day2_value - expected_day2_value;
184
+
185
+ finalAssetFlow[ticker] = {
186
+ net_crowd_flow_pct: net_crowd_flow_pct,
187
+ avg_value_day1_pct: avg_day1_value,
188
+ avg_value_day2_pct: avg_day2_value
189
+ };
190
+ }
191
+
192
+ // 5b. Calculate Sector Rotation
193
+ const finalSectorRotation = {};
194
+ const allSectors = new Set([...Object.keys(todaySectorInvestment), ...Object.keys(yesterdaySectorInvestment)]);
195
+ for (const sector of allSectors) {
196
+ const todayAmount = todaySectorInvestment[sector] || 0;
197
+ const yesterdayAmount = yesterdaySectorInvestment[sector] || 0;
198
+ finalSectorRotation[sector] = todayAmount - yesterdayAmount;
199
+ }
200
+
201
+ if (Object.keys(finalAssetFlow).length === 0) {
202
+ logger.warn('[SmartCohortFlow] No asset flow calculated (likely all price data missing). Returning null.');
203
+ return null;
204
+ }
205
+
206
+ // 6. Return combined result
207
+ return {
208
+ asset_flow: finalAssetFlow,
209
+ sector_rotation: finalSectorRotation,
210
+ user_sample_size: user_count
211
+ };
212
+ }
213
+
214
+ async getResult() { return null; }
215
+ reset() { }
216
+ }
217
+
251
218
  module.exports = SmartCohortFlow;