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,57 +1,136 @@
1
1
  /**
2
- * Analyzes user behavior after a position experiences a >10% gain.
2
+ * @fileoverview Calculation (Pass 2) for gain response.
3
+ *
4
+ * This metric answers: "How do users behave when a position has a
5
+ * gain of over 10%?"
6
+ *
7
+ * It checks all positions from yesterday that were in >10% profit
8
+ * and tracks whether the user:
9
+ * 1. Held (position still open today, same size)
10
+ * 2. Closed (position no longer open today)
11
+ * 3. Reduced (position still open, but Invested % is smaller)
3
12
  */
4
13
  class GainResponse {
5
14
  constructor() {
6
- this.gain_events = {
7
- held_position: 0,
8
- closed_position: 0,
9
- reduced_position: 0 // e.g., took partial profit
15
+ this.actions = {
16
+ held: 0,
17
+ closed: 0,
18
+ reduced: 0
10
19
  };
20
+ this.total_in_gain = 0;
11
21
  }
12
22
 
13
- process(todayPortfolio, yesterdayPortfolio, userId) {
14
- if (!yesterdayPortfolio || !todayPortfolio) {
15
- return; // Need both days for comparison
23
+ /**
24
+ * Defines the output schema for this calculation.
25
+ * @returns {object} JSON Schema object
26
+ */
27
+ static getSchema() {
28
+ return {
29
+ "type": "object",
30
+ "description": "Analyzes user behavior in response to a >10% position gain.",
31
+ "properties": {
32
+ "total_positions_in_gain": {
33
+ "type": "number",
34
+ "description": "Total positions from yesterday that were in >10% gain."
35
+ },
36
+ "action_held_pct": {
37
+ "type": "number",
38
+ "description": "Percentage of gain positions that were held."
39
+ },
40
+ "action_closed_pct": {
41
+ "type": "number",
42
+ "description": "Percentage of gain positions that were closed (profit-taking)."
43
+ },
44
+ "action_reduced_pct": {
45
+ "type": "number",
46
+ "description": "Percentage of gain positions that were reduced (scaling out)."
47
+ },
48
+ "raw_counts": {
49
+ "type": "object",
50
+ "properties": {
51
+ "held": { "type": "number" },
52
+ "closed": { "type": "number" },
53
+ "reduced": { "type": "number" }
54
+ },
55
+ "required": ["held", "closed", "reduced"]
56
+ }
57
+ },
58
+ "required": ["total_positions_in_gain", "action_held_pct", "action_closed_pct", "action_reduced_pct", "raw_counts"]
59
+ };
60
+ }
61
+
62
+ _getPortfolioMap(portfolio) {
63
+ // We MUST use PositionID to track specific trades
64
+ const positions = portfolio?.AggregatedPositions || portfolio?.PublicPositions;
65
+ if (!positions || !Array.isArray(positions)) {
66
+ return new Map();
67
+ }
68
+ // Map<PositionID, { pnl: number, invested: number }>
69
+ return new Map(positions.map(p => [p.PositionID, {
70
+ pnl: (p.NetProfit || 0) / (p.InvestedAmount || p.Amount || 1), // PNL as %
71
+ invested: p.InvestedAmount || p.Amount || 0
72
+ }]));
73
+ }
74
+
75
+ process(todayPortfolio, yesterdayPortfolio) {
76
+ if (!todayPortfolio || !yesterdayPortfolio) {
77
+ return;
16
78
  }
17
79
 
18
- const yPositions = yesterdayPortfolio.AggregatedPositions || yesterdayPortfolio.PublicPositions;
19
- const tPositions = todayPortfolio.AggregatedPositions || todayPortfolio.PublicPositions;
80
+ const yPosMap = this._getPortfolioMap(yesterdayPortfolio);
81
+ const tPosMap = this._getPortfolioMap(todayPortfolio);
20
82
 
21
- if (!yPositions || !Array.isArray(yPositions) || !tPositions || !Array.isArray(tPositions)) {
83
+ if (yPosMap.size === 0) {
22
84
  return;
23
85
  }
24
86
 
25
- // Use PositionID if available (as in original file), fallback to InstrumentID
26
- const todayPositions = new Map(tPositions.map(p => [p.PositionID || p.InstrumentID, p]));
27
-
28
- for (const yPos of yPositions) {
29
- // FIX: Use the NetProfit field, which is already a percentage.
30
- // Your data sample (e.g., 23.5) shows the threshold should be 10.0.
31
- const gainPercent = yPos.NetProfit || 0;
32
- const yPosId = yPos.PositionID || yPos.InstrumentID;
33
-
34
- // Check if this position was in a >10% gain yesterday
35
- if (gainPercent > 10.0) {
36
- const todayPos = todayPositions.get(yPosId);
37
-
38
- if (!todayPos) {
39
- // Position was closed (took full profit)
40
- this.gain_events.closed_position++;
41
- } else if (todayPos.Invested < yPos.Invested) {
42
- // FIX: Use 'Invested' (percentage) to check for reduction
43
- // User reduced the position (took partial profit)
44
- this.gain_events.reduced_position++;
87
+ for (const [yPosId, yPosData] of yPosMap.entries()) {
88
+ // Check if position was in >10% gain
89
+ if (yPosData.pnl > 0.10) {
90
+ this.total_in_gain++;
91
+
92
+ // Now, check what happened today
93
+ if (!tPosMap.has(yPosId)) {
94
+ // 1. Position was closed (Profit Taking)
95
+ this.actions.closed++;
45
96
  } else {
46
- // Position was held (or added to)
47
- this.gain_events.held_position++;
97
+ const tPosData = tPosMap.get(yPosId);
98
+ // 2. Position was reduced (check for > 1% reduction to avoid noise)
99
+ if (tPosData.invested < (yPosData.invested * 0.99)) {
100
+ this.actions.reduced++;
101
+ } else {
102
+ // 3. Position was held
103
+ this.actions.held++;
104
+ }
48
105
  }
49
106
  }
50
107
  }
51
108
  }
52
109
 
53
110
  getResult() {
54
- return this.gain_events;
111
+ const total = this.total_in_gain;
112
+ if (total === 0) {
113
+ return {
114
+ total_positions_in_gain: 0,
115
+ action_held_pct: 0,
116
+ action_closed_pct: 0,
117
+ action_reduced_pct: 0,
118
+ raw_counts: this.actions
119
+ };
120
+ }
121
+
122
+ return {
123
+ total_positions_in_gain: total,
124
+ action_held_pct: (this.actions.held / total) * 100,
125
+ action_closed_pct: (this.actions.closed / total) * 100,
126
+ action_reduced_pct: (this.actions.reduced / total) * 100,
127
+ raw_counts: this.actions
128
+ };
129
+ }
130
+
131
+ reset() {
132
+ this.actions = { held: 0, closed: 0, reduced: 0 };
133
+ this.total_in_gain = 0;
55
134
  }
56
135
  }
57
136
 
@@ -1,69 +1,84 @@
1
1
  /**
2
- * @fileoverview Pass 1: Aggregates each user's historical performance
3
- * from the root history data feed.
2
+ * @fileoverview Calculation (Pass 2) for aggregating user performance.
3
+ *
4
+ * This class processes each user's daily P&L and aggregates it into
5
+ * buckets for statistical analysis. It collects all individual P&Ls
6
+ * to build a distribution.
7
+ *
8
+ * This is a foundational calculation needed by many Pass 3 metrics.
4
9
  */
5
10
  class HistoricalPerformanceAggregator {
6
11
  constructor() {
7
- this.allUserStats = {};
12
+ // Stores the 7-day weighted P&L for every user.
13
+ this.userPnlHistory = [];
8
14
  }
9
15
 
10
16
  /**
11
- * @param {object} todayPortfolio - Not used, but part of signature.
12
- * @param {object} yesterdayPortfolio - Not used.
13
- * @param {string} userId - The user's ID.
14
- * @param {object} context - Shared context data.
15
- * @param {null} todayInsights - Not used.
16
- * @param {null} yesterdayInsights - Not used.
17
- * @param {null} todaySocialPostInsights - Not used.
18
- * @param {null} yesterdaySocialPostInsights - Not used.
19
- * @param {object} todayHistoryData - The full map of { [userId]: history }
20
- * @param {null} yesterdayHistoryData - Not used.
17
+ * Defines the output schema for this calculation.
18
+ * @returns {object} JSON Schema object
21
19
  */
22
- async process(
23
- todayPortfolio, yesterdayPortfolio, userId, context,
24
- todayInsights, yesterdayInsights,
25
- todaySocialPostInsights, yesterdaySocialPostInsights,
26
- todayHistoryData, yesterdayHistoryData
27
- ) {
28
- // This calc only runs if the history data was successfully loaded
29
- if (!todayHistoryData || !userId) {
30
- return;
31
- }
20
+ static getSchema() {
21
+ return {
22
+ "type": "object",
23
+ "description": "Aggregates historical P&L from 'user_profitability_tracker' for all users to build a performance distribution.",
24
+ "properties": {
25
+ "user_pnl_distribution": {
26
+ "type": "array",
27
+ "description": "An array of 7-day weighted average P&L values, one for each user.",
28
+ "items": { "type": "number" }
29
+ },
30
+ "user_count": {
31
+ "type": "number",
32
+ "description": "Total number of users processed."
33
+ }
34
+ },
35
+ "required": ["user_pnl_distribution", "user_count"]
36
+ };
37
+ }
32
38
 
33
- const userHistory = todayHistoryData[userId];
39
+ /**
40
+ * Statically declare dependencies.
41
+ */
42
+ static getDependencies() {
43
+ return ['user_profitability_tracker'];
44
+ }
34
45
 
35
- // 'all' is the object where instrumentId === -1
36
- if (!userHistory || !userHistory.all) {
37
- return;
38
- }
46
+ /**
47
+ * process() is a no-op. All logic is in getResult().
48
+ * This calculation doesn't process portfolios; it processes
49
+ * the *result* of another calculation.
50
+ */
51
+ process() {
52
+ // No-op
53
+ }
39
54
 
40
- const stats = userHistory.all;
55
+ /**
56
+ * Aggregates results from the dependency.
57
+ */
58
+ getResult(fetchedDependencies) {
59
+ const profitabilityData = fetchedDependencies['user_profitability_tracker'];
41
60
 
42
- // Ignore users who are only mirroring
43
- if (stats.isMirror === true) {
44
- return;
45
- }
46
-
47
- // We only care about users with a meaningful trade history
48
- if (!stats.totalTrades || stats.totalTrades < 10) {
49
- return;
61
+ if (!profitabilityData || !profitabilityData.user_details) {
62
+ return {
63
+ user_pnl_distribution: [],
64
+ user_count: 0
65
+ };
50
66
  }
67
+
68
+ // Extract the 7-day weighted P&L for all users
69
+ const pnlDistribution = Object.values(profitabilityData.user_details)
70
+ .map(details => details.weighted_avg_pnl_7d)
71
+ // Filter out any null/undefined/NaN values
72
+ .filter(pnl => typeof pnl === 'number' && !isNaN(pnl));
51
73
 
52
- this.allUserStats[userId] = {
53
- winRatio: stats.winRatio,
54
- avgProfitPct: stats.avgProfitPct,
55
- avgLossPct: stats.avgLossPct,
56
- totalTrades: stats.totalTrades,
57
- avgHoldingTime: stats.avgHoldingTimeInMinutes
74
+ return {
75
+ user_pnl_distribution: pnlDistribution,
76
+ user_count: pnlDistribution.length
58
77
  };
59
78
  }
60
79
 
61
- async getResult() {
62
- return this.allUserStats;
63
- }
64
-
65
80
  reset() {
66
- this.allUserStats = {};
81
+ this.userPnlHistory = [];
67
82
  }
68
83
  }
69
84
 
@@ -1,99 +1,195 @@
1
- const { loadAllPriceData, getDailyPriceChange } = require('../../../utils/price_data_provider');
2
- const { loadInstrumentMappings } = require('../../../utils/sector_mapping_provider');
3
-
4
1
  /**
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.
2
+ * @fileoverview Calculation (Pass 3) for "In Loss" cohort asset flow.
3
+ *
4
+ * This metric calculates the "Net Crowd Flow Percentage" for each asset,
5
+ * but *only* for the cohort of users who are currently *at a loss*
6
+ * on that specific asset.
7
+ *
8
+ * This helps identify if losers are capitulating or doubling down.
9
+ *
10
+ * This calculation *depends* on 'asset_pnl_status' to identify the cohort.
8
11
  */
12
+ const { loadInstrumentMappings } = require('../../../utils/sector_mapping_provider');
13
+
9
14
  class InLossAssetCrowdFlow {
10
15
  constructor() {
11
- this.asset_values = {}; // Stores { day1_value_sum: 0, day2_value_sum: 0 }
12
- this.user_count = 0;
13
- this.priceMap = null;
16
+ this.assetData = new Map();
14
17
  this.mappings = null;
15
- this.dates = {};
18
+ this.inLossCohorts = null; // Map<ticker, Set<userId>>
19
+ }
20
+
21
+ /**
22
+ * Defines the output schema for this calculation.
23
+ * @returns {object} JSON Schema object
24
+ */
25
+ static getSchema() {
26
+ return {
27
+ "type": "object",
28
+ "description": "Calculates net capital flow % (price-adjusted) per asset, but only for the cohort of users currently in loss on that asset.",
29
+ "patternProperties": {
30
+ // Ticker
31
+ "^.*$": {
32
+ "type": "object",
33
+ "description": "Net flow metrics for a specific asset ticker from its 'in loss' cohort.",
34
+ "properties": {
35
+ "net_flow_percentage": {
36
+ "type": "number",
37
+ "description": "Net capital flow % from the 'in loss' cohort, adjusted for price changes."
38
+ },
39
+ "total_invested_today": { "type": "number" },
40
+ "total_invested_yesterday": { "type": "number" },
41
+ "cohort_size": { "type": "number" }
42
+ },
43
+ "required": ["net_flow_percentage", "total_invested_today", "total_invested_yesterday", "cohort_size"]
44
+ }
45
+ },
46
+ "additionalProperties": {
47
+ "type": "object",
48
+ "properties": {
49
+ "net_flow_percentage": { "type": "number" },
50
+ "total_invested_today": { "type": "number" },
51
+ "total_invested_yesterday": { "type": "number" },
52
+ "cohort_size": { "type": "number" }
53
+ }
54
+ }
55
+ };
56
+ }
57
+
58
+ /**
59
+ * Statically declare dependencies.
60
+ */
61
+ static getDependencies() {
62
+ return ['asset_pnl_status'];
63
+ }
64
+
65
+ _getPortfolioPositions(portfolio) {
66
+ return portfolio?.PublicPositions || portfolio?.AggregatedPositions;
16
67
  }
17
68
 
18
69
  _initAsset(instrumentId) {
19
- if (!this.asset_values[instrumentId]) {
20
- this.asset_values[instrumentId] = { day1_value_sum: 0, day2_value_sum: 0 };
70
+ if (!this.assetData.has(instrumentId)) {
71
+ this.assetData.set(instrumentId, {
72
+ total_invested_yesterday: 0,
73
+ total_invested_today: 0,
74
+ price_change_yesterday: 0,
75
+ cohort: new Set()
76
+ });
21
77
  }
22
78
  }
23
79
 
24
- process(todayPortfolio, yesterdayPortfolio, userId, context) {
25
- if (!todayPortfolio || !yesterdayPortfolio || !todayPortfolio.AggregatedPositions || !yesterdayPortfolio.AggregatedPositions) {
26
- return;
80
+ /**
81
+ * Helper to get the cohort data from the dependency.
82
+ */
83
+ _getInLossCohorts(fetchedDependencies) {
84
+ if (this.inLossCohorts) {
85
+ return this.inLossCohorts;
86
+ }
87
+
88
+ const pnlStatusData = fetchedDependencies['asset_pnl_status'];
89
+ if (!pnlStatusData) {
90
+ return new Map();
91
+ }
92
+
93
+ // Re-structure the data for efficient lookup
94
+ // Map<ticker, Set<userId>>
95
+ this.inLossCohorts = new Map();
96
+ for (const [ticker, data] of Object.entries(pnlStatusData)) {
97
+ const userSet = new Set(data.users_in_loss.map(u => u.userId));
98
+ this.inLossCohorts.set(ticker, userSet);
27
99
  }
100
+ return this.inLossCohorts;
101
+ }
28
102
 
29
- if (!this.dates.today && context.todayDateStr && context.yesterdayDateStr) {
30
- this.dates.today = context.todayDateStr;
31
- this.dates.yesterday = context.yesterdayDateStr;
103
+ process(todayPortfolio, yesterdayPortfolio, userId, context, todayInsights, yesterdayInsights, fetchedDependencies) {
104
+ if (!todayPortfolio || !yesterdayPortfolio) {
105
+ return;
32
106
  }
33
107
 
34
- const yesterdayPositions = new Map(yesterdayPortfolio.AggregatedPositions.map(p => [p.InstrumentID, p]));
35
- const todayPositions = new Map(todayPortfolio.AggregatedPositions.map(p => [p.InstrumentID, p]));
108
+ if (!this.mappings) {
109
+ // Context contains the mappings loaded in Pass 1
110
+ this.mappings = context.mappings;
111
+ }
36
112
 
37
- const allInstrumentIds = new Set([
38
- ...yesterdayPositions.keys(),
39
- ...todayPositions.keys()
40
- ]);
113
+ const cohorts = this._getInLossCohorts(fetchedDependencies);
114
+ if (cohorts.size === 0) {
115
+ return; // No dependency data
116
+ }
117
+
118
+ const yPos = this._getPortfolioPositions(yesterdayPortfolio);
119
+ const tPos = this._getPortfolioPositions(todayPortfolio);
120
+
121
+ const yPosMap = new Map(yPos?.map(p => [p.InstrumentID, p]) || []);
122
+ const tPosMap = new Map(tPos?.map(p => [p.InstrumentID, p]) || []);
123
+
124
+ const allInstrumentIds = new Set([...yPosMap.keys(), ...tPosMap.keys()]);
41
125
 
42
126
  for (const instrumentId of allInstrumentIds) {
43
- const yPos = yesterdayPositions.get(instrumentId);
44
- const tPos = todayPositions.get(instrumentId);
127
+ if (!instrumentId) continue;
45
128
 
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 ---
129
+ const ticker = this.mappings.instrumentToTicker[instrumentId] || `id_${instrumentId}`;
130
+ const cohort = cohorts.get(ticker);
53
131
 
132
+ // This user is not in the "in loss" cohort for this asset, skip.
133
+ if (!cohort || !cohort.has(userId)) {
134
+ continue;
135
+ }
136
+
137
+ // User *is* in the cohort, process their data
54
138
  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);
139
+ const asset = this.assetData.get(instrumentId);
140
+ asset.cohort.add(userId); // Track cohort size
141
+
142
+ const yP = yPosMap.get(instrumentId);
143
+ const tP = tPosMap.get(instrumentId);
144
+
145
+ const yInvested = yP?.InvestedAmount || yP?.Amount || 0;
146
+ const tInvested = tP?.InvestedAmount || tP?.Amount || 0;
147
+
148
+ if (yInvested > 0) {
149
+ asset.total_invested_yesterday += yInvested;
150
+ const yPriceChange = (yP?.PipsRate || 0) / (yP?.OpenRate || 1);
151
+ asset.price_change_yesterday += yPriceChange * yInvested;
152
+ }
153
+ if (tInvested > 0) {
154
+ asset.total_invested_today += tInvested;
155
+ }
57
156
  }
58
- this.user_count++;
59
157
  }
60
158
 
61
159
  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;
160
+ if (!this.mappings) {
161
+ this.mappings = await loadInstrumentMappings();
70
162
  }
71
-
72
- const finalResults = {};
73
- const todayStr = this.dates.today;
74
- const yesterdayStr = this.dates.yesterday;
75
163
 
76
- for (const instrumentId in this.asset_values) {
164
+ const result = {};
165
+
166
+ for (const [instrumentId, data] of this.assetData.entries()) {
77
167
  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
168
 
83
- if (priceChangePct === null) continue;
169
+ const { total_invested_yesterday, total_invested_today, price_change_yesterday, cohort } = data;
84
170
 
85
- const expected_day2_value = avg_day1_value * (1 + priceChangePct);
86
- const net_crowd_flow_pct = avg_day2_value - expected_day2_value;
171
+ if (total_invested_yesterday > 0) {
172
+ const avg_price_change_pct = price_change_yesterday / total_invested_yesterday;
173
+ const price_contribution = total_invested_yesterday * avg_price_change_pct;
174
+ const flow_contribution = total_invested_today - (total_invested_yesterday + price_contribution);
175
+ const net_flow_percentage = (flow_contribution / total_invested_yesterday) * 100;
87
176
 
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
- };
177
+ result[ticker] = {
178
+ net_flow_percentage: net_flow_percentage,
179
+ total_invested_today: total_invested_today,
180
+ total_invested_yesterday: total_invested_yesterday,
181
+ cohort_size: cohort.size
182
+ };
183
+ }
93
184
  }
94
- return finalResults;
185
+ return result;
95
186
  }
96
187
 
97
- reset() { /*...reset all properties...*/ }
188
+ reset() {
189
+ this.assetData.clear();
190
+ this.mappings = null;
191
+ this.inLossCohorts = null;
192
+ }
98
193
  }
194
+
99
195
  module.exports = InLossAssetCrowdFlow;