aiden-shared-calculations-unified 1.0.64 → 1.0.66

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,58 +1,136 @@
1
1
  /**
2
- * Analyzes user behavior after a position experiences a >10% drawdown.
2
+ * @fileoverview Calculation (Pass 2) for drawdown response.
3
+ *
4
+ * This metric answers: "How do users behave when a position has a
5
+ * drawdown of over 10%?"
6
+ *
7
+ * It checks all positions from yesterday that were in >10% loss
8
+ * and tracks whether the user:
9
+ * 1. Held (position still open today, same size)
10
+ * 2. Closed (position no longer open today)
11
+ * 3. Added (position still open, but Invested % is larger)
3
12
  */
4
13
  class DrawdownResponse {
5
14
  constructor() {
6
- this.drawdown_events = {
7
- held_position: 0,
8
- closed_position: 0,
9
- added_to_position: 0
15
+ this.actions = {
16
+ held: 0,
17
+ closed: 0,
18
+ added: 0
10
19
  };
20
+ this.total_in_drawdown = 0;
11
21
  }
12
22
 
13
- process(todayPortfolio, yesterdayPortfolio, userId) {
14
- if (!yesterdayPortfolio || !todayPortfolio) {
15
- return; // Need both days for comparison
23
+ /**
24
+ * Defines the output schema for this calculation.
25
+ * @returns {object} JSON Schema object
26
+ */
27
+ static getSchema() {
28
+ return {
29
+ "type": "object",
30
+ "description": "Analyzes user behavior in response to a >10% position drawdown.",
31
+ "properties": {
32
+ "total_positions_in_drawdown": {
33
+ "type": "number",
34
+ "description": "Total positions from yesterday that were in >10% drawdown."
35
+ },
36
+ "action_held_pct": {
37
+ "type": "number",
38
+ "description": "Percentage of drawdown positions that were held."
39
+ },
40
+ "action_closed_pct": {
41
+ "type": "number",
42
+ "description": "Percentage of drawdown positions that were closed."
43
+ },
44
+ "action_added_pct": {
45
+ "type": "number",
46
+ "description": "Percentage of drawdown positions that were added to."
47
+ },
48
+ "raw_counts": {
49
+ "type": "object",
50
+ "properties": {
51
+ "held": { "type": "number" },
52
+ "closed": { "type": "number" },
53
+ "added": { "type": "number" }
54
+ },
55
+ "required": ["held", "closed", "added"]
56
+ }
57
+ },
58
+ "required": ["total_positions_in_drawdown", "action_held_pct", "action_closed_pct", "action_added_pct", "raw_counts"]
59
+ };
60
+ }
61
+
62
+ _getPortfolioMap(portfolio) {
63
+ // We MUST use PositionID to track specific trades
64
+ const positions = portfolio?.AggregatedPositions || portfolio?.PublicPositions;
65
+ if (!positions || !Array.isArray(positions)) {
66
+ return new Map();
67
+ }
68
+ // Map<PositionID, { pnl: number, invested: number }>
69
+ return new Map(positions.map(p => [p.PositionID, {
70
+ pnl: (p.NetProfit || 0) / (p.InvestedAmount || p.Amount || 1), // PNL as %
71
+ invested: p.InvestedAmount || p.Amount || 0
72
+ }]));
73
+ }
74
+
75
+ process(todayPortfolio, yesterdayPortfolio) {
76
+ if (!todayPortfolio || !yesterdayPortfolio) {
77
+ return;
16
78
  }
17
79
 
18
- const yPositions = yesterdayPortfolio.AggregatedPositions || yesterdayPortfolio.PublicPositions;
19
- const tPositions = todayPortfolio.AggregatedPositions || todayPortfolio.PublicPositions;
80
+ const yPosMap = this._getPortfolioMap(yesterdayPortfolio);
81
+ const tPosMap = this._getPortfolioMap(todayPortfolio);
20
82
 
21
- if (!yPositions || !Array.isArray(yPositions) || !tPositions || !Array.isArray(tPositions)) {
83
+ if (yPosMap.size === 0) {
22
84
  return;
23
85
  }
24
86
 
25
- // Use PositionID if available (as in original file), fallback to InstrumentID
26
- const todayPositions = new Map(tPositions.map(p => [p.PositionID || p.InstrumentID, p]));
27
-
28
- for (const yPos of yPositions) {
29
- // FIX: Use the NetProfit field, which is already a percentage.
30
- // Your data sample (e.g., -83.6) shows the threshold should be -10.0.
31
- const drawdownPercent = yPos.NetProfit || 0;
32
- const yPosId = yPos.PositionID || yPos.InstrumentID;
33
-
34
- // Check if this position was in a >10% drawdown yesterday
35
- if (drawdownPercent < -10.0) {
36
- const todayPos = todayPositions.get(yPosId);
37
-
38
- if (!todayPos) {
39
- // Position was closed
40
- this.drawdown_events.closed_position++;
41
- } else if (todayPos.Invested > yPos.Invested) {
42
- // FIX: Use 'Invested' (percentage) to check for increase
43
- // User added money to the losing position
44
- this.drawdown_events.added_to_position++;
87
+ for (const [yPosId, yPosData] of yPosMap.entries()) {
88
+ // Check if position was in >10% drawdown
89
+ if (yPosData.pnl < -0.10) {
90
+ this.total_in_drawdown++;
91
+
92
+ // Now, check what happened today
93
+ if (!tPosMap.has(yPosId)) {
94
+ // 1. Position was closed
95
+ this.actions.closed++;
45
96
  } else {
46
- // Position was held (or reduced, but not added to)
47
- this.drawdown_events.held_position++;
97
+ const tPosData = tPosMap.get(yPosId);
98
+ // 2. Position was added to (check for > 1% increase to avoid noise)
99
+ if (tPosData.invested > (yPosData.invested * 1.01)) {
100
+ this.actions.added++;
101
+ } else {
102
+ // 3. Position was held
103
+ this.actions.held++;
104
+ }
48
105
  }
49
106
  }
50
107
  }
51
108
  }
52
109
 
53
110
  getResult() {
54
- // Return final calculated values
55
- return this.drawdown_events;
111
+ const total = this.total_in_drawdown;
112
+ if (total === 0) {
113
+ return {
114
+ total_positions_in_drawdown: 0,
115
+ action_held_pct: 0,
116
+ action_closed_pct: 0,
117
+ action_added_pct: 0,
118
+ raw_counts: this.actions
119
+ };
120
+ }
121
+
122
+ return {
123
+ total_positions_in_drawdown: total,
124
+ action_held_pct: (this.actions.held / total) * 100,
125
+ action_closed_pct: (this.actions.closed / total) * 100,
126
+ action_added_pct: (this.actions.added / total) * 100,
127
+ raw_counts: this.actions
128
+ };
129
+ }
130
+
131
+ reset() {
132
+ this.actions = { held: 0, closed: 0, added: 0 };
133
+ this.total_in_drawdown = 0;
56
134
  }
57
135
  }
58
136
 
@@ -1,218 +1,238 @@
1
1
  /**
2
- * @fileoverview Calculates "Net Crowd Flow" and "Sector Rotation"
3
- * *only* for the "Dumb Cohort" (Bottom 20% of Investor Scores).
2
+ * @fileoverview Calculation (Pass 3) for dumb 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
+ * "Dumb Cohort" (bottom 20% of investors).
6
+ *
7
+ * This calculation *depends* on 'user_profitability_tracker'
8
+ * to identify the cohort.
9
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
- // 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.2; // Bottom 20%
19
- const PROFILE_CALC_ID = 'user-investment-profile'; // The calc to read IS scores from
10
+ const { loadInstrumentMappings } = require('../../../utils/sector_mapping_provider');
20
11
 
21
12
  class DumbCohortFlow {
13
+ constructor() {
14
+ this.assetData = new Map();
15
+ this.sectorData = new Map();
16
+ this.mappings = null;
17
+ this.dumbCohortUserIds = null;
18
+ }
22
19
 
23
20
  /**
24
- * (NEW) Statically declare dependencies.
21
+ * Defines the output schema for this calculation.
22
+ * @returns {object} JSON Schema object
25
23
  */
26
- static getDependencies() {
27
- return [PROFILE_CALC_ID];
28
- }
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
+ };
29
34
 
30
- constructor() {
31
- // Meta-calc, no constructor state needed
35
+ return {
36
+ "type": "object",
37
+ "description": "Calculates net capital flow % (price-adjusted) for the 'Dumb Cohort' (bottom 20% users), aggregated by asset and sector.",
38
+ "properties": {
39
+ "cohort_size": {
40
+ "type": "number",
41
+ "description": "The number of users identified as being in the Dumb 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
+ };
32
58
  }
33
59
 
34
60
  /**
35
- * Loads the Investor Scores from the fetched dependency.
61
+ * Statically declare dependencies.
36
62
  */
37
- _loadCohort(logger, fetchedDependencies) {
38
- logger.log('INFO', '[DumbCohortFlow] Loading Investor Scores from fetched dependency...');
39
-
40
- const profileData = fetchedDependencies[PROFILE_CALC_ID];
41
-
42
- if (!profileData || !profileData.daily_investor_scores || Object.keys(profileData.daily_investor_scores).length === 0) {
43
- logger.log('WARN', `[DumbCohortFlow] Cannot find dependency in fetched data: ${PROFILE_CALC_ID}. Cohort will not be built.`);
44
- return null; // Return null to signal failure
45
- }
46
-
47
- const scores = profileData.daily_investor_scores;
48
- const allScores = Object.entries(scores).map(([userId, score]) => ({ userId, score }));
49
- allScores.sort((a, b) => a.score - b.score);
50
-
51
- const thresholdIndex = Math.floor(allScores.length * COHORT_PERCENTILE);
52
- const thresholdScore = allScores[thresholdIndex]?.score || 0; // Get 20th percentile score
53
-
54
- const dumbCohortIds = new Set(
55
- allScores.filter(s => s.score <= thresholdScore).map(s => s.userId) // Get users *at or below*
56
- );
57
-
58
- logger.log('INFO', `[DumbCohortFlow] Cohort built. ${dumbCohortIds.size} users at or below ${thresholdScore.toFixed(2)} (20th percentile).`);
59
- return dumbCohortIds;
63
+ static getDependencies() {
64
+ return ['user_profitability_tracker'];
60
65
  }
61
66
 
62
- // --- Asset Flow Helpers (unchanged) ---
63
- _initAsset(asset_values, instrumentId) {
64
- if (!asset_values[instrumentId]) {
65
- asset_values[instrumentId] = { day1_value_sum: 0, day2_value_sum: 0 };
66
- }
67
+ _getPortfolioPositions(portfolio) {
68
+ return portfolio?.PublicPositions || portfolio?.AggregatedPositions;
67
69
  }
68
- _sumAssetValue(positions) {
69
- const valueMap = {};
70
- if (!positions || !Array.isArray(positions)) return valueMap;
71
- for (const pos of positions) {
72
- if (pos && pos.InstrumentID && pos.Value) {
73
- valueMap[pos.InstrumentID] = (valueMap[pos.InstrumentID] || 0) + pos.Value;
74
- }
70
+
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
- return valueMap;
77
79
  }
78
- _accumulateSectorInvestment(portfolio, target, sectorMap) {
79
- if (portfolio && portfolio.AggregatedPositions) {
80
- for (const pos of portfolio.AggregatedPositions) {
81
- const sector = sectorMap[pos.InstrumentID] || 'N/A';
82
- target[sector] = (target[sector] || 0) + (pos.Invested || pos.Amount || 0);
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
89
  }
86
90
 
87
91
  /**
88
- * REFACTORED PROCESS METHOD
89
- * @param {string} dateStr The date to run the analysis for (e.g., "2025-10-31").
90
- * @param {object} dependencies The shared dependencies (db, logger, rootData, etc.).
91
- * @param {object} config The computation system configuration.
92
- * @param {object} fetchedDependencies In-memory results from previous passes.
93
- * @returns {Promise<object|null>} The analysis result or null.
92
+ * Helper to get the cohort IDs from the dependency.
94
93
  */
95
- async process(dateStr, dependencies, config, fetchedDependencies) {
96
- const { logger, db, rootData, calculationUtils } = dependencies;
97
- const { portfolioRefs } = rootData;
98
- logger.log('INFO', '[DumbCohortFlow] Starting meta-process...');
94
+ _getDumbCohort(fetchedDependencies) {
95
+ if (this.dumbCohortUserIds) {
96
+ return this.dumbCohortUserIds;
97
+ }
99
98
 
100
- // 1. Load Cohort from in-memory dependency
101
- const dumbCohortIds = this._loadCohort(logger, fetchedDependencies);
102
- if (!dumbCohortIds) {
103
- return null; // Dependency failed
99
+ const profitabilityData = fetchedDependencies['user_profitability_tracker'];
100
+ if (!profitabilityData || !profitabilityData.ranked_users) {
101
+ return new Set();
104
102
  }
105
103
 
106
- // 2. Load external dependencies (prices, sectors)
107
- const [priceMap, mappings, sectorMap] = await Promise.all([
108
- loadAllPriceData(),
109
- loadInstrumentMappings(),
110
- getInstrumentSectorMap()
111
- ]);
112
- if (!priceMap || !mappings || !sectorMap || Object.keys(priceMap).length === 0) {
113
- logger.log('ERROR', '[DumbCohortFlow] Failed to load critical price/mapping/sector data. Aborting.');
114
- return null;
115
- }
104
+ const rankedUsers = profitabilityData.ranked_users;
105
+ const cohortSize = Math.floor(rankedUsers.length * 0.20);
106
+
107
+ // The 'ranked_users' are sorted ascending by P&L, so bottom 20% is the first 20%.
108
+ this.dumbCohortUserIds = new Set(rankedUsers.slice(0, cohortSize).map(u => u.userId));
109
+ return this.dumbCohortUserIds;
110
+ }
116
111
 
117
- // 3. Load "yesterday's" portfolio data for comparison
118
- const yesterdayDate = new Date(dateStr + 'T00:00:00Z');
119
- yesterdayDate.setUTCDate(yesterdayDate.getUTCDate() - 1);
120
- const yesterdayStr = yesterdayDate.toISOString().slice(0, 10);
121
- const yesterdayRefs = await getPortfolioPartRefs(config, dependencies, yesterdayStr);
122
- const yesterdayPortfolios = await loadFullDayMap(config, dependencies, yesterdayRefs);
123
- logger.log('INFO', `[DumbCohortFlow] Loaded ${yesterdayRefs.length} part refs for yesterday.`);
124
-
125
- // 4. Stream "today's" portfolio data and process
126
- const asset_values = {};
127
- const todaySectorInvestment = {};
128
- const yesterdaySectorInvestment = {};
129
- let user_count = 0;
130
-
131
- const batchSize = config.partRefBatchSize || 10;
132
- for (let i = 0; i < portfolioRefs.length; i += batchSize) {
133
- const batchRefs = portfolioRefs.slice(i, i + batchSize);
134
- const todayPortfoliosChunk = await loadDataByRefs(config, dependencies, batchRefs);
135
-
136
- for (const uid in todayPortfoliosChunk) {
137
-
138
- if (!dumbCohortIds.has(uid)) continue; // --- Filter user ---
139
-
140
- const pToday = todayPortfoliosChunk[uid];
141
- const pYesterday = yesterdayPortfolios[uid];
142
-
143
- if (!pToday || !pYesterday || !pToday.AggregatedPositions || !pYesterday.AggregatedPositions) {
144
- continue;
145
- }
146
-
147
- // 4a. RUN ASSET FLOW LOGIC
148
- const yesterdayValues = this._sumAssetValue(pYesterday.AggregatedPositions);
149
- const todayValues = this._sumAssetValue(pToday.AggregatedPositions);
150
- const allInstrumentIds = new Set([...Object.keys(yesterdayValues), ...Object.keys(todayValues)]);
151
-
152
- for (const instrumentId of allInstrumentIds) {
153
- this._initAsset(asset_values, instrumentId);
154
- asset_values[instrumentId].day1_value_sum += (yesterdayValues[instrumentId] || 0);
155
- asset_values[instrumentId].day2_value_sum += (todayValues[instrumentId] || 0);
156
- }
112
+ process(todayPortfolio, yesterdayPortfolio, userId, context, todayInsights, yesterdayInsights, fetchedDependencies) {
113
+ const dumbCohort = this._getDumbCohort(fetchedDependencies);
114
+
115
+ // This user is not in the "dumb cohort", skip.
116
+ if (!dumbCohort.has(userId)) {
117
+ return;
118
+ }
157
119
 
158
- // 4b. RUN SECTOR ROTATION LOGIC
159
- this._accumulateSectorInvestment(pToday, todaySectorInvestment, sectorMap);
160
- this._accumulateSectorInvestment(pYesterday, yesterdaySectorInvestment, sectorMap);
161
- user_count++;
162
- }
120
+ if (!todayPortfolio || !yesterdayPortfolio) {
121
+ return;
163
122
  }
123
+
124
+ const yPos = this._getPortfolioPositions(yesterdayPortfolio);
125
+ const tPos = this._getPortfolioPositions(todayPortfolio);
126
+
127
+ const yPosMap = new Map(yPos?.map(p => [p.InstrumentID, p]) || []);
128
+ const tPosMap = new Map(tPos?.map(p => [p.InstrumentID, p]) || []);
129
+
130
+ const allInstrumentIds = new Set([...yPosMap.keys(), ...tPosMap.keys()]);
164
131
 
165
- logger.log('INFO', `[DumbCohortFlow] Processed ${user_count} users in cohort.`);
166
-
167
- // --- 5. GETRESULT LOGIC ---
168
- if (user_count === 0) {
169
- logger.warn('[DumbCohortFlow] No users processed for dumb cohort. Returning null.');
170
- return null;
132
+ if (!this.mappings) {
133
+ // Context contains the mappings loaded in Pass 1
134
+ this.mappings = context.mappings;
171
135
  }
172
136
 
173
- // 5a. Calculate Asset Flow
174
- const finalAssetFlow = {};
175
- for (const instrumentId in asset_values) {
176
- const ticker = mappings.instrumentToTicker[instrumentId] || `id_${instrumentId}`;
177
- const avg_day1_value = asset_values[instrumentId].day1_value_sum / user_count;
178
- const avg_day2_value = asset_values[instrumentId].day2_value_sum / user_count;
179
- const priceChangePct = getDailyPriceChange(instrumentId, yesterdayStr, dateStr, priceMap);
137
+ for (const instrumentId of allInstrumentIds) {
138
+ if (!instrumentId) continue;
139
+
140
+ this._initAsset(instrumentId);
141
+ const asset = this.assetData.get(instrumentId);
180
142
 
181
- if (priceChangePct === null) continue;
143
+ const yP = yPosMap.get(instrumentId);
144
+ const tP = tPosMap.get(instrumentId);
182
145
 
183
- const expected_day2_value = avg_day1_value * (1 + priceChangePct);
184
- const net_crowd_flow_pct = avg_day2_value - expected_day2_value;
146
+ const yInvested = yP?.InvestedAmount || yP?.Amount || 0;
147
+ const tInvested = tP?.InvestedAmount || tP?.Amount || 0;
148
+
149
+ // Get sector and initialize it
150
+ const sector = this.mappings.instrumentToSector[instrumentId] || 'Other';
151
+ this._initSector(sector);
152
+ const sectorAsset = this.sectorData.get(sector);
185
153
 
186
- finalAssetFlow[ticker] = {
187
- net_crowd_flow_pct: net_crowd_flow_pct,
188
- avg_value_day1_pct: avg_day1_value,
189
- avg_value_day2_pct: avg_day2_value
190
- };
154
+ if (yInvested > 0) {
155
+ const yPriceChange = (yP?.PipsRate || 0) / (yP?.OpenRate || 1);
156
+
157
+ asset.total_invested_yesterday += yInvested;
158
+ asset.price_change_yesterday += yPriceChange * yInvested;
159
+
160
+ sectorAsset.total_invested_yesterday += yInvested;
161
+ sectorAsset.price_change_yesterday += yPriceChange * yInvested;
162
+ }
163
+ if (tInvested > 0) {
164
+ asset.total_invested_today += tInvested;
165
+ sectorAsset.total_invested_today += tInvested;
166
+ }
191
167
  }
192
-
193
- // 5b. Calculate Sector Rotation
194
- const finalSectorRotation = {};
195
- const allSectors = new Set([...Object.keys(todaySectorInvestment), ...Object.keys(yesterdaySectorInvestment)]);
196
- for (const sector of allSectors) {
197
- const todayAmount = todaySectorInvestment[sector] || 0;
198
- const yesterdayAmount = yesterdaySectorInvestment[sector] || 0;
199
- finalSectorRotation[sector] = todayAmount - yesterdayAmount;
168
+ }
169
+
170
+ _calculateFlow(dataMap) {
171
+ const result = {};
172
+ for (const [key, data] of dataMap.entries()) {
173
+ const { total_invested_yesterday, total_invested_today, price_change_yesterday } = data;
174
+
175
+ if (total_invested_yesterday > 0) {
176
+ const avg_price_change_pct = price_change_yesterday / total_invested_yesterday;
177
+ const price_contribution = total_invested_yesterday * avg_price_change_pct;
178
+ const flow_contribution = total_invested_today - (total_invested_yesterday + price_contribution);
179
+ const net_flow_percentage = (flow_contribution / total_invested_yesterday) * 100;
180
+
181
+ result[key] = {
182
+ net_flow_percentage: net_flow_percentage,
183
+ total_invested_today: total_invested_today,
184
+ total_invested_yesterday: total_invested_yesterday
185
+ };
186
+ }
200
187
  }
188
+ return result;
189
+ }
201
190
 
202
- if (Object.keys(finalAssetFlow).length === 0) {
203
- logger.warn('[DumbCohortFlow] No asset flow calculated (likely all price data missing). Returning null.');
204
- return null;
191
+ async getResult(fetchedDependencies) {
192
+ // Ensure mappings are loaded (can be from context or loaded now)
193
+ if (!this.mappings) {
194
+ this.mappings = await loadInstrumentMappings();
205
195
  }
196
+
197
+ // Ensure cohort is calculated at least once
198
+ const dumbCohort = this._getDumbCohort(fetchedDependencies);
199
+
200
+ // 1. Calculate Asset Flow
201
+ const assetResult = {};
202
+ for (const [instrumentId, data] of this.assetData.entries()) {
203
+ const ticker = this.mappings.instrumentToTicker[instrumentId] || `id_${instrumentId}`;
204
+ const { total_invested_yesterday, total_invested_today, price_change_yesterday } = data;
205
+
206
+ if (total_invested_yesterday > 0) {
207
+ const avg_price_change_pct = price_change_yesterday / total_invested_yesterday;
208
+ const price_contribution = total_invested_yesterday * avg_price_change_pct;
209
+ const flow_contribution = total_invested_today - (total_invested_yesterday + price_contribution);
210
+ const net_flow_percentage = (flow_contribution / total_invested_yesterday) * 100;
211
+
212
+ assetResult[ticker] = {
213
+ net_flow_percentage: net_flow_percentage,
214
+ total_invested_today: total_invested_today,
215
+ total_invested_yesterday: total_invested_yesterday
216
+ };
217
+ }
218
+ }
219
+
220
+ // 2. Calculate Sector Flow
221
+ const sectorResult = this._calculateFlow(this.sectorData);
206
222
 
207
223
  return {
208
- asset_flow: finalAssetFlow,
209
- sector_rotation: finalSectorRotation,
210
- user_sample_size: user_count
224
+ cohort_size: dumbCohort.size,
225
+ assets: assetResult,
226
+ sectors: sectorResult
211
227
  };
212
228
  }
213
229
 
214
- async getResult() { return null; }
215
- reset() { }
230
+ reset() {
231
+ this.assetData.clear();
232
+ this.sectorData.clear();
233
+ this.mappings = null;
234
+ this.dumbCohortUserIds = null;
235
+ }
216
236
  }
217
237
 
218
238
  module.exports = DumbCohortFlow;