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,55 +1,112 @@
1
1
  /**
2
- * @fileoverview Calculates the change in risk appetite for speculators based on stop-loss distance.
2
+ * @fileoverview Calculation (Pass 2) for speculator metric.
3
+ *
4
+ * This metric answers: "What is the daily change in
5
+ * speculators' risk appetite, as measured by the
6
+ * average distance of their stop-loss orders?"
7
+ *
8
+ * This is a *stateful* calculation that computes a 30-day
9
+ * rolling average.
3
10
  */
4
-
5
11
  class RiskAppetiteChange {
6
12
  constructor() {
7
- this.totalYesterdaySLDistance = 0;
8
- this.totalTodaySLDistance = 0;
9
- this.userCount = 0;
13
+ // Stores *today's* raw values
14
+ this.sl_distances = [];
15
+ // Stores *yesterday's* 30-day history
16
+ this.history = [];
17
+ }
18
+
19
+ /**
20
+ * Defines the output schema for this calculation.
21
+ * @returns {object} JSON Schema object
22
+ */
23
+ static getSchema() {
24
+ return {
25
+ "type": "object",
26
+ "description": "Tracks the 30-day rolling average of speculator stop loss distance as a proxy for risk appetite.",
27
+ "properties": {
28
+ "risk_appetite_score": {
29
+ "type": "number",
30
+ "description": "Today's average SL distance %."
31
+ },
32
+ "daily_change_pct": {
33
+ "type": "number",
34
+ "description": "Percentage change from yesterday's average."
35
+ },
36
+ "avg_sl_distance_30d": {
37
+ "type": "number",
38
+ "description": "30-day rolling average of the SL distance."
39
+ },
40
+ "history_30d": {
41
+ "type": "array",
42
+ "description": "30-day history of daily average SL distance.",
43
+ "items": { "type": "number" }
44
+ }
45
+ },
46
+ "required": ["risk_appetite_score", "daily_change_pct", "avg_sl_distance_30d", "history_30d"]
47
+ };
10
48
  }
11
49
 
12
- process(todayPortfolio, yesterdayPortfolio, userId) {
13
- if (!todayPortfolio || !yesterdayPortfolio || !todayPortfolio.PublicPositions || !yesterdayPortfolio.PublicPositions) {
50
+ process(todayPortfolio, yesterdayPortfolio, userId, context) {
51
+ // 1. Get this metric's history from yesterday (pre-loaded)
52
+ if (this.history.length === 0) { // Only run once
53
+ const yHistoryData = context.yesterdaysDependencyData['risk_appetite_change'];
54
+ if (yHistoryData) {
55
+ this.history = yHistoryData.history_30d || [];
56
+ }
57
+ }
58
+
59
+ if (todayPortfolio?.context?.userType !== 'speculator') {
14
60
  return;
15
61
  }
16
-
17
- const yesterdayAvgDistance = this.calculateAverageSLDistance(yesterdayPortfolio.PublicPositions);
18
- const todayAvgDistance = this.calculateAverageSLDistance(todayPortfolio.PublicPositions);
19
-
20
- if (yesterdayAvgDistance !== null && todayAvgDistance !== null) {
21
- this.totalYesterdaySLDistance += yesterdayAvgDistance;
22
- this.totalTodaySLDistance += todayAvgDistance;
23
- this.userCount++;
62
+
63
+ const positions = todayPortfolio.PublicPositions;
64
+ if (!positions || !Array.isArray(positions)) {
65
+ return;
24
66
  }
25
- }
26
67
 
27
- calculateAverageSLDistance(positions) {
28
- let totalDistance = 0;
29
- let count = 0;
30
68
  for (const pos of positions) {
31
- if (pos.StopLossRate > 0.0001 && pos.OpenRate > 0) {
32
- const distance = pos.IsBuy ? pos.OpenRate - pos.StopLossRate : pos.StopLossRate - pos.OpenRate;
33
- if (distance > 0) {
34
- totalDistance += (distance / pos.OpenRate) * 100;
35
- count++;
36
- }
69
+ const sl_rate = pos.StopLossRate || 0;
70
+ const open_rate = pos.OpenRate || 0;
71
+
72
+ if (sl_rate > 0 && open_rate > 0) {
73
+ const distance = Math.abs(open_rate - sl_rate);
74
+ const distance_pct = (distance / open_rate);
75
+ this.sl_distances.push(distance_pct);
37
76
  }
38
77
  }
39
- return count > 0 ? totalDistance / count : null;
40
78
  }
41
79
 
42
80
  getResult() {
43
- if (this.userCount === 0) return {};
44
- const avgYesterday = this.totalYesterdaySLDistance / this.userCount;
45
- const avgToday = this.totalTodaySLDistance / this.userCount;
81
+ const yHistory = this.history;
82
+
83
+ let today_avg_dist = 0;
84
+ if (this.sl_distances.length > 0) {
85
+ today_avg_dist = (this.sl_distances.reduce((a, b) => a + b, 0) / this.sl_distances.length) * 100;
86
+ }
46
87
 
88
+ const newHistory = [today_avg_dist, ...yHistory].slice(0, 30);
89
+
90
+ const yesterday_avg = yHistory[0] || 0;
91
+ const avg_30d = newHistory.reduce((a, b) => a + b, 0) / newHistory.length;
92
+
93
+ let daily_change = 0;
94
+ if (yesterday_avg > 0) {
95
+ daily_change = ((today_avg_dist - yesterday_avg) / yesterday_avg) * 100;
96
+ }
97
+
47
98
  return {
48
- average_stop_loss_distance_yesterday: avgYesterday,
49
- average_stop_loss_distance_today: avgToday,
50
- change_in_risk_appetite: avgToday - avgYesterday,
99
+ risk_appetite_score: today_avg_dist, // Today's score
100
+ daily_change_pct: daily_change,
101
+ avg_sl_distance_30d: avg_30d,
102
+ history_30d: newHistory
51
103
  };
52
104
  }
105
+
106
+ reset() {
107
+ this.sl_distances = [];
108
+ this.history = [];
109
+ }
53
110
  }
54
111
 
55
112
  module.exports = RiskAppetiteChange;
@@ -1,75 +1,85 @@
1
1
  /**
2
- * Compares the average P/L of speculator users who use TSL
3
- * vs. those who do not.
2
+ * @fileoverview Calculation (Pass 2) for TSL effectiveness.
3
+ *
4
+ * This metric answers: "What is the difference in average
5
+ * P&L between speculators who use a Trailing Stop Loss (TSL)
6
+ * versus those who do not?"
4
7
  */
5
8
  class TslEffectiveness {
6
9
  constructor() {
7
- this.tsl_group = { pnl_sum: 0, count: 0 };
8
- this.nontsl_group = { pnl_sum: 0, count: 0 };
10
+ this.with_tsl = { pnl_sum: 0, count: 0 };
11
+ this.without_tsl = { pnl_sum: 0, count: 0 };
9
12
  }
10
13
 
11
14
  /**
12
- * FIX: Helper function to calculate total P&L from positions
13
- * @param {object} portfolio
14
- * @returns {number|null}
15
+ * Defines the output schema for this calculation.
16
+ * @returns {object} JSON Schema object
15
17
  */
16
- _calculateTotalPnl(portfolio) {
17
- // Speculators use PublicPositions
18
- const positions = portfolio?.PublicPositions;
19
- if (positions && Array.isArray(positions)) {
20
- // Sum all NetProfit fields, defaulting to 0 if a position has no NetProfit
21
- return positions.reduce((sum, pos) => sum + (pos.NetProfit || 0), 0);
22
- }
23
- return null;
18
+ static getSchema() {
19
+ return {
20
+ "type": "object",
21
+ "description": "Compares the average P&L of speculators who use TSL vs. those who do not.",
22
+ "properties": {
23
+ "with_tsl_avg_pnl": {
24
+ "type": "number",
25
+ "description": "Average P&L for positions with TSL enabled."
26
+ },
27
+ "without_tsl_avg_pnl": {
28
+ "type": "number",
29
+ "description": "Average P&L for positions without TSL enabled."
30
+ },
31
+ "effectiveness_delta": {
32
+ "type": "number",
33
+ "description": "The difference in P&L (With TSL - Without TSL)."
34
+ },
35
+ "with_tsl_count": { "type": "number" },
36
+ "without_tsl_count": { "type": "number" }
37
+ },
38
+ "required": ["with_tsl_avg_pnl", "without_tsl_avg_pnl", "effectiveness_delta", "with_tsl_count", "without_tsl_count"]
39
+ };
24
40
  }
25
41
 
26
42
  process(todayPortfolio, yesterdayPortfolio, userId) {
27
- // Check if user is a speculator and we have today's data
28
- // FIX: yesterdayPortfolio is not needed for this logic, only today's P&L
29
- if (todayPortfolio?.context?.userType !== 'speculator' || !todayPortfolio) {
43
+ // This calculation is only for speculators
44
+ if (todayPortfolio?.context?.userType !== 'speculator') {
30
45
  return;
31
46
  }
32
47
 
33
48
  const positions = todayPortfolio.PublicPositions;
34
-
35
49
  if (!positions || !Array.isArray(positions)) {
36
- return;
50
+ return;
37
51
  }
38
52
 
39
- // FIX: Calculate dailyPnl by summing NetProfit from all positions
40
- const dailyPnl = this._calculateTotalPnl(todayPortfolio);
41
-
42
- if (dailyPnl === null) {
43
- return; // Cannot calculate P&L
44
- }
45
-
46
- const usesTSL = positions.some(p => p.IsTslEnabled);
47
-
48
- if (usesTSL) {
49
- this.tsl_group.pnl_sum += dailyPnl;
50
- this.tsl_group.count++;
51
- } else {
52
- this.nontsl_group.pnl_sum += dailyPnl;
53
- this.nontsl_group.count++;
53
+ for (const pos of positions) {
54
+ const pnl = pos.NetProfit || 0;
55
+
56
+ if (pos.IsTslEnabled) {
57
+ this.with_tsl.pnl_sum += pnl;
58
+ this.with_tsl.count++;
59
+ } else {
60
+ this.without_tsl.pnl_sum += pnl;
61
+ this.without_tsl.count++;
62
+ }
54
63
  }
55
64
  }
56
65
 
57
66
  getResult() {
58
- // Return final calculated averages
59
- const tsl_avg_pnl = (this.tsl_group.count > 0) ? this.tsl_group.pnl_sum / this.tsl_group.count : 0;
60
- const nontsl_avg_pnl = (this.nontsl_group.count > 0) ? this.nontsl_group.pnl_sum / this.nontsl_group.count : 0;
61
-
67
+ const with_avg = (this.with_tsl.count > 0) ? (this.with_tsl.pnl_sum / this.with_tsl.count) : 0;
68
+ const without_avg = (this.without_tsl.count > 0) ? (this.without_tsl.pnl_sum / this.without_tsl.count) : 0;
69
+
62
70
  return {
63
- tsl_users: {
64
- avg_pnl: tsl_avg_pnl,
65
- count: this.tsl_group.count
66
- },
67
- nontsl_users: {
68
- avg_pnl: nontsl_avg_pnl,
69
- count: this.nontsl_group.count
70
- }
71
+ with_tsl_avg_pnl: with_avg,
72
+ without_tsl_avg_pnl: without_avg,
73
+ effectiveness_delta: with_avg - without_avg,
74
+ with_tsl_count: this.with_tsl.count,
75
+ without_tsl_count: this.without_tsl.count
71
76
  };
72
77
  }
78
+
79
+ reset() {
80
+ this.with_tsl = { pnl_sum: 0, count: 0 };
81
+ this.without_tsl = { pnl_sum: 0, count: 0 };
82
+ }
73
83
  }
74
84
 
75
85
  module.exports = TslEffectiveness;
@@ -1,27 +1,88 @@
1
1
  /**
2
- * @fileoverview Calculates the average holding duration of open speculator positions.
2
+ * @fileoverview Calculation (Pass 1) for speculator metric.
3
+ *
4
+ * This metric answers: "What is the average holding duration
5
+ * (in hours) for open speculator positions, grouped by asset?"
3
6
  */
4
7
  const { loadInstrumentMappings } = require('../../utils/sector_mapping_provider');
5
8
 
6
9
  class HoldingDurationPerAsset {
7
10
  constructor() {
8
- this.durationData = {};
11
+ // { [instrumentId]: { sum_hours: 0, count: 0 } }
12
+ this.assets = new Map();
9
13
  this.mappings = null;
10
14
  }
11
15
 
12
- process(portfolioData, yesterdayPortfolio, userId, context) {
13
- if (portfolioData && portfolioData.PublicPositions) {
14
- const now = new Date();
15
- for (const position of portfolioData.PublicPositions) {
16
- const instrumentId = position.InstrumentID;
17
- const openTime = new Date(position.OpenDateTime);
18
- const durationHours = (now - openTime) / (1000 * 60 * 60);
19
-
20
- if (!this.durationData[instrumentId]) {
21
- this.durationData[instrumentId] = { duration_sum_hours: 0, count: 0 };
16
+ /**
17
+ * Defines the output schema for this calculation.
18
+ * @returns {object} JSON Schema object
19
+ */
20
+ static getSchema() {
21
+ const tickerSchema = {
22
+ "type": "object",
23
+ "properties": {
24
+ "avg_duration_hours": {
25
+ "type": "number",
26
+ "description": "Average holding duration in hours."
27
+ },
28
+ "count": {
29
+ "type": "number",
30
+ "description": "Count of positions used in average."
22
31
  }
23
- this.durationData[instrumentId].duration_sum_hours += durationHours;
24
- this.durationData[instrumentId].count++;
32
+ },
33
+ "required": ["avg_duration_hours", "count"]
34
+ };
35
+
36
+ return {
37
+ "type": "object",
38
+ "description": "Calculates the average holding duration (in hours) for open speculator positions per asset.",
39
+ "patternProperties": {
40
+ "^.*$": tickerSchema // Ticker
41
+ },
42
+ "additionalProperties": tickerSchema
43
+ };
44
+ }
45
+
46
+ _initAsset(instrumentId) {
47
+ if (!this.assets.has(instrumentId)) {
48
+ this.assets.set(instrumentId, { sum_hours: 0, count: 0 });
49
+ }
50
+ }
51
+
52
+ _getHoldingDurationHours(openDateStr) {
53
+ if (!openDateStr) return 0;
54
+ try {
55
+ const openDate = new Date(openDateStr);
56
+ // Get difference from 'now' (or the snapshot time)
57
+ const diffMs = new Date().getTime() - openDate.getTime();
58
+ return diffMs / (1000 * 60 * 60); // Convert ms to hours
59
+ } catch (e) {
60
+ return 0;
61
+ }
62
+ }
63
+
64
+ process(portfolioData) {
65
+ if (portfolioData?.context?.userType !== 'speculator') {
66
+ return;
67
+ }
68
+
69
+ const positions = portfolioData.PublicPositions;
70
+ if (!positions || !Array.isArray(positions)) {
71
+ return;
72
+ }
73
+
74
+ for (const pos of positions) {
75
+ const instrumentId = pos.InstrumentID;
76
+ if (!instrumentId) continue;
77
+
78
+ this._initAsset(instrumentId);
79
+
80
+ const duration = this._getHoldingDurationHours(pos.OpenDateTime);
81
+
82
+ if (duration > 0) {
83
+ const assetData = this.assets.get(instrumentId);
84
+ assetData.sum_hours += duration;
85
+ assetData.count++;
25
86
  }
26
87
  }
27
88
  }
@@ -30,26 +91,25 @@ class HoldingDurationPerAsset {
30
91
  if (!this.mappings) {
31
92
  this.mappings = await loadInstrumentMappings();
32
93
  }
33
- const result = {};
34
- for (const instrumentId in this.durationData) {
35
- const ticker = this.mappings.instrumentToTicker[instrumentId] || instrumentId.toString();
36
- const data = this.durationData[instrumentId];
37
94
 
38
- // REFACTOR: Perform the final calculation directly.
95
+ const result = {};
96
+ for (const [instrumentId, data] of this.assets.entries()) {
97
+ const ticker = this.mappings.instrumentToTicker[instrumentId] || `id_${instrumentId}`;
98
+
39
99
  if (data.count > 0) {
40
100
  result[ticker] = {
41
- average_duration_hours: data.duration_sum_hours / data.count
101
+ avg_duration_hours: data.sum_hours / data.count,
102
+ count: data.count
42
103
  };
43
104
  }
44
105
  }
45
-
46
106
  return result;
47
107
  }
48
108
 
49
109
  reset() {
50
- this.durationData = {};
110
+ this.assets.clear();
51
111
  this.mappings = null;
52
112
  }
53
113
  }
54
114
 
55
- module.exports = HoldingDurationPerAsset;
115
+ module.exports = HoldingDurationPerAsset;
@@ -1,25 +1,76 @@
1
1
  /**
2
- * @fileoverview Calculates leverage usage per instrument for speculators.
2
+ * @fileoverview Calculation (Pass 1) for speculator metric.
3
+ *
4
+ * This metric answers: "For each asset, what is the
5
+ * distribution of leverage levels used by speculators?"
3
6
  */
4
7
  const { loadInstrumentMappings } = require('../../utils/sector_mapping_provider');
5
8
 
6
9
  class LeveragePerAsset {
7
10
  constructor() {
8
- this.leverageData = {};
11
+ // { [instrumentId]: { [leverage]: count } }
12
+ this.assets = new Map();
9
13
  this.mappings = null;
10
14
  }
11
15
 
12
- process(portfolioData, yesterdayPortfolio, userId, context) {
13
- if (portfolioData && portfolioData.PublicPositions) {
14
- for (const position of portfolioData.PublicPositions) {
15
- const instrumentId = position.InstrumentID;
16
- const leverage = position.Leverage;
17
-
18
- if (!this.leverageData[instrumentId]) {
19
- this.leverageData[instrumentId] = {};
16
+ /**
17
+ * Defines the output schema for this calculation.
18
+ * @returns {object} JSON Schema object
19
+ */
20
+ static getSchema() {
21
+ const leverageSchema = {
22
+ "type": "object",
23
+ "description": "Distribution of leverage levels for this asset.",
24
+ "patternProperties": {
25
+ // Leverage level, e.g., "1x", "5x"
26
+ "^[0-9]+x$": {
27
+ "type": "number",
28
+ "description": "Count of positions at this leverage."
20
29
  }
21
- this.leverageData[instrumentId][leverage] = (this.leverageData[instrumentId][leverage] || 0) + 1;
30
+ },
31
+ "additionalProperties": { "type": "number" }
32
+ };
33
+
34
+ return {
35
+ "type": "object",
36
+ "description": "Calculates the distribution of leverage levels used by speculators for each asset.",
37
+ "patternProperties": {
38
+ "^.*$": leverageSchema // Ticker
39
+ },
40
+ "additionalProperties": leverageSchema
41
+ };
42
+ }
43
+
44
+ _initAsset(instrumentId) {
45
+ if (!this.assets.has(instrumentId)) {
46
+ this.assets.set(instrumentId, {});
47
+ }
48
+ }
49
+
50
+ process(portfolioData) {
51
+ if (portfolioData?.context?.userType !== 'speculator') {
52
+ return;
53
+ }
54
+
55
+ const positions = portfolioData.PublicPositions;
56
+ if (!positions || !Array.isArray(positions)) {
57
+ return;
58
+ }
59
+
60
+ for (const pos of positions) {
61
+ const instrumentId = pos.InstrumentID;
62
+ if (!instrumentId) continue;
63
+
64
+ this._initAsset(instrumentId);
65
+ const assetData = this.assets.get(instrumentId);
66
+
67
+ const leverage = pos.Leverage || 1;
68
+ const key = `${leverage}x`;
69
+
70
+ if (!assetData[key]) {
71
+ assetData[key] = 0;
22
72
  }
73
+ assetData[key]++;
23
74
  }
24
75
  }
25
76
 
@@ -27,19 +78,17 @@ class LeveragePerAsset {
27
78
  if (!this.mappings) {
28
79
  this.mappings = await loadInstrumentMappings();
29
80
  }
81
+
30
82
  const result = {};
31
- for (const instrumentId in this.leverageData) {
32
- const ticker = this.mappings.instrumentToTicker[instrumentId] || instrumentId.toString();
33
- result[ticker] = this.leverageData[instrumentId];
83
+ for (const [instrumentId, data] of this.assets.entries()) {
84
+ const ticker = this.mappings.instrumentToTicker[instrumentId] || `id_${instrumentId}`;
85
+ result[ticker] = data;
34
86
  }
35
- if (Object.keys(result).length === 0) return {};
36
- return {
37
- leverage_per_asset: result
38
- };
87
+ return result;
39
88
  }
40
89
 
41
90
  reset() {
42
- this.leverageData = {};
91
+ this.assets.clear();
43
92
  this.mappings = null;
44
93
  }
45
94
  }
@@ -1,44 +1,105 @@
1
- const { getInstrumentSectorMap } = require('../../utils/sector_mapping_provider');
2
-
3
1
  /**
4
- * @fileoverview Calculates leverage usage per sector for speculators.
2
+ * @fileoverview Calculation (Pass 1) for speculator metric.
3
+ *
4
+ * This metric answers: "For each sector, what is the
5
+ * distribution of leverage levels used by speculators?"
5
6
  */
7
+ const { loadInstrumentMappings } = require('../../utils/sector_mapping_provider');
6
8
 
7
9
  class LeveragePerSector {
8
10
  constructor() {
9
- this.positions = [];
11
+ // { [sector]: { [leverage]: count } }
12
+ this.sectors = new Map();
13
+ this.mappings = null;
10
14
  }
11
15
 
12
- process(portfolioData, yesterdayPortfolio, userId, context) {
13
- if (portfolioData && portfolioData.PublicPositions) {
14
- this.positions.push(...portfolioData.PublicPositions);
15
- }
16
- }
16
+ /**
17
+ * Defines the output schema for this calculation.
18
+ * @returns {object} JSON Schema object
19
+ */
20
+ static getSchema() {
21
+ const leverageSchema = {
22
+ "type": "object",
23
+ "description": "Distribution of leverage levels for this sector.",
24
+ "patternProperties": {
25
+ // Leverage level, e.g., "1x", "5x", "inf"
26
+ "^[0-9inf]+x$": {
27
+ "type": "number",
28
+ "description": "Count of positions at this leverage."
29
+ }
30
+ },
31
+ "additionalProperties": { "type": "number" }
32
+ };
17
33
 
18
- async getResult() {
19
- if (this.positions.length === 0) return {};
34
+ return {
35
+ "type": "object",
36
+ "description": "Calculates the distribution of leverage levels used by speculators for each sector.",
37
+ "patternProperties": {
38
+ "^.*$": leverageSchema // Sector
39
+ },
40
+ "additionalProperties": leverageSchema
41
+ };
42
+ }
20
43
 
21
- const sectorMap = await getInstrumentSectorMap();
22
- const leverageData = {};
44
+ _initSector(sector) {
45
+ if (!this.sectors.has(sector)) {
46
+ this.sectors.set(sector, {
47
+ '1x': 0,
48
+ '2x': 0,
49
+ '5x': 0,
50
+ '10x': 0,
51
+ 'inf': 0 // For other leverages
52
+ });
53
+ }
54
+ }
55
+
56
+ _getLeverageBucket(leverage) {
57
+ switch (leverage) {
58
+ case 1: return '1x';
59
+ case 2: return '2x';
60
+ case 5: return '5x';
61
+ case 10: return '10x';
62
+ default: return 'inf';
63
+ }
64
+ }
23
65
 
24
- for (const position of this.positions) {
25
- const instrumentId = position.InstrumentID;
26
- const sector = sectorMap[instrumentId] || 'N/A';
27
- const leverage = position.Leverage;
66
+ process(portfolioData, yesterdayPortfolio, userId, context) {
67
+ if (portfolioData?.context?.userType !== 'speculator') {
68
+ return;
69
+ }
70
+
71
+ if (!this.mappings) {
72
+ this.mappings = context.mappings;
73
+ }
74
+
75
+ const positions = portfolioData.PublicPositions;
76
+ if (!positions || !Array.isArray(positions) || !this.mappings) {
77
+ return;
78
+ }
28
79
 
29
- if (!leverageData[sector]) {
30
- leverageData[sector] = {};
31
- }
32
- leverageData[sector][leverage] = (leverageData[sector][leverage] || 0) + 1;
80
+ for (const pos of positions) {
81
+ const instrumentId = pos.InstrumentID;
82
+ if (!instrumentId) continue;
83
+
84
+ const sector = this.mappings.instrumentToSector[instrumentId] || 'Other';
85
+ this._initSector(sector);
86
+ const sectorData = this.sectors.get(sector);
87
+
88
+ const leverage = pos.Leverage || 1;
89
+ const key = this._getLeverageBucket(leverage);
90
+
91
+ sectorData[key]++;
33
92
  }
93
+ }
34
94
 
35
- return {
36
- leverage_per_sector: leverageData
37
- };
95
+ async getResult() {
96
+ // Convert Map to plain object
97
+ return Object.fromEntries(this.sectors);
38
98
  }
39
99
 
40
100
  reset() {
41
- this.positions = [];
101
+ this.sectors.clear();
102
+ this.mappings = null;
42
103
  }
43
104
  }
44
105