aiden-shared-calculations-unified 1.0.67 → 1.0.69

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.
@@ -1,14 +1,18 @@
1
1
  /**
2
- * @fileoverview Calculation (Pass 1) for speculator metric.
2
+ * @fileoverview Calculation (Pass 1) for user behaviour.
3
3
  *
4
- * This metric answers: "What is the average holding duration
5
- * (in hours) for open speculator positions, grouped by asset?"
4
+ * REFACTOR: This metric now answers: "What is the average holding duration
5
+ * (in hours) for *closed* positions, averaged across all users, grouped by asset?"
6
+ *
7
+ * This calculation now uses the 'history' data source, not 'portfolio'.
6
8
  */
7
- const { loadInstrumentMappings } = require('../../utils/sector_mapping_provider');
9
+ const { loadInstrumentMappings } = require('../../../utils/sector_mapping_provider');
8
10
 
9
11
  class HoldingDurationPerAsset {
10
12
  constructor() {
11
13
  // { [instrumentId]: { sum_hours: 0, count: 0 } }
14
+ // 'sum_hours' will be the sum of user-level averages
15
+ // 'count' will be the number of users who traded that asset
12
16
  this.assets = new Map();
13
17
  this.mappings = null;
14
18
  }
@@ -23,11 +27,11 @@ class HoldingDurationPerAsset {
23
27
  "properties": {
24
28
  "avg_duration_hours": {
25
29
  "type": "number",
26
- "description": "Average holding duration in hours."
30
+ "description": "Average holding duration in hours (averaged across all users)."
27
31
  },
28
32
  "count": {
29
33
  "type": "number",
30
- "description": "Count of positions used in average."
34
+ "description": "Count of users used in average."
31
35
  }
32
36
  },
33
37
  "required": ["avg_duration_hours", "count"]
@@ -35,7 +39,7 @@ class HoldingDurationPerAsset {
35
39
 
36
40
  return {
37
41
  "type": "object",
38
- "description": "Calculates the average holding duration (in hours) for open speculator positions per asset.",
42
+ "description": "Calculates the average holding duration (in hours) for closed positions per asset, averaged across all users.",
39
43
  "patternProperties": {
40
44
  "^.*$": tickerSchema // Ticker
41
45
  },
@@ -48,40 +52,40 @@ class HoldingDurationPerAsset {
48
52
  this.assets.set(instrumentId, { sum_hours: 0, count: 0 });
49
53
  }
50
54
  }
51
-
52
- _getHoldingDurationHours(openDateStr) {
53
- if (!openDateStr) return 0;
54
- try {
55
- const openDate = new Date(openDateStr);
56
- // Get difference from 'now' (or the snapshot time)
57
- const diffMs = new Date().getTime() - openDate.getTime();
58
- return diffMs / (1000 * 60 * 60); // Convert ms to hours
59
- } catch (e) {
60
- return 0;
61
- }
62
- }
63
55
 
64
- process(portfolioData) {
65
- if (portfolioData?.context?.userType !== 'speculator') {
66
- return;
67
- }
68
-
69
- const positions = portfolioData.PublicPositions;
70
- if (!positions || !Array.isArray(positions)) {
56
+ /**
57
+ * Process data from the 'history' root data source.
58
+ * @param {object} rootData - The root data object from the runner.
59
+ * @param {string} userId - The user ID.
60
+ */
61
+ process(rootData, userId) {
62
+ // 1. Get the history data
63
+ const historyData = rootData.history;
64
+ if (!historyData || !Array.isArray(historyData.assets)) {
71
65
  return;
72
66
  }
73
67
 
74
- for (const pos of positions) {
75
- const instrumentId = pos.InstrumentID;
76
- if (!instrumentId) continue;
77
-
78
- this._initAsset(instrumentId);
68
+ // 2. Iterate over the aggregated assets in the history doc
69
+ for (const asset of historyData.assets) {
70
+ const instrumentId = asset.instrumentId;
71
+
72
+ // Skip the "all" aggregate entry
73
+ if (!instrumentId || instrumentId === -1) {
74
+ continue;
75
+ }
79
76
 
80
- const duration = this._getHoldingDurationHours(pos.OpenDateTime);
77
+ const durationMinutes = asset.avgHoldingTimeInMinutes;
81
78
 
82
- if (duration > 0) {
79
+ if (typeof durationMinutes === 'number' && durationMinutes > 0) {
80
+ this._initAsset(instrumentId);
81
+
83
82
  const assetData = this.assets.get(instrumentId);
84
- assetData.sum_hours += duration;
83
+
84
+ // Convert minutes to hours and add to the sum
85
+ assetData.sum_hours += (durationMinutes / 60);
86
+
87
+ // Increment count (this now counts *users* who have
88
+ // an average for this asset, not individual positions)
85
89
  assetData.count++;
86
90
  }
87
91
  }
@@ -98,6 +102,7 @@ class HoldingDurationPerAsset {
98
102
 
99
103
  if (data.count > 0) {
100
104
  result[ticker] = {
105
+ // Calculate the final average (avg of avgs)
101
106
  avg_duration_hours: data.sum_hours / data.count,
102
107
  count: data.count
103
108
  };
@@ -0,0 +1,79 @@
1
+ /**
2
+ * @fileoverview Calculation (Pass 1) for user behaviour.
3
+ *
4
+ * This metric answers: "What is the average holding duration (in hours)
5
+ * for *closed* positions, averaged across all users?"
6
+ *
7
+ * It uses the 'history' data source's 'all' object.
8
+ */
9
+ class AverageHoldingDurationOverall {
10
+ constructor() {
11
+ // Stores all the user-level average durations
12
+ this.durationsHours = [];
13
+ }
14
+
15
+ /**
16
+ * Defines the output schema for this calculation.
17
+ * @returns {object} JSON Schema object
18
+ */
19
+ static getSchema() {
20
+ return {
21
+ "type": "object",
22
+ "description": "Calculates the platform-wide average holding duration (in hours) for closed positions.",
23
+ "properties": {
24
+ "average_duration_hours": {
25
+ "type": "number",
26
+ "description": "The average holding duration in hours, averaged across all users."
27
+ },
28
+ "user_count": {
29
+ "type": "number",
30
+ "description": "The number of users included in the average."
31
+ }
32
+ },
33
+ "required": ["average_duration_hours", "user_count"]
34
+ };
35
+ }
36
+
37
+ /**
38
+ * Process data from the 'history' root data source.
39
+ * @param {object} rootData - The root data object from the runner.
40
+ * @param {string} userId - The user ID.
41
+ */
42
+ process(rootData, userId) {
43
+ // 1. Get the history data
44
+ const historyData = rootData.history;
45
+ if (!historyData || !historyData.all) {
46
+ return;
47
+ }
48
+
49
+ // 2. Get the user's overall average holding time
50
+ const durationMinutes = historyData.all.avgHoldingTimeInMinutes;
51
+
52
+ if (typeof durationMinutes === 'number' && durationMinutes > 0) {
53
+ this.durationsHours.push(durationMinutes / 60);
54
+ }
55
+ }
56
+
57
+ getResult() {
58
+ const count = this.durationsHours.length;
59
+ if (count === 0) {
60
+ return {
61
+ average_duration_hours: 0,
62
+ user_count: 0
63
+ };
64
+ }
65
+
66
+ const sum = this.durationsHours.reduce((a, b) => a + b, 0);
67
+
68
+ return {
69
+ average_duration_hours: sum / count,
70
+ user_count: count
71
+ };
72
+ }
73
+
74
+ reset() {
75
+ this.durationsHours = [];
76
+ }
77
+ }
78
+
79
+ module.exports = AverageHoldingDurationOverall;
@@ -1,16 +1,11 @@
1
1
  /**
2
2
  * @fileoverview Calculation (Pass 1) for daily buy/sell sentiment.
3
3
  *
4
- * This metric tracks the total number of 'buy' (long) vs 'sell' (short)
5
- * positions held across all instruments.
6
- *
7
- * This provides a raw, global sentiment reading.
4
+ * REFACTOR: This is now a 'type: "meta"' calculation. It runs ONCE.
5
+ * It reads the pre-aggregated 'insights' data source and sums the
6
+ * 'buy' and 'sell' counts from all instruments.
8
7
  */
9
8
  class DailyBuySellSentimentCount {
10
- constructor() {
11
- this.buyPositions = 0;
12
- this.sellPositions = 0;
13
- }
14
9
 
15
10
  /**
16
11
  * Defines the output schema for this calculation.
@@ -38,34 +33,49 @@ class DailyBuySellSentimentCount {
38
33
  };
39
34
  }
40
35
 
41
- process(portfolioData) {
42
- const positions = portfolioData.PublicPositions || portfolioData.AggregatedPositions;
43
- if (!positions || !Array.isArray(positions)) {
44
- return;
36
+ /**
37
+ * This is a 'meta' calculation. It runs once.
38
+ * @param {string} dateStr - The date string 'YYYY-MM-DD'.
39
+ * @param {object} rootData - The root data object. We expect rootData.insights.
40
+ * @param {object} dependencies - The shared dependencies (e.g., logger).
41
+ * @returns {object} The calculation result.
42
+ */
43
+ process(dateStr, rootData, dependencies) {
44
+ let totalBuyPositions = 0;
45
+ let totalSellPositions = 0;
46
+
47
+ // rootData.insights contains the document from /daily_instrument_insights/
48
+ const insightsDoc = rootData.insights;
49
+
50
+ if (!insightsDoc || !Array.isArray(insightsDoc.insights)) {
51
+ dependencies.logger.log('WARN', `[daily-buy-sell-sentiment-count] No 'insights' data found for ${dateStr}.`);
52
+ return {
53
+ totalBuyPositions: 0,
54
+ totalSellPositions: 0,
55
+ sentimentRatio: null
56
+ };
45
57
  }
46
58
 
47
- for (const pos of positions) {
48
- if (pos.IsBuy) {
49
- this.buyPositions++;
50
- } else {
51
- this.sellPositions++;
59
+ // Iterate over the pre-aggregated array
60
+ for (const instrument of insightsDoc.insights) {
61
+ // The doc has pre-aggregated 'buy' and 'sell' counts
62
+ if (typeof instrument.buy === 'number') {
63
+ totalBuyPositions += instrument.buy;
64
+ }
65
+ if (typeof instrument.sell === 'number') {
66
+ totalSellPositions += instrument.sell;
52
67
  }
53
68
  }
54
- }
55
69
 
56
- getResult() {
57
70
  return {
58
- totalBuyPositions: this.buyPositions,
59
- totalSellPositions: this.sellPositions,
71
+ totalBuyPositions: totalBuyPositions,
72
+ totalSellPositions: totalSellPositions,
60
73
  // Calculate ratio: Buy / Sell
61
- sentimentRatio: (this.sellPositions > 0) ? (this.buyPositions / this.sellPositions) : null
74
+ sentimentRatio: (totalSellPositions > 0) ? (totalBuyPositions / totalSellPositions) : null
62
75
  };
63
76
  }
64
77
 
65
- reset() {
66
- this.buyPositions = 0;
67
- this.sellPositions = 0;
68
- }
78
+ // No constructor, getResult(), or reset() methods are needed for 'meta' calcs
69
79
  }
70
80
 
71
81
  module.exports = DailyBuySellSentimentCount;
@@ -0,0 +1,87 @@
1
+ /**
2
+ * @fileoverview Calculation (Pass 1) for daily ownership per sector.
3
+ *
4
+ * This is a 'type: "meta"' calculation. It runs ONCE.
5
+ * It reads the pre-aggregated 'insights' data source (/daily_instrument_insights/)
6
+ * and uses the sector mapping provider to aggregate the total number
7
+ * of owners for each sector.
8
+ */
9
+ const { loadInstrumentMappings } = require('../../utils/sector_mapping_provider');
10
+
11
+ class DailyOwnershipPerSector {
12
+
13
+ /**
14
+ * Defines the output schema for this calculation.
15
+ * @returns {object} JSON Schema object
16
+ */
17
+ static getSchema() {
18
+ const sectorSchema = {
19
+ "type": "object",
20
+ "description": "Aggregated ownership for a single sector.",
21
+ "properties": {
22
+ "total_owners": {
23
+ "type": "number",
24
+ "description": "The total number of unique owners for all assets in this sector."
25
+ }
26
+ },
27
+ "required": ["total_owners"]
28
+ };
29
+
30
+ return {
31
+ "type": "object",
32
+ "description": "Calculates the total unique owners per sector based on the 'insights' data source.",
33
+ "patternProperties": {
34
+ "^.*$": sectorSchema // Matches any string key (sector name)
35
+ },
36
+ "additionalProperties": sectorSchema
37
+ };
38
+ }
39
+
40
+ /**
41
+ * This is a 'meta' calculation. It runs once.
42
+ * @param {string} dateStr - The date string 'YYYY-MM-DD'.
43
+ * @param {object} rootData - The root data object. We expect rootData.insights.
44
+ * @param {object} dependencies - The shared dependencies (e.g., logger).
45
+ * @returns {Promise<object>} The calculation result.
46
+ */
47
+ async process(dateStr, rootData, dependencies) {
48
+ // { [sectorName]: { total_owners: 0 } }
49
+ const sectorOwners = new Map();
50
+
51
+ // 1. Load mappings
52
+ const mappings = await loadInstrumentMappings();
53
+
54
+ // 2. Get the insights document
55
+ const insightsDoc = rootData.insights;
56
+ if (!insightsDoc || !Array.isArray(insightsDoc.insights)) {
57
+ dependencies.logger.log('WARN', `[daily-ownership-per-sector] No 'insights' data found for ${dateStr}.`);
58
+ return {};
59
+ }
60
+
61
+ // 3. Iterate over the pre-aggregated array
62
+ for (const instrument of insightsDoc.insights) {
63
+ const instrumentId = instrument.instrumentId;
64
+ const totalOwners = instrument.total; // 'total' is the owner count
65
+
66
+ if (!instrumentId || typeof totalOwners !== 'number' || totalOwners === 0) {
67
+ continue;
68
+ }
69
+
70
+ // Find the sector for this instrument
71
+ const sectorName = mappings.instrumentToSectorName[instrumentId] || 'N/A';
72
+
73
+ // Initialize if new
74
+ if (!sectorOwners.has(sectorName)) {
75
+ sectorOwners.set(sectorName, { total_owners: 0 });
76
+ }
77
+
78
+ // Add this instrument's owners to the sector's total
79
+ sectorOwners.get(sectorName).total_owners += totalOwners;
80
+ }
81
+
82
+ // Convert Map to plain object for Firestore
83
+ return Object.fromEntries(sectorOwners);
84
+ }
85
+ }
86
+
87
+ module.exports = DailyOwnershipPerSector;
@@ -1,16 +1,12 @@
1
1
  /**
2
2
  * @fileoverview Calculation (Pass 1) for total positions held.
3
3
  *
4
- * This is a simple counter that calculates the total number of positions
5
- * held across all users and all instruments today.
6
- *
7
- * It provides a measure of overall market participation.
4
+ * REFACTOR: This is now a 'type: "meta"' calculation. It runs ONCE.
5
+ * It reads the pre-aggregated 'insights' data source and sums the
6
+ * 'total' field from all instruments to get the platform-wide total.
8
7
  */
9
8
  class DailyTotalPositionsHeld {
10
- constructor() {
11
- this.totalPositions = 0;
12
- }
13
-
9
+
14
10
  /**
15
11
  * Defines the output schema for this calculation.
16
12
  * @returns {object} JSON Schema object
@@ -29,22 +25,37 @@ class DailyTotalPositionsHeld {
29
25
  };
30
26
  }
31
27
 
32
- process(portfolioData) {
33
- const positions = portfolioData.PublicPositions || portfolioData.AggregatedPositions;
34
- if (positions && Array.isArray(positions)) {
35
- this.totalPositions += positions.length;
28
+ /**
29
+ * This is a 'meta' calculation. It runs once.
30
+ * @param {string} dateStr - The date string 'YYYY-MM-DD'.
31
+ * @param {object} rootData - The root data object. We expect rootData.insights.
32
+ * @param {object} dependencies - The shared dependencies (e.g., logger).
33
+ * @returns {object} The calculation result.
34
+ */
35
+ process(dateStr, rootData, dependencies) {
36
+ let totalPositions = 0;
37
+
38
+ // rootData.insights contains the document from /daily_instrument_insights/
39
+ const insightsDoc = rootData.insights;
40
+
41
+ if (!insightsDoc || !Array.isArray(insightsDoc.insights)) {
42
+ dependencies.logger.log('WARN', `[daily-total-positions-held] No 'insights' data found for ${dateStr}.`);
43
+ return { totalPositions: 0 };
36
44
  }
37
- }
38
45
 
39
- getResult() {
46
+ for (const instrument of insightsDoc.insights) {
47
+ // The 'total' field from the doc is the total # of positions for that instrument
48
+ if (typeof instrument.total === 'number') {
49
+ totalPositions += instrument.total;
50
+ }
51
+ }
52
+
40
53
  return {
41
- totalPositions: this.totalPositions
54
+ totalPositions: totalPositions
42
55
  };
43
56
  }
44
-
45
- reset() {
46
- this.totalPositions = 0;
47
- }
57
+
58
+ // No getResult() or reset() methods are needed for a 'meta' calculation
48
59
  }
49
60
 
50
61
  module.exports = DailyTotalPositionsHeld;
@@ -1,20 +1,17 @@
1
1
  /**
2
- * @fileoverview Calculation (Pass 2) for daily ownership delta.
2
+ * @fileoverview Calculation (Pass 1) for daily ownership delta.
3
3
  *
4
4
  * This metric calculates the daily change in the total number of *owners*
5
5
  * (unique users) for each instrument.
6
6
  *
7
- * This measures the broadening or narrowing of an asset's holder base.
7
+ * REFACTOR: This is now a 'type: "meta"' and 'isHistorical: true' calculation.
8
+ * It runs ONCE, loads today's and yesterday's pre-aggregated 'insights' docs,
9
+ * and calculates the delta based on the 'total' field.
8
10
  */
9
11
  const { loadInstrumentMappings } = require('../../../utils/sector_mapping_provider');
10
12
 
11
13
  class DailyOwnershipDelta {
12
- constructor() {
13
- // We will store { [instrumentId]: { owners_yesterday: Set(), owners_today: Set() } }
14
- this.assetOwnership = new Map();
15
- this.mappings = null;
16
- }
17
-
14
+
18
15
  /**
19
16
  * Defines the output schema for this calculation.
20
17
  * @returns {object} JSON Schema object
@@ -54,55 +51,49 @@ class DailyOwnershipDelta {
54
51
  };
55
52
  }
56
53
 
57
- _initAsset(instrumentId) {
58
- if (!this.assetOwnership.has(instrumentId)) {
59
- this.assetOwnership.set(instrumentId, {
60
- owners_yesterday: new Set(),
61
- owners_today: new Set()
62
- });
63
- }
64
- }
65
-
66
- _getInstrumentIds(portfolio) {
67
- const positions = portfolio?.AggregatedPositions || portfolio?.PublicPositions;
68
- if (!positions || !Array.isArray(positions)) {
69
- return new Set();
70
- }
71
- return new Set(positions.map(p => p.InstrumentID).filter(Boolean));
72
- }
73
-
74
- process(todayPortfolio, yesterdayPortfolio, userId) {
75
- if (!todayPortfolio || !yesterdayPortfolio) {
76
- return;
77
- }
78
-
79
- const yIds = this._getInstrumentIds(yesterdayPortfolio);
80
- const tIds = this._getInstrumentIds(todayPortfolio);
81
-
82
- // Add user to yesterday's owner sets
83
- for (const yId of yIds) {
84
- this._initAsset(yId);
85
- this.assetOwnership.get(yId).owners_yesterday.add(userId);
86
- }
87
-
88
- // Add user to today's owner sets
89
- for (const tId of tIds) {
90
- this._initAsset(tId);
91
- this.assetOwnership.get(tId).owners_today.add(userId);
54
+ /**
55
+ * This is a 'meta' and 'historical' calculation. It runs once.
56
+ * @param {string} dateStr - The date string 'YYYY-MM-DD'.
57
+ * @param {object} todayRootData - Root data for today. We expect todayRootData.insights.
58
+ * @param {object} yesterdayRootData - Root data for yesterday. We expect yesterdayRootData.insights.
59
+ * @returns {Promise<object>} The calculation result.
60
+ */
61
+ async process(dateStr, todayRootData, yesterdayRootData) {
62
+ const mappings = await loadInstrumentMappings();
63
+
64
+ const todayOwners = new Map();
65
+ const yesterdayOwners = new Map();
66
+ const allInstrumentIds = new Set();
67
+
68
+ // 1. Process today's insights doc
69
+ const todayDoc = todayRootData.insights;
70
+ if (todayDoc && Array.isArray(todayDoc.insights)) {
71
+ for (const instrument of todayDoc.insights) {
72
+ const id = instrument.instrumentId;
73
+ const totalOwners = instrument.total || 0; // 'total' is the owner count
74
+ todayOwners.set(id, totalOwners);
75
+ allInstrumentIds.add(id);
76
+ }
92
77
  }
93
- }
94
-
95
- async getResult() {
96
- if (!this.mappings) {
97
- this.mappings = await loadInstrumentMappings();
78
+
79
+ // 2. Process yesterday's insights doc
80
+ const yesterdayDoc = yesterdayRootData.insights;
81
+ if (yesterdayDoc && Array.isArray(yesterdayDoc.insights)) {
82
+ for (const instrument of yesterdayDoc.insights) {
83
+ const id = instrument.instrumentId;
84
+ const totalOwners = instrument.total || 0; // 'total' is the owner count
85
+ yesterdayOwners.set(id, totalOwners);
86
+ allInstrumentIds.add(id);
87
+ }
98
88
  }
99
-
89
+
90
+ // 3. Calculate deltas
100
91
  const result = {};
101
- for (const [instrumentId, data] of this.assetOwnership.entries()) {
102
- const ticker = this.mappings.instrumentToTicker[instrumentId] || `id_${instrumentId}`;
92
+ for (const instrumentId of allInstrumentIds) {
93
+ const ticker = mappings.instrumentToTicker[instrumentId] || `id_${instrumentId}`;
103
94
 
104
- const yOwners = data.owners_yesterday.size;
105
- const tOwners = data.owners_today.size;
95
+ const tOwners = todayOwners.get(instrumentId) || 0;
96
+ const yOwners = yesterdayOwners.get(instrumentId) || 0;
106
97
 
107
98
  if (yOwners > 0 || tOwners > 0) {
108
99
  result[ticker] = {
@@ -113,13 +104,11 @@ class DailyOwnershipDelta {
113
104
  };
114
105
  }
115
106
  }
107
+
116
108
  return result;
117
109
  }
118
110
 
119
- reset() {
120
- this.assetOwnership.clear();
121
- this.mappings = null;
122
- }
111
+ // No getResult() or reset() methods are needed
123
112
  }
124
113
 
125
114
  module.exports = DailyOwnershipDelta;
@@ -7,7 +7,7 @@
7
7
  * This calculation *depends* on 'user_expectancy_score'
8
8
  * to identify the cohort.
9
9
  */
10
- const { loadInstrumentMappings } = require('../../../utils/sector_mapping_provider');
10
+ const { loadInstrumentMappings } = require('../../utils/sector_mapping_provider');
11
11
 
12
12
  class PositiveExpectancyCohortFlow {
13
13
  constructor() {
@@ -0,0 +1,71 @@
1
+ /**
2
+ * @fileoverview Calculation (Pass 1) for P&L.
3
+ *
4
+ * This metric provides the *overall* platform-wide profitability ratio
5
+ * (total profitable positions / total unprofitable positions)
6
+ * across all users and all open positions.
7
+ */
8
+ class ProfitabilityRatioOverall {
9
+ constructor() {
10
+ this.profitable = 0;
11
+ this.unprofitable = 0;
12
+ }
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": "Calculates the overall profitability ratio (profitable/unprofitable positions) for the entire platform.",
22
+ "properties": {
23
+ "overall_ratio": {
24
+ "type": ["number", "null"],
25
+ "description": "Ratio of profitable to unprofitable positions (profitable / unprofitable). Null if 0 unprofitable."
26
+ },
27
+ "total_profitable": {
28
+ "type": "number",
29
+ "description": "Total count of all profitable positions."
30
+ },
31
+ "total_unprofitable": {
32
+ "type": "number",
33
+ "description": "Total count of all unprofitable positions."
34
+ }
35
+ },
36
+ "required": ["overall_ratio", "total_profitable", "total_unprofitable"]
37
+ };
38
+ }
39
+
40
+ process(portfolioData) {
41
+ const positions = portfolioData.PublicPositions || portfolioData.AggregatedPositions;
42
+ if (!positions || !Array.isArray(positions)) {
43
+ return;
44
+ }
45
+
46
+ for (const pos of positions) {
47
+ const pnl = pos.NetProfit;
48
+
49
+ if (pnl > 0) {
50
+ this.profitable++;
51
+ } else if (pnl < 0) {
52
+ this.unprofitable++;
53
+ }
54
+ }
55
+ }
56
+
57
+ getResult() {
58
+ return {
59
+ overall_ratio: (this.unprofitable > 0) ? (this.profitable / this.unprofitable) : null,
60
+ total_profitable: this.profitable,
61
+ total_unprofitable: this.unprofitable
62
+ };
63
+ }
64
+
65
+ reset() {
66
+ this.profitable = 0;
67
+ this.unprofitable = 0;
68
+ }
69
+ }
70
+
71
+ module.exports = ProfitabilityRatioOverall;