aiden-shared-calculations-unified 1.0.63 → 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
package/README.MD CHANGED
@@ -21,7 +21,7 @@ Contains the core calculation logic, organized into subdirectories representing
21
21
 
22
22
  * **Calculation Class Structure:** Each `.js` file defines a class responsible for a specific metric. Every class **must** implement:
23
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.
24
+ * `process(portfolioData, userId, context)` OR `process(todayPortfolio, yesterdayPortfolio, userId, context)`: Processes a single user's data. The signature depends on whether the calculation requires historical comparison. `context` provides shared data like mappings.
25
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
26
  * `reset()`: (Optional but recommended) Resets the internal state, often used by the calling system between processing batches or days.
27
27
 
@@ -15,6 +15,39 @@ class ActivityByPnlStatus {
15
15
  };
16
16
  }
17
17
 
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": "Measures profit-taking vs. capitulation by analyzing the P&L of closed positions.",
26
+ "properties": {
27
+ "profit_taking_rate_pct": {
28
+ "type": "number",
29
+ "description": "The percentage of profitable positions (from yesterday) that were closed today."
30
+ },
31
+ "capitulation_rate_pct": {
32
+ "type": "number",
33
+ "description": "The percentage of losing positions (from yesterday) that were closed today."
34
+ },
35
+ "raw_counts": {
36
+ "type": "object",
37
+ "description": "Raw counts used to calculate the rates.",
38
+ "properties": {
39
+ "profit_positions_closed": { "type": "number" },
40
+ "loss_positions_closed": { "type": "number" },
41
+ "total_profit_positions_available": { "type": "number" },
42
+ "total_loss_positions_available": { "type": "number" }
43
+ },
44
+ "required": ["profit_positions_closed", "loss_positions_closed", "total_profit_positions_available", "total_loss_positions_available"]
45
+ }
46
+ },
47
+ "required": ["profit_taking_rate_pct", "capitulation_rate_pct", "raw_counts"]
48
+ };
49
+ }
50
+
18
51
  _getPortfolioMap(portfolio) {
19
52
  // We MUST use PositionID here to track specific trades, not just the asset
20
53
  const positions = portfolio?.AggregatedPositions || portfolio?.PublicPositions;
@@ -11,6 +11,48 @@ class DailyAssetActivity {
11
11
  this.mappings = null;
12
12
  }
13
13
 
14
+ /**
15
+ * Defines the output schema for this calculation.
16
+ * @returns {object} JSON Schema object
17
+ */
18
+ static getSchema() {
19
+ return {
20
+ "type": "object",
21
+ "description": "Tracks the net flow of unique users opening or closing positions per asset.",
22
+ "patternProperties": {
23
+ // This matches any string key (which will be a ticker)
24
+ "^.*$": {
25
+ "type": "object",
26
+ "description": "User flow metrics for a specific asset ticker.",
27
+ "properties": {
28
+ "opened_by_user_count": {
29
+ "type": "number",
30
+ "description": "Count of unique users who opened a position in this asset."
31
+ },
32
+ "closed_by_user_count": {
33
+ "type": "number",
34
+ "description": "Count of unique users who closed their position in this asset."
35
+ },
36
+ "net_user_flow": {
37
+ "type": "number",
38
+ "description": "Net change in users (opened - closed)."
39
+ }
40
+ },
41
+ "required": ["opened_by_user_count", "closed_by_user_count", "net_user_flow"]
42
+ }
43
+ },
44
+ // Use 'additionalProperties' as a fallback for patternProperties
45
+ "additionalProperties": {
46
+ "type": "object",
47
+ "properties": {
48
+ "opened_by_user_count": { "type": "number" },
49
+ "closed_by_user_count": { "type": "number" },
50
+ "net_user_flow": { "type": "number" }
51
+ }
52
+ }
53
+ };
54
+ }
55
+
14
56
  _initAsset(instrumentId) {
15
57
  if (!this.assetActivity.has(instrumentId)) {
16
58
  this.assetActivity.set(instrumentId, {
@@ -17,6 +17,43 @@ class DailyUserActivityTracker {
17
17
  };
18
18
  }
19
19
 
20
+ /**
21
+ * Defines the output schema for this calculation.
22
+ * @returns {object} JSON Schema object
23
+ */
24
+ static getSchema() {
25
+ return {
26
+ "type": "object",
27
+ "description": "Tracks the count of daily active users based on portfolio changes.",
28
+ "properties": {
29
+ "rawActiveUserCount": {
30
+ "type": "number",
31
+ "description": "The total count of unique users who were active today (opened, closed, or reallocated)."
32
+ },
33
+ "activityBreakdown": {
34
+ "type": "object",
35
+ "description": "A breakdown of the first activity event type recorded for active users.",
36
+ "properties": {
37
+ "new_position": {
38
+ "type": "number",
39
+ "description": "Count of users whose first detected activity was opening a new position."
40
+ },
41
+ "closed_position": {
42
+ "type": "number",
43
+ "description": "Count of users whose first detected activity was closing an existing position."
44
+ },
45
+ "reallocation": {
46
+ "type": "number",
47
+ "description": "Count of users whose first detected activity was reallocating an existing position."
48
+ }
49
+ },
50
+ "required": ["new_position", "closed_position", "reallocation"]
51
+ }
52
+ },
53
+ "required": ["rawActiveUserCount", "activityBreakdown"]
54
+ };
55
+ }
56
+
20
57
  /**
21
58
  * Helper to get a simplified map of positions for comparison.
22
59
  * @param {object} portfolio - A user's full portfolio object.
@@ -11,6 +11,32 @@ class SpeculatorAdjustmentActivity {
11
11
  this.tsl_toggled_users = new Set();
12
12
  }
13
13
 
14
+ /**
15
+ * Defines the output schema for this calculation.
16
+ * @returns {object} JSON Schema object
17
+ */
18
+ static getSchema() {
19
+ return {
20
+ "type": "object",
21
+ "description": "Tracks unique speculators who 'tinkered' with open trades by adjusting SL, TP, or TSL.",
22
+ "properties": {
23
+ "unique_users_adjusted_sl": {
24
+ "type": "number",
25
+ "description": "Count of unique speculators who adjusted a Stop Loss on at least one position."
26
+ },
27
+ "unique_users_adjusted_tp": {
28
+ "type": "number",
29
+ "description": "Count of unique speculators who adjusted a Take Profit on at least one position."
30
+ },
31
+ "unique_users_toggled_tsl": {
32
+ "type": "number",
33
+ "description": "Count of unique speculators who enabled or disabled a Trailing Stop Loss on at least one position."
34
+ }
35
+ },
36
+ "required": ["unique_users_adjusted_sl", "unique_users_adjusted_tp", "unique_users_toggled_tsl"]
37
+ };
38
+ }
39
+
14
40
  _getPublicPositionsMap(portfolio) {
15
41
  const positions = portfolio?.PublicPositions;
16
42
  if (!positions || !Array.isArray(positions)) {
@@ -9,6 +9,42 @@ class AssetPositionSize {
9
9
  this.mappings = null;
10
10
  }
11
11
 
12
+ /**
13
+ * Defines the output schema for this calculation.
14
+ * @returns {object} JSON Schema object
15
+ */
16
+ static getSchema() {
17
+ return {
18
+ "type": "object",
19
+ "description": "Calculates the average position size (as a portfolio percentage) and holder count for each asset.",
20
+ "patternProperties": {
21
+ // This matches any string key (which will be a ticker)
22
+ "^.*$": {
23
+ "type": "object",
24
+ "description": "Metrics for a specific asset ticker.",
25
+ "properties": {
26
+ "average_position_size": {
27
+ "type": "number",
28
+ "description": "The average portfolio percentage allocated to this asset by users who hold it."
29
+ },
30
+ "position_count": {
31
+ "type": "number",
32
+ "description": "The total number of positions held in this asset across the user sample."
33
+ }
34
+ },
35
+ "required": ["average_position_size", "position_count"]
36
+ }
37
+ },
38
+ "additionalProperties": {
39
+ "type": "object",
40
+ "properties": {
41
+ "average_position_size": { "type": "number" },
42
+ "position_count": { "type": "number" }
43
+ }
44
+ }
45
+ };
46
+ }
47
+
12
48
  process(portfolioData, yesterdayPortfolio, userId, context, todayInsights, yesterdayInsights) {
13
49
  const positions = portfolioData.AggregatedPositions || portfolioData.PublicPositions;
14
50
  if (!positions || !Array.isArray(positions)) return;
@@ -23,6 +23,47 @@ class StrategyPerformance {
23
23
  return ['smart-dumb-divergence-index', 'profit-cohort-divergence'];
24
24
  }
25
25
 
26
+ /**
27
+ * Defines the output schema for this calculation.
28
+ * @returns {object} JSON Schema object
29
+ */
30
+ static getSchema() {
31
+ return {
32
+ "type": ["object", "null"],
33
+ "description": "Result of a full historical backtest of a trading strategy. Returns null if simulation fails.",
34
+ "properties": {
35
+ "strategyName": {
36
+ "type": "string",
37
+ "description": "The name of the simulated strategy."
38
+ },
39
+ "inceptionDate": {
40
+ "type": "string",
41
+ "description": "The first date a signal was found, marking the start of the backtest."
42
+ },
43
+ "endDate": {
44
+ "type": "string",
45
+ "description": "The final date of the backtest simulation."
46
+ },
47
+ "finalPortfolioValue": {
48
+ "type": "number",
49
+ "description": "The final equity value of the portfolio at the end of the simulation."
50
+ },
51
+ "totalReturnPercent": {
52
+ "type": "number",
53
+ "description": "The total percentage return of the strategy from inception to end date."
54
+ }
55
+ },
56
+ // "required" is not needed at the top level since the whole object can be null.
57
+ // If the object is *not* null, we can imply these fields are required.
58
+ "if": {
59
+ "type": "object"
60
+ },
61
+ "then": {
62
+ "required": ["strategyName", "inceptionDate", "endDate", "finalPortfolioValue", "totalReturnPercent"]
63
+ }
64
+ };
65
+ }
66
+
26
67
  constructor() {
27
68
  this.INITIAL_CASH = 100000;
28
69
  this.TRADE_SIZE_USD = 5000;
@@ -1,165 +1,162 @@
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.
6
- *
7
- * This isolates the change in an asset's average portfolio percentage
8
- * that is *not* explained by the asset's own price movement.
2
+ * @fileoverview Calculation (Pass 2) for asset crowd flow.
9
3
  *
10
- * Net Crowd Flow = (Actual % Change) - (Expected % Change from Price Move)
4
+ * This metric calculates the "Net Crowd Flow Percentage" for each asset.
5
+ * It's defined as the net percentage of the crowd's capital flowing in or out
6
+ * of an asset, *adjusted for the asset's own price movement*.
11
7
  *
12
- * A positive value means the crowd actively bought (flowed into) the asset.
13
- * A negative value means the crowd actively sold (flowed out of) the asset.
8
+ * This isolates true buying/selling pressure from simple value changes.
14
9
  */
10
+ const { loadInstrumentMappings } = require('../../../utils/sector_mapping_provider');
11
+
15
12
  class AssetCrowdFlow {
16
13
  constructor() {
17
- this.asset_values = {}; // Stores { day1_value_sum: 0, day2_value_sum: 0 }
18
- this.user_count = 0;
19
- this.priceMap = null;
14
+ this.assetData = new Map();
20
15
  this.mappings = null;
21
- this.dates = {}; // To store { today: '...', yesterday: '...' }
22
16
  }
23
17
 
24
18
  /**
25
- * Helper to safely initialize an asset entry.
19
+ * Defines the output schema for this calculation.
20
+ * @returns {object} JSON Schema object
26
21
  */
27
- _initAsset(instrumentId) {
28
- if (!this.asset_values[instrumentId]) {
29
- this.asset_values[instrumentId] = {
30
- day1_value_sum: 0,
31
- day2_value_sum: 0
32
- };
33
- }
34
- }
35
-
36
- /**
37
- * Helper to sum the 'Value' field from an AggregatedPositions array.
38
- */
39
- _sumAssetValue(positions) {
40
- const valueMap = {};
41
- if (!positions || !Array.isArray(positions)) {
42
- return valueMap;
43
- }
44
- for (const pos of positions) {
45
- if (pos && pos.InstrumentID && pos.Value) {
46
- valueMap[pos.InstrumentID] = (valueMap[pos.InstrumentID] || 0) + pos.Value;
22
+ static getSchema() {
23
+ return {
24
+ "type": "object",
25
+ "description": "Calculates the net capital flow % per asset, adjusted for price movements.",
26
+ "patternProperties": {
27
+ // Ticker
28
+ "^.*$": {
29
+ "type": "object",
30
+ "description": "Net crowd flow metrics for a specific asset ticker.",
31
+ "properties": {
32
+ "net_flow_percentage": {
33
+ "type": "number",
34
+ "description": "Net capital flow (buying/selling) as a percentage of total holdings, adjusted for price changes."
35
+ },
36
+ "total_invested_today": {
37
+ "type": "number",
38
+ "description": "Total USD value invested in this asset today by the sample."
39
+ },
40
+ "total_invested_yesterday": {
41
+ "type": "number",
42
+ "description": "Total USD value invested in this asset yesterday by the sample."
43
+ },
44
+ "price_change_contribution": {
45
+ "type": "number",
46
+ "description": "The portion of the value change attributable to the asset's price movement."
47
+ },
48
+ "net_flow_contribution": {
49
+ "type": "number",
50
+ "description": "The portion of the value change attributable to net buying or selling."
51
+ }
52
+ },
53
+ "required": ["net_flow_percentage", "total_invested_today", "total_invested_yesterday"]
54
+ }
55
+ },
56
+ "additionalProperties": {
57
+ "type": "object",
58
+ "properties": {
59
+ "net_flow_percentage": { "type": "number" },
60
+ "total_invested_today": { "type": "number" },
61
+ "total_invested_yesterday": { "type": "number" },
62
+ "price_change_contribution": { "type": "number" },
63
+ "net_flow_contribution": { "type": "number" }
64
+ }
47
65
  }
48
- }
49
- return valueMap;
66
+ };
50
67
  }
51
68
 
52
- process(todayPortfolio, yesterdayPortfolio, userId, context) {
53
- // This is a historical calculation, requires both days
54
- if (!todayPortfolio || !yesterdayPortfolio || !todayPortfolio.AggregatedPositions || !yesterdayPortfolio.AggregatedPositions) {
55
- return;
56
- }
57
-
58
- // Capture dates from context on the first run
59
- if (!this.dates.today && context.todayDateStr && context.yesterdayDateStr) {
60
- this.dates.today = context.todayDateStr;
61
- this.dates.yesterday = context.yesterdayDateStr;
62
- }
63
-
64
- const yesterdayValues = this._sumAssetValue(yesterdayPortfolio.AggregatedPositions);
65
- const todayValues = this._sumAssetValue(todayPortfolio.AggregatedPositions);
66
-
67
- // Use a set of all unique instruments held across both days
68
- const allInstrumentIds = new Set([
69
- ...Object.keys(yesterdayValues),
70
- ...Object.keys(todayValues)
71
- ]);
69
+ _getPortfolioPositions(portfolio) {
70
+ return portfolio?.PublicPositions || portfolio?.AggregatedPositions;
71
+ }
72
72
 
73
- for (const instrumentId of allInstrumentIds) {
74
- this._initAsset(instrumentId);
75
- this.asset_values[instrumentId].day1_value_sum += (yesterdayValues[instrumentId] || 0);
76
- this.asset_values[instrumentId].day2_value_sum += (todayValues[instrumentId] || 0);
73
+ _initAsset(instrumentId) {
74
+ if (!this.assetData.has(instrumentId)) {
75
+ this.assetData.set(instrumentId, {
76
+ total_invested_yesterday: 0,
77
+ total_invested_today: 0,
78
+ price_change_yesterday: 0, // Used to calculate weighted avg price change
79
+ });
77
80
  }
78
-
79
- this.user_count++;
80
81
  }
81
82
 
82
- async getResult() {
83
- if (this.user_count === 0 || !this.dates.today) {
84
- console.warn('[AssetCrowdFlow] No users processed or dates missing.');
85
- return null; // <--- Returns null if no users
83
+ process(todayPortfolio, yesterdayPortfolio) {
84
+ if (!todayPortfolio || !yesterdayPortfolio) {
85
+ return;
86
86
  }
87
87
 
88
- // Load priceMap and mappings if not loaded
89
- if (!this.priceMap || !this.mappings) {
90
- try {
91
- const [priceData, mappingData] = await Promise.all([
92
- loadAllPriceData(),
93
- loadInstrumentMappings()
94
- ]);
95
- this.priceMap = priceData;
96
- this.mappings = mappingData;
97
-
98
- if (!this.priceMap || Object.keys(this.priceMap).length === 0) {
99
- console.error('[AssetCrowdFlow] CRITICAL: Price map is empty or failed to load. Aborting calculation to allow backfill.');
100
- return null; // Return null to trigger backfill
101
- }
88
+ const yPos = this._getPortfolioPositions(yesterdayPortfolio);
89
+ const tPos = this._getPortfolioPositions(todayPortfolio);
102
90
 
103
- } catch (err) {
104
- console.error('[AssetCrowdFlow] Failed to load dependencies:', err);
105
- return null; // <--- Return null on error
106
- }
107
- }
91
+ const yPosMap = new Map(yPos?.map(p => [p.InstrumentID, p]) || []);
92
+ const tPosMap = new Map(tPos?.map(p => [p.InstrumentID, p]) || []);
108
93
 
109
- const finalResults = {};
110
- const todayStr = this.dates.today;
111
- const yesterdayStr = this.dates.yesterday;
94
+ const allInstrumentIds = new Set([...yPosMap.keys(), ...tPosMap.keys()]);
112
95
 
113
- for (const rawInstrumentId in this.asset_values) {
114
- const instrumentId = String(rawInstrumentId); // normalize
115
- const ticker = this.mappings.instrumentToTicker?.[instrumentId] || `id_${instrumentId}`;
96
+ for (const instrumentId of allInstrumentIds) {
97
+ if (!instrumentId) continue;
98
+
99
+ this._initAsset(instrumentId);
100
+ const asset = this.assetData.get(instrumentId);
116
101
 
117
- const avg_day1_value = this.asset_values[rawInstrumentId].day1_value_sum / this.user_count;
118
- const avg_day2_value = this.asset_values[rawInstrumentId].day2_value_sum / this.user_count;
102
+ const yP = yPosMap.get(instrumentId);
103
+ const tP = tPosMap.get(instrumentId);
119
104
 
120
- // --- THIS IS THE FIX YOU APPLIED ---
121
- const priceChangePct = getDailyPriceChange(instrumentId, yesterdayStr, todayStr, this.priceMap);
122
- // --- END FIX ---
105
+ const yInvested = yP?.InvestedAmount || yP?.Amount || 0;
106
+ const tInvested = tP?.InvestedAmount || tP?.Amount || 0;
123
107
 
124
- if (priceChangePct === null) {
125
- // If price is missing, we simply skip this ticker.
126
- // It will not be in the final output.
127
- continue;
108
+ if (yInvested > 0) {
109
+ asset.total_invested_yesterday += yInvested;
110
+ // Get price change from *yesterday's* portfolio
111
+ const yPriceChange = (yP?.PipsRate || 0) / (yP?.OpenRate || 1); // PipsRate holds 1-day change
112
+ asset.price_change_yesterday += yPriceChange * yInvested; // Weighted sum
113
+ }
114
+ if (tInvested > 0) {
115
+ asset.total_invested_today += tInvested;
128
116
  }
129
-
130
- // --- THIS IS THE LOGIC THAT IS BROKEN IN YOUR FILE ---
131
- // Calculate expected day2 value from price movement
132
- const expected_day2_value = avg_day1_value * (1 + priceChangePct);
133
-
134
- // Net crowd flow = actual minus expected
135
- const net_crowd_flow_pct = avg_day2_value - expected_day2_value;
136
-
137
- finalResults[ticker] = {
138
- net_crowd_flow_pct,
139
- avg_value_day1_pct: avg_day1_value, // <-- Note: I've also fixed the key name here
140
- avg_value_day2_pct: avg_day2_value, // <-- Note: I've also fixed the key name here
141
- expected_day2_value,
142
- price_change_pct: priceChangePct,
143
- user_sample_size: this.user_count
144
- };
145
- // --- END BROKEN LOGIC ---
146
117
  }
118
+ }
147
119
 
148
- if (Object.keys(finalResults).length === 0) {
149
- console.warn(`[AssetCrowdFlow] No results generated for ${this.dates.today}. This likely means all price data was missing. Returning null for backfill.`);
150
- return null; // <--- Returns null if all tickers failed
120
+ async getResult() {
121
+ if (!this.mappings) {
122
+ this.mappings = await loadInstrumentMappings();
151
123
  }
152
124
 
153
- return finalResults;
125
+ const result = {};
126
+
127
+ for (const [instrumentId, data] of this.assetData.entries()) {
128
+ const ticker = this.mappings.instrumentToTicker[instrumentId] || `id_${instrumentId}`;
129
+
130
+ const { total_invested_yesterday, total_invested_today, price_change_yesterday } = data;
131
+
132
+ if (total_invested_yesterday > 0) {
133
+ // Calculate the weighted average price change %
134
+ const avg_price_change_pct = price_change_yesterday / total_invested_yesterday;
135
+
136
+ // Estimate the value change *due to price*
137
+ const price_contribution = total_invested_yesterday * avg_price_change_pct;
138
+
139
+ // Estimate the value change *due to net flow* (buy/sell)
140
+ const flow_contribution = total_invested_today - (total_invested_yesterday + price_contribution);
141
+
142
+ // Calculate Net Flow as a percentage of yesterday's holdings
143
+ const net_flow_percentage = (flow_contribution / total_invested_yesterday) * 100;
144
+
145
+ result[ticker] = {
146
+ net_flow_percentage: net_flow_percentage,
147
+ total_invested_today: total_invested_today,
148
+ total_invested_yesterday: total_invested_yesterday,
149
+ price_change_contribution: price_contribution,
150
+ net_flow_contribution: flow_contribution
151
+ };
152
+ }
153
+ }
154
+ return result;
154
155
  }
155
156
 
156
-
157
157
  reset() {
158
- this.asset_values = {};
159
- this.user_count = 0;
160
- this.priceMap = null;
158
+ this.assetData.clear();
161
159
  this.mappings = null;
162
- this.dates = {};
163
160
  }
164
161
  }
165
162