aiden-shared-calculations-unified 1.0.35 → 1.0.37

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 (58) hide show
  1. package/README.MD +77 -77
  2. package/calculations/activity/historical/activity_by_pnl_status.js +85 -85
  3. package/calculations/activity/historical/daily_asset_activity.js +85 -85
  4. package/calculations/activity/historical/daily_user_activity_tracker.js +144 -144
  5. package/calculations/activity/historical/speculator_adjustment_activity.js +76 -76
  6. package/calculations/asset_metrics/asset_position_size.js +57 -57
  7. package/calculations/backtests/strategy-performance.js +229 -245
  8. package/calculations/behavioural/historical/asset_crowd_flow.js +165 -165
  9. package/calculations/behavioural/historical/drawdown_response.js +58 -58
  10. package/calculations/behavioural/historical/dumb-cohort-flow.js +217 -249
  11. package/calculations/behavioural/historical/gain_response.js +57 -57
  12. package/calculations/behavioural/historical/in_loss_asset_crowd_flow.js +98 -98
  13. package/calculations/behavioural/historical/in_profit_asset_crowd_flow.js +99 -99
  14. package/calculations/behavioural/historical/paper_vs_diamond_hands.js +39 -39
  15. package/calculations/behavioural/historical/position_count_pnl.js +67 -67
  16. package/calculations/behavioural/historical/smart-cohort-flow.js +217 -250
  17. package/calculations/behavioural/historical/smart_money_flow.js +165 -165
  18. package/calculations/behavioural/historical/user-investment-profile.js +358 -412
  19. package/calculations/capital_flow/historical/crowd-cash-flow-proxy.js +121 -121
  20. package/calculations/capital_flow/historical/deposit_withdrawal_percentage.js +117 -117
  21. package/calculations/capital_flow/historical/new_allocation_percentage.js +49 -49
  22. package/calculations/insights/daily_bought_vs_sold_count.js +55 -55
  23. package/calculations/insights/daily_buy_sell_sentiment_count.js +49 -49
  24. package/calculations/insights/daily_ownership_delta.js +55 -55
  25. package/calculations/insights/daily_total_positions_held.js +39 -39
  26. package/calculations/meta/capital_deployment_strategy.js +129 -137
  27. package/calculations/meta/capital_liquidation_performance.js +121 -163
  28. package/calculations/meta/capital_vintage_performance.js +121 -158
  29. package/calculations/meta/cash-flow-deployment.js +110 -124
  30. package/calculations/meta/cash-flow-liquidation.js +126 -142
  31. package/calculations/meta/crowd_sharpe_ratio_proxy.js +83 -91
  32. package/calculations/meta/profit_cohort_divergence.js +77 -91
  33. package/calculations/meta/smart-dumb-divergence-index.js +116 -138
  34. package/calculations/meta/social_flow_correlation.js +99 -125
  35. package/calculations/pnl/asset_pnl_status.js +46 -46
  36. package/calculations/pnl/historical/profitability_migration.js +57 -57
  37. package/calculations/pnl/historical/user_profitability_tracker.js +117 -117
  38. package/calculations/pnl/profitable_and_unprofitable_status.js +64 -64
  39. package/calculations/sectors/historical/diversification_pnl.js +76 -76
  40. package/calculations/sectors/historical/sector_rotation.js +67 -67
  41. package/calculations/sentiment/historical/crowd_conviction_score.js +80 -80
  42. package/calculations/socialPosts/social-asset-posts-trend.js +52 -52
  43. package/calculations/socialPosts/social-top-mentioned-words.js +102 -102
  44. package/calculations/socialPosts/social-topic-interest-evolution.js +53 -53
  45. package/calculations/socialPosts/social-word-mentions-trend.js +62 -62
  46. package/calculations/socialPosts/social_activity_aggregation.js +103 -103
  47. package/calculations/socialPosts/social_event_correlation.js +121 -121
  48. package/calculations/socialPosts/social_sentiment_aggregation.js +114 -114
  49. package/calculations/speculators/historical/risk_appetite_change.js +54 -54
  50. package/calculations/speculators/historical/tsl_effectiveness.js +74 -74
  51. package/index.js +33 -33
  52. package/package.json +32 -32
  53. package/utils/firestore_utils.js +76 -76
  54. package/utils/price_data_provider.js +142 -142
  55. package/utils/sector_mapping_provider.js +74 -74
  56. package/calculations/capital_flow/historical/reallocation_increase_percentage.js +0 -63
  57. package/calculations/speculators/stop_loss_distance_by_sector_short_long_breakdown.js +0 -91
  58. package/calculations/speculators/stop_loss_distance_by_ticker_short_long_breakdown.js +0 -73
