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,188 +1,197 @@
1
1
  /**
2
- * @fileoverview Tracks the investment flow of "smart money".
2
+ * @fileoverview Calculation (Pass 3) for smart money flow.
3
3
  *
4
- * --- META REFACTOR (v2) ---
5
- * This is a streaming meta-calc. It reads historical shards (a side-effect
6
- * from Pass 1) and streams root portfolio data to calculate its result.
7
- * It has no direct computational dependencies passed to `process`.
4
+ * This metric calculates the "Net Crowd Flow Percentage" for
5
+ * "Smart Money" users, defined as users who were profitable
6
+ * in 5 of the last 7 days.
7
+ *
8
+ * This calculation *depends* on 'user_profitability_tracker'
9
+ * to identify the cohort.
8
10
  */
11
+ const { loadInstrumentMappings } = require('../../../utils/sector_mapping_provider');
9
12
 
10
- const { Firestore } = require('@google-cloud/firestore');
11
- const firestore = new Firestore();
12
- const { getInstrumentSectorMap } = require('../../../utils/sector_mapping_provider');
13
- // NOTE: Corrected relative path for data_loader
14
- const { loadDataByRefs, getPortfolioPartRefs, loadFullDayMap } = require('../../../../bulltrackers-module/functions/computation-system/utils/data_loader');
13
+ class SmartMoneyFlow {
14
+ constructor() {
15
+ // This calculation only aggregates by sector
16
+ this.sectorData = new Map();
17
+ this.mappings = null;
18
+ this.smartMoneyUserIds = null;
19
+ }
15
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) for the 'Smart Money' cohort (profitable 5 of last 7 days), aggregated by sector.",
29
+ "properties": {
30
+ "cohort_size": {
31
+ "type": "number",
32
+ "description": "The number of users identified as being in the Smart Money Cohort."
33
+ },
34
+ "sectors": {
35
+ "type": "object",
36
+ "description": "Price-adjusted net flow per sector.",
37
+ "patternProperties": {
38
+ // Sector
39
+ "^.*$": {
40
+ "type": "object",
41
+ "properties": {
42
+ "net_flow_percentage": { "type": "number" },
43
+ "total_invested_today": { "type": "number" },
44
+ "total_invested_yesterday": { "type": "number" }
45
+ },
46
+ "required": ["net_flow_percentage", "total_invested_today", "total_invested_yesterday"]
47
+ }
48
+ },
49
+ "additionalProperties": {
50
+ "type": "object",
51
+ "properties": {
52
+ "net_flow_percentage": { "type": "number" },
53
+ "total_invested_today": { "type": "number" },
54
+ "total_invested_yesterday": { "type": "number" }
55
+ }
56
+ }
57
+ }
58
+ },
59
+ "required": ["cohort_size", "sectors"]
60
+ };
61
+ }
16
62
 
