aiden-shared-calculations-unified 1.0.84 → 1.0.87

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 (70) hide show
  1. package/calculations/core/asset-pnl-status.js +36 -106
  2. package/calculations/core/asset-position-size.js +40 -91
  3. package/calculations/core/average-daily-pnl-all-users.js +18 -57
  4. package/calculations/core/average-daily-pnl-per-sector.js +41 -88
  5. package/calculations/core/average-daily-pnl-per-stock.js +38 -91
  6. package/calculations/core/average-daily-position-pnl.js +19 -49
  7. package/calculations/core/holding-duration-per-asset.js +25 -127
  8. package/calculations/core/instrument-price-change-1d.js +30 -49
  9. package/calculations/core/instrument-price-momentum-20d.js +50 -60
  10. package/calculations/core/long-position-per-stock.js +39 -68
  11. package/calculations/core/overall-holding-duration.js +16 -87
  12. package/calculations/core/overall-profitability-ratio.js +11 -40
  13. package/calculations/core/platform-buy-sell-sentiment.js +41 -124
  14. package/calculations/core/platform-daily-bought-vs-sold-count.js +41 -99
  15. package/calculations/core/platform-daily-ownership-delta.js +68 -126
  16. package/calculations/core/platform-ownership-per-sector.js +45 -96
  17. package/calculations/core/platform-total-positions-held.js +20 -80
  18. package/calculations/core/pnl-distribution-per-stock.js +29 -135
  19. package/calculations/core/price-metrics.js +95 -206
  20. package/calculations/core/profitability-ratio-per-sector.js +34 -79
  21. package/calculations/core/profitability-ratio-per-stock.js +32 -88
  22. package/calculations/core/profitability-skew-per-stock.js +41 -94
  23. package/calculations/core/profitable-and-unprofitable-status.js +44 -76
  24. package/calculations/core/sentiment-per-stock.js +24 -77
  25. package/calculations/core/short-position-per-stock.js +35 -43
  26. package/calculations/core/social-activity-aggregation.js +26 -49
  27. package/calculations/core/social-asset-posts-trend.js +38 -94
  28. package/calculations/core/social-event-correlation.js +26 -93
  29. package/calculations/core/social-sentiment-aggregation.js +20 -44
  30. package/calculations/core/social-top-mentioned-words.js +35 -87
  31. package/calculations/core/social-topic-interest-evolution.js +22 -111
  32. package/calculations/core/social-topic-sentiment-matrix.js +38 -104
  33. package/calculations/core/social-word-mentions-trend.js +27 -104
  34. package/calculations/core/speculator-asset-sentiment.js +31 -72
  35. package/calculations/core/speculator-danger-zone.js +48 -84
  36. package/calculations/core/speculator-distance-to-stop-loss-per-leverage.js +20 -52
  37. package/calculations/core/speculator-distance-to-tp-per-leverage.js +23 -53
  38. package/calculations/core/speculator-entry-distance-to-sl-per-leverage.js +20 -50
  39. package/calculations/core/speculator-entry-distance-to-tp-per-leverage.js +23 -50
  40. package/calculations/core/speculator-leverage-per-asset.js +25 -64
  41. package/calculations/core/speculator-leverage-per-sector.js +27 -63
  42. package/calculations/core/speculator-risk-reward-ratio-per-asset.js +24 -53
  43. package/calculations/core/speculator-stop-loss-distance-by-sector-short-long-breakdown.js +55 -68
  44. package/calculations/core/speculator-stop-loss-distance-by-ticker-short-long-breakdown.js +54 -71
  45. package/calculations/core/speculator-stop-loss-per-asset.js +19 -44
  46. package/calculations/core/speculator-take-profit-per-asset.js +20 -57
  47. package/calculations/core/speculator-tsl-per-asset.js +17 -56
  48. package/calculations/core/total-long-figures.js +16 -31
  49. package/calculations/core/total-long-per-sector.js +39 -61
  50. package/calculations/core/total-short-figures.js +13 -32
  51. package/calculations/core/total-short-per-sector.js +39 -61
  52. package/calculations/core/users-processed.js +11 -46
  53. package/calculations/gauss/cohort-capital-flow.js +54 -173
  54. package/calculations/gauss/cohort-definer.js +77 -163
  55. package/calculations/gauss/daily-dna-filter.js +29 -83
  56. package/calculations/gauss/gauss-divergence-signal.js +22 -109
  57. package/calculations/gem/cohort-momentum-state.js +27 -72
  58. package/calculations/gem/cohort-skill-definition.js +36 -52
  59. package/calculations/gem/platform-conviction-divergence.js +18 -60
  60. package/calculations/gem/quant-skill-alpha-signal.js +25 -98
  61. package/calculations/gem/skilled-cohort-flow.js +67 -175
  62. package/calculations/gem/skilled-unskilled-divergence.js +18 -73
  63. package/calculations/gem/unskilled-cohort-flow.js +64 -172
  64. package/calculations/helix/helix-contrarian-signal.js +20 -114
  65. package/calculations/helix/herd-consensus-score.js +42 -124
  66. package/calculations/helix/winner-loser-flow.js +36 -118
  67. package/calculations/pyro/risk-appetite-index.js +33 -74
  68. package/calculations/pyro/squeeze-potential.js +30 -87
  69. package/calculations/pyro/volatility-signal.js +33 -78
  70. package/package.json +1 -1
