aiden-shared-calculations-unified 1.0.21 → 1.0.23

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.
@@ -0,0 +1,144 @@
1
+ /**
2
+ * @fileoverview Tracks user activity by comparing portfolio snapshots.
3
+ * This is a historical calculation that defines an "active user" as someone
4
+ * who has opened, closed, or reallocated a position within the last 24 hours.
5
+ *
6
+ * This provides the "Daily Active Users" count for the monitored cohort.
7
+ * This depreciates the user activity sampler cloud function which was inefficient for the api usage and now provides this data for free
8
+ */
9
+
10
+ class DailyUserActivityTracker {
11
+ constructor() {
12
+ this.activeUserIds = new Set();
13
+ this.activityEvents = {
14
+ new_position: 0,
15
+ closed_position: 0,
16
+ reallocation: 0
17
+ };
18
+ }
19
+
20
+ /**
21
+ * Helper to get a simplified map of positions for comparison.
22
+ * @param {object} portfolio - A user's full portfolio object.
23
+ * @returns {object} { posMap: Map<InstrumentID, {invested: number}>, hasAggregated: boolean }
24
+ */
25
+ _getPortfolioMaps(portfolio) {
26
+ // Prioritize AggregatedPositions, but fall back to PublicPositions
27
+ const positions = portfolio?.AggregatedPositions || portfolio?.PublicPositions;
28
+ if (!positions || !Array.isArray(positions)) {
29
+ return { posMap: new Map(), hasAggregated: false };
30
+ }
31
+
32
+ const posMap = new Map();
33
+ for (const pos of positions) {
34
+ const key = pos.InstrumentID;
35
+ if (key) {
36
+ posMap.set(key, {
37
+ // 'InvestedAmount' or 'Invested' is the portfolio percentage
38
+ // We use this for reallocation logic.
39
+ invested: pos.InvestedAmount || pos.Invested || pos.Amount || 0
40
+ });
41
+ }
42
+ }
43
+ // Return the map and a flag indicating if we can trust the 'invested' field
44
+ return { posMap, hasAggregated: !!portfolio.AggregatedPositions };
45
+ }
46
+
47
+ /**
48
+ * Processes a single user's daily data.
49
+ */
50
+ process(todayPortfolio, yesterdayPortfolio, userId) {
51
+ // This calculation requires both days to find changes.
52
+ if (!todayPortfolio || !yesterdayPortfolio) {
53
+ return;
54
+ }
55
+
56
+ const { posMap: yPosMap, hasAggregated: yHasAgg } = this._getPortfolioMaps(yesterdayPortfolio);
57
+ const { posMap: tPosMap, hasAggregated: tHasAgg } = this._getPortfolioMaps(todayPortfolio);
58
+
59
+ // Skip if user has no positions on either day
60
+ if (tPosMap.size === 0 && yPosMap.size === 0) {
61
+ return;
62
+ }
63
+
64
+ const yIds = new Set(yPosMap.keys());
65
+ const tIds = new Set(tPosMap.keys());
66
+ let isActive = false;
67
+
68
+ // 1. Check for new positions (high-confidence activity)
69
+ for (const tId of tIds) {
70
+ if (!yIds.has(tId)) {
71
+ isActive = true;
72
+ this.activityEvents.new_position++;
73
+ break; // Found activity, no need to check more
74
+ }
75
+ }
76
+
77
+ if (isActive) {
78
+ this.activeUserIds.add(userId);
79
+ return;
80
+ }
81
+
82
+ // 2. Check for closed positions (high-confidence activity)
83
+ for (const yId of yIds) {
84
+ if (!tIds.has(yId)) {
85
+ isActive = true;
86
+ this.activityEvents.closed_position++;
87
+ break; // Found activity
88
+ }
89
+ }
90
+
91
+ if (isActive) {
92
+ this.activeUserIds.add(userId);
93
+ return;
94
+ }
95
+
96
+ // 3. Check for reallocation (only possible if we have AggregatedPositions for both days)
97
+ // This checks for changes in the 'Invested' percentage
98
+ if (yHasAgg && tHasAgg) {
99
+ for (const tId of tIds) {
100
+ // We know tId is also in yIds from the checks above
101
+ const tInvested = tPosMap.get(tId).invested;
102
+ const yInvested = yPosMap.get(yId).invested;
103
+
104
+ // Check for a meaningful change (e.g., > 0.01% to avoid float noise)
105
+ if (Math.abs(tInvested - yInvested) > 0.0001) {
106
+ isActive = true;
107
+ this.activityEvents.reallocation++;
108
+ break; // Found activity
109
+ }
110
+ }
111
+ }
112
+
113
+ if (isActive) {
114
+ this.activeUserIds.add(userId);
115
+ }
116
+ }
117
+
118
+ /**
119
+ * Returns the final aggregated counts for the day.
120
+ */
121
+ getResult() {
122
+ return {
123
+ // This is the main metric for your graph
124
+ rawActiveUserCount: this.activeUserIds.size,
125
+
126
+ // This is a bonus metric to see *what* users are doing
127
+ activityBreakdown: this.activityEvents
128
+ };
129
+ }
130
+
131
+ /**
132
+ * Resets the counters for the next run.
133
+ */
134
+ reset() {
135
+ this.activeUserIds.clear();
136
+ this.activityEvents = {
137
+ new_position: 0,
138
+ closed_position: 0,
139
+ reallocation: 0
140
+ };
141
+ }
142
+ }
143
+
144
+ module.exports = DailyUserActivityTracker;
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Calculates the average position size for each asset.
2
+ * Calculates the average position size (as a portfolio percentage) for each asset.
3
3
  */
