aiden-shared-calculations-unified 1.0.64 → 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,38 +1,83 @@
1
1
  /**
2
- * @fileoverview Calculates the magnitude of profitable vs. unprofitable positions for each stock,
3
- * providing insight into the skew of returns.
2
+ * @fileoverview Calculation (Pass 1) for profitability skew.
3
+ *
4
+ _ This metric answers: "For each stock, what is the sum and
5
+ * count of *profits* and *losses*?"
6
+ *
7
+ * This helps determine if returns are skewed (e.g., many small
8
+ * wins and a few huge losses).
4
9
  */
5
10
  const { loadInstrumentMappings } = require('../../utils/sector_mapping_provider');
6
11
 
7
12
  class ProfitabilitySkewPerStock {
8
13
  constructor() {
9
- this.skewData = {};
14
+ // { [instrumentId]: { profit_sum: 0, profit_count: 0, loss_sum: 0, loss_count: 0 } }
15
+ this.assets = new Map();
10
16
  this.mappings = null;
11
17
  }
12
18
 
13
- process(portfolioData, yesterdayPortfolio, userId, context) {
14
- const positions = portfolioData.AggregatedPositions || portfolioData.PublicPositions;
15
- if (!positions) return;
16
-
17
- for (const position of positions) {
18
- const instrumentId = position.InstrumentID;
19
- const netProfit = position.NetProfit;
20
-
21
- if (!this.skewData[instrumentId]) {
22
- this.skewData[instrumentId] = {
23
- profit_sum: 0,
24
- loss_sum: 0,
25
- profitable_count: 0,
26
- unprofitable_count: 0
27
- };
28
- }
19
+ /**
20
+ * Defines the output schema for this calculation.
21
+ * @returns {object} JSON Schema object
22
+ */
23
+ static getSchema() {
24
+ const tickerSchema = {
25
+ "type": "object",
26
+ "description": "P&L skew metrics for a specific asset.",
27
+ "properties": {
28
+ "profit_sum": { "type": "number" },
29
+ "profit_count": { "type": "number" },
30
+ "loss_sum": { "type": "number" },
31
+ "loss_count": { "type": "number" },
32
+ "skew_ratio": {
33
+ "type": ["number", "null"],
34
+ "description": "Ratio of (Average Profit / Average Loss). Null if no losses."
35
+ }
36
+ },
37
+ "required": ["profit_sum", "profit_count", "loss_sum", "loss_count", "skew_ratio"]
38
+ };
39
+
40
+ return {
41
+ "type": "object",
42
+ "description": "Calculates the skew of returns (avg profit vs. avg loss) for each asset.",
43
+ "patternProperties": {
44
+ "^.*$": tickerSchema // Ticker
45
+ },
46
+ "additionalProperties": tickerSchema
47
+ };
48
+ }
49
+
50
+ _initAsset(instrumentId) {
51
+ if (!this.assets.has(instrumentId)) {
52
+ this.assets.set(instrumentId, {
53
+ profit_sum: 0,
54
+ profit_count: 0,
55
+ loss_sum: 0, // Will be negative
56
+ loss_count: 0
57
+ });
58
+ }
59
+ }
29
60
 
30
- if (netProfit > 0) {
31
- this.skewData[instrumentId].profit_sum += netProfit;
32
- this.skewData[instrumentId].profitable_count++;
33
- } else if (netProfit < 0) {
34
- this.skewData[instrumentId].loss_sum += netProfit; // Keep it negative to sum up losses
35
- this.skewData[instrumentId].unprofitable_count++;
61
+ process(portfolioData) {
62
+ const positions = portfolioData.PublicPositions || portfolioData.AggregatedPositions;
63
+ if (!positions || !Array.isArray(positions)) {
64
+ return;
65
+ }
66
+
67
+ for (const pos of positions) {
68
+ const instrumentId = pos.InstrumentID;
69
+ if (!instrumentId) continue;
70
+
71
+ this._initAsset(instrumentId);
72
+ const assetData = this.assets.get(instrumentId);
73
+ const pnl = pos.NetProfit || 0;
74
+
75
+ if (pnl > 0) {
76
+ assetData.profit_sum += pnl;
77
+ assetData.profit_count++;
78
+ } else if (pnl < 0) {
79
+ assetData.loss_sum += pnl;
80
+ assetData.loss_count++;
36
81
  }
37
82
  }
38
83
  }
@@ -41,17 +86,27 @@ class ProfitabilitySkewPerStock {
41
86
  if (!this.mappings) {
42
87
  this.mappings = await loadInstrumentMappings();
43
88
  }
89
+
44
90
  const result = {};
45
- for (const instrumentId in this.skewData) {
46
- const ticker = this.mappings.instrumentToTicker[instrumentId] || instrumentId.toString();
47
- result[ticker] = this.skewData[instrumentId];
91
+ for (const [instrumentId, data] of this.assets.entries()) {
92
+ const ticker = this.mappings.instrumentToTicker[instrumentId] || `id_${instrumentId}`;
93
+
94
+ const avgProfit = (data.profit_count > 0) ? (data.profit_sum / data.profit_count) : 0;
95
+ const avgLoss = (data.loss_count > 0) ? (Math.abs(data.loss_sum) / data.loss_count) : 0;
96
+
97
+ result[ticker] = {
98
+ profit_sum: data.profit_sum,
99
+ profit_count: data.profit_count,
100
+ loss_sum: data.loss_sum,
101
+ loss_count: data.loss_count,
102
+ skew_ratio: (avgLoss > 0) ? (avgProfit / avgLoss) : null
103
+ };
48
104
  }
49
- if (Object.keys(result).length === 0) return {};
50
- return { profitability_skew_by_asset: result };
105
+ return result;
51
106
  }
52
107
 
53
108
  reset() {
54
- this.skewData = {};
109
+ this.assets.clear();
55
110
  this.mappings = null;
56
111
  }
57
112
  }
@@ -1,65 +1,65 @@
1
1
  /**
2
- * @fileoverview Counts the number of users whose overall portfolio is in profit vs. loss for the current day.
2
+ * @fileoverview Calculation (Pass 1) for overall profitable status.
3
+ *
4
+ * This metric answers: "How many users have an *overall portfolio*
5
+ * in profit versus in loss today?"
3
6
  */
4
-
5
- class DailyUserPnlStatus {
7
+ class ProfitableAndUnprofitableStatus {
6
8
  constructor() {
7
- // Initialize counters
8
- this.profitableUsers = 0;
9
- this.unprofitableUsers = 0;
9
+ this.total_in_profit = 0;
10
+ this.total_in_loss = 0;
10
11
  }
11
12
 
12
13
  /**
13
- * Processes a single user's portfolio for the current day.
14
- * @param {object} todayPortfolio - The portfolio data for the current day.
15
- * @param {object} yesterdayPortfolio - Not used in this calculation.
16
- * @param {string} userId - The user's ID.
17
- * @param {object} context - Shared context data (not used here).
14
+ * Defines the output schema for this calculation.
15
+ * @returns {object} JSON Schema object
18
16
  */
19
- process(todayPortfolio, yesterdayPortfolio, userId, context) {
20
- // Prefer AggregatedPositions as it typically contains NetProfit
21
- const positions = todayPortfolio?.AggregatedPositions || todayPortfolio?.PublicPositions;
22
-
23
- // Ensure we have portfolio data and positions
24
- if (!positions || !Array.isArray(positions) || positions.length === 0) {
25
- return; // Skip if no positions data for the user today
26
- }
27
-
28
- // Calculate the sum of NetProfit across all positions for the user
29
- let totalUserPnl = 0;
30
- for (const position of positions) {
31
- // Ensure NetProfit exists and is a number, default to 0 otherwise
32
- totalUserPnl += (typeof position.NetProfit === 'number' ? position.NetProfit : 0);
33
- }
17
+ static getSchema() {
18
+ return {
19
+ "type": "object",
20
+ "description": "Tracks the count of users whose *overall portfolio* is in profit vs. in loss.",
21
+ "properties": {
22
+ "total_in_profit": {
23
+ "type": "number",
24
+ "description": "Count of users with a total portfolio P&L > 0."
25
+ },
26
+ "total_in_loss": {
27
+ "type": "number",
28
+ "description": "Count of users with a total portfolio P&L < 0."
29
+ },
30
+ "profit_ratio_pct": {
31
+ "type": "number",
32
+ "description": "Percentage of users in profit (In Profit / Total)."
33
+ }
34
+ },
35
+ "required": ["total_in_profit", "total_in_loss", "profit_ratio_pct"]
36
+ };
37
+ }
34
38
 
35
- // Increment the appropriate counter based on the total P/L
36
- if (totalUserPnl > 0) {
37
- this.profitableUsers++;
38
- } else if (totalUserPnl < 0) {
39
- this.unprofitableUsers++;
39
+ process(portfolioData) {
40
+ // Use the P&L from the summary, which is for the *day*
41
+ const dailyPnl = portfolioData.Summary?.NetProfit || 0;
42
+
43
+ if (dailyPnl > 0) {
44
+ this.total_in_profit++;
45
+ } else if (dailyPnl < 0) {
46
+ this.total_in_loss++;
40
47
  }
41
- // Users with exactly zero P/L are ignored
42
48
  }
43
49
 
44
- /**
45
- * Returns the final aggregated counts.
46
- * @returns {object} Object containing the counts of profitable and unprofitable users.
47
- */
48
50
  getResult() {
49
- // Return the raw counts
51
+ const total = this.total_in_profit + this.total_in_loss;
50
52
  return {
51
- profitable_user_count: this.profitableUsers,
52
- unprofitable_user_count: this.unprofitableUsers
53
+ total_in_profit: this.total_in_profit,
54
+ total_in_loss: this.total_in_loss,
55
+ profit_ratio_pct: (total > 0) ? (this.total_in_profit / total) * 100 : 0
53
56
  };
54
57
  }
55
58
 
56
- /**
57
- * Resets the internal counters for the next processing run.
58
- */
59
59
  reset() {
60
- this.profitableUsers = 0;
61
- this.unprofitableUsers = 0;
60
+ this.total_in_profit = 0;
61
+ this.total_in_loss = 0;
62
62
  }
63
63
  }
64
64
 
65
- module.exports = DailyUserPnlStatus;
65
+ module.exports = ProfitableAndUnprofitableStatus;
@@ -7,12 +7,35 @@ class UsersProcessed {
7
7
  this.userCount = 0;
8
8
  }
9
9
 
10
+ /**
11
+ * Defines the output schema for this calculation.
12
+ * @returns {object} JSON Schema object
13
+ */
14
+ static getSchema() {
15
+ return {
16
+ "type": "object",
17
+ "description": "A simple counter for the total number of users processed.",
18
+ "properties": {
19
+ "rawTotalUsersProcessed": {
20
+ "type": "number",
21
+ "description": "The total count of users processed by the computation system."
22
+ }
23
+ },
24
+ "required": ["rawTotalUsersProcessed"]
25
+ };
26
+ }
27
+
10
28
  process(portfolioData, yesterdayPortfolio, userId, context) {
11
29
  this.userCount++;
12
30
  }
13
31
 
14
32
  getResult() {
15
- if (this.userCount === 0) return {};
33
+ if (this.userCount === 0) {
34
+ // Even if 0, we should return the defined schema shape
35
+ return {
36
+ rawTotalUsersProcessed: 0
37
+ };
38
+ }
16
39
  // FIX: Rename the key to use the 'raw' prefix for correct aggregation.
17
40
  return {
18
41
  rawTotalUsersProcessed: this.userCount
@@ -1,76 +1,138 @@
1
- // CORRECTED PATH: ../utils/ instead of ../../utils/
2
- const { getInstrumentSectorMap } = require('../../../utils/sector_mapping_provider');
3
-
4
1
  /**
5
- * Aggregates P/L by the number of unique sectors a user is invested in.
2
+ * @fileoverview Calculation (Pass 2) for P&L by diversification.
3
+ *
4
+ * This metric answers: "What is the average daily P&L for users,
5
+ * bucketed by the number of *unique sectors* they are invested in?"
6
6
  */
7
+ const { loadInstrumentMappings } = require('../../../utils/sector_mapping_provider');
8
+
7
9
  class DiversificationPnl {
8
10
  constructor() {
9
- this.pnl_by_sector_count = {};
10
- this.sectorMapping = null;
11
- }
12
-
13
- _initBucket(count) {
14
- if (!this.pnl_by_sector_count[count]) {
15
- this.pnl_by_sector_count[count] = { pnl_sum: 0, count: 0 };
16
- }
11
+ // { [bucket]: { pnl_sum: 0, user_count: 0 } }
12
+ this.buckets = {
13
+ '1': { pnl_sum: 0, user_count: 0 },
14
+ '2-3': { pnl_sum: 0, user_count: 0 },
15
+ '4-5': { pnl_sum: 0, user_count: 0 },
16
+ '6-10': { pnl_sum: 0, user_count: 0 },
17
+ '11+': { pnl_sum: 0, user_count: 0 },
18
+ };
19
+ this.mappings = null;
17
20
  }
18
21
 
19
22
  /**
20
- * FIX: Helper function to calculate total P&L from positions
21
- * @param {object} portfolio
22
- * @returns {number|null}
23
+ * Defines the output schema for this calculation.
24
+ * @returns {object} JSON Schema object
23
25
  */
24
- _calculateTotalPnl(portfolio) {
25
- const positions = portfolio?.AggregatedPositions || portfolio?.PublicPositions;
26
- if (positions && Array.isArray(positions)) {
27
- // Sum all NetProfit fields, defaulting to 0 if a position has no NetProfit
28
- return positions.reduce((sum, pos) => sum + (pos.NetProfit || 0), 0);
29
- }
26
+ static getSchema() {
27
+ const bucketSchema = {
28
+ "type": "object",
29
+ "description": "Aggregated P&L metrics for a sector diversification bucket.",
30
+ "properties": {
31
+ "average_daily_pnl": {
32
+ "type": "number",
33
+ "description": "The average daily P&L for users in this bucket."
34
+ },
35
+ "user_count": {
36
+ "type": "number",
37
+ "description": "The number of users in this bucket."
38
+ },
39
+ "pnl_sum": {
40
+ "type": "number",
41
+ "description": "The sum of all P&L for users in this bucket."
42
+ }
43
+ },
44
+ "required": ["average_daily_pnl", "user_count", "pnl_sum"]
45
+ };
46
+
47
+ return {
48
+ "type": "object",
49
+ "description": "Average daily P&L bucketed by the number of unique sectors a user is invested in.",
50
+ "properties": {
51
+ "1": bucketSchema,
52
+ "2-3": bucketSchema,
53
+ "4-5": bucketSchema,
54
+ "6-10": bucketSchema,
55
+ "11+": bucketSchema
56
+ },
57
+ "required": ["1", "2-3", "4-5", "6-10", "11+"]
58
+ };
59
+ }
60
+
61
+ _getBucket(count) {
62
+ if (count === 1) return '1';
63
+ if (count >= 2 && count <= 3) return '2-3';
64
+ if (count >= 4 && count <= 5) return '4-5';
65
+ if (count >= 6 && count <= 10) return '6-10';
66
+ if (count >= 11) return '11+';
30
67
  return null;
31
68
  }
32
69
 
33
- async process(todayPortfolio, yesterdayPortfolio, userId) { // Added async
34
- // FIX: Only need todayPortfolio for this logic
70
+ process(todayPortfolio, yesterdayPortfolio, userId, context) {
71
+ // This calculation only needs today's portfolio state
35
72
  if (!todayPortfolio) {
36
73
  return;
37
74
  }
38
-
39
- if(!this.sectorMapping) {
40
- this.sectorMapping = await getInstrumentSectorMap();
75
+
76
+ if (!this.mappings) {
77
+ // Context contains the mappings loaded in Pass 1
78
+ this.mappings = context.mappings;
41
79
  }
42
80
 
43
81
  const positions = todayPortfolio.AggregatedPositions || todayPortfolio.PublicPositions;
44
-
45
- if (!positions || !Array.isArray(positions)) {
82
+ if (!positions || !Array.isArray(positions) || !this.mappings) {
46
83
  return;
47
84
  }
48
-
85
+
86
+ // Find unique sectors for this user
49
87
  const uniqueSectors = new Set();
50
- for (const position of positions) {
51
- const sector = this.sectorMapping[position.InstrumentID] || 'Other';
52
- uniqueSectors.add(sector);
88
+ for (const pos of positions) {
89
+ const instrumentId = pos.InstrumentID;
90
+ if (instrumentId) {
91
+ const sector = this.mappings.instrumentToSector[instrumentId] || 'Other';
92
+ uniqueSectors.add(sector);
93
+ }
53
94
  }
54
-
95
+
55
96
  const sectorCount = uniqueSectors.size;
56
97
  if (sectorCount === 0) {
57
98
  return;
58
99
  }
59
100
 
60
- // FIX: Calculate dailyPnl by summing NetProfit from all positions
61
- const dailyPnl = this._calculateTotalPnl(todayPortfolio);
62
-
63
- if (dailyPnl === null) {
64
- return; // Cannot calculate P&L for this user
101
+ const bucketKey = this._getBucket(sectorCount);
102
+ if (!bucketKey) {
103
+ return;
65
104
  }
66
105
 
67
- this._initBucket(sectorCount);
68
- this.pnl_by_sector_count[sectorCount].pnl_sum += dailyPnl;
69
- this.pnl_by_sector_count[sectorCount].count++;
106
+ // Use the P&L from the summary, which is for the *day*
107
+ const dailyPnl = todayPortfolio.Summary?.NetProfit || 0;
108
+
109
+ const bucket = this.buckets[bucketKey];
110
+ bucket.pnl_sum += dailyPnl;
111
+ bucket.user_count++;
70
112
  }
71
113
 
72
114
  getResult() {
73
- return this.pnl_by_sector_count;
115
+ const result = {};
116
+ for (const key in this.buckets) {
117
+ const bucket = this.buckets[key];
118
+ result[key] = {
119
+ average_daily_pnl: (bucket.user_count > 0) ? (bucket.pnl_sum / bucket.user_count) : 0,
120
+ user_count: bucket.user_count,
121
+ pnl_sum: bucket.pnl_sum
122
+ };
123
+ }
124
+ return result;
125
+ }
126
+
127
+ reset() {
128
+ this.buckets = {
129
+ '1': { pnl_sum: 0, user_count: 0 },
130
+ '2-3': { pnl_sum: 0, user_count: 0 },
131
+ '4-5': { pnl_sum: 0, user_count: 0 },
132
+ '6-10': { pnl_sum: 0, user_count: 0 },
133
+ '11+': { pnl_sum: 0, user_count: 0 },
134
+ };
135
+ this.mappings = null;
74
136
  }
75
137
  }
76
138
 
@@ -1,68 +1,117 @@
1
1
  /**
2
- * @fileoverview Analyzes sector rotation by comparing investment amounts between two days.
2
+ * @fileoverview Calculation (Pass 2) for sector rotation.
3
+ *
4
+ * This metric answers: "What is the net change in investment
5
+ * (as a portfolio percentage) for each sector today?"
6
+ *
7
+ * This measures which sectors the crowd is rotating into or out of.
3
8
  */
4
- // CORRECTED PATH: ../utils/ instead of ../../utils/
5
- const { getInstrumentSectorMap } = require('../../../utils/sector_mapping_provider');
9
+ const { loadInstrumentMappings } = require('../../../utils/sector_mapping_provider');
6
10
 
7
11
  class SectorRotation {
8
- // ... (rest of the code is unchanged) ...
9
12
  constructor() {
10
- this.todaySectorInvestment = {};
11
- this.yesterdaySectorInvestment = {};
12
- this.sectorMap = null;
13
+ // We will store { [sector]: { y_invested_sum: 0, t_invested_sum: 0 } }
14
+ this.sectors = new Map();
15
+ this.mappings = null;
13
16
  }
14
17
 
15
- async process(todayPortfolio, yesterdayPortfolio, userId) {
16
- if (!this.sectorMap) {
17
- this.sectorMap = await getInstrumentSectorMap();
18
- }
18
+ /**
19
+ * Defines the output schema for this calculation.
20
+ * @returns {object} JSON Schema object
21
+ */
22
+ static getSchema() {
23
+ const sectorSchema = {
24
+ "type": "object",
25
+ "description": "Net change in portfolio allocation for a specific sector.",
26
+ "properties": {
27
+ "net_change_pct": {
28
+ "type": "number",
29
+ "description": "The net change in percentage allocation (Today Sum - Yesterday Sum)."
30
+ },
31
+ "y_total_invested_pct": {
32
+ "type": "number",
33
+ "description": "Total portfolio percentage invested in this sector yesterday."
34
+ },
35
+ "t_total_invested_pct": {
36
+ "type": "number",
37
+ "description": "Total portfolio percentage invested in this sector today."
38
+ }
39
+ },
40
+ "required": ["net_change_pct", "y_total_invested_pct", "t_total_invested_pct"]
41
+ };
42
+
43
+ return {
44
+ "type": "object",
45
+ "description": "Calculates the net rotation of portfolio percentage allocation between sectors.",
46
+ "patternProperties": {
47
+ "^.*$": sectorSchema // Sector name
48
+ },
49
+ "additionalProperties": sectorSchema
50
+ };
51
+ }
19
52
 
20
- if (todayPortfolio) {
21
- this.accumulateSectorInvestment(todayPortfolio, this.todaySectorInvestment);
22
- }
23
- if (yesterdayPortfolio) {
24
- this.accumulateSectorInvestment(yesterdayPortfolio, this.yesterdaySectorInvestment);
53
+ _initSector(sector) {
54
+ if (!this.sectors.has(sector)) {
55
+ this.sectors.set(sector, {
56
+ y_invested_sum: 0,
57
+ t_invested_sum: 0
58
+ });
25
59
  }
26
60
  }
27
61
 
28
- accumulateSectorInvestment(portfolio, target) {
29
- // Use AggregatedPositions, as PublicPositions don't have Invested amount
30
- if (portfolio && portfolio.AggregatedPositions) {
31
- for (const pos of portfolio.AggregatedPositions) {
32
- // Check if sectorMap is loaded before accessing it
33
- if (!this.sectorMap) {
34
- console.warn('Sector map not loaded in accumulateSectorInvestment');
35
- continue; // or handle appropriately
36
- }
37
- const sector = this.sectorMap[pos.InstrumentID] || 'N/A';
38
- if (!target[sector]) {
39
- target[sector] = 0;
40
- }
41
- // Use Invested amount if available, otherwise fallback might be needed
42
- target[sector] += (pos.Invested || pos.Amount || 0); // Added fallback
62
+ _sumSectorAllocations(portfolio, mapKey) {
63
+ const positions = portfolio?.AggregatedPositions; // Must use Aggregated for 'Invested' %
64
+ if (!positions || !Array.isArray(positions) || !this.mappings) {
65
+ return;
66
+ }
67
+
68
+ for (const pos of positions) {
69
+ const instrumentId = pos.InstrumentID;
70
+ if (instrumentId) {
71
+ const sector = this.mappings.instrumentToSector[instrumentId] || 'Other';
72
+ this._initSector(sector);
73
+
74
+ const investedPct = pos.InvestedAmount || pos.Invested || 0;
75
+ this.sectors.get(sector)[mapKey] += investedPct;
43
76
  }
44
77
  }
45
- // Optional: Handle PublicPositions if needed, though they lack 'Invested'
46
- else if (portfolio && portfolio.PublicPositions) {
47
- // Logic to handle PublicPositions if necessary, e.g., using 'Amount'
48
- // console.warn("Using PublicPositions for sector rotation, 'Invested' amount missing.");
49
- }
50
78
  }
51
79
 
80
+ process(todayPortfolio, yesterdayPortfolio, userId, context) {
81
+ if (!todayPortfolio || !yesterdayPortfolio) {
82
+ return;
83
+ }
52
84
 
53
- getResult() {
54
- const sectorRotationData = {};
55
- const allSectors = new Set([...Object.keys(this.todaySectorInvestment), ...Object.keys(this.yesterdaySectorInvestment)]);
85
+ if (!this.mappings) {
86
+ // Context contains the mappings loaded in Pass 1
87
+ this.mappings = context.mappings;
88
+ }
89
+
90
+ this._sumSectorAllocations(yesterdayPortfolio, 'y_invested_sum');
91
+ this._sumSectorAllocations(todayPortfolio, 't_invested_sum');
92
+ }
56
93
 
57
- for (const sector of allSectors) {
58
- const todayAmount = this.todaySectorInvestment[sector] || 0;
59
- const yesterdayAmount = this.yesterdaySectorInvestment[sector] || 0;
60
- sectorRotationData[sector] = todayAmount - yesterdayAmount;
94
+ getResult() {
95
+ const result = {};
96
+ for (const [sector, data] of this.sectors.entries()) {
97
+ const netChange = data.t_invested_sum - data.y_invested_sum;
98
+
99
+ // Only report sectors that had some activity
100
+ if (data.t_invested_sum > 0 || data.y_invested_sum > 0) {
101
+ result[sector] = {
102
+ net_change_pct: netChange,
103
+ y_total_invested_pct: data.y_invested_sum,
104
+ t_total_invested_pct: data.t_invested_sum
105
+ };
106
+ }
61
107
  }
108
+ return result;
109
+ }
62
110
 
63
- return { sector_rotation: sectorRotationData };
111
+ reset() {
112
+ this.sectors.clear();
113
+ this.mappings = null;
64
114
  }
65
115
  }
66
116
 
67
-
68
117
  module.exports = SectorRotation;