aiden-shared-calculations-unified 1.0.34 → 1.0.36

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 -170
  9. package/calculations/behavioural/historical/drawdown_response.js +58 -58
  10. package/calculations/behavioural/historical/dumb-cohort-flow.js +249 -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 +250 -250
  17. package/calculations/behavioural/historical/smart_money_flow.js +165 -165
  18. package/calculations/behavioural/historical/user-investment-profile.js +412 -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
@@ -1,171 +1,166 @@
1
- const { loadAllPriceData, getDailyPriceChange } = require('../../../utils/price_data_provider');
2
- const { loadInstrumentMappings } = require('../../../utils/sector_mapping_provider');
3
-
4
- /**
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.
9
- *
10
- * Net Crowd Flow = (Actual % Change) - (Expected % Change from Price Move)
11
- *
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.
14
- */
15
- class AssetCrowdFlow {
16
- constructor() {
17
- this.asset_values = {}; // Stores { day1_value_sum: 0, day2_value_sum: 0 }
18
- this.user_count = 0;
19
- this.priceMap = null;
20
- this.mappings = null;
21
- this.dates = {}; // To store { today: '...', yesterday: '...' }
22
- }
23
-
24
- /**
25
- * Helper to safely initialize an asset entry.
26
- */
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;
47
- }
48
- }
49
- return valueMap;
50
- }
51
-
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
- ]);
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);
77
- }
78
-
79
- this.user_count++;
80
- }
81
-
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; // <--- MODIFICATION
86
- }
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
- // --- START NEW CHECK ---
99
- // If priceData failed to load (e.g., returned {} from provider)
100
- // this.priceMap will be empty. We must abort.
101
- if (!this.priceMap || Object.keys(this.priceMap).length === 0) {
102
- console.error('[AssetCrowdFlow] CRITICAL: Price map is empty or failed to load. Aborting calculation to allow backfill.');
103
- return null; // Return null to trigger backfill
104
- }
105
- // --- END NEW CHECK ---
106
-
107
- } catch (err) {
108
- console.error('[AssetCrowdFlow] Failed to load dependencies:', err);
109
- return null; // <--- MODIFICATION: Return null on error
110
- }
111
- }
112
-
113
- const finalResults = {};
114
- const todayStr = this.dates.today;
115
- const yesterdayStr = this.dates.yesterday;
116
-
117
- for (const rawInstrumentId in this.asset_values) {
118
- const instrumentId = String(rawInstrumentId); // normalize
119
- const ticker = this.mappings.instrumentToTicker?.[instrumentId] || `id_${instrumentId}`;
120
-
121
- const avg_day1_value = this.asset_values[rawInstrumentId].day1_value_sum / this.user_count;
122
- const avg_day2_value = this.asset_values[rawInstrumentId].day2_value_sum / this.user_count;
123
-
124
- // FIX to detect weekends and skip them
125
- const priceChangePct = getDailyPriceChange(instrumentId, yesterdayStr, todayStr, this.priceMap);
126
-
127
- if (priceChangePct === null) {
128
- // <--- MODIFICATION: We no longer add a warning object. We just skip it.
129
- // If it's missing price data, it won't be in finalResults.
130
- continue;
131
- }
132
-
133
- // Calculate expected day2 value from price movement
134
- const expected_day2_value = avg_day1_value * (1 + priceChangePct);
135
-
136
- // Net crowd flow = actual minus expected
137
- const net_crowd_flow_pct = avg_day2_value - expected_day2_value;
138
-
139
- finalResults[ticker] = {
140
- net_crowd_flow_pct,
141
- avg_value_day1,
142
- avg_value_day2,
143
- expected_day2_value,
144
- price_change_pct: priceChangePct,
145
- user_sample_size: this.user_count
146
- };
147
- }
148
-
149
- // --- START NEW CHECK ---
150
- // If all tickers were skipped due to missing price data,
151
- // finalResults will be empty. Return null to trigger backfill.
152
- if (Object.keys(finalResults).length === 0) {
153
- console.warn(`[AssetCrowdFlow] No results generated for ${this.dates.today}. This likely means all price data was missing. Returning null for backfill.`);
154
- return null;
155
- }
156
- // --- END NEW CHECK ---
157
-
158
- return finalResults;
159
- }
160
-
161
-
162
- reset() {
163
- this.asset_values = {};
164
- this.user_count = 0;
165
- this.priceMap = null;
166
- this.mappings = null;
167
- this.dates = {};
168
- }
169
- }
170
-
1
+ const { loadAllPriceData, getDailyPriceChange } = require('../../../utils/price_data_provider');
2
+ const { loadInstrumentMappings } = require('../../../utils/sector_mapping_provider');
3
+
4
+ /**
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.
9
+ *
10
+ * Net Crowd Flow = (Actual % Change) - (Expected % Change from Price Move)
11
+ *
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.
14
+ */
15
+ class AssetCrowdFlow {
16
+ constructor() {
17
+ this.asset_values = {}; // Stores { day1_value_sum: 0, day2_value_sum: 0 }
18
+ this.user_count = 0;
19
+ this.priceMap = null;
20
+ this.mappings = null;
21
+ this.dates = {}; // To store { today: '...', yesterday: '...' }
22
+ }
23
+
24
+ /**
25
+ * Helper to safely initialize an asset entry.
26
+ */
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;
47
+ }
48
+ }
49
+ return valueMap;
50
+ }
51
+
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
+ ]);
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);
77
+ }
78
+
79
+ this.user_count++;
80
+ }
81
+
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
86
+ }
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
+ }
102
+
103
+ } catch (err) {
104
+ console.error('[AssetCrowdFlow] Failed to load dependencies:', err);
105
+ return null; // <--- Return null on error
106
+ }
107
+ }
108
+
109
+ const finalResults = {};
110
+ const todayStr = this.dates.today;
111
+ const yesterdayStr = this.dates.yesterday;
112
+
113
+ for (const rawInstrumentId in this.asset_values) {
114
+ const instrumentId = String(rawInstrumentId); // normalize
115
+ const ticker = this.mappings.instrumentToTicker?.[instrumentId] || `id_${instrumentId}`;
116
+
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;
119
+
120
+ // --- THIS IS THE FIX YOU APPLIED ---
121
+ const priceChangePct = getDailyPriceChange(instrumentId, yesterdayStr, todayStr, this.priceMap);
122
+ // --- END FIX ---
123
+
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;
128
+ }
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
+ }
147
+
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
151
+ }
152
+
153
+ return finalResults;
154
+ }
155
+
156
+
157
+ reset() {
158
+ this.asset_values = {};
159
+ this.user_count = 0;
160
+ this.priceMap = null;
161
+ this.mappings = null;
162
+ this.dates = {};
163
+ }
164
+ }
165
+
171
166
  module.exports = AssetCrowdFlow;