@@ -1,75 +1,64 @@
1
1
  /**
2
- * @fileoverview CORE Product Line (Pass 1)
3
- *
4
- * This 'meta' calculation computes the 1-day percentage price
5
- * change for all instruments.
2
+ * @fileoverview Calculation (Pass 1 - Meta) for 1-day price change.
3
+ * REFACTORED: Uses process(context) solely.
6
4
  */
7
-
8
5
  class InstrumentPriceChange1D {
9
6
  constructor() {
10
7
  this.result = {};
11
8
  }
12
9
 
13
- /** Statically defines metadata */
14
10
  static getMetadata() {
15
11
  return {
16
12
  type: 'meta',
17
13
  rootDataDependencies: ['price'],
18
14
  isHistorical: true,
19
- userType: null, // n/a
15
+ userType: 'n/a',
20
16
  category: 'core'
21
17
  };
22
18
  }
23
19
 
24
- /** Statically declare dependencies */
25
- static getDependencies() {
26
- return [];
20
+ static getDependencies() { return []; }
21
+
22
+ static getSchema() {
23
+ const tickerSchema = {
24
+ "type": "object",
25
+ "properties": {
26
+ "change_1d_pct": { "type": "number" }
27
+ },
28
+ "required": ["change_1d_pct"]
29
+ };
30
+ return { "type": "object", "patternProperties": { "^.*$": tickerSchema } };
27
31
  }
28
32
 
29
- /**
30
- * Helper to parse price data from the { instrumentId, prices: { '2025-10-01': 123 } } format
31
- * and return a sorted list.
32
- */
33
33
  _getPrices(priceData) {
34
- if (!priceData || !priceData.prices) {
35
- return [];
36
- }
37
- // FIX: Correctly parse and sort price data
34
+ if (!priceData || !priceData.prices) return [];
38
35
  return Object.entries(priceData.prices)
39
36
  .map(([date, price]) => ({ date, price }))
40
37
  .sort((a, b) => new Date(a.date) - new Date(b.date));
41
38
  }
42
39
 
43
- /**
44
- * process (FIXED for 5-arg META signature)
45
- * Arg 1: metaPayload (contains all root data from worker's "HACK" fix)
46
- * Arg 2: metaRootData (production compliance, unused in test)
47
- * Arg 3: dependencies (logger, etc., unused in test)
48
- * Arg 4: config (mappings)
49
- * Arg 5: fetchedDependencies (unused)
50
- */
51
- process(metaPayload, metaRootData, dependencies, config, fetchedDependencies) {
52
- const results = {};
40
+ process(context) {
41
+ const { mappings, prices } = context;
42
+ const { instrumentToTicker } = mappings;
53
43
 
54
- // FIX: Read from metaPayload (Arg 1) passed by worker.js
55
- const todayPrices = metaPayload.priceData || [];
56
- const yesterdayPrices = metaPayload.yesterdayPriceData || [];
44
+ // Expecting context.prices to be populated by the controller for 'meta' + 'price' dependency
45
+ const todayPrices = prices?.today || [];
46
+ const yesterdayPrices = prices?.yesterday || [];
47
+
48
+ const results = {};
57
49
 
58
- // Create lookup maps for today's and yesterday's prices
59
50
  const todayPriceMap = new Map(todayPrices.map(p => {
60
- const prices = this._getPrices(p);
61
- const lastPrice = prices.length > 0 ? prices[prices.length - 1].price : 0;
51
+ const pList = this._getPrices(p);
52
+ const lastPrice = pList.length > 0 ? pList[pList.length - 1].price : 0;
62
53
  return [p.instrumentId, lastPrice];
63
54
  }));
64
55
 
65
56
  const yesterdayPriceMap = new Map(yesterdayPrices.map(p => {
66
- const prices = this._getPrices(p);
67
- const lastPrice = prices.length > 0 ? prices[prices.length - 1].price : 0;
57
+ const pList = this._getPrices(p);
58
+ const lastPrice = pList.length > 0 ? pList[pList.length - 1].price : 0;
68
59
  return [p.instrumentId, lastPrice];
69
60
  }));
70
61
 
71
- const { instrumentToTicker } = config; // Read from Arg 4
72
-
73
62
  for (const [instrumentId, todayPrice] of todayPriceMap) {
74
63
  const yesterdayPrice = yesterdayPriceMap.get(instrumentId);
75
64
  const ticker = instrumentToTicker[instrumentId];
@@ -81,21 +70,13 @@ class InstrumentPriceChange1D {
81
70
  change_1d_pct: isFinite(changePct) ? changePct : 0
82
71
  };
83
72
  } else {
84
- results[ticker] = {
85
- change_1d_pct: 0
86
- };
73
+ results[ticker] = { change_1d_pct: 0 };
87
74
  }
88
75
  }
89
76
  this.result = results;
90
77
  }
91
78
 
92
- async getResult() {
93
- return this.result;
94
- }
95
-
96
- reset() {
97
- this.result = {};
98
- }
79
+ async getResult() { return this.result; }
80
+ reset() { this.result = {}; }
99
81
  }
100
-
101
82
  module.exports = InstrumentPriceChange1D;
@@ -1,94 +1,84 @@
1
1
  /**
2
- * @fileoverview CORE Product Line (Pass 1)
3
- *
4
- * This 'meta' calculation computes the 20-day percentage price
5
- * change (momentum) for all instruments.
2
+ * @fileoverview Calculation (Pass 1 - Meta) for 20-day momentum.
3
+ * REFACTORED: Uses process(context) solely.
6
4
  */
7
-
8
5
  class InstrumentPriceMomentum20D {
9
6
  constructor() {
10
7
  this.result = {};
11
8
  }
12
9
 
13
- /** Statically defines metadata */
14
10
  static getMetadata() {
15
11
  return {
16
12
  type: 'meta',
17
13
  rootDataDependencies: ['price'],
18
- isHistorical: true, // Needs history, but just from one 'today' file
19
- userType: null, // n/a
14
+ isHistorical: true,
15
+ userType: 'n/a',
20
16
  category: 'core'
21
17
  };
22
18
  }
23
19
 
24
- /** Statically declare dependencies */
25
- static getDependencies() {
26
- return [];
20
+ static getDependencies() { return []; }
21
+
22
+ static getSchema() {
23
+ const tickerSchema = {
24
+ "type": "object",
25
+ "properties": {
26
+ "momentum_20d_pct": { "type": "number" }
27
+ },
28
+ "required": ["momentum_20d_pct"]
29
+ };
30
+ return { "type": "object", "patternProperties": { "^.*$": tickerSchema } };
27
31
  }
28
32
 
29
- /**
30
- * Helper to parse price data from the { instrumentId, prices: { '2025-10-01': 123 } } format
31
- * and return a sorted list.
32
- */
33
- _getPrices(priceData) {
34
- if (!priceData || !priceData.prices) {
35
- return [];
33
+ _findPriceOnOrBefore(priceHistoryObj, targetDateStr) {
34
+ if (!priceHistoryObj || !priceHistoryObj.prices) return null;
35
+
36
+ let checkDate = new Date(targetDateStr + 'T00:00:00Z');
37
+ for (let i = 0; i < 5; i++) {
38
+ const str = checkDate.toISOString().slice(0, 10);
39
+ const price = priceHistoryObj.prices[str];
40
+ if (price !== undefined && price !== null && price > 0) return price;
41
+ checkDate.setUTCDate(checkDate.getUTCDate() - 1);
36
42
  }
37
- // FIX: Correctly parse and sort price data
38
- return Object.entries(priceData.prices)
39
- .map(([date, price]) => ({ date, price }))
40
- .sort((a, b) => new Date(a.date) - new Date(b.date));
43
+ return null;
41
44
  }
42
45
 
43
- /**
44
- * process (FIXED for 5-arg META signature)
45
- * Arg 1: metaPayload (contains all root data from worker's "HACK" fix)
46
- * Arg 2: metaRootData (production compliance, unused in test)
47
- * Arg 3: dependencies (logger, etc., unused in test)
48
- * Arg 4: config (mappings)
49
- * Arg 5: fetchedDependencies (unused)
50
- */
51
- process(metaPayload, metaRootData, dependencies, config, fetchedDependencies) {
52
- const results = {};
46
+ process(context) {
47
+ const { mappings, prices, date } = context;
48
+ const { instrumentToTicker } = mappings;
49
+
50
+ // Use historical price data available in context
51
+ const priceData = prices?.history || [];
53
52
 
54
- // FIX: Read from metaPayload (Arg 1) passed by worker.js
55
- // We only need today's data, which contains the 90-day history
56
- const allPriceData = metaPayload.priceData || [];
57
- const { instrumentToTicker } = config; // Read from Arg 4
53
+ const todayStr = date.today;
54
+ const todayDate = new Date(todayStr + 'T00:00:00Z');
55
+ const twentyDaysAgo = new Date(todayDate);
56
+ twentyDaysAgo.setUTCDate(todayDate.getUTCDate() - 20);
57
+ const oldStr = twentyDaysAgo.toISOString().slice(0, 10);
58
58
 
59
- for (const instrumentData of allPriceData) {
60
- const { instrumentId } = instrumentData;
59
+ const results = {};
60
+
61
+ for (const p of priceData) {
62
+ const instrumentId = p.instrumentId;
61
63
  const ticker = instrumentToTicker[instrumentId];
62
64
  if (!ticker) continue;
63
-
64
- const prices = this._getPrices(instrumentData);
65
-
66
- if (prices.length >= 20) {
67
- const currentPrice = prices[prices.length - 1].price;
68
- const price20DaysAgo = prices[prices.length - 20].price;
69
65
 
70
- if (price20DaysAgo > 0) {
71
- const changePct = ((currentPrice - price20DaysAgo) / price20DaysAgo) * 100;
72
- results[ticker] = {
73
- momentum_20d_pct: isFinite(changePct) ? changePct : 0
74
- };
75
- } else {
76
- results[ticker] = { momentum_20d_pct: 0 };
77
- }
66
+ const currentPrice = this._findPriceOnOrBefore(p, todayStr);
67
+ const oldPrice = this._findPriceOnOrBefore(p, oldStr);
68
+
69
+ if (currentPrice && oldPrice && oldPrice > 0) {
70
+ const momPct = ((currentPrice - oldPrice) / oldPrice) * 100;
71
+ results[ticker] = {
72
+ momentum_20d_pct: isFinite(momPct) ? momPct : 0
73
+ };
78
74
  } else {
79
75
  results[ticker] = { momentum_20d_pct: 0 };
80
76
  }
81
77
  }
82
78
  this.result = results;
83
79
  }
84
-
85
- async getResult() {
86
- return this.result;
87
- }
88
80
 
89
- reset() {
90
- this.result = {};
91
- }
81
+ async getResult() { return this.result; }
82
+ reset() { this.result = {}; }
92
83
  }
93
-
94
84
  module.exports = InstrumentPriceMomentum20D;
@@ -1,112 +1,83 @@
1
1
  /**
2
2
  * @fileoverview Calculation (Pass 1) for long positions per stock.
3
- *
4
- * This metric answers: "How many long ('buy') positions
5
- * are there for each stock?"
3
+ * REFACTORED: Uses context.math.extract. No USD.
6
4
  */
7
- // --- STANDARD 0: REMOVED require('../../utils/sector_mapping_provider') ---
8
-
9
5
  class LongPositionPerStock {
10
6
  constructor() {
11
- // We will store { [instrumentId]: count }
12
- this.assets = new Map();
13
- // --- STANDARD 0: RENAMED ---
7
+ this.stockData = new Map();
14
8
  this.tickerMap = null;
15
9
  }
16
10
 
17
- /**
18
- * Statically defines all metadata for the manifest builder.
19
- */
20
11
  static getMetadata() {
21
12
  return {
22
13
  type: 'standard',
23
14
  rootDataDependencies: ['portfolio'],
24
15
  isHistorical: false,
25
16
  userType: 'all',
26
- category: 'core_sentiment' // Based on Computation_documentation.md (short_and_long_stats -> sentiment_per_stock)
17
+ category: 'core_sentiment'
27
18
  };
28
19
  }
29
20
 
30
- /**
31
- * Statically declare dependencies.
32
- */
33
- static getDependencies() {
34
- return [];
35
- }
21
+ static getDependencies() { return []; }
36
22
 
37
- /**
38
- * Defines the output schema for this calculation.
39
- */
40
23
  static getSchema() {
41
- return {
24
+ const schema = {
42
25
  "type": "object",
43
- "description": "Calculates the total count of long ('buy') positions for each asset.",
44
- "patternProperties": {
45
- // Ticker
46
- "^.*$": {
47
- "type": "number",
48
- "description": "The total count of long positions for this asset."
49
- }
26
+ "properties": {
27
+ "long_count": { "type": "number" },
28
+ "total_long_exposure_weight": { "type": "number" }
50
29
  },
51
- "additionalProperties": {
52
- "type": "number"
53
- }
30
+ "required": ["long_count", "total_long_exposure_weight"]
54
31
  };
32
+ return { "type": "object", "patternProperties": { "^.*$": schema } };
55
33
  }
56
34
 
57
- _initAsset(instrumentId) {
58
- if (!this.assets.has(instrumentId)) {
59
- this.assets.set(instrumentId, 0);
35
+ _initStock(instId) {
36
+ if (!this.stockData.has(instId)) {
37
+ this.stockData.set(instId, { count: 0, weight: 0 });
60
38
  }
61
39
  }
62
40
 
63
- // --- STANDARD 0: UPDATED SIGNATURE ---
64
- process(todayPortfolio, yesterdayPortfolio, userId, context) {
65
- // --- STANDARD 0: ADDED ---
66
- if (!this.tickerMap) {
67
- this.tickerMap = context.instrumentToTicker;
68
- }
41
+ process(context) {
42
+ const { extract } = context.math;
43
+ const { mappings, user } = context;
44
+ if (!this.tickerMap) this.tickerMap = mappings.instrumentToTicker;
69
45
 
70
- // --- STANDARD 0: UPDATED ---
71
- const positions = todayPortfolio.PublicPositions || todayPortfolio.AggregatedPositions;
72
- if (!positions || !Array.isArray(positions)) {
73
- return;
74
- }
46
+ const positions = extract.getPositions(user.portfolio.today, user.type);
75
47
 
76
48
  for (const pos of positions) {
77
- // Only count 'buy' (long) positions
78
- if (pos.IsBuy) {
79
- const instrumentId = pos.InstrumentID;
80
- if (!instrumentId) continue;
81
-
82
- this._initAsset(instrumentId);
83
- this.assets.set(instrumentId, this.assets.get(instrumentId) + 1);
84
- }
85
- }
86
- }
49
+ if (extract.getDirection(pos) !== 'Buy') continue;
87
50
 
88
- async getResult() {
89
- // --- STANDARD 0: REMOVED forbidden data load ---
51
+ const instId = extract.getInstrumentId(pos);
52
+ if (!instId) continue;
90
53
 
91
- // Failsafe check
92
- if (!this.tickerMap) {
93
- return {}; // process() must run first
54
+ const weight = extract.getPositionWeight(pos, user.type);
55
+
56
+ this._initStock(instId);
57
+ const data = this.stockData.get(instId);
58
+ data.count++;
59
+ data.weight += weight;
94
60
  }
61
+ }
95
62
 
63
+ async getResult() {
64
+ if (!this.tickerMap) return {};
96
65
  const result = {};
97
- for (const [instrumentId, count] of this.assets.entries()) {
98
- // --- STANDARD 0: SIMPLIFIED ---
99
- const ticker = this.tickerMap[instrumentId] || `id_${instrumentId}`;
100
- result[ticker] = count;
66
+ for (const [instId, data] of this.stockData.entries()) {
67
+ const ticker = this.tickerMap[instId] || `id_${instId}`;
68
+ if (data.count > 0) {
69
+ result[ticker] = {
70
+ long_count: data.count,
71
+ total_long_exposure_weight: data.weight
72
+ };
73
+ }
101
74
  }
102
75
  return result;
103
76
  }
104
77
 
105
78
  reset() {
106
- this.assets.clear();
107
- // --- STANDARD 0: RENAMED ---
79
+ this.stockData.clear();
108
80
  this.tickerMap = null;
109
81
  }
110
82
  }
111
-
112
83
  module.exports = LongPositionPerStock;
@@ -1,108 +1,37 @@
1
1
  /**
2
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.
3
+ * REFACTORED: Uses context.math.history.getDailyHistory().
8
4
  */
9
5
  class AverageHoldingDurationOverall {
10
- constructor() {
11
- // Stores all the user-level average durations
12
- this.durationsHours = [];
13
- }
6
+ constructor() { this.durationsHours = []; }
14
7
 
15
- // --- NEW ---
16
- /**
17
- * Statically defines all metadata for the manifest builder.
18
- */
19
8
  static getMetadata() {
20
- return {
21
- type: 'standard',
22
- rootDataDependencies: ['history'], // Needs the history doc
23
- isHistorical: false, // Needs today's history doc
24
- userType: 'all',
25
- category: 'core_metrics'
26
- };
27
- }
28
-
29
- // --- NEW ---
30
- /**
31
- * Statically declare dependencies.
32
- */
33
- static getDependencies() {
34
- return [];
9
+ return { type: 'standard', rootDataDependencies: ['history'], isHistorical: false, userType: 'all', category: 'core_metrics' };
35
10
  }
36
-
37
- /**
38
- * Defines the output schema for this calculation.
39
- * @returns {object} JSON Schema object
40
- */
11
+ static getDependencies() { return []; }
41
12
  static getSchema() {
42
- return {
43
- "type": "object",
44
- "description": "Calculates the platform-wide average holding duration (in hours) for closed positions.",
45
- "properties": {
46
- "average_duration_hours": {
47
- "type": "number",
48
- "description": "The average holding duration in hours, averaged across all users."
49
- },
50
- "user_count": {
51
- "type": "number",
52
- "description": "The number of users included in the average."
53
- }
54
- },
55
- "required": ["average_duration_hours", "user_count"]
56
- };
13
+ return { "type": "object", "properties": { "average_duration_hours": { "type": "number" }, "user_count": { "type": "number" } }, "required": ["average_duration_hours", "user_count"] };
57
14
  }
58
15
 
59
- // --- REFACTORED ---
60
- /**
61
- * Process data from the 'history' root data source.
62
- * @param {object} todayPortfolio - Contains today's history doc
63
- * @param {object} yesterdayPortfolio - (Not used)
64
- * @param {string} userId - The user ID.
65
- */
66
- process(todayPortfolio, yesterdayPortfolio, userId, context) {
67
-
68
- // ---
69
- // FIX: The test harness injects data based on getMetadata().
70
- // Because rootDataDependencies is ONLY ['history'], the test harness
71
- // places the history object in the *first* argument (todayPortfolio).
72
- // ---
73
- const historyData = todayPortfolio; // NOT todayPortfolio?.history
74
- if (!historyData || !historyData.all) {
75
- return;
76
- }
16
+ process(context) {
17
+ const { history } = context.math;
18
+ const { user } = context;
77
19
 
78
- // 2. Get the user's overall average holding time
79
- const durationMinutes = historyData.all.avgHoldingTimeInMinutes;
20
+ // V4: Strict Access via Math Layer
21
+ const historyDoc = history.getDailyHistory(user);
22
+ const summary = history.getSummary(historyDoc);
80
23
 
81
- if (typeof durationMinutes === 'number' && durationMinutes > 0) {
82
- this.durationsHours.push(durationMinutes / 60);
24
+ if (summary && summary.avgHoldingTimeInMinutes > 0) {
25
+ this.durationsHours.push(summary.avgHoldingTimeInMinutes / 60);
83
26
  }
84
27
  }
85
28
 
86
29
  getResult() {
87
30
  const count = this.durationsHours.length;
88
- if (count === 0) {
89
- return {
90
- average_duration_hours: 0,
91
- user_count: 0
92
- };
93
- }
94
-
31
+ if (count === 0) return { average_duration_hours: 0, user_count: 0 };
95
32
  const sum = this.durationsHours.reduce((a, b) => a + b, 0);
96
-
97
- return {
98
- average_duration_hours: sum / count,
99
- user_count: count
100
- };
101
- }
102
-
103
- reset() {
104
- this.durationsHours = [];
33
+ return { average_duration_hours: sum / count, user_count: count };
105
34
  }
35
+ reset() { this.durationsHours = []; }
106
36
  }
107
-
108
37
  module.exports = AverageHoldingDurationOverall;
@@ -1,9 +1,6 @@
1
1
  /**
2
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.
3
+ * REFACTORED: Standardized extraction.
7
4
  */
8
5
  class ProfitabilityRatioOverall {
9
6
  constructor() {
@@ -11,10 +8,6 @@ class ProfitabilityRatioOverall {
11
8
  this.unprofitable = 0;
12
9
  }
13
10
 
14
- // --- NEW ---
15
- /**
16
- * Statically defines all metadata for the manifest builder.
17
- */
18
11
  static getMetadata() {
19
12
  return {
20
13
  type: 'standard',
@@ -25,50 +18,28 @@ class ProfitabilityRatioOverall {
25
18
  };
26
19
  }
27
20
 
28
- // --- NEW ---
29
- /**
30
- * Statically declare dependencies.
31
- */
32
- static getDependencies() {
33
- return [];
34
- }
21
+ static getDependencies() { return []; }
35
22
 
36
- /**
37
- * Defines the output schema for this calculation.
38
- * @returns {object} JSON Schema object
39
- */
40
23
  static getSchema() {
41
24
  return {
42
25
  "type": "object",
43
- "description": "Calculates the overall profitability ratio (profitable/unprofitable positions) for the entire platform.",
44
26
  "properties": {
45
- "overall_ratio": {
46
- "type": ["number", "null"],
47
- "description": "Ratio of profitable to unprofitable positions (profitable / unprofitable). Null if 0 unprofitable."
48
- },
49
- "total_profitable": {
50
- "type": "number",
51
- "description": "Total count of all profitable positions."
52
- },
53
- "total_unprofitable": {
54
- "type": "number",
55
- "description": "Total count of all unprofitable positions."
56
- }
27
+ "overall_ratio": { "type": ["number", "null"] },
28
+ "total_profitable": { "type": "number" },
29
+ "total_unprofitable": { "type": "number" }
57
30
  },
58
31
  "required": ["overall_ratio", "total_profitable", "total_unprofitable"]
59
32
  };
60
33
  }
61
34
 
62
- // --- REFACTORED ---
63
- // Simplified signature
64
- process(todayPortfolio, yesterdayPortfolio, userId, context) {
65
- const positions = todayPortfolio.PublicPositions || todayPortfolio.AggregatedPositions;
66
- if (!positions || !Array.isArray(positions)) {
67
- return;
68
- }
35
+ process(context) {
36
+ const { extract } = context.math;
37
+ const { user } = context;
38
+
39
+ const positions = extract.getPositions(user.portfolio.today, user.type);
69
40
 
70
41
  for (const pos of positions) {
71
- const pnl = pos.NetProfit;
42
+ const pnl = extract.getNetProfit(pos); // This is a %
72
43
 
73
44
  if (pnl > 0) {
74
45
  this.profitable++;