@@ -1,99 +1,99 @@
1
- const { loadAllPriceData, getDailyPriceChange } = require('../../../utils/price_data_provider');
2
- const { loadInstrumentMappings } = require('../../../utils/sector_mapping_provider');
3
-
4
- /**
5
- * @fileoverview Calculates "Net Crowd Flow" for each asset, BUT
6
- * *only* for the cohort of users who are currently IN LOSS
7
- * on their positions for that asset.
8
- */
9
- class InLossAssetCrowdFlow {
10
- constructor() {
11
- this.asset_values = {}; // Stores { day1_value_sum: 0, day2_value_sum: 0 }
12
- this.user_count = 0;
13
- this.priceMap = null;
14
- this.mappings = null;
15
- this.dates = {};
16
- }
17
-
18
- _initAsset(instrumentId) {
19
- if (!this.asset_values[instrumentId]) {
20
- this.asset_values[instrumentId] = { day1_value_sum: 0, day2_value_sum: 0 };
21
- }
22
- }
23
-
24
- process(todayPortfolio, yesterdayPortfolio, userId, context) {
25
- if (!todayPortfolio || !yesterdayPortfolio || !todayPortfolio.AggregatedPositions || !yesterdayPortfolio.AggregatedPositions) {
26
- return;
27
- }
28
-
29
- if (!this.dates.today && context.todayDateStr && context.yesterdayDateStr) {
30
- this.dates.today = context.todayDateStr;
31
- this.dates.yesterday = context.yesterdayDateStr;
32
- }
33
-
34
- const yesterdayPositions = new Map(yesterdayPortfolio.AggregatedPositions.map(p => [p.InstrumentID, p]));
35
- const todayPositions = new Map(todayPortfolio.AggregatedPositions.map(p => [p.InstrumentID, p]));
36
-
37
- const allInstrumentIds = new Set([
38
- ...yesterdayPositions.keys(),
39
- ...todayPositions.keys()
40
- ]);
41
-
42
- for (const instrumentId of allInstrumentIds) {
43
- const yPos = yesterdayPositions.get(instrumentId);
44
- const tPos = todayPositions.get(instrumentId);
45
-
46
- // --- COHORT LOGIC ---
47
- // Only aggregate if the user is in LOSS on this asset.
48
- const tNetProfit = tPos?.NetProfit || 0;
49
- if (tNetProfit >= 0) { // Note: >= 0 (includes zero profit)
50
- continue; // Skip this asset for this user
51
- }
52
- // --- END COHORT LOGIC ---
53
-
54
- this._initAsset(instrumentId);
55
- this.asset_values[instrumentId].day1_value_sum += (yPos?.Value || 0);
56
- this.asset_values[instrumentId].day2_value_sum += (tPos?.Value || 0);
57
- }
58
- this.user_count++;
59
- }
60
-
61
- async getResult() {
62
- if (this.user_count === 0 || !this.dates.today) return {};
63
- if (!this.priceMap || !this.mappings) {
64
- const [priceData, mappingData] = await Promise.all([
65
- loadAllPriceData(),
66
- loadInstrumentMappings()
67
- ]);
68
- this.priceMap = priceData;
69
- this.mappings = mappingData;
70
- }
71
-
72
- const finalResults = {};
73
- const todayStr = this.dates.today;
74
- const yesterdayStr = this.dates.yesterday;
75
-
76
- for (const instrumentId in this.asset_values) {
77
- const ticker = this.mappings.instrumentToTicker[instrumentId] || `id_${instrumentId}`;
78
-
79
- const avg_day1_value = this.asset_values[instrumentId].day1_value_sum / this.user_count;
80
- const avg_day2_value = this.asset_values[instrumentId].day2_value_sum / this.user_count;
81
- const priceChangePct = getDailyPriceChange(instrumentId, yesterdayStr, todayStr, this.priceMap);
82
-
83
- if (priceChangePct === null) continue;
84
-
85
- const expected_day2_value = avg_day1_value * (1 + priceChangePct);
86
- const net_crowd_flow_pct = avg_day2_value - expected_day2_value;
87
-
88
- finalResults[ticker] = {
89
- net_crowd_flow_pct: net_crowd_flow_pct,
90
- avg_value_day1_pct: avg_day1_value,
91
- avg_value_day2_pct: avg_day2_value
92
- };
93
- }
94
- return finalResults;
95
- }
96
-
97
- reset() { /*...reset all properties...*/ }
98
- }
1
+ const { loadAllPriceData, getDailyPriceChange } = require('../../../utils/price_data_provider');
2
+ const { loadInstrumentMappings } = require('../../../utils/sector_mapping_provider');
3
+
4
+ /**
5
+ * @fileoverview Calculates "Net Crowd Flow" for each asset, BUT
6
+ * *only* for the cohort of users who are currently IN LOSS
7
+ * on their positions for that asset.
8
+ */
9
+ class InLossAssetCrowdFlow {
10
+ constructor() {
11
+ this.asset_values = {}; // Stores { day1_value_sum: 0, day2_value_sum: 0 }
12
+ this.user_count = 0;
13
+ this.priceMap = null;
14
+ this.mappings = null;
15
+ this.dates = {};
16
+ }
17
+
18
+ _initAsset(instrumentId) {
19
+ if (!this.asset_values[instrumentId]) {
20
+ this.asset_values[instrumentId] = { day1_value_sum: 0, day2_value_sum: 0 };
21
+ }
22
+ }
23
+
24
+ process(todayPortfolio, yesterdayPortfolio, userId, context) {
25
+ if (!todayPortfolio || !yesterdayPortfolio || !todayPortfolio.AggregatedPositions || !yesterdayPortfolio.AggregatedPositions) {
26
+ return;
27
+ }
28
+
29
+ if (!this.dates.today && context.todayDateStr && context.yesterdayDateStr) {
30
+ this.dates.today = context.todayDateStr;
31
+ this.dates.yesterday = context.yesterdayDateStr;
32
+ }
33
+
34
+ const yesterdayPositions = new Map(yesterdayPortfolio.AggregatedPositions.map(p => [p.InstrumentID, p]));
35
+ const todayPositions = new Map(todayPortfolio.AggregatedPositions.map(p => [p.InstrumentID, p]));
36
+
37
+ const allInstrumentIds = new Set([
38
+ ...yesterdayPositions.keys(),
39
+ ...todayPositions.keys()
40
+ ]);
41
+
42
+ for (const instrumentId of allInstrumentIds) {
43
+ const yPos = yesterdayPositions.get(instrumentId);
44
+ const tPos = todayPositions.get(instrumentId);
45
+
46
+ // --- COHORT LOGIC ---
47
+ // Only aggregate if the user is in LOSS on this asset.
48
+ const tNetProfit = tPos?.NetProfit || 0;
49
+ if (tNetProfit >= 0) { // Note: >= 0 (includes zero profit)
50
+ continue; // Skip this asset for this user
51
+ }
52
+ // --- END COHORT LOGIC ---
53
+
54
+ this._initAsset(instrumentId);
55
+ this.asset_values[instrumentId].day1_value_sum += (yPos?.Value || 0);
56
+ this.asset_values[instrumentId].day2_value_sum += (tPos?.Value || 0);
57
+ }
58
+ this.user_count++;
59
+ }
60
+
61
+ async getResult() {
62
+ if (this.user_count === 0 || !this.dates.today) return {};
63
+ if (!this.priceMap || !this.mappings) {
64
+ const [priceData, mappingData] = await Promise.all([
65
+ loadAllPriceData(),
66
+ loadInstrumentMappings()
67
+ ]);
68
+ this.priceMap = priceData;
69
+ this.mappings = mappingData;
70
+ }
71
+
72
+ const finalResults = {};
73
+ const todayStr = this.dates.today;
74
+ const yesterdayStr = this.dates.yesterday;
75
+
76
+ for (const instrumentId in this.asset_values) {
77
+ const ticker = this.mappings.instrumentToTicker[instrumentId] || `id_${instrumentId}`;
78
+
79
+ const avg_day1_value = this.asset_values[instrumentId].day1_value_sum / this.user_count;
80
+ const avg_day2_value = this.asset_values[instrumentId].day2_value_sum / this.user_count;
81
+ const priceChangePct = getDailyPriceChange(instrumentId, yesterdayStr, todayStr, this.priceMap);
82
+
83
+ if (priceChangePct === null) continue;
84
+
85
+ const expected_day2_value = avg_day1_value * (1 + priceChangePct);
86
+ const net_crowd_flow_pct = avg_day2_value - expected_day2_value;
87
+
88
+ finalResults[ticker] = {
89
+ net_crowd_flow_pct: net_crowd_flow_pct,
90
+ avg_value_day1_pct: avg_day1_value,
91
+ avg_value_day2_pct: avg_day2_value
92
+ };
93
+ }
94
+ return finalResults;
95
+ }
96
+
97
+ reset() { /*...reset all properties...*/ }
98
+ }
99
99
  module.exports = InLossAssetCrowdFlow;
