aiden-shared-calculations-unified 1.0.82 → 1.0.84

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 (71) hide show
  1. package/calculations/core/asset-pnl-status.js +122 -104
  2. package/calculations/core/asset-position-size.js +110 -73
  3. package/calculations/core/average-daily-pnl-all-users.js +17 -3
  4. package/calculations/core/average-daily-pnl-per-sector.js +83 -75
  5. package/calculations/core/average-daily-pnl-per-stock.js +84 -73
  6. package/calculations/core/average-daily-position-pnl.js +2 -2
  7. package/calculations/core/holding-duration-per-asset.js +24 -23
  8. package/calculations/core/instrument-price-change-1d.js +72 -82
  9. package/calculations/core/instrument-price-momentum-20d.js +66 -100
  10. package/calculations/core/long-position-per-stock.js +21 -13
  11. package/calculations/core/overall-holding-duration.js +8 -3
  12. package/calculations/core/overall-profitability-ratio.js +2 -2
  13. package/calculations/core/platform-buy-sell-sentiment.js +75 -22
  14. package/calculations/core/platform-daily-bought-vs-sold-count.js +19 -10
  15. package/calculations/core/platform-daily-ownership-delta.js +39 -15
  16. package/calculations/core/platform-ownership-per-sector.js +38 -18
  17. package/calculations/core/platform-total-positions-held.js +36 -14
  18. package/calculations/core/pnl-distribution-per-stock.js +39 -36
  19. package/calculations/core/price-metrics.js +70 -172
  20. package/calculations/core/profitability-ratio-per-sector.js +23 -29
  21. package/calculations/core/profitability-ratio-per-stock.js +20 -13
  22. package/calculations/core/profitability-skew-per-stock.js +20 -13
  23. package/calculations/core/profitable-and-unprofitable-status.js +34 -10
  24. package/calculations/core/sentiment-per-stock.js +20 -9
  25. package/calculations/core/short-position-per-stock.js +23 -37
  26. package/calculations/core/social-activity-aggregation.js +41 -115
  27. package/calculations/core/social-asset-posts-trend.js +77 -94
  28. package/calculations/core/social-event-correlation.js +87 -106
  29. package/calculations/core/social-sentiment-aggregation.js +56 -138
  30. package/calculations/core/social-top-mentioned-words.js +74 -106
  31. package/calculations/core/social-topic-interest-evolution.js +94 -94
  32. package/calculations/core/social-topic-sentiment-matrix.js +90 -74
  33. package/calculations/core/social-word-mentions-trend.js +92 -106
  34. package/calculations/core/speculator-asset-sentiment.js +63 -92
  35. package/calculations/core/speculator-danger-zone.js +77 -90
  36. package/calculations/core/speculator-distance-to-stop-loss-per-leverage.js +75 -90
  37. package/calculations/core/speculator-distance-to-tp-per-leverage.js +75 -88
  38. package/calculations/core/speculator-entry-distance-to-sl-per-leverage.js +75 -90
  39. package/calculations/core/speculator-entry-distance-to-tp-per-leverage.js +74 -89
  40. package/calculations/core/speculator-leverage-per-asset.js +62 -57
  41. package/calculations/core/speculator-leverage-per-sector.js +53 -65
  42. package/calculations/core/speculator-risk-reward-ratio-per-asset.js +71 -76
  43. package/calculations/core/speculator-stop-loss-distance-by-sector-short-long-breakdown.js +60 -81
  44. package/calculations/core/speculator-stop-loss-distance-by-ticker-short-long-breakdown.js +57 -77
  45. package/calculations/core/speculator-stop-loss-per-asset.js +43 -80
  46. package/calculations/core/speculator-take-profit-per-asset.js +45 -69
  47. package/calculations/core/speculator-tsl-per-asset.js +42 -49
  48. package/calculations/core/total-long-figures.js +19 -19
  49. package/calculations/core/total-long-per-sector.js +39 -36
  50. package/calculations/core/total-short-figures.js +19 -19
  51. package/calculations/core/total-short-per-sector.js +39 -36
  52. package/calculations/core/users-processed.js +52 -25
  53. package/calculations/gauss/cohort-capital-flow.js +38 -29
  54. package/calculations/gauss/cohort-definer.js +17 -25
  55. package/calculations/gauss/daily-dna-filter.js +10 -4
  56. package/calculations/gauss/gauss-divergence-signal.js +28 -6
  57. package/calculations/gem/cohort-momentum-state.js +113 -92
  58. package/calculations/gem/cohort-skill-definition.js +23 -53
  59. package/calculations/gem/platform-conviction-divergence.js +62 -116
  60. package/calculations/gem/quant-skill-alpha-signal.js +107 -123
  61. package/calculations/gem/skilled-cohort-flow.js +178 -167
  62. package/calculations/gem/skilled-unskilled-divergence.js +73 -113
  63. package/calculations/gem/unskilled-cohort-flow.js +176 -166
  64. package/calculations/helix/helix-contrarian-signal.js +91 -83
  65. package/calculations/helix/herd-consensus-score.js +135 -97
  66. package/calculations/helix/winner-loser-flow.js +14 -14
  67. package/calculations/pyro/risk-appetite-index.js +121 -123
  68. package/calculations/pyro/squeeze-potential.js +93 -125
  69. package/calculations/pyro/volatility-signal.js +109 -97
  70. package/package.json +9 -9
  71. package/README.MD +0 -78
