aiden-shared-calculations-unified 1.0.35 → 1.0.37

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/README.MD +77 -77
  2. package/calculations/activity/historical/activity_by_pnl_status.js +85 -85
  3. package/calculations/activity/historical/daily_asset_activity.js +85 -85
  4. package/calculations/activity/historical/daily_user_activity_tracker.js +144 -144
  5. package/calculations/activity/historical/speculator_adjustment_activity.js +76 -76
  6. package/calculations/asset_metrics/asset_position_size.js +57 -57
  7. package/calculations/backtests/strategy-performance.js +229 -245
  8. package/calculations/behavioural/historical/asset_crowd_flow.js +165 -165
  9. package/calculations/behavioural/historical/drawdown_response.js +58 -58
  10. package/calculations/behavioural/historical/dumb-cohort-flow.js +217 -249
  11. package/calculations/behavioural/historical/gain_response.js +57 -57
  12. package/calculations/behavioural/historical/in_loss_asset_crowd_flow.js +98 -98
  13. package/calculations/behavioural/historical/in_profit_asset_crowd_flow.js +99 -99
  14. package/calculations/behavioural/historical/paper_vs_diamond_hands.js +39 -39
  15. package/calculations/behavioural/historical/position_count_pnl.js +67 -67
  16. package/calculations/behavioural/historical/smart-cohort-flow.js +217 -250
  17. package/calculations/behavioural/historical/smart_money_flow.js +165 -165
  18. package/calculations/behavioural/historical/user-investment-profile.js +358 -412
  19. package/calculations/capital_flow/historical/crowd-cash-flow-proxy.js +121 -121
  20. package/calculations/capital_flow/historical/deposit_withdrawal_percentage.js +117 -117
  21. package/calculations/capital_flow/historical/new_allocation_percentage.js +49 -49
  22. package/calculations/insights/daily_bought_vs_sold_count.js +55 -55
  23. package/calculations/insights/daily_buy_sell_sentiment_count.js +49 -49
  24. package/calculations/insights/daily_ownership_delta.js +55 -55
  25. package/calculations/insights/daily_total_positions_held.js +39 -39
  26. package/calculations/meta/capital_deployment_strategy.js +129 -137
  27. package/calculations/meta/capital_liquidation_performance.js +121 -163
  28. package/calculations/meta/capital_vintage_performance.js +121 -158
  29. package/calculations/meta/cash-flow-deployment.js +110 -124
  30. package/calculations/meta/cash-flow-liquidation.js +126 -142
  31. package/calculations/meta/crowd_sharpe_ratio_proxy.js +83 -91
  32. package/calculations/meta/profit_cohort_divergence.js +77 -91
  33. package/calculations/meta/smart-dumb-divergence-index.js +116 -138
  34. package/calculations/meta/social_flow_correlation.js +99 -125
  35. package/calculations/pnl/asset_pnl_status.js +46 -46
  36. package/calculations/pnl/historical/profitability_migration.js +57 -57
  37. package/calculations/pnl/historical/user_profitability_tracker.js +117 -117
  38. package/calculations/pnl/profitable_and_unprofitable_status.js +64 -64
  39. package/calculations/sectors/historical/diversification_pnl.js +76 -76
  40. package/calculations/sectors/historical/sector_rotation.js +67 -67
  41. package/calculations/sentiment/historical/crowd_conviction_score.js +80 -80
  42. package/calculations/socialPosts/social-asset-posts-trend.js +52 -52
  43. package/calculations/socialPosts/social-top-mentioned-words.js +102 -102
  44. package/calculations/socialPosts/social-topic-interest-evolution.js +53 -53
  45. package/calculations/socialPosts/social-word-mentions-trend.js +62 -62
  46. package/calculations/socialPosts/social_activity_aggregation.js +103 -103
  47. package/calculations/socialPosts/social_event_correlation.js +121 -121
  48. package/calculations/socialPosts/social_sentiment_aggregation.js +114 -114
  49. package/calculations/speculators/historical/risk_appetite_change.js +54 -54
  50. package/calculations/speculators/historical/tsl_effectiveness.js +74 -74
  51. package/index.js +33 -33
  52. package/package.json +32 -32
  53. package/utils/firestore_utils.js +76 -76
  54. package/utils/price_data_provider.js +142 -142
  55. package/utils/sector_mapping_provider.js +74 -74
  56. package/calculations/capital_flow/historical/reallocation_increase_percentage.js +0 -63
  57. package/calculations/speculators/stop_loss_distance_by_sector_short_long_breakdown.js +0 -91
  58. package/calculations/speculators/stop_loss_distance_by_ticker_short_long_breakdown.js +0 -73
