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,58 +1,58 @@
1
- /**
2
- * @fileoverview Calculates the migration of users between profitability states.
3
- */
4
-
5
- class ProfitabilityMigration {
6
- constructor() {
7
- this.profitableToUnprofitable = 0;
8
- this.unprofitableToProfitable = 0;
9
- this.remainedProfitable = 0;
10
- this.remainedUnprofitable = 0;
11
- }
12
-
13
- process(todayPortfolio, yesterdayPortfolio, userId) {
14
- if (!todayPortfolio || !yesterdayPortfolio) {
15
- return;
16
- }
17
-
18
- const todayProfit = this.calculateTotalPnl(todayPortfolio);
19
- const yesterdayProfit = this.calculateTotalPnl(yesterdayPortfolio);
20
-
21
- if (todayProfit === null || yesterdayProfit === null) {
22
- return;
23
- }
24
-
25
- const wasProfitable = yesterdayProfit > 0;
26
- const isProfitable = todayProfit > 0;
27
-
28
- if (wasProfitable && !isProfitable) {
29
- this.profitableToUnprofitable++;
30
- } else if (!wasProfitable && isProfitable) {
31
- this.unprofitableToProfitable++;
32
- } else if (wasProfitable && isProfitable) {
33
- this.remainedProfitable++;
34
- } else {
35
- this.remainedUnprofitable++;
36
- }
37
- }
38
-
39
- calculateTotalPnl(portfolio) {
40
- if (portfolio && portfolio.AggregatedPositions) {
41
- return portfolio.AggregatedPositions.reduce((sum, pos) => sum + pos.NetProfit, 0);
42
- } else if (portfolio && portfolio.PublicPositions) {
43
- return portfolio.PublicPositions.reduce((sum, pos) => sum + pos.NetProfit, 0);
44
- }
45
- return null;
46
- }
47
-
48
- getResult() {
49
- return {
50
- profitable_to_unprofitable: this.profitableToUnprofitable,
51
- unprofitable_to_profitable: this.unprofitableToProfitable,
52
- remained_profitable: this.remainedProfitable,
53
- remained_unprofitable: this.remainedUnprofitable,
54
- };
55
- }
56
- }
57
-
1
+ /**
2
+ * @fileoverview Calculates the migration of users between profitability states.
3
+ */
4
+
5
+ class ProfitabilityMigration {
6
+ constructor() {
7
+ this.profitableToUnprofitable = 0;
8
+ this.unprofitableToProfitable = 0;
9
+ this.remainedProfitable = 0;
10
+ this.remainedUnprofitable = 0;
11
+ }
12
+
13
+ process(todayPortfolio, yesterdayPortfolio, userId) {
14
+ if (!todayPortfolio || !yesterdayPortfolio) {
15
+ return;
16
+ }
17
+
18
+ const todayProfit = this.calculateTotalPnl(todayPortfolio);
19
+ const yesterdayProfit = this.calculateTotalPnl(yesterdayPortfolio);
20
+
21
+ if (todayProfit === null || yesterdayProfit === null) {
22
+ return;
23
+ }
24
+
25
+ const wasProfitable = yesterdayProfit > 0;
26
+ const isProfitable = todayProfit > 0;
27
+
28
+ if (wasProfitable && !isProfitable) {
29
+ this.profitableToUnprofitable++;
30
+ } else if (!wasProfitable && isProfitable) {
31
+ this.unprofitableToProfitable++;
32
+ } else if (wasProfitable && isProfitable) {
33
+ this.remainedProfitable++;
34
+ } else {
35
+ this.remainedUnprofitable++;
36
+ }
37
+ }
38
+
39
+ calculateTotalPnl(portfolio) {
40
+ if (portfolio && portfolio.AggregatedPositions) {
41
+ return portfolio.AggregatedPositions.reduce((sum, pos) => sum + pos.NetProfit, 0);
42
+ } else if (portfolio && portfolio.PublicPositions) {
43
+ return portfolio.PublicPositions.reduce((sum, pos) => sum + pos.NetProfit, 0);
44
+ }
45
+ return null;
46
+ }
47
+
48
+ getResult() {
49
+ return {
50
+ profitable_to_unprofitable: this.profitableToUnprofitable,
51
+ unprofitable_to_profitable: this.unprofitableToProfitable,
52
+ remained_profitable: this.remainedProfitable,
53
+ remained_unprofitable: this.remainedUnprofitable,
54
+ };
55
+ }
56
+ }
57
+
58
58
  module.exports = ProfitabilityMigration;
