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,100 +1,195 @@
1
- const { loadAllPriceData, getDailyPriceChange } = require('../../../utils/price_data_provider');
2
- const { loadInstrumentMappings } = require('../../../utils/sector_mapping_provider');
3
-
4
1
  /**
5
- * @fileoverview Calculates "Net Crowd Flow" for each asset, BUT
6
- * *only* for the cohort of users who are currently IN PROFIT
7
- * on their positions for that asset.
2
+ * @fileoverview Calculation (Pass 3) for "In Profit" cohort asset flow.
3
+ *
4
+ * This metric calculates the "Net Crowd Flow Percentage" for each asset,
5
+ * but *only* for the cohort of users who are currently *in profit*
6
+ * on that specific asset.
7
+ *
8
+ * This helps identify if winners are taking profit or adding to positions.
9
+ *
10
+ * This calculation *depends* on 'asset_pnl_status' to identify the cohort.
8
11
  */
12
+ const { loadInstrumentMappings } = require('../../../utils/sector_mapping_provider');
13
+
9
14
  class InProfitAssetCrowdFlow {
10
15
  constructor() {
11
- this.asset_values = {}; // Stores { day1_value_sum: 0, day2_value_sum: 0 }
12
- this.user_count = 0;
13
- this.priceMap = null;
16
+ this.assetData = new Map();
14
17
  this.mappings = null;
15
- this.dates = {};
18
+ this.inProfitCohorts = null; // Map<ticker, Set<userId>>
19
+ }
20
+
21
+ /**
22
+ * Defines the output schema for this calculation.
23
+ * @returns {object} JSON Schema object
24
+ */
25
+ static getSchema() {
26
+ return {
27
+ "type": "object",
28
+ "description": "Calculates net capital flow % (price-adjusted) per asset, but only for the cohort of users currently in profit on that asset.",
29
+ "patternProperties": {
30
+ // Ticker
31
+ "^.*$": {
32
+ "type": "object",
33
+ "description": "Net flow metrics for a specific asset ticker from its 'in profit' cohort.",
34
+ "properties": {
35
+ "net_flow_percentage": {
36
+ "type": "number",
37
+ "description": "Net capital flow % from the 'in profit' cohort, adjusted for price changes."
38
+ },
39
+ "total_invested_today": { "type": "number" },
40
+ "total_invested_yesterday": { "type": "number" },
41
+ "cohort_size": { "type": "number" }
42
+ },
43
+ "required": ["net_flow_percentage", "total_invested_today", "total_invested_yesterday", "cohort_size"]
44
+ }
45
+ },
46
+ "additionalProperties": {
47
+ "type": "object",
48
+ "properties": {
49
+ "net_flow_percentage": { "type": "number" },
50
+ "total_invested_today": { "type": "number" },
51
+ "total_invested_yesterday": { "type": "number" },
52
+ "cohort_size": { "type": "number" }
53
+ }
54
+ }
55
+ };
56
+ }
57
+
58
+ /**
59
+ * Statically declare dependencies.
60
+ */
61
+ static getDependencies() {
62
+ return ['asset_pnl_status'];
63
+ }
64
+
65
+ _getPortfolioPositions(portfolio) {
66
+ return portfolio?.PublicPositions || portfolio?.AggregatedPositions;
16
67
  }
17
68
 
18
69
  _initAsset(instrumentId) {
19
- if (!this.asset_values[instrumentId]) {
20
- this.asset_values[instrumentId] = { day1_value_sum: 0, day2_value_sum: 0 };
70
+ if (!this.assetData.has(instrumentId)) {
71
+ this.assetData.set(instrumentId, {
72
+ total_invested_yesterday: 0,
73
+ total_invested_today: 0,
74
+ price_change_yesterday: 0,
75
+ cohort: new Set()
76
+ });
21
77
  }
22
78
  }
23
79
 
24
- process(todayPortfolio, yesterdayPortfolio, userId, context) {
25
- if (!todayPortfolio || !yesterdayPortfolio || !todayPortfolio.AggregatedPositions || !yesterdayPortfolio.AggregatedPositions) {
26
- return;
80
+ /**
81
+ * Helper to get the cohort data from the dependency.
82
+ */
83
+ _getInProfitCohorts(fetchedDependencies) {
84
+ if (this.inProfitCohorts) {
85
+ return this.inProfitCohorts;
86
+ }
87
+
88
+ const pnlStatusData = fetchedDependencies['asset_pnl_status'];
89
+ if (!pnlStatusData) {
90
+ return new Map();
91
+ }
92
+
93
+ // Re-structure the data for efficient lookup
94
+ // Map<ticker, Set<userId>>
95
+ this.inProfitCohorts = new Map();
96
+ for (const [ticker, data] of Object.entries(pnlStatusData)) {
97
+ const userSet = new Set(data.users_in_profit.map(u => u.userId));
98
+ this.inProfitCohorts.set(ticker, userSet);
27
99
  }
100
+ return this.inProfitCohorts;
101
+ }
28
102
 
29
- if (!this.dates.today && context.todayDateStr && context.yesterdayDateStr) {
30
- this.dates.today = context.todayDateStr;
31
- this.dates.yesterday = context.yesterdayDateStr;
103
+ process(todayPortfolio, yesterdayPortfolio, userId, context, todayInsights, yesterdayInsights, fetchedDependencies) {
104
+ if (!todayPortfolio || !yesterdayPortfolio) {
105
+ return;
32
106
  }
33
107
 
34
- const yesterdayPositions = new Map(yesterdayPortfolio.AggregatedPositions.map(p => [p.InstrumentID, p]));
35
- const todayPositions = new Map(todayPortfolio.AggregatedPositions.map(p => [p.InstrumentID, p]));
108
+ if (!this.mappings) {
109
+ // Context contains the mappings loaded in Pass 1
110
+ this.mappings = context.mappings;
111
+ }
36
112
 
37
- const allInstrumentIds = new Set([
38
- ...yesterdayPositions.keys(),
39
- ...todayPositions.keys()
40
- ]);
113
+ const cohorts = this._getInProfitCohorts(fetchedDependencies);
114
+ if (cohorts.size === 0) {
115
+ return; // No dependency data
116
+ }
117
+
118
+ const yPos = this._getPortfolioPositions(yesterdayPortfolio);
119
+ const tPos = this._getPortfolioPositions(todayPortfolio);
120
+
121
+ const yPosMap = new Map(yPos?.map(p => [p.InstrumentID, p]) || []);
122
+ const tPosMap = new Map(tPos?.map(p => [p.InstrumentID, p]) || []);
123
+
124
+ const allInstrumentIds = new Set([...yPosMap.keys(), ...tPosMap.keys()]);
41
125
 
42
126
  for (const instrumentId of allInstrumentIds) {
43
- const yPos = yesterdayPositions.get(instrumentId);
44
- const tPos = todayPositions.get(instrumentId);
127
+ if (!instrumentId) continue;
45
128
 
46
- // --- COHORT LOGIC ---
47
- // Only aggregate if the user is in PROFIT on this asset.
48
- // We check *today's* profit status as the primary signal.
49
- const tNetProfit = tPos?.NetProfit || 0;
50
- if (tNetProfit <= 0) {
51
- continue; // Skip this asset for this user
52
- }
53
- // --- END COHORT LOGIC ---
129
+ const ticker = this.mappings.instrumentToTicker[instrumentId] || `id_${instrumentId}`;
130
+ const cohort = cohorts.get(ticker);
54
131
 
132
+ // This user is not in the "in profit" cohort for this asset, skip.
133
+ if (!cohort || !cohort.has(userId)) {
134
+ continue;
135
+ }
136
+
137
+ // User *is* in the cohort, process their data
55
138
  this._initAsset(instrumentId);
56
- this.asset_values[instrumentId].day1_value_sum += (yPos?.Value || 0);
57
- this.asset_values[instrumentId].day2_value_sum += (tPos?.Value || 0);
139
+ const asset = this.assetData.get(instrumentId);
140
+ asset.cohort.add(userId); // Track cohort size
141
+
142
+ const yP = yPosMap.get(instrumentId);
143
+ const tP = tPosMap.get(instrumentId);
144
+
145
+ const yInvested = yP?.InvestedAmount || yP?.Amount || 0;
146
+ const tInvested = tP?.InvestedAmount || tP?.Amount || 0;
147
+
148
+ if (yInvested > 0) {
149
+ asset.total_invested_yesterday += yInvested;
150
+ const yPriceChange = (yP?.PipsRate || 0) / (yP?.OpenRate || 1);
151
+ asset.price_change_yesterday += yPriceChange * yInvested;
152
+ }
153
+ if (tInvested > 0) {
154
+ asset.total_invested_today += tInvested;
155
+ }
58
156
  }
59
- this.user_count++; // Note: This is user_count of *all* users, which is fine for avg.
60
157
  }
61
158
 
62
159
  async getResult() {
63
- if (this.user_count === 0 || !this.dates.today) return {};
64
- if (!this.priceMap || !this.mappings) {
65
- const [priceData, mappingData] = await Promise.all([
66
- loadAllPriceData(),
67
- loadInstrumentMappings()
68
- ]);
69
- this.priceMap = priceData;
70
- this.mappings = mappingData;
160
+ if (!this.mappings) {
161
+ this.mappings = await loadInstrumentMappings();
71
162
  }
72
-
73
- const finalResults = {};
74
- const todayStr = this.dates.today;
75
- const yesterdayStr = this.dates.yesterday;
76
163
 
77
- for (const instrumentId in this.asset_values) {
164
+ const result = {};
165
+
166
+ for (const [instrumentId, data] of this.assetData.entries()) {
78
167
  const ticker = this.mappings.instrumentToTicker[instrumentId] || `id_${instrumentId}`;
79
-
80
- const avg_day1_value = this.asset_values[instrumentId].day1_value_sum / this.user_count;
81
- const avg_day2_value = this.asset_values[instrumentId].day2_value_sum / this.user_count;
82
- const priceChangePct = getDailyPriceChange(instrumentId, yesterdayStr, todayStr, this.priceMap);
83
168
 
84
- if (priceChangePct === null) continue;
169
+ const { total_invested_yesterday, total_invested_today, price_change_yesterday, cohort } = data;
85
170
 
86
- const expected_day2_value = avg_day1_value * (1 + priceChangePct);
87
- const net_crowd_flow_pct = avg_day2_value - expected_day2_value;
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;
88
176
 
89
- finalResults[ticker] = {
90
- net_crowd_flow_pct: net_crowd_flow_pct,
91
- avg_value_day1_pct: avg_day1_value,
92
- avg_value_day2_pct: avg_day2_value
93
- };
177
+ result[ticker] = {
178
+ net_flow_percentage: net_flow_percentage,
179
+ total_invested_today: total_invested_today,
180
+ total_invested_yesterday: total_invested_yesterday,
181
+ cohort_size: cohort.size
182
+ };
183
+ }
94
184
  }
95
- return finalResults;
185
+ return result;
96
186
  }
97
187
 
98
- reset() { /*...reset all properties...*/ }
188
+ reset() {
189
+ this.assetData.clear();
190
+ this.mappings = null;
191
+ this.inProfitCohorts = null;
192
+ }
99
193
  }
194
+
100
195
  module.exports = InProfitAssetCrowdFlow;
@@ -1,40 +1,107 @@
1
1
  /**
2
- * @fileoverview Calculates the "Paper Hands vs. Diamond Hands" index.
2
+ * @fileoverview Calculation (Pass 2) for Paper vs Diamond Hands.
3
+ *
4
+ * This metric provides a simple ratio of positions that were
5
+ * closed today ("paper hands") vs. positions that were held
6
+ * ("diamond hands").
7
+ *
8
+ * It gives a general sense of market turnover.
3
9
  */
4
-
5
10
  class PaperVsDiamondHands {
6
11
  constructor() {
7
- this.newPositions = 0;
8
- this.closedPositions = 0;
9
- this.heldPositions = 0;
12
+ this.paper_hands = 0; // Positions closed
13
+ this.diamond_hands = 0; // Positions held
10
14
  }
11
15
 
12
- process(todayPortfolio, yesterdayPortfolio, userId) {
16
+ /**
17
+ * Defines the output schema for this calculation.
18
+ * @returns {object} JSON Schema object
19
+ */
20
+ static getSchema() {
21
+ return {
22
+ "type": "object",
23
+ "description": "Calculates the ratio of positions closed ('paper hands') vs. held ('diamond hands') from yesterday to today.",
24
+ "properties": {
25
+ "paper_hands_count": {
26
+ "type": "number",
27
+ "description": "Total count of positions that existed yesterday but were closed today."
28
+ },
29
+ "diamond_hands_count": {
30
+ "type": "number",
31
+ "description": "Total count of positions that existed yesterday and are still held today."
32
+ },
33
+ "total_positions_yesterday": {
34
+ "type": "number",
35
+ "description": "The sum of paper and diamond hands counts."
36
+ },
37
+ "paper_hands_ratio": {
38
+ "type": "number",
39
+ "description": "Ratio of paper hands to diamond hands (Paper / Diamond). Null if no diamond hands."
40
+ },
41
+ "paper_hands_pct": {
42
+ "type": "number",
43
+ "description": "Percentage of positions that were 'paper handed' (closed)."
44
+ },
45
+ "diamond_hands_pct": {
46
+ "type": "number",
47
+ "description": "Percentage of positions that were 'diamond handed' (held)."
48
+ }
49
+ },
50
+ "required": ["paper_hands_count", "diamond_hands_count", "total_positions_yesterday", "paper_hands_pct", "diamond_hands_pct"]
51
+ };
52
+ }
53
+
54
+ _getPortfolioPositionIds(portfolio) {
55
+ // We MUST use PositionID to track specific trades
56
+ const positions = portfolio?.AggregatedPositions || portfolio?.PublicPositions;
57
+ if (!positions || !Array.isArray(positions)) {
58
+ return new Set();
59
+ }
60
+ return new Set(positions.map(p => p.PositionID).filter(Boolean));
61
+ }
62
+
63
+ process(todayPortfolio, yesterdayPortfolio) {
13
64
  if (!todayPortfolio || !yesterdayPortfolio) {
14
65
  return;
15
66
  }
16
67
 
17
- const todayIds = new Set((todayPortfolio.PublicPositions || todayPortfolio.AggregatedPositions).map(p => p.PositionID || p.InstrumentID));
18
- const yesterdayIds = new Set((yesterdayPortfolio.PublicPositions || yesterdayPortfolio.AggregatedPositions).map(p => p.PositionID || p.InstrumentID));
68
+ const yPosIds = this._getPortfolioPositionIds(yesterdayPortfolio);
69
+ const tPosIds = this._getPortfolioPositionIds(todayPortfolio);
19
70
 
20
- const newPos = [...todayIds].filter(id => !yesterdayIds.has(id)).length;
21
- const closedPos = [...yesterdayIds].filter(id => !todayIds.has(id)).length;
22
- const heldPos = [...todayIds].filter(id => yesterdayIds.has(id)).length;
71
+ if (yPosIds.size === 0) {
72
+ return; // No positions yesterday to analyze
73
+ }
23
74
 
24
- this.newPositions += newPos;
25
- this.closedPositions += closedPos;
26
- this.heldPositions += heldPos;
75
+ for (const yPosId of yPosIds) {
76
+ if (tPosIds.has(yPosId)) {
77
+ // Position was held
78
+ this.diamond_hands++;
79
+ } else {
80
+ // Position was closed
81
+ this.paper_hands++;
82
+ }
83
+ }
27
84
  }
28
85
 
29
86
  getResult() {
30
- const totalPositions = this.newPositions + this.closedPositions + this.heldPositions;
31
- if (totalPositions === 0) return {};
32
-
87
+ const total = this.paper_hands + this.diamond_hands;
88
+
33
89
  return {
34
- paper_hands_index: (this.closedPositions / totalPositions) * 100, // High turnover
35
- diamond_hands_index: (this.heldPositions / totalPositions) * 100, // Low turnover
90
+ paper_hands_count: this.paper_hands,
91
+ diamond_hands_count: this.diamond_hands,
92
+ total_positions_yesterday: total,
93
+ // Ratio of paper-to-diamond. Can be null if diamond_hands is 0.
94
+ paper_hands_ratio: (this.diamond_hands > 0) ? (this.paper_hands / this.diamond_hands) : null,
95
+ // Percentage of total positions
96
+ paper_hands_pct: (total > 0) ? (this.paper_hands / total) * 100 : 0,
97
+ diamond_hands_pct: (total > 0) ? (this.diamond_hands / total) * 100 : 0,
36
98
  };
37
99
  }
100
+
101
+ reset() {
102
+ this.paper_hands = 0;
103
+ this.diamond_hands = 0;
104
+ }
38
105
  }
39
106
 
40
107
  module.exports = PaperVsDiamondHands;
@@ -1,67 +1,119 @@
1
1
  /**
2
- * Aggregates P/L by the number of positions a user holds.
3
- * Used to create a dot plot.
2
+ * @fileoverview Calculation (Pass 2) for P&L by position count.
3
+ *
4
+ * This metric answers: "What is the average daily P&L for users,
5
+ * bucketed by the number of positions they hold?"
6
+ *
7
+ * This helps determine if holding more positions (diversifying)
8
+ * correlates with better or worse P&L.
4
9
  */
5
10
  class PositionCountPnl {
6
11
  constructor() {
7
- // We will store sums and counts to calculate averages later
8
- this.pnl_by_position_count = {};
9
- }
10
-
11
- _initBucket(count) {
12
- if (!this.pnl_by_position_count[count]) {
13
- this.pnl_by_position_count[count] = { pnl_sum: 0, count: 0 };
14
- }
12
+ // We will store { [count_bucket]: { pnl_sum: 0, user_count: 0 } }
13
+ this.buckets = {
14
+ '1': { pnl_sum: 0, user_count: 0 },
15
+ '2-5': { pnl_sum: 0, user_count: 0 },
16
+ '6-10': { pnl_sum: 0, user_count: 0 },
17
+ '11-20': { pnl_sum: 0, user_count: 0 },
18
+ '21+': { pnl_sum: 0, user_count: 0 },
19
+ };
15
20
  }
16
21
 
17
22
  /**
18
- * FIX: Helper function to calculate total P&L from positions
19
- * @param {object} portfolio
20
- * @returns {number|null}
23
+ * Defines the output schema for this calculation.
24
+ * @returns {object} JSON Schema object
21
25
  */
22
- _calculateTotalPnl(portfolio) {
23
- const positions = portfolio?.AggregatedPositions || portfolio?.PublicPositions;
24
- if (positions && Array.isArray(positions)) {
25
- // Sum all NetProfit fields, defaulting to 0 if a position has no NetProfit
26
- return positions.reduce((sum, pos) => sum + (pos.NetProfit || 0), 0);
27
- }
26
+ static getSchema() {
27
+ const bucketSchema = {
28
+ "type": "object",
29
+ "description": "Aggregated P&L metrics for a position count 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 positions a user holds.",
50
+ "properties": {
51
+ "1": bucketSchema,
52
+ "2-5": bucketSchema,
53
+ "6-10": bucketSchema,
54
+ "11-20": bucketSchema,
55
+ "21+": bucketSchema
56
+ },
57
+ "required": ["1", "2-5", "6-10", "11-20", "21+"]
58
+ };
59
+ }
60
+
61
+ _getBucket(count) {
62
+ if (count === 1) return '1';
63
+ if (count >= 2 && count <= 5) return '2-5';
64
+ if (count >= 6 && count <= 10) return '6-10';
65
+ if (count >= 11 && count <= 20) return '11-20';
66
+ if (count >= 21) return '21+';
28
67
  return null;
29
68
  }
30
69
 
31
- process(todayPortfolio, yesterdayPortfolio, userId) {
32
- // FIX: Only need todayPortfolio for this logic
70
+ process(todayPortfolio, yesterdayPortfolio) {
71
+ // This calculation only needs today's portfolio state
33
72
  if (!todayPortfolio) {
34
73
  return;
35
74
  }
36
75
 
37
76
  const positions = todayPortfolio.AggregatedPositions || todayPortfolio.PublicPositions;
77
+ const positionCount = Array.isArray(positions) ? positions.length : 0;
38
78
 
39
- if (!positions || !Array.isArray(positions)) {
40
- return; // Skip users with no positions array
41
- }
42
-
43
- const positionCount = positions.length;
44
79
  if (positionCount === 0) {
45
- return; // Skip users with no positions
80
+ return;
46
81
  }
47
82
 
48
- // FIX: Calculate dailyPnl by summing NetProfit from all positions
49
- const dailyPnl = this._calculateTotalPnl(todayPortfolio);
50
-
51
- if (dailyPnl === null) {
52
- return; // Cannot calculate P&L for this user
83
+ const bucketKey = this._getBucket(positionCount);
84
+ if (!bucketKey) {
85
+ return;
53
86
  }
54
87
 
55
- this._initBucket(positionCount);
56
- this.pnl_by_position_count[positionCount].pnl_sum += dailyPnl;
57
- this.pnl_by_position_count[positionCount].count++;
88
+ // Use the P&L from the summary, which is for the *day*
89
+ const dailyPnl = todayPortfolio.Summary?.NetProfit || 0;
90
+
91
+ const bucket = this.buckets[bucketKey];
92
+ bucket.pnl_sum += dailyPnl;
93
+ bucket.user_count++;
58
94
  }
59
95
 
60
96
  getResult() {
61
- // Return the aggregated object.
62
- // Frontend will iterate keys, calculate avg (pnl_sum/count),
63
- // and plot { x: positionCount, y: avg_pnl }
64
- return this.pnl_by_position_count;
97
+ const result = {};
98
+ for (const key in this.buckets) {
99
+ const bucket = this.buckets[key];
100
+ result[key] = {
101
+ average_daily_pnl: (bucket.user_count > 0) ? (bucket.pnl_sum / bucket.user_count) : 0,
102
+ user_count: bucket.user_count,
103
+ pnl_sum: bucket.pnl_sum
104
+ };
105
+ }
106
+ return result;
107
+ }
108
+
109
+ reset() {
110
+ this.buckets = {
111
+ '1': { pnl_sum: 0, user_count: 0 },
112
+ '2-5': { pnl_sum: 0, user_count: 0 },
113
+ '6-10': { pnl_sum: 0, user_count: 0 },
114
+ '11-20': { pnl_sum: 0, user_count: 0 },
115
+ '21+': { pnl_sum: 0, user_count: 0 },
116
+ };
65
117
  }
66
118
  }
67
119