aiden-shared-calculations-unified 1.0.71 → 1.0.73

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.
@@ -0,0 +1,110 @@
1
+ /**
2
+ * @fileoverview Calculation (Pass 3) for cohort momentum state.
3
+ *
4
+ * This metric answers: "Are the 'Skilled' and 'Unskilled' cohorts
5
+ * trend-following or acting as contrarians?"
6
+ *
7
+ * It multiplies cohort flow by price momentum.
8
+ * - High positive score = Buying into a rally (FOMO)
9
+ * - High negative score = Buying into a dip, or Selling into a rally
10
+ *
11
+ * It *depends* on Pass 2 cohort flows and Pass 1 price momentum.
12
+ */
13
+ class CohortMomentumState {
14
+ constructor() {
15
+ // No per-user processing
16
+ }
17
+
18
+ /**
19
+ * Defines the output schema for this calculation.
20
+ * @returns {object} JSON Schema object
21
+ */
22
+ static getSchema() {
23
+ const tickerSchema = {
24
+ "type": "object",
25
+ "properties": {
26
+ "skilled_momentum_score": {
27
+ "type": "number",
28
+ "description": "Skilled Flow % * 20d Momentum %. High positive = trend-following."
29
+ },
30
+ "unskilled_momentum_score": {
31
+ "type": "number",
32
+ "description": "Unskilled Flow % * 20d Momentum %. High positive = trend-following (FOMO)."
33
+ },
34
+ "skilled_flow_pct": { "type": "number" },
35
+ "unskilled_flow_pct": { "type": "number" },
36
+ "momentum_20d_pct": { "type": ["number", "null"] }
37
+ },
38
+ "required": ["skilled_momentum_score", "unskilled_momentum_score"]
39
+ };
40
+
41
+ return {
42
+ "type": "object",
43
+ "description": "Calculates a momentum-following score for Skilled and Unskilled cohorts.",
44
+ "patternProperties": {
45
+ "^.*$": tickerSchema // Ticker
46
+ },
47
+ "additionalProperties": tickerSchema
48
+ };
49
+ }
50
+
51
+ /**
52
+ * Statically declare dependencies.
53
+ */
54
+ static getDependencies() {
55
+ return [
56
+ 'gem_skilled-cohort-flow', // Pass 2
57
+ 'gem_unskilled-cohort-flow', // Pass 2
58
+ 'gem_instrument-price-momentum' // Pass 1
59
+ ];
60
+ }
61
+
62
+ process() {
63
+ // No-op
64
+ }
65
+
66
+ /**
67
+ * This is a 'meta' calculation.
68
+ * @param {object} fetchedDependencies - Results from previous passes.
69
+ */
70
+ getResult(fetchedDependencies) {
71
+ const skilledFlowData = fetchedDependencies['skilled-cohort-flow']?.assets;
72
+ const unskilledFlowData = fetchedDependencies['unskilled-cohort-flow']?.assets;
73
+ const momentumData = fetchedDependencies['instrument-price-momentum'];
74
+
75
+ if (!skilledFlowData || !unskilledFlowData || !momentumData) {
76
+ return {};
77
+ }
78
+
79
+ const result = {};
80
+ const allTickers = new Set([
81
+ ...Object.keys(skilledFlowData),
82
+ ...Object.keys(unskilledFlowData)
83
+ ]);
84
+
85
+ for (const ticker of allTickers) {
86
+ const sFlow = skilledFlowData[ticker]?.net_flow_percentage || 0;
87
+ const uFlow = unskilledFlowData[ticker]?.net_flow_percentage || 0;
88
+ const mom = momentumData[ticker]?.momentum_20d_pct || 0;
89
+
90
+ const skilled_momentum_score = sFlow * mom;
91
+ const unskilled_momentum_score = uFlow * mom;
92
+
93
+ result[ticker] = {
94
+ skilled_momentum_score: skilled_momentum_score,
95
+ unskilled_momentum_score: unskilled_momentum_score,
96
+ skilled_flow_pct: sFlow,
97
+ unskilled_flow_pct: uFlow,
98
+ momentum_20d_pct: mom
99
+ };
100
+ }
101
+
102
+ return result;
103
+ }
104
+
105
+ reset() {
106
+ // No state
107
+ }
108
+ }
109
+
110
+ module.exports = CohortMomentumState;
@@ -0,0 +1,115 @@
1
+ /**
2
+ * @fileoverview Calculation (Pass 1 - Meta) for 20-day price momentum.
3
+ *
4
+ * This metric answers: "What is the 20-day percentage price change for
5
+ * every instrument?"
6
+ *
7
+ * It is a 'meta' calculation that runs once, loads all price data,
8
+ * and provides a reusable momentum signal for downstream passes.
9
+ */
10
+ const { loadAllPriceData } = require('../../utils/price_data_provider');
11
+
12
+ class InstrumentPriceMomentum {
13
+
14
+ /**
15
+ * Defines the output schema for this calculation.
16
+ * @returns {object} JSON Schema object
17
+ */
18
+ static getSchema() {
19
+ const tickerSchema = {
20
+ "type": "object",
21
+ "properties": {
22
+ "momentum_20d_pct": {
23
+ "type": ["number", "null"],
24
+ "description": "The 20-day rolling price change percentage."
25
+ }
26
+ },
27
+ "required": ["momentum_20d_pct"]
28
+ };
29
+
30
+ return {
31
+ "type": "object",
32
+ "description": "Calculates the 20-day price momentum for all instruments.",
33
+ "patternProperties": {
34
+ "^.*$": tickerSchema // Ticker
35
+ },
36
+ "additionalProperties": tickerSchema
37
+ };
38
+ }
39
+
40
+ /**
41
+ * This is a Pass 1 calculation and has no dependencies.
42
+ */
43
+ static getDependencies() {
44
+ return [];
45
+ }
46
+
47
+ // Helper to get date string N days ago
48
+ _getDateStr(baseDateStr, daysOffset) {
49
+ const date = new Date(baseDateStr + 'T00:00:00Z');
50
+ date.setUTCDate(date.getUTCDate() + daysOffset);
51
+ return date.toISOString().slice(0, 10);
52
+ }
53
+
54
+ // Helper to find the last available price on or before a date
55
+ _findPrice(priceHistory, dateStr, maxLookback = 5) {
56
+ if (!priceHistory) return null;
57
+ let checkDate = new Date(dateStr + 'T00:00:00Z');
58
+ for (let i = 0; i < maxLookback; i++) {
59
+ const checkDateStr = checkDate.toISOString().slice(0, 10);
60
+ const price = priceHistory[checkDateStr];
61
+ if (price !== undefined && price !== null && price > 0) {
62
+ return price;
63
+ }
64
+ checkDate.setUTCDate(checkDate.getUTCDate() - 1);
65
+ }
66
+ return null;
67
+ }
68
+
69
+ /**
70
+ * This is a 'meta' calculation. It runs once.
71
+ * @param {string} dateStr - The date string 'YYYY-MM-DD'.
72
+ * @param {object} dependencies - The shared dependencies (e.g., logger, calculationUtils).
73
+ * @param {object} config - The computation system configuration.
74
+ * @param {object} fetchedDependencies - (Unused)
75
+ * @returns {Promise<object>} The calculation result.
76
+ */
77
+ async process(dateStr, dependencies, config, fetchedDependencies) {
78
+ const { logger, calculationUtils, mappings } = dependencies;
79
+
80
+ // Load all price data and mappings
81
+ const priceMap = await loadAllPriceData();
82
+ const tickerMap = await calculationUtils.loadInstrumentMappings();
83
+
84
+ if (!priceMap || !tickerMap || !tickerMap.instrumentToTicker) {
85
+ logger.log('ERROR', '[instrument-price-momentum] Failed to load priceMap or mappings.');
86
+ return {};
87
+ }
88
+
89
+ const dateStrT20 = this._getDateStr(dateStr, -20);
90
+ const result = {};
91
+
92
+ for (const instrumentId in priceMap) {
93
+ const instrumentPrices = priceMap[instrumentId];
94
+ const ticker = tickerMap.instrumentToTicker[instrumentId];
95
+
96
+ if (!ticker) continue; // Skip if we can't map ID to ticker
97
+
98
+ const priceT = this._findPrice(instrumentPrices, dateStr);
99
+ const priceT20 = this._findPrice(instrumentPrices, dateStrT20);
100
+
101
+ let momentum = null;
102
+ if (priceT && priceT20 && priceT20 > 0) {
103
+ momentum = ((priceT - priceT20) / priceT20) * 100; // As percentage
104
+ }
105
+
106
+ result[ticker] = {
107
+ momentum_20d_pct: momentum
108
+ };
109
+ }
110
+
111
+ return result;
112
+ }
113
+ }
114
+
115
+ module.exports = InstrumentPriceMomentum;
@@ -0,0 +1,139 @@
1
+ /**
2
+ * @fileoverview Calculation (Pass 3) for skilled-unskilled divergence.
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'.
9
+ */
10
+ class SkilledUnskilledDivergence {
11
+ constructor() {
12
+ // No per-user processing
13
+ }
14
+
15
+ /**
16
+ * Defines the output schema for this calculation.
17
+ * @returns {object} JSON Schema object
18
+ */
19
+ static getSchema() {
20
+ const signalSchema = {
21
+ "type": "object",
22
+ "properties": {
23
+ "status": {
24
+ "type": "string",
25
+ "enum": ["Capitulation", "Euphoria", "Confirmation (Buy)", "Confirmation (Sell)", "Divergence (Skilled Buy)", "Divergence (Skilled Sell)", "Neutral"]
26
+ },
27
+ "skilled_flow_pct": { "type": "number" },
28
+ "unskilled_flow_pct": { "type": "number" }
29
+ },
30
+ "required": ["status", "skilled_flow_pct", "unskilled_flow_pct"]
31
+ };
32
+
33
+ return {
34
+ "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 declare dependencies.
56
+ */
57
+ static getDependencies() {
58
+ return [
59
+ 'gem_skilled-cohort-flow', // Pass 2
60
+ 'gem_unskilled-cohort-flow' // Pass 2
61
+ ];
62
+ }
63
+
64
+ process() {
65
+ // No-op
66
+ }
67
+
68
+ _calculateDivergence(skilledFlow, unskilledFlow) {
69
+ const result = {};
70
+ if (!skilledFlow || !unskilledFlow) {
71
+ return result;
72
+ }
73
+
74
+ const allKeys = new Set([...Object.keys(skilledFlow), ...Object.keys(unskilledFlow)]);
75
+ const THRESHOLD = 1.0; // Min flow % to register as a signal
76
+
77
+ for (const key of allKeys) {
78
+ const sFlow = skilledFlow[key]?.net_flow_percentage || 0;
79
+ const dFlow = unskilledFlow[key]?.net_flow_percentage || 0;
80
+
81
+ let status = 'Neutral';
82
+
83
+ // Both buying
84
+ if (sFlow > THRESHOLD && dFlow > THRESHOLD) {
85
+ status = 'Confirmation (Buy)';
86
+ }
87
+ // Both selling
88
+ else if (sFlow < -THRESHOLD && dFlow < -THRESHOLD) {
89
+ status = 'Confirmation (Sell)';
90
+ }
91
+ // Skilled buying, Unskilled selling
92
+ else if (sFlow > THRESHOLD && dFlow < -THRESHOLD) {
93
+ status = 'Capitulation'; // Skilled buying the dip from unskilled
94
+ }
95
+ // Skilled selling, Unskilled buying
96
+ else if (sFlow < -THRESHOLD && dFlow > THRESHOLD) {
97
+ status = 'Euphoria'; // Skilled selling into unskilled fomo
98
+ }
99
+ // Skilled buying, Unskilled neutral
100
+ else if (sFlow > THRESHOLD && Math.abs(dFlow) < THRESHOLD) {
101
+ status = 'Divergence (Skilled Buy)';
102
+ }
103
+ // Skilled selling, Unskilled neutral
104
+ else if (sFlow < -THRESHOLD && Math.abs(dFlow) < THRESHOLD) {
105
+ status = 'Divergence (Skilled Sell)';
106
+ }
107
+
108
+ result[key] = {
109
+ status: status,
110
+ skilled_flow_pct: sFlow,
111
+ unskilled_flow_pct: dFlow
112
+ };
113
+ }
114
+ return result;
115
+ }
116
+
117
+ /**
118
+ * This is a 'meta' calculation.
119
+ * @param {object} fetchedDependencies - Results from Pass 2.
120
+ */
121
+ getResult(fetchedDependencies) {
122
+ const skilledFlowData = fetchedDependencies['skilled-cohort-flow'];
123
+ const unskilledFlowData = fetchedDependencies['unskilled-cohort-flow'];
124
+
125
+ const assetResult = this._calculateDivergence(skilledFlowData?.assets, unskilledFlowData?.assets);
126
+ const sectorResult = this._calculateDivergence(skilledFlowData?.sectors, unskilledFlowData?.sectors);
127
+
128
+ return {
129
+ assets: assetResult,
130
+ sectors: sectorResult
131
+ };
132
+ }
133
+
134
+ reset() {
135
+ // No state
136
+ }
137
+ }
138
+
139
+ module.exports = SkilledUnskilledDivergence;
@@ -0,0 +1,152 @@
1
+ /**
2
+ * @fileoverview Calculation (Pass 4) for the "Quant-Skill Alpha Signal".
3
+ *
4
+ * This metric synthesizes multiple Pass 1, 2, and 3 signals into a
5
+ * single, actionable "raw score" for each asset. It is designed to
6
+ * be the final, tradable signal from this computation branch.
7
+ *
8
+ * It weights:
9
+ * 1. Skilled/Unskilled Divergence (Pass 3)
10
+ * 2. Unskilled "FOMO" (from Cohort Momentum, Pass 3)
11
+ * 3. Platform vs. Sample Divergence (Pass 1)
12
+ * 4. Social Media Sentiment (Pass 1)
13
+ */
14
+ class QuantSkillAlphaSignal {
15
+ constructor() {
16
+ // Define the weights for the model.
17
+ // These would be optimized via backtesting.
18
+ this.W_DIVERGENCE = 0.40; // Skilled vs Unskilled
19
+ this.W_FOMO = 0.30; // Unskilled Momentum (faded)
20
+ this.W_PLATFORM = 0.15; // Sample vs Platform
21
+ this.W_SOCIAL = 0.15; // Social Sentiment
22
+ }
23
+
24
+ /**
25
+ * Defines the output schema for this calculation.
26
+ * @returns {object} JSON Schema object
27
+ */
28
+ static getSchema() {
29
+ const tickerSchema = {
30
+ "type": "object",
31
+ "properties": {
32
+ "raw_alpha_score": {
33
+ "type": "number",
34
+ "description": "The final weighted signal score. > 0 is bullish, < 0 is bearish."
35
+ },
36
+ "signal_status": {
37
+ "type": "string",
38
+ "description": "A human-readable signal (e.g., 'Strong Buy', 'Neutral')."
39
+ },
40
+ "component_divergence_score": { "type": "number" },
41
+ "component_unskilled_fomo_score": { "type": "number" },
42
+ "component_platform_divergence_score": { "type": "number" },
43
+ "component_social_sentiment_score": { "type": "number" }
44
+ },
45
+ "required": ["raw_alpha_score", "signal_status"]
46
+ };
47
+
48
+ return {
49
+ "type": "object",
50
+ "description": "A final, weighted alpha signal combining skill divergence, momentum, and sentiment.",
51
+ "patternProperties": {
52
+ "^.*$": tickerSchema // Ticker
53
+ },
54
+ "additionalProperties": tickerSchema
55
+ };
56
+ }
57
+
58
+ /**
59
+ * Statically declare dependencies.
60
+ */
61
+ static getDependencies() {
62
+ return [
63
+ 'gem_skilled-unskilled-divergence', // Pass 3
64
+ 'gem_cohort-momentum-state', // Pass 3
65
+ 'gem_platform-conviction-divergence', // Pass 1
66
+ 'gem_social_sentiment_aggregation' // Pass 1
67
+ ];
68
+ }
69
+
70
+ process() {
71
+ // No-op
72
+ }
73
+
74
+ /**
75
+ * This is a 'meta' calculation.
76
+ * @param {object} fetchedDependencies - Results from previous passes.
77
+ */
78
+ getResult(fetchedDependencies) {
79
+ const divergenceData = fetchedDependencies['skilled-unskilled-divergence']?.assets;
80
+ const momentumData = fetchedDependencies['cohort-momentum-state'];
81
+ const platformData = fetchedDependencies['platform-conviction-divergence'];
82
+ const socialData = fetchedDependencies['social_sentiment_aggregation']?.per_ticker;
83
+
84
+ if (!divergenceData || !momentumData || !platformData || !socialData) {
85
+ return {}; // Missing one or more key dependencies
86
+ }
87
+
88
+ const result = {};
89
+ const allTickers = new Set([
90
+ ...Object.keys(divergenceData),
91
+ ...Object.keys(momentumData),
92
+ ...Object.keys(platformData),
93
+ ...Object.keys(socialData)
94
+ ]);
95
+
96
+ for (const ticker of allTickers) {
97
+ // 1. Get Divergence Signal (Buy = +1, Sell = -1)
98
+ const divStatus = divergenceData[ticker]?.status;
99
+ const divScore = divStatus === 'Capitulation' ? 1.0 : (divStatus === 'Euphoria' ? -1.0 : 0);
100
+
101
+ // 2. Get Unskilled FOMO Signal (We fade this, so we use -1)
102
+ // 'unskilled_momentum_score' is high positive when they buy a rally
103
+ const fomoScore = momentumData[ticker]?.unskilled_momentum_score || 0;
104
+
105
+ // 3. Get Platform Divergence Signal
106
+ // 'divergence' is positive when our sample is more bullish than the platform
107
+ const platformScore = platformData[ticker]?.divergence || 0;
108
+
109
+ // 4. Get Social Sentiment Signal
110
+ // 'net_sentiment_pct' is positive when social is bullish
111
+ const socialScore = socialData[ticker]?.net_sentiment_pct || 0;
112
+
113
+ // Normalize scores to a similar range (approx -100 to 100)
114
+ // (This is a simple normalization; a z-score would be more robust)
115
+ const s_div = divScore * 100.0;
116
+ const s_fomo = -1 * fomoScore; // Fading the signal
117
+ const s_plat = platformScore; // Already 0-100
118
+ const s_soc = socialScore; // Already 0-100
119
+
120
+ // Calculate final weighted score
121
+ const raw_alpha_score =
122
+ (s_div * this.W_DIVERGENCE) +
123
+ (s_fomo * this.W_FOMO) +
124
+ (s_plat * this.W_PLATFORM) +
125
+ (s_soc * this.W_SOCIAL);
126
+
127
+ // Determine human-readable status
128
+ let status = 'Neutral';
129
+ if (raw_alpha_score > 30) status = 'Strong Buy';
130
+ else if (raw_alpha_score > 10) status = 'Buy';
131
+ else if (raw_alpha_score < -30) status = 'Strong Sell';
132
+ else if (raw_alpha_score < -10) status = 'Sell';
133
+
134
+ result[ticker] = {
135
+ raw_alpha_score: raw_alpha_score,
136
+ signal_status: status,
137
+ component_divergence_score: s_div,
138
+ component_unskilled_fomo_score: s_fomo,
139
+ component_platform_divergence_score: s_plat,
140
+ component_social_sentiment_score: s_soc
141
+ };
142
+ }
143
+
144
+ return result;
145
+ }
146
+
147
+ reset() {
148
+ // No state
149
+ }
150
+ }
151
+
152
+ module.exports = QuantSkillAlphaSignal;
@@ -21,8 +21,10 @@ class SocialTopicDriverIndex {
21
21
  "properties": {
22
22
  "topic": { "type": "string" },
23
23
  "driver_score": { "type": "number" },
24
- "correlation_flow_30d": { "type": "number" },
25
- "correlation_price_30d": { "type": "number" }
24
+ // These fields are from an older version but kept for schema
25
+ // compatibility. They will be null in the corrected logic.
26
+ "correlation_flow_30d": { "type": ["number", "null"] },
27
+ "correlation_price_30d": { "type": ["number", "null"] }
26
28
  },
27
29
  "required": ["topic", "driver_score"]
28
30
  };
@@ -48,6 +50,7 @@ class SocialTopicDriverIndex {
48
50
 
49
51
  /**
50
52
  * Statically declare dependencies.
53
+ * (This was already correct)
51
54
  */
52
55
  static getDependencies() {
53
56
  return [
@@ -59,6 +62,13 @@ class SocialTopicDriverIndex {
59
62
  // No-op
60
63
  }
61
64
 
65
+ /**
66
+ * --- LOGIC FIXED ---
67
+ * This function is rewritten to correctly consume the output of
68
+ * 'social-topic-predictive-potential'. It aggregates the
69
+ * 'predictivePotential' score for each topic across *all* tickers
70
+ * to create a global driver score.
71
+ */
62
72
  getResult(fetchedDependencies) {
63
73
  const potentialData = fetchedDependencies['social-topic-predictive-potential'];
64
74
 
@@ -67,23 +77,60 @@ class SocialTopicDriverIndex {
67
77
  all_topics: []
68
78
  };
69
79
 
70
- if (!potentialData) {
80
+ // The dependency returns a nested object. We need 'daily_topic_signals'.
81
+ const dailyTopicSignals = potentialData?.daily_topic_signals;
82
+
83
+ if (!dailyTopicSignals || Object.keys(dailyTopicSignals).length === 0) {
71
84
  return defaults;
72
85
  }
73
86
 
87
+ // Use a Map to aggregate scores for each topic
88
+ const topicAggregator = new Map();
89
+
90
+ // Iterate over each TICKER (e.g., 'AAPL', 'TSLA') in the signals
91
+ for (const tickerData of Object.values(dailyTopicSignals)) {
92
+
93
+ // Combine bullish and bearish topics for that ticker
94
+ const allTickerTopics = [
95
+ ...(tickerData.topPredictiveBullishTopics || []),
96
+ ...(tickerData.topPredictiveBearishTopics || [])
97
+ ];
98
+
99
+ // Iterate over the topics *for that ticker*
100
+ for (const topicData of allTickerTopics) {
101
+ const topicName = topicData.topic;
102
+
103
+ // Use the 'predictivePotential' score calculated by the dependency
104
+ const score = topicData.predictivePotential || 0;
105
+
106
+ if (!topicAggregator.has(topicName)) {
107
+ topicAggregator.set(topicName, { totalScore: 0, count: 0 });
108
+ }
109
+
110
+ const agg = topicAggregator.get(topicName);
111
+ agg.totalScore += score;
112
+ agg.count += 1;
113
+ }
114
+ }
115
+
74
116
  const allTopics = [];
75
- for (const [topic, data] of Object.entries(potentialData)) {
76
- // Create a "Driver Score" - prioritize flow correlation
77
- const score = (data.correlation_flow_30d * 0.7) + (data.correlation_price_30d * 0.3);
117
+ // Now, create the final ranked list
118
+ for (const [topic, data] of topicAggregator.entries()) {
119
+ if (data.count === 0) continue;
120
+
121
+ // Calculate the average driver score across all tickers
122
+ const avgScore = data.totalScore / data.count;
78
123
 
79
124
  allTopics.push({
80
125
  topic: topic,
81
- driver_score: score,
82
- correlation_flow_30d: data.correlation_flow_30d,
83
- correlation_price_30d: data.correlation_price_30d
126
+ driver_score: avgScore,
127
+ // Set old/incompatible fields to null to match schema
128
+ correlation_flow_30d: null,
129
+ correlation_price_30d: null
84
130
  });
85
131
  }
86
132
 
133
+ // Sort by the new, correct driver_score
87
134
  allTopics.sort((a, b) => b.driver_score - a.driver_score);
88
135
 
89
136
  return {
@@ -152,7 +152,8 @@ function _findPriceForward(instrumentId, dateStr, priceMap) {
152
152
  class SocialTopicPredictivePotentialIndex {
153
153
 
154
154
  static getDependencies() {
155
- return ['social-topic-driver-index'];
155
+ // --- FIX 1: Changed from 'social-topic-driver-index' to break the cycle ---
156
+ return ['social-topic-sentiment-matrix'];
156
157
  }
157
158
 
158
159
  constructor() {
@@ -195,10 +196,13 @@ class SocialTopicPredictivePotentialIndex {
195
196
  // pLimit is not in calculationUtils by default, so we'll use our own
196
197
  // If it were, we'd use: this.pLimit = calculationUtils.pLimit(MAX_CONCURRENT_TRANSACTIONS);
197
198
  await this._loadDependencies(calculationUtils);
198
- const todaySignals = fetchedDependencies['social-topic-driver-index'];
199
+
200
+ // --- FIX 2: Read from the correct dependency ---
201
+ const todaySignals = fetchedDependencies['social-topic-sentiment-matrix'];
199
202
 
200
203
  if (!todaySignals || Object.keys(todaySignals).length === 0) {
201
- logger.log('WARN', `[SocialTopicPredictive] Missing or empty dependency 'social-topic-driver-index' for ${dateStr}. Skipping.`);
204
+ // --- FIX 2.1: Updated log message ---
205
+ logger.log('WARN', `[SocialTopicPredictive] Missing or empty dependency 'social-topic-sentiment-matrix' for ${dateStr}. Skipping.`);
202
206
  return null;
203
207
  }
204
208
 
@@ -264,6 +268,8 @@ class SocialTopicPredictivePotentialIndex {
264
268
  this._updateForwardReturns(state, instrumentId, dateStr, todayPrice, this.priceMap);
265
269
 
266
270
  // --- 4c. Add New Signals (Factored Helper) ---
271
+ // We assume 'todaySignal' (from social-topic-sentiment-matrix)
272
+ // has an 'allDrivers' property.
267
273
  this._addNewSignals(state, todaySignal.allDrivers || [], dateStr);
268
274
 
269
275
  // --- 4d. Recalculate Correlations (Factored Helper) ---