@@ -1,118 +1,118 @@
1
- /**
2
- * @fileoverview Tracks user profitability over a 7-day rolling window.
3
- * This version shards the output AND calculates the user's *weighted average daily PNL (as a decimal %)*.
4
- */
5
-
6
- const { Firestore } = require('@google-cloud/firestore');
7
- const firestore = new Firestore();
8
-
9
- const NUM_SHARDS = 50;
10
-
11
- class UserProfitabilityTracker {
12
- constructor() {
13
- // This will store { userId: { weightedPnlSum: 0, totalInvested: 0 } }
14
- this.dailyData = {};
15
- }
16
-
17
- /**
18
- * Calculates the weighted PNL for the day.
19
- * NetProfit is a decimal % return (e.g., 0.03)
20
- * Invested is a decimal % weight (e.g., 0.05)
21
- */
22
- calculateWeightedDailyPnl(portfolio) {
23
- if (!portfolio || !portfolio.AggregatedPositions || portfolio.AggregatedPositions.length === 0) {
24
- return { weightedPnl: 0, totalInvested: 0 };
25
- }
26
-
27
- let weightedPnlSum = 0;
28
- let totalInvested = 0;
29
-
30
- for (const pos of portfolio.AggregatedPositions) {
31
- // Use NetProfit (the % return)
32
- const netProfit = ('NetProfit' in pos) ? pos.NetProfit : (pos.ProfitAndLoss || 0);
33
- // Use InvestedAmount (the % portfolio weight)
34
- const invested = pos.InvestedAmount || pos.Amount || 0;
35
-
36
- if (invested > 0) {
37
- weightedPnlSum += netProfit * invested;
38
- totalInvested += invested;
39
- }
40
- }
41
-
42
- return { weightedPnlSum, totalInvested };
43
- }
44
-
45
- process(todayPortfolio, yesterdayPortfolio, userId) {
46
- if (!todayPortfolio) return;
47
-
48
- const { weightedPnlSum, totalInvested } = this.calculateWeightedDailyPnl(todayPortfolio);
49
-
50
- if (totalInvested > 0) {
51
- this.dailyData[userId] = { weightedPnlSum, totalInvested };
52
- }
53
- }
54
-
55
- async getResult() {
56
- // --- START MODIFICATION ---
57
- // If no data was processed (e.g., no portfolios found for the day),
58
- // return null to allow the backfill to retry.
59
- if (Object.keys(this.dailyData).length === 0) {
60
- console.warn('[UserProfitabilityTracker] No daily data was processed. Returning null for backfill.');
61
- return null;
62
- }
63
- // --- END MODIFICATION ---
64
-
65
- const today = new Date().toISOString().slice(0, 10);
66
- const results = {}; // For sharded history
67
- const dailyPnlMap = {}; // For the new profile calc
68
-
69
- // Prepare sharded data structure
70
- for (let i = 0; i < NUM_SHARDS; i++) {
71
- results[`user_profitability_shard_${i}`] = {};
72
- }
73
-
74
- // ... (Fetch existing shards logic, same as your file) ...
75
- const shardPromises = [];
76
- for (let i = 0; i < NUM_SHARDS; i++) {
77
- const docRef = firestore.collection('historical_insights').doc(`user_profitability_shard_${i}`);
78
- shardPromises.push(docRef.get());
79
- }
80
- const shardSnapshots = await Promise.all(shardPromises);
81
- const existingData = shardSnapshots.map(snap => (snap.exists ? snap.data().profits : {}));
82
-
83
-
84
- for (const userId in this.dailyData) {
85
- const { weightedPnlSum, totalInvested } = this.dailyData[userId];
86
-
87
- // Calculate the final weighted average % return for the day
88
- // We cap totalInvested at 1.0 (100%) in case of data issues
89
- const totalWeight = Math.min(1.0, totalInvested);
90
- const dailyAvgPnl = (totalWeight > 0) ? (weightedPnlSum / totalWeight) : 0;
91
-
92
- // Store this for the profile calc dependency
93
- dailyPnlMap[userId] = dailyAvgPnl;
94
-
95
- // --- Now, update the sharded history ---
96
- const shardIndex = parseInt(userId, 10) % NUM_SHARDS;
97
- const userHistory = existingData[shardIndex][userId] || [];
98
-
99
- // Store the decimal % pnl in the history
100
- userHistory.push({ date: today, pnl: dailyAvgPnl });
101
-
102
- const shardKey = `user_profitability_shard_${shardIndex}`;
103
- if (!results[shardKey]) results[shardKey] = {};
104
- results[shardKey][userId] = userHistory.slice(-7);
105
- }
106
-
107
- return {
108
- sharded_user_profitability: results,
109
- daily_pnl_map: dailyPnlMap // <-- This now correctly outputs the weighted avg % PNL
110
- };
111
- }
112
-
113
- reset() {
114
- this.dailyData = {};
115
- }
116
- }
117
-
1
+ /**
2
+ * @fileoverview Tracks user profitability over a 7-day rolling window.
3
+ * This version shards the output AND calculates the user's *weighted average daily PNL (as a decimal %)*.
4
+ */
5
+
6
+ const { Firestore } = require('@google-cloud/firestore');
7
+ const firestore = new Firestore();
8
+
9
+ const NUM_SHARDS = 50;
10
+
11
+ class UserProfitabilityTracker {
12
+ constructor() {
13
+ // This will store { userId: { weightedPnlSum: 0, totalInvested: 0 } }
14
+ this.dailyData = {};
15
+ }
16
+
17
+ /**
18
+ * Calculates the weighted PNL for the day.
19
+ * NetProfit is a decimal % return (e.g., 0.03)
20
+ * Invested is a decimal % weight (e.g., 0.05)
21
+ */
22
+ calculateWeightedDailyPnl(portfolio) {
23
+ if (!portfolio || !portfolio.AggregatedPositions || portfolio.AggregatedPositions.length === 0) {
24
+ return { weightedPnl: 0, totalInvested: 0 };
25
+ }
26
+
27
+ let weightedPnlSum = 0;
28
+ let totalInvested = 0;
29
+
30
+ for (const pos of portfolio.AggregatedPositions) {
31
+ // Use NetProfit (the % return)
32
+ const netProfit = ('NetProfit' in pos) ? pos.NetProfit : (pos.ProfitAndLoss || 0);
33
+ // Use InvestedAmount (the % portfolio weight)
34
+ const invested = pos.InvestedAmount || pos.Amount || 0;
35
+
36
+ if (invested > 0) {
37
+ weightedPnlSum += netProfit * invested;
38
+ totalInvested += invested;
39
+ }
40
+ }
41
+
42
+ return { weightedPnlSum, totalInvested };
43
+ }
44
+
45
+ process(todayPortfolio, yesterdayPortfolio, userId) {
46
+ if (!todayPortfolio) return;
47
+
48
+ const { weightedPnlSum, totalInvested } = this.calculateWeightedDailyPnl(todayPortfolio);
49
+
50
+ if (totalInvested > 0) {
51
+ this.dailyData[userId] = { weightedPnlSum, totalInvested };
52
+ }
53
+ }
54
+
55
+ async getResult() {
56
+ // --- START MODIFICATION ---
57
+ // If no data was processed (e.g., no portfolios found for the day),
58
+ // return null to allow the backfill to retry.
59
+ if (Object.keys(this.dailyData).length === 0) {
60
+ console.warn('[UserProfitabilityTracker] No daily data was processed. Returning null for backfill.');
61
+ return null;
62
+ }
63
+ // --- END MODIFICATION ---
64
+
65
+ const today = new Date().toISOString().slice(0, 10);
66
+ const results = {}; // For sharded history
67
+ const dailyPnlMap = {}; // For the new profile calc
68
+
69
+ // Prepare sharded data structure
70
+ for (let i = 0; i < NUM_SHARDS; i++) {
71
+ results[`user_profitability_shard_${i}`] = {};
72
+ }
73
+
74
+ // ... (Fetch existing shards logic, same as your file) ...
75
+ const shardPromises = [];
76
+ for (let i = 0; i < NUM_SHARDS; i++) {
77
+ const docRef = firestore.collection('historical_insights').doc(`user_profitability_shard_${i}`);
78
+ shardPromises.push(docRef.get());
79
+ }
80
+ const shardSnapshots = await Promise.all(shardPromises);
81
+ const existingData = shardSnapshots.map(snap => (snap.exists ? snap.data().profits : {}));
82
+
83
+
84
+ for (const userId in this.dailyData) {
85
+ const { weightedPnlSum, totalInvested } = this.dailyData[userId];
86
+
87
+ // Calculate the final weighted average % return for the day
88
+ // We cap totalInvested at 1.0 (100%) in case of data issues
89
+ const totalWeight = Math.min(1.0, totalInvested);
90
+ const dailyAvgPnl = (totalWeight > 0) ? (weightedPnlSum / totalWeight) : 0;
91
+
92
+ // Store this for the profile calc dependency
93
+ dailyPnlMap[userId] = dailyAvgPnl;
94
+
95
+ // --- Now, update the sharded history ---
96
+ const shardIndex = parseInt(userId, 10) % NUM_SHARDS;
97
+ const userHistory = existingData[shardIndex][userId] || [];
98
+
99
+ // Store the decimal % pnl in the history
100
+ userHistory.push({ date: today, pnl: dailyAvgPnl });
101
+
102
+ const shardKey = `user_profitability_shard_${shardIndex}`;
103
+ if (!results[shardKey]) results[shardKey] = {};
104
+ results[shardKey][userId] = userHistory.slice(-7);
105
+ }
106
+
107
+ return {
108
+ sharded_user_profitability: results,
109
+ daily_pnl_map: dailyPnlMap // <-- This now correctly outputs the weighted avg % PNL
110
+ };
111
+ }
112
+
113
+ reset() {
114
+ this.dailyData = {};
115
+ }
116
+ }
117
+
118
118
  module.exports = UserProfitabilityTracker;
