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,43 +1,78 @@
1
1
  /**
2
- * @fileoverview Counts the total number of 'long' (Buy) positions for each sector.
2
+ * @fileoverview Calculation (Pass 1) for total long positions per sector.
3
+ *
4
+ * This metric answers: "What is the total number of long ('buy')
5
+ * positions for each sector?"
3
6
  */
4
- const { getInstrumentSectorMap } = require('../../utils/sector_mapping_provider'); // Correctly import the new function
7
+ const { loadInstrumentMappings } = require('../../utils/sector_mapping_provider');
5
8
 
6
9
  class TotalLongPerSector {
7
10
  constructor() {
8
- this.longPositions = [];
11
+ // We will store { [sector]: count }
12
+ this.sectors = new Map();
13
+ this.mappings = null;
9
14
  }
10
15
 
11
- process(portfolioData, yesterdayPortfolio, userId, context) {
12
- if (portfolioData && portfolioData.AggregatedPositions) {
13
- for (const position of portfolioData.AggregatedPositions) {
14
- if (position.Direction === 'Buy') {
15
- this.longPositions.push(position);
16
+ /**
17
+ * Defines the output schema for this calculation.
18
+ * @returns {object} JSON Schema object
19
+ */
20
+ static getSchema() {
21
+ return {
22
+ "type": "object",
23
+ "description": "Calculates the total count of long ('buy') positions per sector.",
24
+ "patternProperties": {
25
+ // Sector name
26
+ "^.*$": {
27
+ "type": "number",
28
+ "description": "The total count of long positions for this sector."
16
29
  }
30
+ },
31
+ "additionalProperties": {
32
+ "type": "number"
17
33
  }
34
+ };
35
+ }
36
+
37
+ _initSector(sector) {
38
+ if (!this.sectors.has(sector)) {
39
+ this.sectors.set(sector, 0);
18
40
  }
19
41
  }
20
42
 
21
- async getResult() {
22
- const longCountBySector = {};
23
- const sectorMap = await getInstrumentSectorMap(); // Use the new function
43
+ process(portfolioData, yesterdayPortfolio, userId, context) {
44
+ // This calculation needs the sector mappings from Pass 1 context
45
+ if (!this.mappings) {
46
+ this.mappings = context.mappings;
47
+ }
24
48
 
25
- for (const position of this.longPositions) {
26
- const instrumentId = position.InstrumentID;
27
- const sector = sectorMap[instrumentId] || 'N/A';
28
- longCountBySector[sector] = (longCountBySector[sector] || 0) + 1;
49
+ const positions = portfolioData.PublicPositions || portfolioData.AggregatedPositions;
50
+ if (!positions || !Array.isArray(positions) || !this.mappings) {
51
+ return;
29
52
  }
30
53
 
31
- const result = {};
32
- for (const sector in longCountBySector) {
33
- result[`raw_${sector}_long_count`] = longCountBySector[sector];
54
+ for (const pos of positions) {
55
+ // Only count 'buy' (long) positions
56
+ if (pos.IsBuy) {
57
+ const instrumentId = pos.InstrumentID;
58
+ if (!instrumentId) continue;
59
+
60
+ const sector = this.mappings.instrumentToSector[instrumentId] || 'Other';
61
+ this._initSector(sector);
62
+
63
+ this.sectors.set(sector, this.sectors.get(sector) + 1);
64
+ }
34
65
  }
66
+ }
35
67
 
36
- return result;
68
+ getResult() {
69
+ // Convert Map to plain object
70
+ return Object.fromEntries(this.sectors);
37
71
  }
38
72
 
39
73
  reset() {
40
- this.longPositions = [];
74
+ this.sectors.clear();
75
+ this.mappings = null;
41
76
  }
42
77
  }
43
78
 
@@ -1,43 +1,78 @@
1
1
  /**
2
- * @fileoverview Counts the total number of 'short' (Sell) positions for each sector.
2
+ * @fileoverview Calculation (Pass 1) for total short positions per sector.
3
+ *
4
+ * This metric answers: "What is the total number of short ('sell')
5
+ * positions for each sector?"
3
6
  */
4
- const { getInstrumentSectorMap } = require('../../utils/sector_mapping_provider'); // Correctly import the new function
7
+ const { loadInstrumentMappings } = require('../../utils/sector_mapping_provider');
5
8
 
6
9
  class TotalShortPerSector {
7
10
  constructor() {
8
- this.shortPositions = [];
11
+ // We will store { [sector]: count }
12
+ this.sectors = new Map();
13
+ this.mappings = null;
9
14
  }
10
15
 
11
- process(portfolioData, yesterdayPortfolio, userId, context) {
12
- if (portfolioData && portfolioData.AggregatedPositions) {
13
- for (const position of portfolioData.AggregatedPositions) {
14
- if (position.Direction === 'Sell') {
15
- this.shortPositions.push(position);
16
+ /**
17
+ * Defines the output schema for this calculation.
18
+ * @returns {object} JSON Schema object
19
+ */
20
+ static getSchema() {
21
+ return {
22
+ "type": "object",
23
+ "description": "Calculates the total count of short ('sell') positions per sector.",
24
+ "patternProperties": {
25
+ // Sector name
26
+ "^.*$": {
27
+ "type": "number",
28
+ "description": "The total count of short positions for this sector."
16
29
  }
30
+ },
31
+ "additionalProperties": {
32
+ "type": "number"
17
33
  }
34
+ };
35
+ }
36
+
37
+ _initSector(sector) {
38
+ if (!this.sectors.has(sector)) {
39
+ this.sectors.set(sector, 0);
18
40
  }
19
41
  }
20
42
 
21
- async getResult() {
22
- const shortCountBySector = {};
23
- const sectorMap = await getInstrumentSectorMap(); // Use the new function
43
+ process(portfolioData, yesterdayPortfolio, userId, context) {
44
+ // This calculation needs the sector mappings from Pass 1 context
45
+ if (!this.mappings) {
46
+ this.mappings = context.mappings;
47
+ }
24
48
 
25
- for (const position of this.shortPositions) {
26
- const instrumentId = position.InstrumentID;
27
- const sector = sectorMap[instrumentId] || 'N/A';
28
- shortCountBySector[sector] = (shortCountBySector[sector] || 0) + 1;
49
+ const positions = portfolioData.PublicPositions || portfolioData.AggregatedPositions;
50
+ if (!positions || !Array.isArray(positions) || !this.mappings) {
51
+ return;
29
52
  }
30
53
 
31
- const result = {};
32
- for (const sector in shortCountBySector) {
33
- result[`raw_${sector}_short_count`] = shortCountBySector[sector];
54
+ for (const pos of positions) {
55
+ // Only count 'sell' (short) positions
56
+ if (!pos.IsBuy) {
57
+ const instrumentId = pos.InstrumentID;
58
+ if (!instrumentId) continue;
59
+
60
+ const sector = this.mappings.instrumentToSector[instrumentId] || 'Other';
61
+ this._initSector(sector);
62
+
63
+ this.sectors.set(sector, this.sectors.get(sector) + 1);
64
+ }
34
65
  }
66
+ }
35
67
 
36
- return result;
68
+ getResult() {
69
+ // Convert Map to plain object
70
+ return Object.fromEntries(this.sectors);
37
71
  }
38
72
 
39
73
  reset() {
40
- this.shortPositions = [];
74
+ this.sectors.clear();
75
+ this.mappings = null;
41
76
  }
42
77
  }
43
78
 
@@ -1,80 +1,260 @@
1
1
  /**
2
- * @fileoverview Calculates a "Crowd Conviction" score for each instrument.
2
+ * @fileoverview Calculation (Pass 2) for crowd conviction.
3
+ *
4
+ * This metric answers: "What is the 'Crowd Conviction Score'
5
+ * for each instrument?"
6
+ *
7
+ * It's based on factors like:
8
+ * 1. Holding Duration (longer = more conviction)
9
+ * 2. P&L % (positive = more conviction)
10
+ * 3. Risk/Reward Ratio (higher = more conviction)
11
+ * 4. Leverage (lower = more conviction)
12
+ *
13
+ * This is a *stateful* calculation that computes a 30-day
14
+ * rolling average of these metrics to build the score.
3
15
  */
4
- // CORRECTED PATH: ../utils/ instead of ../../utils/
5
16
  const { loadInstrumentMappings } = require('../../../utils/sector_mapping_provider');
6
17
 
7
-
8
18
  class CrowdConvictionScore {
9
- // ... (rest of the code is unchanged) ...
10
19
  constructor() {
11
- this.convictionData = {};
20
+ // { [instrumentId]: { metrics: [], history: [] } }
21
+ this.assetData = new Map();
12
22
  this.mappings = null;
13
23
  }
14
24
 
15
- async process(todayPortfolio, yesterdayPortfolio, userId) {
16
- if (!todayPortfolio) return;
25
+ /**
26
+ * Defines the output schema for this calculation.
27
+ * @returns {object} JSON Schema object
28
+ */
29
+ static getSchema() {
30
+ const historyItemSchema = {
31
+ "type": "object",
32
+ "properties": {
33
+ "avg_holding_duration": { "type": "number" },
34
+ "avg_pnl": { "type": "number" },
35
+ "avg_rr": { "type": "number" },
36
+ "avg_leverage": { "type": "number" }
37
+ },
38
+ "required": ["avg_holding_duration", "avg_pnl", "avg_rr", "avg_leverage"]
39
+ };
17
40
 
18
- if(!this.mappings) {
19
- this.mappings = await loadInstrumentMappings();
41
+ const tickerSchema = {
42
+ "type": "object",
43
+ "description": "Crowd conviction score and its components for a specific asset.",
44
+ "properties": {
45
+ "conviction_score": {
46
+ "type": "number",
47
+ "description": "Final conviction score (0-100), based on 30-day rolling averages."
48
+ },
49
+ "roll_avg_holding_duration": {
50
+ "type": "number",
51
+ "description": "30-day rolling average of holding duration (hours)."
52
+ },
53
+ "roll_avg_pnl": {
54
+ "type": "number",
55
+ "description": "30-day rolling average of P&L percentage."
56
+ },
57
+ "roll_avg_rr": {
58
+ "type": "number",
59
+ "description": "30-day rolling average of risk/reward ratio."
60
+ },
61
+ "roll_avg_leverage": {
62
+ "type": "number",
63
+ "description": "30-day rolling average of leverage."
64
+ },
65
+ "avg_holding_duration": {
66
+ "type": "number",
67
+ "description": "Today's average holding duration."
68
+ },
69
+ "avg_pnl": {
70
+ "type": "number",
71
+ "description": "Today's average P&L percentage."
72
+ },
73
+ "avg_rr": {
74
+ "type": "number",
75
+ "description": "Today's average risk/reward ratio."
76
+ },
77
+ "avg_leverage": {
78
+ "type": "number",
79
+ "description": "Today's average leverage."
80
+ },
81
+ "history": {
82
+ "type": "array",
83
+ "description": "30-day history of daily average metrics.",
84
+ "items": historyItemSchema
85
+ }
86
+ },
87
+ "required": [
88
+ "conviction_score", "roll_avg_holding_duration", "roll_avg_pnl",
89
+ "roll_avg_rr", "roll_avg_leverage", "avg_holding_duration",
90
+ "avg_pnl", "avg_rr", "avg_leverage", "history"
91
+ ]
92
+ };
93
+
94
+ return {
95
+ "type": "object",
96
+ "description": "Calculates a 30-day rolling 'Crowd Conviction Score' (0-100) for each asset.",
97
+ "patternProperties": {
98
+ "^.*$": tickerSchema // Ticker
99
+ },
100
+ "additionalProperties": tickerSchema
101
+ };
102
+ }
103
+
104
+ _initAsset(instrumentId) {
105
+ if (!this.assetData.has(instrumentId)) {
106
+ // metrics: Stores *today's* raw values before averaging
107
+ // history: Stores *yesterday's* 30-day history
108
+ this.assetData.set(instrumentId, {
109
+ metrics: [],
110
+ history: []
111
+ });
112
+ }
113
+ }
114
+
115
+ _getHoldingDurationHours(openDateStr) {
116
+ if (!openDateStr) return 0;
117
+ try {
118
+ const openDate = new Date(openDateStr);
119
+ const diffMs = new Date().getTime() - openDate.getTime();
120
+ return diffMs / (1000 * 60 * 60);
121
+ } catch (e) {
122
+ return 0;
123
+ }
124
+ }
125
+
126
+ // Simple min-max normalization helper
127
+ _normalize(value, min, max) {
128
+ if (max === min) return 0.5; // Avoid division by zero
129
+ const normalized = (value - min) / (max - min);
130
+ return Math.max(0, Math.min(1, normalized)); // Clamp between 0 and 1
131
+ }
132
+
133
+ process(portfolioData, yesterdayPortfolio, userId, context) {
134
+ // This score is based on speculator actions
135
+ if (portfolioData?.context?.userType !== 'speculator') {
136
+ return;
137
+ }
138
+
139
+ // 1. Get this metric's history from yesterday (pre-loaded)
140
+ if (this.assetData.size === 0) { // Only run once
141
+ const yHistoryData = context.yesterdaysDependencyData['crowd_conviction_score'];
142
+ if (yHistoryData) {
143
+ if (!this.mappings) {
144
+ // We need mappings to convert *yesterday's* tickers back to IDs
145
+ this.mappings = context.mappings;
146
+ }
147
+ for (const [ticker, data] of Object.entries(yHistoryData)) {
148
+ const instrumentId = this.mappings.tickerToInstrument[ticker];
149
+ if (instrumentId) {
150
+ this._initAsset(instrumentId);
151
+ this.assetData.get(instrumentId).history = data.history || [];
152
+ }
153
+ }
154
+ }
20
155
  }
21
156
 
22
- const positions = todayPortfolio.PublicPositions || todayPortfolio.AggregatedPositions;
23
- if (!positions) return;
157
+ const positions = portfolioData.PublicPositions;
158
+ if (!positions || !Array.isArray(positions)) {
159
+ return;
160
+ }
24
161
 
25
162
  for (const pos of positions) {
26
163
  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
- };
164
+ if (!instrumentId) continue;
165
+
166
+ this._initAsset(instrumentId);
167
+
168
+ const pnl_percent = (pos.NetProfit || 0) / (pos.Amount || 1);
169
+ const holding_duration = this._getHoldingDurationHours(pos.OpenDateTime);
170
+ const leverage = pos.Leverage || 1;
171
+
172
+ const sl_rate = pos.StopLossRate || 0;
173
+ const tp_rate = pos.TakeProfitRate || 0;
174
+ const open_rate = pos.OpenRate || 1;
175
+
176
+ let rr_ratio = 0;
177
+ if (sl_rate > 0 && tp_rate > 0) {
178
+ const risk = Math.abs(open_rate - sl_rate);
179
+ const reward = Math.abs(tp_rate - open_rate);
180
+ if (risk > 0) {
181
+ rr_ratio = reward / risk;
182
+ }
34
183
  }
184
+
185
+ this.assetData.get(instrumentId).metrics.push({
186
+ holding_duration,
187
+ pnl_percent,
188
+ rr_ratio,
189
+ leverage
190
+ });
191
+ }
192
+ }
35
193
 
36
- let score = 0;
37
- let weights = 0;
194
+ async getResult() {
195
+ if (!this.mappings) {
196
+ this.mappings = await loadInstrumentMappings();
197
+ }
38
198
 
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
- }
199
+ const result = {};
200
+ for (const [instrumentId, data] of this.assetData.entries()) {
201
+ const ticker = this.mappings.instrumentToTicker[instrumentId] || `id_${instrumentId}`;
202
+ const metrics = data.metrics;
203
+ const yHistory = data.history; // Yesterday's 30-day history
45
204
 
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
- }
205
+ if (metrics.length === 0) continue;
55
206
 
56
- if (pos.Leverage > 1) {
57
- score += Math.log(pos.Leverage) * 1.5;
58
- weights += 1.5;
59
- }
207
+ // 1. Calculate today's averages
208
+ const count = metrics.length;
209
+ const avg_holding_duration = metrics.reduce((s, m) => s + m.holding_duration, 0) / count;
210
+ const avg_pnl = metrics.reduce((s, m) => s + m.pnl_percent, 0) / count;
211
+ const avg_rr = metrics.reduce((s, m) => s + m.rr_ratio, 0) / count;
212
+ const avg_leverage = metrics.reduce((s, m) => s + m.leverage, 0) / count;
213
+
214
+ const todayMetrics = { avg_holding_duration, avg_pnl, avg_rr, avg_leverage };
60
215
 
61
- if (weights > 0) {
62
- const finalScore = (score / weights) * 100;
63
- this.convictionData[ticker].totalScore += finalScore;
64
- this.convictionData[ticker].count++;
65
- }
216
+ // 2. Create new 30-day history
217
+ const newHistory = [todayMetrics, ...yHistory].slice(0, 30);
218
+
219
+ // 3. Calculate 30-day rolling averages
220
+ const historyCount = newHistory.length;
221
+ const roll_avg_duration = newHistory.reduce((s, m) => s + m.avg_holding_duration, 0) / historyCount;
222
+ const roll_avg_pnl = newHistory.reduce((s, m) => s + m.avg_pnl, 0) / historyCount;
223
+ const roll_avg_rr = newHistory.reduce((s, m) => s + m.avg_rr, 0) / historyCount;
224
+ const roll_avg_leverage = newHistory.reduce((s, m) => s + m.avg_leverage, 0) / historyCount;
225
+
226
+ // 4. Calculate Conviction Score (example normalization)
227
+ // (Assumes a 0-1 range for normalization, then scales to 0-100)
228
+ const norm_duration = this._normalize(roll_avg_duration, 0, 240); // 0-10 days
229
+ const norm_pnl = this._normalize(roll_avg_pnl, -0.5, 0.5); // -50% to +50%
230
+ const norm_rr = this._normalize(roll_avg_rr, 0, 3); // 0 to 3 R:R
231
+ const norm_leverage = 1 - this._normalize(roll_avg_leverage, 1, 10); // 1x to 10x (inverted)
232
+
233
+ // Combine scores (equal weight for this example)
234
+ const score_pct = (norm_duration + norm_pnl + norm_rr + norm_leverage) / 4;
235
+
236
+ result[ticker] = {
237
+ conviction_score: score_pct * 100, // Final score 0-100
238
+ roll_avg_holding_duration,
239
+ roll_avg_pnl,
240
+ roll_avg_rr,
241
+ roll_avg_leverage,
242
+
243
+ // Also include today's raw averages
244
+ avg_holding_duration,
245
+ avg_pnl,
246
+ avg_rr,
247
+ avg_leverage,
248
+
249
+ history: newHistory
250
+ };
66
251
  }
252
+ return result;
67
253
  }
68
254
 
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 };
255
+ reset() {
256
+ this.assetData.clear();
257
+ this.mappings = null;
78
258
  }
79
259
  }
80
260
 
@@ -1,24 +1,59 @@
1
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.
2
+ * @fileoverview Calculation (Pass 1) for long positions per stock.
3
+ *
4
+ * This metric answers: "How many long ('buy') positions
5
+ * are there for each stock?"
4
6
  */
5
7
  const { loadInstrumentMappings } = require('../../utils/sector_mapping_provider');
6
8
 
7
9
  class LongPositionPerStock {
8
10
  constructor() {
9
- this.longCountByInstrument = {};
11
+ // We will store { [instrumentId]: count }
12
+ this.assets = new Map();
10
13
  this.mappings = null;
11
14
  }
12
15
 
13
- process(portfolioData, yesterdayPortfolio, userId, context) {
14
- if (!portfolioData || !portfolioData.AggregatedPositions) {
16
+ /**
17
+ * Defines the output schema for this calculation.
18
+ * @returns {object} JSON Schema object
19
+ */
20
+ static getSchema() {
21
+ return {
22
+ "type": "object",
23
+ "description": "Calculates the total count of long ('buy') positions for each asset.",
24
+ "patternProperties": {
25
+ // Ticker
26
+ "^.*$": {
27
+ "type": "number",
28
+ "description": "The total count of long positions for this asset."
29
+ }
30
+ },
31
+ "additionalProperties": {
32
+ "type": "number"
33
+ }
34
+ };
35
+ }
36
+
37
+ _initAsset(instrumentId) {
38
+ if (!this.assets.has(instrumentId)) {
39
+ this.assets.set(instrumentId, 0);
40
+ }
41
+ }
42
+
43
+ process(portfolioData) {
44
+ const positions = portfolioData.PublicPositions || portfolioData.AggregatedPositions;
45
+ if (!positions || !Array.isArray(positions)) {
15
46
  return;
16
47
  }
17
48
 
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;
49
+ for (const pos of positions) {
50
+ // Only count 'buy' (long) positions
51
+ if (pos.IsBuy) {
52
+ const instrumentId = pos.InstrumentID;
53
+ if (!instrumentId) continue;
54
+
55
+ this._initAsset(instrumentId);
56
+ this.assets.set(instrumentId, this.assets.get(instrumentId) + 1);
22
57
  }
23
58
  }
24
59
  }
@@ -27,16 +62,17 @@ class LongPositionPerStock {
27
62
  if (!this.mappings) {
28
63
  this.mappings = await loadInstrumentMappings();
29
64
  }
65
+
30
66
  const result = {};
31
- for (const instrumentId in this.longCountByInstrument) {
32
- const ticker = this.mappings.instrumentToTicker[instrumentId] || instrumentId.toString();
33
- result[ticker] = this.longCountByInstrument[instrumentId];
67
+ for (const [instrumentId, count] of this.assets.entries()) {
68
+ const ticker = this.mappings.instrumentToTicker[instrumentId] || `id_${instrumentId}`;
69
+ result[ticker] = count;
34
70
  }
35
- return { long_positions_by_stock: result };
71
+ return result;
36
72
  }
37
73
 
38
74
  reset() {
39
- this.longCountByInstrument = {};
75
+ this.assets.clear();
40
76
  this.mappings = null;
41
77
  }
42
78
  }