aiden-shared-calculations-unified 1.0.63 → 1.0.65

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 (89) hide show
  1. package/README.MD +1 -1
  2. package/calculations/activity/historical/activity_by_pnl_status.js +33 -0
  3. package/calculations/activity/historical/daily_asset_activity.js +42 -0
  4. package/calculations/activity/historical/daily_user_activity_tracker.js +37 -0
  5. package/calculations/activity/historical/speculator_adjustment_activity.js +26 -0
  6. package/calculations/asset_metrics/asset_position_size.js +36 -0
  7. package/calculations/backtests/strategy-performance.js +41 -0
  8. package/calculations/behavioural/historical/asset_crowd_flow.js +124 -127
  9. package/calculations/behavioural/historical/drawdown_response.js +113 -35
  10. package/calculations/behavioural/historical/dumb-cohort-flow.js +191 -171
  11. package/calculations/behavioural/historical/gain_response.js +113 -34
  12. package/calculations/behavioural/historical/historical_performance_aggregator.js +63 -48
  13. package/calculations/behavioural/historical/in_loss_asset_crowd_flow.js +159 -63
  14. package/calculations/behavioural/historical/in_profit_asset_crowd_flow.js +159 -64
  15. package/calculations/behavioural/historical/paper_vs_diamond_hands.js +86 -19
  16. package/calculations/behavioural/historical/position_count_pnl.js +91 -39
  17. package/calculations/behavioural/historical/smart-cohort-flow.js +192 -172
  18. package/calculations/behavioural/historical/smart_money_flow.js +160 -151
  19. package/calculations/capital_flow/historical/crowd-cash-flow-proxy.js +95 -89
  20. package/calculations/capital_flow/historical/deposit_withdrawal_percentage.js +88 -81
  21. package/calculations/capital_flow/historical/new_allocation_percentage.js +75 -26
  22. package/calculations/capital_flow/historical/reallocation_increase_percentage.js +73 -32
  23. package/calculations/insights/daily_buy_sell_sentiment_count.js +47 -32
  24. package/calculations/insights/daily_total_positions_held.js +28 -24
  25. package/calculations/insights/historical/daily_bought_vs_sold_count.js +101 -36
  26. package/calculations/insights/historical/daily_ownership_delta.js +95 -32
  27. package/calculations/meta/capital_deployment_strategy.js +78 -110
  28. package/calculations/meta/capital_liquidation_performance.js +114 -111
  29. package/calculations/meta/cash-flow-deployment.js +114 -107
  30. package/calculations/meta/cash-flow-liquidation.js +114 -107
  31. package/calculations/meta/crowd_sharpe_ratio_proxy.js +94 -54
  32. package/calculations/meta/negative_expectancy_cohort_flow.js +185 -177
  33. package/calculations/meta/positive_expectancy_cohort_flow.js +186 -181
  34. package/calculations/meta/profit_cohort_divergence.js +83 -59
  35. package/calculations/meta/shark_attack_signal.js +91 -39
  36. package/calculations/meta/smart-dumb-divergence-index.js +114 -98
  37. package/calculations/meta/smart_dumb_divergence_index_v2.js +109 -98
  38. package/calculations/meta/social-predictive-regime-state.js +76 -155
  39. package/calculations/meta/social-topic-driver-index.js +74 -127
  40. package/calculations/meta/user_expectancy_score.js +83 -31
  41. package/calculations/pnl/asset_pnl_status.js +120 -31
  42. package/calculations/pnl/average_daily_pnl_all_users.js +42 -27
  43. package/calculations/pnl/average_daily_pnl_per_sector.js +84 -26
  44. package/calculations/pnl/average_daily_pnl_per_stock.js +71 -29
  45. package/calculations/pnl/average_daily_position_pnl.js +49 -21
  46. package/calculations/pnl/historical/profitability_migration.js +81 -35
  47. package/calculations/pnl/historical/user_profitability_tracker.js +107 -104
  48. package/calculations/pnl/pnl_distribution_per_stock.js +65 -45
  49. package/calculations/pnl/profitability_ratio_per_stock.js +78 -21
  50. package/calculations/pnl/profitability_skew_per_stock.js +86 -31
  51. package/calculations/pnl/profitable_and_unprofitable_status.js +45 -45
  52. package/calculations/sanity/users_processed.js +24 -1
  53. package/calculations/sectors/historical/diversification_pnl.js +104 -42
  54. package/calculations/sectors/historical/sector_rotation.js +94 -45
  55. package/calculations/sectors/total_long_per_sector.js +55 -20
  56. package/calculations/sectors/total_short_per_sector.js +55 -20
  57. package/calculations/sentiment/historical/crowd_conviction_score.js +233 -53
  58. package/calculations/short_and_long_stats/long_position_per_stock.js +50 -14
  59. package/calculations/short_and_long_stats/sentiment_per_stock.js +76 -19
  60. package/calculations/short_and_long_stats/short_position_per_stock.js +50 -13
  61. package/calculations/short_and_long_stats/total_long_figures.js +34 -13
  62. package/calculations/short_and_long_stats/total_short_figures.js +34 -14
  63. package/calculations/socialPosts/social-asset-posts-trend.js +96 -29
  64. package/calculations/socialPosts/social-top-mentioned-words.js +95 -74
  65. package/calculations/socialPosts/social-topic-interest-evolution.js +92 -29
  66. package/calculations/socialPosts/social-topic-sentiment-matrix.js +70 -78
  67. package/calculations/socialPosts/social-word-mentions-trend.js +96 -38
  68. package/calculations/socialPosts/social_activity_aggregation.js +106 -77
  69. package/calculations/socialPosts/social_sentiment_aggregation.js +115 -86
  70. package/calculations/speculators/distance_to_stop_loss_per_leverage.js +82 -43
  71. package/calculations/speculators/distance_to_tp_per_leverage.js +81 -42
  72. package/calculations/speculators/entry_distance_to_sl_per_leverage.js +80 -44
  73. package/calculations/speculators/entry_distance_to_tp_per_leverage.js +81 -44
  74. package/calculations/speculators/historical/risk_appetite_change.js +89 -32
  75. package/calculations/speculators/historical/tsl_effectiveness.js +57 -47
  76. package/calculations/speculators/holding_duration_per_asset.js +83 -23
  77. package/calculations/speculators/leverage_per_asset.js +68 -19
  78. package/calculations/speculators/leverage_per_sector.js +86 -25
  79. package/calculations/speculators/risk_reward_ratio_per_asset.js +82 -28
  80. package/calculations/speculators/speculator_asset_sentiment.js +100 -48
  81. package/calculations/speculators/speculator_danger_zone.js +101 -33
  82. package/calculations/speculators/stop_loss_distance_by_sector_short_long_breakdown.js +93 -66
  83. package/calculations/speculators/stop_loss_distance_by_ticker_short_long_breakdown.js +94 -47
  84. package/calculations/speculators/stop_loss_per_asset.js +94 -26
  85. package/calculations/speculators/take_profit_per_asset.js +95 -27
  86. package/calculations/speculators/tsl_per_asset.js +77 -23
  87. package/package.json +1 -1
  88. package/utils/price_data_provider.js +142 -142
  89. package/utils/sector_mapping_provider.js +74 -74
