aiden-shared-calculations-unified 1.0.63 → 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,33 +1,88 @@
1
1
  /**
2
- * @fileoverview Calculates the average risk-reward ratio per asset from speculator positions.
2
+ * @fileoverview Calculation (Pass 1) for speculator metric.
3
+ *
4
+ * This metric answers: "What is the average Risk/Reward
5
+ * ratio (based on SL/TP) for speculator positions on each asset?"
3
6
  */
4
7
  const { loadInstrumentMappings } = require('../../utils/sector_mapping_provider');
5
8
 
6
9
  class RiskRewardRatioPerAsset {
7
10
  constructor() {
8
- this.rrData = {};
11
+ // { [instrumentId]: { sum_rr: 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
- for (const position of portfolioData.PublicPositions) {
15
- if (position.TakeProfitRate > 0 && position.StopLossRate > 0) {
16
- const instrumentId = position.InstrumentID;
17
- const openRate = position.OpenRate;
18
- const potentialReward = Math.abs(position.TakeProfitRate - openRate);
19
- const potentialRisk = Math.abs(openRate - position.StopLossRate);
20
-
21
- if (potentialRisk > 0) {
22
- const ratio = potentialReward / potentialRisk;
23
- if (!this.rrData[instrumentId]) {
24
- this.rrData[instrumentId] = { ratio_sum: 0, count: 0 };
25
- }
26
- this.rrData[instrumentId].ratio_sum += ratio;
27
- this.rrData[instrumentId].count++;
28
- }
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_rr_ratio": {
25
+ "type": "number",
26
+ "description": "Average Risk/Reward ratio (Reward / Risk)."
27
+ },
28
+ "count": {
29
+ "type": "number",
30
+ "description": "Count of positions with both SL and TP set."
29
31
  }
32
+ },
33
+ "required": ["avg_rr_ratio", "count"]
34
+ };
35
+
36
+ return {
37
+ "type": "object",
38
+ "description": "Calculates the average Risk/Reward ratio from SL/TP settings for each 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_rr: 0, count: 0 });
49
+ }
50
+ }
51
+
52
+ process(portfolioData) {
53
+ if (portfolioData?.context?.userType !== 'speculator') {
54
+ return;
55
+ }
56
+
57
+ const positions = portfolioData.PublicPositions;
58
+ if (!positions || !Array.isArray(positions)) {
59
+ return;
60
+ }
61
+
62
+ for (const pos of positions) {
63
+ const instrumentId = pos.InstrumentID;
64
+ const sl_rate = pos.StopLossRate || 0;
65
+ const tp_rate = pos.TakeProfitRate || 0;
66
+ const open_rate = pos.OpenRate || 0;
67
+
68
+ // Need all three to calculate R/R
69
+ if (!instrumentId || sl_rate === 0 || tp_rate === 0 || open_rate === 0) {
70
+ continue;
71
+ }
72
+
73
+ const risk = Math.abs(open_rate - sl_rate);
74
+ const reward = Math.abs(tp_rate - open_rate);
75
+
76
+ if (risk === 0) {
77
+ continue; // Cannot divide by zero
30
78
  }
79
+
80
+ const rr_ratio = reward / risk;
81
+
82
+ this._initAsset(instrumentId);
83
+ const assetData = this.assets.get(instrumentId);
84
+ assetData.sum_rr += rr_ratio;
85
+ assetData.count++;
31
86
  }
32
87
  }
33
88
 
@@ -35,26 +90,25 @@ class RiskRewardRatioPerAsset {
35
90
  if (!this.mappings) {
36
91
  this.mappings = await loadInstrumentMappings();
37
92
  }
38
- const result = {};
39
- for (const instrumentId in this.rrData) {
40
- const ticker = this.mappings.instrumentToTicker[instrumentId] || instrumentId.toString();
41
- const data = this.rrData[instrumentId];
42
93
 
43
- // REFACTOR: Perform the final calculation directly.
94
+ const result = {};
95
+ for (const [instrumentId, data] of this.assets.entries()) {
96
+ const ticker = this.mappings.instrumentToTicker[instrumentId] || `id_${instrumentId}`;
97
+
44
98
  if (data.count > 0) {
45
99
  result[ticker] = {
46
- average_ratio: data.ratio_sum / data.count
100
+ avg_rr_ratio: data.sum_rr / data.count,
101
+ count: data.count
47
102
  };
48
103
  }
49
104
  }
50
-
51
105
  return result;
52
106
  }
53
107
 
54
108
  reset() {
55
- this.rrData = {};
109
+ this.assets.clear();
56
110
  this.mappings = null;
57
111
  }
58
112
  }
