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,241 +1,252 @@
1
1
  /**
2
- * @fileoverview Calculation (Pass 2) for "Skilled Cohort" flow.
3
- *
4
- * This metric calculates the "Net Crowd Flow Percentage" for the
5
- * "Skilled Cohort" (top 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 SkilledCohortFlow {
14
12
  constructor() {
15
- this.assetData = new Map();
16
- this.sectorData = new Map();
17
- this.mappings = null;
18
- this.skilledCohortUserIds = 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 'Skilled Cohort' (top 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 Skilled 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
+ ];
35
+ }
36
+
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 'Skilled' cohort.",
52
+ "patternProperties": { "^.*$": tickerSchema },
53
+ "additionalProperties": tickerSchema
54
+ };
79
55
  }
80
56
 
81
57
  _getPortfolioPositions(portfolio) {
82
- return portfolio?.PublicPositions || portfolio?.AggregatedPositions;
58
+ // --- FIX: Support both Normal (Aggregated) and Speculator (Public) ---
59
+ return portfolio?.AggregatedPositions || portfolio?.PublicPositions;
83
60
  }
84
61
 
85
- _initAsset(instrumentId) {
86
- if (!this.assetData.has(instrumentId)) {
87
- this.assetData.set(instrumentId, {
62
+ // --- THIS IS THE FIX (Part 1) ---
63
+ _initFlowData(instrumentId) {
64
+ if (!this.assetFlows.has(instrumentId)) {
65
+ this.assetFlows.set(instrumentId, {
88
66
  total_invested_yesterday: 0,
89
67
  total_invested_today: 0,
90
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
91
74
  });
92
75
  }
93
76
  }
94
-
95
- _initSector(sector) {
96
- if (!this.sectorData.has(sector)) {
97
- this.sectorData.set(sector, {
98
- total_invested_yesterday: 0,
99
- total_invested_today: 0,
100
- price_change_yesterday: 0,
101
- });
77
+ // --- END FIX (Part 1) ---
78
+
79
+ _loadDependencies(fetchedDependencies) {
80
+ if (this.dependenciesLoaded) return;
81
+
82
+ if (!fetchedDependencies) {
83
+ // This will stop the test and explain the root cause.
84
+ throw new Error(
85
+ `[skilled-cohort-flow] CRITICAL ERROR: fetchedDependencies object was UNDEFINED.
86
+ This means a dependency test (like 'cohort-skill-definition' or 'instrument-price-change-1d')
87
+ failed to run or produced an invalid result.`
88
+ );
102
89
  }
103
- }
104
90
 
105
- _getSkilledCohort(fetchedDependencies) {
106
- if (this.skilledCohortUserIds) {
107
- return this.skilledCohortUserIds;
108
- }
109
-
110
- // FIX: Use normalized dependency name
111
91
  const cohortData = fetchedDependencies['cohort-skill-definition'];
112
- if (!cohortData || !cohortData.skilled_user_ids) {
113
- return new Set();
92
+ if (!cohortData) {
93
+ throw new Error(
94
+ `[skilled-cohort-flow] DEPENDENCY ERROR: 'cohort-skill-definition' was missing.
95
+ Check logs for errors in that calculation.`
96
+ );
97
+ }
98
+
99
+ // This logic is correct for 'cohort-skill-definition' output
100
+ if (cohortData && cohortData.skilled_user_ids) {
101
+ (cohortData.skilled_user_ids || []).forEach(uid => this.cohortMap.set(String(uid), 'skilled'));
102
+ }
103
+
104
+ this.priceChangeMap = fetchedDependencies['instrument-price-change-1d'];
105
+ if (!this.priceChangeMap) {
106
+ throw new Error(
107
+ `[skilled-cohort-flow] DEPENDENCY ERROR: 'instrument-price-change-1d' was missing.
108
+ This is likely due to the worker bug for 'price' dependencies.`
109
+ );
114
110
  }
115
111
 
116
- this.skilledCohortUserIds = new Set(cohortData.skilled_user_ids);
117
- return this.skilledCohortUserIds;
112
+ this.dependenciesLoaded = true;
118
113
  }
119
114
 
115
+ // --- THIS IS THE FIX (Part 2) ---
120
116
  process(todayPortfolio, yesterdayPortfolio, userId, context, todayInsights, yesterdayInsights, fetchedDependencies) {
121
- const skilledCohort = this._getSkilledCohort(fetchedDependencies);
122
-
123
- if (!skilledCohort.has(userId)) {
124
- return; // This user is not in the "skilled cohort", skip.
117
+ // Load dependencies *before* checking cohort
118
+ this._loadDependencies(fetchedDependencies);
119
+
120
+ if (!this.tickerMap) {
121
+ this.tickerMap = context.instrumentToTicker;
125
122
  }
126
123
 
127
- if (!todayPortfolio || !yesterdayPortfolio) {
124
+ const cohortName = this.cohortMap.get(String(userId)); // Ensure string ID
125
+ if (cohortName !== 'skilled') {
126
+ return; // Not in skilled cohort
127
+ }
128
+
129
+ if (!todayPortfolio || !yesterdayPortfolio || !this.priceChangeMap || !this.tickerMap) {
128
130
  return;
129
131
  }
130
132
 
131
- // Logic from here is identical to smart-cohort-flow.js
132
133
  const yPos = this._getPortfolioPositions(yesterdayPortfolio);
133
134
  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]) || []);
135
+ if (!yPos || !tPos) return;
136
+
137
+ const yPosMap = new Map(yPos.map(p => [p.InstrumentID, p]));
138
+ const tPosMap = new Map(tPos.map(p => [p.InstrumentID, p]));
136
139
  const allInstrumentIds = new Set([...yPosMap.keys(), ...tPosMap.keys()]);
137
-
138
- if (!this.mappings) {
139
- this.mappings = context.mappings;
140
- }
141
140
 
142
141
  for (const instrumentId of allInstrumentIds) {
143
142
  if (!instrumentId) continue;
144
-
145
- this._initAsset(instrumentId);
146
- const asset = this.assetData.get(instrumentId);
143
+
144
+ this._initFlowData(instrumentId);
145
+ const asset = this.assetFlows.get(instrumentId);
146
+
147
147
  const yP = yPosMap.get(instrumentId);
148
148
  const tP = tPosMap.get(instrumentId);
149
+
150
+ // Get $ flow
151
+ const yInvested = yP?.Invested || yP?.Amount || 0;
152
+ const tInvested = tP?.Invested || tP?.Amount || 0;
149
153
 
150
- // Use 'Invested' for % portfolio, not 'Amount'
151
- const yInvested = yP?.Invested || 0;
152
- const tInvested = tP?.Invested || 0;
153
-
154
- const sector = this.mappings.instrumentToSector[instrumentId] || 'Other';
155
- this._initSector(sector);
156
- const sectorAsset = this.sectorData.get(sector);
154
+ // Get position size (same as $ flow for these schemas)
155
+ const ySize = yP?.Invested || yP?.Amount || 0;
156
+ const tSize = tP?.Invested || tP?.Amount || 0;
157
157
 
158
158
  if (yInvested > 0) {
159
- // Use P&L % from portfolio 'NetProfit'
160
- const yPriceChange = (yP?.NetProfit || 0) / (yP?.Invested || 1);
161
-
162
159
  asset.total_invested_yesterday += yInvested;
163
- asset.price_change_yesterday += yPriceChange * yInvested;
160
+
161
+ const ticker = this.tickerMap[instrumentId];
162
+ const yPriceChange_pct = (ticker && this.priceChangeMap[ticker])
163
+ ? this.priceChangeMap[ticker].price_change_1d_pct
164
+ : 0;
165
+
166
+ const yPriceChange_decimal = (yPriceChange_pct || 0) / 100.0;
167
+ asset.price_change_yesterday += yPriceChange_decimal * yInvested;
164
168
 
165
- sectorAsset.total_invested_yesterday += yInvested;
166
- sectorAsset.price_change_yesterday += yPriceChange * yInvested;
169
+ // Track yesterday's size and user count
170
+ asset.total_pos_size_yesterday += ySize;
171
+ asset.user_count_yesterday++;
167
172
  }
168
173
  if (tInvested > 0) {
169
174
  asset.total_invested_today += tInvested;
170
- sectorAsset.total_invested_today += tInvested;
171
- }
172
- }
173
- }
174
-
175
- _calculateFlow(dataMap) {
176
- const result = {};
177
- for (const [key, data] of dataMap.entries()) {
178
- const { total_invested_yesterday, total_invested_today, price_change_yesterday } = data;
179
-
180
- if (total_invested_yesterday > 0) {
181
- const avg_price_change_pct = price_change_yesterday / total_invested_yesterday;
182
- const price_contribution = total_invested_yesterday * avg_price_change_pct;
183
- const flow_contribution = total_invested_today - (total_invested_yesterday + price_contribution);
184
- const net_flow_percentage = (flow_contribution / total_invested_yesterday) * 100;
175
+ asset.user_count_today++; // Count users holding today
185
176
 
186
- result[key] = {
187
- net_flow_percentage: net_flow_percentage,
188
- total_invested_today: total_invested_today,
189
- total_invested_yesterday: total_invested_yesterday
190
- };
177
+ // Track today's size
178
+ asset.total_pos_size_today += tSize;
191
179
  }
192
180
  }
193
- return result;
194
181
  }
182
+ // --- END FIX (Part 2) ---
195
183
 
196
- async getResult(fetchedDependencies) {
197
- if (!this.mappings) {
198
- this.mappings = await loadInstrumentMappings();
184
+ // --- THIS IS THE FIX (Part 3) ---
185
+ async getResult() {
186
+ if (!this.tickerMap) {
187
+ return {};
199
188
  }
200
-
201
- const skilledCohort = this._getSkilledCohort(fetchedDependencies);
202
-
203
- // 1. Calculate Asset Flow
204
- const assetResult = {};
205
- for (const [instrumentId, data] of this.assetData.entries()) {
206
- const ticker = this.mappings.instrumentToTicker[instrumentId] || `id_${instrumentId}`;
207
- const { total_invested_yesterday, total_invested_today, price_change_yesterday } = data;
208
-
189
+
190
+ const finalResult = {};
191
+
192
+ for (const [instrumentId, data] of this.assetFlows.entries()) {
193
+ const ticker = this.tickerMap[instrumentId];
194
+ if (!ticker) continue;
195
+
196
+ const {
197
+ total_invested_yesterday, total_invested_today, price_change_yesterday,
198
+ // Destructure the restored fields
199
+ total_pos_size_yesterday, total_pos_size_today,
200
+ user_count_yesterday, user_count_today
201
+ } = data;
202
+
203
+ let net_flow_percentage = 0;
204
+ let flow_contribution = 0;
205
+
209
206
  if (total_invested_yesterday > 0) {
210
- const avg_price_change_pct = price_change_yesterday / total_invested_yesterday;
211
- const price_contribution = total_invested_yesterday * avg_price_change_pct;
212
- const flow_contribution = total_invested_today - (total_invested_yesterday + price_contribution);
213
- const net_flow_percentage = (flow_contribution / total_invested_yesterday) * 100;
214
-
215
- assetResult[ticker] = {
216
- net_flow_percentage: net_flow_percentage,
217
- total_invested_today: total_invested_today,
218
- total_invested_yesterday: total_invested_yesterday
207
+ const avg_price_change_decimal = (price_change_yesterday === 0) ? 0 : (price_change_yesterday / total_invested_yesterday);
208
+ const price_adjusted_yesterday_value = total_invested_yesterday * (1 + avg_price_change_decimal);
209
+
210
+ flow_contribution = total_invested_today - price_adjusted_yesterday_value;
211
+ net_flow_percentage = (flow_contribution / total_invested_yesterday) * 100;
212
+ } else if (total_invested_today > 0) {
213
+ flow_contribution = total_invested_today;
214
+ net_flow_percentage = Infinity;
215
+ }
216
+
217
+ // Restore the calculation logic for conviction
218
+ let avg_position_change_pct = 0;
219
+ if (user_count_yesterday > 0 && user_count_today > 0) {
220
+ const avg_pos_y = total_pos_size_yesterday / user_count_yesterday;
221
+ const avg_pos_t = total_pos_size_today / user_count_today;
222
+ if (avg_pos_y > 0) {
223
+ avg_position_change_pct = ((avg_pos_t - avg_pos_y) / avg_pos_y) * 100;
224
+ }
225
+ } else if (user_count_today > 0) {
226
+ // Pure inflow, conviction is max
227
+ avg_position_change_pct = Infinity;
228
+ }
229
+
230
+ if (isFinite(net_flow_percentage) && isFinite(flow_contribution)) {
231
+ finalResult[ticker] = {
232
+ net_flow_pct: net_flow_percentage,
233
+ net_flow_contribution: flow_contribution,
234
+ avg_position_change_pct: avg_position_change_pct, // Now correctly calculated
235
+ user_count: user_count_today
219
236
  };
220
237
  }
221
238
  }
222
-
223
- // 2. Calculate Sector Flow
224
- const sectorResult = this._calculateFlow(this.sectorData);
225
239
 
226
- return {
227
- cohort_size: skilledCohort.size,
228
- assets: assetResult,
229
- sectors: sectorResult
230
- };
240
+ return finalResult;
231
241
  }
242
+ // --- END FIX (Part 3) ---
232
243
 
233
244
  reset() {
234
- this.assetData.clear();
235
- this.sectorData.clear();
236
- this.mappings = null;
237
- this.skilledCohortUserIds = null;
245
+ this.assetFlows.clear();
246
+ this.cohortMap.clear();
247
+ this.tickerMap = null;
248
+ this.dependenciesLoaded = false;
249
+ this.priceChangeMap = null;
238
250
  }
239
251
  }
240
-
241
252
  module.exports = SkilledCohortFlow;
@@ -1,153 +1,113 @@
1
1
  /**
2
- * @fileoverview Calculation (Pass 3) for skilled-unskilled divergence.
2
+ * @fileoverview GEM Product Line (Pass 3)
3
3
  *
4
- * This metric answers: "What divergence signals (e.g., capitulation,
5
- * euphoria) can be found by comparing the net asset and sector flow
6
- * of the 'Skilled Cohort' vs. the 'Unskilled Cohort'?"
7
- *
8
- * It *depends* on 'skilled-cohort-flow' and 'unskilled-cohort-flow'.
4
+ * This 'meta' calculation answers: "What is the net flow
5
+ * divergence between Skilled and Unskilled cohorts?"
9
6
  */
