aiden-shared-calculations-unified 1.0.46 → 1.0.48

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,127 @@
1
+ /**
2
+ * @fileoverview Meta-calculation (Pass 4) that correlates the asset/sector flow
3
+ * of the "Smart Cohort" vs. the "Dumb Cohort" to find divergence signals.
4
+ *
5
+ * --- META REFACTOR (v2) ---
6
+ * This calculation is now stateless. It declares its dependencies and
7
+ * expects them to be passed to its `process` method.
8
+ */
9
+
10
+ class SmartDumbDivergenceIndex {
11
+
12
+ /**
13
+ * (NEW) Statically declare dependencies.
14
+ */
15
+ static getDependencies() {
16
+ return ['positive-expectancy-cohort-flow', 'negative-expectancy-cohort-flow'];
17
+ }
18
+
19
+ constructor() {
20
+ // Minimum net flow (as a percentage) to be considered a signal
21
+ this.FLOW_THRESHOLD = 0.005; // Formerly 0.5
22
+ }
23
+
24
+ /**
25
+ * REFACTORED PROCESS METHOD
26
+ * @param {string} dateStr The date to run the analysis for (e.g., "2025-10-31").
27
+ * @param {object} dependencies The shared dependencies (db, logger).
28
+ * @param {object} config The computation system configuration.
29
+ * @param {object} fetchedDependencies In-memory results from previous passes.
30
+ * e.g., { 'smart-cohort-flow': ..., 'dumb-cohort-flow': ... }
31
+ * @returns {Promise<object|null>} The analysis result or null.
32
+ */
33
+ async process(dateStr, dependencies, config, fetchedDependencies) {
34
+ const { logger } = dependencies;
35
+
36
+ // 1. Get dependencies from the new argument
37
+ const smartData = fetchedDependencies['positive-expectancy-cohort-flow'];
38
+ const dumbData = fetchedDependencies['negative-expectancy-cohort-flow'];
39
+
40
+ // 2. Handle missing dependencies
41
+ if (!smartData || !dumbData) {
42
+ logger.log('WARN', `[SmartDumbDivergence] Missing cohort flow data dependency for ${dateStr}. Skipping.`);
43
+ return null;
44
+ }
45
+
46
+ const results = {
47
+ assets: {},
48
+ sectors: {}
49
+ };
50
+
51
+ const smartAssetFlow = smartData.asset_flow;
52
+ const dumbAssetFlow = dumbData.asset_flow;
53
+ const smartSectorFlow = smartData.sector_rotation;
54
+ const dumbSectorFlow = dumbData.sector_rotation;
55
+
56
+ if (!smartAssetFlow || !dumbAssetFlow || !smartSectorFlow || !dumbSectorFlow) {
57
+ logger.log('WARN', `[SmartDumbDivergence] Dependency data for ${dateStr} is incomplete (missing asset_flow or sector_rotation). Skipping.`);
58
+ return null;
59
+ }
60
+
61
+ // 3. Correlate Assets
62
+ const allTickers = new Set([...Object.keys(smartAssetFlow), ...Object.keys(dumbAssetFlow)]);
63
+ for (const ticker of allTickers) {
64
+ const sFlow = smartAssetFlow[ticker]?.net_crowd_flow_pct || 0;
65
+ const dFlow = dumbAssetFlow[ticker]?.net_crowd_flow_pct || 0;
66
+
67
+ const smartBuys = sFlow >= this.FLOW_THRESHOLD;
68
+ const smartSells = sFlow <= -this.FLOW_THRESHOLD;
69
+ const dumbBuys = dFlow >= this.FLOW_THRESHOLD;
70
+ const dumbSells = dFlow <= -this.FLOW_THRESHOLD;
71
+
72
+ let status = 'No_Divergence';
73
+ let detail = 'Cohorts are aligned or flow is insignificant.';
74
+
75
+ if (smartBuys && dumbSells) {
76
+ status = 'Capitulation';
77
+ detail = 'Smart cohort is buying the dip from the panic-selling dumb cohort.';
78
+ } else if (smartSells && dumbBuys) {
79
+ status = 'Euphoria';
80
+ detail = 'Smart cohort is selling into the FOMO-buying dumb cohort.';
81
+ } else if (smartBuys && dumbBuys) {
82
+ status = 'Aligned_Buy';
83
+ } else if (smartSells && dumbSells) {
84
+ status = 'Aligned_Sell';
85
+ }
86
+
87
+ if (status !== 'No_Divergence') {
88
+ results.assets[ticker] = {
89
+ status: status,
90
+ detail: detail,
91
+ smart_cohort_flow_pct: sFlow,
92
+ dumb_cohort_flow_pct: dFlow
93
+ };
94
+ }
95
+ }
96
+
97
+ // 4. Correlate Sectors
98
+ const allSectors = new Set([...Object.keys(smartSectorFlow), ...Object.keys(dumbSectorFlow)]);
99
+ for (const sector of allSectors) {
100
+ const sFlow = smartSectorFlow[sector] || 0;
101
+ const dFlow = dumbSectorFlow[sector] || 0;
102
+
103
+ let status = 'No_Divergence';
104
+
105
+ if (sFlow > 0 && dFlow < 0) {
106
+ status = 'Capitulation';
107
+ } else if (sFlow < 0 && dFlow > 0) {
108
+ status = 'Euphoria';
109
+ }
110
+
111
+ if (status !== 'No_Divergence') {
112
+ results.sectors[sector] = {
113
+ status: status,
114
+ smart_cohort_flow_usd: sFlow,
115
+ dumb_cohort_flow_usd: dFlow
116
+ };
117
+ }
118
+ }
119
+
120
+ return results;
121
+ }
122
+
123
+ async getResult() { return null; }
124
+ reset() {}
125
+ }
126
+
127
+ module.exports = SmartDumbDivergenceIndex;
@@ -0,0 +1,181 @@
1
+ /**
2
+ * @fileoverview NEW CALCULATION (PASS 4 - META)
3
+ * This is the "Quant Regime Engine" or "Master Switch."
4
+ *
5
+ * It determines if the entire class of "social-to-price" signals
6
+ * is currently effective ("ON") or not ("OFF").
7
+ *
8
+ * It is a STATEFUL, ROLLING calculation.
9
+ * 1. It consumes the 'daily_topic_signals' from Pass 3 for the current day.
10
+ * 2. It reads its *own* rolling history of past "breadth" metrics
11
+ * from a single document in Firestore ('social_prediction_regime_state/history').
12
+ * 3. It calculates the "predictive breadth" for the current day:
13
+ * (What % of all topics analyzed had a Predictive Potential > threshold?)
14
+ * 4. It appends this new breadth metric to its history.
15
+ * 5. It calculates a short-term and long-term EMA of this breadth metric.
16
+ * 6. It uses the EMA crossover to determine the `regime_state`.
17
+ * 7. It returns its updated state AND the daily regime signal.
18
+ */
19
+
20
+ const { FieldValue } = require('@google-cloud/firestore');
21
+
22
+ // --- CONFIGURATION ---
23
+ // The single doc where this calc stores its rolling history
24
+ const STATE_DOC_PATH = 'social_prediction_regime_state/history';
25
+ const ROLLING_HISTORY_DAYS = 90; // History for EMAs
26
+ const EMA_SHORT_PERIOD = 3; // 3-day EMA
27
+ const EMA_LONG_PERIOD = 14; // 14-day EMA
28
+ // How far apart the EMAs must be to declare a firm regime
29
+ const REGIME_THRESHOLD = 0.02; // e.g., 2% difference
30
+ // The Predictive Potential (from Pass 3) threshold to be counted
31
+ const PP_THRESHOLD = 0.5;
32
+
33
+ // --- STATS HELPER ---
34
+ /**
35
+ * Calculates a simple Exponential Moving Average (EMA) from an array of values.
36
+ * @param {number[]} data - Array of numbers (oldest to newest).
37
+ * @param {number} period - The EMA period.
38
+ * @returns {number|null} The final EMA value, or null if not enough data.
39
+ */
40
+ function _calculateEMA(data, period) {
41
+ if (data.length < period) {
42
+ return null;
43
+ }
44
+ const k = 2 / (period + 1); // Smoothing factor
45
+ let ema = data[0]; // Start with the first value
46
+ for (let i = 1; i < data.length; i++) {
47
+ ema = (data[i] * k) + (ema * (1 - k));
48
+ }
49
+ return ema;
50
+ }
51
+
52
+
53
+ class SocialPredictiveRegimeState {
54
+
55
+ static getDependencies() {
56
+ // Depends on the *daily output* from Pass 3
57
+ return ['social-topic-predictive-potential'];
58
+ }
59
+
60
+ constructor() {}
61
+
62
+ /**
63
+ * @param {string} dateStr The date to run the analysis for (e.g., "2025-11-06").
64
+ * @param {object} dependencies The shared dependencies (db, logger).
65
+ * @param {object} config The computation system configuration.
66
+ * @param {object} fetchedDependencies In-memory results from Pass 3.
67
+ * @returns {Promise<object|null>} The analysis result or null.
68
+ */
69
+ async process(dateStr, dependencies, config, fetchedDependencies) {
70
+ const { db, logger } = dependencies;
71
+
72
+ // 1. Get Pass 3 Dependency
73
+ const pass3_output = fetchedDependencies['social-topic-predictive-potential'];
74
+ if (!pass3_output || !pass3_output.daily_topic_signals) {
75
+ logger.log('WARN', `[SocialRegime] Missing or empty dependency 'social-topic-predictive-potential' for ${dateStr}. Skipping.`);
76
+ return null;
77
+ }
78
+ const daily_signals = pass3_output.daily_topic_signals;
79
+
80
+ // 2. Calculate Today's "Predictive Breadth"
81
+ let totalTopicsAnalyzed = 0;
82
+ let totalPredictiveTopics = 0;
83
+
84
+ for (const ticker in daily_signals) {
85
+ const allTopics = [
86
+ ...(daily_signals[ticker].topPredictiveBullishTopics || []),
87
+ ...(daily_signals[ticker].topPredictiveBearishTopics || [])
88
+ ];
89
+
90
+ for (const topic of allTopics) {
91
+ totalTopicsAnalyzed++;
92
+ // Check if this topic is "predictive"
93
+ if (topic.predictivePotential > PP_THRESHOLD) {
94
+ // Check if it has at least one stable window
95
+ const hasStableWindow = Object.values(topic.correlations).some(
96
+ corr => corr.samples >= MIN_SAMPLE_COUNT && Math.abs(corr.value) > CORR_THRESHOLD
97
+ );
98
+ if (hasStableWindow) {
99
+ totalPredictiveTopics++;
100
+ }
101
+ }
102
+ }
103
+ }
104
+
105
+ const todayBreadth = (totalTopicsAnalyzed > 0)
106
+ ? (totalPredictiveTopics / totalTopicsAnalyzed)
107
+ : 0; // 0% breadth if no topics found
108
+
109
+ // 3. Load State, Update, and Save (all in one transaction)
110
+
111
+ let dailyOutput = {}; // The daily signal for the API
112
+ let stateToSave = {}; // The new state to save back to Firestore
113
+
114
+ try {
115
+ await db.runTransaction(async (transaction) => {
116
+ const docRef = db.doc(STATE_DOC_PATH);
117
+ const doc = await transaction.get(docRef);
118
+
119
+ // 4a. Load and update history
120
+ const history = doc.exists ? (doc.data().history || []) : [];
121
+ history.push({ date: dateStr, breadth: todayBreadth });
122
+
123
+ // 4b. Prune history
124
+ const prunedHistory = history.slice(-ROLLING_HISTORY_DAYS);
125
+
126
+ // 4c. Calculate EMAs
127
+ const breadthValues = prunedHistory.map(h => h.breadth);
128
+ const emaShort = _calculateEMA(breadthValues, EMA_SHORT_PERIOD);
129
+ const emaLong = _calculateEMA(breadthValues, EMA_LONG_PERIOD);
130
+
131
+ // 4d. Determine Regime State
132
+ let regimeState = "TRANSITIONING";
133
+ if (emaShort !== null && emaLong !== null) {
134
+ const diff = emaShort - emaLong;
135
+ if (diff > REGIME_THRESHOLD) {
136
+ regimeState = "ON";
137
+ } else if (diff < -REGIME_THRESHOLD) {
138
+ regimeState = "OFF";
139
+ }
140
+ }
141
+
142
+ // 4e. Prepare data to save and return
143
+ stateToSave = {
144
+ history: prunedHistory,
145
+ lastUpdated: FieldValue.serverTimestamp()
146
+ };
147
+
148
+ dailyOutput = {
149
+ social_predictive_regime_strength: emaShort, // The "fast" signal
150
+ regime_state: regimeState,
151
+ components: {
152
+ today_breadth_pct: todayBreadth,
153
+ ema_short: emaShort,
154
+ ema_long: emaLong,
155
+ threshold: REGIME_THRESHOLD
156
+ }
157
+ };
158
+
159
+ // 4f. Save state back to Firestore
160
+ transaction.set(docRef, stateToSave);
161
+ });
162
+
163
+ } catch (error) {
164
+ logger.log('ERROR', `[SocialRegime] Transaction failed for ${dateStr}`, { err: error.message });
165
+ return null; // Don't return partial data
166
+ }
167
+
168
+ // 5. Return both state (for sharding) and daily signal (for API)
169
+ return {
170
+ // This key name must be unique
171
+ sharded_regime_state: { [STATE_DOC_PATH]: stateToSave },
172
+ // This is the daily result for unified_insights
173
+ daily_regime_signal: dailyOutput
174
+ };
175
+ }
176
+
177
+ async getResult() { return null; } // Logic is in process()
178
+ reset() {}
179
+ }
180
+
181
+ module.exports = SocialPredictiveRegimeState;
@@ -0,0 +1,153 @@
1
+ /**
2
+ * @fileoverview NEW CALCULATION (PASS 2 - META)
3
+ * This is the advanced signal calculation. It consumes the
4
+ * 'social-topic-sentiment-matrix' from Pass 1, loads the
5
+ * daily price change for each asset, and then determines
6
+ * the primary bullish and bearish topics driving the conversation.
7
+ */
8
+
9
+ // We need calculation utils to load price and mapping data
10
+ const { loadAllPriceData, getDailyPriceChange } = require('../../utils/price_data_provider');
11
+ const { loadInstrumentMappings } = require('../../utils/sector_mapping_provider');
12
+
13
+ class SocialTopicDriverIndex {
14
+
15
+ /**
16
+ * Statically declare dependencies.
17
+ * This will run in Pass 2, after the matrix is built.
18
+ */
19
+ static getDependencies() {
20
+ return ['social-topic-sentiment-matrix'];
21
+ }
22
+
23
+ constructor() {
24
+ this.priceMap = null;
25
+ this.tickerToIdMap = null; // For { "AAPL" -> 123 }
26
+ this.dependenciesLoaded = false;
27
+ }
28
+
29
+ /**
30
+ * Helper to load price/mapping data on the first run.
31
+ */
32
+ async _loadDependencies(calculationUtils) {
33
+ if (this.dependenciesLoaded) return;
34
+
35
+ const [priceData, mappings] = await Promise.all([
36
+ calculationUtils.loadAllPriceData(),
37
+ calculationUtils.loadInstrumentMappings()
38
+ ]);
39
+
40
+ this.priceMap = priceData;
41
+
42
+ // Create the Ticker -> InstrumentID map
43
+ this.tickerToIdMap = {};
44
+ if (mappings && mappings.instrumentToTicker) {
45
+ for (const [id, ticker] of Object.entries(mappings.instrumentToTicker)) {
46
+ this.tickerToIdMap[ticker] = id;
47
+ }
48
+ }
49
+
50
+ this.dependenciesLoaded = true;
51
+ }
52
+
53
+ /**
54
+ * Helper to get the previous day's date string.
55
+ */
56
+ _getYesterday(dateStr) {
57
+ const date = new Date(dateStr + 'T00:00:00Z');
58
+ date.setUTCDate(date.getUTCDate() - 1);
59
+ return date.toISOString().slice(0, 10);
60
+ }
61
+
62
+ /**
63
+ * @param {string} dateStr The date to run the analysis for (e.g., "2025-11-05").
64
+ * @param {object} dependencies The shared dependencies (db, logger, calculationUtils).
65
+ * @param {object} config The computation system configuration.
66
+ * @param {object} fetchedDependencies In-memory results from Pass 1.
67
+ * e.g., { 'social-topic-sentiment-matrix': { "AAPL": { ... } } }
68
+ * @returns {Promise<object|null>} The analysis result or null.
69
+ */
70
+ async process(dateStr, dependencies, config, fetchedDependencies) {
71
+ const { logger, calculationUtils } = dependencies;
72
+
73
+ // 1. Load dependencies
74
+ await this._loadDependencies(calculationUtils);
75
+ const matrix = fetchedDependencies['social-topic-sentiment-matrix'];
76
+
77
+ if (!matrix || Object.keys(matrix).length === 0) {
78
+ logger.log('WARN', `[SocialTopicDriverIndex] Missing or empty dependency 'social-topic-sentiment-matrix' for ${dateStr}. Skipping.`);
79
+ return null;
80
+ }
81
+
82
+ if (!this.priceMap || !this.tickerToIdMap) {
83
+ logger.log('ERROR', `[SocialTopicDriverIndex] Price map or Ticker map failed to load. Aborting.`);
84
+ return null;
85
+ }
86
+
87
+ const yesterdayStr = this._getYesterday(dateStr); // e.g., "2025-11-04"
88
+ const finalResults = {};
89
+
90
+ // 2. Correlate for each ticker
91
+ for (const ticker in matrix) {
92
+ const topicData = matrix[ticker];
93
+ const instrumentId = this.tickerToIdMap[ticker];
94
+
95
+ if (!instrumentId) {
96
+ logger.log('TRACE', `[SocialTopicDriverIndex] Skipping ${ticker}, no instrumentId found.`);
97
+ continue;
98
+ }
99
+
100
+ // 3. Get Price Change
101
+ // This correctly uses the resilient _findPreviousAvailablePrice helper
102
+ const priceChangePct = getDailyPriceChange(instrumentId, yesterdayStr, dateStr, this.priceMap);
103
+
104
+ if (priceChangePct === null) {
105
+ logger.log('TRACE', `[SocialTopicDriverIndex] Skipping ${ticker}, no price data found for ${dateStr}.`);
106
+ continue; // Skip if we can't get price data
107
+ }
108
+
109
+ // 4. Analyze topics for this ticker
110
+ const topicDrivers = [];
111
+ for (const topic in topicData) {
112
+ const data = topicData[topic];
113
+ const totalSentientPosts = data.bullishPosts + data.bearishPosts;
114
+
115
+ let sentimentScore = 0;
116
+ if (totalSentientPosts > 0) {
117
+ sentimentScore = (data.bullishPosts - data.bearishPosts) / totalSentientPosts;
118
+ }
119
+
120
+ topicDrivers.push({
121
+ topic: topic,
122
+ sentimentScore: sentimentScore, // -1 (Bearish) to +1 (Bullish)
123
+ convictionScore: data.convictionScore,
124
+ totalPosts: data.totalPosts,
125
+ bullishPosts: data.bullishPosts,
126
+ bearishPosts: data.bearishPosts
127
+ });
128
+ }
129
+
130
+ // 5. Find top drivers by conviction
131
+ // Sort by most conviction (likes/comments)
132
+ topicDrivers.sort((a, b) => b.convictionScore - a.convictionScore);
133
+
134
+ // Find the most-discussed bullish and bearish topics
135
+ const topBullishDriver = topicDrivers.find(d => d.sentimentScore > 0.1) || null;
136
+ const topBearishDriver = topicDrivers.find(d => d.sentimentScore < -0.1) || null;
137
+
138
+ finalResults[ticker] = {
139
+ priceChangePct: priceChangePct,
140
+ topBullishDriver: topBullishDriver,
141
+ topBearishDriver: topBearishDriver,
142
+ allDrivers: topicDrivers // Store all for potential future analysis
143
+ };
144
+ }
145
+
146
+ return finalResults;
147
+ }
148
+
149
+ async getResult() { return null; } // This is a meta-calc, logic is in process()
150
+ reset() {}
151
+ }
152
+
153
+ module.exports = SocialTopicDriverIndex;