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,47 @@
1
+ /**
2
+ * @fileoverview Calculates leverage usage per instrument for speculators.
3
+ */
4
+ const { loadInstrumentMappings } = require('../../utils/sector_mapping_provider');
5
+
6
+ class LeveragePerAsset {
7
+ constructor() {
8
+ this.leverageData = {};
9
+ this.mappings = null;
10
+ }
11
+
12
+ process(portfolioData, userId) {
13
+ if (portfolioData && portfolioData.PublicPositions) {
14
+ for (const position of portfolioData.PublicPositions) {
15
+ const instrumentId = position.InstrumentID;
16
+ const leverage = position.Leverage;
17
+
18
+ if (!this.leverageData[instrumentId]) {
19
+ this.leverageData[instrumentId] = {};
20
+ }
21
+ this.leverageData[instrumentId][leverage] = (this.leverageData[instrumentId][leverage] || 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.leverageData) {
32
+ const ticker = this.mappings.instrumentToTicker[instrumentId] || instrumentId.toString();
33
+ result[ticker] = this.leverageData[instrumentId];
34
+ }
35
+ if (Object.keys(result).length === 0) return {};
36
+ return {
37
+ leverage_per_asset: result
38
+ };
39
+ }
40
+
41
+ reset() {
42
+ this.leverageData = {};
43
+ this.mappings = null;
44
+ }
45
+ }
46
+
47
+ module.exports = LeveragePerAsset;
@@ -0,0 +1,45 @@
1
+ const { getInstrumentSectorMap } = require('../../utils/sector_mapping_provider');
2
+
3
+ /**
4
+ * @fileoverview Calculates leverage usage per sector for speculators.
5
+ */
6
+
7
+ class LeveragePerSector {
8
+ constructor() {
9
+ this.positions = [];
10
+ }
11
+
12
+ process(portfolioData, userId) {
13
+ if (portfolioData && portfolioData.PublicPositions) {
14
+ this.positions.push(...portfolioData.PublicPositions);
15
+ }
16
+ }
17
+
18
+ async getResult() {
19
+ if (this.positions.length === 0) return {};
20
+
21
+ const sectorMap = await getInstrumentSectorMap();
22
+ const leverageData = {};
23
+
24
+ for (const position of this.positions) {
25
+ const instrumentId = position.InstrumentID;
26
+ const sector = sectorMap[instrumentId] || 'N/A';
27
+ const leverage = position.Leverage;
28
+
29
+ if (!leverageData[sector]) {
30
+ leverageData[sector] = {};
31
+ }
32
+ leverageData[sector][leverage] = (leverageData[sector][leverage] || 0) + 1;
33
+ }
34
+
35
+ return {
36
+ leverage_per_sector: leverageData
37
+ };
38
+ }
39
+
40
+ reset() {
41
+ this.positions = [];
42
+ }
43
+ }
44
+
45
+ module.exports = LeveragePerSector;
@@ -0,0 +1,55 @@
1
+ /**
2
+ * @fileoverview Calculates the change in risk appetite for speculators based on stop-loss distance.
3
+ */
4
+
5
+ class RiskAppetiteChange {
6
+ constructor() {
7
+ this.totalYesterdaySLDistance = 0;
8
+ this.totalTodaySLDistance = 0;
9
+ this.userCount = 0;
10
+ }
11
+
12
+ process(todayPortfolio, yesterdayPortfolio, userId) {
13
+ if (!todayPortfolio || !yesterdayPortfolio || !todayPortfolio.PublicPositions || !yesterdayPortfolio.PublicPositions) {
14
+ return;
15
+ }
16
+
17
+ const yesterdayAvgDistance = this.calculateAverageSLDistance(yesterdayPortfolio.PublicPositions);
18
+ const todayAvgDistance = this.calculateAverageSLDistance(todayPortfolio.PublicPositions);
19
+
20
+ if (yesterdayAvgDistance !== null && todayAvgDistance !== null) {
21
+ this.totalYesterdaySLDistance += yesterdayAvgDistance;
22
+ this.totalTodaySLDistance += todayAvgDistance;
23
+ this.userCount++;
24
+ }
25
+ }
26
+
27
+ calculateAverageSLDistance(positions) {
28
+ let totalDistance = 0;
29
+ let count = 0;
30
+ for (const pos of positions) {
31
+ if (pos.StopLossRate > 0.0001 && pos.OpenRate > 0) {
32
+ const distance = pos.IsBuy ? pos.OpenRate - pos.StopLossRate : pos.StopLossRate - pos.OpenRate;
33
+ if (distance > 0) {
34
+ totalDistance += (distance / pos.OpenRate) * 100;
35
+ count++;
36
+ }
37
+ }
38
+ }
39
+ return count > 0 ? totalDistance / count : null;
40
+ }
41
+
42
+ getResult() {
43
+ if (this.userCount === 0) return {};
44
+ const avgYesterday = this.totalYesterdaySLDistance / this.userCount;
45
+ const avgToday = this.totalTodaySLDistance / this.userCount;
46
+
47
+ return {
48
+ average_stop_loss_distance_yesterday: avgYesterday,
49
+ average_stop_loss_distance_today: avgToday,
50
+ change_in_risk_appetite: avgToday - avgYesterday,
51
+ };
52
+ }
53
+ }
54
+
55
+ module.exports = RiskAppetiteChange;
@@ -0,0 +1,60 @@
1
+ /**
2
+ * @fileoverview Calculates the average risk-reward ratio per asset from speculator positions.
3
+ */
4
+ const { loadInstrumentMappings } = require('../../utils/sector_mapping_provider');
5
+
6
+ class RiskRewardRatioPerAsset {
7
+ constructor() {
8
+ this.rrData = {};
9
+ this.mappings = null;
10
+ }
11
+
12
+ process(portfolioData, 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
+ }
29
+ }
30
+ }
31
+ }
32
+ }
33
+
34
+ async getResult() {
35
+ if (!this.mappings) {
36
+ this.mappings = await loadInstrumentMappings();
37
+ }
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
+
43
+ // REFACTOR: Perform the final calculation directly.
44
+ if (data.count > 0) {
45
+ result[ticker] = {
46
+ average_ratio: data.ratio_sum / data.count
47
+ };
48
+ }
49
+ }
50
+
51
+ return result;
52
+ }
53
+
54
+ reset() {
55
+ this.rrData = {};
56
+ this.mappings = null;
57
+ }
58
+ }
59
+
60
+ module.exports = RiskRewardRatioPerAsset;
@@ -0,0 +1,81 @@
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.
4
+ */
5
+ const { loadInstrumentMappings } = require('../../utils/sector_mapping_provider');
6
+
7
+ class SpeculatorAssetSentiment {
8
+ constructor() {
9
+ this.assets = {};
10
+ this.mappings = null;
11
+ }
12
+
13
+ _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
+ };
19
+ }
20
+ }
21
+
22
+ process(portfolioData, userId, context) {
23
+ const positions = portfolioData.PublicPositions;
24
+ if (!positions || !Array.isArray(positions)) return;
25
+
26
+ for (const position of positions) {
27
+ const instrumentId = position.InstrumentID;
28
+ if (!instrumentId) continue;
29
+
30
+ 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
+ }
45
+ }
46
+ }
47
+
48
+ async getResult() {
49
+ if (!this.mappings) {
50
+ this.mappings = await loadInstrumentMappings();
51
+ }
52
+
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
+ }
70
+ }
71
+
72
+ return finalResult;
73
+ }
74
+
75
+ reset() {
76
+ this.assets = {};
77
+ this.mappings = null;
78
+ }
79
+ }
80
+
81
+ module.exports = SpeculatorAssetSentiment;
@@ -0,0 +1,58 @@
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.
4
+ */
5
+ class SpeculatorDangerZone {
6
+ constructor() {
7
+ this.danger_zone = {};
8
+ }
9
+
10
+ _initAsset(ticker) {
11
+ if (!this.danger_zone[ticker]) {
12
+ this.danger_zone[ticker] = { long_danger_count: 0, short_danger_count: 0 };
13
+ }
14
+ }
15
+
16
+ process(portfolioData, userId, context) {
17
+ const { instrumentMappings } = context;
18
+
19
+ // FIX: Use the correct PublicPositions property for speculators
20
+ const positions = portfolioData.PublicPositions;
21
+ if (!positions || !Array.isArray(positions)) return;
22
+
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) {
26
+ continue;
27
+ }
28
+
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++;
42
+ }
43
+ } 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++;
48
+ }
49
+ }
50
+ }
51
+ }
52
+
53
+ getResult() {
54
+ return this.danger_zone;
55
+ }
56
+ }
57
+
58
+ module.exports = SpeculatorDangerZone;
@@ -0,0 +1,91 @@
1
+ /**
2
+ * @fileoverview Calculates the average stop loss distance (percent and value)
3
+ * for long and short positions, grouped by SECTOR.
4
+ */
5
+ const { getInstrumentSectorMap } = require('../../utils/sector_mapping_provider');
6
+
7
+ class StopLossDistanceBySector {
8
+ constructor() {
9
+ this.instrumentData = {};
10
+ this.instrumentToSector = null;
11
+ }
12
+
13
+ process(portfolioData, userId) {
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;
19
+
20
+ const distance_value = IsBuy ? CurrentRate - StopLossRate : StopLossRate - CurrentRate;
21
+ const distance_percent = (distance_value / CurrentRate) * 100;
22
+
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
+ }
38
+ }
39
+ }
40
+
41
+ async getResult() {
42
+ if (Object.keys(this.instrumentData).length === 0) return {};
43
+ if (!this.instrumentToSector) {
44
+ this.instrumentToSector = await getInstrumentSectorMap();
45
+ }
46
+
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;
65
+ }
66
+ }
67
+
68
+ 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
+ }
82
+ }
83
+ return result;
84
+ }
85
+
86
+ reset() {
87
+ this.instrumentData = {};
88
+ }
89
+ }
90
+
91
+ module.exports = StopLossDistanceBySector;
@@ -0,0 +1,73 @@
1
+ /**
2
+ * @fileoverview Calculates the average stop loss distance (percent and value)
3
+ * for long and short positions, grouped by TICKER.
4
+ */
5
+ const { loadInstrumentMappings } = require('../../utils/sector_mapping_provider');
6
+
7
+ class StopLossDistanceByTicker {
8
+ constructor() {
9
+ this.instrumentData = {};
10
+ this.instrumentToTicker = null;
11
+ }
12
+
13
+ process(portfolioData, userId) {
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;
19
+
20
+ const distance_value = IsBuy ? CurrentRate - StopLossRate : StopLossRate - CurrentRate;
21
+ const distance_percent = (distance_value / CurrentRate) * 100;
22
+
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
+ }
38
+ }
39
+ }
40
+
41
+ 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;
46
+ }
47
+
48
+ 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
+ }
64
+ }
65
+ return result;
66
+ }
67
+
68
+ reset() {
69
+ this.instrumentData = {};
70
+ }
71
+ }
72
+
73
+ module.exports = StopLossDistanceByTicker;
@@ -0,0 +1,55 @@
1
+ /**
2
+ * @fileoverview Calculates the average stop loss level per asset.
3
+ */
4
+ const { loadInstrumentMappings } = require('../../utils/sector_mapping_provider');
5
+
6
+ class StopLossPerAsset {
7
+ constructor() {
8
+ this.stopLossData = {};
9
+ this.mappings = null;
10
+ }
11
+
12
+ process(portfolioData, userId) {
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++;
24
+ }
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.stopLossData) {
35
+ const ticker = this.mappings.instrumentToTicker[instrumentId] || instrumentId.toString();
36
+ const data = this.stopLossData[instrumentId];
37
+
38
+ // REFACTOR: Perform final calculation directly.
39
+ if (data.count > 0) {
40
+ result[ticker] = {
41
+ average_stop_loss: data.sum / data.count
42
+ };
43
+ }
44
+ }
45
+
46
+ return result;
47
+ }
48
+
49
+ reset() {
50
+ this.stopLossData = {};
51
+ this.mappings = null;
52
+ }
53
+ }
54
+
55
+ module.exports = StopLossPerAsset;
@@ -0,0 +1,55 @@
1
+ /**
2
+ * @fileoverview Calculates the average take profit level per asset.
3
+ */
4
+ const { loadInstrumentMappings } = require('../../utils/sector_mapping_provider');
5
+
6
+ class TakeProfitPerAsset {
7
+ constructor() {
8
+ this.takeProfitData = {};
9
+ this.mappings = null;
10
+ }
11
+
12
+ process(portfolioData, userId) {
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++;
24
+ }
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.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
+ }
44
+ }
45
+
46
+ return result;
47
+ }
48
+
49
+ reset() {
50
+ this.takeProfitData = {};
51
+ this.mappings = null;
52
+ }
53
+ }
54
+
55
+ module.exports = TakeProfitPerAsset;
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Compares the average P/L of speculator users who use TSL
3
+ * vs. those who do not.
4
+ */
5
+ class TslEffectiveness {
6
+ constructor() {
7
+ this.tsl_group = { pnl_sum: 0, count: 0 };
8
+ this.nontsl_group = { pnl_sum: 0, count: 0 };
9
+ }
10
+
11
+ process(todayPortfolio, yesterdayPortfolio, userId) {
12
+ // Check if user is a speculator and we have both days' data
13
+ if (todayPortfolio?.context?.userType !== 'speculator' || !yesterdayPortfolio) {
14
+ return;
15
+ }
16
+
17
+ // FIX: This calculation is for speculators, so we use PublicPositions
18
+ const positions = todayPortfolio.PublicPositions;
19
+
20
+ // FIX: Add check to ensure positions is an iterable array
21
+ if (!positions || !Array.isArray(positions)) {
22
+ return;
23
+ }
24
+
25
+ const dailyPnl = todayPortfolio.PortfolioValue - yesterdayPortfolio.PortfolioValue;
26
+ const usesTSL = positions.some(p => p.IsTslEnabled);
27
+
28
+ if (usesTSL) {
29
+ this.tsl_group.pnl_sum += dailyPnl;
30
+ this.tsl_group.count++;
31
+ } else {
32
+ this.nontsl_group.pnl_sum += dailyPnl;
33
+ this.nontsl_group.count++;
34
+ }
35
+ }
36
+
37
+ getResult() {
38
+ // Return final calculated averages
39
+ const tsl_avg_pnl = (this.tsl_group.count > 0) ? this.tsl_group.pnl_sum / this.tsl_group.count : 0;
40
+ const nontsl_avg_pnl = (this.nontsl_group.count > 0) ? this.nontsl_group.pnl_sum / this.nontsl_group.count : 0;
41
+
42
+ return {
43
+ tsl_users: {
44
+ avg_pnl: tsl_avg_pnl,
45
+ count: this.tsl_group.count
46
+ },
47
+ nontsl_users: {
48
+ avg_pnl: nontsl_avg_pnl,
49
+ count: this.nontsl_group.count
50
+ }
51
+ };
52
+ }
53
+ }
54
+
55
+ module.exports = TslEffectiveness;