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,240 +1,250 @@
1
1
  /**
2
- * @fileoverview Calculation (Pass 2) for "Unskilled Cohort" flow.
3
- *
4
- * This metric calculates the "Net Crowd Flow Percentage" for the
5
- * "Unskilled Cohort" (bottom 20% by long-term skill score).
6
- *
7
- * This calculation *depends* on 'cohort-skill-definition'
8
- * to identify the cohort.
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'.
9
9
  */
10
- const { loadInstrumentMappings } = require('../../utils/sector_mapping_provider');
11
-
12
10
 
13
11
  class UnskilledCohortFlow {
14
12
  constructor() {
15
- this.assetData = new Map();
16
- this.sectorData = new Map();
17
- this.mappings = null;
18
- this.unskilledCohortUserIds = null;
19
- }
20
-
21
- /**
22
- * Defines the output schema for this calculation.
23
- * @returns {object} JSON Schema object
24
- */
25
- static getSchema() {
26
- const flowSchema = {
27
- "type": "object",
28
- "properties": {
29
- "net_flow_percentage": { "type": "number" },
30
- "total_invested_today": { "type": "number" },
31
- "total_invested_yesterday": { "type": "number" }
32
- },
33
- "required": ["net_flow_percentage", "total_invested_today", "total_invested_yesterday"]
34
- };
35
-
36
- return {
37
- "type": "object",
38
- "description": "Calculates net capital flow % (price-adjusted) for the 'Unskilled Cohort' (bottom 20% by skill score), aggregated by asset and sector.",
39
- "properties": {
40
- "cohort_size": {
41
- "type": "number",
42
- "description": "The number of users identified as being in the Unskilled Cohort."
43
- },
44
- "assets": {
45
- "type": "object",
46
- "description": "Price-adjusted net flow per asset.",
47
- "patternProperties": { "^.*$": flowSchema }, // Ticker
48
- "additionalProperties": flowSchema
49
- },
50
- "sectors": {
51
- "type": "object",
52
- "description": "Price-adjusted net flow per sector.",
53
- "patternProperties": { "^.*$": flowSchema }, // Sector
54
- "additionalProperties": flowSchema
55
- }
56
- },
57
- "required": ["cohort_size", "assets", "sectors"]
58
- };
13
+ this.assetFlows = new Map();
14
+ this.cohortMap = new Map();
15
+ this.tickerMap = null;
16
+ this.dependenciesLoaded = false;
17
+ this.priceChangeMap = null;
59
18
  }
60
19
 
61
- /**
62
- * Statically defines all metadata for the manifest builder.
63
- */
64
20
  static getMetadata() {
65
21
  return {
66
22
  type: 'standard',
67
23
  rootDataDependencies: ['portfolio'],
68
- isHistorical: true, // Compares today vs yesterday portfolio
24
+ isHistorical: true,
69
25
  userType: 'all',
70
26
  category: 'gem'
71
27
  };
72
28
  }
73
29
 
74
- /**
75
- * Statically declare dependencies.
76
- */
77
30
  static getDependencies() {
78
- return ['cohort-skill-definition']; // Pass 1
31
+ return [
32
+ 'cohort-skill-definition', // from gem (Pass 1)
33
+ 'instrument-price-change-1d' // from core (Pass 1)
34
+ ];
79
35
  }
80
36
 
81
- _getPortfolioPositions(portfolio) {
82
- return portfolio?.PublicPositions || portfolio?.AggregatedPositions;
37
+ static getSchema() {
38
+ const tickerSchema = {
39
+ "type": "object",
40
+ "properties": {
41
+ "net_flow_pct": { "type": "number" },
42
+ "net_flow_contribution": { "type": "number" },
43
+ "avg_position_change_pct": { "type": "number" },
44
+ "user_count": { "type": "number" }
45
+ },
46
+ "required": ["net_flow_pct", "net_flow_contribution", "avg_position_change_pct", "user_count"]
47
+ };
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
+ };
83
55
  }
84
56
 
85
- _initAsset(instrumentId) {
86
- if (!this.assetData.has(instrumentId)) {
87
- this.assetData.set(instrumentId, {
88
- total_invested_yesterday: 0,
89
- total_invested_today: 0,
90
- price_change_yesterday: 0,
91
- });
92
- }
57
+ _getPortfolioPositions(portfolio) {
58
+ // --- FIX: Support both Normal (Aggregated) and Speculator (Public) ---
59
+ return portfolio?.AggregatedPositions || portfolio?.PublicPositions;
93
60
  }
94
-
95
- _initSector(sector) {
96
- if (!this.sectorData.has(sector)) {
97
- this.sectorData.set(sector, {
61
+
62
+ // --- THIS IS THE FIX (Part 1) ---
63
+ _initFlowData(instrumentId) {
64
+ if (!this.assetFlows.has(instrumentId)) {
65
+ this.assetFlows.set(instrumentId, {
98
66
  total_invested_yesterday: 0,
99
67
  total_invested_today: 0,
100
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
101
74
  });
102
75
  }
103
76
  }
77
+ // --- END FIX (Part 1) ---
104
78
 
105
- _getUnskilledCohort(fetchedDependencies) {
106
- if (this.unskilledCohortUserIds) {
107
- return this.unskilledCohortUserIds;
79
+ _loadDependencies(fetchedDependencies) {
80
+ 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
+ );
108
88
  }
109
-
110
- // FIX: Use normalized dependency name
89
+
111
90
  const cohortData = fetchedDependencies['cohort-skill-definition'];
112
- if (!cohortData || !cohortData.unskilled_user_ids) {
113
- return new Set();
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) {
100
+ (cohortData.unskilled_user_ids || []).forEach(uid => this.cohortMap.set(String(uid), 'unskilled'));
114
101
  }
115
102
 
116
- this.unskilledCohortUserIds = new Set(cohortData.unskilled_user_ids);
117
- return this.unskilledCohortUserIds;
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
+ this.dependenciesLoaded = true;
118
112
  }
119
113
 
114
+ // --- THIS IS THE FIX (Part 2) ---
120
115
  process(todayPortfolio, yesterdayPortfolio, userId, context, todayInsights, yesterdayInsights, fetchedDependencies) {
121
- const unskilledCohort = this._getUnskilledCohort(fetchedDependencies);
122
-
123
- if (!unskilledCohort.has(userId)) {
124
- return; // This user is not in the "unskilled cohort", skip.
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
125
  }
126
126
 
127
- if (!todayPortfolio || !yesterdayPortfolio) {
127
+ if (!todayPortfolio || !yesterdayPortfolio || !this.priceChangeMap || !this.tickerMap) {
128
128
  return;
129
129
  }
130
130
 
131
- // Logic from here is identical to smart-cohort-flow.js
132
131
  const yPos = this._getPortfolioPositions(yesterdayPortfolio);
133
132
  const tPos = this._getPortfolioPositions(todayPortfolio);
134
- const yPosMap = new Map(yPos?.map(p => [p.InstrumentID, p]) || []);
135
- const tPosMap = new Map(tPos?.map(p => [p.InstrumentID, p]) || []);
133
+ if (!yPos || !tPos) return;
134
+
135
+ const yPosMap = new Map(yPos.map(p => [p.InstrumentID, p]));
136
+ const tPosMap = new Map(tPos.map(p => [p.InstrumentID, p]));
136
137
  const allInstrumentIds = new Set([...yPosMap.keys(), ...tPosMap.keys()]);
137
-
138
- if (!this.mappings) {
139
- this.mappings = context.mappings;
140
- }
141
138
 
142
139
  for (const instrumentId of allInstrumentIds) {
143
140
  if (!instrumentId) continue;
144
-
145
- this._initAsset(instrumentId);
146
- const asset = this.assetData.get(instrumentId);
141
+
142
+ this._initFlowData(instrumentId);
143
+ const asset = this.assetFlows.get(instrumentId);
144
+
147
145
  const yP = yPosMap.get(instrumentId);
148
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;
149
151
 
150
- const yInvested = yP?.Invested || 0;
151
- const tInvested = tP?.Invested || 0;
152
-
153
- const sector = this.mappings.instrumentToSector[instrumentId] || 'Other';
154
- this._initSector(sector);
155
- const sectorAsset = this.sectorData.get(sector);
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;
156
155
 
157
156
  if (yInvested > 0) {
158
- const yPriceChange = (yP?.NetProfit || 0) / (yP?.Invested || 1);
159
-
160
157
  asset.total_invested_yesterday += yInvested;
161
- asset.price_change_yesterday += yPriceChange * yInvested;
158
+
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;
162
166
 
163
- sectorAsset.total_invested_yesterday += yInvested;
164
- sectorAsset.price_change_yesterday += yPriceChange * yInvested;
167
+ // Track yesterday's size and user count
168
+ asset.total_pos_size_yesterday += ySize;
169
+ asset.user_count_yesterday++;
165
170
  }
166
171
  if (tInvested > 0) {
167
172
  asset.total_invested_today += tInvested;
168
- sectorAsset.total_invested_today += tInvested;
169
- }
170
- }
171
- }
172
-
173
- _calculateFlow(dataMap) {
174
- // This helper is identical to the one in skilled-cohort-flow
175
- const result = {};
176
- for (const [key, data] of dataMap.entries()) {
177
- const { total_invested_yesterday, total_invested_today, price_change_yesterday } = data;
178
-
179
- if (total_invested_yesterday > 0) {
180
- const avg_price_change_pct = price_change_yesterday / total_invested_yesterday;
181
- const price_contribution = total_invested_yesterday * avg_price_change_pct;
182
- const flow_contribution = total_invested_today - (total_invested_yesterday + price_contribution);
183
- const net_flow_percentage = (flow_contribution / total_invested_yesterday) * 100;
173
+ asset.user_count_today++;
184
174
 
185
- result[key] = {
186
- net_flow_percentage: net_flow_percentage,
187
- total_invested_today: total_invested_today,
188
- total_invested_yesterday: total_invested_yesterday
189
- };
175
+ // Track today's size
176
+ asset.total_pos_size_today += tSize;
190
177
  }
191
178
  }
192
- return result;
193
179
  }
180
+ // --- END FIX (Part 2) ---
194
181
 
195
- async getResult(fetchedDependencies) {
196
- if (!this.mappings) {
197
- this.mappings = await loadInstrumentMappings();
182
+ // --- THIS IS THE FIX (Part 3) ---
183
+ async getResult() {
184
+ if (!this.tickerMap) {
185
+ return {};
198
186
  }
199
-
200
- const unskilledCohort = this._getUnskilledCohort(fetchedDependencies);
201
-
202
- // 1. Calculate Asset Flow
203
- const assetResult = {};
204
- for (const [instrumentId, data] of this.assetData.entries()) {
205
- const ticker = this.mappings.instrumentToTicker[instrumentId] || `id_${instrumentId}`;
206
- const { total_invested_yesterday, total_invested_today, price_change_yesterday } = data;
207
-
187
+
188
+ const finalResult = {};
189
+
190
+ for (const [instrumentId, data] of this.assetFlows.entries()) {
191
+ const ticker = this.tickerMap[instrumentId];
192
+ 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
+
208
204
  if (total_invested_yesterday > 0) {
209
- const avg_price_change_pct = price_change_yesterday / total_invested_yesterday;
210
- const price_contribution = total_invested_yesterday * avg_price_change_pct;
211
- const flow_contribution = total_invested_today - (total_invested_yesterday + price_contribution);
212
- const net_flow_percentage = (flow_contribution / total_invested_yesterday) * 100;
213
-
214
- assetResult[ticker] = {
215
- net_flow_percentage: net_flow_percentage,
216
- total_invested_today: total_invested_today,
217
- total_invested_yesterday: total_invested_yesterday
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
218
234
  };
219
235
  }
220
236
  }
221
-
222
- // 2. Calculate Sector Flow
223
- const sectorResult = this._calculateFlow(this.sectorData);
224
237
 
225
- return {
226
- cohort_size: unskilledCohort.size,
227
- assets: assetResult,
228
- sectors: sectorResult
229
- };
238
+ return finalResult;
230
239
  }
240
+ // --- END FIX (Part 3) ---
231
241
 
232
242
  reset() {
233
- this.assetData.clear();
234
- this.sectorData.clear();
235
- this.mappings = null;
236
- this.unskilledCohortUserIds = null;
243
+ this.assetFlows.clear();
244
+ this.cohortMap.clear();
245
+ this.tickerMap = null;
246
+ this.dependenciesLoaded = false;
247
+ this.priceChangeMap = null;
237
248
  }
238
249
  }
239
-
240
250
  module.exports = UnskilledCohortFlow;
@@ -1,20 +1,19 @@
1
1
  /**
2
2
  * @fileoverview HELIX Product Line (Pass 4)
3
3
  *
4
- * This metric answers: "What is the final HELIX contrarian signal?"
5
- *
6
- * It combines the 'Herd Consensus' with the 'Winner Cohort Flow'
7
- * to generate actionable signals.
8
- *
9
- * HYPOTHESIS:
10
- * - If Herd is in Euphoria (> +8) AND Winners are Selling = "Strong Sell"
11
- * - If Herd is in Capitulation (< -8) AND Winners are Buying = "Strong Buy"
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?"
12
7
  */
13
8
  class HelixContrarianSignal {
14
9
 
10
+ // --- STANDARD 2: ADDED ---
11
+ constructor() {
12
+ this.result = {};
13
+ }
14
+
15
15
  /**
16
16
  * Defines the output schema for this calculation.
17
- * @returns {object} JSON Schema object
18
17
  */
19
18
  static getSchema() {
20
19
  const tickerSchema = {
@@ -22,30 +21,22 @@ class HelixContrarianSignal {
22
21
  "properties": {
23
22
  "signal": {
24
23
  "type": "string",
25
- "enum": ["Capitulation (Strong Buy)", "Euphoria (Strong Sell)", "Confirmation (Buy)", "Confirmation (Sell)", "FOMO (Losers Buying)", "Panic (Losers Selling)", "Neutral"]
26
- },
27
- "consensus_score": {
28
- "type": "number",
29
- "description": "The Herd Consensus Score (-10 to +10)."
24
+ "enum": ["Strong Contrarian", "Contrarian", "Neutral", "Consensus"]
30
25
  },
31
- "net_winner_flow": {
26
+ "helix_score": {
32
27
  "type": "number",
33
- "description": "Net unique winners who joined/left the asset."
28
+ "description": "Divergence score (-10 to +10). Positive = Contrarian (Winners sell, Losers buy), Negative = Consensus (Both buy/sell)."
34
29
  },
35
- "net_loser_flow": {
36
- "type": "number",
37
- "description": "Net unique losers who joined/left the asset."
38
- }
30
+ "winner_net_flow": { "type": "number" },
31
+ "loser_net_flow": { "type": "number" }
39
32
  },
40
- "required": ["signal", "consensus_score", "net_winner_flow", "net_loser_flow"]
33
+ "required": ["signal", "helix_score", "winner_net_flow", "loser_net_flow"]
41
34
  };
42
35
 
43
36
  return {
44
37
  "type": "object",
45
- "description": "Generates final contrarian signals by comparing herd consensus to winner/loser cohort flows.",
46
- "patternProperties": {
47
- "^.*$": tickerSchema // Ticker
48
- },
38
+ "description": "Generates a final contrarian signal based on Winner vs. Loser flow.",
39
+ "patternProperties": { "^.*$": tickerSchema },
49
40
  "additionalProperties": tickerSchema
50
41
  };
51
42
  }
@@ -68,86 +59,103 @@ class HelixContrarianSignal {
68
59
  */
69
60
  static getDependencies() {
70
61
  return [
71
- 'winner-loser-flow', // from helix (Pass 2)
72
- 'herd-consensus-score' // from helix (Pass 3)
62
+ 'winner-loser-flow', // from helix (Pass 2)
63
+ 'herd-consensus-score' // from helix (Pass 2)
73
64
  ];
74
65
  }
66
+
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
+ }
75
77
 
76
78
  /**
77
79
  * This is a 'meta' calculation. It runs once.
78
80
  */
79
- async process(dateStr, dependencies, config, fetchedDependencies) {
81
+ // --- STANDARD 1: UPDATED SIGNATURE (added rootData) ---
82
+ async process(dateStr, rootData, dependencies, config, fetchedDependencies) {
80
83
  const { logger } = dependencies;
81
84
 
82
- const flowData = fetchedDependencies['winner-loser-flow'];
83
- const consensusData = fetchedDependencies['herd-consensus-score'];
85
+ const cohortFlows = fetchedDependencies['winner-loser-flow'];
86
+ const herdConviction = fetchedDependencies['herd-consensus-score'];
84
87
 
85
- if (!flowData || !consensusData) {
86
- logger.log('WARN', `[helix/helix-contrarian-signal] Missing dependencies for ${dateStr}. Skipping.`);
87
- return {};
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;
88
93
  }
89
94
 
90
95
  const allTickers = new Set([
91
- ...Object.keys(flowData),
92
- ...Object.keys(consensusData)
96
+ ...Object.keys(cohortFlows),
97
+ ...Object.keys(herdConviction)
93
98
  ]);
94
-
99
+
95
100
  const result = {};
96
- const CONSENSUS_THRESHOLD = 8.0; // Score for "Extreme"
97
- const FLOW_THRESHOLD = 5; // Min 5 users moving
98
101
 
99
102
  for (const ticker of allTickers) {
100
- const consensus = consensusData[ticker]?.consensus_score || 0;
101
- const winner_flow = flowData[ticker]?.net_winner_flow || 0;
102
- const loser_flow = flowData[ticker]?.net_loser_flow || 0;
103
-
104
- let signal = "Neutral";
103
+ const flow = cohortFlows[ticker];
104
+ const conviction = herdConviction[ticker];
105
105
 
106
- // --- Primary Contrarian Signals ---
106
+ // 1. Get Winner Flow (using net_winner_flow)
107
+ const winner_flow = flow?.net_winner_flow || 0;
107
108
 
108
- // 1. Capitulation: Herd is at extreme fear, but winners are buying.
109
- if (consensus < -CONSENSUS_THRESHOLD && winner_flow > FLOW_THRESHOLD) {
110
- signal = "Capitulation (Strong Buy)";
111
- }
112
- // 2. Euphoria: Herd is at extreme greed, but winners are selling.
113
- else if (consensus > CONSENSUS_THRESHOLD && winner_flow < -FLOW_THRESHOLD) {
114
- signal = "Euphoria (Strong Sell)";
115
- }
116
-
117
- // --- Secondary Confirmation Signals ---
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)
118
122
 
119
- // 3. Confirmation (Buy): Herd is bullish, winners are also buying.
120
- else if (consensus > 0 && winner_flow > FLOW_THRESHOLD) {
121
- signal = "Confirmation (Buy)";
122
- }
123
- // 4. Confirmation (Sell): Herd is bearish, winners are also selling.
124
- else if (consensus < 0 && winner_flow < -FLOW_THRESHOLD) {
125
- signal = "Confirmation (Sell)";
126
- }
127
-
128
- // --- Tertiary "Dumb Money" Signals (Warning signs) ---
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)
129
130
 
130
- // 5. FOMO: Herd is euphoric, and losers are piling in.
131
- else if (consensus > CONSENSUS_THRESHOLD && loser_flow > FLOW_THRESHOLD) {
132
- signal = "FOMO (Losers Buying)";
133
- }
134
- // 6. Panic: Herd is fearful, and losers are capitulating.
135
- else if (consensus < -CONSENSUS_THRESHOLD && loser_flow < -FLOW_THRESHOLD) {
136
- signal = "Panic (Losers Selling)";
137
- }
138
-
139
- // Add to result if not neutral
140
- if (signal !== "Neutral") {
141
- result[ticker] = {
142
- signal: signal,
143
- consensus_score: consensus,
144
- net_winner_flow: winner_flow,
145
- net_loser_flow: loser_flow
146
- };
147
- }
131
+ const divergence = loser_flow - winner_flow;
132
+ const helix_score = this._normalize(divergence);
133
+
134
+ let signal = "Neutral";
135
+ if (helix_score > 7.5) signal = "Strong Contrarian"; // Losers buying, Winners selling
136
+ else if (helix_score > 2.5) signal = "Contrarian";
137
+ else if (helix_score < -2.5) signal = "Consensus"; // Both moving same way
138
+
139
+ result[ticker] = {
140
+ signal: signal,
141
+ helix_score: helix_score,
142
+ winner_net_flow: winner_flow,
143
+ loser_net_flow: loser_flow // Using conviction as flow proxy
144
+ };
148
145
  }
149
146
 
150
- return result;
147
+ // --- STANDARD 2: SET STATE, DO NOT RETURN ---
148
+ this.result = result;
149
+ }
150
+
151
+ // --- STANDARD 2: ADDED ---
152
+ async getResult(fetchedDependencies) {
153
+ return this.result;
154
+ }
155
+
156
+ // --- STANDARD 2: ADDED ---
157
+ reset() {
158
+ this.result = {};
151
159
  }
152
160
  }
153
161