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,20 +1,13 @@
1
1
  /**
2
2
  * @fileoverview GEM Product Line (Pass 2)
3
- * --- FIX ---
4
- * - Added defensive checks in '_loadDependencies' to prevent crash
5
- * when dependencies fail to load (e.g., due to worker bug).
6
- * - **FIXED:** Restored logic to calculate 'avg_position_change_pct'
7
- * to resolve the hardcoded '0' value. This involves
8
- * re-adding fields to '_initFlowData' and 'process'.
3
+ * REFACTORED: Uses context.computed and context.math.extract.
9
4
  */
10
-
11
5
  class UnskilledCohortFlow {
12
6
  constructor() {
13
7
  this.assetFlows = new Map();
14
8
  this.cohortMap = new Map();
15
- this.tickerMap = null;
16
9
  this.dependenciesLoaded = false;
17
- this.priceChangeMap = null;
10
+ this.tickerMap = null;
18
11
  }
19
12
 
20
13
  static getMetadata() {
@@ -28,10 +21,7 @@ class UnskilledCohortFlow {
28
21
  }
29
22
 
30
23
  static getDependencies() {
31
- return [
32
- 'cohort-skill-definition', // from gem (Pass 1)
33
- 'instrument-price-change-1d' // from core (Pass 1)
34
- ];
24
+ return ['cohort-skill-definition', 'instrument-price-change-1d'];
35
25
  }
36
26
 
37
27
  static getSchema() {
@@ -45,206 +35,108 @@ class UnskilledCohortFlow {
45
35
  },
46
36
  "required": ["net_flow_pct", "net_flow_contribution", "avg_position_change_pct", "user_count"]
47
37
  };
48
-
49
- return {
50
- "type": "object",
51
- "description": "Calculates capital flow and conviction change for the 'Unskilled' cohort.",
52
- "patternProperties": { "^.*$": tickerSchema },
53
- "additionalProperties": tickerSchema
54
- };
38
+ return { "type": "object", "patternProperties": { "^.*$": tickerSchema } };
55
39
  }
56
40
 
57
- _getPortfolioPositions(portfolio) {
58
- // --- FIX: Support both Normal (Aggregated) and Speculator (Public) ---
59
- return portfolio?.AggregatedPositions || portfolio?.PublicPositions;
60
- }
61
-
62
- // --- THIS IS THE FIX (Part 1) ---
63
41
  _initFlowData(instrumentId) {
64
42
  if (!this.assetFlows.has(instrumentId)) {
65
43
  this.assetFlows.set(instrumentId, {
66
- total_invested_yesterday: 0,
67
- total_invested_today: 0,
68
- price_change_yesterday: 0,
69
- // Restore fields needed for avg_position_change_pct
70
- total_pos_size_yesterday: 0,
71
- total_pos_size_today: 0,
72
- user_count_yesterday: 0,
73
- user_count_today: 0
44
+ total_invested_yesterday: 0, total_invested_today: 0, price_change_yesterday: 0,
45
+ total_pos_size_yesterday: 0, total_pos_size_today: 0, user_count_yesterday: 0, user_count_today: 0
74
46
  });
75
47
  }
76
48
  }
77
- // --- END FIX (Part 1) ---
78
49
 
79
- _loadDependencies(fetchedDependencies) {
50
+ _loadDependencies(computed) {
80
51
  if (this.dependenciesLoaded) return;
81
-
82
- if (!fetchedDependencies) {
83
- throw new Error(
84
- `[unskilled-cohort-flow] CRITICAL ERROR: fetchedDependencies object was UNDEFINED.
85
- This means a dependency test (like 'cohort-skill-definition' or 'instrument-price-change-1d')
86
- failed to run or produced an invalid result.`
87
- );
88
- }
89
-
90
- const cohortData = fetchedDependencies['cohort-skill-definition'];
91
- if (!cohortData) {
92
- throw new Error(
93
- `[unskilled-cohort-flow] DEPENDENCY ERROR: 'cohort-skill-definition' was missing.
94
- Check logs for errors in that calculation.`
95
- );
96
- }
97
-
98
- // This logic is correct for 'cohort-skill-definition' output
99
- if (cohortData && cohortData.unskilled_user_ids) {
52
+ const cohortData = computed['cohort-skill-definition'];
53
+ if (cohortData) {
100
54
  (cohortData.unskilled_user_ids || []).forEach(uid => this.cohortMap.set(String(uid), 'unskilled'));
101
55
  }
102
-
103
- this.priceChangeMap = fetchedDependencies['instrument-price-change-1d'];
104
- if (!this.priceChangeMap) {
105
- throw new Error(
106
- `[unskilled-cohort-flow] DEPENDENCY ERROR: 'instrument-price-change-1d' was missing.
107
- This is likely due to the worker bug for 'price' dependencies.`
108
- );
109
- }
110
-
111
56
  this.dependenciesLoaded = true;
112
57
  }
113
58
 
114
- // --- THIS IS THE FIX (Part 2) ---
115
- process(todayPortfolio, yesterdayPortfolio, userId, context, todayInsights, yesterdayInsights, fetchedDependencies) {
116
- this._loadDependencies(fetchedDependencies);
117
-
118
- if (!this.tickerMap) {
119
- this.tickerMap = context.instrumentToTicker;
120
- }
121
-
122
- const cohortName = this.cohortMap.get(String(userId));
123
- if (cohortName !== 'unskilled') {
124
- return; // Not in unskilled cohort
125
- }
59
+ process(context) {
60
+ const { user, computed, mappings, math } = context;
61
+ const { extract } = math;
62
+
63
+ this._loadDependencies(computed);
64
+ if (!this.tickerMap) this.tickerMap = mappings.instrumentToTicker;
126
65
 
127
- if (!todayPortfolio || !yesterdayPortfolio || !this.priceChangeMap || !this.tickerMap) {
128
- return;
129
- }
66
+ if (this.cohortMap.get(user.id) !== 'unskilled') return;
67
+
68
+ const priceChangeMap = computed['instrument-price-change-1d'];
69
+ if (!priceChangeMap) return;
130
70
 
131
- const yPos = this._getPortfolioPositions(yesterdayPortfolio);
132
- const tPos = this._getPortfolioPositions(todayPortfolio);
133
- if (!yPos || !tPos) return;
71
+ const yPos = extract.getPositions(user.portfolio.yesterday, user.type);
72
+ const tPos = extract.getPositions(user.portfolio.today, user.type);
134
73
 
135
- const yPosMap = new Map(yPos.map(p => [p.InstrumentID, p]));
136
- const tPosMap = new Map(tPos.map(p => [p.InstrumentID, p]));
74
+ const yPosMap = new Map(yPos.map(p => [extract.getInstrumentId(p), p]));
75
+ const tPosMap = new Map(tPos.map(p => [extract.getInstrumentId(p), p]));
137
76
  const allInstrumentIds = new Set([...yPosMap.keys(), ...tPosMap.keys()]);
138
77
 
139
- for (const instrumentId of allInstrumentIds) {
140
- if (!instrumentId) continue;
141
-
142
- this._initFlowData(instrumentId);
143
- const asset = this.assetFlows.get(instrumentId);
144
-
145
- const yP = yPosMap.get(instrumentId);
146
- const tP = tPosMap.get(instrumentId);
147
-
148
- // Get $ flow
149
- const yInvested = yP?.Invested || yP?.Amount || 0;
150
- const tInvested = tP?.Invested || tP?.Amount || 0;
151
-
152
- // Get position size (same as $ flow for these schemas)
153
- const ySize = yP?.Invested || yP?.Amount || 0;
154
- const tSize = tP?.Invested || tP?.Amount || 0;
78
+ for (const instId of allInstrumentIds) {
79
+ if (!instId) continue;
80
+ this._initFlowData(instId);
81
+ const asset = this.assetFlows.get(instId);
155
82
 
156
- if (yInvested > 0) {
157
- asset.total_invested_yesterday += yInvested;
83
+ const yWeight = extract.getPositionWeight(yPosMap.get(instId), user.type);
84
+ const tWeight = extract.getPositionWeight(tPosMap.get(instId), user.type);
158
85
 
159
- const ticker = this.tickerMap[instrumentId];
160
- const yPriceChange_pct = (ticker && this.priceChangeMap[ticker])
161
- ? this.priceChangeMap[ticker].price_change_1d_pct
162
- : 0;
163
-
164
- const yPriceChange_decimal = (yPriceChange_pct || 0) / 100.0;
165
- asset.price_change_yesterday += yPriceChange_decimal * yInvested;
166
-
167
- // Track yesterday's size and user count
168
- asset.total_pos_size_yesterday += ySize;
86
+ if (yWeight > 0) {
87
+ asset.total_invested_yesterday += yWeight;
88
+ const ticker = mappings.instrumentToTicker[instId];
89
+ const priceChange = (ticker && priceChangeMap[ticker]) ? priceChangeMap[ticker].change_1d_pct : 0;
90
+ asset.price_change_yesterday += (priceChange / 100.0) * yWeight;
91
+ asset.total_pos_size_yesterday += yWeight;
169
92
  asset.user_count_yesterday++;
170
93
  }
171
- if (tInvested > 0) {
172
- asset.total_invested_today += tInvested;
94
+ if (tWeight > 0) {
95
+ asset.total_invested_today += tWeight;
173
96
  asset.user_count_today++;
174
-
175
- // Track today's size
176
- asset.total_pos_size_today += tSize;
97
+ asset.total_pos_size_today += tWeight;
177
98
  }
178
99
  }
179
100
  }
180
- // --- END FIX (Part 2) ---
181
101
 
182
- // --- THIS IS THE FIX (Part 3) ---
183
102
  async getResult() {
184
- if (!this.tickerMap) {
185
- return {};
186
- }
187
-
103
+ if (!this.tickerMap) return {};
188
104
  const finalResult = {};
189
-
190
- for (const [instrumentId, data] of this.assetFlows.entries()) {
191
- const ticker = this.tickerMap[instrumentId];
105
+ for (const [instId, data] of this.assetFlows.entries()) {
106
+ const ticker = this.tickerMap[instId];
192
107
  if (!ticker) continue;
193
-
194
- const {
195
- total_invested_yesterday, total_invested_today, price_change_yesterday,
196
- // Destructure the restored fields
197
- total_pos_size_yesterday, total_pos_size_today,
198
- user_count_yesterday, user_count_today
199
- } = data;
200
-
201
- let net_flow_percentage = 0;
202
- let flow_contribution = 0;
203
-
204
- if (total_invested_yesterday > 0) {
205
- const avg_price_change_decimal = (price_change_yesterday === 0) ? 0 : (price_change_yesterday / total_invested_yesterday);
206
- const price_adjusted_yesterday_value = total_invested_yesterday * (1 + avg_price_change_decimal);
207
-
208
- flow_contribution = total_invested_today - price_adjusted_yesterday_value;
209
- net_flow_percentage = (flow_contribution / total_invested_yesterday) * 100;
210
- } else if (total_invested_today > 0) {
211
- flow_contribution = total_invested_today;
212
- net_flow_percentage = Infinity;
213
- }
214
-
215
- // Restore the calculation logic for conviction
216
- let avg_position_change_pct = 0;
217
- if (user_count_yesterday > 0 && user_count_today > 0) {
218
- const avg_pos_y = total_pos_size_yesterday / user_count_yesterday;
219
- const avg_pos_t = total_pos_size_today / user_count_today;
220
- if (avg_pos_y > 0) {
221
- avg_position_change_pct = ((avg_pos_t - avg_pos_y) / avg_pos_y) * 100;
222
- }
223
- } else if (user_count_today > 0) {
224
- // Pure inflow, conviction is max
225
- avg_position_change_pct = Infinity;
226
- }
227
-
228
- if (isFinite(net_flow_percentage) && isFinite(flow_contribution)) {
229
- finalResult[ticker] = {
230
- net_flow_pct: net_flow_percentage,
231
- net_flow_contribution: flow_contribution,
232
- avg_position_change_pct: avg_position_change_pct, // Now correctly calculated
233
- user_count: user_count_today
234
- };
108
+ let net_flow = 0, flow_contrib = 0;
109
+ if (data.total_invested_yesterday > 0) {
110
+ const avg_change = data.price_change_yesterday / data.total_invested_yesterday;
111
+ const adjusted_y = data.total_invested_yesterday * (1 + avg_change);
112
+ flow_contrib = data.total_invested_today - adjusted_y;
113
+ net_flow = (flow_contrib / data.total_invested_yesterday) * 100;
114
+ } else if (data.total_invested_today > 0) {
115
+ flow_contrib = data.total_invested_today;
116
+ net_flow = Infinity;
235
117
  }
118
+ let avg_pos_change = 0;
119
+ if (data.user_count_yesterday > 0 && data.user_count_today > 0) {
120
+ const avg_y = data.total_pos_size_yesterday / data.user_count_yesterday;
121
+ const avg_t = data.total_pos_size_today / data.user_count_today;
122
+ if (avg_y > 0) avg_pos_change = ((avg_t - avg_y) / avg_y) * 100;
123
+ } else if (data.user_count_today > 0) avg_pos_change = Infinity;
124
+
125
+ finalResult[ticker] = {
126
+ net_flow_pct: isFinite(net_flow) ? net_flow : 0,
127
+ net_flow_contribution: isFinite(flow_contrib) ? flow_contrib : 0,
128
+ avg_position_change_pct: isFinite(avg_pos_change) ? avg_pos_change : 0,
129
+ user_count: data.user_count_today
130
+ };
236
131
  }
237
-
238
132
  return finalResult;
239
133
  }
240
- // --- END FIX (Part 3) ---
241
134
 
242
135
  reset() {
243
136
  this.assetFlows.clear();
244
137
  this.cohortMap.clear();
245
- this.tickerMap = null;
246
138
  this.dependenciesLoaded = false;
247
- this.priceChangeMap = null;
139
+ this.tickerMap = null;
248
140
  }
249
141
  }
250
142
  module.exports = UnskilledCohortFlow;
@@ -1,49 +1,24 @@
1
1
  /**
2
2
  * @fileoverview HELIX Product Line (Pass 4)
3
- *
4
- * This is the final, stateless signal generator for the HELIX line.
5
- * It answers: "Are 'Winners' (in-profit) and 'Losers'
6
- * (in-loss) cohorts flowing in opposite directions?"
3
+ * REFACTORED: Uses context.math.signals and process(context).
7
4
  */
8
5
  class HelixContrarianSignal {
6
+ constructor() { this.result = {}; }
9
7
 
10
- // --- STANDARD 2: ADDED ---
11
- constructor() {
12
- this.result = {};
13
- }
14
-
15
- /**
16
- * Defines the output schema for this calculation.
17
- */
18
8
  static getSchema() {
19
9
  const tickerSchema = {
20
10
  "type": "object",
21
11
  "properties": {
22
- "signal": {
23
- "type": "string",
24
- "enum": ["Strong Contrarian", "Contrarian", "Neutral", "Consensus"]
25
- },
26
- "helix_score": {
27
- "type": "number",
28
- "description": "Divergence score (-10 to +10). Positive = Contrarian (Winners sell, Losers buy), Negative = Consensus (Both buy/sell)."
29
- },
12
+ "signal": { "type": "string" },
13
+ "helix_score": { "type": "number" },
30
14
  "winner_net_flow": { "type": "number" },
31
15
  "loser_net_flow": { "type": "number" }
32
16
  },
33
- "required": ["signal", "helix_score", "winner_net_flow", "loser_net_flow"]
34
- };
35
-
36
- return {
37
- "type": "object",
38
- "description": "Generates a final contrarian signal based on Winner vs. Loser flow.",
39
- "patternProperties": { "^.*$": tickerSchema },
40
- "additionalProperties": tickerSchema
17
+ "required": ["signal", "helix_score"]
41
18
  };
19
+ return { "type": "object", "patternProperties": { "^.*$": tickerSchema } };
42
20
  }
43
21
 
44
- /**
45
- * Statically defines all metadata for the manifest builder.
46
- */
47
22
  static getMetadata() {
48
23
  return {
49
24
  type: 'meta',
@@ -54,109 +29,40 @@ class HelixContrarianSignal {
54
29
  };
55
30
  }
56
31
 
57
- /**
58
- * Statically declare dependencies.
59
- */
60
32
  static getDependencies() {
61
- return [
62
- 'winner-loser-flow', // from helix (Pass 2)
63
- 'herd-consensus-score' // from helix (Pass 2)
64
- ];
33
+ return ['winner-loser-flow', 'herd-consensus-score'];
65
34
  }
66
35
 
67
- /**
68
- * Simple tanh normalization. Scales any number to a -10 to +10 range.
69
- */
70
- _normalize(score) {
71
- // Scale input score to tune sensitivity
72
- // A score of 50 will be ~9.9 (strong signal)
73
- // A score of 20 will be ~9.6
74
- // A score of 10 will be ~7.6
75
- return Math.tanh(score / 10.0) * 10;
76
- }
77
-
78
- /**
79
- * This is a 'meta' calculation. It runs once.
80
- */
81
- // --- STANDARD 1: UPDATED SIGNATURE (added rootData) ---
82
- async process(dateStr, rootData, dependencies, config, fetchedDependencies) {
83
- const { logger } = dependencies;
36
+ process(context) {
37
+ const { computed, math } = context;
38
+ const { signals } = math;
84
39
 
85
- const cohortFlows = fetchedDependencies['winner-loser-flow'];
86
- const herdConviction = fetchedDependencies['herd-consensus-score'];
87
-
88
- if (!cohortFlows || !herdConviction) {
89
- logger.log('WARN', `[helix/helix-contrarian-signal] Missing dependencies for ${dateStr}.`);
90
- // --- STANDARD 2: DO NOT RETURN ---
91
- this.result = {};
92
- return;
93
- }
94
-
95
- const allTickers = new Set([
96
- ...Object.keys(cohortFlows),
97
- ...Object.keys(herdConviction)
98
- ]);
99
-
40
+ const tickers = signals.getUnionKeys(computed, ['winner-loser-flow', 'herd-consensus-score']);
100
41
  const result = {};
101
42
 
102
- for (const ticker of allTickers) {
103
- const flow = cohortFlows[ticker];
104
- const conviction = herdConviction[ticker];
43
+ for (const ticker of tickers) {
44
+ const winner_flow = signals.getMetric(computed, 'winner-loser-flow', ticker, 'net_winner_flow');
45
+ const loser_flow = signals.getMetric(computed, 'herd-consensus-score', ticker, 'herd_conviction_score');
105
46
 
106
- // 1. Get Winner Flow (using net_winner_flow)
107
- const winner_flow = flow?.net_winner_flow || 0;
108
-
109
- // 2. Get Loser Flow (using herd_conviction_score)
110
- // This is a proxy for "Loser Flow".
111
- // If conviction > 0, losers are buying.
112
- // If conviction < 0, losers are selling.
113
- const loser_flow = conviction?.herd_conviction_score || 0;
114
-
115
- // --- Signal Logic ---
116
- // A contrarian signal (positive score) happens when:
117
- // 1. Winners SELL (flow < 0) and Losers BUY (flow > 0)
118
- //
119
- // A consensus signal (negative score) happens when:
120
- // 2. Winners BUY (flow > 0) and Losers BUY (flow > 0)
121
- // 3. Winners SELL (flow < 0) and Losers SELL (flow < 0)
122
-
123
- // We can simplify this:
124
- // Score = Loser Flow - Winner Flow
125
- //
126
- // Case 1: Loser (Buy, +50) - Winner (Sell, -20) = +70 (Strong Contrarian)
127
- // Case 2: Loser (Buy, +50) - Winner (Buy, +20) = +30 (Weak Contrarian)
128
- // Case 3: Loser (Sell, -50) - Winner (Sell, -20) = -30 (Consensus)
129
- // Case 4: Loser (Sell, -50) - Winner (Buy, +20) = -70 (Strong Consensus)
130
-
131
47
  const divergence = loser_flow - winner_flow;
132
- const helix_score = this._normalize(divergence);
48
+ const helix_score = signals.normalizeTanh(divergence, 10, 10.0);
133
49
 
134
50
  let signal = "Neutral";
135
- if (helix_score > 7.5) signal = "Strong Contrarian"; // Losers buying, Winners selling
51
+ if (helix_score > 7.5) signal = "Strong Contrarian";
136
52
  else if (helix_score > 2.5) signal = "Contrarian";
137
- else if (helix_score < -2.5) signal = "Consensus"; // Both moving same way
53
+ else if (helix_score < -2.5) signal = "Consensus";
138
54
 
139
55
  result[ticker] = {
140
56
  signal: signal,
141
57
  helix_score: helix_score,
142
58
  winner_net_flow: winner_flow,
143
- loser_net_flow: loser_flow // Using conviction as flow proxy
59
+ loser_net_flow: loser_flow
144
60
  };
145
61
  }
146
-
147
- // --- STANDARD 2: SET STATE, DO NOT RETURN ---
148
62
  this.result = result;
149
63
  }
150
64
 
151
- // --- STANDARD 2: ADDED ---
152
- async getResult(fetchedDependencies) {
153
- return this.result;
154
- }
155
-
156
- // --- STANDARD 2: ADDED ---
157
- reset() {
158
- this.result = {};
159
- }
65
+ async getResult() { return this.result; }
66
+ reset() { this.result = {}; }
160
67
  }
161
-
162
68
  module.exports = HelixContrarianSignal;