@@ -1,65 +1,65 @@
1
- /**
2
- * @fileoverview Counts the number of users whose overall portfolio is in profit vs. loss for the current day.
3
- */
4
-
5
- class DailyUserPnlStatus {
6
- constructor() {
7
- // Initialize counters
8
- this.profitableUsers = 0;
9
- this.unprofitableUsers = 0;
10
- }
11
-
12
- /**
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).
18
- */
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
- }
34
-
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++;
40
- }
41
- // Users with exactly zero P/L are ignored
42
- }
43
-
44
- /**
45
- * Returns the final aggregated counts.
46
- * @returns {object} Object containing the counts of profitable and unprofitable users.
47
- */
48
- getResult() {
49
- // Return the raw counts
50
- return {
51
- profitable_user_count: this.profitableUsers,
52
- unprofitable_user_count: this.unprofitableUsers
53
- };
54
- }
55
-
56
- /**
57
- * Resets the internal counters for the next processing run.
58
- */
59
- reset() {
60
- this.profitableUsers = 0;
61
- this.unprofitableUsers = 0;
62
- }
63
- }
64
-
1
+ /**
2
+ * @fileoverview Counts the number of users whose overall portfolio is in profit vs. loss for the current day.
3
+ */
4
+
5
+ class DailyUserPnlStatus {
6
+ constructor() {
7
+ // Initialize counters
8
+ this.profitableUsers = 0;
9
+ this.unprofitableUsers = 0;
10
+ }
11
+
12
+ /**
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).
18
+ */
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
+ }
34
+
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++;
40
+ }
41
+ // Users with exactly zero P/L are ignored
42
+ }
43
+
44
+ /**
45
+ * Returns the final aggregated counts.
46
+ * @returns {object} Object containing the counts of profitable and unprofitable users.
47
+ */
48
+ getResult() {
49
+ // Return the raw counts
50
+ return {
51
+ profitable_user_count: this.profitableUsers,
52
+ unprofitable_user_count: this.unprofitableUsers
53
+ };
54
+ }
55
+
56
+ /**
57
+ * Resets the internal counters for the next processing run.
58
+ */
59
+ reset() {
60
+ this.profitableUsers = 0;
61
+ this.unprofitableUsers = 0;
62
+ }
63
+ }
64
+
65
65
  module.exports = DailyUserPnlStatus;