@@ -1,227 +1,232 @@
1
1
  /**
2
- * @fileoverview Calculates "Net Crowd Flow" and "Sector Rotation"
3
- * *only* for the "Smart Cohort" (Top 20% of Investor Scores).
2
+ * @fileoverview Calculation (Pass 4) for positive expectancy cohort flow.
4
3
  *
5
- * --- META REFACTOR (v2) ---
6
- * This calculation is `type: "meta"` and expects its dependencies
7
- * (the user-investment-profile results) to be fetched by the pass runner.
8
- * It then streams root portfolio data.
4
+ * This metric calculates the "Net Crowd Flow Percentage" for the
5
+ * "Positive Expectancy Cohort" (users with a high expectancy score).
6
+ *
7
+ * This calculation *depends* on 'user_expectancy_score'
8
+ * to identify the cohort.
9
9
  */
10
+ const { loadInstrumentMappings } = require('../../../utils/sector_mapping_provider');
10
11
 
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
- // NOTE: Corrected relative path for data_loader
16
- const { loadDataByRefs, getPortfolioPartRefs, loadFullDayMap } = require('../../../bulltrackers-module/functions/computation-system/utils/data_loader');
17
-
18
- const COHORT_PERCENTILE = 0.8; // Top 20%
19
- const PROFILE_CALC_ID = 'user-investment-profile'; // The calc to read IS scores from
20
-
21
- class SmartCohortFlow {
22
-
23
- /**
24
- * (NEW) Statically declare dependencies.
25
- */
26
- static getDependencies() {
27
- return ['user_expectancy_score'];
28
- }
29
-
12
+ class PositiveExpectancyCohortFlow {
30
13
  constructor() {
31
- // Meta-calc, no constructor state needed
14
+ this.assetData = new Map();
15
+ this.sectorData = new Map();
16
+ this.mappings = null;
17
+ this.posExpCohortUserIds = null;
32
18
  }
33
19
 
34
20
  /**
35
- * Loads the Investor Scores from the fetched dependency.
21
+ * Defines the output schema for this calculation.
22
+ * @returns {object} JSON Schema object
36
23
  */
37
- _loadCohort(logger, fetchedDependencies) {
38
- logger.log('INFO', '[PositiveExpectancyCohortFlow] Loading Expectancy Scores from fetched dependency...');
39
-
40
- // 1. Get the new dependency
41
- const profileData = fetchedDependencies['user-expectancy-score'];
24
+ static getSchema() {
25
+ const flowSchema = {
26
+ "type": "object",
27
+ "properties": {
28
+ "net_flow_percentage": { "type": "number" },
29
+ "total_invested_today": { "type": "number" },
30
+ "total_invested_yesterday": { "type": "number" }
31
+ },
32
+ "required": ["net_flow_percentage", "total_invested_today", "total_invested_yesterday"]
33
+ };
42
34
 
43
- if (!profileData || Object.keys(profileData).length === 0) {
44
- logger.log('WARN', `[PositiveExpectancyCohortFlow] Cannot find dependency 'user-expectancy-score'. Cohort will not be built.`);
45
- return null;
35
+ return {
36
+ "type": "object",
37
+ "description": "Calculates net capital flow % (price-adjusted) for the 'Positive Expectancy' cohort (score > 0.5), aggregated by asset and sector.",
38
+ "properties": {
39
+ "cohort_size": {
40
+ "type": "number",
41
+ "description": "The number of users identified as being in the Positive Expectancy Cohort."
42
+ },
43
+ "assets": {
44
+ "type": "object",
45
+ "description": "Price-adjusted net flow per asset.",
46
+ "patternProperties": { "^.*$": flowSchema }, // Ticker
47
+ "additionalProperties": flowSchema
48
+ },
49
+ "sectors": {
50
+ "type": "object",
51
+ "description": "Price-adjusted net flow per sector.",
52
+ "patternProperties": { "^.*$": flowSchema }, // Sector
53
+ "additionalProperties": flowSchema
54
+ }
55
+ },
56
+ "required": ["cohort_size", "assets", "sectors"]
57
+ };
46
58
  }
47
59
 
48
- // 2. The data is already the map we need: { [userId]: { expectancy_score: X } }
49
- const allScores = Object.entries(profileData).map(([userId, data]) => ({
50
- userId,
51
- score: data.expectancy_score
52
- }));
53
-
54
- // 3. Sort by score, lowest to highest
55
- allScores.sort((a, b) => a.score - b.score);
56
-
57
- // 4. Find the 80th percentile (Top 20%)
58
- const thresholdIndex = Math.floor(allScores.length * 0.80);
59
- const thresholdScore = allScores[thresholdIndex]?.score || 999;
60
-
61
- // 5. Filter for users with a *positive expectancy score* AND are in the top 20%
62
- const cohortIds = new Set(
63
- allScores.filter(s => s.score >= thresholdScore && s.score > 0)
64
- .map(s => s.userId)
65
- );
60
+ /**
61
+ * Statically declare dependencies.
62
+ */
63
+ static getDependencies() {
64
+ return ['user_expectancy_score']; // Pass 3
65
+ }
66
66
 
67
- logger.log('INFO', `[PositiveExpectancyCohortFlow] Cohort built. ${cohortIds.size} users at or above ${thresholdScore.toFixed(2)} (80th percentile) and > 0.`);
68
- return cohortIds;
67
+ _getPortfolioPositions(portfolio) {
68
+ return portfolio?.PublicPositions || portfolio?.AggregatedPositions;
69
69
  }
70
70
 
71
- // --- Asset Flow Helpers (unchanged) ---
72
- _initAsset(asset_values, instrumentId) {
73
- if (!asset_values[instrumentId]) {
74
- asset_values[instrumentId] = { day1_value_sum: 0, day2_value_sum: 0 };
71
+ _initAsset(instrumentId) {
72
+ if (!this.assetData.has(instrumentId)) {
73
+ this.assetData.set(instrumentId, {
74
+ total_invested_yesterday: 0,
75
+ total_invested_today: 0,
76
+ price_change_yesterday: 0,
77
+ });
75
78
  }
76
79
  }
77
- _sumAssetValue(positions) {
78
- const valueMap = {};
79
- if (!positions || !Array.isArray(positions)) return valueMap;
80
- for (const pos of positions) {
81
- if (pos && pos.InstrumentID && pos.Value) {
82
- valueMap[pos.InstrumentID] = (valueMap[pos.InstrumentID] || 0) + pos.Value;
83
- }
80
+
81
+ _initSector(sector) {
82
+ if (!this.sectorData.has(sector)) {
83
+ this.sectorData.set(sector, {
84
+ total_invested_yesterday: 0,
85
+ total_invested_today: 0,
86
+ price_change_yesterday: 0,
87
+ });
84
88
  }
85
- return valueMap;
86
89
  }
87
- _accumulateSectorInvestment(portfolio, target, sectorMap) {
88
- if (portfolio && portfolio.AggregatedPositions) {
89
- for (const pos of portfolio.AggregatedPositions) {
90
- const sector = sectorMap[pos.InstrumentID] || 'N/A';
91
- target[sector] = (target[sector] || 0) + (pos.Invested || pos.Amount || 0);
90
+
91
+ _getPosExpCohort(fetchedDependencies) {
92
+ if (this.posExpCohortUserIds) {
93
+ return this.posExpCohortUserIds;
94
+ }
95
+
96
+ const expectancyData = fetchedDependencies['user_expectancy_score'];
97
+ if (!expectancyData) {
98
+ return new Set();
99
+ }
100
+
101
+ this.posExpCohortUserIds = new Set();
102
+ for (const [userId, data] of Object.entries(expectancyData)) {
103
+ // Definition: Expectancy score > 0.5
104
+ if (data.expectancy_score > 0.5) {
105
+ this.posExpCohortUserIds.add(userId);
92
106
  }
93
107
  }
108
+ return this.posExpCohortUserIds;
94
109
  }
95
110
 
96
- /**
97
- * REFACTORED PROCESS METHOD
98
- * @param {string} dateStr The date to run the analysis for (e.g., "2025-10-31").
99
- * @param {object} dependencies The shared dependencies (db, logger, rootData, etc.).
100
- * @param {object} config The computation system configuration.
101
- * @param {object} fetchedDependencies In-memory results from previous passes.
102
- * @returns {Promise<object|null>} The analysis result or null.
103
- */
104
- async process(dateStr, dependencies, config, fetchedDependencies) {
105
- const { logger, db, rootData, calculationUtils } = dependencies;
106
- const { portfolioRefs } = rootData;
107
- logger.log('INFO', '[SmartCohortFlow] Starting meta-process...');
111
+ process(todayPortfolio, yesterdayPortfolio, userId, context, todayInsights, yesterdayInsights, fetchedDependencies) {
112
+ const cohort = this._getPosExpCohort(fetchedDependencies);
108
113
 
109
- // 1. Load Cohort from in-memory dependency
110
- const smartCohortIds = this._loadCohort(logger, fetchedDependencies);
111
- if (!smartCohortIds) {
112
- return null; // Dependency failed
114
+ if (!cohort.has(userId)) {
115
+ return;
113
116
  }
114
117
 
115
- // 2. Load external dependencies (prices, sectors)
116
- const [priceMap, mappings, sectorMap] = await Promise.all([
117
- loadAllPriceData(),
118
- loadInstrumentMappings(),
119
- getInstrumentSectorMap()
120
- ]);
121
- if (!priceMap || !mappings || !sectorMap || Object.keys(priceMap).length === 0) {
122
- logger.log('ERROR', '[SmartCohortFlow] Failed to load critical price/mapping/sector data. Aborting.');
123
- return null;
118
+ if (!todayPortfolio || !yesterdayPortfolio) {
119
+ return;
124
120
  }
125
121
 
126
- // 3. Load "yesterday's" portfolio data for comparison
127
- const yesterdayDate = new Date(dateStr + 'T00:00:00Z');
128
- yesterdayDate.setUTCDate(yesterdayDate.getUTCDate() - 1);
129
- const yesterdayStr = yesterdayDate.toISOString().slice(0, 10);
130
- const yesterdayRefs = await getPortfolioPartRefs(config, dependencies, yesterdayStr);
131
- const yesterdayPortfolios = await loadFullDayMap(config, dependencies, yesterdayRefs);
132
- logger.log('INFO', `[SmartCohortFlow] Loaded ${yesterdayRefs.length} part refs for yesterday.`);
133
-
134
- // 4. Stream "today's" portfolio data and process
135
- const asset_values = {};
136
- const todaySectorInvestment = {};
137
- const yesterdaySectorInvestment = {};
138
- let user_count = 0;
139
-
140
- const batchSize = config.partRefBatchSize || 10;
141
- for (let i = 0; i < portfolioRefs.length; i += batchSize) {
142
- const batchRefs = portfolioRefs.slice(i, i + batchSize);
143
- const todayPortfoliosChunk = await loadDataByRefs(config, dependencies, batchRefs);
144
-
145
- for (const uid in todayPortfoliosChunk) {
146
-
147
- if (!smartCohortIds.has(uid)) continue; // --- Filter user ---
148
-
149
- const pToday = todayPortfoliosChunk[uid];
150
- const pYesterday = yesterdayPortfolios[uid];
151
-
152
- if (!pToday || !pYesterday || !pToday.AggregatedPositions || !pYesterday.AggregatedPositions) {
153
- continue;
154
- }
155
-
156
- // 4a. RUN ASSET FLOW LOGIC
157
- const yesterdayValues = this._sumAssetValue(pYesterday.AggregatedPositions);
158
- const todayValues = this._sumAssetValue(pToday.AggregatedPositions);
159
- const allInstrumentIds = new Set([...Object.keys(yesterdayValues), ...Object.keys(todayValues)]);
160
-
161
- for (const instrumentId of allInstrumentIds) {
162
- this._initAsset(asset_values, instrumentId);
163
- asset_values[instrumentId].day1_value_sum += (yesterdayValues[instrumentId] || 0);
164
- asset_values[instrumentId].day2_value_sum += (todayValues[instrumentId] || 0);
165
- }
122
+ const yPos = this._getPortfolioPositions(yesterdayPortfolio);
123
+ const tPos = this._getPortfolioPositions(todayPortfolio);
166
124
 
167
- // 4b. RUN SECTOR ROTATION LOGIC
168
- this._accumulateSectorInvestment(pToday, todaySectorInvestment, sectorMap);
169
- this._accumulateSectorInvestment(pYesterday, yesterdaySectorInvestment, sectorMap);
170
- user_count++;
171
- }
172
- }
173
-
174
- logger.log('INFO', `[SmartCohortFlow] Processed ${user_count} users in cohort.`);
125
+ const yPosMap = new Map(yPos?.map(p => [p.InstrumentID, p]) || []);
126
+ const tPosMap = new Map(tPos?.map(p => [p.InstrumentID, p]) || []);
127
+
128
+ const allInstrumentIds = new Set([...yPosMap.keys(), ...tPosMap.keys()]);
175
129
 
176
- // --- 5. GETRESULT LOGIC ---
177
- if (user_count === 0) {
178
- logger.warn('[SmartCohortFlow] No users processed for smart cohort. Returning null.');
179
- return null;
130
+ if (!this.mappings) {
131
+ this.mappings = context.mappings;
180
132
  }
181
133
 
182
- // 5a. Calculate Asset Flow
183
- const finalAssetFlow = {};
184
- for (const instrumentId in asset_values) {
185
- const ticker = mappings.instrumentToTicker[instrumentId] || `id_${instrumentId}`;
186
- const avg_day1_value = asset_values[instrumentId].day1_value_sum / user_count;
187
- const avg_day2_value = asset_values[instrumentId].day2_value_sum / user_count;
188
- const priceChangePct = getDailyPriceChange(instrumentId, yesterdayStr, dateStr, priceMap);
134
+ for (const instrumentId of allInstrumentIds) {
135
+ if (!instrumentId) continue;
136
+
137
+ this._initAsset(instrumentId);
138
+ const asset = this.assetData.get(instrumentId);
189
139
 
190
- if (priceChangePct === null) continue;
140
+ const yP = yPosMap.get(instrumentId);
141
+ const tP = tPosMap.get(instrumentId);
191
142
 
192
- const expected_day2_value = avg_day1_value * (1 + priceChangePct);
193
- const net_crowd_flow_pct = avg_day2_value - expected_day2_value;
143
+ const yInvested = yP?.InvestedAmount || yP?.Amount || 0;
144
+ const tInvested = tP?.InvestedAmount || tP?.Amount || 0;
145
+
146
+ const sector = this.mappings.instrumentToSector[instrumentId] || 'Other';
147
+ this._initSector(sector);
148
+ const sectorAsset = this.sectorData.get(sector);
194
149
 
195
- finalAssetFlow[ticker] = {
196
- net_crowd_flow_pct: net_crowd_flow_pct,
197
- avg_value_day1_pct: avg_day1_value,
198
- avg_value_day2_pct: avg_day2_value
199
- };
150
+ if (yInvested > 0) {
151
+ const yPriceChange = (yP?.PipsRate || 0) / (yP?.OpenRate || 1);
152
+
153
+ asset.total_invested_yesterday += yInvested;
154
+ asset.price_change_yesterday += yPriceChange * yInvested;
155
+
156
+ sectorAsset.total_invested_yesterday += yInvested;
157
+ sectorAsset.price_change_yesterday += yPriceChange * yInvested;
158
+ }
159
+ if (tInvested > 0) {
160
+ asset.total_invested_today += tInvested;
161
+ sectorAsset.total_invested_today += tInvested;
162
+ }
200
163
  }
201
-
202
- // 5b. Calculate Sector Rotation
203
- const finalSectorRotation = {};
204
- const allSectors = new Set([...Object.keys(todaySectorInvestment), ...Object.keys(yesterdaySectorInvestment)]);
205
- for (const sector of allSectors) {
206
- const todayAmount = todaySectorInvestment[sector] || 0;
207
- const yesterdayAmount = yesterdaySectorInvestment[sector] || 0;
208
- finalSectorRotation[sector] = todayAmount - yesterdayAmount;
164
+ }
165
+
166
+ _calculateFlow(dataMap) {
167
+ const result = {};
168
+ for (const [key, data] of dataMap.entries()) {
169
+ const { total_invested_yesterday, total_invested_today, price_change_yesterday } = data;
170
+
171
+ if (total_invested_yesterday > 0) {
172
+ const avg_price_change_pct = price_change_yesterday / total_invested_yesterday;
173
+ const price_contribution = total_invested_yesterday * avg_price_change_pct;
174
+ const flow_contribution = total_invested_today - (total_invested_yesterday + price_contribution);
175
+ const net_flow_percentage = (flow_contribution / total_invested_yesterday) * 100;
176
+
177
+ result[key] = {
178
+ net_flow_percentage: net_flow_percentage,
179
+ total_invested_today: total_invested_today,
180
+ total_invested_yesterday: total_invested_yesterday
181
+ };
182
+ }
209
183
  }
184
+ return result;
185
+ }
210
186
 
211
- if (Object.keys(finalAssetFlow).length === 0) {
212
- logger.warn('[SmartCohortFlow] No asset flow calculated (likely all price data missing). Returning null.');
213
- return null;
187
+ async getResult(fetchedDependencies) {
188
+ if (!this.mappings) {
189
+ this.mappings = await loadInstrumentMappings();
214
190
  }
191
+
192
+ const cohort = this._getPosExpCohort(fetchedDependencies);
193
+
194
+ // 1. Calculate Asset Flow
195
+ const assetResult = {};
196
+ for (const [instrumentId, data] of this.assetData.entries()) {
197
+ const ticker = this.mappings.instrumentToTicker[instrumentId] || `id_${instrumentId}`;
198
+ const { total_invested_yesterday, total_invested_today, price_change_yesterday } = data;
199
+
200
+ if (total_invested_yesterday > 0) {
201
+ const avg_price_change_pct = price_change_yesterday / total_invested_yesterday;
202
+ const price_contribution = total_invested_yesterday * avg_price_change_pct;
203
+ const flow_contribution = total_invested_today - (total_invested_yesterday + price_contribution);
204
+ const net_flow_percentage = (flow_contribution / total_invested_yesterday) * 100;
205
+
206
+ assetResult[ticker] = {
207
+ net_flow_percentage: net_flow_percentage,
208
+ total_invested_today: total_invested_today,
209
+ total_invested_yesterday: total_invested_yesterday
210
+ };
211
+ }
212
+ }
213
+
214
+ // 2. Calculate Sector Flow
215
+ const sectorResult = this._calculateFlow(this.sectorData);
215
216
 
216
217
  return {
217
- asset_flow: finalAssetFlow,
218
- sector_rotation: finalSectorRotation,
219
- user_sample_size: user_count
218
+ cohort_size: cohort.size,
219
+ assets: assetResult,
220
+ sectors: sectorResult
220
221
  };
221
222
  }
222
223
 
223
- async getResult() { return null; }
224
- reset() { }
224
+ reset() {
225
+ this.assetData.clear();
226
+ this.sectorData.clear();
227
+ this.mappings = null;
228
+ this.posExpCohortUserIds = null;
229
+ }
225
230
  }
226
231
 
227
- module.exports = SmartCohortFlow;
232
+ module.exports = PositiveExpectancyCohortFlow;
@@ -1,91 +1,115 @@
1
1
  /**
2
- * @fileoverview Meta-calculation (Pass 2) that correlates the asset flow
3
- * of the "In Profit" cohort vs. the "In Loss" cohort to find
4
- * powerful divergence signals (e.g., profit-taking, capitulation).
2
+ * @fileoverview Calculation (Pass 4) for profit cohort divergence.
5
3
  *
6
- * --- META REFACTOR (v2) ---
7
- * This calculation is now stateless. It declares its dependencies and
8
- * expects them to be passed to its `process` method.
4
+ * This metric answers: "What divergence signals can be found by
5
+ * comparing the net asset flow of the 'in-profit' cohort vs.
6
+ * the 'in-loss' cohort?"
7
+ *
8
+ * e.g.,
9
+ * - Profit cohort selling, Loss cohort holding = "Profit Taking"
10
+ * - Profit cohort holding, Loss cohort selling = "Capitulation"
11
+ * - Both buying = "Confirmation"
12
+ * - Both selling = "Exodus"
13
+ *
14
+ * It *depends* on 'in_profit_asset_crowd_flow' and
15
+ * 'in_loss_asset_crowd_flow'.
9
16
  */
10
-
11
17
  class ProfitCohortDivergence {
12
-
18
+ constructor() {
19
+ // No per-user processing
20
+ }
21
+
13
22
  /**
14
- * (NEW) Statically declare dependencies.
23
+ * Defines the output schema for this calculation.
24
+ * @returns {object} JSON Schema object
15
25
  */
16
- static getDependencies() {
17
- return ['in-profit-asset-crowd-flow', 'in-loss-asset-crowd-flow'];
18
- }
19
-
20
- constructor() {
21
- this.flowThreshold = 0.005; // Min abs flow %
26
+ static getSchema() {
27
+ const signalSchema = {
28
+ "type": "object",
29
+ "properties": {
30
+ "status": {
31
+ "type": "string",
32
+ "enum": ["Profit Taking", "Capitulation", "Confirmation (Buy)", "Confirmation (Sell)", "Divergence (Profit Buy / Loss Sell)", "Divergence (Profit Sell / Loss Buy)", "Neutral"]
33
+ },
34
+ "profit_cohort_flow_pct": { "type": "number" },
35
+ "loss_cohort_flow_pct": { "type": "number" }
36
+ },
37
+ "required": ["status", "profit_cohort_flow_pct", "loss_cohort_flow_pct"]
38
+ };
39
+
40
+ return {
41
+ "type": "object",
42
+ "description": "Generates divergence signals by comparing net flow of 'in-profit' vs. 'in-loss' cohorts.",
43
+ "patternProperties": {
44
+ "^.*$": signalSchema // Ticker
45
+ },
46
+ "additionalProperties": signalSchema
47
+ };
22
48
  }
23
49
 
24
50
  /**
25
- * REFACTORED PROCESS METHOD
26
- * @param {string} dateStr The date to run the analysis for (e.g., "2025-10-31").
27
- * @param {object} dependencies The shared dependencies (db, logger).
28
- * @param {object} config The computation system configuration.
29
- * @param {object} fetchedDependencies In-memory results from previous passes.
30
- * e.g., { 'in-profit-asset-crowd-flow': ..., 'in-loss-asset-crowd-flow': ... }
31
- * @returns {Promise<object|null>} The analysis result or null.
51
+ * Statically declare dependencies.
32
52
  */
33
- async process(dateStr, dependencies, config, fetchedDependencies) {
34
- const { logger } = dependencies;
53
+ static getDependencies() {
54
+ return [
55
+ 'in_profit_asset_crowd_flow', // Pass 3
56
+ 'in_loss_asset_crowd_flow' // Pass 3
57
+ ];
58
+ }
59
+
60
+ process() {
61
+ // No-op
62
+ }
35
63
 
36
- // 1. Get dependencies
37
- const profitFlowData = fetchedDependencies['in-profit-asset-crowd-flow'];
38
- const lossFlowData = fetchedDependencies['in-loss-asset-crowd-flow'];
64
+ getResult(fetchedDependencies) {
65
+ const profitFlowData = fetchedDependencies['in_profit_asset_crowd_flow'];
66
+ const lossFlowData = fetchedDependencies['in_loss_asset_crowd_flow'];
39
67
 
40
- // 2. Handle missing dependencies
41
68
  if (!profitFlowData || !lossFlowData) {
42
- logger.log('WARN', `[ProfitCohortDivergence] Missing computed dependency for ${dateStr}. Skipping.`);
43
- return null;
69
+ return {};
44
70
  }
45
71
 
46
- const results = {};
47
72
  const allTickers = new Set([...Object.keys(profitFlowData), ...Object.keys(lossFlowData)]);
73
+ const result = {};
74
+ const THRESHOLD = 1; // Min flow % to be considered 'active'
48
75
 
49
- // 3. Correlate
50
76
  for (const ticker of allTickers) {
51
- const profitFlow = profitFlowData[ticker]?.net_crowd_flow_pct || 0;
52
- const lossFlow = lossFlowData[ticker]?.net_crowd_flow_pct || 0;
53
-
54
- const profitSells = profitFlow <= -this.flowThreshold;
55
- const profitBuys = profitFlow >= this.flowThreshold;
56
- const lossSells = lossFlow <= -this.flowThreshold;
57
- const lossBuys = lossFlow >= this.flowThreshold;
77
+ const pFlow = profitFlowData[ticker]?.net_flow_percentage || 0;
78
+ const lFlow = lossFlowData[ticker]?.net_flow_percentage || 0;
58
79
 
59
- let status = 'No Divergence';
60
- let detail = 'Both cohorts are acting similarly or flow is insignificant.';
80
+ let status = 'Neutral';
61
81
 
62
- if (profitSells && lossBuys) {
82
+ if (pFlow > THRESHOLD && lFlow > THRESHOLD) {
83
+ status = 'Confirmation (Buy)';
84
+ } else if (pFlow < -THRESHOLD && lFlow < -THRESHOLD) {
85
+ status = 'Confirmation (Sell)';
86
+ } else if (pFlow > THRESHOLD && Math.abs(lFlow) < THRESHOLD) {
87
+ status = 'Divergence (Profit Buy / Loss Sell)'; // Profit cohort buying, loss cohort holding
88
+ } else if (pFlow < -THRESHOLD && Math.abs(lFlow) < THRESHOLD) {
63
89
  status = 'Profit Taking';
64
- detail = 'The "in-profit" cohort is selling to the "in-loss" cohort, who are averaging down.';
65
- } else if (profitBuys && lossSells) {
90
+ } else if (Math.abs(pFlow) < THRESHOLD && lFlow < -THRESHOLD) {
66
91
  status = 'Capitulation';
67
- detail = 'The "in-loss" cohort is panic-selling, and the "in-profit" cohort is buying the dip.';
68
- } else if (profitBuys && lossBuys) {
69
- status = 'High Conviction Buy';
70
- detail = 'All cohorts are net-buying.';
71
- } else if (profitSells && lossSells) {
72
- status = 'High Conviction Sell';
73
- detail = 'All cohorts are net-selling.';
92
+ } else if (Math.abs(pFlow) < THRESHOLD && lFlow > THRESHOLD) {
93
+ status = 'Divergence (Profit Sell / Loss Buy)'; // Loss cohort buying, profit cohort holding
94
+ } else if (pFlow > THRESHOLD && lFlow < -THRESHOLD) {
95
+ status = 'Divergence (Profit Buy / Loss Sell)';
96
+ } else if (pFlow < -THRESHOLD && lFlow > THRESHOLD) {
97
+ status = 'Divergence (Profit Sell / Loss Buy)';
74
98
  }
75
99
 
76
- results[ticker] = {
100
+ result[ticker] = {
77
101
  status: status,
78
- detail: detail,
79
- profit_cohort_flow: profitFlow,
80
- loss_cohort_flow: lossFlow
102
+ profit_cohort_flow_pct: pFlow,
103
+ loss_cohort_flow_pct: lFlow
81
104
  };
82
105
  }
83
-
84
- return results;
106
+
107
+ return result;
85
108
  }
86
109
 
87
- async getResult() { return null; }
88
- reset() {}
110
+ reset() {
111
+ // No state
112
+ }
89
113
  }
90
114
 
91
115
  module.exports = ProfitCohortDivergence;