aiden-shared-calculations-unified 1.0.64 → 1.0.66

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,58 +1,104 @@
1
1
  /**
2
- * @fileoverview Calculates the migration of users between profitability states.
2
+ * @fileoverview Calculation (Pass 2) for profitability migration.
3
+ *
4
+ * This metric answers: "How many users migrated between
5
+ * profitable and unprofitable states today?"
6
+ *
7
+ * It tracks the "churn" between P&L states.
3
8
  */
4
-
5
9
  class ProfitabilityMigration {
6
10
  constructor() {
7
- this.profitableToUnprofitable = 0;
8
- this.unprofitableToProfitable = 0;
9
- this.remainedProfitable = 0;
10
- this.remainedUnprofitable = 0;
11
+ this.to_profit_count = 0;
12
+ this.to_loss_count = 0;
13
+ this.remained_profit_count = 0;
14
+ this.remained_loss_count = 0;
15
+ this.total_processed = 0;
11
16
  }
12
17
 
13
- process(todayPortfolio, yesterdayPortfolio, userId) {
14
- if (!todayPortfolio || !yesterdayPortfolio) {
15
- return;
16
- }
18
+ /**
19
+ * Defines the output schema for this calculation.
20
+ * @returns {object} JSON Schema object
21
+ */
22
+ static getSchema() {
23
+ return {
24
+ "type": "object",
25
+ "description": "Tracks the migration of users between profitable and unprofitable states day-over-day.",
26
+ "properties": {
27
+ "to_profit_count": {
28
+ "type": "number",
29
+ "description": "Count of users who were in loss yesterday and are in profit today."
30
+ },
31
+ "to_loss_count": {
32
+ "type": "number",
33
+ "description": "Count of users who were in profit yesterday and are in loss today."
34
+ },
35
+ "remained_profit_count": {
36
+ "type": "number",
37
+ "description": "Count of users who were in profit yesterday and today."
38
+ },
39
+ "remained_loss_count": {
40
+ "type": "number",
41
+ "description": "Count of users who were in loss yesterday and today."
42
+ },
43
+ "total_processed": {
44
+ "type": "number",
45
+ "description": "Total users who had a P&L status on both days."
46
+ }
47
+ },
48
+ "required": ["to_profit_count", "to_loss_count", "remained_profit_count", "remained_loss_count", "total_processed"]
49
+ };
50
+ }
17
51
 
18
- const todayProfit = this.calculateTotalPnl(todayPortfolio);
19
- const yesterdayProfit = this.calculateTotalPnl(yesterdayPortfolio);
52
+ _getPnlState(portfolio) {
53
+ // This checks *overall portfolio* P&L for the day
54
+ const dailyPnl = portfolio?.Summary?.NetProfit || 0;
55
+ if (dailyPnl > 0) return 'profit';
56
+ if (dailyPnl < 0) return 'loss';
57
+ return 'neutral';
58
+ }
20
59
 
21
- if (todayProfit === null || yesterdayProfit === null) {
60
+ process(todayPortfolio, yesterdayPortfolio) {
61
+ if (!todayPortfolio || !yesterdayPortfolio) {
22
62
  return;
23
63
  }
24
64
 
25
- const wasProfitable = yesterdayProfit > 0;
26
- const isProfitable = todayProfit > 0;
27
-
28
- if (wasProfitable && !isProfitable) {
29
- this.profitableToUnprofitable++;
30
- } else if (!wasProfitable && isProfitable) {
31
- this.unprofitableToProfitable++;
32
- } else if (wasProfitable && isProfitable) {
33
- this.remainedProfitable++;
34
- } else {
35
- this.remainedUnprofitable++;
65
+ const yState = this._getPnlState(yesterdayPortfolio);
66
+ const tState = this._getPnlState(todayPortfolio);
67
+
68
+ if (yState === 'neutral' || tState === 'neutral') {
69
+ return; // Only track transitions between profit/loss
36
70
  }
37
- }
71
+
72
+ this.total_processed++;
38
73
 
39
- calculateTotalPnl(portfolio) {
40
- if (portfolio && portfolio.AggregatedPositions) {
41
- return portfolio.AggregatedPositions.reduce((sum, pos) => sum + pos.NetProfit, 0);
42
- } else if (portfolio && portfolio.PublicPositions) {
43
- return portfolio.PublicPositions.reduce((sum, pos) => sum + pos.NetProfit, 0);
74
+ if (yState === 'profit' && tState === 'profit') {
75
+ this.remained_profit_count++;
76
+ } else if (yState === 'loss' && tState === 'loss') {
77
+ this.remained_loss_count++;
78
+ } else if (yState === 'loss' && tState === 'profit') {
79
+ this.to_profit_count++;
80
+ } else if (yState === 'profit' && tState === 'loss') {
81
+ this.to_loss_count++;
44
82
  }
45
- return null;
46
83
  }
47
84
 
48
85
  getResult() {
49
86
  return {
50
- profitable_to_unprofitable: this.profitableToUnprofitable,
51
- unprofitable_to_profitable: this.unprofitableToProfitable,
52
- remained_profitable: this.remainedProfitable,
53
- remained_unprofitable: this.remainedUnprofitable,
87
+ to_profit_count: this.to_profit_count,
88
+ to_loss_count: this.to_loss_count,
89
+ remained_profit_count: this.remained_profit_count,
90
+ remained_loss_count: this.remained_loss_count,
91
+ total_processed: this.total_processed
54
92
  };
55
93
  }
94
+
95
+ reset() {
96
+ this.to_profit_count = 0;
97
+ this.to_loss_count = 0;
98
+ this.remained_profit_count = 0;
99
+ this.remained_loss_count = 0;
100
+ this.total_processed = 0;
101
+ }
56
102
  }
57
103
 
58
104
  module.exports = ProfitabilityMigration;
@@ -1,128 +1,131 @@
1
1
  /**
2
- * @fileoverview Tracks user profitability over a 7-day rolling window.
3
- * This version shards the output AND calculates the user's *weighted average daily PNL (as a decimal %)*.
2
+ * @fileoverview Calculation (Pass 2) for user profitability tracker.
3
+ *
4
+ * This metric answers: "What is each user's weighted average
5
+ * daily P&L, and what is their 7-day rolling profitability history?"
6
+ *
7
+ * This is a foundational calculation for identifying 'smart' and
8
+ * 'dumb' cohorts in later passes.
9
+ *
10
+ * It is *stateful* and requires reading the *previous day's*
11
+ * result of *this same calculation* from Firestore.
4
12
  */
5
-
6
- const { Firestore } = require('@google-cloud/firestore');
7
- const firestore = new Firestore();
8
-
9
- const NUM_SHARDS = 50;
10
-
11
13
  class UserProfitabilityTracker {
12
14
  constructor() {
13
- // This will store { userId: { weightedPnlSum: 0, totalInvested: 0 } }
14
- this.dailyData = {};
15
+ // Map<userId, { weighted_avg_pnl_7d, profitable_days_7d, pnl_history_7d: [] }>
16
+ this.userHistory = new Map();
15
17
  }
16
18
 
17
19
  /**
18
- * Calculates the weighted PNL for the day.
19
- * NetProfit is a decimal % return (e.g., 0.03)
20
- * Invested is a decimal % weight (e.g., 0.05)
20
+ * Defines the output schema for this calculation.
21
+ * @returns {object} JSON Schema object
21
22
  */
22
- calculateWeightedDailyPnl(portfolio) {
23
- if (!portfolio || !portfolio.AggregatedPositions || portfolio.AggregatedPositions.length === 0) {
24
- return { weightedPnl: 0, totalInvested: 0 };
25
- }
26
-
27
- let weightedPnlSum = 0;
28
- let totalInvested = 0;
29
-
30
- for (const pos of portfolio.AggregatedPositions) {
31
- // Use NetProfit (the % return)
32
- const netProfit = ('NetProfit' in pos) ? pos.NetProfit : (pos.ProfitAndLoss || 0);
33
-
34
- // --- START FIX ---
35
- // "Normal" user portfolios use the 'Invested' field for portfolio percentage.
36
- // 'InvestedAmount' and 'Amount' are different fields.
37
- const invested = pos.Invested || 0;
38
- // --- END FIX ---
39
-
40
- if (invested > 0) {
41
- weightedPnlSum += netProfit * invested;
42
- totalInvested += invested;
43
- }
44
- }
23
+ static getSchema() {
24
+ const userDetailSchema = {
25
+ "type": "object",
26
+ "properties": {
27
+ "weighted_avg_pnl_7d": {
28
+ "type": "number",
29
+ "description": "Weighted average P&L over the last 7 days (recent days weighted more)."
30
+ },
31
+ "profitable_days_7d": {
32
+ "type": "number",
33
+ "description": "Count of profitable days in the last 7 days."
34
+ },
35
+ "pnl_history_7d": {
36
+ "type": "array",
37
+ "description": "List of the last 7 days' P&L values.",
38
+ "items": { "type": "number" }
39
+ }
40
+ },
41
+ "required": ["weighted_avg_pnl_7d", "profitable_days_7d", "pnl_history_7d"]
42
+ };
45
43
 
46
- return { weightedPnlSum, totalInvested };
44
+ return {
45
+ "type": "object",
46
+ "description": "Tracks 7-day rolling profitability for each user; used to define cohorts.",
47
+ "properties": {
48
+ "ranked_users": {
49
+ "type": "array",
50
+ "description": "List of all users, sorted ascending by their 7-day weighted average P&L.",
51
+ "items": {
52
+ "type": "object",
53
+ "properties": {
54
+ "userId": { "type": "string" },
55
+ "weighted_avg_pnl_7d": { "type": "number" }
56
+ },
57
+ "required": ["userId", "weighted_avg_pnl_7d"]
58
+ }
59
+ },
60
+ "user_details": {
61
+ "type": "object",
62
+ "description": "A map of user IDs to their detailed profitability data.",
63
+ "patternProperties": {
64
+ "^.*$": userDetailSchema // UserID
65
+ },
66
+ "additionalProperties": userDetailSchema
67
+ }
68
+ },
69
+ "required": ["ranked_users", "user_details"]
70
+ };
47
71
  }
48
72
 
49
- process(todayPortfolio, yesterdayPortfolio, userId) {
50
- if (!todayPortfolio) return;
51
-
52
- const { weightedPnlSum, totalInvested } = this.calculateWeightedDailyPnl(todayPortfolio);
53
-
54
- if (totalInvested > 0) {
55
- this.dailyData[userId] = { weightedPnlSum, totalInvested };
73
+ _calculateWeightedAverage(history) {
74
+ const weights = [0.25, 0.20, 0.15, 0.12, 0.10, 0.08, 0.10]; // Example weights (sum to 1.0)
75
+ let weightedSum = 0;
76
+
77
+ for (let i = 0; i < history.length; i++) {
78
+ // Apply weights in reverse (most recent gets highest weight)
79
+ weightedSum += history[i] * (weights[i] || 0);
56
80
  }
81
+ return weightedSum;
57
82
  }
58
83
 
59
- async getResult() {
60
- // --- START MODIFICATION ---
61
- // If no data was processed (e.g., no portfolios found for the day),
62
- // return null to allow the backfill to retry.
63
- if (Object.keys(this.dailyData).length === 0) {
64
- console.warn('[UserProfitabilityTracker] No daily data was processed. Returning null for backfill.');
65
- return null;
66
- }
67
- // --- END MODIFICATION ---
68
-
69
- const today = new Date().toISOString().slice(0, 10);
70
- const results = {}; // For sharded history
71
- const dailyPnlMap = {}; // For the new profile calc
72
-
73
- // Prepare sharded data structure
74
- for (let i = 0; i < NUM_SHARDS; i++) {
75
- results[`user_profitability_shard_${i}`] = {};
76
- }
77
-
78
- // ... (Fetch existing shards logic, same as your file) ...
79
- const shardPromises = [];
80
- for (let i = 0; i < NUM_SHARDS; i++) {
81
- const docRef = firestore.collection('historical_insights').doc(`user_profitability_shard_${i}`);
82
- shardPromises.push(docRef.get());
83
- }
84
- const shardSnapshots = await Promise.all(shardPromises);
85
- const existingData = shardSnapshots.map(snap => (snap.exists ? snap.data() : {}) || {});
86
-
87
-
88
- for (const userId in this.dailyData) {
89
- const { weightedPnlSum, totalInvested } = this.dailyData[userId];
90
-
91
- // Calculate the final weighted average % return for the day
92
- // We cap totalInvested at 1.0 (100%) in case of data issues
93
- const totalWeight = Math.min(1.0, totalInvested);
94
- const dailyAvgPnl = (totalWeight > 0) ? (weightedPnlSum / totalWeight) : 0;
95
-
96
- // Store this for the profile calc dependency
97
- dailyPnlMap[userId] = dailyAvgPnl;
98
-
99
- // --- Now, update the sharded history ---
100
- const shardIndex = parseInt(userId, 10) % NUM_SHARDS;
84
+ process(todayPortfolio, yesterdayPortfolio, userId, context, todayInsights, yesterdayInsights, fetchedDependencies) {
85
+ // 1. Get this user's history from *yesterday's* run of this metric
86
+ // This data is pre-loaded into context by the computation system
87
+ const yHistoryData = context.yesterdaysDependencyData['user_profitability_tracker'];
88
+ const userHistory = yHistoryData?.user_details?.[userId]?.pnl_history_7d || [];
89
+
90
+ // 2. Get today's P&L
91
+ const todayPnl = todayPortfolio?.Summary?.NetProfit || 0;
92
+
93
+ // 3. Create new 7-day history
94
+ const newHistory = [todayPnl, ...userHistory].slice(0, 7);
95
+
96
+ // 4. Calculate new metrics
97
+ const weightedAvg = this._calculateWeightedAverage(newHistory);
98
+ const profitableDays = newHistory.filter(pnl => pnl > 0).length;
99
+
100
+ // 5. Store for getResult()
101
+ this.userHistory.set(userId, {
102
+ weighted_avg_pnl_7d: weightedAvg,
103
+ profitable_days_7d: profitableDays,
104
+ pnl_history_7d: newHistory
105
+ });
106
+ }
101
107
 
102
- // --- START FIX for userHistory.push error ---
103
- // The existing data is an object like { history: [...] }, not the array itself.
104
- const userHistory = (existingData[shardIndex][userId] && existingData[shardIndex][userId].history)
105
- ? existingData[shardIndex][userId].history
106
- : [];
107
- // --- END FIX ---
108
-
109
- // Store the decimal % pnl in the history
110
- userHistory.push({ date: today, pnl: dailyAvgPnl });
111
-
112
- const shardKey = `user_profitability_shard_${shardIndex}`;
113
- if (!results[shardKey]) results[shardKey] = {};
114
- results[shardKey][userId] = { history: userHistory.slice(-7) };
115
- }
108
+ getResult() {
109
+ const user_details = Object.fromEntries(this.userHistory);
110
+
111
+ // Create the ranked list for cohorts
112
+ const ranked_users = Array.from(this.userHistory.entries())
113
+ .map(([userId, data]) => ({
114
+ userId: userId,
115
+ weighted_avg_pnl_7d: data.weighted_avg_pnl_7d
116
+ }))
117
+ // Sort ascending (lowest P&L first)
118
+ .sort((a, b) => a.weighted_avg_pnl_7d - b.weighted_avg_pnl_7d);
116
119
 
117
120
  return {
118
- sharded_user_profitability: results,
119
- daily_pnl_map: dailyPnlMap // <-- This now correctly outputs the weighted avg % PNL
121
+ ranked_users: ranked_users,
122
+ user_details: user_details
120
123
  };
121
124
  }
122
125
 
123
126
  reset() {
124
- this.dailyData = {};
127
+ this.userHistory.clear();
125
128
  }
126
129
  }
127
130
 
128
- module.exports = UserProfitabilityTracker;
131
+ module.exports = UserProfitabilityTracker;
@@ -1,65 +1,85 @@
1
1
  /**
2
- * @fileoverview Aggregates P&L data points for statistical analysis.
3
- * This calculation is a dependency for the 'Crowd Sharpe Ratio Proxy'.
4
- * It gathers the sum, sum of squares, and count of P&L for each stock.
2
+ * @fileoverview Calculation (Pass 1) for P&L distribution.
3
+ *
4
+ * This metric answers: "What are the sum, sum of squares, and
5
+ * count of P&L for each stock?"
6
+ *
7
+ * This is a foundational calculation used by other metrics
8
+ * (like 'crowd_sharpe_ratio_proxy') to calculate variance
9
+ * and standard deviation.
5
10
  */
6
- const { loadInstrumentMappings } = require('../../utils/sector_mapping_provider');
7
-
11
+ // No mappings needed here, as the consumer (e.g., Sharpe Ratio)
12
+ // will handle the mapping. This class just aggregates by ID.
8
13
  class PnlDistributionPerStock {
9
14
  constructor() {
10
- this.distributionData = {}; // { [instrumentId]: { pnl_sum: 0, pnl_sum_sq: 0, position_count: 0 } }
11
- this.mappings = null;
15
+ // We will store { [instrumentId]: { sum: 0, sumSq: 0, count: 0 } }
16
+ this.assets = {};
12
17
  }
13
18
 
14
- process(portfolioData, yesterdayPortfolio, userId, context) {
15
- const positions = portfolioData.AggregatedPositions || portfolioData.PublicPositions;
16
- if (!positions) {
17
- return;
18
- }
19
-
20
- for (const position of positions) {
21
- const instrumentId = position.InstrumentID;
22
- // Use NetProfit (which is the P&L value)
23
- const netProfit = position.NetProfit;
24
-
25
- // Ensure netProfit is a valid number
26
- if (instrumentId && typeof netProfit === 'number' && isFinite(netProfit)) {
27
- if (!this.distributionData[instrumentId]) {
28
- this.distributionData[instrumentId] = {
29
- pnl_sum: 0,
30
- pnl_sum_sq: 0, // Sum of squares
31
- position_count: 0
32
- };
19
+ /**
20
+ * Defines the output schema for this calculation.
21
+ * @returns {object} JSON Schema object
22
+ */
23
+ static getSchema() {
24
+ const distSchema = {
25
+ "type": "object",
26
+ "description": "Raw statistical components for P&L on a single asset.",
27
+ "properties": {
28
+ "sum": {
29
+ "type": "number",
30
+ "description": "Sum of P&L values."
31
+ },
32
+ "sumSq": {
33
+ "type": "number",
34
+ "description": "Sum of squared P&L values (for variance calculation)."
35
+ },
36
+ "count": {
37
+ "type": "number",
38
+ "description": "Count of positions."
33
39
  }
40
+ },
41
+ "required": ["sum", "sumSq", "count"]
42
+ };
34
43
 
35
- this.distributionData[instrumentId].pnl_sum += netProfit;
36
- this.distributionData[instrumentId].pnl_sum_sq += (netProfit * netProfit); // Add the square
37
- this.distributionData[instrumentId].position_count++;
38
- }
39
- }
44
+ return {
45
+ "type": "object",
46
+ "description": "Collects P&L distribution components (sum, sumSq, count) per asset, keyed by InstrumentID.",
47
+ "patternProperties": {
48
+ "^[0-9]+$": distSchema // InstrumentID (numeric string)
49
+ },
50
+ "additionalProperties": distSchema
51
+ };
40
52
  }
41
53
 
42
- async getResult() {
43
- if (!this.mappings) {
44
- this.mappings = await loadInstrumentMappings();
54
+ process(portfolioData) {
55
+ const positions = portfolioData.PublicPositions || portfolioData.AggregatedPositions;
56
+ if (!positions || !Array.isArray(positions)) {
57
+ return;
45
58
  }
46
59
 
47
- const result = {};
48
- for (const instrumentId in this.distributionData) {
49
- const ticker = this.mappings.instrumentToTicker[instrumentId] || instrumentId.toString();
50
- result[ticker] = this.distributionData[instrumentId];
60
+ for (const pos of positions) {
61
+ const instrumentId = pos.InstrumentID;
62
+ if (!instrumentId) continue;
63
+
64
+ if (!this.assets[instrumentId]) {
65
+ this.assets[instrumentId] = { sum: 0, sumSq: 0, count: 0 };
66
+ }
67
+
68
+ const pnl = pos.NetProfit || 0;
69
+
70
+ this.assets[instrumentId].sum += pnl;
71
+ this.assets[instrumentId].sumSq += (pnl * pnl);
72
+ this.assets[instrumentId].count++;
51
73
  }
74
+ }
52
75
 
53
- if (Object.keys(result).length === 0) return {};
54
-
55
- return {
56
- pnl_distribution_by_asset: result
57
- };
76
+ getResult() {
77
+ // Return the raw aggregated data, keyed by instrumentId
78
+ return this.assets;
58
79
  }
59
80
 
60
81
  reset() {
61
- this.distributionData = {};
62
- this.mappings = null;
82
+ this.assets = {};
63
83
  }
64
84
  }
65
85
 
@@ -1,30 +1,80 @@
1
1
  /**
2
- * @fileoverview Calculates the ratio of profitable vs. unprofitable positions for each stock.
2
+ * @fileoverview Calculation (Pass 1) for profitability ratio per stock.
3
+ *
4
+ * This metric answers: "For each stock, what is the count of
5
+ * profitable versus unprofitable positions?"
3
6
  */
4
7
  const { loadInstrumentMappings } = require('../../utils/sector_mapping_provider');
5
8
 
6
9
  class ProfitabilityRatioPerStock {
7
10
  constructor() {
8
- this.profitabilityData = {};
11
+ // We will store { [instrumentId]: { profitable: 0, unprofitable: 0 } }
12
+ this.assets = new Map();
9
13
  this.mappings = null;
10
14
  }
11
15
 
12
- process(portfolioData, yesterdayPortfolio, userId, context) {
13
- const positions = portfolioData.AggregatedPositions || portfolioData.PublicPositions;
14
- if (!positions) return;
16
+ /**
17
+ * Defines the output schema for this calculation.
18
+ * @returns {object} JSON Schema object
19
+ */
20
+ static getSchema() {
21
+ const tickerSchema = {
22
+ "type": "object",
23
+ "description": "Profit/loss position counts for a specific asset.",
24
+ "properties": {
25
+ "profitable_count": {
26
+ "type": "number",
27
+ "description": "Count of positions in profit."
28
+ },
29
+ "unprofitable_count": {
30
+ "type": "number",
31
+ "description": "Count of positions in loss."
32
+ },
33
+ "ratio": {
34
+ "type": ["number", "null"],
35
+ "description": "Ratio of profitable to unprofitable (Profit / Loss). Null if no losing positions."
36
+ }
37
+ },
38
+ "required": ["profitable_count", "unprofitable_count", "ratio"]
39
+ };
15
40
 
16
- for (const position of positions) {
17
- const instrumentId = position.InstrumentID;
18
- const netProfit = position.NetProfit;
41
+ return {
42
+ "type": "object",
43
+ "description": "Calculates the count of profitable vs. unprofitable positions for each asset.",
44
+ "patternProperties": {
45
+ "^.*$": tickerSchema // Ticker
46
+ },
47
+ "additionalProperties": tickerSchema
48
+ };
49
+ }
19
50
 
20
- if (!this.profitabilityData[instrumentId]) {
21
- this.profitabilityData[instrumentId] = { profitable: 0, unprofitable: 0 };
22
- }
51
+ _initAsset(instrumentId) {
52
+ if (!this.assets.has(instrumentId)) {
53
+ this.assets.set(instrumentId, {
54
+ profitable: 0,
55
+ unprofitable: 0
56
+ });
57
+ }
58
+ }
23
59
 
24
- if (netProfit > 0) {
25
- this.profitabilityData[instrumentId].profitable++;
26
- } else if (netProfit < 0) {
27
- this.profitabilityData[instrumentId].unprofitable++;
60
+ process(portfolioData) {
61
+ const positions = portfolioData.PublicPositions || portfolioData.AggregatedPositions;
62
+ if (!positions || !Array.isArray(positions)) {
63
+ return;
64
+ }
65
+
66
+ for (const pos of positions) {
67
+ const instrumentId = pos.InstrumentID;
68
+ if (!instrumentId) continue;
69
+
70
+ this._initAsset(instrumentId);
71
+ const assetData = this.assets.get(instrumentId);
72
+ const pnl = pos.NetProfit || 0;
73
+
74
+ if (pnl > 0) {
75
+ assetData.profitable++;
76
+ } else if (pnl < 0) {
77
+ assetData.unprofitable++;
28
78
  }
29
79
  }
30
80
  }
@@ -33,17 +83,24 @@ class ProfitabilityRatioPerStock {
33
83
  if (!this.mappings) {
34
84
  this.mappings = await loadInstrumentMappings();
35
85
  }
86
+
36
87
  const result = {};
37
- for (const instrumentId in this.profitabilityData) {
38
- const ticker = this.mappings.instrumentToTicker[instrumentId] || instrumentId.toString();
39
- result[ticker] = this.profitabilityData[instrumentId];
88
+ for (const [instrumentId, data] of this.assets.entries()) {
89
+ const ticker = this.mappings.instrumentToTicker[instrumentId] || `id_${instrumentId}`;
90
+
91
+ if (data.profitable > 0 || data.unprofitable > 0) {
92
+ result[ticker] = {
93
+ profitable_count: data.profitable,
94
+ unprofitable_count: data.unprofitable,
95
+ ratio: (data.unprofitable > 0) ? (data.profitable / data.unprofitable) : null
96
+ };
97
+ }
40
98
  }
41
- if (Object.keys(result).length === 0) return {};
42
- return { profitability_by_asset: result };
99
+ return result;
43
100
  }
44
101
 
45
102
  reset() {
46
- this.profitabilityData = {};
103
+ this.assets.clear();
47
104
  this.mappings = null;
48
105
  }
49
106
  }