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,48 +1,89 @@
1
1
  /**
2
- * @fileoverview Calculates the average distance to the stop loss from the current price for
3
- * both long and short positions, grouped by asset and leverage level.
4
- * It ignores near-zero stop loss values.
2
+ * @fileoverview Calculation (Pass 1) for speculator metric.
3
+ *
4
+ * This metric answers: "For each asset and leverage level,
5
+ * what is the average percentage distance from the *current price*
6
+ * to the stop loss?"
5
7
  */
6
8
  const { loadInstrumentMappings } = require('../../utils/sector_mapping_provider');
7
9
 
8
10
  class DistanceToStopLossPerLeverage {
9
11
  constructor() {
10
- this.stopLossData = {};
12
+ // { [instrumentId]: { [leverage]: { sum_dist: 0, count: 0 } } }
13
+ this.assets = new Map();
11
14
  this.mappings = null;
12
15
  }
13
16
 
14
- process(portfolioData, yesterdayPortfolio, userId, context) {
15
- if (portfolioData && portfolioData.PublicPositions) {
16
- for (const position of portfolioData.PublicPositions) {
17
- const instrumentId = position.InstrumentID;
18
- const leverage = position.Leverage;
19
- const stopLossRate = position.StopLossRate;
20
- const currentRate = position.CurrentRate;
21
- const isBuy = position.IsBuy;
17
+ /**
18
+ * Defines the output schema for this calculation.
19
+ * @returns {object} JSON Schema object
20
+ */
21
+ static getSchema() {
22
+ const leverageSchema = {
23
+ "type": "object",
24
+ "description": "Average SL distance grouped by leverage.",
25
+ "patternProperties": {
26
+ // Leverage level, e.g., "1x", "5x", "10x"
27
+ "^[0-9]+x$": {
28
+ "type": "number",
29
+ "description": "The average percentage distance from current price to Stop Loss for this leverage level."
30
+ }
31
+ },
32
+ "additionalProperties": { "type": "number" }
33
+ };
22
34
 
23
- if (stopLossRate > 0.0001 && currentRate > 0) {
24
- let distance;
35
+ return {
36
+ "type": "object",
37
+ "description": "Calculates avg % distance from current price to SL, bucketed by asset and leverage.",
38
+ "patternProperties": {
39
+ "^.*$": leverageSchema // Ticker
40
+ },
41
+ "additionalProperties": leverageSchema
42
+ };
43
+ }
25
44
 
26
- if (isBuy) {
27
- distance = currentRate - stopLossRate;
28
- } else {
29
- distance = stopLossRate - currentRate;
30
- }
31
-
32
- const distancePercent = (distance / currentRate) * 100;
45
+ _initLeverage(assetData, leverage) {
46
+ const key = `${leverage}x`;
47
+ if (!assetData[key]) {
48
+ assetData[key] = { sum_dist: 0, count: 0 };
49
+ }
50
+ return key;
51
+ }
33
52
 
34
- if (distancePercent > 0) {
35
- if (!this.stopLossData[instrumentId]) {
36
- this.stopLossData[instrumentId] = {};
37
- }
38
- if (!this.stopLossData[instrumentId][leverage]) {
39
- this.stopLossData[instrumentId][leverage] = { distance_sum_percent: 0, count: 0 };
40
- }
41
- this.stopLossData[instrumentId][leverage].distance_sum_percent += distancePercent;
42
- this.stopLossData[instrumentId][leverage].count++;
43
- }
44
- }
53
+ process(portfolioData) {
54
+ // This calculation is only for speculators
55
+ if (portfolioData?.context?.userType !== 'speculator') {
56
+ return;
57
+ }
58
+
59
+ const positions = portfolioData.PublicPositions;
60
+ if (!positions || !Array.isArray(positions)) {
61
+ return;
62
+ }
63
+
64
+ for (const pos of positions) {
65
+ const instrumentId = pos.InstrumentID;
66
+ const sl_rate = pos.StopLossRate || 0;
67
+ const current_price = pos.LastCloseRate || 0; // Use last close as current price
68
+
69
+ if (!instrumentId || sl_rate === 0 || current_price === 0) {
70
+ continue; // Skip positions without SL or price
71
+ }
72
+
73
+ if (!this.assets.has(instrumentId)) {
74
+ this.assets.set(instrumentId, {});
45
75
  }
76
+ const assetData = this.assets.get(instrumentId);
77
+
78
+ const leverage = pos.Leverage || 1;
79
+ const key = this._initLeverage(assetData, leverage);
80
+
81
+ // Calculate distance
82
+ const distance = Math.abs(current_price - sl_rate);
83
+ const distance_pct = (distance / current_price);
84
+
85
+ assetData[key].sum_dist += distance_pct;
86
+ assetData[key].count++;
46
87
  }
47
88
  }
48
89
 
@@ -52,17 +93,15 @@ class DistanceToStopLossPerLeverage {
52
93
  }
53
94
 
54
95
  const result = {};
55
- for (const instrumentId in this.stopLossData) {
56
- const ticker = this.mappings.instrumentToTicker[instrumentId] || instrumentId.toString();
96
+ for (const [instrumentId, leverageData] of this.assets.entries()) {
97
+ const ticker = this.mappings.instrumentToTicker[instrumentId] || `id_${instrumentId}`;
57
98
  result[ticker] = {};
58
- for (const leverage in this.stopLossData[instrumentId]) {
59
- const data = this.stopLossData[instrumentId][leverage];
60
- // REFACTOR: Perform final calculation directly.
99
+
100
+ for (const leverageKey in leverageData) {
101
+ const data = leverageData[leverageKey];
61
102
  if (data.count > 0) {
62
- result[ticker][leverage] = {
63
- average_distance_percent: data.distance_sum_percent / data.count,
64
- count: data.count
65
- };
103
+ // Store the final average distance * 100 for percentage
104
+ result[ticker][leverageKey] = (data.sum_dist / data.count) * 100;
66
105
  }
67
106
  }
68
107
  }
@@ -70,9 +109,9 @@ class DistanceToStopLossPerLeverage {
70
109
  }
71
110
 
72
111
  reset() {
73
- this.stopLossData = {};
112
+ this.assets.clear();
74
113
  this.mappings = null;
75
114
  }
76
115
  }
77
116
 
78
- module.exports = DistanceToStopLossPerLeverage;
117
+ module.exports = DistanceToStopLossPerLeverage;
@@ -1,47 +1,88 @@
1
1
  /**
2
- * @fileoverview Calculates the average distance to the take profit from the current price for
3
- * both long and short positions, grouped by asset and leverage level.
4
- * It ignores positions where take profit is not set.
2
+ * @fileoverview Calculation (Pass 1) for speculator metric.
3
+ *
4
+ * This metric answers: "For each asset and leverage level,
5
+ * what is the average percentage distance from the *current price*
6
+ * to the take profit?"
5
7
  */
6
8
  const { loadInstrumentMappings } = require('../../utils/sector_mapping_provider');
7
9
 
8
10
  class DistanceToTakeProfitPerLeverage {
9
11
  constructor() {
10
- this.takeProfitData = {};
12
+ // { [instrumentId]: { [leverage]: { sum_dist: 0, count: 0 } } }
13
+ this.assets = new Map();
11
14
  this.mappings = null;
12
15
  }
13
16
 
14
- process(portfolioData, yesterdayPortfolio, userId, context) {
15
- if (portfolioData && portfolioData.PublicPositions) {
16
- for (const position of portfolioData.PublicPositions) {
17
- const instrumentId = position.InstrumentID;
18
- const leverage = position.Leverage;
19
- const takeProfitRate = position.TakeProfitRate;
20
- const currentRate = position.CurrentRate;
21
- const isBuy = position.IsBuy;
17
+ /**
18
+ * Defines the output schema for this calculation.
19
+ * @returns {object} JSON Schema object
20
+ */
21
+ static getSchema() {
22
+ const leverageSchema = {
23
+ "type": "object",
24
+ "description": "Average TP distance grouped by leverage.",
25
+ "patternProperties": {
26
+ // Leverage level, e.g., "1x", "5x", "10x"
27
+ "^[0-9]+x$": {
28
+ "type": "number",
29
+ "description": "The average percentage distance from current price to Take Profit for this leverage level."
30
+ }
31
+ },
32
+ "additionalProperties": { "type": "number" }
33
+ };
22
34
 
23
- if (takeProfitRate > 0.0001 && currentRate > 0) {
24
- let distance;
25
- if (isBuy) {
26
- distance = takeProfitRate - currentRate;
27
- } else {
28
- distance = currentRate - takeProfitRate;
29
- }
35
+ return {
36
+ "type": "object",
37
+ "description": "Calculates avg % distance from current price to TP, bucketed by asset and leverage.",
38
+ "patternProperties": {
39
+ "^.*$": leverageSchema // Ticker
40
+ },
41
+ "additionalProperties": leverageSchema
42
+ };
43
+ }
30
44
 
31
- const distancePercent = (distance / currentRate) * 100;
45
+ _initLeverage(assetData, leverage) {
46
+ const key = `${leverage}x`;
47
+ if (!assetData[key]) {
48
+ assetData[key] = { sum_dist: 0, count: 0 };
49
+ }
50
+ return key;
51
+ }
32
52
 
33
- if (distancePercent > 0) {
34
- if (!this.takeProfitData[instrumentId]) {
35
- this.takeProfitData[instrumentId] = {};
36
- }
37
- if (!this.takeProfitData[instrumentId][leverage]) {
38
- this.takeProfitData[instrumentId][leverage] = { distance_sum_percent: 0, count: 0 };
39
- }
40
- this.takeProfitData[instrumentId][leverage].distance_sum_percent += distancePercent;
41
- this.takeProfitData[instrumentId][leverage].count++;
42
- }
43
- }
53
+ process(portfolioData) {
54
+ // This calculation is only for speculators
55
+ if (portfolioData?.context?.userType !== 'speculator') {
56
+ return;
57
+ }
58
+
59
+ const positions = portfolioData.PublicPositions;
60
+ if (!positions || !Array.isArray(positions)) {
61
+ return;
62
+ }
63
+
64
+ for (const pos of positions) {
65
+ const instrumentId = pos.InstrumentID;
66
+ const tp_rate = pos.TakeProfitRate || 0;
67
+ const current_price = pos.LastCloseRate || 0;
68
+
69
+ if (!instrumentId || tp_rate === 0 || current_price === 0) {
70
+ continue; // Skip positions without TP or price
44
71
  }
72
+
73
+ if (!this.assets.has(instrumentId)) {
74
+ this.assets.set(instrumentId, {});
75
+ }
76
+ const assetData = this.assets.get(instrumentId);
77
+
78
+ const leverage = pos.Leverage || 1;
79
+ const key = this._initLeverage(assetData, leverage);
80
+
81
+ const distance = Math.abs(tp_rate - current_price);
82
+ const distance_pct = (distance / current_price);
83
+
84
+ assetData[key].sum_dist += distance_pct;
85
+ assetData[key].count++;
45
86
  }
46
87
  }
47
88
 
@@ -49,18 +90,16 @@ class DistanceToTakeProfitPerLeverage {
49
90
  if (!this.mappings) {
50
91
  this.mappings = await loadInstrumentMappings();
51
92
  }
93
+
52
94
  const result = {};
53
- for (const instrumentId in this.takeProfitData) {
54
- const ticker = this.mappings.instrumentToTicker[instrumentId] || instrumentId.toString();
95
+ for (const [instrumentId, leverageData] of this.assets.entries()) {
96
+ const ticker = this.mappings.instrumentToTicker[instrumentId] || `id_${instrumentId}`;
55
97
  result[ticker] = {};
56
- for (const leverage in this.takeProfitData[instrumentId]) {
57
- const data = this.takeProfitData[instrumentId][leverage];
58
- // REFACTOR: Perform final calculation directly.
98
+
99
+ for (const leverageKey in leverageData) {
100
+ const data = leverageData[leverageKey];
59
101
  if (data.count > 0) {
60
- result[ticker][leverage] = {
61
- average_distance_percent: data.distance_sum_percent / data.count,
62
- count: data.count
63
- };
102
+ result[ticker][leverageKey] = (data.sum_dist / data.count) * 100;
64
103
  }
65
104
  }
66
105
  }
@@ -68,9 +107,9 @@ class DistanceToTakeProfitPerLeverage {
68
107
  }
69
108
 
70
109
  reset() {
71
- this.takeProfitData = {};
110
+ this.assets.clear();
72
111
  this.mappings = null;
73
112
  }
74
113
  }
75
114
 
76
- module.exports = DistanceToTakeProfitPerLeverage;
115
+ module.exports = DistanceToTakeProfitPerLeverage;
@@ -1,48 +1,87 @@
1
1
  /**
2
- * @fileoverview Calculates the average percentage distance from the entry price (OpenRate)
3
- * to the stop loss rate for both long and short positions, grouped by asset and leverage level.
4
- * It ignores near-zero stop loss values, which signify no stop loss is set.
2
+ * @fileoverview Calculation (Pass 1) for speculator metric.
3
+ *
4
+ * This metric answers: "For each asset and leverage level,
5
+ * what is the average percentage distance from the *entry price*
6
+ * to the stop loss?" (This defines the intended risk).
5
7
  */
6
8
  const { loadInstrumentMappings } = require('../../utils/sector_mapping_provider');
7
9
 
8
- class EntryDistanceToStopLossPerLeverage {
10
+ class EntryDistanceToSlPerLeverage {
9
11
  constructor() {
10
- this.stopLossData = {};
12
+ // { [instrumentId]: { [leverage]: { sum_dist: 0, count: 0 } } }
13
+ this.assets = new Map();
11
14
  this.mappings = null;
12
15
  }
13
16
 
14
- process(portfolioData, yesterdayPortfolio, userId, context) {
15
- if (portfolioData && portfolioData.PublicPositions) {
16
- for (const position of portfolioData.PublicPositions) {
17
- const instrumentId = position.InstrumentID;
18
- const leverage = position.Leverage;
19
- const stopLossRate = position.StopLossRate;
20
- const openRate = position.OpenRate;
21
- const isBuy = position.IsBuy;
17
+ /**
18
+ * Defines the output schema for this calculation.
19
+ * @returns {object} JSON Schema object
20
+ */
21
+ static getSchema() {
22
+ const leverageSchema = {
23
+ "type": "object",
24
+ "description": "Average entry-to-SL distance grouped by leverage.",
25
+ "patternProperties": {
26
+ // Leverage level, e.g., "1x", "5x", "10x"
27
+ "^[0-9]+x$": {
28
+ "type": "number",
29
+ "description": "The average percentage distance from entry price to Stop Loss for this leverage level."
30
+ }
31
+ },
32
+ "additionalProperties": { "type": "number" }
33
+ };
22
34
 
23
- if (stopLossRate > 0.0001 && openRate > 0) {
24
- let distance;
25
-
26
- if (isBuy) {
27
- distance = openRate - stopLossRate;
28
- } else {
29
- distance = stopLossRate - openRate;
30
- }
35
+ return {
36
+ "type": "object",
37
+ "description": "Calculates avg % distance from entry price to SL, bucketed by asset and leverage.",
38
+ "patternProperties": {
39
+ "^.*$": leverageSchema // Ticker
40
+ },
41
+ "additionalProperties": leverageSchema
42
+ };
43
+ }
31
44
 
32
- const distancePercent = (distance / openRate) * 100;
45
+ _initLeverage(assetData, leverage) {
46
+ const key = `${leverage}x`;
47
+ if (!assetData[key]) {
48
+ assetData[key] = { sum_dist: 0, count: 0 };
49
+ }
50
+ return key;
51
+ }
33
52
 
34
- if (distancePercent > 0) {
35
- if (!this.stopLossData[instrumentId]) {
36
- this.stopLossData[instrumentId] = {};
37
- }
38
- if (!this.stopLossData[instrumentId][leverage]) {
39
- this.stopLossData[instrumentId][leverage] = { distance_sum_percent: 0, count: 0 };
40
- }
41
- this.stopLossData[instrumentId][leverage].distance_sum_percent += distancePercent;
42
- this.stopLossData[instrumentId][leverage].count++;
43
- }
44
- }
53
+ process(portfolioData) {
54
+ if (portfolioData?.context?.userType !== 'speculator') {
55
+ return;
56
+ }
57
+
58
+ const positions = portfolioData.PublicPositions;
59
+ if (!positions || !Array.isArray(positions)) {
60
+ return;
61
+ }
62
+
63
+ for (const pos of positions) {
64
+ const instrumentId = pos.InstrumentID;
65
+ const sl_rate = pos.StopLossRate || 0;
66
+ const open_rate = pos.OpenRate || 0;
67
+
68
+ if (!instrumentId || sl_rate === 0 || open_rate === 0) {
69
+ continue; // Skip positions without SL or open rate
70
+ }
71
+
72
+ if (!this.assets.has(instrumentId)) {
73
+ this.assets.set(instrumentId, {});
45
74
  }
75
+ const assetData = this.assets.get(instrumentId);
76
+
77
+ const leverage = pos.Leverage || 1;
78
+ const key = this._initLeverage(assetData, leverage);
79
+
80
+ const distance = Math.abs(open_rate - sl_rate);
81
+ const distance_pct = (distance / open_rate);
82
+
83
+ assetData[key].sum_dist += distance_pct;
84
+ assetData[key].count++;
46
85
  }
47
86
  }
48
87
 
@@ -52,17 +91,14 @@ class EntryDistanceToStopLossPerLeverage {
52
91
  }
53
92
 
54
93
  const result = {};
55
- for (const instrumentId in this.stopLossData) {
56
- const ticker = this.mappings.instrumentToTicker[instrumentId] || instrumentId.toString();
94
+ for (const [instrumentId, leverageData] of this.assets.entries()) {
95
+ const ticker = this.mappings.instrumentToTicker[instrumentId] || `id_${instrumentId}`;
57
96
  result[ticker] = {};
58
- for (const leverage in this.stopLossData[instrumentId]) {
59
- const data = this.stopLossData[instrumentId][leverage];
60
- // REFACTOR: Perform final calculation directly.
97
+
98
+ for (const leverageKey in leverageData) {
99
+ const data = leverageData[leverageKey];
61
100
  if (data.count > 0) {
62
- result[ticker][leverage] = {
63
- average_distance_percent: data.distance_sum_percent / data.count,
64
- count: data.count
65
- };
101
+ result[ticker][leverageKey] = (data.sum_dist / data.count) * 100;
66
102
  }
67
103
  }
68
104
  }
@@ -70,9 +106,9 @@ class EntryDistanceToStopLossPerLeverage {
70
106
  }
71
107
 
72
108
  reset() {
73
- this.stopLossData = {};
109
+ this.assets.clear();
74
110
  this.mappings = null;
75
111
  }
76
112
  }
77
113
 
78
- module.exports = EntryDistanceToStopLossPerLeverage;
114
+ module.exports = EntryDistanceToSlPerLeverage;
@@ -1,48 +1,87 @@
1
1
  /**
2
- * @fileoverview Calculates the average percentage distance from the entry price (OpenRate)
3
- * to the take profit rate for both long and short positions, grouped by asset and leverage level.
4
- * It ignores positions where the take profit is not meaningfully set.
2
+ * @fileoverview Calculation (Pass 1) for speculator metric.
3
+ *
4
+ * This metric answers: "For each asset and leverage level,
5
+ * what is the average percentage distance from the *entry price*
6
+ * to the take profit?" (This defines the intended reward).
5
7
  */
6
8
  const { loadInstrumentMappings } = require('../../utils/sector_mapping_provider');
7
9
 
8
- class EntryDistanceToTakeProfitPerLeverage {
10
+ class EntryDistanceToTpPerLeverage {
9
11
  constructor() {
10
- this.takeProfitData = {};
12
+ // { [instrumentId]: { [leverage]: { sum_dist: 0, count: 0 } } }
13
+ this.assets = new Map();
11
14
  this.mappings = null;
12
15
  }
13
16
 
14
- process(portfolioData, yesterdayPortfolio, userId, context) {
15
- if (portfolioData && portfolioData.PublicPositions) {
16
- for (const position of portfolioData.PublicPositions) {
17
- const instrumentId = position.InstrumentID;
18
- const leverage = position.Leverage;
19
- const takeProfitRate = position.TakeProfitRate;
20
- const openRate = position.OpenRate;
21
- const isBuy = position.IsBuy;
17
+ /**
18
+ * Defines the output schema for this calculation.
19
+ * @returns {object} JSON Schema object
20
+ */
21
+ static getSchema() {
22
+ const leverageSchema = {
23
+ "type": "object",
24
+ "description": "Average entry-to-TP distance grouped by leverage.",
25
+ "patternProperties": {
26
+ // Leverage level, e.g., "1x", "5x", "10x"
27
+ "^[0-9]+x$": {
28
+ "type": "number",
29
+ "description": "The average percentage distance from entry price to Take Profit for this leverage level."
30
+ }
31
+ },
32
+ "additionalProperties": { "type": "number" }
33
+ };
22
34
 
23
- if (takeProfitRate > 0.0001 && openRate > 0) {
24
- let distance;
25
-
26
- if (isBuy) {
27
- distance = takeProfitRate - openRate;
28
- } else {
29
- distance = openRate - takeProfitRate;
30
- }
35
+ return {
36
+ "type": "object",
37
+ "description": "Calculates avg % distance from entry price to TP, bucketed by asset and leverage.",
38
+ "patternProperties": {
39
+ "^.*$": leverageSchema // Ticker
40
+ },
41
+ "additionalProperties": leverageSchema
42
+ };
43
+ }
31
44
 
32
- const distancePercent = (distance / openRate) * 100;
45
+ _initLeverage(assetData, leverage) {
46
+ const key = `${leverage}x`;
47
+ if (!assetData[key]) {
48
+ assetData[key] = { sum_dist: 0, count: 0 };
49
+ }
50
+ return key;
51
+ }
33
52
 
34
- if (distancePercent > 0) {
35
- if (!this.takeProfitData[instrumentId]) {
36
- this.takeProfitData[instrumentId] = {};
37
- }
38
- if (!this.takeProfitData[instrumentId][leverage]) {
39
- this.takeProfitData[instrumentId][leverage] = { distance_sum_percent: 0, count: 0 };
40
- }
41
- this.takeProfitData[instrumentId][leverage].distance_sum_percent += distancePercent;
42
- this.takeProfitData[instrumentId][leverage].count++;
43
- }
44
- }
53
+ process(portfolioData) {
54
+ if (portfolioData?.context?.userType !== 'speculator') {
55
+ return;
56
+ }
57
+
58
+ const positions = portfolioData.PublicPositions;
59
+ if (!positions || !Array.isArray(positions)) {
60
+ return;
61
+ }
62
+
63
+ for (const pos of positions) {
64
+ const instrumentId = pos.InstrumentID;
65
+ const tp_rate = pos.TakeProfitRate || 0;
66
+ const open_rate = pos.OpenRate || 0;
67
+
68
+ if (!instrumentId || tp_rate === 0 || open_rate === 0) {
69
+ continue; // Skip positions without TP or open rate
45
70
  }
71
+
72
+ if (!this.assets.has(instrumentId)) {
73
+ this.assets.set(instrumentId, {});
74
+ }
75
+ const assetData = this.assets.get(instrumentId);
76
+
77
+ const leverage = pos.Leverage || 1;
78
+ const key = this._initLeverage(assetData, leverage);
79
+
80
+ const distance = Math.abs(tp_rate - open_rate);
81
+ const distance_pct = (distance / open_rate);
82
+
83
+ assetData[key].sum_dist += distance_pct;
84
+ assetData[key].count++;
46
85
  }
47
86
  }
48
87
 
@@ -50,18 +89,16 @@ class EntryDistanceToTakeProfitPerLeverage {
50
89
  if (!this.mappings) {
51
90
  this.mappings = await loadInstrumentMappings();
52
91
  }
92
+
53
93
  const result = {};
54
- for (const instrumentId in this.takeProfitData) {
55
- const ticker = this.mappings.instrumentToTicker[instrumentId] || instrumentId.toString();
94
+ for (const [instrumentId, leverageData] of this.assets.entries()) {
95
+ const ticker = this.mappings.instrumentToTicker[instrumentId] || `id_${instrumentId}`;
56
96
  result[ticker] = {};
57
- for (const leverage in this.takeProfitData[instrumentId]) {
58
- const data = this.takeProfitData[instrumentId][leverage];
59
- // REFACTOR: Perform final calculation directly.
97
+
98
+ for (const leverageKey in leverageData) {
99
+ const data = leverageData[leverageKey];
60
100
  if (data.count > 0) {
61
- result[ticker][leverage] = {
62
- average_distance_percent: data.distance_sum_percent / data.count,
63
- count: data.count
64
- };
101
+ result[ticker][leverageKey] = (data.sum_dist / data.count) * 100;
65
102
  }
66
103
  }
67
104
  }
@@ -69,9 +106,9 @@ class EntryDistanceToTakeProfitPerLeverage {
69
106
  }
70
107
 
71
108
  reset() {
72
- this.takeProfitData = {};
109
+ this.assets.clear();
73
110
  this.mappings = null;
74
111
  }
75
112
  }
76
113
 
77
- module.exports = EntryDistanceToTakeProfitPerLeverage;
114
+ module.exports = EntryDistanceToTpPerLeverage;