59
113
 
60
- module.exports = RiskRewardRatioPerAsset;
114
+ module.exports = RiskRewardRatioPerAsset;
@@ -1,48 +1,110 @@
1
1
  /**
2
- * Aggregates and calculates average P/L, leverage, SL, and TP data for longs vs. shorts
3
- * on a per-asset basis for speculators.
2
+ * @fileoverview Calculation (Pass 1) for speculator metric.
3
+ *
4
+ * This metric answers: "For each asset, what are the average
5
+ * P&L, leverage, SL rate, and TP rate for *long* and *short*
6
+ * speculator positions?"
4
7
  */
5
8
  const { loadInstrumentMappings } = require('../../utils/sector_mapping_provider');
6
9
 
7
10
  class SpeculatorAssetSentiment {
8
11
  constructor() {
9
- this.assets = {};
12
+ // { [instrumentId]: { long: {...}, short: {...} } }
13
+ this.assets = new Map();
10
14
  this.mappings = null;
11
15
  }
12
16
 
17
+ /**
18
+ * Defines the output schema for this calculation.
19
+ * @returns {object} JSON Schema object
20
+ */
21
+ static getSchema() {
22
+ const sentimentDetailSchema = {
23
+ "type": "object",
24
+ "properties": {
25
+ "avg_pnl_pct": { "type": "number" },
26
+ "avg_leverage": { "type": "number" },
27
+ "sl_rate_pct": { "type": "number" },
28
+ "tp_rate_pct": { "type": "number" },
29
+ "count": { "type": "number" }
30
+ },
31
+ "required": ["avg_pnl_pct", "avg_leverage", "sl_rate_pct", "tp_rate_pct", "count"]
32
+ };
33
+
34
+ const tickerSchema = {
35
+ "type": "object",
36
+ "properties": {
37
+ "long": sentimentDetailSchema,
38
+ "short": sentimentDetailSchema
39
+ },
40
+ "required": ["long", "short"]
41
+ };
42
+
43
+ return {
44
+ "type": "object",
45
+ "description": "Aggregates P&L, leverage, and SL/TP usage for speculators, split by long/short positions per asset.",
46
+ "patternProperties": {
47
+ "^.*$": tickerSchema // Ticker
48
+ },
49
+ "additionalProperties": tickerSchema
50
+ };
51
+ }
52
+
13
53
  _initAsset(instrumentId) {
14
- if (!this.assets[instrumentId]) {
15
- this.assets[instrumentId] = {
16
- long: { count: 0, pnl_sum: 0, leverage_sum: 0, sl_rate_sum: 0, tp_rate_sum: 0 },
17
- short: { count: 0, pnl_sum: 0, leverage_sum: 0, sl_rate_sum: 0, tp_rate_sum: 0 }
18
- };
54
+ if (!this.assets.has(instrumentId)) {
55
+ const createSide = () => ({
56
+ pnl_sum: 0,
57
+ leverage_sum: 0,
58
+ sl_set_count: 0,
59
+ tp_set_count: 0,
60
+ count: 0
61
+ });
62
+ this.assets.set(instrumentId, {
63
+ long: createSide(),
64
+ short: createSide()
65
+ });
19
66
  }
20
67
  }
21
68
 
22
- process(portfolioData, yesterdayPortfolio, userId, context) {
69
+ process(portfolioData) {
70
+ if (portfolioData?.context?.userType !== 'speculator') {
71
+ return;
72
+ }
73
+
23
74
  const positions = portfolioData.PublicPositions;
24
- if (!positions || !Array.isArray(positions)) return;
75
+ if (!positions || !Array.isArray(positions)) {
76
+ return;
77
+ }
25
78
 
26
- for (const position of positions) {
27
- const instrumentId = position.InstrumentID;
79
+ for (const pos of positions) {
80
+ const instrumentId = pos.InstrumentID;
28
81
  if (!instrumentId) continue;
29
-
82
+
30
83
  this._initAsset(instrumentId);
31
-
32
- const direction = position.IsBuy ? 'long' : 'short';
33
- const stats = this.assets[instrumentId][direction];
34
-
35
- stats.count++;
36
- stats.pnl_sum += position.NetProfit;
37
- stats.leverage_sum += position.Leverage;
38
-
39
- if (position.StopLossRate && position.StopLossRate > 0) {
40
- stats.sl_rate_sum += position.StopLossRate;
41
- }
42
- if (position.TakeProfitRate && position.TakeProfitRate > 0) {
43
- stats.tp_rate_sum += position.TakeProfitRate;
44
- }
84
+ const sideData = pos.IsBuy ? this.assets.get(instrumentId).long : this.assets.get(instrumentId).short;
85
+
86
+ const pnl_percent = (pos.NetProfit || 0) / (pos.Amount || 1);
87
+
88
+ sideData.pnl_sum += pnl_percent;
89
+ sideData.leverage_sum += pos.Leverage || 1;
90
+ if (pos.StopLossRate) sideData.sl_set_count++;
91
+ if (pos.TakeProfitRate) sideData.tp_set_count++;
92
+ sideData.count++;
93
+ }
94
+ }
95
+
96
+ _calculateAverages(data) {
97
+ const count = data.count;
98
+ if (count === 0) {
99
+ return { avg_pnl_pct: 0, avg_leverage: 0, sl_rate_pct: 0, tp_rate_pct: 0, count: 0 };
45
100
  }
101
+ return {
102
+ avg_pnl_pct: data.pnl_sum / count,
103
+ avg_leverage: data.leverage_sum / count,
104
+ sl_rate_pct: (data.sl_set_count / count) * 100,
105
+ tp_rate_pct: (data.tp_set_count / count) * 100,
106
+ count: count
107
+ };
46
108
  }
47
109
 
48
110
  async getResult() {
@@ -50,32 +112,22 @@ class SpeculatorAssetSentiment {
50
112
  this.mappings = await loadInstrumentMappings();
51
113
  }
52
114
 
53
- const finalResult = {};
54
-
55
- for (const instrumentId in this.assets) {
56
- const ticker = this.mappings.instrumentToTicker[instrumentId] || instrumentId.toString();
57
- finalResult[ticker] = {};
58
-
59
- for (const direction in this.assets[instrumentId]) {
60
- const stats = this.assets[instrumentId][direction];
61
- // REFACTOR: Perform final calculations.
62
- finalResult[ticker][direction] = {
63
- count: stats.count,
64
- average_pnl: stats.count > 0 ? stats.pnl_sum / stats.count : 0,
65
- average_leverage: stats.count > 0 ? stats.leverage_sum / stats.count : 0,
66
- average_sl_rate: stats.count > 0 ? stats.sl_rate_sum / stats.count : 0,
67
- average_tp_rate: stats.count > 0 ? stats.tp_rate_sum / stats.count : 0,
68
- };
69
- }
115
+ const result = {};
116
+ for (const [instrumentId, data] of this.assets.entries()) {
117
+ const ticker = this.mappings.instrumentToTicker[instrumentId] || `id_${instrumentId}`;
118
+
119
+ result[ticker] = {
120
+ long: this._calculateAverages(data.long),
121
+ short: this._calculateAverages(data.short)
122
+ };
70
123
  }
71
-
72
- return finalResult;
124
+ return result;
73
125
  }
74
126
 
75
127
  reset() {
76
- this.assets = {};
128
+ this.assets.clear();
77
129
  this.mappings = null;
78
130
  }
79
131
  }
80
132
 
81
- module.exports = SpeculatorAssetSentiment;
133
+ module.exports = SpeculatorAssetSentiment;
@@ -1,57 +1,125 @@
1
1
  /**
2
- * Counts speculator positions that are close to their stop loss ("Danger Zone").
3
- * This example defines "close" as within 5% of the current rate.
2
+ * @fileoverview Calculation (Pass 1) for speculator metric.
3
+ *
4
+ * This metric answers: "For each asset, how many long and
5
+ * short speculator positions are within 5% of their stop loss?"
4
6
  */
7
+ const { loadInstrumentMappings } = require('../../utils/sector_mapping_provider');
8
+
5
9
  class SpeculatorDangerZone {
6
10
  constructor() {
7
- this.danger_zone = {};
11
+ // { [instrumentId]: { long_in_danger: 0, short_in_danger: 0, long_total: 0, short_total: 0 } }
12
+ this.assets = new Map();
13
+ this.mappings = null;
8
14
  }
9
15
 
10
- _initAsset(ticker) {
11
- if (!this.danger_zone[ticker]) {
12
- this.danger_zone[ticker] = { long_danger_count: 0, short_danger_count: 0 };
13
- }
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
+ "long_in_danger": { "type": "number" },
25
+ "short_in_danger": { "type": "number" },
26
+ "long_total": { "type": "number" },
27
+ "short_total": { "type": "number" },
28
+ "long_danger_pct": {
29
+ "type": "number",
30
+ "description": "Percentage of long positions in the danger zone."
31
+ },
32
+ "short_danger_pct": {
33
+ "type": "number",
34
+ "description": "Percentage of short positions in the danger zone."
35
+ }
36
+ },
37
+ "required": ["long_in_danger", "short_in_danger", "long_total", "short_total", "long_danger_pct", "short_danger_pct"]
38
+ };
39
+
40
+ return {
41
+ "type": "object",
42
+ "description": "Tracks speculator positions within 5% of their Stop Loss.",
43
+ "patternProperties": {
44
+ "^.*$": tickerSchema // Ticker
45
+ },
46
+ "additionalProperties": tickerSchema
47
+ };
14
48
  }
15
49
 
16
- process(portfolioData, yesterdayPortfolio, userId, context) {
17
- const { instrumentMappings } = context;
50
+ _initAsset(instrumentId) {
51
+ if (!this.assets.has(instrumentId)) {
52
+ this.assets.set(instrumentId, {
53
+ long_in_danger: 0,
54
+ short_in_danger: 0,
55
+ long_total: 0,
56
+ short_total: 0
57
+ });
58
+ }
59
+ }
18
60
 
19
- // FIX: Use the correct PublicPositions property for speculators
61
+ process(portfolioData) {
62
+ if (portfolioData?.context?.userType !== 'speculator') {
63
+ return;
64
+ }
65
+
20
66
  const positions = portfolioData.PublicPositions;
21
- if (!positions || !Array.isArray(positions)) return;
67
+ if (!positions || !Array.isArray(positions)) {
68
+ return;
69
+ }
22
70
 
23
- for (const position of positions) {
24
- // Ensure position has a valid stop loss and rate
25
- if (!position.StopLossRate || position.StopLossRate <= 0 || !position.CurrentRate) {
71
+ for (const pos of positions) {
72
+ const instrumentId = pos.InstrumentID;
73
+ const sl_rate = pos.StopLossRate || 0;
74
+ const current_price = pos.LastCloseRate || 0;
75
+
76
+ if (!instrumentId || sl_rate === 0 || current_price === 0) {
26
77
  continue;
27
78
  }
79
+
80
+ this._initAsset(instrumentId);
81
+ const assetData = this.assets.get(instrumentId);
82
+
83
+ const distance = Math.abs(current_price - sl_rate);
84
+ const distance_pct = (distance / current_price);
85
+
86
+ const isInDanger = distance_pct <= 0.05; // 5% danger zone
28
87
 
29
- // FIX: Use the correct PascalCase InstrumentID
30
- const ticker = instrumentMappings[position.InstrumentID];
31
- if (!ticker) continue;
32
-
33
- this._initAsset(ticker);
34
-
35
- let distance_percent = 0;
36
- // FIX: Use the correct PascalCase IsBuy
37
- if (position.IsBuy) {
38
- // Long position: Danger if SL is just below current rate
39
- distance_percent = (position.CurrentRate - position.StopLossRate) / position.CurrentRate;
40
- if (distance_percent > 0 && distance_percent < 0.05) {
41
- this.danger_zone[ticker].long_danger_count++;
88
+ if (pos.IsBuy) {
89
+ assetData.long_total++;
90
+ if (isInDanger) {
91
+ assetData.long_in_danger++;
42
92
  }
43
93
  } else {
44
- // Short position: Danger if SL is just above current rate
45
- distance_percent = (position.StopLossRate - position.CurrentRate) / position.CurrentRate;
46
- if (distance_percent > 0 && distance_percent < 0.05) {
47
- this.danger_zone[ticker].short_danger_count++;
94
+ assetData.short_total++;
95
+ if (isInDanger) {
96
+ assetData.short_in_danger++;
48
97
  }
49
98
  }
50
99
  }
51
100
  }
52
101
 
53
- getResult() {
54
- return this.danger_zone;
102
+ async getResult() {
103
+ if (!this.mappings) {
104
+ this.mappings = await loadInstrumentMappings();
105
+ }
106
+
107
+ const result = {};
108
+ for (const [instrumentId, data] of this.assets.entries()) {
109
+ const ticker = this.mappings.instrumentToTicker[instrumentId] || `id_${instrumentId}`;
110
+
111
+ result[ticker] = {
112
+ ...data,
113
+ long_danger_pct: (data.long_total > 0) ? (data.long_in_danger / data.long_total) * 100 : 0,
114
+ short_danger_pct: (data.short_total > 0) ? (data.short_in_danger / data.short_total) * 100 : 0
115
+ };
116
+ }
117
+ return result;
118
+ }
119
+
120
+ reset() {
121
+ this.assets.clear();
122
+ this.mappings = null;
55
123
  }
56
124
  }
57
125
 
@@ -1,91 +1,118 @@
1
1
  /**
2
- * @fileoverview Calculates the average stop loss distance (percent and value)
3
- * for long and short positions, grouped by SECTOR.
2
+ * @fileoverview Calculation (Pass 1) for speculator metric.
3
+ *
4
+ * This metric answers: "For each sector, what is the
5
+ * average stop loss distance (in % and value) for
6
+ * long and short positions?"
4
7
  */
5
- const { getInstrumentSectorMap } = require('../../utils/sector_mapping_provider');
8
+ const { loadInstrumentMappings } = require('../../utils/sector_mapping_provider');
6
9
 
7
- class StopLossDistanceBySector {
10
+ class StopLossDistanceBySectorShortLongBreakdown {
8
11
  constructor() {
9
- this.instrumentData = {};
10
- this.instrumentToSector = null;
12
+ // { [sector]: { long_pct: [], long_val: [], short_pct: [], short_val: [] } }
13
+ this.sectors = new Map();
14
+ this.mappings = null;
11
15
  }
12
16
 
13
- process(portfolioData, yesterdayPortfolio, userId, context) {
14
- if (!portfolioData || !portfolioData.PublicPositions) return;
15
-
16
- for (const position of portfolioData.PublicPositions) {
17
- const { InstrumentID, Leverage, StopLossRate, CurrentRate, IsBuy } = position;
18
- if (Leverage <= 1 || StopLossRate <= 0.0001 || CurrentRate <= 0) continue;
17
+ /**
18
+ * Defines the output schema for this calculation.
19
+ * @returns {object} JSON Schema object
20
+ */
21
+ static getSchema() {
22
+ const sectorSchema = {
23
+ "type": "object",
24
+ "properties": {
25
+ "long_avg_dist_pct": { "type": "number" },
26
+ "long_avg_dist_val": { "type": "number" },
27
+ "long_count": { "type": "number" },
28
+ "short_avg_dist_pct": { "type": "number" },
29
+ "short_avg_dist_val": { "type": "number" },
30
+ "short_count": { "type": "number" }
31
+ },
32
+ "required": ["long_avg_dist_pct", "long_avg_dist_val", "long_count", "short_avg_dist_pct", "short_avg_dist_val", "short_count"]
33
+ };
19
34
 
20
- const distance_value = IsBuy ? CurrentRate - StopLossRate : StopLossRate - CurrentRate;
21
- const distance_percent = (distance_value / CurrentRate) * 100;
35
+ return {
36
+ "type": "object",
37
+ "description": "Calculates avg SL distance (% and value) for long/short positions, grouped by sector.",
38
+ "patternProperties": {
39
+ "^.*$": sectorSchema // Sector
40
+ },
41
+ "additionalProperties": sectorSchema
42
+ };
43
+ }
22
44
 
23
- if (distance_percent > 0) {
24
- const posType = IsBuy ? 'long' : 'short';
25
- if (!this.instrumentData[InstrumentID]) this.instrumentData[InstrumentID] = {};
26
- if (!this.instrumentData[InstrumentID][posType]) {
27
- this.instrumentData[InstrumentID][posType] = {
28
- distance_percent_sum: 0,
29
- distance_value_sum: 0,
30
- count: 0
31
- };
32
- }
33
- const agg = this.instrumentData[InstrumentID][posType];
34
- agg.distance_percent_sum += distance_percent;
35
- agg.distance_value_sum += distance_value;
36
- agg.count++;
37
- }
45
+ _initSector(sector) {
46
+ if (!this.sectors.has(sector)) {
47
+ this.sectors.set(sector, {
48
+ long_pct: [], long_val: [], short_pct: [], short_val: []
49
+ });
38
50
  }
39
51
  }
52
+
53
+ _avg(arr) {
54
+ if (arr.length === 0) return 0;
55
+ return arr.reduce((a, b) => a + b, 0) / arr.length;
56
+ }
40
57
 
41
- async getResult() {
42
- if (Object.keys(this.instrumentData).length === 0) return {};
43
- if (!this.instrumentToSector) {
44
- this.instrumentToSector = await getInstrumentSectorMap();
58
+ process(portfolioData, yesterdayPortfolio, userId, context) {
59
+ if (portfolioData?.context?.userType !== 'speculator') {
60
+ return;
61
+ }
62
+ if (!this.mappings) {
63
+ this.mappings = context.mappings;
64
+ }
65
+
66
+ const positions = portfolioData.PublicPositions;
67
+ if (!positions || !Array.isArray(positions) || !this.mappings) {
68
+ return;
45
69
  }
46
70
 
47
- const sectorData = {};
48
- for (const instrumentId in this.instrumentData) {
49
- const sector = this.instrumentToSector[instrumentId] || 'N/A';
50
- if (!sectorData[sector]) sectorData[sector] = {};
51
-
52
- for (const posType in this.instrumentData[instrumentId]) {
53
- if (!sectorData[sector][posType]) {
54
- sectorData[sector][posType] = {
55
- distance_percent_sum: 0,
56
- distance_value_sum: 0,
57
- count: 0
58
- };
59
- }
60
- const source = this.instrumentData[instrumentId][posType];
61
- const target = sectorData[sector][posType];
62
- target.distance_percent_sum += source.distance_percent_sum;
63
- target.distance_value_sum += source.distance_value_sum;
64
- target.count += source.count;
71
+ for (const pos of positions) {
72
+ const instrumentId = pos.InstrumentID;
73
+ const sl_rate = pos.StopLossRate || 0;
74
+ const open_rate = pos.OpenRate || 0;
75
+
76
+ if (!instrumentId || sl_rate === 0 || open_rate === 0) {
77
+ continue;
78
+ }
79
+
80
+ const sector = this.mappings.instrumentToSector[instrumentId] || 'Other';
81
+ this._initSector(sector);
82
+ const sectorData = this.sectors.get(sector);
83
+
84
+ const distance_val = Math.abs(open_rate - sl_rate);
85
+ const distance_pct = (distance_val / open_rate) * 100;
86
+
87
+ if (pos.IsBuy) {
88
+ sectorData.long_pct.push(distance_pct);
89
+ sectorData.long_val.push(distance_val);
90
+ } else {
91
+ sectorData.short_pct.push(distance_pct);
92
+ sectorData.short_val.push(distance_val);
65
93
  }
66
94
  }
95
+ }
67
96
 
97
+ async getResult() {
68
98
  const result = {};
69
- for (const sector in sectorData) {
70
- result[sector] = {};
71
- for (const posType in sectorData[sector]) {
72
- const data = sectorData[sector][posType];
73
- // REFACTOR: Perform final calculation and return in standardized format.
74
- if (data.count > 0) {
75
- result[sector][posType] = {
76
- average_distance_percent: data.distance_percent_sum / data.count,
77
- average_distance_value: data.distance_value_sum / data.count,
78
- count: data.count
79
- };
80
- }
81
- }
99
+ for (const [sector, data] of this.sectors.entries()) {
100
+ result[sector] = {
101
+ long_avg_dist_pct: this._avg(data.long_pct),
102
+ long_avg_dist_val: this._avg(data.long_val),
103
+ long_count: data.long_pct.length,
104
+ short_avg_dist_pct: this._avg(data.short_pct),
105
+ short_avg_dist_val: this._avg(data.short_val),
106
+ short_count: data.short_pct.length
107
+ };
82
108
  }
83
109
  return result;
84
110
  }
85
111
 
86
112
  reset() {
87
- this.instrumentData = {};
113
+ this.sectors.clear();
114
+ this.mappings = null;
88
115
  }
89
116
  }
90
117
 
91
- module.exports = StopLossDistanceBySector;
118
+ module.exports = StopLossDistanceBySectorShortLongBreakdown;