aiden-shared-calculations-unified 1.0.64 → 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,118 +1,125 @@
1
1
  /**
2
- * @fileoverview Estimates a proxy for net crowd cash flow (Deposits vs. Withdrawals)
3
- * by aggregating portfolio percentage changes across all users.
2
+ * @fileoverview Calculation (Pass 2) for deposit/withdrawal percentage.
4
3
  *
5
- * This calculation is based on the formula:
6
- * Total_Change = P/L_Effect + Trading_Effect + Cash_Flow_Effect
4
+ * This metric estimates the net cash flow (deposits vs. withdrawals)
5
+ * by analyzing changes in portfolio percentages.
7
6
  *
8
- * Where:
9
- * - Total_Change = avg_value_day2 - avg_value_day1
10
- * - P/L_Effect = avg_value_day1 - avg_invested_day1
11
- * - Trading_Effect = avg_invested_day2 - avg_invested_day1
7
+ * It's a proxy based on the assumption that changes in the *sum* of
8
+ * position percentages not accounted for by P&L represent new cash.
12
9
  *
13
- * We solve for Cash_Flow_Effect, which serves as our proxy.
14
- * A negative value indicates a net DEPOSIT (denominator grew, shrinking all %s).
15
- * A positive value indicates a net WITHDRAWAL (denominator shrank, inflating all %s).
10
+ * (DEPRECIATED by crowd-cash-flow-proxy, but kept for reference)
16
11
  */