@@ -1,59 +1,59 @@
1
- /**
2
- * Analyzes user behavior after a position experiences a >10% drawdown.
3
- */
4
- class DrawdownResponse {
5
- constructor() {
6
- this.drawdown_events = {
7
- held_position: 0,
8
- closed_position: 0,
9
- added_to_position: 0
10
- };
11
- }
12
-
13
- process(todayPortfolio, yesterdayPortfolio, userId) {
14
- if (!yesterdayPortfolio || !todayPortfolio) {
15
- return; // Need both days for comparison
16
- }
17
-
18
- const yPositions = yesterdayPortfolio.AggregatedPositions || yesterdayPortfolio.PublicPositions;
19
- const tPositions = todayPortfolio.AggregatedPositions || todayPortfolio.PublicPositions;
20
-
21
- if (!yPositions || !Array.isArray(yPositions) || !tPositions || !Array.isArray(tPositions)) {
22
- return;
23
- }
24
-
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., -83.6) shows the threshold should be -10.0.
31
- const drawdownPercent = yPos.NetProfit || 0;
32
- const yPosId = yPos.PositionID || yPos.InstrumentID;
33
-
34
- // Check if this position was in a >10% drawdown yesterday
35
- if (drawdownPercent < -10.0) {
36
- const todayPos = todayPositions.get(yPosId);
37
-
38
- if (!todayPos) {
39
- // Position was closed
40
- this.drawdown_events.closed_position++;
41
- } else if (todayPos.Invested > yPos.Invested) {
42
- // FIX: Use 'Invested' (percentage) to check for increase
43
- // User added money to the losing position
44
- this.drawdown_events.added_to_position++;
45
- } else {
46
- // Position was held (or reduced, but not added to)
47
- this.drawdown_events.held_position++;
48
- }
49
- }
50
- }
51
- }
52
-
53
- getResult() {
54
- // Return final calculated values
55
- return this.drawdown_events;
56
- }
57
- }
58
-
1
+ /**
2
+ * Analyzes user behavior after a position experiences a >10% drawdown.
3
+ */
4
+ class DrawdownResponse {
5
+ constructor() {
6
+ this.drawdown_events = {
7
+ held_position: 0,
8
+ closed_position: 0,
9
+ added_to_position: 0
10
+ };
11
+ }
12
+
13
+ process(todayPortfolio, yesterdayPortfolio, userId) {
14
+ if (!yesterdayPortfolio || !todayPortfolio) {
15
+ return; // Need both days for comparison
16
+ }
17
+
18
+ const yPositions = yesterdayPortfolio.AggregatedPositions || yesterdayPortfolio.PublicPositions;
19
+ const tPositions = todayPortfolio.AggregatedPositions || todayPortfolio.PublicPositions;
20
+
21
+ if (!yPositions || !Array.isArray(yPositions) || !tPositions || !Array.isArray(tPositions)) {
22
+ return;
23
+ }
24
+
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., -83.6) shows the threshold should be -10.0.
31
+ const drawdownPercent = yPos.NetProfit || 0;
32
+ const yPosId = yPos.PositionID || yPos.InstrumentID;
33
+
34
+ // Check if this position was in a >10% drawdown yesterday
35
+ if (drawdownPercent < -10.0) {
36
+ const todayPos = todayPositions.get(yPosId);
37
+
38
+ if (!todayPos) {
39
+ // Position was closed
40
+ this.drawdown_events.closed_position++;
41
+ } else if (todayPos.Invested > yPos.Invested) {
42
+ // FIX: Use 'Invested' (percentage) to check for increase
43
+ // User added money to the losing position
44
+ this.drawdown_events.added_to_position++;
45
+ } else {
46
+ // Position was held (or reduced, but not added to)
47
+ this.drawdown_events.held_position++;
48
+ }
49
+ }
50
+ }
51
+ }
52
+
53
+ getResult() {
54
+ // Return final calculated values
55
+ return this.drawdown_events;
56
+ }
57
+ }
58
+
59
59
  module.exports = DrawdownResponse;