package/README.MD CHANGED
@@ -1,78 +1,78 @@
1
- # Unified Calculations Package (`aiden-shared-calculations-unified`)
2
-
3
- **Version:** 1.0.0
4
-
5
- ## Overview
6
-
7
- This package centralizes all data calculation logic for the BullTrackers project. It provides a standardized structure for calculations run by the unified Computation System and includes shared utility functions. Calculations are dynamically loaded and categorized.
8
-
9
- ## Package Structure
10
-
11
- ### `/utils`
12
-
13
- Shared utility functions used by calculations or the systems consuming them.
14
-
15
- * `firestore_utils.js`: Provides a resilient `withRetry` wrapper for Firestore operations using exponential backoff.
16
- * `sector_mapping_provider.js`: Provides functions (`loadInstrumentMappings`, `getInstrumentSectorMap`) to fetch and cache instrument-to-ticker and instrument-to-sector mappings from Firestore.
17
-
18
- ### `/calculations`
19
-
20
- Contains the core calculation logic, organized into subdirectories representing categories.
21
-
22
- * **Calculation Class Structure:** Each `.js` file defines a class responsible for a specific metric. Every class **must** implement:
23
- * `constructor()`: Initializes any internal state needed for aggregation.
24
- * `process(portfolioData, userId, context)` OR `process(todayPortfolio, yesterdayPortfolio, userId)`: Processes a single user's data. The signature depends on whether the calculation requires historical comparison. `context` provides shared data like mappings.
25
- * `getResult()`: Returns the final, calculated result for the aggregation period. **Crucially, this method must perform any final averaging (e.g., sum/count) itself.** It should return the final value or object ready for storage, not raw components.
26
- * `reset()`: (Optional but recommended) Resets the internal state, often used by the calling system between processing batches or days.
27
-
28
- * **Categories (Examples based on your files):**
29
- * `/asset_metrics`: Calculations focused on individual assets (e.g., `asset_dollar_metrics`, `asset_position_size`).
30
- * `/behavioural`: Calculations analyzing user trading patterns (e.g., `drawdown_response`, `gain_response`, `paper_vs_diamond_hands`, `position_count_pnl`, `smart_money_flow`).
31
- * `/pnl`: Profit and Loss related calculations (e.g., `asset_pnl_status`, `average_daily_pnl_all_users`, `average_daily_pnl_per_sector`, `average_daily_pnl_per_stock`, `average_daily_position_pnl`, `pnl_distribution_per_stock`, `profitability_migration`, `profitability_ratio_per_stock`, `profitability_skew_per_stock`, `user_profitability_tracker`).
32
- * `/sanity`: Basic checks and counts (e.g., `users_processed`).
33
- * `/sectors`: Calculations aggregated by market sector (e.g., `diversification_pnl`, `sector_dollar_metrics`, `sector_rotation`, `total_long_per_sector`, `total_short_per_sector`).
34
- * `/sentiment`: Calculations related to market sentiment (e.g., `crowd_conviction_score`).
35
- * `/short_and_long_stats`: Specific counts for short and long positions (e.g., `long_position_per_stock`, `sentiment_per_stock`, `short_position_per_stock`, `total_long_figures`, `total_short_figures`).
36
- * `/speculators`: Calculations **specifically** for the 'speculator' user type, often involving leverage, stop-loss, or take-profit data (e.g., `distance_to_stop_loss_per_leverage`, `distance_to_tp_per_leverage`, `entry_distance_to_sl_per_leverage`, `entry_distance_to_tp_per_leverage`, `holding_duration_per_asset`, `leverage_per_asset`, `leverage_per_sector`, `risk_appetite_change`, `risk_reward_ratio_per_asset`, `speculator_asset_sentiment`, `speculator_danger_zone`, `stop_loss_distance_by_sector_short_long_breakdown`, `stop_loss_distance_by_ticker_short_long_breakdown`, `stop_loss_per_asset`, `take_profit_per_asset`, `tsl_effectiveness`, `tsl_per_asset`).
37
-
38
- * **Output Formats:** Calculations should adhere to the standardized output formats defined in `docs/Notes/output_formats.md`.
39
-
40
- ## Usage
41
-
42
- This package is intended to be consumed primarily by the unified Computation System.
43
-
44
- ```javascript
45
- // Example usage within Computation System
46
- const { calculations, utils } = require('aiden-shared-calculations-unified');
47
-
48
- const CalculationClass = calculations.pnl.average_daily_pnl_per_stock;
49
- const calculator = new CalculationClass();
50
-
51
- // ... load data ...
52
-
53
- // In a loop for each user:
54
- calculator.process(portfolioData, userId, context);
55
-
56
- // After processing all users:
57
- const results = await calculator.getResult();
58
-
59
- // ... store results ...
60
- ````
61
-
62
- ## Contributing
63
-
64
- *(Outline the process for adding new calculations)*
65
-
66
- 1. **Determine Category:** Decide which subdirectory (`/calculations/<category>`) the new metric belongs to. Create a new category if necessary.
67
- 2. **Create File:** Create a new `.js` file using kebab-case (e.g., `my-new-metric.js`).
68
- 3. **Implement Class:** Define the calculation class, ensuring it has `constructor`, `process`, and `getResult` methods adhering to the standards. Remember `getResult` must return the *final* computed value.
69
- 4. **Add to Manifest:** The main `index.js` uses `require-all`, so the new calculation should be automatically included in the exports upon the next package build/publish, assuming the file naming and structure are correct.
70
- 5. **Publish:** Bump the package version (`npm version patch` or minor/major as appropriate) and publish (`npm publish --access public`).
71
- 6. **Update Consumer:** Update the version dependency in the Computation System's `package.json`.
72
-
73
- <!-- end list -->
74
-
75
- ```
76
-
77
- ---
1
+ # Unified Calculations Package (`aiden-shared-calculations-unified`)
2
+
3
+ **Version:** 1.0.0
4
+
5
+ ## Overview
6
+
7
+ This package centralizes all data calculation logic for the BullTrackers project. It provides a standardized structure for calculations run by the unified Computation System and includes shared utility functions. Calculations are dynamically loaded and categorized.
8
+
9
+ ## Package Structure
10
+
11
+ ### `/utils`
12
+
13
+ Shared utility functions used by calculations or the systems consuming them.
14
+
15
+ * `firestore_utils.js`: Provides a resilient `withRetry` wrapper for Firestore operations using exponential backoff.
16
+ * `sector_mapping_provider.js`: Provides functions (`loadInstrumentMappings`, `getInstrumentSectorMap`) to fetch and cache instrument-to-ticker and instrument-to-sector mappings from Firestore.
17
+
18
+ ### `/calculations`
19
+
20
+ Contains the core calculation logic, organized into subdirectories representing categories.
21
+
22
+ * **Calculation Class Structure:** Each `.js` file defines a class responsible for a specific metric. Every class **must** implement:
23
+ * `constructor()`: Initializes any internal state needed for aggregation.
24
+ * `process(portfolioData, userId, context)` OR `process(todayPortfolio, yesterdayPortfolio, userId)`: Processes a single user's data. The signature depends on whether the calculation requires historical comparison. `context` provides shared data like mappings.
25
+ * `getResult()`: Returns the final, calculated result for the aggregation period. **Crucially, this method must perform any final averaging (e.g., sum/count) itself.** It should return the final value or object ready for storage, not raw components.
26
+ * `reset()`: (Optional but recommended) Resets the internal state, often used by the calling system between processing batches or days.
27
+
28
+ * **Categories (Examples based on your files):**
29
+ * `/asset_metrics`: Calculations focused on individual assets (e.g., `asset_dollar_metrics`, `asset_position_size`).
30
+ * `/behavioural`: Calculations analyzing user trading patterns (e.g., `drawdown_response`, `gain_response`, `paper_vs_diamond_hands`, `position_count_pnl`, `smart_money_flow`).
31
+ * `/pnl`: Profit and Loss related calculations (e.g., `asset_pnl_status`, `average_daily_pnl_all_users`, `average_daily_pnl_per_sector`, `average_daily_pnl_per_stock`, `average_daily_position_pnl`, `pnl_distribution_per_stock`, `profitability_migration`, `profitability_ratio_per_stock`, `profitability_skew_per_stock`, `user_profitability_tracker`).
32
+ * `/sanity`: Basic checks and counts (e.g., `users_processed`).
33
+ * `/sectors`: Calculations aggregated by market sector (e.g., `diversification_pnl`, `sector_dollar_metrics`, `sector_rotation`, `total_long_per_sector`, `total_short_per_sector`).
34
+ * `/sentiment`: Calculations related to market sentiment (e.g., `crowd_conviction_score`).
35
+ * `/short_and_long_stats`: Specific counts for short and long positions (e.g., `long_position_per_stock`, `sentiment_per_stock`, `short_position_per_stock`, `total_long_figures`, `total_short_figures`).
36
+ * `/speculators`: Calculations **specifically** for the 'speculator' user type, often involving leverage, stop-loss, or take-profit data (e.g., `distance_to_stop_loss_per_leverage`, `distance_to_tp_per_leverage`, `entry_distance_to_sl_per_leverage`, `entry_distance_to_tp_per_leverage`, `holding_duration_per_asset`, `leverage_per_asset`, `leverage_per_sector`, `risk_appetite_change`, `risk_reward_ratio_per_asset`, `speculator_asset_sentiment`, `speculator_danger_zone`, `stop_loss_distance_by_sector_short_long_breakdown`, `stop_loss_distance_by_ticker_short_long_breakdown`, `stop_loss_per_asset`, `take_profit_per_asset`, `tsl_effectiveness`, `tsl_per_asset`).
37
+
38
+ * **Output Formats:** Calculations should adhere to the standardized output formats defined in `docs/Notes/output_formats.md`.
39
+
40
+ ## Usage
41
+
42
+ This package is intended to be consumed primarily by the unified Computation System.
43
+
44
+ ```javascript
45
+ // Example usage within Computation System
46
+ const { calculations, utils } = require('aiden-shared-calculations-unified');
47
+
48
+ const CalculationClass = calculations.pnl.average_daily_pnl_per_stock;
49
+ const calculator = new CalculationClass();
50
+
51
+ // ... load data ...
52
+
53
+ // In a loop for each user:
54
+ calculator.process(portfolioData, userId, context);
55
+
56
+ // After processing all users:
57
+ const results = await calculator.getResult();
58
+
59
+ // ... store results ...
60
+ ````
61
+
62
+ ## Contributing
63
+
64
+ *(Outline the process for adding new calculations)*
65
+
66
+ 1. **Determine Category:** Decide which subdirectory (`/calculations/<category>`) the new metric belongs to. Create a new category if necessary.
67
+ 2. **Create File:** Create a new `.js` file using kebab-case (e.g., `my-new-metric.js`).
68
+ 3. **Implement Class:** Define the calculation class, ensuring it has `constructor`, `process`, and `getResult` methods adhering to the standards. Remember `getResult` must return the *final* computed value.
69
+ 4. **Add to Manifest:** The main `index.js` uses `require-all`, so the new calculation should be automatically included in the exports upon the next package build/publish, assuming the file naming and structure are correct.
70
+ 5. **Publish:** Bump the package version (`npm version patch` or minor/major as appropriate) and publish (`npm publish --access public`).
71
+ 6. **Update Consumer:** Update the version dependency in the Computation System's `package.json`.
72
+
73
+ <!-- end list -->
74
+
75
+ ```
76
+
77
+ ---
78
78
  ```
@@ -1,86 +1,86 @@
1
- /**
2
- * @fileoverview Analyzes *why* users were active by checking the P&L status
3
- * of positions just before they were closed.
4
- * This measures "Profit Taking" vs. "Capitulation".
5
- */
6
- class ActivityByPnlStatus {
7
- constructor() {
8
- this.total_positions_yesterday = {
9
- in_profit: 0,
10
- in_loss: 0
11
- };
12
- this.closed_positions_today = {
13
- profit_taken: 0,
14
- loss_realized: 0
15
- };
16
- }
17
-
18
- _getPortfolioMap(portfolio) {
19
- // We MUST use PositionID here to track specific trades, not just the asset
20
- const positions = portfolio?.AggregatedPositions || portfolio?.PublicPositions;
21
- if (!positions || !Array.isArray(positions)) {
22
- return new Map();
23
- }
24
- // Map<PositionID, NetProfit>
25
- return new Map(positions.map(p => [p.PositionID, p.NetProfit || 0]));
26
- }
27
-
28
- process(todayPortfolio, yesterdayPortfolio, userId) {
29
- if (!todayPortfolio || !yesterdayPortfolio) {
30
- return;
31
- }
32
-
33
- const yPosMap = this._getPortfolioMap(yesterdayPortfolio);
34
- const tPosMap = this._getPortfolioMap(todayPortfolio);
35
-
36
- if (yPosMap.size === 0) {
37
- return; // No positions yesterday to analyze
38
- }
39
-
40
- for (const [yPosId, yNetProfit] of yPosMap.entries()) {
41
- // 1. Bucket yesterday's P&L state
42
- if (yNetProfit > 0) {
43
- this.total_positions_yesterday.in_profit++;
44
- } else if (yNetProfit < 0) {
45
- this.total_positions_yesterday.in_loss++;
46
- }
47
-
48
- // 2. Check if this position was closed
49
- if (!tPosMap.has(yPosId)) {
50
- // Position was closed. Check its P&L from yesterday.
51
- if (yNetProfit > 0) {
52
- this.closed_positions_today.profit_taken++;
53
- } else if (yNetProfit < 0) {
54
- this.closed_positions_today.loss_realized++;
55
- }
56
- }
57
- }
58
- }
59
-
60
- getResult() {
61
- const { in_profit, in_loss } = this.total_positions_yesterday;
62
- const { profit_taken, loss_realized } = this.closed_positions_today;
63
-
64
- // Calculate rates to normalize the data
65
- const profit_taking_rate = (in_profit > 0) ? (profit_taken / in_profit) * 100 : 0;
66
- const capitulation_rate = (in_loss > 0) ? (loss_realized / in_loss) * 100 : 0;
67
-
68
- return {
69
- profit_taking_rate_pct: profit_taking_rate, // % of profitable positions that were closed
70
- capitulation_rate_pct: capitulation_rate, // % of losing positions that were closed
71
- raw_counts: {
72
- profit_positions_closed: profit_taken,
73
- loss_positions_closed: loss_realized,
74
- total_profit_positions_available: in_profit,
75
- total_loss_positions_available: in_loss
76
- }
77
- };
78
- }
79
-
80
- reset() {
81
- this.total_positions_yesterday = { in_profit: 0, in_loss: 0 };
82
- this.closed_positions_today = { profit_taken: 0, loss_realized: 0 };
83
- }
84
- }
85
-
1
+ /**
2
+ * @fileoverview Analyzes *why* users were active by checking the P&L status
3
+ * of positions just before they were closed.
4
+ * This measures "Profit Taking" vs. "Capitulation".
5
+ */
6
+ class ActivityByPnlStatus {
7
+ constructor() {
8
+ this.total_positions_yesterday = {
9
+ in_profit: 0,
10
+ in_loss: 0
11
+ };
12
+ this.closed_positions_today = {
13
+ profit_taken: 0,
14
+ loss_realized: 0
15
+ };
16
+ }
17
+
18
+ _getPortfolioMap(portfolio) {
19
+ // We MUST use PositionID here to track specific trades, not just the asset
20
+ const positions = portfolio?.AggregatedPositions || portfolio?.PublicPositions;
21
+ if (!positions || !Array.isArray(positions)) {
22
+ return new Map();
23
+ }
24
+ // Map<PositionID, NetProfit>
25
+ return new Map(positions.map(p => [p.PositionID, p.NetProfit || 0]));
26
+ }
27
+
28
+ process(todayPortfolio, yesterdayPortfolio, userId) {
29
+ if (!todayPortfolio || !yesterdayPortfolio) {
30
+ return;
31
+ }
32
+
33
+ const yPosMap = this._getPortfolioMap(yesterdayPortfolio);
34
+ const tPosMap = this._getPortfolioMap(todayPortfolio);
35
+
36
+ if (yPosMap.size === 0) {
37
+ return; // No positions yesterday to analyze
38
+ }
39
+
40
+ for (const [yPosId, yNetProfit] of yPosMap.entries()) {
41
+ // 1. Bucket yesterday's P&L state
42
+ if (yNetProfit > 0) {
43
+ this.total_positions_yesterday.in_profit++;
44
+ } else if (yNetProfit < 0) {
45
+ this.total_positions_yesterday.in_loss++;
46
+ }
47
+
48
+ // 2. Check if this position was closed
49
+ if (!tPosMap.has(yPosId)) {
50
+ // Position was closed. Check its P&L from yesterday.
51
+ if (yNetProfit > 0) {
52
+ this.closed_positions_today.profit_taken++;
53
+ } else if (yNetProfit < 0) {
54
+ this.closed_positions_today.loss_realized++;
55
+ }
56
+ }
57
+ }
58
+ }
59
+
60
+ getResult() {
61
+ const { in_profit, in_loss } = this.total_positions_yesterday;
62
+ const { profit_taken, loss_realized } = this.closed_positions_today;
63
+
64
+ // Calculate rates to normalize the data
65
+ const profit_taking_rate = (in_profit > 0) ? (profit_taken / in_profit) * 100 : 0;
66
+ const capitulation_rate = (in_loss > 0) ? (loss_realized / in_loss) * 100 : 0;
67
+
68
+ return {
69
+ profit_taking_rate_pct: profit_taking_rate, // % of profitable positions that were closed
70
+ capitulation_rate_pct: capitulation_rate, // % of losing positions that were closed
71
+ raw_counts: {
72
+ profit_positions_closed: profit_taken,
73
+ loss_positions_closed: loss_realized,
74
+ total_profit_positions_available: in_profit,
75
+ total_loss_positions_available: in_loss
76
+ }
77
+ };
78
+ }
79
+
80
+ reset() {
81
+ this.total_positions_yesterday = { in_profit: 0, in_loss: 0 };
82
+ this.closed_positions_today = { profit_taken: 0, loss_realized: 0 };
83
+ }
84
+ }
85
+
86
86
  module.exports = ActivityByPnlStatus;
@@ -1,86 +1,86 @@
1
- /**
2
- * @fileoverview Tracks the flow of unique users opening or closing positions
3
- * on a per-asset basis. This measures the "focus" of the crowd's activity.
4
- */
5
- const { loadInstrumentMappings } = require('../../../utils/sector_mapping_provider');
6
-
7
- class DailyAssetActivity {
8
- constructor() {
9
- // We will store { [instrumentId]: { new_users: Set(), closed_users: Set() } }
10
- this.assetActivity = new Map();
11
- this.mappings = null;
12
- }
13
-
14
- _initAsset(instrumentId) {
15
- if (!this.assetActivity.has(instrumentId)) {
16
- this.assetActivity.set(instrumentId, {
17
- new_users: new Set(),
18
- closed_users: new Set()
19
- });
20
- }
21
- }
22
-
23
- _getInstrumentIds(portfolio) {
24
- const positions = portfolio?.AggregatedPositions || portfolio?.PublicPositions;
25
- if (!positions || !Array.isArray(positions)) {
26
- return new Set();
27
- }
28
- return new Set(positions.map(p => p.InstrumentID).filter(Boolean));
29
- }
30
-
31
- process(todayPortfolio, yesterdayPortfolio, userId) {
32
- if (!todayPortfolio || !yesterdayPortfolio) {
33
- return;
34
- }
35
-
36
- const yIds = this._getInstrumentIds(yesterdayPortfolio);
37
- const tIds = this._getInstrumentIds(todayPortfolio);
38
-
39
- // Find new positions (in today but not yesterday)
40
- for (const tId of tIds) {
41
- if (!yIds.has(tId)) {
42
- this._initAsset(tId);
43
- this.assetActivity.get(tId).new_users.add(userId);
44
- }
45
- }
46
-
47
- // Find closed positions (in yesterday but not today)
48
- for (const yId of yIds) {
49
- if (!tIds.has(yId)) {
50
- this._initAsset(yId);
51
- this.assetActivity.get(yId).closed_users.add(userId);
52
- }
53
- }
54
- }
55
-
56
- async getResult() {
57
- if (!this.mappings) {
58
- this.mappings = await loadInstrumentMappings();
59
- }
60
-
61
- const result = {};
62
- for (const [instrumentId, data] of this.assetActivity.entries()) {
63
- const ticker = this.mappings.instrumentToTicker[instrumentId] || `id_${instrumentId}`;
64
-
65
- const openCount = data.new_users.size;
66
- const closeCount = data.closed_users.size;
67
-
68
- if (openCount > 0 || closeCount > 0) {
69
- result[ticker] = {
70
- opened_by_user_count: openCount,
71
- closed_by_user_count: closeCount,
72
- // "Net User Flow" - positive means more users joined than left
73
- net_user_flow: openCount - closeCount
74
- };
75
- }
76
- }
77
- return result;
78
- }
79
-
80
- reset() {
81
- this.assetActivity.clear();
82
- this.mappings = null;
83
- }
84
- }
85
-
1
+ /**
2
+ * @fileoverview Tracks the flow of unique users opening or closing positions
3
+ * on a per-asset basis. This measures the "focus" of the crowd's activity.
4
+ */
5
+ const { loadInstrumentMappings } = require('../../../utils/sector_mapping_provider');
6
+
7
+ class DailyAssetActivity {
8
+ constructor() {
9
+ // We will store { [instrumentId]: { new_users: Set(), closed_users: Set() } }
10
+ this.assetActivity = new Map();
11
+ this.mappings = null;
12
+ }
13
+
14
+ _initAsset(instrumentId) {
15
+ if (!this.assetActivity.has(instrumentId)) {
16
+ this.assetActivity.set(instrumentId, {
17
+ new_users: new Set(),
18
+ closed_users: new Set()
19
+ });
20
+ }
21
+ }
22
+
23
+ _getInstrumentIds(portfolio) {
24
+ const positions = portfolio?.AggregatedPositions || portfolio?.PublicPositions;
25
+ if (!positions || !Array.isArray(positions)) {
26
+ return new Set();
27
+ }
28
+ return new Set(positions.map(p => p.InstrumentID).filter(Boolean));
29
+ }
30
+
31
+ process(todayPortfolio, yesterdayPortfolio, userId) {
32
+ if (!todayPortfolio || !yesterdayPortfolio) {
33
+ return;
34
+ }
35
+
36
+ const yIds = this._getInstrumentIds(yesterdayPortfolio);
37
+ const tIds = this._getInstrumentIds(todayPortfolio);
38
+
39
+ // Find new positions (in today but not yesterday)
40
+ for (const tId of tIds) {
41
+ if (!yIds.has(tId)) {
42
+ this._initAsset(tId);
43
+ this.assetActivity.get(tId).new_users.add(userId);
44
+ }
45
+ }
46
+
47
+ // Find closed positions (in yesterday but not today)
48
+ for (const yId of yIds) {
49
+ if (!tIds.has(yId)) {
50
+ this._initAsset(yId);
51
+ this.assetActivity.get(yId).closed_users.add(userId);
52
+ }
53
+ }
54
+ }
55
+
56
+ async getResult() {
57
+ if (!this.mappings) {
58
+ this.mappings = await loadInstrumentMappings();
59
+ }
60
+
61
+ const result = {};
62
+ for (const [instrumentId, data] of this.assetActivity.entries()) {
63
+ const ticker = this.mappings.instrumentToTicker[instrumentId] || `id_${instrumentId}`;
64
+
65
+ const openCount = data.new_users.size;
66
+ const closeCount = data.closed_users.size;
67
+
68
+ if (openCount > 0 || closeCount > 0) {
69
+ result[ticker] = {
70
+ opened_by_user_count: openCount,
71
+ closed_by_user_count: closeCount,
72
+ // "Net User Flow" - positive means more users joined than left
73
+ net_user_flow: openCount - closeCount
74
+ };
75
+ }
76
+ }
77
+ return result;
78
+ }
79
+
80
+ reset() {
81
+ this.assetActivity.clear();
82
+ this.mappings = null;
83
+ }
84
+ }
85
+
86
86
  module.exports = DailyAssetActivity;