@@ -1,100 +1,100 @@
1
- const { loadAllPriceData, getDailyPriceChange } = require('../../../utils/price_data_provider');
2
- const { loadInstrumentMappings } = require('../../../utils/sector_mapping_provider');
3
-
4
- /**
5
- * @fileoverview Calculates "Net Crowd Flow" for each asset, BUT
6
- * *only* for the cohort of users who are currently IN PROFIT
7
- * on their positions for that asset.
8
- */
9
- class InProfitAssetCrowdFlow {
10
- constructor() {
11
- this.asset_values = {}; // Stores { day1_value_sum: 0, day2_value_sum: 0 }
12
- this.user_count = 0;
13
- this.priceMap = null;
14
- this.mappings = null;
15
- this.dates = {};
16
- }
17
-
18
- _initAsset(instrumentId) {
19
- if (!this.asset_values[instrumentId]) {
20
- this.asset_values[instrumentId] = { day1_value_sum: 0, day2_value_sum: 0 };
21
- }
22
- }
23
-
24
- process(todayPortfolio, yesterdayPortfolio, userId, context) {
25
- if (!todayPortfolio || !yesterdayPortfolio || !todayPortfolio.AggregatedPositions || !yesterdayPortfolio.AggregatedPositions) {
26
- return;
27
- }
28
-
29
- if (!this.dates.today && context.todayDateStr && context.yesterdayDateStr) {
30
- this.dates.today = context.todayDateStr;
31
- this.dates.yesterday = context.yesterdayDateStr;
32
- }
33
-
34
- const yesterdayPositions = new Map(yesterdayPortfolio.AggregatedPositions.map(p => [p.InstrumentID, p]));
35
- const todayPositions = new Map(todayPortfolio.AggregatedPositions.map(p => [p.InstrumentID, p]));
36
-
37
- const allInstrumentIds = new Set([
38
- ...yesterdayPositions.keys(),
39
- ...todayPositions.keys()
40
- ]);
41
-
42
- for (const instrumentId of allInstrumentIds) {
43
- const yPos = yesterdayPositions.get(instrumentId);
44
- const tPos = todayPositions.get(instrumentId);
45
-
46
- // --- COHORT LOGIC ---
47
- // Only aggregate if the user is in PROFIT on this asset.
48
- // We check *today's* profit status as the primary signal.
49
- const tNetProfit = tPos?.NetProfit || 0;
50
- if (tNetProfit <= 0) {
51
- continue; // Skip this asset for this user
52
- }
53
- // --- END COHORT LOGIC ---
54
-
55
- this._initAsset(instrumentId);
56
- this.asset_values[instrumentId].day1_value_sum += (yPos?.Value || 0);
57
- this.asset_values[instrumentId].day2_value_sum += (tPos?.Value || 0);
58
- }
59
- this.user_count++; // Note: This is user_count of *all* users, which is fine for avg.
60
- }
61
-
62
- async getResult() {
63
- if (this.user_count === 0 || !this.dates.today) return {};
64
- if (!this.priceMap || !this.mappings) {
65
- const [priceData, mappingData] = await Promise.all([
66
- loadAllPriceData(),
67
- loadInstrumentMappings()
68
- ]);
69
- this.priceMap = priceData;
70
- this.mappings = mappingData;
71
- }
72
-
73
- const finalResults = {};
74
- const todayStr = this.dates.today;
75
- const yesterdayStr = this.dates.yesterday;
76
-
77
- for (const instrumentId in this.asset_values) {
78
- const ticker = this.mappings.instrumentToTicker[instrumentId] || `id_${instrumentId}`;
79
-
80
- const avg_day1_value = this.asset_values[instrumentId].day1_value_sum / this.user_count;
81
- const avg_day2_value = this.asset_values[instrumentId].day2_value_sum / this.user_count;
82
- const priceChangePct = getDailyPriceChange(instrumentId, yesterdayStr, todayStr, this.priceMap);
83
-
84
- if (priceChangePct === null) continue;
85
-
86
- const expected_day2_value = avg_day1_value * (1 + priceChangePct);
87
- const net_crowd_flow_pct = avg_day2_value - expected_day2_value;
88
-
89
- finalResults[ticker] = {
90
- net_crowd_flow_pct: net_crowd_flow_pct,
91
- avg_value_day1_pct: avg_day1_value,
92
- avg_value_day2_pct: avg_day2_value
93
- };
94
- }
95
- return finalResults;
96
- }
97
-
98
- reset() { /*...reset all properties...*/ }
99
- }
1
+ const { loadAllPriceData, getDailyPriceChange } = require('../../../utils/price_data_provider');
2
+ const { loadInstrumentMappings } = require('../../../utils/sector_mapping_provider');
3
+
4
+ /**
5
+ * @fileoverview Calculates "Net Crowd Flow" for each asset, BUT
6
+ * *only* for the cohort of users who are currently IN PROFIT
7
+ * on their positions for that asset.
8
+ */
9
+ class InProfitAssetCrowdFlow {
10
+ constructor() {
11
+ this.asset_values = {}; // Stores { day1_value_sum: 0, day2_value_sum: 0 }
12
+ this.user_count = 0;
13
+ this.priceMap = null;
14
+ this.mappings = null;
15
+ this.dates = {};
16
+ }
17
+
18
+ _initAsset(instrumentId) {
19
+ if (!this.asset_values[instrumentId]) {
20
+ this.asset_values[instrumentId] = { day1_value_sum: 0, day2_value_sum: 0 };
21
+ }
22
+ }
23
+
24
+ process(todayPortfolio, yesterdayPortfolio, userId, context) {
25
+ if (!todayPortfolio || !yesterdayPortfolio || !todayPortfolio.AggregatedPositions || !yesterdayPortfolio.AggregatedPositions) {
26
+ return;
27
+ }
28
+
29
+ if (!this.dates.today && context.todayDateStr && context.yesterdayDateStr) {
30
+ this.dates.today = context.todayDateStr;
31
+ this.dates.yesterday = context.yesterdayDateStr;
32
+ }
33
+
34
+ const yesterdayPositions = new Map(yesterdayPortfolio.AggregatedPositions.map(p => [p.InstrumentID, p]));
35
+ const todayPositions = new Map(todayPortfolio.AggregatedPositions.map(p => [p.InstrumentID, p]));
36
+
37
+ const allInstrumentIds = new Set([
38
+ ...yesterdayPositions.keys(),
39
+ ...todayPositions.keys()
40
+ ]);
41
+
42
+ for (const instrumentId of allInstrumentIds) {
43
+ const yPos = yesterdayPositions.get(instrumentId);
44
+ const tPos = todayPositions.get(instrumentId);
45
+
46
+ // --- COHORT LOGIC ---
47
+ // Only aggregate if the user is in PROFIT on this asset.
48
+ // We check *today's* profit status as the primary signal.
49
+ const tNetProfit = tPos?.NetProfit || 0;
50
+ if (tNetProfit <= 0) {
51
+ continue; // Skip this asset for this user
52
+ }
53
+ // --- END COHORT LOGIC ---
54
+
55
+ this._initAsset(instrumentId);
56
+ this.asset_values[instrumentId].day1_value_sum += (yPos?.Value || 0);
57
+ this.asset_values[instrumentId].day2_value_sum += (tPos?.Value || 0);
58
+ }
59
+ this.user_count++; // Note: This is user_count of *all* users, which is fine for avg.
60
+ }
61
+
62
+ async getResult() {
63
+ if (this.user_count === 0 || !this.dates.today) return {};
64
+ if (!this.priceMap || !this.mappings) {
65
+ const [priceData, mappingData] = await Promise.all([
66
+ loadAllPriceData(),
67
+ loadInstrumentMappings()
68
+ ]);
69
+ this.priceMap = priceData;
70
+ this.mappings = mappingData;
71
+ }
72
+
73
+ const finalResults = {};
74
+ const todayStr = this.dates.today;
75
+ const yesterdayStr = this.dates.yesterday;
76
+
77
+ for (const instrumentId in this.asset_values) {
78
+ const ticker = this.mappings.instrumentToTicker[instrumentId] || `id_${instrumentId}`;
79
+
80
+ const avg_day1_value = this.asset_values[instrumentId].day1_value_sum / this.user_count;
81
+ const avg_day2_value = this.asset_values[instrumentId].day2_value_sum / this.user_count;
82
+ const priceChangePct = getDailyPriceChange(instrumentId, yesterdayStr, todayStr, this.priceMap);
83
+
84
+ if (priceChangePct === null) continue;
85
+
86
+ const expected_day2_value = avg_day1_value * (1 + priceChangePct);
87
+ const net_crowd_flow_pct = avg_day2_value - expected_day2_value;
88
+
89
+ finalResults[ticker] = {
90
+ net_crowd_flow_pct: net_crowd_flow_pct,
91
+ avg_value_day1_pct: avg_day1_value,
92
+ avg_value_day2_pct: avg_day2_value
93
+ };
94
+ }
95
+ return finalResults;
96
+ }
97
+
98
+ reset() { /*...reset all properties...*/ }
99
+ }
100
100
  module.exports = InProfitAssetCrowdFlow;