17
- class SmartMoneyFlow {
18
-
19
63
  /**
20
- * (NEW) Statically declare dependencies.
21
- * This calc reads shards from Pass 1, but doesn't take a computed
22
- * result as an argument. The manifest ensures it runs in Pass 2.
64
+ * Statically declare dependencies.
23
65
  */
24
66
  static getDependencies() {
25
- return ['user-profitability-tracker']; // This is just for ordering, not for `fetchedDependencies`
67
+ return ['user_profitability_tracker'];
26
68
  }
27
-
28
- constructor() {
29
- this.smartMoneyUsers = new Set();
30
- this.sectorMap = null;
31
-
32
- // We now store the portfolio data during process()
33
- // (This state is reset for each date by the runner)
34
- this.todayPortfolios = {};
35
- this.yesterdayPortfolios = {};
69
+
70
+ _getPortfolioPositions(portfolio) {
71
+ return portfolio?.PublicPositions || portfolio?.AggregatedPositions;
72
+ }
73
+
74
+ _initSector(sector) {
75
+ if (!this.sectorData.has(sector)) {
76
+ this.sectorData.set(sector, {
77
+ total_invested_yesterday: 0,
78
+ total_invested_today: 0,
79
+ price_change_yesterday: 0,
80
+ });
81
+ }
36
82
  }
37
83
 
38
84
  /**
39
- * REFACTORED PROCESS METHOD
40
- * This is now the *first* method called by the runner.
41
- * It caches the portfolios.
85
+ * Helper to get the cohort IDs from the dependency.
42
86
  */
43
- async process(dateStr, dependencies, config, fetchedDependencies) {
44
- const { logger, db, rootData } = dependencies;
45
- const { portfolioRefs } = rootData;
46
- logger.log('INFO', '[SmartMoneyFlow] Starting meta-process...');
47
-
48
- // 1. Load external dependencies (sectors)
49
- if (!this.sectorMap) {
50
- try {
51
- this.sectorMap = await getInstrumentSectorMap();
52
- } catch (error) {
53
- logger.log('ERROR', '[SmartMoneyFlow] Failed to load sector map.', { err: error.message });
54
- return null; // Abort
55
- }
87
+ _getSmartMoneyCohort(fetchedDependencies) {
88
+ if (this.smartMoneyUserIds) {
89
+ return this.smartMoneyUserIds;
56
90
  }
57
91
 
58
- // 2. Load "yesterday's" portfolio data for comparison
59
- const yesterdayDate = new Date(dateStr + 'T00:00:00Z');
60
- yesterdayDate.setUTCDate(yesterdayDate.getUTCDate() - 1);
61
- const yesterdayStr = yesterdayDate.toISOString().slice(0, 10);
62
- const yesterdayRefs = await getPortfolioPartRefs(config, dependencies, yesterdayStr);
63
- const yesterdayPortfolios = await loadFullDayMap(config, dependencies, yesterdayRefs);
64
- logger.log('INFO', `[SmartMoneyFlow] Loaded ${yesterdayRefs.length} part refs for yesterday.`);
65
-
66
- // 3. Identify smart money users by reading Pass 1 shards
67
- // This is the dependency.
68
- await this.identifySmartMoney(dependencies);
69
- if (this.smartMoneyUsers.size === 0) {
70
- logger.log('WARN', '[SmartMoneyFlow] No smart money users identified. Aborting.');
71
- return { smart_money_flow: {} };
92
+ const profitabilityData = fetchedDependencies['user_profitability_tracker'];
93
+ if (!profitabilityData || !profitabilityData.user_details) {
94
+ return new Set();
72
95
  }
73
96
 
74
- // 4. Stream "today's" portfolio data and process *only smart users*
75
- const batchSize = config.partRefBatchSize || 10;
76
- const sectorFlow = {};
97
+ this.smartMoneyUserIds = new Set();
98
+ for (const [userId, data] of Object.entries(profitabilityData.user_details)) {
99
+ // Definition: Profitable in 5 of the last 7 days.
100
+ if (data.profitable_days_7d >= 5) {
101
+ this.smartMoneyUserIds.add(userId);
102
+ }
103
+ }
104
+ return this.smartMoneyUserIds;
105
+ }
77
106
 
78
- for (let i = 0; i < portfolioRefs.length; i += batchSize) {
79
- const batchRefs = portfolioRefs.slice(i, i + batchSize);
80
- const todayPortfoliosChunk = await loadDataByRefs(config, dependencies, batchRefs);
107
+ process(todayPortfolio, yesterdayPortfolio, userId, context, todayInsights, yesterdayInsights, fetchedDependencies) {
108
+ const smartMoneyCohort = this._getSmartMoneyCohort(fetchedDependencies);
109
+
110
+ // This user is not in the "smart money cohort", skip.
111
+ if (!smartMoneyCohort.has(userId)) {
112
+ return;
113
+ }
81
114
 
82
- for (const uid in todayPortfoliosChunk) {
83
- // --- Filter user ---
84
- if (!this.smartMoneyUsers.has(uid)) {
85
- continue;
86
- }
87
-
88
- const pToday = todayPortfoliosChunk[uid];
89
- const pYesterday = yesterdayPortfolios[uid];
90
-
91
- if (!pToday || !pYesterday) continue;
115
+ if (!todayPortfolio || !yesterdayPortfolio) {
116
+ return;
117
+ }
92
118
 
93
- // --- User is in cohort, run logic ---
94
- const todaySectorInvestment = this.calculateSectorInvestment(pToday);
95
- const yesterdaySectorInvestment = this.calculateSectorInvestment(pYesterday);
119
+ const yPos = this._getPortfolioPositions(yesterdayPortfolio);
120
+ const tPos = this._getPortfolioPositions(todayPortfolio);
96
121
 
97
- const allSectors = new Set([...Object.keys(todaySectorInvestment), ...Object.keys(yesterdaySectorInvestment)]);
98
- for (const sector of allSectors) {
99
- const todayAmount = todaySectorInvestment[sector] || 0;
100
- const yesterdayAmount = yesterdaySectorInvestment[sector] || 0;
101
- const change = todayAmount - yesterdayAmount;
122
+ const yPosMap = new Map(yPos?.map(p => [p.InstrumentID, p]) || []);
123
+ const tPosMap = new Map(tPos?.map(p => [p.InstrumentID, p]) || []);
102
124
 
103
- if (change !== 0) {
104
- if (!sectorFlow[sector]) {
105
- sectorFlow[sector] = 0;
106
- }
107
- sectorFlow[sector] += change;
108
- }
109
- }
110
- }
111
- }
125
+ const allInstrumentIds = new Set([...yPosMap.keys(), ...tPosMap.keys()]);
112
126
 
113
- logger.log('INFO', `[SmartMoneyFlow] Processed ${this.smartMoneyUsers.size} smart users.`);
114
-
115
- // 5. Return final result
116
- return { smart_money_flow: sectorFlow };
117
- }
127
+ if (!this.mappings) {
128
+ // Context contains the mappings loaded in Pass 1
129
+ this.mappings = context.mappings;
130
+ }
118
131
 
119
- async identifySmartMoney(dependencies) {
120
- const { logger } = dependencies;
121
- logger.log('INFO', "[SmartMoneyFlow] Attempting to identify smart money users by reading shards...");
122
- try {
123
- const shardPromises = [];
124
- const NUM_SHARDS = 50;
125
- for (let i = 0; i < NUM_SHARDS; i++) {
126
- const docRef = firestore.collection('historical_insights').doc(`user_profitability_shard_${i}`);
127
- shardPromises.push(docRef.get());
132
+ for (const instrumentId of allInstrumentIds) {
133
+ if (!instrumentId) continue;
134
+
135
+ const yP = yPosMap.get(instrumentId);
136
+ const tP = tPosMap.get(instrumentId);
137
+
138
+ const yInvested = yP?.InvestedAmount || yP?.Amount || 0;
139
+ const tInvested = tP?.InvestedAmount || tP?.Amount || 0;
140
+
141
+ // Get sector and initialize it
142
+ const sector = this.mappings.instrumentToSector[instrumentId] || 'Other';
143
+ this._initSector(sector);
144
+ const sectorAsset = this.sectorData.get(sector);
145
+
146
+ if (yInvested > 0) {
147
+ const yPriceChange = (yP?.PipsRate || 0) / (yP?.OpenRate || 1);
148
+ sectorAsset.total_invested_yesterday += yInvested;
149
+ sectorAsset.price_change_yesterday += yPriceChange * yInvested;
150
+ }
151
+ if (tInvested > 0) {
152
+ sectorAsset.total_invested_today += tInvested;
128
153
  }
129
- const shardSnapshots = await Promise.all(shardPromises);
130
-
131
- let profitableUsersFound = 0;
132
- shardSnapshots.forEach((snap, index) => {
133
- if (snap.exists) {
134
- const shardData = snap.data() || {};
135
- for (const userId in shardData) {
136
- const history = shardData[userId];
137
- if (Array.isArray(history) && history.length >= 5) {
138
- const profitableDays = history.slice(-7).filter(d => d && typeof d.pnl === 'number' && d.pnl > 0).length;
139
- if (profitableDays >= 5) {
140
- this.smartMoneyUsers.add(userId);
141
- profitableUsersFound++;
142
- }
143
- }
144
- }
145
- } else {
146
- logger.log('WARN', `[SmartMoneyFlow] Profitability shard user_profitability_shard_${index} does not exist.`);
147
- }
148
- });
149
- logger.log('INFO', `[SmartMoneyFlow] Found ${profitableUsersFound} smart money users across all shards.`);
150
-
151
- } catch (error) {
152
- logger.log('ERROR', '[SmartMoneyFlow] Error identifying smart money users', { err: error.message });
153
- this.smartMoneyUsers.clear();
154
154
  }
155
155
  }
156
156
 
157
-
158
- calculateSectorInvestment(portfolio) {
159
- const sectorInvestment = {};
160
- if (!this.sectorMap) {
161
- console.warn("[SmartMoneyFlow] Cannot calculate sector investment: Sector map not loaded.");
162
- return sectorInvestment;
163
- }
164
-
165
- if (portfolio && portfolio.AggregatedPositions && Array.isArray(portfolio.AggregatedPositions)) {
166
- for (const pos of portfolio.AggregatedPositions) {
167
- if (pos && typeof pos.InstrumentID !== 'undefined' && typeof pos.Invested === 'number') {
168
- const sector = this.sectorMap[pos.InstrumentID] || 'N/A';
169
- if (!sectorInvestment[sector]) {
170
- sectorInvestment[sector] = 0;
171
- }
172
- sectorInvestment[sector] += pos.Invested;
173
- }
157
+ async getResult(fetchedDependencies) {
158
+ // Ensure mappings are loaded (can be from context or loaded now)
159
+ if (!this.mappings) {
160
+ this.mappings = await loadInstrumentMappings();
161
+ }
162
+
163
+ // Ensure cohort is calculated at least once
164
+ const smartMoneyCohort = this._getSmartMoneyCohort(fetchedDependencies);
165
+
166
+ // Calculate Sector Flow
167
+ const sectorResult = {};
168
+ for (const [sector, data] of this.sectorData.entries()) {
169
+ const { total_invested_yesterday, total_invested_today, price_change_yesterday } = data;
170
+
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;
176
+
177
+ sectorResult[sector] = {
178
+ net_flow_percentage: net_flow_percentage,
179
+ total_invested_today: total_invested_today,
180
+ total_invested_yesterday: total_invested_yesterday
181
+ };
174
182
  }
175
183
  }
176
- return sectorInvestment;
177
- }
178
184
 
185
+ return {
186
+ cohort_size: smartMoneyCohort.size,
187
+ sectors: sectorResult
188
+ };
189
+ }
179
190
 
180
- async getResult() { return null; }
181
191
  reset() {
182
- this.smartMoneyUsers.clear();
183
- this.sectorMap = null;
184
- this.todayPortfolios = {};
185
- this.yesterdayPortfolios = {};
192
+ this.sectorData.clear();
193
+ this.mappings = null;
194
+ this.smartMoneyUserIds = null;
186
195
  }
187
196
  }
188
197
 
@@ -1,121 +1,127 @@
1
1
  /**
2
- * @fileoverview Estimates a proxy for net crowd cash flow (Deposits vs. Withdrawals)
3
- * by aggregating portfolio percentage changes across all users.
2
+ * @fileoverview Calculation (Pass 2) for crowd cash flow proxy.
4
3
  *
5
- * This calculation is based on the formula:
6
- * Total_Change = P/L_Effect + Trading_Effect + Cash_Flow_Effect
4
+ * This metric estimates the net cash flow (deposits vs. withdrawals)
5
+ * of the crowd by analyzing changes in 'Invested' vs. 'Cash' balances
6
+ * in user portfolios.
7
7
  *
8
- * Where:
9
- * - Total_Change = avg_value_day2 - avg_value_day1
10
- * - P/L_Effect = avg_value_day1 - avg_invested_day1
11
- * - Trading_Effect = avg_invested_day2 - avg_invested_day1
12
- *
13
- * We solve for Cash_Flow_Effect, which serves as our proxy.
14
- * A negative value indicates a net DEPOSIT (denominator grew, shrinking all %s).
15
- * A positive value indicates a net WITHDRAWAL (denominator shrank, inflating all %s).
16
- *
17
- * NOTE: This file is a logical duplicate of deposit_withdrawal_percentage.js
18
- * to satisfy the hardcoded 'crowd-cash-flow-proxy' dependency in several
19
- * meta-calculations.
8
+ * It's a proxy because it doesn't see real bank transactions, but
9
+ * infers from the total portfolio value vs. the invested amount.
20
10
  */
21
-
22
11
  class CrowdCashFlowProxy {
23
12
  constructor() {
24
- this.total_invested_day1 = 0;
25
- this.total_value_day1 = 0;
26
- this.total_invested_day2 = 0;
27
- this.total_value_day2 = 0;
28
- this.user_count = 0;
13
+ this.total_portfolio_value_yesterday = 0;
14
+ this.total_invested_yesterday = 0;
15
+
16
+ this.total_portfolio_value_today = 0;
17
+ this.total_invested_today = 0;
29
18
  }
30
19
 
31
20
  /**
32
- * Helper to sum a specific field from an AggregatedPositions array.
33
- * @param {Array<object>} positions - The AggregatedPositions array.
34
- * @param {string} field - The field to sum ('Invested' or 'Value').
35
- * @returns {number} The total sum of that field.
21
+ * Defines the output schema for this calculation.
22
+ * @returns {object} JSON Schema object
36
23
  */
37
- _sumPositions(positions, field) {
38
- if (!positions || !Array.isArray(positions)) {
39
- return 0;
24
+ static getSchema() {
25
+ return {
26
+ "type": "object",
27
+ "description": "Estimates net cash flow (deposits/withdrawals) by comparing changes in total portfolio value vs. invested capital.",
28
+ "properties": {
29
+ "total_portfolio_value_yesterday": {
30
+ "type": "number",
31
+ "description": "Sum of all users' total portfolio values from yesterday."
32
+ },
33
+ "total_invested_yesterday": {
34
+ "type": "number",
35
+ "description": "Sum of all users' invested capital from yesterday."
36
+ },
37
+ "total_portfolio_value_today": {
38
+ "type": "number",
39
+ "description": "Sum of all users' total portfolio values from today."
40
+ },
41
+ "total_invested_today": {
42
+ "type": "number",
43
+ "description": "Sum of all users' invested capital from today."
44
+ },
45
+ "total_pnl_contribution": {
46
+ "type": "number",
47
+ "description": "The estimated change in portfolio value attributable to P&L."
48
+ },
49
+ "net_cash_flow_proxy": {
50
+ "type": "number",
51
+ "description": "The estimated net cash flow (Deposits - Withdrawals). Positive indicates net deposits."
52
+ },
53
+ "net_cash_flow_proxy_pct": {
54
+ "type": "number",
55
+ "description": "The net cash flow proxy as a percentage of yesterday's total portfolio value."
56
+ }
57
+ },
58
+ "required": [
59
+ "total_portfolio_value_yesterday", "total_invested_yesterday",
60
+ "total_portfolio_value_today", "total_invested_today",
61
+ "total_pnl_contribution", "net_cash_flow_proxy", "net_cash_flow_proxy_pct"
62
+ ]
63
+ };
64
+ }
65
+
66
+ _getPortfolioValues(portfolio) {
67
+ if (!portfolio || !portfolio.Summary) {
68
+ return { portfolioValue: 0, invested: 0 };
40
69
  }
41
- return positions.reduce((sum, pos) => sum + (pos[field] || 0), 0);
70
+
71
+ // Total value (Invested + Cash)
72
+ const portfolioValue = portfolio.Summary.PortfolioValue || 0;
73
+ // Total invested in positions
74
+ const invested = portfolio.Summary.Invested || 0;
75
+
76
+ return { portfolioValue, invested };
42
77
  }
43
78
 
44
- process(todayPortfolio, yesterdayPortfolio, userId) {
45
- // This calculation requires both days' data to compare
46
- if (!todayPortfolio || !yesterdayPortfolio || !todayPortfolio.AggregatedPositions || !yesterdayPortfolio.AggregatedPositions) {
79
+ process(todayPortfolio, yesterdayPortfolio) {
80
+ if (!todayPortfolio || !yesterdayPortfolio) {
47
81
  return;
48
82
  }
49
83
 
50
- const invested_day1 = this._sumPositions(yesterdayPortfolio.AggregatedPositions, 'Invested');
51
- const value_day1 = this._sumPositions(yesterdayPortfolio.AggregatedPositions, 'Value');
52
- const invested_day2 = this._sumPositions(todayPortfolio.AggregatedPositions, 'Invested');
53
- const value_day2 = this._sumPositions(todayPortfolio.AggregatedPositions, 'Value');
84
+ const yValues = this._getPortfolioValues(yesterdayPortfolio);
85
+ const tValues = this._getPortfolioValues(todayPortfolio);
54
86
 
55
- // Only include users who have some form of positions
56
- if (invested_day1 === 0 && invested_day2 === 0 && value_day1 === 0 && value_day2 === 0) {
57
- return;
58
- }
87
+ this.total_portfolio_value_yesterday += yValues.portfolioValue;
88
+ this.total_invested_yesterday += yValues.invested;
59
89
 
60
- this.total_invested_day1 += invested_day1;
61
- this.total_value_day1 += value_day1;
62
- this.total_invested_day2 += invested_day2;
63
- this.total_value_day2 += value_day2;
64
- this.user_count++;
90
+ this.total_portfolio_value_today += tValues.portfolioValue;
91
+ this.total_invested_today += tValues.invested;
65
92
  }
66
93
 
67
94
  getResult() {
68
- if (this.user_count === 0) {
69
- return {}; // No users processed, return empty.
70
- }
71
-
72
- // 1. Calculate the average portfolio for the crowd
73
- const avg_invested_day1 = this.total_invested_day1 / this.user_count;
74
- const avg_value_day1 = this.total_value_day1 / this.user_count;
75
- const avg_invested_day2 = this.total_invested_day2 / this.user_count;
76
- const avg_value_day2 = this.total_value_day2 / this.user_count;
95
+ // P&L is the change in *invested* value, not total value
96
+ const totalPnl = this.total_invested_today - this.total_invested_yesterday;
77
97
 
78
- // 2. Isolate the three effects
79
- const total_value_change = avg_value_day2 - avg_value_day1;
80
- const pl_effect = avg_value_day1 - avg_invested_day1;
81
- const trading_effect = avg_invested_day2 - avg_invested_day1;
82
-
83
- // 3. Solve for the Cash Flow Effect
84
- // Total_Change = pl_effect + trading_effect + cash_flow_effect
85
- const cash_flow_effect = total_value_change - pl_effect - trading_effect;
98
+ // The change in total value is (P&L + Net Cash Flow)
99
+ // So, Net Cash Flow = (Total Value Change) - P&L
100
+ const totalValueChange = this.total_portfolio_value_today - this.total_portfolio_value_yesterday;
101
+
102
+ const netCashFlowProxy = totalValueChange - totalPnl;
103
+
104
+ const netCashFlowProxyPct = (this.total_portfolio_value_yesterday > 0)
105
+ ? (netCashFlowProxy / this.total_portfolio_value_yesterday) * 100
106
+ : 0;
86
107
 
87
108
  return {
88
- // The final proxy value.
89
- // A negative value signals a net DEPOSIT.
90
- // A positive value signals a net WITHDRAWAL.
91
- cash_flow_effect_proxy: cash_flow_effect,
92
-
93
- // Interpretation for the frontend
94
- interpretation: "A negative value signals a net crowd deposit. A positive value signals a net crowd withdrawal.",
109
+ total_portfolio_value_yesterday: this.total_portfolio_value_yesterday,
110
+ total_invested_yesterday: this.total_invested_yesterday,
111
+ total_portfolio_value_today: this.total_portfolio_value_today,
112
+ total_invested_today: this.total_invested_today,
95
113
 
96
- // Debug components
97
- components: {
98
- total_value_change: total_value_change,
99
- pl_effect: pl_effect,
100
- trading_effect: trading_effect
101
- },
102
- // Debug averages
103
- averages: {
104
- avg_invested_day1: avg_invested_day1,
105
- avg_value_day1: avg_value_day1,
106
- avg_invested_day2: avg_invested_day2,
107
- avg_value_day2: avg_value_day2
108
- },
109
- user_sample_size: this.user_count
114
+ total_pnl_contribution: totalPnl,
115
+ net_cash_flow_proxy: netCashFlowProxy,
116
+ net_cash_flow_proxy_pct: netCashFlowProxyPct
110
117
  };
111
118
  }
112
119
 
113
120
  reset() {
114
- this.total_invested_day1 = 0;
115
- this.total_value_day1 = 0;
116
- this.total_invested_day2 = 0;
117
- this.total_value_day2 = 0;
118
- this.user_count = 0;
121
+ this.total_portfolio_value_yesterday = 0;
122
+ this.total_invested_yesterday = 0;
123
+ this.total_portfolio_value_today = 0;
124
+ this.total_invested_today = 0;
119
125
  }
120
126
  }
121
127