4
4
  const { loadInstrumentMappings } = require('../../utils/sector_mapping_provider');
5
5
 
@@ -22,7 +22,8 @@ class AssetPositionSize {
22
22
  }
23
23
 
24
24
  this.assets[instrumentId].position_count++;
25
- this.assets[instrumentId].position_value_sum += (position.InvestedAmount || position.Amount || 0);
25
+ // FIX: Use the 'Invested' field, which holds the portfolio percentage
26
+ this.assets[instrumentId].position_value_sum += (position.Invested || 0);
26
27
  }
27
28
  }
28
29
 
@@ -36,11 +37,11 @@ class AssetPositionSize {
36
37
  const ticker = this.mappings.instrumentToTicker[instrumentId] || instrumentId.toString();
37
38
  const data = this.assets[instrumentId];
38
39
 
39
- // REFACTOR: Perform final calculation and return in standardized format.
40
40
  if (data.position_count > 0) {
41
41
  result[ticker] = {
42
+ // This is now the average *percentage* size
42
43
  average_position_size: data.position_value_sum / data.position_count,
43
- position_count: data.position_count // Also include count for context
44
+ position_count: data.position_count
44
45
  };
45
46
  }
46
47
  }
@@ -54,4 +55,4 @@ class AssetPositionSize {
54
55
  }
55
56
  }
56
57
 
57
- module.exports = AssetPositionSize;
58
+ module.exports = AssetPositionSize;
@@ -15,7 +15,6 @@ class DrawdownResponse {
15
15
  return; // Need both days for comparison
16
16
  }
17
17
 
18
- // FIX: Get the correct positions arrays and ensure they are iterable
19
18
  const yPositions = yesterdayPortfolio.AggregatedPositions || yesterdayPortfolio.PublicPositions;
20
19
  const tPositions = todayPortfolio.AggregatedPositions || todayPortfolio.PublicPositions;
21
20
 
@@ -23,20 +22,24 @@ class DrawdownResponse {
23
22
  return;
24
23
  }
25
24
 
26
- // Create a map of today's positions for efficient lookup
27
- const todayPositions = new Map(tPositions.map(p => [p.PositionID, p]));
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]));
28
27
 
