aiden-shared-calculations-unified 1.0.86 → 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,136 +1,89 @@
1
1
  /**
2
- * @fileoverview Core Metric (Pass 2)
3
- *
4
- * This 'standard' calculation streams all user portfolios
5
- * and calculates the average daily P&L (in %) for all
6
- * users, bucketed by sector.
2
+ * @fileoverview Calculation (Pass 1) for average daily P&L % per sector.
3
+ * REFACTORED: Aggregates P&L percentages.
7
4
  */
8
- // --- STANDARD 0: REMOVED require('../../utils/sector_mapping_provider') ---
9
-
10
5
  class AverageDailyPnlPerSector {
11
6
  constructor() {
12
- // { [sector]: { pnl_sum: 0, count: 0 } }
13
- this.sectorBuckets = new Map();
14
-
15
- // --- STANDARD 0: RENAMED ---
7
+ this.sectorData = new Map();
16
8
  this.sectorMap = null;
17
9
  }
18
10
 
19
- /** Statically defines metadata */
20
11
  static getMetadata() {
21
12
  return {
22
13
  type: 'standard',
23
14
  rootDataDependencies: ['portfolio'],
24
- isHistorical: false, // Reads today's portfolio
15
+ isHistorical: false,
25
16
  userType: 'all',
26
- category: 'core'
17
+ category: 'core_pnl'
27
18
  };
28
19
  }
29
20
 
30
- /** Statically declare dependencies */
31
- static getDependencies() {
32
- return [];
33
- }
21
+ static getDependencies() { return []; }
34
22
 
35
- /**
36
- * Defines the output schema for this calculation.
37
- */
38
23
  static getSchema() {
39
- const schema = {
24
+ const sectorSchema = {
40
25
  "type": "object",
41
26
  "properties": {
42
27
  "avg_daily_pnl_pct": { "type": "number" },
43
- "total_pnl_pct": { "type": "number" },
28
+ "aggregate_pnl_pct_sum": { "type": "number", "description": "Sum of P&L % points for sector." },
44
29
  "user_count": { "type": "number" }
45
30
  },
46
- "required": ["avg_daily_pnl_pct", "total_pnl_pct", "user_count"]
47
- };
48
-
49
- return {
50
- "type": "object",
51
- "description": "Calculates the avg. daily P&L % for all users, bucketed by sector.",
52
- "patternProperties": { "^.*$": schema },
53
- "additionalProperties": schema
31
+ "required": ["avg_daily_pnl_pct", "aggregate_pnl_pct_sum", "user_count"]
54
32
  };
55
- }
56
-
57
- _init(map, key) {
58
- if (!map.has(key)) {
59
- map.set(key, { pnl_sum: 0, count: 0 });
60
- }
33
+ return { "type": "object", "patternProperties": { "^.*$": sectorSchema } };
61
34
  }
62
35
 
63
- process(todayPortfolio, yesterdayPortfolio, userId, context) {
64
- // --- STANDARD 0: FIXED ---
65
- if (!this.sectorMap) {
66
- this.sectorMap = context.sectorMapping;
67
- }
68
-
69
- if (!this.sectorMap) {
70
- return; // Failsafe if context is missing map
36
+ _initSector(sector) {
37
+ if (!this.sectorData.has(sector)) {
38
+ this.sectorData.set(sector, { total_pnl_pct: 0, count: 0 });
71
39
  }
72
-
73
- let dailyPnl = 0;
40
+ }
74
41
 
75
- // Check if it's a SPECULATOR portfolio (it has a root 'NetProfit' and 'PublicPositions')
76
- if (todayPortfolio.NetProfit !== undefined && todayPortfolio.PublicPositions) {
77
-
78
- dailyPnl = todayPortfolio.NetProfit;
42
+ process(context) {
43
+ const { extract } = context.math;
44
+ const { mappings, user } = context;
45
+ if (!this.sectorMap) this.sectorMap = mappings.sectorMapping;
79
46
 
80
- // Check if it's a NORMAL user portfolio (it has 'AggregatedPositionsByInstrumentTypeID')
81
- } else if (todayPortfolio.AggregatedPositionsByInstrumentTypeID) {
82
-
83
- // Sum the P&L by calculating (Value - Invested) from the aggregates
84
- dailyPnl = todayPortfolio.AggregatedPositionsByInstrumentTypeID.reduce((sum, agg) => {
85
- return sum + (agg.Value - agg.Invested);
86
- }, 0);
87
-
88
- }
89
- if (dailyPnl === 0) return; // No P&L
90
-
91
- const positions = todayPortfolio?.AggregatedPositions || todayPortfolio?.PublicPositions;
92
- if (!positions || positions.length === 0) {
93
- return;
94
- }
47
+ const positions = extract.getPositions(user.portfolio.today, user.type);
48
+ const userSectorPnL = new Map();
95
49
 
96
- // We only care about sectors this user *holds*
97
- const heldSectors = new Set();
98
50
  for (const pos of positions) {
99
- if (!pos.InstrumentID) continue;
100
- // --- STANDARD 0: SIMPLIFIED ---
101
- const sector = this.sectorMap[pos.InstrumentID];
102
- if (sector) {
103
- heldSectors.add(sector);
104
- }
51
+ const instId = extract.getInstrumentId(pos);
52
+ if (!instId) continue;
53
+
54
+ const sector = this.sectorMap[instId] || 'Unknown';
55
+ // NetProfit is the % profit relative to invested capital for that position
56
+ // We sum these to get the user's "sector performance points"
57
+ const pnlPct = extract.getNetProfit(pos);
58
+
59
+ if (!userSectorPnL.has(sector)) userSectorPnL.set(sector, 0);
60
+ userSectorPnL.set(sector, userSectorPnL.get(sector) + pnlPct);
105
61
  }
106
-
107
- for (const sector of heldSectors) {
108
- this._init(this.sectorBuckets, sector);
109
- const sec = this.sectorBuckets.get(sector);
110
- sec.pnl_sum += dailyPnl;
111
- sec.count++;
62
+
63
+ for (const [sector, pnl] of userSectorPnL.entries()) {
64
+ this._initSector(sector);
65
+ const data = this.sectorData.get(sector);
66
+ data.total_pnl_pct += pnl;
67
+ data.count++;
112
68
  }
113
69
  }
114
70
 
115
71
  async getResult() {
116
72
  const result = {};
117
-
118
- for (const [sector, data] of this.sectorBuckets.entries()) {
73
+ for (const [sector, data] of this.sectorData.entries()) {
119
74
  if (data.count > 0) {
120
75
  result[sector] = {
121
- avg_daily_pnl_pct: (data.pnl_sum / data.count) * 100, // Convert decimal to %
122
- total_pnl_pct: data.pnl_sum * 100,
76
+ avg_daily_pnl_pct: data.total_pnl_pct / data.count,
77
+ aggregate_pnl_pct_sum: data.total_pnl_pct,
123
78
  user_count: data.count
124
79
  };
125
80
  }
126
81
  }
127
-
128
82
  return result;
129
83
  }
130
-
84
+
131
85
  reset() {
132
- this.sectorBuckets.clear();
133
- // --- STANDARD 0: RENAMED ---
86
+ this.sectorData.clear();
134
87
  this.sectorMap = null;
135
88
  }
136
89
  }
@@ -1,136 +1,83 @@
1
1
  /**
2
- * @fileoverview Core Metric (Pass 2)
3
- *
4
- * This 'standard' calculation streams all user portfolios
5
- * and calculates the average daily P&L (in %) for all
6
- * users, bucketed by stock ticker.
2
+ * @fileoverview Calculation (Pass 1) for average daily P&L % per stock.
3
+ * REFACTORED: Aggregates P&L percentages.
7
4
  */
8
- // --- STANDARD 0: REMOVED require('../../utils/sector_mapping_provider') ---
9
-
10
5
  class AverageDailyPnlPerStock {
11
6
  constructor() {
12
- // { [ticker]: { pnl_sum: 0, count: 0 } }
13
- this.tickerBuckets = new Map();
14
-
15
- // --- STANDARD 0: RENAMED ---
7
+ this.stockData = new Map();
16
8
  this.tickerMap = null;
17
9
  }
18
10
 
19
- /** Statically defines metadata */
20
11
  static getMetadata() {
21
12
  return {
22
13
  type: 'standard',
23
14
  rootDataDependencies: ['portfolio'],
24
- isHistorical: false, // Reads today's portfolio
15
+ isHistorical: false,
25
16
  userType: 'all',
26
- category: 'core'
17
+ category: 'core_pnl'
27
18
  };
28
19
  }
29
20
 
30
- /** Statically declare dependencies */
31
- static getDependencies() {
32
- return [];
33
- }
21
+ static getDependencies() { return []; }
34
22
 
35
- /**
36
- * Defines the output schema for this calculation.
37
- */
38
23
  static getSchema() {
39
- const schema = {
24
+ const stockSchema = {
40
25
  "type": "object",
41
26
  "properties": {
42
27
  "avg_daily_pnl_pct": { "type": "number" },
43
- "total_pnl_pct": { "type": "number" },
44
- "user_count": { "type": "number" }
28
+ "aggregate_pnl_pct_sum": { "type": "number" },
29
+ "position_count": { "type": "number" }
45
30
  },
46
- "required": ["avg_daily_pnl_pct", "total_pnl_pct", "user_count"]
31
+ "required": ["avg_daily_pnl_pct", "aggregate_pnl_pct_sum", "position_count"]
47
32
  };
48
-
49
- return {
50
- "type": "object",
51
- "description": "Calculates the avg. daily P&L % for all users, bucketed by stock.",
52
- "patternProperties": { "^.*$": schema },
53
- "additionalProperties": schema
54
- };
55
- }
56
-
57
- _init(map, key) {
58
- if (!map.has(key)) {
59
- map.set(key, { pnl_sum: 0, count: 0 });
60
- }
33
+ return { "type": "object", "patternProperties": { "^.*$": stockSchema } };
61
34
  }
62
35
 
63
- process(todayPortfolio, yesterdayPortfolio, userId, context) {
64
- // --- STANDARD 0: FIXED ---
65
- if (!this.tickerMap) {
66
- this.tickerMap = context.instrumentToTicker;
67
- }
68
-
69
- if (!this.tickerMap) {
70
- return; // Failsafe if context is missing map
36
+ _initStock(instId) {
37
+ if (!this.stockData.has(instId)) {
38
+ this.stockData.set(instId, { total_pnl_pct: 0, count: 0 });
71
39
  }
72
-
73
- let dailyPnl = 0;
40
+ }
74
41
 
75
- // Check if it's a SPECULATOR portfolio (it has a root 'NetProfit' and 'PublicPositions')
76
- if (todayPortfolio.NetProfit !== undefined && todayPortfolio.PublicPositions) {
77
-
78
- dailyPnl = todayPortfolio.NetProfit;
42
+ process(context) {
43
+ const { extract } = context.math;
44
+ const { mappings, user } = context;
45
+ if (!this.tickerMap) this.tickerMap = mappings.instrumentToTicker;
79
46
 
80
- // Check if it's a NORMAL user portfolio (it has 'AggregatedPositionsByInstrumentTypeID')
81
- } else if (todayPortfolio.AggregatedPositionsByInstrumentTypeID) {
82
-
83
- // Sum the P&L by calculating (Value - Invested) from the aggregates
84
- dailyPnl = todayPortfolio.AggregatedPositionsByInstrumentTypeID.reduce((sum, agg) => {
85
- return sum + (agg.Value - agg.Invested);
86
- }, 0);
87
-
88
- }
89
- if (dailyPnl === 0) return; // No P&L
90
-
91
- const positions = todayPortfolio?.AggregatedPositions || todayPortfolio?.PublicPositions;
92
- if (!positions || positions.length === 0) {
93
- return;
94
- }
47
+ const positions = extract.getPositions(user.portfolio.today, user.type);
95
48
 
96
- // We only care about tickers this user *holds*
97
- const heldTickers = new Set();
98
49
  for (const pos of positions) {
99
- if (!pos.InstrumentID) continue;
100
- // --- STANDARD 0: SIMPLIFIED ---
101
- const ticker = this.tickerMap[pos.InstrumentID];
102
- if (ticker) {
103
- heldTickers.add(ticker);
104
- }
105
- }
106
-
107
- for (const ticker of heldTickers) {
108
- this._init(this.tickerBuckets, ticker);
109
- const asset = this.tickerBuckets.get(ticker);
110
- asset.pnl_sum += dailyPnl;
111
- asset.count++;
50
+ const instId = extract.getInstrumentId(pos);
51
+ if (!instId) continue;
52
+
53
+ // NetProfit is %
54
+ const pnlPct = extract.getNetProfit(pos);
55
+
56
+ this._initStock(instId);
57
+ const data = this.stockData.get(instId);
58
+ data.total_pnl_pct += pnlPct;
59
+ data.count++;
112
60
  }
113
61
  }
114
62
 
115
63
  async getResult() {
64
+ if (!this.tickerMap) return {};
116
65
  const result = {};
117
-
118
- for (const [ticker, data] of this.tickerBuckets.entries()) {
66
+ for (const [instId, data] of this.stockData.entries()) {
67
+ const ticker = this.tickerMap[instId] || `id_${instId}`;
119
68
  if (data.count > 0) {
120
69
  result[ticker] = {
121
- avg_daily_pnl_pct: (data.pnl_sum / data.count) * 100, // Convert decimal to %
122
- total_pnl_pct: data.pnl_sum * 100,
123
- user_count: data.count
70
+ avg_daily_pnl_pct: data.total_pnl_pct / data.count,
71
+ aggregate_pnl_pct_sum: data.total_pnl_pct,
72
+ position_count: data.count
124
73
  };
125
74
  }
126
75
  }
127
-
128
76
  return result;
129
77
  }
130
-
78
+
131
79
  reset() {
132
- this.tickerBuckets.clear();
133
- // --- STANDARD 0: RENAMED ---
80
+ this.stockData.clear();
134
81
  this.tickerMap = null;
135
82
  }
136
83
  }
@@ -1,90 +1,60 @@
1
1
  /**
2
- * @fileoverview Calculation (Pass 1) for average P&L per position.
3
- *
4
- * This metric answers: "What was the average P&L across all
5
- * individual positions held by the crowd today?"
6
- *
7
- * This is different from "average P&L per user" as a user
8
- * can have multiple positions.
2
+ * @fileoverview Calculation (Pass 1) for average P&L % per position.
3
+ * REFACTORED: Uses P&L %.
9
4
  */
10
5
  class AverageDailyPositionPnl {
11
6
  constructor() {
12
- this.totalPnl = 0;
7
+ this.totalPnlPct = 0;
13
8
  this.positionCount = 0;
14
9
  }
15
10
 
16
- // --- NEW ---
17
- /**
18
- * Statically defines all metadata for the manifest builder.
19
- */
20
11
  static getMetadata() {
21
12
  return {
22
13
  type: 'standard',
23
- rootDataDependencies: ['portfolio'], // Needs portfolio positions
14
+ rootDataDependencies: ['portfolio'],
24
15
  isHistorical: false,
25
16
  userType: 'all',
26
17
  category: 'core_pnl'
27
18
  };
28
19
  }
29
20
 
30
- // --- NEW ---
31
- /**
32
- * Statically declare dependencies.
33
- */
34
- static getDependencies() {
35
- return [];
36
- }
21
+ static getDependencies() { return []; }
37
22
 
38
- /**
39
- * Defines the output schema for this calculation.
40
- * @returns {object} JSON Schema object
41
- */
42
23
  static getSchema() {
43
24
  return {
44
25
  "type": "object",
45
- "description": "Calculates the average daily P&L across all individual positions.",
46
26
  "properties": {
47
- "average_pnl": {
48
- "type": "number",
49
- "description": "The average daily P&L per position (Total P&L / Position Count)."
50
- },
51
- "total_pnl": {
52
- "type": "number",
53
- "description": "The sum of all daily P&L from all positions."
54
- },
55
- "position_count": {
56
- "type": "number",
57
- "description": "The total number of positions processed."
58
- }
27
+ "avg_position_pnl_pct": { "type": "number" },
28
+ "aggregate_pnl_pct_sum": { "type": "number" },
29
+ "total_positions": { "type": "number" }
59
30
  },
60
- "required": ["average_pnl", "total_pnl", "position_count"]
31
+ "required": ["avg_position_pnl_pct", "aggregate_pnl_pct_sum", "total_positions"]
61
32
  };
62
33
  }
63
34
 
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
- this.totalPnl += pos.NetProfit || 0;
42
+ this.totalPnlPct += extract.getNetProfit(pos);
72
43
  this.positionCount++;
73
44
  }
74
45
  }
75
46
 
76
47
  getResult() {
77
48
  return {
78
- average_pnl: (this.positionCount > 0) ? (this.totalPnl / this.positionCount) : 0,
79
- total_pnl: this.totalPnl,
80
- position_count: this.positionCount
49
+ avg_position_pnl_pct: (this.positionCount > 0) ? (this.totalPnlPct / this.positionCount) : 0,
50
+ aggregate_pnl_pct_sum: this.totalPnlPct,
51
+ total_positions: this.positionCount
81
52
  };
82
53
  }
83
54
 
84
55
  reset() {
85
- this.totalPnl = 0;
56
+ this.totalPnlPct = 0;
86
57
  this.positionCount = 0;
87
58
  }
88
59
  }
89
-
90
60
  module.exports = AverageDailyPositionPnl;
@@ -1,162 +1,60 @@
1
1
  /**
2
2
  * @fileoverview Calculation (Pass 1) for user behaviour.
3
- *
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'.
3
+ * REFACTORED: Uses context.math.history.getDailyHistory().
8
4
  */
9
- // --- STANDARD 0: REMOVED require('../../utils/sector_mapping_provider') ---
10
-
11
-
12
5
  class HoldingDurationPerAsset {
13
6
  constructor() {
14
- // { [instrumentId]: { sum_hours: 0, count: 0 } }
15
- // 'sum_hours' will be the sum of user-level averages
16
- // 'count' will be the number of users who traded that asset
17
7
  this.assets = new Map();
18
- // --- STANDARD 0: RENAMED ---
19
8
  this.tickerMap = null;
20
9
  }
21
10
 
22
- /**
23
- * Statically defines all metadata for the manifest builder.
24
- */
25
11
  static getMetadata() {
26
- return {
27
- type: 'standard',
28
- rootDataDependencies: ['history'], // It only needs the history doc
29
- isHistorical: false, // It only needs today's history doc
30
- userType: 'all',
31
- category: 'core_metrics'
32
- };
12
+ return { type: 'standard', rootDataDependencies: ['history'], isHistorical: false, userType: 'all', category: 'core_metrics' };
33
13
  }
34
-
35
- /**
36
- * Statically declare dependencies.
37
- */
38
- static getDependencies() {
39
- return [];
40
- }
41
-
42
- /**
43
- * Defines the output schema for this calculation.
44
- */
14
+ static getDependencies() { return []; }
45
15
  static getSchema() {
46
- const tickerSchema = {
47
- "type": "object",
48
- "properties": {
49
- "avg_duration_hours": {
50
- "type": "number",
51
- "description": "Average holding duration in hours (averaged across all users)."
52
- },
53
- "count": {
54
- "type": "number",
55
- "description": "Count of users used in average."
56
- }
57
- },
58
- "required": ["avg_duration_hours", "count"]
59
- };
60
-
61
- return {
62
- "type": "object",
63
- "description": "Calculates the average holding duration (in hours) for closed positions per asset, averaged across all users.",
64
- "patternProperties": {
65
- "^.*$": tickerSchema // Ticker
66
- },
67
- "additionalProperties": tickerSchema
68
- };
16
+ const tickerSchema = { "type": "object", "properties": { "avg_duration_hours": { "type": "number" }, "count": { "type": "number" } }, "required": ["avg_duration_hours", "count"] };
17
+ return { "type": "object", "patternProperties": { "^.*$": tickerSchema } };
69
18
  }
70
19
 
71
20
  _initAsset(instrumentId) {
72
- if (!this.assets.has(instrumentId)) {
73
- this.assets.set(instrumentId, { sum_hours: 0, count: 0 });
74
- }
21
+ if (!this.assets.has(instrumentId)) this.assets.set(instrumentId, { sum_hours: 0, count: 0 });
75
22
  }
76
23
 
77
- /**
78
- * Process data from the 'history' root data source.
79
- */
80
- process(
81
- todayPortfolio,
82
- yesterdayPortfolio,
83
- userId,
84
- context,
85
- todayInsights,
86
- yesterdayInsights,
87
- todaySocialPostInsights,
88
- yesterdaySocialPostInsights,
89
- todayHistory
90
- ) {
91
- // --- STANDARD 0: FIXED ---
92
- if (!this.tickerMap) {
93
- this.tickerMap = context.instrumentToTicker;
94
- }
24
+ process(context) {
25
+ const { history } = context.math;
26
+ const { mappings, user } = context;
27
+ if (!this.tickerMap) this.tickerMap = mappings.instrumentToTicker;
95
28
 
96
- // ---
97
- // FIX: The test harness injects data based on getMetadata().
98
- // Because rootDataDependencies is ONLY ['history'], the test harness
99
- // places the history object in the *first* argument (todayPortfolio).
100
- // ---
101
- const historyData = todayPortfolio; // NOT todayHistory
102
- if (!historyData || !Array.isArray(historyData.assets)) {
103
- return;
104
- }
29
+ // V4: Strict Access via Math Layer
30
+ const historyDoc = history.getDailyHistory(user);
31
+ const tradedAssets = history.getTradedAssets(historyDoc);
105
32
 
106
- // 2. Iterate over the aggregated assets in the history doc
107
- for (const asset of historyData.assets) {
108
- const instrumentId = asset.instrumentId;
33
+ for (const asset of tradedAssets) {
34
+ const instId = history.getInstrumentId(asset);
35
+ if (!instId || instId === -1) continue;
109
36
 
110
- // Skip the "all" aggregate entry
111
- if (!instrumentId || instrumentId === -1) {
112
- continue;
113
- }
114
-
115
- const durationMinutes = asset.avgHoldingTimeInMinutes;
116
-
117
- if (typeof durationMinutes === 'number' && durationMinutes > 0) {
118
- this._initAsset(instrumentId);
119
-
120
- const assetData = this.assets.get(instrumentId);
121
-
122
- // Convert minutes to hours and add to the sum
37
+ const durationMinutes = history.getAvgHoldingTimeMinutes(asset);
38
+ if (durationMinutes > 0) {
39
+ this._initAsset(instId);
40
+ const assetData = this.assets.get(instId);
123
41
  assetData.sum_hours += (durationMinutes / 60);
124
-
125
- // Increment count
126
42
  assetData.count++;
127
43
  }
128
44
  }
129
45
  }
130
46
 
131
47
  async getResult() {
132
- // --- STANDARD 0: REMOVED forbidden data load ---
133
-
134
- // Failsafe check
135
- if (!this.tickerMap) {
136
- return {}; // process() must run first
137
- }
138
-
48
+ if (!this.tickerMap) return {};
139
49
  const result = {};
140
- for (const [instrumentId, data] of this.assets.entries()) {
141
- // --- STANDARD 0: SIMPLIFIED ---
142
- const ticker = this.tickerMap[instrumentId] || `id_${instrumentId}`;
143
-
50
+ for (const [instId, data] of this.assets.entries()) {
51
+ const ticker = this.tickerMap[instId] || `id_${instId}`;
144
52
  if (data.count > 0) {
145
- result[ticker] = {
146
- // Calculate the final average (avg of avgs)
147
- avg_duration_hours: data.sum_hours / data.count,
148
- count: data.count
149
- };
53
+ result[ticker] = { avg_duration_hours: data.sum_hours / data.count, count: data.count };
150
54
  }
151
55
  }
152
56
  return result;
153
57
  }
154
-
155
- reset() {
156
- this.assets.clear();
157
- // --- STANDARD 0: RENAMED ---
158
- this.tickerMap = null;
159
- }
58
+ reset() { this.assets.clear(); this.tickerMap = null; }
160
59
  }
161
-
162
60
  module.exports = HoldingDurationPerAsset;