@@ -1,40 +1,40 @@
1
- /**
2
- * @fileoverview Calculates the "Paper Hands vs. Diamond Hands" index.
3
- */
4
-
5
- class PaperVsDiamondHands {
6
- constructor() {
7
- this.newPositions = 0;
8
- this.closedPositions = 0;
9
- this.heldPositions = 0;
10
- }
11
-
12
- process(todayPortfolio, yesterdayPortfolio, userId) {
13
- if (!todayPortfolio || !yesterdayPortfolio) {
14
- return;
15
- }
16
-
17
- const todayIds = new Set((todayPortfolio.PublicPositions || todayPortfolio.AggregatedPositions).map(p => p.PositionID || p.InstrumentID));
18
- const yesterdayIds = new Set((yesterdayPortfolio.PublicPositions || yesterdayPortfolio.AggregatedPositions).map(p => p.PositionID || p.InstrumentID));
19
-
20
- const newPos = [...todayIds].filter(id => !yesterdayIds.has(id)).length;
21
- const closedPos = [...yesterdayIds].filter(id => !todayIds.has(id)).length;
22
- const heldPos = [...todayIds].filter(id => yesterdayIds.has(id)).length;
23
-
24
- this.newPositions += newPos;
25
- this.closedPositions += closedPos;
26
- this.heldPositions += heldPos;
27
- }
28
-
29
- getResult() {
30
- const totalPositions = this.newPositions + this.closedPositions + this.heldPositions;
31
- if (totalPositions === 0) return {};
32
-
33
- return {
34
- paper_hands_index: (this.closedPositions / totalPositions) * 100, // High turnover
35
- diamond_hands_index: (this.heldPositions / totalPositions) * 100, // Low turnover
36
- };
37
- }
38
- }
39
-
1
+ /**
2
+ * @fileoverview Calculates the "Paper Hands vs. Diamond Hands" index.
3
+ */
4
+
5
+ class PaperVsDiamondHands {
6
+ constructor() {
7
+ this.newPositions = 0;
8
+ this.closedPositions = 0;
9
+ this.heldPositions = 0;
10
+ }
11
+
12
+ process(todayPortfolio, yesterdayPortfolio, userId) {
13
+ if (!todayPortfolio || !yesterdayPortfolio) {
14
+ return;
15
+ }
16
+
17
+ const todayIds = new Set((todayPortfolio.PublicPositions || todayPortfolio.AggregatedPositions).map(p => p.PositionID || p.InstrumentID));
18
+ const yesterdayIds = new Set((yesterdayPortfolio.PublicPositions || yesterdayPortfolio.AggregatedPositions).map(p => p.PositionID || p.InstrumentID));
19
+
20
+ const newPos = [...todayIds].filter(id => !yesterdayIds.has(id)).length;
21
+ const closedPos = [...yesterdayIds].filter(id => !todayIds.has(id)).length;
22
+ const heldPos = [...todayIds].filter(id => yesterdayIds.has(id)).length;
23
+
24
+ this.newPositions += newPos;
25
+ this.closedPositions += closedPos;
26
+ this.heldPositions += heldPos;
27
+ }
28
+
29
+ getResult() {
30
+ const totalPositions = this.newPositions + this.closedPositions + this.heldPositions;
31
+ if (totalPositions === 0) return {};
32
+
33
+ return {
34
+ paper_hands_index: (this.closedPositions / totalPositions) * 100, // High turnover
35
+ diamond_hands_index: (this.heldPositions / totalPositions) * 100, // Low turnover
36
+ };
37
+ }
38
+ }
39
+
40
40
  module.exports = PaperVsDiamondHands;
