aiden-shared-calculations-unified 1.0.0

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 (50) hide show
  1. package/calculations/asset_metrics/asset_dollar_metrics.js +51 -0
  2. package/calculations/asset_metrics/asset_position_size.js +57 -0
  3. package/calculations/behavioural/drawdown_response.js +56 -0
  4. package/calculations/behavioural/gain_response.js +54 -0
  5. package/calculations/behavioural/paper_vs_diamond_hands.js +40 -0
  6. package/calculations/behavioural/position_count_pnl.js +50 -0
  7. package/calculations/behavioural/smart_money_flow.js +166 -0
  8. package/calculations/pnl/asset_pnl_status.js +47 -0
  9. package/calculations/pnl/average_daily_pnl_all_users.js +48 -0
  10. package/calculations/pnl/average_daily_pnl_per_sector.js +49 -0
  11. package/calculations/pnl/average_daily_pnl_per_stock.js +62 -0
  12. package/calculations/pnl/average_daily_position_pnl.js +40 -0
  13. package/calculations/pnl/pnl_distribution_per_stock.js +53 -0
  14. package/calculations/pnl/profitability_migration.js +58 -0
  15. package/calculations/pnl/profitability_ratio_per_stock.js +51 -0
  16. package/calculations/pnl/profitability_skew_per_stock.js +59 -0
  17. package/calculations/pnl/user_profitability_tracker.js +68 -0
  18. package/calculations/sanity/users_processed.js +27 -0
  19. package/calculations/sectors/diversification_pnl.js +59 -0
  20. package/calculations/sectors/sector_dollar_metrics.js +49 -0
  21. package/calculations/sectors/sector_rotation.js +68 -0
  22. package/calculations/sectors/total_long_per_sector.js +44 -0
  23. package/calculations/sectors/total_short_per_sector.js +44 -0
  24. package/calculations/sentiment/crowd_conviction_score.js +81 -0
  25. package/calculations/short_and_long_stats/long_position_per_stock.js +44 -0
  26. package/calculations/short_and_long_stats/sentiment_per_stock.js +50 -0
  27. package/calculations/short_and_long_stats/short_position_per_stock.js +43 -0
  28. package/calculations/short_and_long_stats/total_long_figures.js +33 -0
  29. package/calculations/short_and_long_stats/total_short_figures.js +34 -0
  30. package/calculations/speculators/distance_to_stop_loss_per_leverage.js +78 -0
  31. package/calculations/speculators/distance_to_tp_per_leverage.js +76 -0
  32. package/calculations/speculators/entry_distance_to_sl_per_leverage.js +78 -0
  33. package/calculations/speculators/entry_distance_to_tp_per_leverage.js +77 -0
  34. package/calculations/speculators/holding_duration_per_asset.js +55 -0
  35. package/calculations/speculators/leverage_per_asset.js +47 -0
  36. package/calculations/speculators/leverage_per_sector.js +45 -0
  37. package/calculations/speculators/risk_appetite_change.js +55 -0
  38. package/calculations/speculators/risk_reward_ratio_per_asset.js +60 -0
  39. package/calculations/speculators/speculator_asset_sentiment.js +81 -0
  40. package/calculations/speculators/speculator_danger_zone.js +58 -0
  41. package/calculations/speculators/stop_loss_distance_by_sector_short_long_breakdown.js +91 -0
  42. package/calculations/speculators/stop_loss_distance_by_ticker_short_long_breakdown.js +73 -0
  43. package/calculations/speculators/stop_loss_per_asset.js +55 -0
  44. package/calculations/speculators/take_profit_per_asset.js +55 -0
  45. package/calculations/speculators/tsl_effectiveness.js +55 -0
  46. package/calculations/speculators/tsl_per_asset.js +52 -0
  47. package/index.js +31 -0
  48. package/package.json +32 -0
  49. package/utils/firestore_utils.js +77 -0
  50. package/utils/sector_mapping_provider.js +75 -0