17
-
18
- class CrowdCashFlowProxy {
12
+ class DepositWithdrawalPercentage {
19
13
  constructor() {
20
- this.total_invested_day1 = 0;
21
- this.total_value_day1 = 0;
22
- this.total_invested_day2 = 0;
23
- this.total_value_day2 = 0;
14
+ this.invested_pct_yesterday_sum = 0;
15
+ this.invested_pct_today_sum = 0;
16
+ this.pnl_pct_today_sum = 0;
24
17
  this.user_count = 0;
25
18
  }
26
19
 
27
20
  /**
28
- * Helper to sum a specific field from an AggregatedPositions array.
29
- * @param {Array<object>} positions - The AggregatedPositions array.
30
- * @param {string} field - The field to sum ('Invested' or 'Value').
31
- * @returns {number} The total sum of that field.
21
+ * Defines the output schema for this calculation.
22
+ * @returns {object} JSON Schema object
32
23
  */
33
- _sumPositions(positions, field) {
24
+ static getSchema() {
25
+ return {
26
+ "type": "object",
27
+ "description": "(DEPRECIATED) Estimates net cash flow by analyzing changes in portfolio position percentages.",
28
+ "properties": {
29
+ "estimated_net_flow_pct": {
30
+ "type": "number",
31
+ "description": "The estimated net cash flow (deposit/withdrawal) as an average percentage change across all users."
32
+ },
33
+ "user_count": {
34
+ "type": "number",
35
+ "description": "Number of users processed."
36
+ },
37
+ "debug": {
38
+ "type": "object",
39
+ "properties": {
40
+ "invested_pct_yesterday_sum": { "type": "number" },
41
+ "invested_pct_today_sum": { "type": "number" },
42
+ "pnl_pct_today_sum": { "type": "number" }
43
+ }
44
+ }
45
+ },
46
+ "required": ["estimated_net_flow_pct", "user_count"]
47
+ };
48
+ }
49
+
50
+ _getPortfolioSums(portfolio) {
51
+ const positions = portfolio?.AggregatedPositions;
34
52
  if (!positions || !Array.isArray(positions)) {
35
- return 0;
53
+ return { investedPct: 0, pnlPct: 0 };
54
+ }
55
+
56
+ let investedPct = 0;
57
+ let pnlPct = 0;
58
+
59
+ for (const pos of positions) {
60
+ investedPct += pos.InvestedAmount || pos.Invested || 0;
61
+ pnlPct += (pos.NetProfit || 0) / 100; // Assuming NetProfit is in %
36
62
  }
37
- return positions.reduce((sum, pos) => sum + (pos[field] || 0), 0);
63
+
64
+ return { investedPct, pnlPct };
38
65
  }
39
66
 
40
- process(todayPortfolio, yesterdayPortfolio, userId) {
41
- // This calculation requires both days' data to compare
42
- if (!todayPortfolio || !yesterdayPortfolio || !todayPortfolio.AggregatedPositions || !yesterdayPortfolio.AggregatedPositions) {
67
+ process(todayPortfolio, yesterdayPortfolio) {
68
+ if (!todayPortfolio || !yesterdayPortfolio) {
43
69
  return;
44
70
  }
45
71
 
46
- const invested_day1 = this._sumPositions(yesterdayPortfolio.AggregatedPositions, 'Invested');
47
- const value_day1 = this._sumPositions(yesterdayPortfolio.AggregatedPositions, 'Value');
48
- const invested_day2 = this._sumPositions(todayPortfolio.AggregatedPositions, 'Invested');
49
- const value_day2 = this._sumPositions(todayPortfolio.AggregatedPositions, 'Value');
50
-
51
- // Only include users who have some form of positions
52
- if (invested_day1 === 0 && invested_day2 === 0 && value_day1 === 0 && value_day2 === 0) {
53
- return;
72
+ const ySums = this._getPortfolioSums(yesterdayPortfolio);
73
+ const tSums = this._getPortfolioSums(todayPortfolio);
74
+
75
+ if (ySums.investedPct === 0 && tSums.investedPct === 0) {
76
+ return; // Skip users with no positions
54
77
  }
55
78
 
56
- this.total_invested_day1 += invested_day1;
57
- this.total_value_day1 += value_day1;
58
- this.total_invested_day2 += invested_day2;
59
- this.total_value_day2 += value_day2;
79
+ this.invested_pct_yesterday_sum += ySums.investedPct;
80
+ this.invested_pct_today_sum += tSums.investedPct;
81
+ this.pnl_pct_today_sum += tSums.pnlPct; // Use today's P&L
60
82
  this.user_count++;
61
83
  }
62
84
 
63
85
  getResult() {
64
86
  if (this.user_count === 0) {
65
- return {}; // No users processed, return empty.
87
+ return {
88
+ estimated_net_flow_pct: 0,
89
+ user_count: 0,
90
+ debug: {
91
+ invested_pct_yesterday_sum: 0,
92
+ invested_pct_today_sum: 0,
93
+ pnl_pct_today_sum: 0
94
+ }
95
+ };
66
96
  }
97
+
98
+ // Avg % change in invested capital
99
+ const avg_invested_change = (this.invested_pct_today_sum - this.invested_pct_yesterday_sum) / this.user_count;
100
+ // Avg P&L
101
+ const avg_pnl = this.pnl_pct_today_sum / this.user_count;
67
102
 
68
- // 1. Calculate the average portfolio for the crowd
69
- const avg_invested_day1 = this.total_invested_day1 / this.user_count;
70
- const avg_value_day1 = this.total_value_day1 / this.user_count;
71
- const avg_invested_day2 = this.total_invested_day2 / this.user_count;
72
- const avg_value_day2 = this.total_value_day2 / this.user_count;
73
-
74
- // 2. Isolate the three effects
75
- const total_value_change = avg_value_day2 - avg_value_day1;
76
- const pl_effect = avg_value_day1 - avg_invested_day1;
77
- const trading_effect = avg_invested_day2 - avg_invested_day1;
78
-
79
- // 3. Solve for the Cash Flow Effect
80
- // Total_Change = pl_effect + trading_effect + cash_flow_effect
81
- const cash_flow_effect = total_value_change - pl_effect - trading_effect;
103
+ // The change not accounted for by P&L is the proxy flow
104
+ const net_flow_pct = avg_invested_change - avg_pnl;
82
105
 
83
106
  return {
84
- // The final proxy value.
85
- // A negative value signals a net DEPOSIT.
86
- // A positive value signals a net WITHDRAWAL.
87
- cash_flow_effect_proxy: cash_flow_effect,
88
-
89
- // Interpretation for the frontend
90
- interpretation: "A negative value signals a net crowd deposit. A positive value signals a net crowd withdrawal.",
91
-
92
- // Debug components
93
- components: {
94
- total_value_change: total_value_change,
95
- pl_effect: pl_effect,
96
- trading_effect: trading_effect
97
- },
98
- // Debug averages
99
- averages: {
100
- avg_invested_day1: avg_invested_day1,
101
- avg_value_day1: avg_value_day1,
102
- avg_invested_day2: avg_invested_day2,
103
- avg_value_day2: avg_value_day2
104
- },
105
- user_sample_size: this.user_count
107
+ estimated_net_flow_pct: net_flow_pct,
108
+ user_count: this.user_count,
109
+ debug: {
110
+ invested_pct_yesterday_sum: this.invested_pct_yesterday_sum,
111
+ invested_pct_today_sum: this.invested_pct_today_sum,
112
+ pnl_pct_today_sum: this.pnl_pct_today_sum
113
+ }
106
114
  };
107
115
  }
108
116
 
109
117
  reset() {
110
- this.total_invested_day1 = 0;
111
- this.total_value_day1 = 0;
112
- this.total_invested_day2 = 0;
113
- this.total_value_day2 = 0;
118
+ this.invested_pct_yesterday_sum = 0;
119
+ this.invested_pct_today_sum = 0;
120
+ this.pnl_pct_today_sum = 0;
114
121
  this.user_count = 0;
115
122
  }
116
123
  }
117
124
 
118
- module.exports = CrowdCashFlowProxy;
125
+ module.exports = DepositWithdrawalPercentage;
@@ -1,49 +1,98 @@
1
1
  /**
2
- * @fileoverview Calculates the average percentage of total portfolio value
3
- * newly allocated to assets not held on the previous day.
2
+ * @fileoverview Calculation (Pass 2) for new allocation percentage.
3
+ *
4
+ * This metric answers: "On average, what percentage of a user's
5
+ * portfolio was allocated to *new* positions today?"
6
+ *
7
+ * This measures the flow of capital into new ideas vs. existing ones.
4
8
  */
5
-
6
9
  class NewAllocationPercentage {
7
10
  constructor() {
8
- this.accumulatedNewAllocationPercentage = 0;
9
- this.userCount = 0;
11
+ this.total_new_allocation_pct = 0;
12
+ this.users_with_new_positions = 0;
13
+ }
14
+
15
+ /**
16
+ * Defines the output schema for this calculation.
17
+ * @returns {object} JSON Schema object
18
+ */
19
+ static getSchema() {
20
+ return {
21
+ "type": "object",
22
+ "description": "Calculates the average portfolio percentage allocated to *new* positions today.",
23
+ "properties": {
24
+ "average_new_allocation_pct": {
25
+ "type": "number",
26
+ "description": "The average percentage of a portfolio allocated to new positions, for users who opened new positions."
27
+ },
28
+ "total_new_allocation_pct_sum": {
29
+ "type": "number",
30
+ "description": "The sum of all new allocation percentages."
31
+ },
32
+ "user_count_with_new_positions": {
33
+ "type": "number",
34
+ "description": "The count of users who opened at least one new position."
35
+ }
36
+ },
37
+ "required": ["average_new_allocation_pct", "total_new_allocation_pct_sum", "user_count_with_new_positions"]
38
+ };
39
+ }
40
+
41
+ _getPortfolioPositionMap(portfolio) {
42
+ // We MUST use AggregatedPositions for the 'Invested' %
43
+ const positions = portfolio?.AggregatedPositions;
44
+ if (!positions || !Array.isArray(positions)) {
45
+ return new Map();
46
+ }
47
+ // Map<InstrumentID, { invested: number }>
48
+ return new Map(positions.map(p => [p.InstrumentID, {
49
+ invested: p.InvestedAmount || p.Invested || 0
50
+ }]));
10
51
  }
11
52
 
12
- process(todayPortfolio, yesterdayPortfolio, userId) {
13
- const todayPositions = todayPortfolio?.AggregatedPositions;
14
- const yesterdayPositions = yesterdayPortfolio?.AggregatedPositions || [];
53
+ process(todayPortfolio, yesterdayPortfolio) {
54
+ if (!todayPortfolio || !yesterdayPortfolio) {
55
+ return;
56
+ }
57
+
58
+ const yPosMap = this._getPortfolioPositionMap(yesterdayPortfolio);
59
+ const tPosMap = this._getPortfolioPositionMap(todayPortfolio);
15
60
 
16
- if (!todayPositions || todayPositions.length === 0) return;
61
+ if (tPosMap.size === 0) {
62
+ return; // No positions today
63
+ }
17
64
 
18
- const yesterdayIds = new Set(yesterdayPositions.map(p => p.InstrumentID));
19
- let userNewAllocation = 0;
65
+ let userNewAllocationPct = 0;
20
66
 
21
- for (const pos of todayPositions) {
22
- if (!yesterdayIds.has(pos.InstrumentID)) {
23
- const invested = typeof pos.Invested === 'number' ? pos.Invested : 0;
24
- userNewAllocation += invested;
67
+ for (const [tId, tPosData] of tPosMap.entries()) {
68
+ // Check if this position is new (in today but not yesterday)
69
+ if (!yPosMap.has(tId)) {
70
+ userNewAllocationPct += tPosData.invested;
25
71
  }
26
72
  }
27
73
 
28
- // Guard against data rounding or eToro API drift
29
- if (userNewAllocation > 100) userNewAllocation = 100;
30
-
31
- this.accumulatedNewAllocationPercentage += userNewAllocation;
32
- this.userCount++;
74
+ if (userNewAllocationPct > 0) {
75
+ this.total_new_allocation_pct += userNewAllocationPct;
76
+ this.users_with_new_positions++;
77
+ }
33
78
  }
34
79
 
35
80
  getResult() {
36
- if (this.userCount === 0) return {};
81
+ const avg_pct = (this.users_with_new_positions > 0)
82
+ ? (this.total_new_allocation_pct / this.users_with_new_positions)
83
+ : 0;
84
+
37
85
  return {
38
- average_new_allocation_percentage:
39
- this.accumulatedNewAllocationPercentage / this.userCount
86
+ average_new_allocation_pct: avg_pct,
87
+ total_new_allocation_pct_sum: this.total_new_allocation_pct,
88
+ user_count_with_new_positions: this.users_with_new_positions
40
89
  };
41
90
  }
42
91
 
43
92
  reset() {
44
- this.accumulatedNewAllocationPercentage = 0;
45
- this.userCount = 0;
93
+ this.total_new_allocation_pct = 0;
94
+ this.users_with_new_positions = 0;
46
95
  }
47
96
  }
48
97
 
49
- module.exports = NewAllocationPercentage;
98
+ module.exports = NewAllocationPercentage;
@@ -1,62 +1,103 @@
1
1
  /**
2
- * @fileoverview Calculates the average percentage increase in allocation
3
- * specifically towards assets already held on the previous day.
2
+ * @fileoverview Calculation (Pass 2) for reallocation increase percentage.
3
+ *
4
+ * This metric answers: "On average, what was the total percentage
5
+ * increase in allocation to *existing* positions today?"
6
+ *
7
+ * This measures "doubling down" or adding to winners/losers.
4
8
  */
5
-
6
9
  class ReallocationIncreasePercentage {
7
10
  constructor() {
8
- this.accumulatedIncreasePercentage = 0;
9
- this.userCount = 0;
11
+ this.total_reallocation_increase_pct = 0;
12
+ this.users_with_reallocations = 0;
13
+ }
14
+
15
+ /**
16
+ * Defines the output schema for this calculation.
17
+ * @returns {object} JSON Schema object
18
+ */
19
+ static getSchema() {
20
+ return {
21
+ "type": "object",
22
+ "description": "Calculates the average portfolio percentage *added* to *existing* positions today.",
23
+ "properties": {
24
+ "average_reallocation_increase_pct": {
25
+ "type": "number",
26
+ "description": "The average percentage of a portfolio added to existing positions, for users who reallocated."
27
+ },
28
+ "total_reallocation_increase_pct_sum": {
29
+ "type": "number",
30
+ "description": "The sum of all reallocation increase percentages."
31
+ },
32
+ "user_count_with_reallocations": {
33
+ "type": "number",
34
+ "description": "The count of users who added to at least one existing position."
35
+ }
36
+ },
37
+ "required": ["average_reallocation_increase_pct", "total_reallocation_increase_pct_sum", "user_count_with_reallocations"]
38
+ };
10
39
  }
11
40
 
12
- process(todayPortfolio, yesterdayPortfolio, userId) {
13
- if (!todayPortfolio || !yesterdayPortfolio || !todayPortfolio.AggregatedPositions || !yesterdayPortfolio.AggregatedPositions) {
14
- // Requires AggregatedPositions which contain the 'Invested' percentage
41
+ _getPortfolioPositionMap(portfolio) {
42
+ // We MUST use AggregatedPositions for the 'Invested' %
43
+ const positions = portfolio?.AggregatedPositions;
44
+ if (!positions || !Array.isArray(positions)) {
45
+ return new Map();
46
+ }
47
+ // Map<InstrumentID, { invested: number }>
48
+ return new Map(positions.map(p => [p.InstrumentID, {
49
+ invested: p.InvestedAmount || p.Invested || 0
50
+ }]));
51
+ }
52
+
53
+ process(todayPortfolio, yesterdayPortfolio) {
54
+ if (!todayPortfolio || !yesterdayPortfolio) {
15
55
  return;
16
56
  }
17
57
 
18
- const yesterdayPositions = new Map(yesterdayPortfolio.AggregatedPositions.map(p => [p.InstrumentID, p]));
19
- let userTotalIncreasePercentage = 0;
58
+ const yPosMap = this._getPortfolioPositionMap(yesterdayPortfolio);
59
+ const tPosMap = this._getPortfolioPositionMap(todayPortfolio);
20
60
 
21
- for (const todayPos of todayPortfolio.AggregatedPositions) {
22
- const yesterdayPos = yesterdayPositions.get(todayPos.InstrumentID);
61
+ if (tPosMap.size === 0 || yPosMap.size === 0) {
62
+ return; // No positions to compare
63
+ }
23
64
 
24
- // Check if the asset was held yesterday
25
- if (yesterdayPos) {
26
- // Ensure 'Invested' property exists and is a number
27
- const todayInvested = typeof todayPos.Invested === 'number' ? todayPos.Invested : 0;
28
- const yesterdayInvested = typeof yesterdayPos.Invested === 'number' ? yesterdayPos.Invested : 0;
65
+ let userReallocationIncrease = 0;
29
66
 
30
- const deltaInvested = todayInvested - yesterdayInvested;
67
+ for (const [tId, tPosData] of tPosMap.entries()) {
68
+ // Check if this position also existed yesterday
69
+ if (yPosMap.has(tId)) {
70
+ const yPosData = yPosMap.get(tId);
71
+ const increase = tPosData.invested - yPosData.invested;
31
72
 
32
- // Accumulate only the increases
33
- if (deltaInvested > 0) {
34
- userTotalIncreasePercentage += deltaInvested;
73
+ // We only care about *increases*
74
+ if (increase > 0) {
75
+ userReallocationIncrease += increase;
35
76
  }
36
77
  }
37
78
  }
38
79
 
39
- // Only count users who had positions on both days for this metric
40
- if (yesterdayPortfolio.AggregatedPositions.length > 0 && todayPortfolio.AggregatedPositions.length > 0) {
41
- this.accumulatedIncreasePercentage += userTotalIncreasePercentage;
42
- this.userCount++;
80
+ if (userReallocationIncrease > 0) {
81
+ this.total_reallocation_increase_pct += userReallocationIncrease;
82
+ this.users_with_reallocations++;
43
83
  }
44
84
  }
45
85
 
46
86
  getResult() {
47
- if (this.userCount === 0) {
48
- return {};
49
- }
87
+ const avg_pct = (this.users_with_reallocations > 0)
88
+ ? (this.total_reallocation_increase_pct / this.users_with_reallocations)
89
+ : 0;
50
90
 
51
91
  return {
52
- // Calculate the final average directly
53
- average_reallocation_increase_percentage: this.accumulatedIncreasePercentage / this.userCount
92
+ average_reallocation_increase_pct: avg_pct,
93
+ total_reallocation_increase_pct_sum: this.total_reallocation_increase_pct,
94
+ user_count_with_reallocations: this.users_with_reallocations
54
95
  };
55
96
  }
56
97
 
57
98
  reset() {
58
- this.accumulatedIncreasePercentage = 0;
59
- this.userCount = 0;
99
+ this.total_reallocation_increase_pct = 0;
100
+ this.users_with_reallocations = 0;
60
101
  }
61
102
  }
62
103
 
@@ -1,55 +1,70 @@
1
1
  /**
2
- * @fileoverview Calculates the total number of 'buy' vs 'sell' (short) positions across all instruments.
3
- * Uses the 'daily_instrument_insights' collection (truth of source).
2
+ * @fileoverview Calculation (Pass 1) for daily buy/sell sentiment.
3
+ *
4
+ * This metric tracks the total number of 'buy' (long) vs 'sell' (short)
5
+ * positions held across all instruments.
6
+ *
7
+ * This provides a raw, global sentiment reading.
4
8
  */
5
-
6
9
  class DailyBuySellSentimentCount {
7
10
  constructor() {
8
- this.totalBuyPositions = 0;
9
- this.totalSellPositions = 0;
10
- this.processedDay = false; // Flag to run logic only once per day
11
+ this.buyPositions = 0;
12
+ this.sellPositions = 0;
11
13
  }
12
14
 
13
- // Only needs today's insights
14
- async process(todayPortfolio, yesterdayPortfolio, userId, context, todayInsights, yesterdayInsights) {
15
- if (this.processedDay) return;
16
- this.processedDay = true;
15
+ /**
16
+ * Defines the output schema for this calculation.
17
+ * @returns {object} JSON Schema object
18
+ */
19
+ static getSchema() {
20
+ return {
21
+ "type": "object",
22
+ "description": "Total count of 'buy' (long) vs 'sell' (short) positions and the sentiment ratio.",
23
+ "properties": {
24
+ "totalBuyPositions": {
25
+ "type": "number",
26
+ "description": "Total count of all 'buy' (long) positions."
27
+ },
28
+ "totalSellPositions": {
29
+ "type": "number",
30
+ "description": "Total count of all 'sell' (short) positions."
31
+ },
32
+ "sentimentRatio": {
33
+ "type": ["number", "null"],
34
+ "description": "Ratio of buy to sell positions (buy/sell). Null if no sell positions."
35
+ }
36
+ },
37
+ "required": ["totalBuyPositions", "totalSellPositions", "sentimentRatio"]
38
+ };
39
+ }
17
40
 
18
- if (!todayInsights || !todayInsights.insights) {
19
- console.warn('[DailyBuySellSentimentCount] Missing insights data for today.');
41
+ process(portfolioData) {
42
+ const positions = portfolioData.PublicPositions || portfolioData.AggregatedPositions;
43
+ if (!positions || !Array.isArray(positions)) {
20
44
  return;
21
45
  }
22
46
 
23
- for (const instrument of todayInsights.insights) {
24
- const totalOwners = instrument.total || 0;
25
- if (totalOwners > 0) {
26
- const buyPercent = (instrument.buy || 0) / 100;
27
- const sellPercent = (instrument.sell || 0) / 100; // 'sell' means short here
28
-
29
- this.totalBuyPositions += (buyPercent * totalOwners);
30
- this.totalSellPositions += (sellPercent * totalOwners);
47
+ for (const pos of positions) {
48
+ if (pos.IsBuy) {
49
+ this.buyPositions++;
50
+ } else {
51
+ this.sellPositions++;
31
52
  }
32
53
  }
33
54
  }
34
55
 
35
56
  getResult() {
36
- // --- ADD THIS CHECK ---
37
- if (!this.processedDay) {
38
- console.warn('[DailyBuySellSentimentCount] getResult called but process never ran (missing insights). Returning null for backfill.');
39
- return null;
40
- }
41
- // --- END ADD ---
42
-
43
57
  return {
44
- total_buy_positions: Math.round(this.totalBuyPositions),
45
- total_sell_positions: Math.round(this.totalSellPositions)
58
+ totalBuyPositions: this.buyPositions,
59
+ totalSellPositions: this.sellPositions,
60
+ // Calculate ratio: Buy / Sell
61
+ sentimentRatio: (this.sellPositions > 0) ? (this.buyPositions / this.sellPositions) : null
46
62
  };
47
63
  }
48
64
 
49
65
  reset() {
50
- this.totalBuyPositions = 0;
51
- this.totalSellPositions = 0;
52
- this.processedDay = false;
66
+ this.buyPositions = 0;
67
+ this.sellPositions = 0;
53
68
  }
54
69
  }
55
70
 
@@ -1,45 +1,49 @@
1
1
  /**
2
- * @fileoverview Calculates the total number of positions held across all instruments for the day.
3
- * Uses the 'daily_instrument_insights' collection (truth of source).
2
+ * @fileoverview Calculation (Pass 1) for total positions held.
3
+ *
4
+ * This is a simple counter that calculates the total number of positions
5
+ * held across all users and all instruments today.
6
+ *
7
+ * It provides a measure of overall market participation.
4
8
  */
5
-
6
9
  class DailyTotalPositionsHeld {
7
10
  constructor() {
8
11
  this.totalPositions = 0;
9
- this.processedDay = false; // Flag to run logic only once per day
10
12
  }
11
13
 
12
- // Only needs today's insights
13
- async process(todayPortfolio, yesterdayPortfolio, userId, context, todayInsights, yesterdayInsights) {
14
- if (this.processedDay) return;
15
- this.processedDay = true;
16
-
17
- if (!todayInsights || !todayInsights.insights) {
18
- console.warn('[DailyTotalPositionsHeld] Missing insights data for today.');
19
- return;
20
- }
14
+ /**
15
+ * Defines the output schema for this calculation.
16
+ * @returns {object} JSON Schema object
17
+ */
18
+ static getSchema() {
19
+ return {
20
+ "type": "object",
21
+ "description": "Calculates the total number of positions held across all users and instruments.",
22
+ "properties": {
23
+ "totalPositions": {
24
+ "type": "number",
25
+ "description": "The total aggregated count of all positions."
26
+ }
27
+ },
28
+ "required": ["totalPositions"]
29
+ };
30
+ }
21
31
 
22
- for (const instrument of todayInsights.insights) {
23
- this.totalPositions += (instrument.total || 0);
32
+ process(portfolioData) {
33
+ const positions = portfolioData.PublicPositions || portfolioData.AggregatedPositions;
34
+ if (positions && Array.isArray(positions)) {
35
+ this.totalPositions += positions.length;
24
36
  }
25
37
  }
26
38
 
27
39
  getResult() {
28
- // --- ADD THIS CHECK ---
29
- if (!this.processedDay) {
30
- console.warn('[DailyTotalPositionsHeld] getResult called but process never ran (missing insights). Returning null for backfill.');
31
- return null;
32
- }
33
- // --- END ADD ---
34
-
35
40
  return {
36
- total_positions_held: this.totalPositions
41
+ totalPositions: this.totalPositions
37
42
  };
38
43
  }
39
44
 
40
45
  reset() {
41
46
  this.totalPositions = 0;
42
- this.processedDay = false;
43
47
  }
44
48
  }
45
49