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,73 +1,120 @@
1
1
  /**
2
- * @fileoverview Calculates the average stop loss distance (percent and value)
3
- * for long and short positions, grouped by TICKER.
2
+ * @fileoverview Calculation (Pass 1) for speculator metric.
3
+ *
4
+ * This metric answers: "For each ticker, what is the
5
+ * average stop loss distance (in % and value) for
6
+ * long and short positions?"
4
7
  */
5
8
  const { loadInstrumentMappings } = require('../../utils/sector_mapping_provider');
6
9
 
7
- class StopLossDistanceByTicker {
10
+ class StopLossDistanceByTickerShortLongBreakdown {
8
11
  constructor() {
9
- this.instrumentData = {};
10
- this.instrumentToTicker = null;
12
+ // { [instrumentId]: { long_pct: [], long_val: [], short_pct: [], short_val: [] } }
13
+ this.assets = new Map();
14
+ this.mappings = null;
11
15
  }
12
16
 
13
- process(portfolioData, yesterdayPortfolio, userId, context) {
14
- if (!portfolioData || !portfolioData.PublicPositions) return;
17
+ /**
18
+ * Defines the output schema for this calculation.
19
+ * @returns {object} JSON Schema object
20
+ */
21
+ static getSchema() {
22
+ const tickerSchema = {
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
+ };
15
34
 
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;
35
+ return {
36
+ "type": "object",
37
+ "description": "Calculates avg SL distance (% and value) for long/short positions, grouped by asset.",
38
+ "patternProperties": {
39
+ "^.*$": tickerSchema // Ticker
40
+ },
41
+ "additionalProperties": tickerSchema
42
+ };
43
+ }
44
+
45
+ _initAsset(instrumentId) {
46
+ if (!this.assets.has(instrumentId)) {
47
+ this.assets.set(instrumentId, {
48
+ long_pct: [], long_val: [], short_pct: [], short_val: []
49
+ });
50
+ }
51
+ }
52
+
53
+ _avg(arr) {
54
+ if (arr.length === 0) return 0;
55
+ return arr.reduce((a, b) => a + b, 0) / arr.length;
56
+ }
19
57
 
20
- const distance_value = IsBuy ? CurrentRate - StopLossRate : StopLossRate - CurrentRate;
21
- const distance_percent = (distance_value / CurrentRate) * 100;
58
+ process(portfolioData) {
59
+ if (portfolioData?.context?.userType !== 'speculator') {
60
+ return;
61
+ }
62
+
63
+ const positions = portfolioData.PublicPositions;
64
+ if (!positions || !Array.isArray(positions)) {
65
+ return;
66
+ }
22
67
 
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++;
68
+ for (const pos of positions) {
69
+ const instrumentId = pos.InstrumentID;
70
+ const sl_rate = pos.StopLossRate || 0;
71
+ const open_rate = pos.OpenRate || 0;
72
+
73
+ if (!instrumentId || sl_rate === 0 || open_rate === 0) {
74
+ continue;
75
+ }
76
+
77
+ this._initAsset(instrumentId);
78
+ const assetData = this.assets.get(instrumentId);
79
+
80
+ const distance_val = Math.abs(open_rate - sl_rate);
81
+ const distance_pct = (distance_val / open_rate) * 100;
82
+
83
+ if (pos.IsBuy) {
84
+ assetData.long_pct.push(distance_pct);
85
+ assetData.long_val.push(distance_val);
86
+ } else {
87
+ assetData.short_pct.push(distance_pct);
88
+ assetData.short_val.push(distance_val);
37
89
  }
38
90
  }
39
91
  }
40
92
 
41
93
  async getResult() {
42
- if (Object.keys(this.instrumentData).length === 0) return {};
43
- if (!this.instrumentToTicker) {
44
- const mappings = await loadInstrumentMappings();
45
- this.instrumentToTicker = mappings.instrumentToTicker;
94
+ if (!this.mappings) {
95
+ this.mappings = await loadInstrumentMappings();
46
96
  }
47
97
 
48
98
  const result = {};
49
- for (const instrumentId in this.instrumentData) {
50
- const ticker = this.instrumentToTicker[instrumentId] || `unknown_${instrumentId}`;
51
- if (!result[ticker]) result[ticker] = {};
52
-
53
- for (const posType in this.instrumentData[instrumentId]) {
54
- const data = this.instrumentData[instrumentId][posType];
55
- // REFACTOR: Perform final calculation and return in standardized format.
56
- if (data.count > 0) {
57
- result[ticker][posType] = {
58
- average_distance_percent: data.distance_percent_sum / data.count,
59
- average_distance_value: data.distance_value_sum / data.count,
60
- count: data.count
61
- };
62
- }
63
- }
99
+ for (const [instrumentId, data] of this.assets.entries()) {
100
+ const ticker = this.mappings.instrumentToTicker[instrumentId] || `id_${instrumentId}`;
101
+
102
+ result[ticker] = {
103
+ long_avg_dist_pct: this._avg(data.long_pct),
104
+ long_avg_dist_val: this._avg(data.long_val),
105
+ long_count: data.long_pct.length,
106
+ short_avg_dist_pct: this._avg(data.short_pct),
107
+ short_avg_dist_val: this._avg(data.short_val),
108
+ short_count: data.short_pct.length
109
+ };
64
110
  }
65
111
  return result;
66
112
  }
67
113
 
68
114
  reset() {
69
- this.instrumentData = {};
115
+ this.assets.clear();
116
+ this.mappings = null;
70
117
  }
71
118
  }
72
119
 
73
- module.exports = StopLossDistanceByTicker;
120
+ module.exports = StopLossDistanceByTickerShortLongBreakdown;
@@ -1,26 +1,93 @@
1
1
  /**
2
- * @fileoverview Calculates the average stop loss level per asset.
2
+ * @fileoverview Calculation (Pass 1) for speculator metric.
3
+ *
4
+ * This metric answers: "For each asset, what is the
5
+ * average stop loss level and the percentage of
6
+ * positions that have a stop loss set?"
3
7
  */
4
8
  const { loadInstrumentMappings } = require('../../utils/sector_mapping_provider');
5
9
 
6
10
  class StopLossPerAsset {
7
11
  constructor() {
8
- this.stopLossData = {};
12
+ // { [instrumentId]: { sl_rate_sum: 0, sl_dist_sum: 0, sl_set_count: 0, total_count: 0 } }
13
+ this.assets = new Map();
9
14
  this.mappings = null;
10
15
  }
11
16
 
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 stopLoss = position.StopLossRate;
17
-
18
- if (stopLoss > 0) {
19
- if (!this.stopLossData[instrumentId]) {
20
- this.stopLossData[instrumentId] = { sum: 0, count: 0 };
21
- }
22
- this.stopLossData[instrumentId].sum += stopLoss;
23
- this.stopLossData[instrumentId].count++;
17
+ /**
18
+ * Defines the output schema for this calculation.
19
+ * @returns {object} JSON Schema object
20
+ */
21
+ static getSchema() {
22
+ const tickerSchema = {
23
+ "type": "object",
24
+ "properties": {
25
+ "avg_sl_rate": {
26
+ "type": "number",
27
+ "description": "Average SL price level (for positions with SL)."
28
+ },
29
+ "avg_sl_pct_dist": {
30
+ "type": "number",
31
+ "description": "Average SL distance from open price % (for positions with SL)."
32
+ },
33
+ "sl_set_count": { "type": "number" },
34
+ "total_count": { "type": "number" },
35
+ "sl_set_rate_pct": {
36
+ "type": "number",
37
+ "description": "Percentage of positions that have a SL set."
38
+ }
39
+ },
40
+ "required": ["avg_sl_rate", "avg_sl_pct_dist", "sl_set_count", "total_count", "sl_set_rate_pct"]
41
+ };
42
+
43
+ return {
44
+ "type": "object",
45
+ "description": "Calculates the average SL level and usage rate per asset for speculators.",
46
+ "patternProperties": {
47
+ "^.*$": tickerSchema // Ticker
48
+ },
49
+ "additionalProperties": tickerSchema
50
+ };
51
+ }
52
+
53
+ _initAsset(instrumentId) {
54
+ if (!this.assets.has(instrumentId)) {
55
+ this.assets.set(instrumentId, {
56
+ sl_rate_sum: 0,
57
+ sl_dist_sum: 0,
58
+ sl_set_count: 0,
59
+ total_count: 0
60
+ });
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
+ const assetData = this.assets.get(instrumentId);
80
+ assetData.total_count++;
81
+
82
+ const sl_rate = pos.StopLossRate || 0;
83
+ if (sl_rate > 0) {
84
+ assetData.sl_set_count++;
85
+ assetData.sl_rate_sum += sl_rate;
86
+
87
+ const open_rate = pos.OpenRate || 0;
88
+ if (open_rate > 0) {
89
+ const distance = Math.abs(open_rate - sl_rate);
90
+ assetData.sl_dist_sum += (distance / open_rate);
24
91
  }
25
92
  }
26
93
  }
@@ -30,26 +97,27 @@ class StopLossPerAsset {
30
97
  if (!this.mappings) {
31
98
  this.mappings = await loadInstrumentMappings();
32
99
  }
100
+
33
101
  const result = {};
34
- for (const instrumentId in this.stopLossData) {
35
- const ticker = this.mappings.instrumentToTicker[instrumentId] || instrumentId.toString();
36
- const data = this.stopLossData[instrumentId];
102
+ for (const [instrumentId, data] of this.assets.entries()) {
103
+ const ticker = this.mappings.instrumentToTicker[instrumentId] || `id_${instrumentId}`;
104
+ const count = data.sl_set_count;
37
105
 
38
- // REFACTOR: Perform final calculation directly.
39
- if (data.count > 0) {
40
- result[ticker] = {
41
- average_stop_loss: data.sum / data.count
42
- };
43
- }
106
+ result[ticker] = {
107
+ avg_sl_rate: (count > 0) ? (data.sl_rate_sum / count) : 0,
108
+ avg_sl_pct_dist: (count > 0) ? (data.sl_dist_sum / count) * 100 : 0,
109
+ sl_set_count: data.sl_set_count,
110
+ total_count: data.total_count,
111
+ sl_set_rate_pct: (data.total_count > 0) ? (data.sl_set_count / data.total_count) * 100 : 0
112
+ };
44
113
  }
45
-
46
114
  return result;
47
115
  }
48
116
 
49
117
  reset() {
50
- this.stopLossData = {};
118
+ this.assets.clear();
51
119
  this.mappings = null;
52
120
  }
53
121
  }
54
122
 
55
- module.exports = StopLossPerAsset;
123
+ module.exports = StopLossPerAsset;
@@ -1,26 +1,93 @@
1
1
  /**
2
- * @fileoverview Calculates the average take profit level per asset.
2
+ * @fileoverview Calculation (Pass 1) for speculator metric.
3
+ *
4
+ * This metric answers: "For each asset, what is the
5
+ * average take profit level and the percentage of
6
+ * positions that have a take profit set?"
3
7
  */
4
8
  const { loadInstrumentMappings } = require('../../utils/sector_mapping_provider');
5
9
 
6
10
  class TakeProfitPerAsset {
7
11
  constructor() {
8
- this.takeProfitData = {};
12
+ // { [instrumentId]: { tp_rate_sum: 0, tp_dist_sum: 0, tp_set_count: 0, total_count: 0 } }
13
+ this.assets = new Map();
9
14
  this.mappings = null;
10
15
  }
11
16
 
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 takeProfit = position.TakeProfitRate;
17
-
18
- if (takeProfit > 0) {
19
- if (!this.takeProfitData[instrumentId]) {
20
- this.takeProfitData[instrumentId] = { sum: 0, count: 0 };
21
- }
22
- this.takeProfitData[instrumentId].sum += takeProfit;
23
- this.takeProfitData[instrumentId].count++;
17
+ /**
18
+ * Defines the output schema for this calculation.
19
+ * @returns {object} JSON Schema object
20
+ */
21
+ static getSchema() {
22
+ const tickerSchema = {
23
+ "type": "object",
24
+ "properties": {
25
+ "avg_tp_rate": {
26
+ "type": "number",
27
+ "description": "Average TP price level (for positions with TP)."
28
+ },
29
+ "avg_tp_pct_dist": {
30
+ "type": "number",
31
+ "description": "Average TP distance from open price % (for positions with TP)."
32
+ },
33
+ "tp_set_count": { "type": "number" },
34
+ "total_count": { "type": "number" },
35
+ "tp_set_rate_pct": {
36
+ "type": "number",
37
+ "description": "Percentage of positions that have a TP set."
38
+ }
39
+ },
40
+ "required": ["avg_tp_rate", "avg_tp_pct_dist", "tp_set_count", "total_count", "tp_set_rate_pct"]
41
+ };
42
+
43
+ return {
44
+ "type": "object",
45
+ "description": "Calculates the average TP level and usage rate per asset for speculators.",
46
+ "patternProperties": {
47
+ "^.*$": tickerSchema // Ticker
48
+ },
49
+ "additionalProperties": tickerSchema
50
+ };
51
+ }
52
+
53
+ _initAsset(instrumentId) {
54
+ if (!this.assets.has(instrumentId)) {
55
+ this.assets.set(instrumentId, {
56
+ tp_rate_sum: 0,
57
+ tp_dist_sum: 0,
58
+ tp_set_count: 0,
59
+ total_count: 0
60
+ });
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
+ const assetData = this.assets.get(instrumentId);
80
+ assetData.total_count++;
81
+
82
+ const tp_rate = pos.TakeProfitRate || 0;
83
+ if (tp_rate > 0) {
84
+ assetData.tp_set_count++;
85
+ assetData.tp_rate_sum += tp_rate;
86
+
87
+ const open_rate = pos.OpenRate || 0;
88
+ if (open_rate > 0) {
89
+ const distance = Math.abs(tp_rate - open_rate);
90
+ assetData.tp_dist_sum += (distance / open_rate);
24
91
  }
25
92
  }
26
93
  }
@@ -30,26 +97,27 @@ class TakeProfitPerAsset {
30
97
  if (!this.mappings) {
31
98
  this.mappings = await loadInstrumentMappings();
32
99
  }
100
+
33
101
  const result = {};
34
- for (const instrumentId in this.takeProfitData) {
35
- const ticker = this.mappings.instrumentToTicker[instrumentId] || instrumentId.toString();
36
- const data = this.takeProfitData[instrumentId];
37
-
38
- // REFACTOR: Perform final calculation directly.
39
- if (data.count > 0) {
40
- result[ticker] = {
41
- average_take_profit: data.sum / data.count
42
- };
43
- }
102
+ for (const [instrumentId, data] of this.assets.entries()) {
103
+ const ticker = this.mappings.instrumentToTicker[instrumentId] || `id_${instrumentId}`;
104
+ const count = data.tp_set_count;
105
+
106
+ result[ticker] = {
107
+ avg_tp_rate: (count > 0) ? (data.tp_rate_sum / count) : 0,
108
+ avg_tp_pct_dist: (count > 0) ? (data.tp_dist_sum / count) * 100 : 0,
109
+ tp_set_count: data.tp_set_count,
110
+ total_count: data.total_count,
111
+ tp_set_rate_pct: (data.total_count > 0) ? (data.tp_set_count / data.total_count) * 100 : 0
112
+ };
44
113
  }
45
-
46
114
  return result;
47
115
  }
48
116
 
49
117
  reset() {
50
- this.takeProfitData = {};
118
+ this.assets.clear();
51
119
  this.mappings = null;
52
120
  }
53
121
  }
54
122
 
55
- module.exports = TakeProfitPerAsset;
123
+ module.exports = TakeProfitPerAsset;
@@ -1,29 +1,77 @@
1
1
  /**
2
- * @fileoverview Counts how many users have TSL enabled vs. disabled per asset.
2
+ * @fileoverview Calculation (Pass 1) for speculator metric.
3
+ *
4
+ * This metric answers: "For each asset, how many
5
+ * speculators have Trailing Stop Loss (TSL) enabled
6
+ * versus disabled?"
3
7
  */
4
8
  const { loadInstrumentMappings } = require('../../utils/sector_mapping_provider');
5
9
 
6
10
  class TslPerAsset {
7
11
  constructor() {
8
- this.tslData = {};
12
+ // { [instrumentId]: { enabled: 0, disabled: 0 } }
13
+ this.assets = new Map();
9
14
  this.mappings = null;
10
15
  }
11
16
 
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 isTslEnabled = position.IsTslEnabled;
17
-
18
- if (!this.tslData[instrumentId]) {
19
- this.tslData[instrumentId] = { enabled: 0, disabled: 0 };
17
+ /**
18
+ * Defines the output schema for this calculation.
19
+ * @returns {object} JSON Schema object
20
+ */
21
+ static getSchema() {
22
+ const tickerSchema = {
23
+ "type": "object",
24
+ "properties": {
25
+ "tsl_enabled_count": { "type": "number" },
26
+ "tsl_disabled_count": { "type": "number" },
27
+ "tsl_enabled_pct": {
28
+ "type": "number",
29
+ "description": "Percentage of positions with TSL enabled."
20
30
  }
31
+ },
32
+ "required": ["tsl_enabled_count", "tsl_disabled_count", "tsl_enabled_pct"]
33
+ };
21
34
 
22
- if (isTslEnabled) {
23
- this.tslData[instrumentId].enabled++;
24
- } else {
25
- this.tslData[instrumentId].disabled++;
26
- }
35
+ return {
36
+ "type": "object",
37
+ "description": "Calculates the TSL (Trailing Stop Loss) enabled vs. disabled rate per asset.",
38
+ "patternProperties": {
39
+ "^.*$": tickerSchema // Ticker
40
+ },
41
+ "additionalProperties": tickerSchema
42
+ };
43
+ }
44
+
45
+ _initAsset(instrumentId) {
46
+ if (!this.assets.has(instrumentId)) {
47
+ this.assets.set(instrumentId, {
48
+ enabled: 0,
49
+ disabled: 0
50
+ });
51
+ }
52
+ }
53
+
54
+ process(portfolioData) {
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
+ if (!instrumentId) continue;
67
+
68
+ this._initAsset(instrumentId);
69
+ const assetData = this.assets.get(instrumentId);
70
+
71
+ if (pos.IsTslEnabled) {
72
+ assetData.enabled++;
73
+ } else {
74
+ assetData.disabled++;
27
75
  }
28
76
  }
29
77
  }
@@ -32,19 +80,25 @@ class TslPerAsset {
32
80
  if (!this.mappings) {
33
81
  this.mappings = await loadInstrumentMappings();
34
82
  }
83
+
35
84
  const result = {};
36
- for (const instrumentId in this.tslData) {
37
- const ticker = this.mappings.instrumentToTicker[instrumentId] || instrumentId.toString();
38
- result[ticker] = this.tslData[instrumentId];
85
+ for (const [instrumentId, data] of this.assets.entries()) {
86
+ const ticker = this.mappings.instrumentToTicker[instrumentId] || `id_${instrumentId}`;
87
+ const total = data.enabled + data.disabled;
88
+
89
+ if (total > 0) {
90
+ result[ticker] = {
91
+ tsl_enabled_count: data.enabled,
92
+ tsl_disabled_count: data.disabled,
93
+ tsl_enabled_pct: (data.enabled / total) * 100
94
+ };
95
+ }
39
96
  }
40
- if (Object.keys(result).length === 0) return {};
41
- return {
42
- tsl_per_asset: result
43
- };
97
+ return result;
44
98
  }
45
99
 
46
100
  reset() {
47
- this.tslData = {};
101
+ this.assets.clear();
48
102
  this.mappings = null;
49
103
  }
50
104
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aiden-shared-calculations-unified",
3
- "version": "1.0.63",
3
+ "version": "1.0.65",
4
4
  "description": "Shared calculation modules for the BullTrackers Computation System.",
5
5
  "main": "index.js",
6
6
  "files": [