@@ -0,0 +1,81 @@
1
+ /**
2
+ * @fileoverview Calculates a "Crowd Conviction" score for each instrument.
3
+ */
4
+ // CORRECTED PATH: ../utils/ instead of ../../utils/
5
+ const { loadInstrumentMappings } = require('../../utils/sector_mapping_provider');
6
+
7
+
8
+ class CrowdConvictionScore {
9
+ // ... (rest of the code is unchanged) ...
10
+ constructor() {
11
+ this.convictionData = {};
12
+ this.mappings = null;
13
+ }
14
+
15
+ async process(todayPortfolio, yesterdayPortfolio, userId) {
16
+ if (!todayPortfolio) return;
17
+
18
+ if(!this.mappings) {
19
+ this.mappings = await loadInstrumentMappings();
20
+ }
21
+
22
+ const positions = todayPortfolio.PublicPositions || todayPortfolio.AggregatedPositions;
23
+ if (!positions) return;
24
+
25
+ for (const pos of positions) {
26
+ const instrumentId = pos.InstrumentID;
27
+ const ticker = this.mappings.instrumentToTicker[instrumentId] || instrumentId.toString();
28
+
29
+ if (!this.convictionData[ticker]) {
30
+ this.convictionData[ticker] = {
31
+ totalScore: 0,
32
+ count: 0,
33
+ };
34
+ }
35
+
36
+ let score = 0;
37
+ let weights = 0;
38
+
39
+ if (pos.OpenDateTime) {
40
+ const openTime = new Date(pos.OpenDateTime);
41
+ const holdingHours = (new Date() - openTime) / (1000 * 60 * 60);
42
+ score += Math.log(holdingHours + 1);
43
+ weights += 1;
44
+ }
45
+
46
+ if (pos.StopLossRate > 0 && pos.TakeProfitRate > 0) {
47
+ const risk = Math.abs(pos.OpenRate - pos.StopLossRate);
48
+ const reward = Math.abs(pos.TakeProfitRate - pos.OpenRate);
49
+ if (risk > 0) {
50
+ const ratio = reward / risk;
51
+ score += ratio * 0.5;
52
+ weights += 0.5;
53
+ }
54
+ }
55
+
56
+ if (pos.Leverage > 1) {
57
+ score += Math.log(pos.Leverage) * 1.5;
58
+ weights += 1.5;
59
+ }
60
+
61
+ if (weights > 0) {
62
+ const finalScore = (score / weights) * 100;
63
+ this.convictionData[ticker].totalScore += finalScore;
64
+ this.convictionData[ticker].count++;
65
+ }
66
+ }
67
+ }
68
+
69
+ getResult() {
70
+ const result = {};
71
+ for (const ticker in this.convictionData) {
72
+ const data = this.convictionData[ticker];
73
+ if (data.count > 0) {
74
+ result[ticker] = data.totalScore / data.count;
75
+ }
76
+ }
77
+ return { crowd_conviction_score: result };
78
+ }
79
+ }
80
+
81
+ module.exports = CrowdConvictionScore;
@@ -0,0 +1,44 @@
1
+ /**
2
+ * @fileoverview Counts the total number of 'long' (Buy) positions for each instrument.
3
+ * This provides a simple measure of how many users are holding a long position in a given stock.
4
+ */
5
+ const { loadInstrumentMappings } = require('../../utils/sector_mapping_provider');
6
+
7
+ class LongPositionPerStock {
8
+ constructor() {
9
+ this.longCountByInstrument = {};
10
+ this.mappings = null;
11
+ }
12
+
13
+ process(portfolioData, userId, context) {
14
+ if (!portfolioData || !portfolioData.AggregatedPositions) {
15
+ return;
16
+ }
17
+
18
+ for (const position of portfolioData.AggregatedPositions) {
19
+ if (position.Direction === 'Buy') {
20
+ const instrumentId = position.InstrumentID;
21
+ this.longCountByInstrument[instrumentId] = (this.longCountByInstrument[instrumentId] || 0) + 1;
22
+ }
23
+ }
24
+ }
25
+
26
+ async getResult() {
27
+ if (!this.mappings) {
28
+ this.mappings = await loadInstrumentMappings();
29
+ }
30
+ const result = {};
31
+ for (const instrumentId in this.longCountByInstrument) {
32
+ const ticker = this.mappings.instrumentToTicker[instrumentId] || instrumentId.toString();
33
+ result[ticker] = this.longCountByInstrument[instrumentId];
34
+ }
35
+ return { long_positions_by_stock: result };
36
+ }
37
+
38
+ reset() {
39
+ this.longCountByInstrument = {};
40
+ this.mappings = null;
41
+ }
42
+ }
43
+
44
+ module.exports = LongPositionPerStock;
@@ -0,0 +1,50 @@
1
+ /**
2
+ * @fileoverview Calculates a crowd sentiment score for each instrument by counting long and short positions.
3
+ */
4
+ const { loadInstrumentMappings } = require('../../utils/sector_mapping_provider');
5
+
6
+ class SentimentPerStock {
7
+ constructor() {
8
+ this.sentimentData = {};
9
+ this.mappings = null;
10
+ }
11
+
12
+ process(portfolioData, userId, context) {
13
+ if (!portfolioData || !portfolioData.AggregatedPositions) {
14
+ return;
15
+ }
16
+
17
+ for (const position of portfolioData.AggregatedPositions) {
18
+ const instrumentId = position.InstrumentID;
19
+ if (!this.sentimentData[instrumentId]) {
20
+ this.sentimentData[instrumentId] = { long_positions: 0, short_positions: 0 };
21
+ }
22
+
23
+ if (position.Direction === 'Buy') {
24
+ this.sentimentData[instrumentId].long_positions++;
25
+ } else if (position.Direction === 'Sell') {
26
+ this.sentimentData[instrumentId].short_positions++;
27
+ }
28
+ }
29
+ }
30
+
31
+ async getResult() {
32
+ if (!this.mappings) {
33
+ this.mappings = await loadInstrumentMappings();
34
+ }
35
+ const result = {};
36
+ for (const instrumentId in this.sentimentData) {
37
+ const ticker = this.mappings.instrumentToTicker[instrumentId] || instrumentId.toString();
38
+ result[ticker] = this.sentimentData[instrumentId];
39
+ }
40
+ if (Object.keys(result).length === 0) return {};
41
+ return { sentiment_by_asset: result };
42
+ }
43
+
44
+ reset() {
45
+ this.sentimentData = {};
46
+ this.mappings = null;
47
+ }
48
+ }
49
+
50
+ module.exports = SentimentPerStock;
@@ -0,0 +1,43 @@
1
+ /**
2
+ * @fileoverview Counts the total number of 'short' (Sell) positions for each instrument.
3
+ */
4
+ const { loadInstrumentMappings } = require('../../utils/sector_mapping_provider');
5
+
6
+ class ShortPositionPerStock {
7
+ constructor() {
8
+ this.shortCountByInstrument = {};
9
+ this.mappings = null;
10
+ }
11
+
12
+ process(portfolioData, userId, context) {
13
+ if (!portfolioData || !portfolioData.AggregatedPositions) {
14
+ return;
15
+ }
16
+
17
+ for (const position of portfolioData.AggregatedPositions) {
18
+ if (position.Direction === 'Sell') {
19
+ const instrumentId = position.InstrumentID;
20
+ this.shortCountByInstrument[instrumentId] = (this.shortCountByInstrument[instrumentId] || 0) + 1;
21
+ }
22
+ }
23
+ }
24
+
25
+ async getResult() {
26
+ if (!this.mappings) {
27
+ this.mappings = await loadInstrumentMappings();
28
+ }
29
+ const result = {};
30
+ for (const instrumentId in this.shortCountByInstrument) {
31
+ const ticker = this.mappings.instrumentToTicker[instrumentId] || instrumentId.toString();
32
+ result[ticker] = this.shortCountByInstrument[instrumentId];
33
+ }
34
+ return { short_positions_by_stock: result };
35
+ }
36
+
37
+ reset() {
38
+ this.shortCountByInstrument = {};
39
+ this.mappings = null;
40
+ }
41
+ }
42
+
43
+ module.exports = ShortPositionPerStock;
@@ -0,0 +1,33 @@
1
+ /**
2
+ * @fileoverview Calculates the total number of 'long' (Buy) positions across all users and instruments.
3
+ */
4
+
5
+ class TotalLongFigures {
6
+ constructor() {
7
+ this.totalLongPositions = 0;
8
+ }
9
+
10
+ process(portfolioData, userId, context) {
11
+ if (portfolioData && portfolioData.AggregatedPositions) {
12
+ for (const position of portfolioData.AggregatedPositions) {
13
+ if (position.Direction === 'Buy') {
14
+ this.totalLongPositions++;
15
+ }
16
+ }
17
+ }
18
+ }
19
+
20
+ // Change getResult() (lines 20-22) to:
21
+ getResult() {
22
+ if (this.totalLongPositions === 0) return {};
23
+ return {
24
+ rawTotalLongPositions: this.totalLongPositions
25
+ };
26
+ }
27
+
28
+ reset() {
29
+ this.totalLongPositions = 0;
30
+ }
31
+ }
32
+
33
+ module.exports = TotalLongFigures;
@@ -0,0 +1,34 @@
1
+ /**
2
+ * @fileoverview Calculates the total number of 'short' (Sell) positions across all users and instruments.
3
+ */
4
+
5
+ class TotalShortFigures {
6
+ constructor() {
7
+ this.totalShortPositions = 0;
8
+ }
9
+
10
+ process(portfolioData, userId, context) {
11
+ if (portfolioData && portfolioData.AggregatedPositions) {
12
+ for (const position of portfolioData.AggregatedPositions) {
13
+ // Note: Short positions are indicated by Direction: 'Sell' in this data model
14
+ if (position.Direction === 'Sell') {
15
+ this.totalShortPositions++;
16
+ }
17
+ }
18
+ }
19
+ }
20
+
21
+ // Change getResult() (lines 20-22) to:
22
+ getResult() {
23
+ if (this.totalShortPositions === 0) return {};
24
+ return {
25
+ rawTotalShortPositions: this.totalShortPositions
26
+ };
27
+ }
28
+
29
+ reset() {
30
+ this.totalShortPositions = 0;
31
+ }
32
+ }
33
+
34
+ module.exports = TotalShortFigures;
@@ -0,0 +1,78 @@
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.
5
+ */
6
+ const { loadInstrumentMappings } = require('../../utils/sector_mapping_provider');
7
+
8
+ class DistanceToStopLossPerLeverage {
9
+ constructor() {
10
+ this.stopLossData = {};
11
+ this.mappings = null;
12
+ }
13
+
14
+ process(portfolioData, userId) {
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;
22
+
23
+ if (stopLossRate > 0.0001 && currentRate > 0) {
24
+ let distance;
25
+
26
+ if (isBuy) {
27
+ distance = currentRate - stopLossRate;
28
+ } else {
29
+ distance = stopLossRate - currentRate;
30
+ }
31
+
32
+ const distancePercent = (distance / currentRate) * 100;
33
+
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
+ }
45
+ }
46
+ }
47
+ }
48
+
49
+ async getResult() {
50
+ if (!this.mappings) {
51
+ this.mappings = await loadInstrumentMappings();
52
+ }
53
+
54
+ const result = {};
55
+ for (const instrumentId in this.stopLossData) {
56
+ const ticker = this.mappings.instrumentToTicker[instrumentId] || instrumentId.toString();
57
+ result[ticker] = {};
58
+ for (const leverage in this.stopLossData[instrumentId]) {
59
+ const data = this.stopLossData[instrumentId][leverage];
60
+ // REFACTOR: Perform final calculation directly.
61
+ if (data.count > 0) {
62
+ result[ticker][leverage] = {
63
+ average_distance_percent: data.distance_sum_percent / data.count,
64
+ count: data.count
65
+ };
66
+ }
67
+ }
68
+ }
69
+ return result;
70
+ }
71
+
72
+ reset() {
73
+ this.stopLossData = {};
74
+ this.mappings = null;
75
+ }
76
+ }
77
+
78
+ module.exports = DistanceToStopLossPerLeverage;
@@ -0,0 +1,76 @@
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.
5
+ */
6
+ const { loadInstrumentMappings } = require('../../utils/sector_mapping_provider');
7
+
8
+ class DistanceToTakeProfitPerLeverage {
9
+ constructor() {
10
+ this.takeProfitData = {};
11
+ this.mappings = null;
12
+ }
13
+
14
+ process(portfolioData, userId) {
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;
22
+
23
+ if (takeProfitRate > 0.0001 && currentRate > 0) {
24
+ let distance;
25
+ if (isBuy) {
26
+ distance = takeProfitRate - currentRate;
27
+ } else {
28
+ distance = currentRate - takeProfitRate;
29
+ }
30
+
31
+ const distancePercent = (distance / currentRate) * 100;
32
+
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
+ }
44
+ }
45
+ }
46
+ }
47
+
48
+ async getResult() {
49
+ if (!this.mappings) {
50
+ this.mappings = await loadInstrumentMappings();
51
+ }
52
+ const result = {};
53
+ for (const instrumentId in this.takeProfitData) {
54
+ const ticker = this.mappings.instrumentToTicker[instrumentId] || instrumentId.toString();
55
+ result[ticker] = {};
56
+ for (const leverage in this.takeProfitData[instrumentId]) {
57
+ const data = this.takeProfitData[instrumentId][leverage];
58
+ // REFACTOR: Perform final calculation directly.
59
+ if (data.count > 0) {
60
+ result[ticker][leverage] = {
61
+ average_distance_percent: data.distance_sum_percent / data.count,
62
+ count: data.count
63
+ };
64
+ }
65
+ }
66
+ }
67
+ return result;
68
+ }
69
+
70
+ reset() {
71
+ this.takeProfitData = {};
72
+ this.mappings = null;
73
+ }
74
+ }
75
+
76
+ module.exports = DistanceToTakeProfitPerLeverage;
@@ -0,0 +1,78 @@
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.
5
+ */
6
+ const { loadInstrumentMappings } = require('../../utils/sector_mapping_provider');
7
+
8
+ class EntryDistanceToStopLossPerLeverage {
9
+ constructor() {
10
+ this.stopLossData = {};
11
+ this.mappings = null;
12
+ }
13
+
14
+ process(portfolioData, userId) {
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;
22
+
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
+ }
31
+
32
+ const distancePercent = (distance / openRate) * 100;
33
+
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
+ }
45
+ }
46
+ }
47
+ }
48
+
49
+ async getResult() {
50
+ if (!this.mappings) {
51
+ this.mappings = await loadInstrumentMappings();
52
+ }
53
+
54
+ const result = {};
55
+ for (const instrumentId in this.stopLossData) {
56
+ const ticker = this.mappings.instrumentToTicker[instrumentId] || instrumentId.toString();
57
+ result[ticker] = {};
58
+ for (const leverage in this.stopLossData[instrumentId]) {
59
+ const data = this.stopLossData[instrumentId][leverage];
60
+ // REFACTOR: Perform final calculation directly.
61
+ if (data.count > 0) {
62
+ result[ticker][leverage] = {
63
+ average_distance_percent: data.distance_sum_percent / data.count,
64
+ count: data.count
65
+ };
66
+ }
67
+ }
68
+ }
69
+ return result;
70
+ }
71
+
72
+ reset() {
73
+ this.stopLossData = {};
74
+ this.mappings = null;
75
+ }
76
+ }
77
+
78
+ module.exports = EntryDistanceToStopLossPerLeverage;
@@ -0,0 +1,77 @@
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.
5
+ */
6
+ const { loadInstrumentMappings } = require('../../utils/sector_mapping_provider');
7
+
8
+ class EntryDistanceToTakeProfitPerLeverage {
9
+ constructor() {
10
+ this.takeProfitData = {};
11
+ this.mappings = null;
12
+ }
13
+
14
+ process(portfolioData, userId) {
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;
22
+
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
+ }
31
+
32
+ const distancePercent = (distance / openRate) * 100;
33
+
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
+ }
45
+ }
46
+ }
47
+ }
48
+
49
+ async getResult() {
50
+ if (!this.mappings) {
51
+ this.mappings = await loadInstrumentMappings();
52
+ }
53
+ const result = {};
54
+ for (const instrumentId in this.takeProfitData) {
55
+ const ticker = this.mappings.instrumentToTicker[instrumentId] || instrumentId.toString();
56
+ result[ticker] = {};
57
+ for (const leverage in this.takeProfitData[instrumentId]) {
58
+ const data = this.takeProfitData[instrumentId][leverage];
59
+ // REFACTOR: Perform final calculation directly.
60
+ if (data.count > 0) {
61
+ result[ticker][leverage] = {
62
+ average_distance_percent: data.distance_sum_percent / data.count,
63
+ count: data.count
64
+ };
65
+ }
66
+ }
67
+ }
68
+ return result;
69
+ }
70
+
71
+ reset() {
72
+ this.takeProfitData = {};
73
+ this.mappings = null;
74
+ }
75
+ }
76
+
77
+ module.exports = EntryDistanceToTakeProfitPerLeverage;
@@ -0,0 +1,55 @@
1
+ /**
2
+ * @fileoverview Calculates the average holding duration of open speculator positions.
3
+ */
4
+ const { loadInstrumentMappings } = require('../../utils/sector_mapping_provider');
5
+
6
+ class HoldingDurationPerAsset {
7
+ constructor() {
8
+ this.durationData = {};
9
+ this.mappings = null;
10
+ }
11
+
12
+ process(portfolioData, 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 };
22
+ }
23
+ this.durationData[instrumentId].duration_sum_hours += durationHours;
24
+ this.durationData[instrumentId].count++;
25
+ }
26
+ }
27
+ }
28
+
29
+ async getResult() {
30
+ if (!this.mappings) {
31
+ this.mappings = await loadInstrumentMappings();
32
+ }
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
+
38
+ // REFACTOR: Perform the final calculation directly.
39
+ if (data.count > 0) {
40
+ result[ticker] = {
41
+ average_duration_hours: data.duration_sum_hours / data.count
42
+ };
43
+ }
44
+ }
45
+
46
+ return result;
47
+ }
48
+
49
+ reset() {
50
+ this.durationData = {};
51
+ this.mappings = null;
52
+ }
53
+ }
54
+
55
+ module.exports = HoldingDurationPerAsset;