@@ -1,44 +1,42 @@
1
1
  /**
2
- * @fileoverview HELIX Product Line (Pass 3)
2
+ * @fileoverview HELIX Product Line (Pass 2)
3
3
  *
4
- * This metric answers: "What is the 'Herd Consensus Score' for
5
- * each asset?"
6
- *
7
- * It blends sentiment from three different views of the herd:
8
- * 1. The entire Platform (from gem/platform-conviction-divergence)
9
- * 2. Our 20k User Sample (from core/sentiment-per-stock)
10
- * 3. Social Media (from core/social-sentiment-aggregation)
11
- *
12
- * The score is normalized from -10 (Extreme Bearish Consensus)
13
- * to +10 (Extreme Bullish Consensus).
4
+ * This metric answers: "For each asset, what is the
5
+ * net 'conviction' (avg. position change) of the
6
+ * 'Herd' (in-loss) cohort?"
14
7
  */
8
+ // --- STANDARD 0: REMOVED require('../../utils/sector_mapping_provider') ---
9
+
10
+
15
11
  class HerdConsensusScore {
12
+ constructor() {
13
+ // { [instrumentId]: { ... } }
14
+ this.assetConviction = new Map();
15
+
16
+ // --- STANDARD 0: RENAMED ---
17
+ this.tickerMap = null;
18
+ }
16
19
 
17
20
  /**
18
21
  * Defines the output schema for this calculation.
19
- * @returns {object} JSON Schema object
20
22
  */
21
23
  static getSchema() {
22
24
  const tickerSchema = {
23
25
  "type": "object",
24
26
  "properties": {
25
- "consensus_score": {
27
+ "herd_conviction_score": {
26
28
  "type": "number",
27
- "description": "Blended score (-10 to +10) of platform, sample, and social sentiment."
29
+ "description": "Avg. % change in position size for 'Loser' cohort. Positive = Buying the Dip, Negative = Capitulating."
28
30
  },
29
- "platform_sentiment": { "type": "number" },
30
- "sample_sentiment": { "type": "number" },
31
- "social_sentiment": { "type": "number" }
31
+ "user_count": { "type": "number" }
32
32
  },
33
- "required": ["consensus_score"]
33
+ "required": ["herd_conviction_score", "user_count"]
34
34
  };
35
35
 
36
36
  return {
37
37
  "type": "object",
38
- "description": "Calculates a blended 'Herd Consensus Score' (-10 to +10) per asset.",
39
- "patternProperties": {
40
- "^.*$": tickerSchema // Ticker
41
- },
38
+ "description": "Tracks the change in avg. position size for the 'Loser' (in-loss) cohort.",
39
+ "patternProperties": { "^.*$": tickerSchema },
42
40
  "additionalProperties": tickerSchema
43
41
  };
44
42
  }
@@ -48,10 +46,10 @@ class HerdConsensusScore {
48
46
  */
49
47
  static getMetadata() {
50
48
  return {
51
- type: 'meta',
52
- rootDataDependencies: [],
53
- isHistorical: false,
54
- userType: 'n/a',
49
+ type: 'standard',
50
+ rootDataDependencies: ['portfolio'],
51
+ isHistorical: true, // Needs T-1 portfolio
52
+ userType: 'all',
55
53
  category: 'helix'
56
54
  };
57
55
  }
@@ -60,93 +58,133 @@ class HerdConsensusScore {
60
58
  * Statically declare dependencies.
61
59
  */
62
60
  static getDependencies() {
63
- return [
64
- // This is the only source of *per-ticker* platform sentiment
65
- 'platform-conviction-divergence', // from gem
66
- 'sentiment-per-stock', // from core
67
- 'social-sentiment-aggregation' // from core
68
- ];
69
- }
70
-
71
- /**
72
- * Normalizes a 0-100 value (like platform_long_pct) to -1.0 to +1.0
73
- */
74
- _normalizePercent(v) {
75
- if (v === null || v === undefined) return 0;
76
- const score = (v - 50) / 50; // 50 -> 0, 100 -> 1, 0 -> -1
77
- return Math.max(-1, Math.min(1, score));
61
+ return [];
78
62
  }
79
63
 
80
- /**
81
- * Normalizes a net percent value (-100 to 100) to -1.0 to +1.0
82
- */
83
- _normalizeNetPercent(v) {
84
- if (v === null || v === undefined) return 0;
85
- const score = v / 100;
86
- return Math.max(-1, Math.min(1, score));
64
+ _getPortfolioPositions(portfolio) {
65
+ return portfolio?.AggregatedPositions || portfolio?.PublicPositions;
87
66
  }
88
67
 
89
- /**
90
- * Normalizes a ratio (0 to Inf) to -1.0 to +1.0
91
- * 1.0 -> 0.0 (Neutral)
92
- * 0.5 -> -1.0 (Bearish)
93
- * 2.0 -> +1.0 (Bullish)
94
- */
95
- _normalizeRatio(v) {
96
- if (v === null || v === undefined || v <= 0) return -1.0;
97
- const score = Math.log2(v); // log2(1)=0, log2(0.5)=-1, log2(2)=1
98
- return Math.max(-1, Math.min(1, score));
68
+ _initAsset(instrumentId) {
69
+ if (!this.assetConviction.has(instrumentId)) {
70
+ this.assetConviction.set(instrumentId, {
71
+ total_pos_size_yesterday: 0,
72
+ total_pos_size_today: 0,
73
+ user_count_yesterday: 0,
74
+ user_count_today: 0
75
+ });
76
+ }
99
77
  }
100
78
 
101
- /**
102
- * This is a 'meta' calculation. It runs once.
103
- */
104
- async process(dateStr, dependencies, config, fetchedDependencies) {
105
- const { logger } = dependencies;
79
+ process(todayPortfolio, yesterdayPortfolio, userId, context) {
80
+ if (!todayPortfolio) {
81
+ return;
82
+ }
106
83
 
107
- const platformData = fetchedDependencies['platform-conviction-divergence'];
108
- const sampleData = fetchedDependencies['sentiment-per-stock'];
109
- const socialData = fetchedDependencies['social-sentiment-aggregation']?.per_ticker;
110
-
111
- if (!platformData || !sampleData || !socialData) {
112
- logger.log('WARN', `[helix/herd-consensus-score] Missing dependencies for ${dateStr}. Skipping.`);
113
- return {};
84
+ // --- STANDARD 0: FIXED ---
85
+ if (!this.tickerMap) {
86
+ this.tickerMap = context.instrumentToTicker;
114
87
  }
115
88
 
116
- const allTickers = new Set([
117
- ...Object.keys(platformData),
118
- ...Object.keys(sampleData),
119
- ...Object.keys(socialData)
120
- ]);
121
-
122
- const result = {};
89
+ const tPos = this._getPortfolioPositions(todayPortfolio);
90
+ if (!tPos) return;
123
91
 
124
- for (const ticker of allTickers) {
125
- // 1. Platform Sentiment (0-100) -> -1 to +1
126
- const plat_long_pct = platformData[ticker]?.platform_long_pct;
127
- const s_plat = this._normalizePercent(plat_long_pct);
92
+ // --- Determine Cohort ---
93
+ // We *only* care about users who are *currently* in the
94
+ // 'Loser' cohort for any given asset.
95
+ const tPosMap = new Map(tPos.map(p => [p.InstrumentID, p]));
96
+ const yPosMap = new Map((this._getPortfolioPositions(yesterdayPortfolio) || []).map(p => [p.InstrumentID, p]));
97
+
98
+ const allInstrumentIds = new Set([...yPosMap.keys(), ...tPosMap.keys()]);
99
+ if (allInstrumentIds.size === 0) {
100
+ return;
101
+ }
102
+
103
+ for (const instrumentId of allInstrumentIds) {
104
+ const tP = tPosMap.get(instrumentId);
105
+ const yP = yPosMap.get(instrumentId);
128
106
 
129
- // 2. Sample Sentiment (Ratio) -> -1 to +1
130
- const sample_ratio = sampleData[ticker]?.sentiment_ratio;
131
- const s_samp = this._normalizeRatio(sample_ratio);
107
+ // --- Check Cohort Status (In-Loss) ---
108
+ const tPnl = tP?.NetProfit || 0;
109
+ const isLoser = tPnl < 0;
132
110
 
133
- // 3. Social Sentiment (-100 to 100) -> -1 to +1
134
- const social_pct = socialData[ticker]?.net_sentiment_pct;
135
- const s_soc = this._normalizeNetPercent(social_pct);
136
-
137
- // Blend scores (equal weight) and scale to -10 to +10
138
- const consensus_score = ((s_plat + s_samp + s_soc) / 3) * 10;
111
+ // We only process conviction for assets this user
112
+ // is *currently* losing on.
113
+ if (!isLoser) {
114
+ continue;
115
+ }
139
116
 
140
- result[ticker] = {
141
- consensus_score: consensus_score,
142
- platform_sentiment: s_plat,
143
- sample_sentiment: s_samp,
144
- social_sentiment: s_soc
145
- };
117
+ // --- This user is in the 'Herd' for this asset ---
118
+ this._initAsset(instrumentId);
119
+ const asset = this.assetConviction.get(instrumentId);
120
+
121
+ // ---
122
+ // FIX: The field 'PositionSizeInUnits' does not exist in schema.md.
123
+ // For Normal users, the field is 'Invested'.
124
+ // For Speculator users, the field is 'Amount'.
125
+ // This line now correctly checks for either.
126
+ // ---
127
+ const ySize = yP?.Invested || yP?.Amount || 0;
128
+ const tSize = tP?.Invested || tP?.Amount || 0;
129
+
130
+ if (ySize > 0) {
131
+ asset.total_pos_size_yesterday += ySize;
132
+ asset.user_count_yesterday++;
133
+ }
134
+ if (tSize > 0) {
135
+ asset.total_pos_size_today += tSize;
136
+ asset.user_count_today++;
137
+ }
138
+ }
139
+ }
140
+
141
+ async getResult() {
142
+ // --- STANDARD 0: REMOVED forbidden data load ---
143
+
144
+ // Failsafe check
145
+ if (!this.tickerMap) {
146
+ return {}; // process() must run first
147
+ }
148
+
149
+ const result = {};
150
+ for (const [instrumentId, data] of this.assetConviction.entries()) {
151
+ // --- STANDARD 0: SIMPLIFIED ---
152
+ const ticker = this.tickerMap[instrumentId];
153
+ if (!ticker) continue;
154
+
155
+ const {
156
+ total_pos_size_yesterday, total_pos_size_today,
157
+ user_count_yesterday, user_count_today
158
+ } = data;
159
+
160
+ let avg_position_change_pct = 0;
161
+
162
+ if (user_count_yesterday > 0 && user_count_today > 0) {
163
+ const avg_pos_y = total_pos_size_yesterday / user_count_yesterday;
164
+ const avg_pos_t = total_pos_size_today / user_count_today;
165
+ if (avg_pos_y > 0) {
166
+ avg_position_change_pct = ((avg_pos_t - avg_pos_y) / avg_pos_y) * 100;
167
+ }
168
+ } else if (user_count_today > 0) {
169
+ // Pure inflow, conviction is max
170
+ avg_position_change_pct = Infinity;
171
+ }
172
+
173
+ if (isFinite(avg_position_change_pct) && user_count_today > 0) {
174
+ result[ticker] = {
175
+ herd_conviction_score: avg_position_change_pct,
176
+ user_count: user_count_today
177
+ };
178
+ }
146
179
  }
147
-
148
180
  return result;
149
181
  }
182
+
183
+ reset() {
184
+ this.assetConviction.clear();
185
+ // --- STANDARD 0: RENAMED ---
186
+ this.tickerMap = null;
187
+ }
150
188
  }
151
189
 
152
190
  module.exports = HerdConsensusScore;
@@ -10,14 +10,15 @@
10
10
  * This calculation now determines "winner/loser" status internally
11
11
  * by reading the user's portfolio P&L directly.
12
12
  */
13
- const { loadInstrumentMappings } = require('../../utils/sector_mapping_provider');
13
+ // --- STANDARD 0: REMOVED require('../../utils/sector_mapping_provider') ---
14
14
 
15
15
 
16
16
  class WinnerLoserFlow {
17
17
  constructor() {
18
18
  // We will store { [ticker]: { winners_joined: 0, ... } }
19
19
  this.assetFlows = new Map();
20
- this.mappings = null; // Caches mappings
20
+ // --- STANDARD 0: RENAMED ---
21
+ this.tickerMap = null; // Caches mappings
21
22
  }
22
23
 
23
24
  /**
@@ -74,15 +75,12 @@ class WinnerLoserFlow {
74
75
 
75
76
  /**
76
77
  * Statically declare dependencies.
77
- * --- MODIFIED ---
78
- * Removed 'asset-pnl-status' dependency.
79
78
  */
80
79
  static getDependencies() {
81
80
  return [];
82
81
  }
83
82
 
84
83
  /**
85
- * --- MODIFIED ---
86
84
  * Helper to get a user's holdings (Instrument IDs) AND their P&L.
87
85
  * @returns {Map<InstrumentID, {pnl: number}>}
88
86
  */
@@ -119,8 +117,13 @@ class WinnerLoserFlow {
119
117
  return;
120
118
  }
121
119
 
122
- if (!this.mappings) {
123
- this.mappings = context.mappings;
120
+ // --- STANDARD 0: FIXED ---
121
+ if (!this.tickerMap) {
122
+ this.tickerMap = context.instrumentToTicker;
123
+ }
124
+
125
+ if (!this.tickerMap) {
126
+ return; // Failsafe if context is missing map
124
127
  }
125
128
 
126
129
  const yHoldings = this._getHoldingsWithPnl(yesterdayPortfolio); // Map<InstID, {pnl}>
@@ -132,17 +135,14 @@ class WinnerLoserFlow {
132
135
  }
133
136
 
134
137
  for (const instrumentId of allInstrumentIds) {
135
- const ticker = this.mappings.instrumentToTicker[instrumentId];
138
+ // --- STANDARD 0: FIXED ---
139
+ const ticker = this.tickerMap[instrumentId];
136
140
  if (!ticker) continue;
137
141
 
138
- // --- MODIFIED ---
139
- // Determine cohort status from today's portfolio.
140
142
  const tPnl = tHoldings.get(instrumentId)?.pnl;
141
143
  const isWinner = tPnl > 0;
142
144
  const isLoser = tPnl < 0;
143
- // --- END MODIFIED ---
144
145
 
145
- // We only care about users who are *currently* in a cohort
146
146
  if (!isWinner && !isLoser) {
147
147
  continue;
148
148
  }
@@ -150,7 +150,6 @@ class WinnerLoserFlow {
150
150
  const heldYesterday = yHoldings.has(instrumentId);
151
151
  const holdsToday = tHoldings.has(instrumentId);
152
152
 
153
- // Skip if no change in holding status
154
153
  if (heldYesterday === holdsToday) {
155
154
  continue;
156
155
  }
@@ -183,7 +182,8 @@ class WinnerLoserFlow {
183
182
 
184
183
  reset() {
185
184
  this.assetFlows.clear();
186
- this.mappings = null;
185
+ // --- STANDARD 0: RENAMED ---
186
+ this.tickerMap = null;
187
187
  }
188
188
  }
189
189
 
@@ -1,153 +1,151 @@
1
1
  /**
2
2
  * @fileoverview PYRO Product Line (Pass 2)
3
- *
4
- * This metric answers: "For each asset, what is the 'Risk-On'
5
- * score from speculators?"
6
- *
7
- * It combines the average leverage being used with the
8
- * average Risk/Reward ratio being set. A high score means
9
- * speculators are confident: using high leverage *and*
10
- * setting wide R/R targets.
3
+ * --- FIX ---
4
+ * - Updated 'process' signature to the 7-arg standard to get context.
5
+ * - Logic is already schema-compliant (uses pos.Leverage).
11
6
  */
7
+
12
8
  class RiskAppetiteIndex {
9
+ constructor() {
10
+ this.tickerLeverage = new Map();
11
+ this.sectorLeverage = new Map();
12
+ this.tickerMap = null;
13
+ this.sectorMap = null;
14
+ }
15
+
16
+ static getMetadata() {
17
+ return {
18
+ type: 'standard',
19
+ rootDataDependencies: ['portfolio'],
20
+ isHistorical: false,
21
+ userType: 'speculator', // Only processes speculators
22
+ category: 'pyro'
23
+ };
24
+ }
25
+
26
+ static getDependencies() {
27
+ return [];
28
+ }
13
29
 
14
- /**
15
- * Defines the output schema for this calculation.
16
- * @returns {object} JSON Schema object
17
- */
18
30
  static getSchema() {
19
- const tickerSchema = {
31
+ const schema = {
20
32
  "type": "object",
21
33
  "properties": {
22
- "risk_appetite_score": {
23
- "type": "number",
24
- "description": "Blended score (0-10) of leverage use and R/R conviction."
25
- },
26
- "avg_leverage": {
27
- "type": "number",
28
- "description": "Weighted average leverage used on this asset."
29
- },
30
- "avg_rr_ratio": {
31
- "type": "number",
32
- "description": "Average R/R ratio set on this asset."
33
- }
34
+ "avg_leverage": { "type": "number" },
35
+ "user_count": { "type": "number" }
34
36
  },
35
- "required": ["risk_appetite_score", "avg_leverage", "avg_rr_ratio"]
37
+ "required": ["avg_leverage", "user_count"]
36
38
  };
37
-
39
+
38
40
  return {
39
41
  "type": "object",
40
- "description": "Calculates a 'Risk-On' score for speculators per asset.",
41
- "patternProperties": {
42
- "^.*$": tickerSchema // Ticker
42
+ "description": "Calculates the average leverage used by speculators, aggregated by ticker and sector.",
43
+ "properties": {
44
+ "by_ticker": { "type": "object", "patternProperties": { "^.*$": schema }, "additionalProperties": schema },
45
+ "by_sector": { "type": "object", "patternProperties": { "^.*$": schema }, "additionalProperties": schema }
43
46
  },
44
- "additionalProperties": tickerSchema
47
+ "required": ["by_ticker", "by_sector"]
45
48
  };
46
49
  }
47
50
 
48
- /**
49
- * Statically defines all metadata for the manifest builder.
50
- */
51
- static getMetadata() {
52
- return {
53
- type: 'meta',
54
- rootDataDependencies: [],
55
- isHistorical: false,
56
- userType: 'n/a',
57
- category: 'pyro'
58
- };
51
+ _init(map, key) {
52
+ if (!map.has(key)) {
53
+ map.set(key, { leverage_sum: 0, user_count: 0 });
54
+ }
59
55
  }
60
56
 
61
- /**
62
- * Statically declare dependencies.
63
- */
64
- static getDependencies() {
65
- return [
66
- 'speculator-leverage-per-asset', // from core
67
- 'speculator-risk-reward-ratio-per-asset' // from core
68
- ];
69
- }
70
-
71
- /**
72
- * Helper to calculate weighted average leverage from buckets.
73
- * e.g., { "1x": 10, "5x": 5 }
74
- */
75
- _calculateAvgLeverage(levBuckets) {
76
- if (!levBuckets || typeof levBuckets !== 'object') {
77
- return 1; // Default to 1x leverage
57
+ // --- THIS IS THE FIX ---
58
+ // Updated signature to match the test worker's standard 7-arg call
59
+ process(todayPortfolio, yesterdayPortfolio, userId, context, todayInsights, yesterdayInsights, fetchedDependencies) {
60
+ // --- END FIX ---
61
+
62
+ if (!todayPortfolio?.PublicPositions) {
63
+ return; // Not a speculator or no positions
78
64
  }
79
-
80
- let totalCount = 0;
81
- let weightedSum = 0;
82
-
83
- for (const [key, count] of Object.entries(levBuckets)) {
84
- const leverage = parseInt(key, 10); // "5x" -> 5
85
- if (!isNaN(leverage) && count > 0) {
86
- totalCount += count;
87
- weightedSum += (leverage * count);
88
- }
65
+
66
+ if (!this.tickerMap) {
67
+ this.tickerMap = context.instrumentToTicker;
68
+ this.sectorMap = context.sectorMapping;
69
+ }
70
+
71
+ if (!this.tickerMap || !this.sectorMap) {
72
+ return;
89
73
  }
90
-
91
- if (totalCount === 0) {
92
- return 1; // Default to 1x if no positions
74
+
75
+ const positions = todayPortfolio.PublicPositions;
76
+
77
+ const seenTickers = new Set();
78
+ const seenSectors = new Set();
79
+
80
+ for (const pos of positions) {
81
+ if (!pos.InstrumentID || !pos.Leverage) continue;
82
+
83
+ const leverage = pos.Leverage;
84
+ if (leverage <= 1) continue; // Only care about leveraged positions
85
+
86
+ const ticker = this.tickerMap[pos.InstrumentID];
87
+ const sector = this.sectorMap[pos.InstrumentID];
88
+
89
+ if (ticker) {
90
+ this._init(this.tickerLeverage, ticker);
91
+ const asset = this.tickerLeverage.get(ticker);
92
+ asset.leverage_sum += leverage;
93
+
94
+ if (!seenTickers.has(ticker)) {
95
+ asset.user_count++;
96
+ seenTickers.add(ticker);
97
+ }
98
+ }
99
+
100
+ if (sector) {
101
+ this._init(this.sectorLeverage, sector);
102
+ const sec = this.sectorLeverage.get(sector);
103
+ sec.leverage_sum += leverage;
104
+
105
+ if (!seenSectors.has(sector)) {
106
+ sec.user_count++;
107
+ seenSectors.add(sector);
108
+ }
109
+ }
93
110
  }
94
- return weightedSum / totalCount;
95
- }
96
-
97
- /**
98
- * Simple min-max normalization, clamped 0-1.
99
- */
100
- _normalize(value, min, max) {
101
- const normalized = (value - min) / (max - min);
102
- return Math.max(0, Math.min(1, normalized));
103
111
  }
104
112
 
105
- /**
106
- * This is a 'meta' calculation. It runs once.
107
- * @param {string} dateStr - The date string 'YYYY-MM-DD'.
108
- * @param {object} dependencies - The shared dependencies (e.g., logger).
109
- * @param {object} config - The computation system configuration.
110
- * @param {object} fetchedDependencies - Results from Pass 1.
111
- * @returns {Promise<object>} The calculation result.
112
- */
113
- async process(dateStr, dependencies, config, fetchedDependencies) {
114
- const { logger } = dependencies;
115
-
116
- const leverageData = fetchedDependencies['speculator-leverage-per-asset'];
117
- const rrData = fetchedDependencies['speculator-risk-reward-ratio-per-asset'];
118
-
119
- if (!leverageData || !rrData) {
120
- logger.log('WARN', `[pyro/risk-appetite-index] Missing core dependencies for ${dateStr}. Skipping.`);
121
- return {};
113
+ async getResult() {
114
+ if (!this.tickerMap) {
115
+ return { by_ticker: {}, by_sector: {} };
116
+ }
117
+
118
+ const result = {
119
+ by_ticker: {},
120
+ by_sector: {}
121
+ };
122
+
123
+ for (const [ticker, data] of this.tickerLeverage.entries()) {
124
+ if (data.user_count > 0) {
125
+ result.by_ticker[ticker] = {
126
+ avg_leverage: data.leverage_sum / data.user_count,
127
+ user_count: data.user_count
128
+ };
129
+ }
122
130
  }
123
131
 
124
- const allTickers = new Set([
125
- ...Object.keys(leverageData),
126
- ...Object.keys(rrData)
127
- ]);
128
-
129
- const result = {};
130
-
131
- for (const ticker of allTickers) {
132
- const avg_leverage = this._calculateAvgLeverage(leverageData[ticker]);
133
- const avg_rr_ratio = rrData[ticker]?.avg_rr_ratio || 0;
134
-
135
- // Normalize: Leverage from 1x-10x, R/R from 0-5
136
- const norm_lev = this._normalize(avg_leverage, 1, 10);
137
- const norm_rr = this._normalize(avg_rr_ratio, 0, 5);
138
-
139
- // Combine scores and scale to 0-10
140
- const risk_appetite_score = (norm_lev * 0.6 + norm_rr * 0.4) * 10;
141
-
142
- result[ticker] = {
143
- risk_appetite_score: risk_appetite_score,
144
- avg_leverage: avg_leverage,
145
- avg_rr_ratio: avg_rr_ratio
146
- };
132
+ for (const [sector, data] of this.sectorLeverage.entries()) {
133
+ if (data.user_count > 0) {
134
+ result.by_sector[sector] = {
135
+ avg_leverage: data.leverage_sum / data.user_count,
136
+ user_count: data.user_count
137
+ };
138
+ }
147
139
  }
148
-
140
+
149
141
  return result;
150
142
  }
151
- }
152
143
 
144
+ reset() {
145
+ this.tickerLeverage.clear();
146
+ this.sectorLeverage.clear();
147
+ this.tickerMap = null;
148
+ this.sectorMap = null;
149
+ }
150
+ }
153
151
  module.exports = RiskAppetiteIndex;