@@ -1,68 +1,68 @@
1
- /**
2
- * Aggregates P/L by the number of positions a user holds.
3
- * Used to create a dot plot.
4
- */
5
- class PositionCountPnl {
6
- constructor() {
7
- // We will store sums and counts to calculate averages later
8
- this.pnl_by_position_count = {};
9
- }
10
-
11
- _initBucket(count) {
12
- if (!this.pnl_by_position_count[count]) {
13
- this.pnl_by_position_count[count] = { pnl_sum: 0, count: 0 };
14
- }
15
- }
16
-
17
- /**
18
- * FIX: Helper function to calculate total P&L from positions
19
- * @param {object} portfolio
20
- * @returns {number|null}
21
- */
22
- _calculateTotalPnl(portfolio) {
23
- const positions = portfolio?.AggregatedPositions || portfolio?.PublicPositions;
24
- if (positions && Array.isArray(positions)) {
25
- // Sum all NetProfit fields, defaulting to 0 if a position has no NetProfit
26
- return positions.reduce((sum, pos) => sum + (pos.NetProfit || 0), 0);
27
- }
28
- return null;
29
- }
30
-
31
- process(todayPortfolio, yesterdayPortfolio, userId) {
32
- // FIX: Only need todayPortfolio for this logic
33
- if (!todayPortfolio) {
34
- return;
35
- }
36
-
37
- const positions = todayPortfolio.AggregatedPositions || todayPortfolio.PublicPositions;
38
-
39
- if (!positions || !Array.isArray(positions)) {
40
- return; // Skip users with no positions array
41
- }
42
-
43
- const positionCount = positions.length;
44
- if (positionCount === 0) {
45
- return; // Skip users with no positions
46
- }
47
-
48
- // FIX: Calculate dailyPnl by summing NetProfit from all positions
49
- const dailyPnl = this._calculateTotalPnl(todayPortfolio);
50
-
51
- if (dailyPnl === null) {
52
- return; // Cannot calculate P&L for this user
53
- }
54
-
55
- this._initBucket(positionCount);
56
- this.pnl_by_position_count[positionCount].pnl_sum += dailyPnl;
57
- this.pnl_by_position_count[positionCount].count++;
58
- }
59
-
60
- getResult() {
61
- // Return the aggregated object.
62
- // Frontend will iterate keys, calculate avg (pnl_sum/count),
63
- // and plot { x: positionCount, y: avg_pnl }
64
- return this.pnl_by_position_count;
65
- }
66
- }
67
-
1
+ /**
2
+ * Aggregates P/L by the number of positions a user holds.
3
+ * Used to create a dot plot.
4
+ */
5
+ class PositionCountPnl {
6
+ constructor() {
7
+ // We will store sums and counts to calculate averages later
8
+ this.pnl_by_position_count = {};
9
+ }
10
+
11
+ _initBucket(count) {
12
+ if (!this.pnl_by_position_count[count]) {
13
+ this.pnl_by_position_count[count] = { pnl_sum: 0, count: 0 };
14
+ }
15
+ }
16
+
17
+ /**
18
+ * FIX: Helper function to calculate total P&L from positions
19
+ * @param {object} portfolio
20
+ * @returns {number|null}
21
+ */
22
+ _calculateTotalPnl(portfolio) {
23
+ const positions = portfolio?.AggregatedPositions || portfolio?.PublicPositions;
24
+ if (positions && Array.isArray(positions)) {
25
+ // Sum all NetProfit fields, defaulting to 0 if a position has no NetProfit
26
+ return positions.reduce((sum, pos) => sum + (pos.NetProfit || 0), 0);
27
+ }
28
+ return null;
29
+ }
30
+
31
+ process(todayPortfolio, yesterdayPortfolio, userId) {
32
+ // FIX: Only need todayPortfolio for this logic
33
+ if (!todayPortfolio) {
34
+ return;
35
+ }
36
+
37
+ const positions = todayPortfolio.AggregatedPositions || todayPortfolio.PublicPositions;
38
+
39
+ if (!positions || !Array.isArray(positions)) {
40
+ return; // Skip users with no positions array
41
+ }
42
+
43
+ const positionCount = positions.length;
44
+ if (positionCount === 0) {
45
+ return; // Skip users with no positions
46
+ }
47
+
48
+ // FIX: Calculate dailyPnl by summing NetProfit from all positions
49
+ const dailyPnl = this._calculateTotalPnl(todayPortfolio);
50
+
51
+ if (dailyPnl === null) {
52
+ return; // Cannot calculate P&L for this user
53
+ }
54
+
55
+ this._initBucket(positionCount);
56
+ this.pnl_by_position_count[positionCount].pnl_sum += dailyPnl;
57
+ this.pnl_by_position_count[positionCount].count++;
58
+ }
59
+
60
+ getResult() {
61
+ // Return the aggregated object.
62
+ // Frontend will iterate keys, calculate avg (pnl_sum/count),
63
+ // and plot { x: positionCount, y: avg_pnl }
64
+ return this.pnl_by_position_count;
65
+ }
66
+ }
67
+
68
68
  module.exports = PositionCountPnl;