10
7
  class SkilledUnskilledDivergence {
8
+
9
+ // --- STANDARD 2: ADDED ---
11
10
  constructor() {
12
- // No per-user processing
11
+ this.result = {};
13
12
  }
14
13
 
14
+ /** Statically defines metadata */
15
+ static getMetadata() {
16
+ return {
17
+ type: 'meta',
18
+ rootDataDependencies: [],
19
+ isHistorical: false,
20
+ userType: 'n/a',
21
+ category: 'gem'
22
+ };
23
+ }
24
+
25
+ /** Statically declare dependencies */
26
+ static getDependencies() {
27
+ return [
28
+ 'skilled-cohort-flow', // from gem (Pass 2)
29
+ 'unskilled-cohort-flow' // from gem (Pass 2)
30
+ ];
31
+ }
32
+
15
33
  /**
16
34
  * Defines the output schema for this calculation.
17
- * @returns {object} JSON Schema object
18
35
  */
19
36
  static getSchema() {
20
- const signalSchema = {
37
+ const tickerSchema = {
21
38
  "type": "object",
22
39
  "properties": {
23
- "status": {
24
- "type": "string",
25
- "enum": ["Capitulation", "Euphoria", "Confirmation (Buy)", "Confirmation (Sell)", "Divergence (Skilled Buy)", "Divergence (Skilled Sell)", "Neutral"]
26
- },
27
40
  "skilled_flow_pct": { "type": "number" },
28
- "unskilled_flow_pct": { "type": "number" }
41
+ "unskilled_flow_pct": { "type": "number" },
42
+ "flow_divergence_score": { "type": "number" }
29
43
  },
30
- "required": ["status", "skilled_flow_pct", "unskilled_flow_pct"]
44
+ "required": ["skilled_flow_pct", "unskilled_flow_pct", "flow_divergence_score"]
31
45
  };
32
-
46
+
33
47
  return {
34
48
  "type": "object",
35
- "description": "Generates divergence signals by comparing net flow of 'Skilled' vs. 'Unskilled' cohorts, by asset and sector.",
36
- "properties": {
37
- "assets": {
38
- "type": "object",
39
- "description": "Divergence signals per asset.",
40
- "patternProperties": { "^.*$": signalSchema }, // Ticker
41
- "additionalProperties": signalSchema
42
- },
43
- "sectors": {
44
- "type": "object",
45
- "description": "Divergence signals per sector.",
46
- "patternProperties": { "^.*$": signalSchema }, // Sector
47
- "additionalProperties": signalSchema
48
- }
49
- },
50
- "required": ["assets", "sectors"]
51
- };
52
- }
53
-
54
- /**
55
- * Statically defines all metadata for the manifest builder.
56
- */
57
- static getMetadata() {
58
- return {
59
- type: 'meta',
60
- rootDataDependencies: [],
61
- isHistorical: false,
62
- userType: 'n/a',
63
- category: 'gem'
49
+ "description": "Tracks the net % flow divergence between skilled and unskilled cohorts.",
50
+ "patternProperties": { "^.*$": tickerSchema },
51
+ "additionalProperties": tickerSchema
64
52
  };
65
53
  }
66
54
 
67
55
  /**
68
- * Statically declare dependencies.
56
+ * This is a 'meta' calculation. It runs once.
69
57
  */
70
- static getDependencies() {
71
- return [
72
- 'skilled-cohort-flow', // Pass 2
73
- 'unskilled-cohort-flow' // Pass 2
74
- ];
75
- }
58
+ // --- STANDARD 1: UPDATED SIGNATURE (added rootData) ---
59
+ async process(dateStr, rootData, dependencies, config, fetchedDependencies) {
60
+ const { logger } = dependencies;
61
+
62
+ const skilledFlow = fetchedDependencies['skilled-cohort-flow'];
63
+ const unskilledFlow = fetchedDependencies['unskilled-cohort-flow'];
76
64
 
77
- process() {
78
- // No-op
79
- }
80
-
81
- _calculateDivergence(skilledFlow, unskilledFlow) {
82
- const result = {};
83
65
  if (!skilledFlow || !unskilledFlow) {
84
- return result;
66
+ logger.log('WARN', `[gem/skilled-unskilled-divergence] Missing dependencies for ${dateStr}.`);
67
+ // --- STANDARD 2: DO NOT RETURN ---
68
+ this.result = {};
69
+ return;
85
70
  }
86
-
87
- const allKeys = new Set([...Object.keys(skilledFlow), ...Object.keys(unskilledFlow)]);
88
- const THRESHOLD = 1.0; // Min flow % to register as a signal
89
71
 
90
- for (const key of allKeys) {
91
- const sFlow = skilledFlow[key]?.net_flow_percentage || 0;
92
- const dFlow = unskilledFlow[key]?.net_flow_percentage || 0;
72
+ const allTickers = new Set([
73
+ ...Object.keys(skilledFlow),
74
+ ...Object.keys(unskilledFlow)
75
+ ]);
93
76
 
94
- let status = 'Neutral';
77
+ const result = {};
95
78
 
96
- // Both buying
97
- if (sFlow > THRESHOLD && dFlow > THRESHOLD) {
98
- status = 'Confirmation (Buy)';
99
- }
100
- // Both selling
101
- else if (sFlow < -THRESHOLD && dFlow < -THRESHOLD) {
102
- status = 'Confirmation (Sell)';
103
- }
104
- // Skilled buying, Unskilled selling
105
- else if (sFlow > THRESHOLD && dFlow < -THRESHOLD) {
106
- status = 'Capitulation'; // Skilled buying the dip from unskilled
107
- }
108
- // Skilled selling, Unskilled buying
109
- else if (sFlow < -THRESHOLD && dFlow > THRESHOLD) {
110
- status = 'Euphoria'; // Skilled selling into unskilled fomo
111
- }
112
- // Skilled buying, Unskilled neutral
113
- else if (sFlow > THRESHOLD && Math.abs(dFlow) < THRESHOLD) {
114
- status = 'Divergence (Skilled Buy)';
115
- }
116
- // Skilled selling, Unskilled neutral
117
- else if (sFlow < -THRESHOLD && Math.abs(dFlow) < THRESHOLD) {
118
- status = 'Divergence (Skilled Sell)';
119
- }
79
+ for (const ticker of allTickers) {
80
+ const skilledData = skilledFlow[ticker];
81
+ const unskilledData = unskilledFlow[ticker];
120
82
 
121
- result[key] = {
122
- status: status,
123
- skilled_flow_pct: sFlow,
124
- unskilled_flow_pct: dFlow
83
+ // Get net_flow_pct
84
+ const skilled_flow_pct = skilledData?.net_flow_pct || 0;
85
+ const unskilled_flow_pct = unskilledData?.net_flow_pct || 0;
86
+
87
+ // Only include assets that are actively being traded
88
+ if (skilled_flow_pct === 0 && unskilled_flow_pct === 0) {
89
+ continue;
90
+ }
91
+
92
+ result[ticker] = {
93
+ skilled_flow_pct: skilled_flow_pct,
94
+ unskilled_flow_pct: unskilled_flow_pct,
95
+ flow_divergence_score: skilled_flow_pct - unskilled_flow_pct
125
96
  };
126
97
  }
127
- return result;
98
+
99
+ // --- STANDARD 2: SET STATE, DO NOT RETURN ---
100
+ this.result = result;
128
101
  }
129
102
 
130
- /**
131
- * This is a 'meta' calculation.
132
- * @param {object} fetchedDependencies - Results from Pass 2.
133
- */
134
- getResult(fetchedDependencies) {
135
- // FIX: Use normalized dependency names
136
- const skilledFlowData = fetchedDependencies['skilled-cohort-flow'];
137
- const unskilledFlowData = fetchedDependencies['unskilled-cohort-flow'];
138
-
139
- const assetResult = this._calculateDivergence(skilledFlowData?.assets, unskilledFlowData?.assets);
140
- const sectorResult = this._calculateDivergence(skilledFlowData?.sectors, unskilledFlowData?.sectors);
141
-
142
- return {
143
- assets: assetResult,
144
- sectors: sectorResult
145
- };
103
+ // --- STANDARD 2: ADDED ---
104
+ async getResult(fetchedDependencies) {
105
+ return this.result;
146
106
  }
147
107
 
108
+ // --- STANDARD 2: ADDED ---
148
109
  reset() {
149
- // No state
110
+ this.result = {};
150
111
  }
151
112
  }
152
-
153
113
  module.exports = SkilledUnskilledDivergence;