@@ -1,77 +1,77 @@
1
- // CORRECTED PATH: ../utils/ instead of ../../utils/
2
- const { getInstrumentSectorMap } = require('../../../utils/sector_mapping_provider');
3
-
4
- /**
5
- * Aggregates P/L by the number of unique sectors a user is invested in.
6
- */
7
- class DiversificationPnl {
8
- 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
- }
17
- }
18
-
19
- /**
20
- * FIX: Helper function to calculate total P&L from positions
21
- * @param {object} portfolio
22
- * @returns {number|null}
23
- */
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
- }
30
- return null;
31
- }
32
-
33
- async process(todayPortfolio, yesterdayPortfolio, userId) { // Added async
34
- // FIX: Only need todayPortfolio for this logic
35
- if (!todayPortfolio) {
36
- return;
37
- }
38
-
39
- if(!this.sectorMapping) {
40
- this.sectorMapping = await getInstrumentSectorMap();
41
- }
42
-
43
- const positions = todayPortfolio.AggregatedPositions || todayPortfolio.PublicPositions;
44
-
45
- if (!positions || !Array.isArray(positions)) {
46
- return;
47
- }
48
-
49
- const uniqueSectors = new Set();
50
- for (const position of positions) {
51
- const sector = this.sectorMapping[position.InstrumentID] || 'Other';
52
- uniqueSectors.add(sector);
53
- }
54
-
55
- const sectorCount = uniqueSectors.size;
56
- if (sectorCount === 0) {
57
- return;
58
- }
59
-
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
65
- }
66
-
67
- this._initBucket(sectorCount);
68
- this.pnl_by_sector_count[sectorCount].pnl_sum += dailyPnl;
69
- this.pnl_by_sector_count[sectorCount].count++;
70
- }
71
-
72
- getResult() {
73
- return this.pnl_by_sector_count;
74
- }
75
- }
76
-
1
+ // CORRECTED PATH: ../utils/ instead of ../../utils/
2
+ const { getInstrumentSectorMap } = require('../../../utils/sector_mapping_provider');
3
+
4
+ /**
5
+ * Aggregates P/L by the number of unique sectors a user is invested in.
6
+ */
7
+ class DiversificationPnl {
8
+ 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
+ }
17
+ }
18
+
19
+ /**
20
+ * FIX: Helper function to calculate total P&L from positions
21
+ * @param {object} portfolio
22
+ * @returns {number|null}
23
+ */
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
+ }
30
+ return null;
31
+ }
32
+
33
+ async process(todayPortfolio, yesterdayPortfolio, userId) { // Added async
34
+ // FIX: Only need todayPortfolio for this logic
35
+ if (!todayPortfolio) {
36
+ return;
37
+ }
38
+
39
+ if(!this.sectorMapping) {
40
+ this.sectorMapping = await getInstrumentSectorMap();
41
+ }
42
+
43
+ const positions = todayPortfolio.AggregatedPositions || todayPortfolio.PublicPositions;
44
+
45
+ if (!positions || !Array.isArray(positions)) {
46
+ return;
47
+ }
48
+
49
+ const uniqueSectors = new Set();
50
+ for (const position of positions) {
51
+ const sector = this.sectorMapping[position.InstrumentID] || 'Other';
52
+ uniqueSectors.add(sector);
53
+ }
54
+
55
+ const sectorCount = uniqueSectors.size;
56
+ if (sectorCount === 0) {
57
+ return;
58
+ }
59
+
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
65
+ }
66
+
67
+ this._initBucket(sectorCount);
68
+ this.pnl_by_sector_count[sectorCount].pnl_sum += dailyPnl;
69
+ this.pnl_by_sector_count[sectorCount].count++;
70
+ }
71
+
72
+ getResult() {
73
+ return this.pnl_by_sector_count;
74
+ }
75
+ }
76
+
77
77
  module.exports = DiversificationPnl;