29
28
  for (const yPos of yPositions) {
30
- const drawdownPercent = yPos.InvestedAmount > 0 ? yPos.ProfitAndLoss / yPos.InvestedAmount : 0;
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;
31
33
 
32
34
  // Check if this position was in a >10% drawdown yesterday
33
- if (drawdownPercent < -0.10) {
34
- const todayPos = todayPositions.get(yPos.PositionID);
35
+ if (drawdownPercent < -10.0) {
36
+ const todayPos = todayPositions.get(yPosId);
35
37
 
36
38
  if (!todayPos) {
37
39
  // Position was closed
38
40
  this.drawdown_events.closed_position++;
39
- } else if (todayPos.InvestedAmount > yPos.InvestedAmount) {
41
+ } else if (todayPos.Invested > yPos.Invested) {
42
+ // FIX: Use 'Invested' (percentage) to check for increase
40
43
  // User added money to the losing position
41
44
  this.drawdown_events.added_to_position++;
42
45
  } else {
@@ -15,7 +15,6 @@ class GainResponse {
15
15
  return; // Need both days for comparison
16
16
  }
17
17
 
18
- // FIX: Get the correct positions arrays and ensure they are iterable
19
18
  const yPositions = yesterdayPortfolio.AggregatedPositions || yesterdayPortfolio.PublicPositions;
20
19
  const tPositions = todayPortfolio.AggregatedPositions || todayPortfolio.PublicPositions;
21
20
 
@@ -23,19 +22,24 @@ class GainResponse {
23
22
  return;
24
23
  }
25
24
 
26
- const todayPositions = new Map(tPositions.map(p => [p.PositionID, p]));
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
27
 
28
28
  for (const yPos of yPositions) {
29
- const gainPercent = yPos.InvestedAmount > 0 ? yPos.ProfitAndLoss / yPos.InvestedAmount : 0;
29
+ // FIX: Use the NetProfit field, which is already a percentage.
30
+ // Your data sample (e.g., 23.5) shows the threshold should be 10.0.
31
+ const gainPercent = yPos.NetProfit || 0;
32
+ const yPosId = yPos.PositionID || yPos.InstrumentID;
30
33
 
31
34
  // Check if this position was in a >10% gain yesterday
32
- if (gainPercent > 0.10) {
33
- const todayPos = todayPositions.get(yPos.PositionID);
35
+ if (gainPercent > 10.0) {
36
+ const todayPos = todayPositions.get(yPosId);
34
37
 
35
38
  if (!todayPos) {
36
39
  // Position was closed (took full profit)
37
40
  this.gain_events.closed_position++;
38
- } else if (todayPos.InvestedAmount < yPos.InvestedAmount) {
41
+ } else if (todayPos.Invested < yPos.Invested) {
42
+ // FIX: Use 'Invested' (percentage) to check for reduction
39
43
  // User reduced the position (took partial profit)
40
44
  this.gain_events.reduced_position++;
41
45
  } else {
@@ -14,15 +14,28 @@ class PositionCountPnl {
14
14
  }
15
15
  }
16
16
 
17
+ /**
18
+ * FIX: Helper function to calculate total P&L from positions
19
+ * @param {object} portfolio
20
+ * @returns {number|null}
21
+ */
22
+ _calculateTotalPnl(portfolio) {
23
+ const positions = portfolio?.AggregatedPositions || portfolio?.PublicPositions;
24
+ if (positions && Array.isArray(positions)) {
25
+ // Sum all NetProfit fields, defaulting to 0 if a position has no NetProfit
26
+ return positions.reduce((sum, pos) => sum + (pos.NetProfit || 0), 0);
27
+ }
28
+ return null;
29
+ }
30
+
17
31
  process(todayPortfolio, yesterdayPortfolio, userId) {
18
- if (!todayPortfolio || !yesterdayPortfolio) {
19
- return; // Need P/L and today's data
32
+ // FIX: Only need todayPortfolio for this logic
33
+ if (!todayPortfolio) {
34
+ return;
20
35
  }
21
36
 
22
- // FIX: Get the correct positions array
23
37
  const positions = todayPortfolio.AggregatedPositions || todayPortfolio.PublicPositions;
24
38
 
25
- // FIX: Add check to ensure positions is an iterable array
26
39
  if (!positions || !Array.isArray(positions)) {
27
40
  return; // Skip users with no positions array
28
41
  }
@@ -32,7 +45,12 @@ class PositionCountPnl {
32
45
  return; // Skip users with no positions
33
46
  }
34
47
 
35
- const dailyPnl = todayPortfolio.PortfolioValue - yesterdayPortfolio.PortfolioValue;
48
+ // FIX: Calculate dailyPnl by summing NetProfit from all positions
49
+ const dailyPnl = this._calculateTotalPnl(todayPortfolio);
50
+
51
+ if (dailyPnl === null) {
52
+ return; // Cannot calculate P&L for this user
53
+ }
36
54
 
37
55
  this._initBucket(positionCount);
38
56
  this.pnl_by_position_count[positionCount].pnl_sum += dailyPnl;
@@ -5,11 +5,9 @@ const { getInstrumentSectorMap } = require('../../../utils/sector_mapping_provid
5
5
  * Aggregates P/L by the number of unique sectors a user is invested in.
6
6
  */
7
7
  class DiversificationPnl {
8
- // ... (rest of the code is unchanged) ...
9
8
  constructor() {
10
9
  this.pnl_by_sector_count = {};
11
- // Load the mapping on initialization
12
- this.sectorMapping = null; // Changed: Load async in process
10
+ this.sectorMapping = null;
13
11
  }
14
12
 
15
13
  _initBucket(count) {
@@ -18,11 +16,26 @@ class DiversificationPnl {
18
16
  }
19
17
  }
20
18
 
19
+ /**
20
+ * FIX: Helper function to calculate total P&L from positions
21
+ * @param {object} portfolio
22
+ * @returns {number|null}
23
+ */
24
+ _calculateTotalPnl(portfolio) {
25
+ const positions = portfolio?.AggregatedPositions || portfolio?.PublicPositions;
26
+ if (positions && Array.isArray(positions)) {
27
+ // Sum all NetProfit fields, defaulting to 0 if a position has no NetProfit
28
+ return positions.reduce((sum, pos) => sum + (pos.NetProfit || 0), 0);
29
+ }
30
+ return null;
31
+ }
32
+
21
33
  async process(todayPortfolio, yesterdayPortfolio, userId) { // Added async
22
- if (!yesterdayPortfolio || !todayPortfolio) {
34
+ // FIX: Only need todayPortfolio for this logic
35
+ if (!todayPortfolio) {
23
36
  return;
24
37
  }
25
- // Load mapping if not already loaded
38
+
26
39
  if(!this.sectorMapping) {
27
40
  this.sectorMapping = await getInstrumentSectorMap();
28
41
  }
@@ -44,7 +57,12 @@ class DiversificationPnl {
44
57
  return;
45
58
  }
46
59
 
47
- const dailyPnl = todayPortfolio.PortfolioValue - yesterdayPortfolio.PortfolioValue;
60
+ // FIX: Calculate dailyPnl by summing NetProfit from all positions
61
+ const dailyPnl = this._calculateTotalPnl(todayPortfolio);
62
+
63
+ if (dailyPnl === null) {
64
+ return; // Cannot calculate P&L for this user
65
+ }
48
66
 
49
67
  this._initBucket(sectorCount);
50
68
  this.pnl_by_sector_count[sectorCount].pnl_sum += dailyPnl;
@@ -8,21 +8,41 @@ class TslEffectiveness {
8
8
  this.nontsl_group = { pnl_sum: 0, count: 0 };
9
9
  }
10
10
 
11
+ /**
12
+ * FIX: Helper function to calculate total P&L from positions
13
+ * @param {object} portfolio
14
+ * @returns {number|null}
15
+ */
16
+ _calculateTotalPnl(portfolio) {
17
+ // Speculators use PublicPositions
18
+ const positions = portfolio?.PublicPositions;
19
+ if (positions && Array.isArray(positions)) {
20
+ // Sum all NetProfit fields, defaulting to 0 if a position has no NetProfit
21
+ return positions.reduce((sum, pos) => sum + (pos.NetProfit || 0), 0);
22
+ }
23
+ return null;
24
+ }
25
+
11
26
  process(todayPortfolio, yesterdayPortfolio, userId) {
12
- // Check if user is a speculator and we have both days' data
13
- if (todayPortfolio?.context?.userType !== 'speculator' || !yesterdayPortfolio) {
27
+ // Check if user is a speculator and we have today's data
28
+ // FIX: yesterdayPortfolio is not needed for this logic, only today's P&L
29
+ if (todayPortfolio?.context?.userType !== 'speculator' || !todayPortfolio) {
14
30
  return;
15
31
  }
16
32
 
17
- // FIX: This calculation is for speculators, so we use PublicPositions
18
33
  const positions = todayPortfolio.PublicPositions;
19
34
 
20
- // FIX: Add check to ensure positions is an iterable array
21
35
  if (!positions || !Array.isArray(positions)) {
22
36
  return;
23
37
  }
24
38
 
25
- const dailyPnl = todayPortfolio.PortfolioValue - yesterdayPortfolio.PortfolioValue;
39
+ // FIX: Calculate dailyPnl by summing NetProfit from all positions
40
+ const dailyPnl = this._calculateTotalPnl(todayPortfolio);
41
+
42
+ if (dailyPnl === null) {
43
+ return; // Cannot calculate P&L
44
+ }
45
+
26
46
  const usesTSL = positions.some(p => p.IsTslEnabled);
27
47
 
28
48
  if (usesTSL) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aiden-shared-calculations-unified",
3
- "version": "1.0.21",
3
+ "version": "1.0.23",
4
4
  "description": "Shared calculation modules for the BullTrackers Computation System.",
5
5
  "main": "index.js",
6
6
  "files": [
@@ -1,51 +0,0 @@
1
- /**
2
- * Aggregates the total dollar amount invested in assets,
3
- * broken down by profit or loss (from our sample).
4
- */
5
- class AssetDollarMetrics {
6
- constructor() {
7
- this.assets = {};
8
- }
9
-
10
- _initAsset(ticker) {
11
- if (!this.assets[ticker]) {
12
- this.assets[ticker] = {
13
- total_invested_sum: 0,
14
- profit_invested_sum: 0,
15
- loss_invested_sum: 0
16
- };
17
- }
18
- }
19
-
20
- process(portfolioData, yesterdayPortfolio, userId, context, todayInsights, yesterdayInsights) {
21
- const { instrumentMappings } = context;
22
-
23
- // FIX: Use the correct portfolio position properties
24
- const positions = portfolioData.AggregatedPositions || portfolioData.PublicPositions;
25
- if (!positions || !Array.isArray(positions)) return;
26
-
27
- for (const position of positions) {
28
- // FIX: Use the correct PascalCase InstrumentID
29
- const ticker = instrumentMappings[position.InstrumentID];
30
- if (!ticker) continue;
31
-
32
- this._initAsset(ticker);
33
-
34
- // FIX: Use the correct PascalCase InvestedAmount
35
- this.assets[ticker].total_invested_sum += position.InvestedAmount;
36
-
37
- // FIX: Use the correct PascalCase NetProfit
38
- if (position.NetProfit > 0) {
39
- this.assets[ticker].profit_invested_sum += position.InvestedAmount;
40
- } else {
41
- this.assets[ticker].loss_invested_sum += position.InvestedAmount;
42
- }
43
- }
44
- }
45
-
46
- getResult() {
47
- return this.assets;
48
- }
49
- }
50
-
51
- module.exports = AssetDollarMetrics;
@@ -1,49 +0,0 @@
1
- /**
2
- * Aggregates the total dollar amount invested in sectors,
3
- * broken down by profit or loss (from our sample).
4
- */
5
- class SectorDollarMetrics {
6
- constructor() {
7
- this.sectors = {};
8
- }
9
-
10
- _initSector(sector) {
11
- if (!this.sectors[sector]) {
12
- this.sectors[sector] = {
13
- total_invested_sum: 0,
14
- profit_invested_sum: 0,
15
- loss_invested_sum: 0
16
- };
17
- }
18
- }
19
-
20
- process(portfolioData, yesterdayPortfolio, userId, context, todayInsights, yesterdayInsights) {
21
- const { sectorMapping } = context; // Assumes sectorMapping is in context
22
-
23
- // FIX: Use the correct portfolio position properties
24
- const positions = portfolioData.AggregatedPositions || portfolioData.PublicPositions;
25
- if (!positions || !Array.isArray(positions)) return;
26
-
27
- for (const position of positions) {
28
- // FIX: Use the correct PascalCase InstrumentID
29
- const sector = sectorMapping[position.InstrumentID] || 'Other';
30
- this._initSector(sector);
31
-
32
- // FIX: Use the correct PascalCase InvestedAmount
33
- this.sectors[sector].total_invested_sum += position.InvestedAmount;
34
-
35
- // FIX: Use the correct PascalCase NetProfit
36
- if (position.NetProfit > 0) {
37
- this.sectors[sector].profit_invested_sum += position.InvestedAmount;
38
- } else {
39
- this.sectors[sector].loss_invested_sum += position.InvestedAmount;
40
- }
41
- }
42
- }
43
-
44
- getResult() {
45
- return this.sectors;
46
- }
47
- }
48
-
49
- module.exports = SectorDollarMetrics;