aiden-shared-calculations-unified 1.0.35 → 1.0.37

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 (58) hide show
  1. package/README.MD +77 -77
  2. package/calculations/activity/historical/activity_by_pnl_status.js +85 -85
  3. package/calculations/activity/historical/daily_asset_activity.js +85 -85
  4. package/calculations/activity/historical/daily_user_activity_tracker.js +144 -144
  5. package/calculations/activity/historical/speculator_adjustment_activity.js +76 -76
  6. package/calculations/asset_metrics/asset_position_size.js +57 -57
  7. package/calculations/backtests/strategy-performance.js +229 -245
  8. package/calculations/behavioural/historical/asset_crowd_flow.js +165 -165
  9. package/calculations/behavioural/historical/drawdown_response.js +58 -58
  10. package/calculations/behavioural/historical/dumb-cohort-flow.js +217 -249
  11. package/calculations/behavioural/historical/gain_response.js +57 -57
  12. package/calculations/behavioural/historical/in_loss_asset_crowd_flow.js +98 -98
  13. package/calculations/behavioural/historical/in_profit_asset_crowd_flow.js +99 -99
  14. package/calculations/behavioural/historical/paper_vs_diamond_hands.js +39 -39
  15. package/calculations/behavioural/historical/position_count_pnl.js +67 -67
  16. package/calculations/behavioural/historical/smart-cohort-flow.js +217 -250
  17. package/calculations/behavioural/historical/smart_money_flow.js +165 -165
  18. package/calculations/behavioural/historical/user-investment-profile.js +358 -412
  19. package/calculations/capital_flow/historical/crowd-cash-flow-proxy.js +121 -121
  20. package/calculations/capital_flow/historical/deposit_withdrawal_percentage.js +117 -117
  21. package/calculations/capital_flow/historical/new_allocation_percentage.js +49 -49
  22. package/calculations/insights/daily_bought_vs_sold_count.js +55 -55
  23. package/calculations/insights/daily_buy_sell_sentiment_count.js +49 -49
  24. package/calculations/insights/daily_ownership_delta.js +55 -55
  25. package/calculations/insights/daily_total_positions_held.js +39 -39
  26. package/calculations/meta/capital_deployment_strategy.js +129 -137
  27. package/calculations/meta/capital_liquidation_performance.js +121 -163
  28. package/calculations/meta/capital_vintage_performance.js +121 -158
  29. package/calculations/meta/cash-flow-deployment.js +110 -124
  30. package/calculations/meta/cash-flow-liquidation.js +126 -142
  31. package/calculations/meta/crowd_sharpe_ratio_proxy.js +83 -91
  32. package/calculations/meta/profit_cohort_divergence.js +77 -91
  33. package/calculations/meta/smart-dumb-divergence-index.js +116 -138
  34. package/calculations/meta/social_flow_correlation.js +99 -125
  35. package/calculations/pnl/asset_pnl_status.js +46 -46
  36. package/calculations/pnl/historical/profitability_migration.js +57 -57
  37. package/calculations/pnl/historical/user_profitability_tracker.js +117 -117
  38. package/calculations/pnl/profitable_and_unprofitable_status.js +64 -64
  39. package/calculations/sectors/historical/diversification_pnl.js +76 -76
  40. package/calculations/sectors/historical/sector_rotation.js +67 -67
  41. package/calculations/sentiment/historical/crowd_conviction_score.js +80 -80
  42. package/calculations/socialPosts/social-asset-posts-trend.js +52 -52
  43. package/calculations/socialPosts/social-top-mentioned-words.js +102 -102
  44. package/calculations/socialPosts/social-topic-interest-evolution.js +53 -53
  45. package/calculations/socialPosts/social-word-mentions-trend.js +62 -62
  46. package/calculations/socialPosts/social_activity_aggregation.js +103 -103
  47. package/calculations/socialPosts/social_event_correlation.js +121 -121
  48. package/calculations/socialPosts/social_sentiment_aggregation.js +114 -114
  49. package/calculations/speculators/historical/risk_appetite_change.js +54 -54
  50. package/calculations/speculators/historical/tsl_effectiveness.js +74 -74
  51. package/index.js +33 -33
  52. package/package.json +32 -32
  53. package/utils/firestore_utils.js +76 -76
  54. package/utils/price_data_provider.js +142 -142
  55. package/utils/sector_mapping_provider.js +74 -74
  56. package/calculations/capital_flow/historical/reallocation_increase_percentage.js +0 -63
  57. package/calculations/speculators/stop_loss_distance_by_sector_short_long_breakdown.js +0 -91
  58. package/calculations/speculators/stop_loss_distance_by_ticker_short_long_breakdown.js +0 -73
@@ -1,143 +1,127 @@
1
- /**
2
- * @fileoverview Correlates a crowd-wide withdrawal signal with the specific assets
3
- * that are being sold (liquidated) to fund those withdrawals.
4
- * This is a meta-calculation that runs in Pass 3.
5
- */
6
-
7
- const { FieldValue } = require('@google-cloud/firestore');
8
-
9
- class CashFlowLiquidation {
10
- constructor() {
11
- this.lookbackDays = 7;
12
- this.correlationWindow = 3;
13
- // A positive value signals a net crowd withdrawal
14
- this.withdrawalSignalThreshold = 0.005; // Formerly 1.0
15
- }
16
-
17
- _getDateStr(baseDate, daysAgo) {
18
- const date = new Date(baseDate + 'T00:00:00Z');
19
- date.setUTCDate(date.getUTCDate() - daysAgo);
20
- return date.toISOString().slice(0, 10);
21
- }
22
-
23
- /**
24
- * @param {string} dateStr The date to run the analysis for (e.g., "2025-10-31").
25
- * @param {object} dependencies The shared dependencies (db, logger).
26
- * @param {object} config The computation system configuration.
27
- * @returns {Promise<object|null>} The analysis result or null.
28
- */
29
- async process(dateStr, dependencies, config) {
30
- const { db, logger } = dependencies;
31
- const collection = config.resultsCollection;
32
-
33
- // 1. Build all needed refs in advance for this day
34
- const dateRefs = [];
35
- const dates = [];
36
-
37
- // Refs for the lookback period (for the signal)
38
- for (let i = 1; i <= this.lookbackDays; i++) {
39
- const checkDate = this._getDateStr(dateStr, i);
40
- dates.push({ date: checkDate, category: 'capital_flow', computation: 'crowd-cash-flow-proxy' });
41
- }
42
-
43
- // Refs for today's two dependencies
44
- dates.push({ date: dateStr, category: 'capital_flow', computation: 'crowd-cash-flow-proxy' });
45
- dates.push({ date: dateStr, category: 'behavioural', computation: 'asset-crowd-flow' });
46
-
47
- // Build refs array
48
- const refs = dates.map(d =>
49
- db.collection(collection).doc(d.date)
50
- .collection('results').doc(d.category)
51
- .collection('computations').doc(d.computation)
52
- );
53
-
54
- const snapshots = await db.getAll(...refs);
55
-
56
- // Build map(path -> data)
57
- const dataMap = new Map();
58
- snapshots.forEach((snap, idx) => {
59
- if (snap.exists) dataMap.set(idx, snap.data());
60
- });
61
-
62
- // --- START MODIFICATION ---
63
- // Check for THIS DATE's dependencies (cash-flow and asset-flow) FIRST.
64
- const cashFlowData = dataMap.get(this.lookbackDays);
65
- const assetFlowData = dataMap.get(this.lookbackDays + 1);
66
-
67
- if (!cashFlowData || !assetFlowData) {
68
- logger.log('WARN', `[CashFlowLiquidation] Missing critical dependency data (cash-flow or asset-flow) for ${dateStr}. Skipping to allow backfill.`);
69
- return null; // This allows backfill
70
- }
71
- // --- END MODIFICATION ---
72
-
73
-
74
- // 2. Find the withdrawal signal
75
- let withdrawalSignal = null;
76
- let withdrawalSignalDay = null;
77
-
78
- for (let i = 0; i < this.lookbackDays; i++) {
79
- const flowData = dataMap.get(i);
80
- const dateUsed = dates[i].date;
81
- // INVERTED LOGIC: Look for a POSITIVE value
82
- if (flowData && flowData.cash_flow_effect_proxy > this.withdrawalSignalThreshold) {
83
- withdrawalSignal = flowData;
84
- withdrawalSignalDay = dateUsed;
85
- break; // Found the most recent signal
86
- }
87
- }
88
-
89
- if (!withdrawalSignal) {
90
- return {
91
- status: 'no_withdrawal_signal_found',
92
- lookback_days: this.lookbackDays,
93
- signal_threshold: this.withdrawalSignalThreshold
94
- };
95
- }
96
-
97
- const daysSinceSignal = (new Date(dateStr) - new Date(withdrawalSignalDay)) / (1000 * 60 * 60 * 24);
98
-
99
- if (daysSinceSignal <= 0 || daysSinceSignal > this.correlationWindow) {
100
- return {
101
- status: 'outside_correlation_window',
102
- signal_day: withdrawalSignalDay,
103
- days_since_signal: daysSinceSignal
104
- };
105
- }
106
-
107
- // --- REMOVED ---
108
- // The check for cashFlowData and assetFlowData was here
109
- // but has been moved up.
110
- // --- END REMOVED ---
111
-
112
- // 'trading_effect' will be negative if the crowd is net-selling
113
- const netSellPct = cashFlowData.components?.trading_effect || 0;
114
- const netWithdrawalPct = Math.abs(withdrawalSignal.cash_flow_effect_proxy);
115
-
116
- // INVERTED LOGIC: Find top *sells*
117
- const topLiquidations = Object.entries(assetFlowData)
118
- .filter(([ticker, data]) => data.net_crowd_flow_pct < 0) // Find assets with negative flow
119
- .sort(([, a], [, b]) => a.net_crowd_flow_pct - b.net_crowd_flow_pct) // Sort ascending (most negative first)
120
- .slice(0, 10)
121
- .map(([ticker, data]) => ({
122
- ticker,
123
- net_flow_pct: data.net_crowd_flow_pct
124
- }));
125
-
126
- return {
127
- status: 'analysis_complete',
128
- analysis_date: dateStr,
129
- signal_date: withdrawalSignalDay,
130
- days_since_signal: daysSinceSignal,
131
- signal_withdrawal_proxy_pct: netWithdrawalPct,
132
- day_net_sell_pct: netSellPct, // This value should be negative
133
- pct_of_withdrawal_funded_today: (Math.abs(netSellPct) / netWithdrawalPct) * 100,
134
- top_liquidation_assets: topLiquidations
135
- };
136
- }
137
-
138
- // Must exist for the meta-computation runner
139
- async getResult() { return null; }
140
- reset() {}
141
- }
142
-
1
+ /**
2
+ * @fileoverview Correlates a crowd-wide withdrawal signal with the specific assets
3
+ * that are being sold (liquidated) to fund those withdrawals.
4
+ * This is a meta-calculation that runs in Pass 3.
5
+ */
6
+
7
+ const { FieldValue } = require('@google-cloud/firestore');
8
+
9
+ class CashFlowLiquidation {
10
+ constructor() {
11
+ this.lookbackDays = 7;
12
+ this.correlationWindow = 3;
13
+ // A positive value signals a net crowd withdrawal
14
+ this.withdrawalSignalThreshold = 0.005; // Formerly 1.0
15
+ }
16
+
17
+ _getDateStr(baseDate, daysAgo) {
18
+ const date = new Date(baseDate + 'T00:00:00Z');
19
+ date.setUTCDate(date.getUTCDate() - daysAgo);
20
+ return date.toISOString().slice(0, 10);
21
+ }
22
+
23
+ /**
24
+ * @param {string} dateStr The date to run the analysis for (e.g., "2025-10-31").
25
+ * @param {object} dependencies The shared dependencies (db, logger).
26
+ * @param {object} config The computation system configuration.
27
+ * @param {object} computedDependencies In-memory results from previous passes.
28
+ * @returns {Promise<object|null>} The analysis result or null.
29
+ */
30
+ async process(dateStr, dependencies, config, computedDependencies) {
31
+ const { db, logger } = dependencies;
32
+ const collection = config.resultsCollection;
33
+
34
+ // --- MODIFICATION: Get same-day dependencies from cache ---
35
+ const cashFlowData = computedDependencies['crowd-cash-flow-proxy'];
36
+ const assetFlowData = computedDependencies['asset-crowd-flow'];
37
+
38
+ if (!cashFlowData || !assetFlowData) {
39
+ logger.log('WARN', `[CashFlowLiquidation] Missing critical in-memory dependency data for ${dateStr}. Skipping.`);
40
+ return null;
41
+ }
42
+ // --- END MODIFICATION ---
43
+
44
+ // --- Historical lookback for signal still uses Firestore ---
45
+ const dates = [];
46
+ for (let i = 1; i <= this.lookbackDays; i++) {
47
+ const checkDate = this._getDateStr(dateStr, i);
48
+ dates.push({ date: checkDate, category: 'capital_flow', computation: 'crowd-cash-flow-proxy' });
49
+ }
50
+ const refs = dates.map(d =>
51
+ db.collection(collection).doc(d.date)
52
+ .collection('results').doc(d.category)
53
+ .collection('computations').doc(d.computation)
54
+ );
55
+ const snapshots = await db.getAll(...refs);
56
+ const dataMap = new Map();
57
+ snapshots.forEach((snap, idx) => {
58
+ if (snap.exists) dataMap.set(idx, snap.data());
59
+ });
60
+ // --- End historical lookback ---
61
+
62
+
63
+ // 2. Find the withdrawal signal
64
+ let withdrawalSignal = null;
65
+ let withdrawalSignalDay = null;
66
+
67
+ for (let i = 0; i < this.lookbackDays; i++) {
68
+ const flowData = dataMap.get(i);
69
+ const dateUsed = dates[i].date;
70
+ // INVERTED LOGIC: Look for a POSITIVE value
71
+ if (flowData && flowData.cash_flow_effect_proxy > this.withdrawalSignalThreshold) {
72
+ withdrawalSignal = flowData;
73
+ withdrawalSignalDay = dateUsed;
74
+ break; // Found the most recent signal
75
+ }
76
+ }
77
+
78
+ if (!withdrawalSignal) {
79
+ return {
80
+ status: 'no_withdrawal_signal_found',
81
+ lookback_days: this.lookbackDays,
82
+ signal_threshold: this.withdrawalSignalThreshold
83
+ };
84
+ }
85
+
86
+ const daysSinceSignal = (new Date(dateStr) - new Date(withdrawalSignalDay)) / (1000 * 60 * 60 * 24);
87
+
88
+ if (daysSinceSignal <= 0 || daysSinceSignal > this.correlationWindow) {
89
+ return {
90
+ status: 'outside_correlation_window',
91
+ signal_day: withdrawalSignalDay,
92
+ days_since_signal: daysSinceSignal
93
+ };
94
+ }
95
+
96
+ // 'trading_effect' will be negative if the crowd is net-selling
97
+ const netSellPct = cashFlowData.components?.trading_effect || 0;
98
+ const netWithdrawalPct = Math.abs(withdrawalSignal.cash_flow_effect_proxy);
99
+
100
+ // INVERTED LOGIC: Find top *sells*
101
+ const topLiquidations = Object.entries(assetFlowData)
102
+ .filter(([ticker, data]) => data.net_crowd_flow_pct < 0) // Find assets with negative flow
103
+ .sort(([, a], [, b]) => a.net_crowd_flow_pct - b.net_crowd_flow_pct) // Sort ascending (most negative first)
104
+ .slice(0, 10)
105
+ .map(([ticker, data]) => ({
106
+ ticker,
107
+ net_flow_pct: data.net_crowd_flow_pct
108
+ }));
109
+
110
+ return {
111
+ status: 'analysis_complete',
112
+ analysis_date: dateStr,
113
+ signal_date: withdrawalSignalDay,
114
+ days_since_signal: daysSinceSignal,
115
+ signal_withdrawal_proxy_pct: netWithdrawalPct,
116
+ day_net_sell_pct: netSellPct, // This value should be negative
117
+ pct_of_withdrawal_funded_today: (Math.abs(netSellPct) / netWithdrawalPct) * 100,
118
+ top_liquidation_assets: topLiquidations
119
+ };
120
+ }
121
+
122
+ // Must exist for the meta-computation runner
123
+ async getResult() { return null; }
124
+ reset() {}
125
+ }
126
+
143
127
  module.exports = CashFlowLiquidation;
@@ -1,92 +1,84 @@
1
- /**
2
- * @fileoverview Meta-calculation (Pass 3) to calculate a proxy for the
3
- * crowd's "Sharpe Ratio" (risk-adjusted return) on a per-asset basis.
4
- * It uses the components from 'pnl_distribution_per_stock' to calculate
5
- * the standard deviation of P/L, which serves as the "risk".
6
- */
7
-
8
- class CrowdSharpeRatioProxy {
9
- constructor() {}
10
-
11
- /**
12
- * @param {string} dateStr The date to run the analysis for (e.g., "2025-10-31").
13
- * @param {object} dependencies The shared dependencies (db, logger).
14
- * @param {object} config The computation system configuration.
15
- * @returns {Promise<object|null>} The analysis result or null.
16
- */
17
- async process(dateStr, dependencies, config) {
18
- const { db, logger } = dependencies;
19
- const collection = config.resultsCollection;
20
-
21
- // 1. Define dependency
22
- const dependency = { category: 'pnl', computation: 'pnl-distribution-per-stock' };
23
-
24
- // 2. Build ref and fetch
25
- const docRef = db.collection(collection).doc(dateStr)
26
- .collection('results').doc(dependency.category)
27
- .collection('computations').doc(dependency.computation);
28
-
29
- const snapshot = await docRef.get();
30
-
31
- // 3. Handle the "day-delay"
32
- if (!snapshot.exists) {
33
- logger.log('WARN', `[CrowdSharpeRatioProxy] Missing dependency 'pnl-distribution-per-stock' for ${dateStr}. Allowing backfill.`);
34
- return null; // Let backfill handle it
35
- }
36
-
37
- const data = snapshot.data();
38
- const pnlDistribution = data.pnl_distribution_by_asset;
39
-
40
- if (!pnlDistribution) {
41
- logger.log('WARN', `[CrowdSharpeRatioProxy] Dependency data for ${dateStr} is empty. Skipping.`);
42
- return null;
43
- }
44
-
45
- const results = {};
46
-
47
- // 4. Calculate Sharpe Proxy for each asset
48
- for (const ticker in pnlDistribution) {
49
- const stats = pnlDistribution[ticker];
50
- const N = stats.position_count;
51
-
52
- // Need at least 2 data points to calculate variance
53
- if (N < 2) continue;
54
-
55
- const mean = stats.pnl_sum / N; // E(x)
56
- const mean_sq = stats.pnl_sum_sq / N; // E(x^2)
57
-
58
- const variance = mean_sq - (mean * mean);
59
-
60
- // If variance is negative (floating point error) or zero, we can't get std_dev
61
- if (variance <= 0) {
62
- results[ticker] = {
63
- average_pnl: mean,
64
- std_dev_pnl: 0,
65
- sharpe_ratio_proxy: 0,
66
- position_count: N
67
- };
68
- continue;
69
- }
70
-
71
- const std_dev = Math.sqrt(variance); // "Risk"
72
-
73
- // Calculate Sharpe Ratio (Return / Risk)
74
- // (Assuming 0 risk-free rate)
75
- const sharpe_proxy = mean / std_dev;
76
-
77
- results[ticker] = {
78
- average_pnl: mean,
79
- std_dev_pnl: std_dev,
80
- sharpe_ratio_proxy: sharpe_proxy,
81
- position_count: N
82
- };
83
- }
84
-
85
- return results;
86
- }
87
-
88
- async getResult() { return null; }
89
- reset() {}
90
- }
91
-
1
+ /**
2
+ * @fileoverview Meta-calculation (Pass 3) to calculate a proxy for the
3
+ * crowd's "Sharpe Ratio" (risk-adjusted return) on a per-asset basis.
4
+ * It uses the components from 'pnl_distribution_per_stock' to calculate
5
+ * the standard deviation of P/L, which serves as the "risk".
6
+ */
7
+
8
+ class CrowdSharpeRatioProxy {
9
+ constructor() {}
10
+
11
+ /**
12
+ * @param {string} dateStr The date to run the analysis for (e.g., "2025-10-31").
13
+ * @param {object} dependencies The shared dependencies (db, logger).
14
+ * @param {object} config The computation system configuration.
15
+ * @param {object} computedDependencies In-memory results from previous passes.
16
+ * @returns {Promise<object|null>} The analysis result or null.
17
+ */
18
+ async process(dateStr, dependencies, config, computedDependencies) {
19
+ const { logger } = dependencies;
20
+
21
+ // 1. Get dependency from in-memory cache
22
+ const data = computedDependencies['pnl-distribution-per-stock'];
23
+
24
+ // 2. Handle missing dependency
25
+ if (!data) {
26
+ logger.log('WARN', `[CrowdSharpeRatioProxy] Missing dependency 'pnl-distribution-per-stock' for ${dateStr}. Skipping.`);
27
+ return null;
28
+ }
29
+
30
+ const pnlDistribution = data.pnl_distribution_by_asset;
31
+
32
+ if (!pnlDistribution) {
33
+ logger.log('WARN', `[CrowdSharpeRatioProxy] Dependency data for ${dateStr} is empty. Skipping.`);
34
+ return null;
35
+ }
36
+
37
+ const results = {};
38
+
39
+ // 3. Calculate Sharpe Proxy for each asset
40
+ for (const ticker in pnlDistribution) {
41
+ const stats = pnlDistribution[ticker];
42
+ const N = stats.position_count;
43
+
44
+ // Need at least 2 data points to calculate variance
45
+ if (N < 2) continue;
46
+
47
+ const mean = stats.pnl_sum / N; // E(x)
48
+ const mean_sq = stats.pnl_sum_sq / N; // E(x^2)
49
+
50
+ const variance = mean_sq - (mean * mean);
51
+
52
+ // If variance is negative (floating point error) or zero, we can't get std_dev
53
+ if (variance <= 0) {
54
+ results[ticker] = {
55
+ average_pnl: mean,
56
+ std_dev_pnl: 0,
57
+ sharpe_ratio_proxy: 0,
58
+ position_count: N
59
+ };
60
+ continue;
61
+ }
62
+
63
+ const std_dev = Math.sqrt(variance); // "Risk"
64
+
65
+ // Calculate Sharpe Ratio (Return / Risk)
66
+ // (Assuming 0 risk-free rate)
67
+ const sharpe_proxy = mean / std_dev;
68
+
69
+ results[ticker] = {
70
+ average_pnl: mean,
71
+ std_dev_pnl: std_dev,
72
+ sharpe_ratio_proxy: sharpe_proxy,
73
+ position_count: N
74
+ };
75
+ }
76
+
77
+ return results;
78
+ }
79
+
80
+ async getResult() { return null; }
81
+ reset() {}
82
+ }
83
+
92
84
  module.exports = CrowdSharpeRatioProxy;
@@ -1,92 +1,78 @@
1
- /**
2
- * @fileoverview Meta-calculation (Pass 3) that correlates the asset flow
3
- * of the "In Profit" cohort vs. the "In Loss" cohort to find
4
- * powerful divergence signals (e.g., profit-taking, capitulation).
5
- */
6
-
7
- class ProfitCohortDivergence {
8
- constructor() {
9
- this.flowThreshold = 0.005; // Min abs flow % to be considered a signal (formerly 0.5)
10
- }
11
-
12
- /**
13
- * @param {string} dateStr The date to run the analysis for (e.g., "2025-10-31").
14
- * @param {object} dependencies The shared dependencies (db, logger).
15
- * @param {object} config The computation system configuration.
16
- * @returns {Promise<object|null>} The analysis result or null.
17
- */
18
- async process(dateStr, dependencies, config) {
19
- const { db, logger } = dependencies;
20
- const collection = config.resultsCollection;
21
- const resultsSub = config.resultsSubcollection || 'results';
22
- const compsSub = config.computationsSubcollection || 'computations';
23
-
24
- // 1. Define dependencies
25
- const refsToGet = [
26
- {
27
- key: 'profit_flow',
28
- ref: db.collection(collection).doc(dateStr).collection(resultsSub).doc('behavioural').collection(compsSub).doc('in-profit-asset-crowd-flow')
29
- },
30
- {
31
- key: 'loss_flow',
32
- ref: db.collection(collection).doc(dateStr).collection(resultsSub).doc('behavioural').collection(compsSub).doc('in-loss-asset-crowd-flow')
33
- }
34
- ];
35
-
36
- // 2. Fetch
37
- const snapshots = await db.getAll(...refsToGet.map(r => r.ref));
38
- const profitFlowData = snapshots[0].exists ? snapshots[0].data() : null;
39
- const lossFlowData = snapshots[1].exists ? snapshots[1].data() : null;
40
-
41
- // 3. Handle "day-delay"
42
- if (!profitFlowData || !lossFlowData) {
43
- logger.log('WARN', `[ProfitCohortDivergence] Missing cohort flow data for ${dateStr}. Allowing backfill.`);
44
- return null; // Let backfill handle it
45
- }
46
-
47
- const results = {};
48
- const allTickers = new Set([...Object.keys(profitFlowData), ...Object.keys(lossFlowData)]);
49
-
50
- // 4. Correlate
51
- for (const ticker of allTickers) {
52
- const profitFlow = profitFlowData[ticker]?.net_crowd_flow_pct || 0;
53
- const lossFlow = lossFlowData[ticker]?.net_crowd_flow_pct || 0;
54
-
55
- const profitSells = profitFlow <= -this.flowThreshold;
56
- const profitBuys = profitFlow >= this.flowThreshold;
57
- const lossSells = lossFlow <= -this.flowThreshold;
58
- const lossBuys = lossFlow >= this.flowThreshold;
59
-
60
- let status = 'No Divergence';
61
- let detail = 'Both cohorts are acting similarly or flow is insignificant.';
62
-
63
- if (profitSells && lossBuys) {
64
- status = 'Profit Taking';
65
- detail = 'The "in-profit" cohort is selling to the "in-loss" cohort, who are averaging down.';
66
- } else if (profitBuys && lossSells) {
67
- status = 'Capitulation';
68
- detail = 'The "in-loss" cohort is panic-selling, and the "in-profit" cohort is buying the dip.';
69
- } else if (profitBuys && lossBuys) {
70
- status = 'High Conviction Buy';
71
- detail = 'All cohorts are net-buying.';
72
- } else if (profitSells && lossSells) {
73
- status = 'High Conviction Sell';
74
- detail = 'All cohorts are net-selling.';
75
- }
76
-
77
- results[ticker] = {
78
- status: status,
79
- detail: detail,
80
- profit_cohort_flow: profitFlow,
81
- loss_cohort_flow: lossFlow
82
- };
83
- }
84
-
85
- return results;
86
- }
87
-
88
- async getResult() { return null; }
89
- reset() {}
90
- }
91
-
1
+ /**
2
+ * @fileoverview Meta-calculation (Pass 3) that correlates the asset flow
3
+ * of the "In Profit" cohort vs. the "In Loss" cohort to find
4
+ * powerful divergence signals (e.g., profit-taking, capitulation).
5
+ */
6
+
7
+ class ProfitCohortDivergence {
8
+ constructor() {
9
+ this.flowThreshold = 0.005; // Min abs flow % to be considered a signal (formerly 0.5)
10
+ }
11
+
12
+ /**
13
+ * @param {string} dateStr The date to run the analysis for (e.g., "2025-10-31").
14
+ * @param {object} dependencies The shared dependencies (db, logger).
15
+ * @param {object} config The computation system configuration.
16
+ * @param {object} computedDependencies In-memory results from previous passes.
17
+ * @returns {Promise<object|null>} The analysis result or null.
18
+ */
19
+ async process(dateStr, dependencies, config, computedDependencies) {
20
+ const { logger } = dependencies;
21
+
22
+ // 1. Get dependencies directly from the new argument
23
+ // Names are normalized to kebab-case by the orchestrator
24
+ const profitFlowData = computedDependencies['in-profit-asset-crowd-flow'];
25
+ const lossFlowData = computedDependencies['in-loss-asset-crowd-flow'];
26
+
27
+ // 2. Handle missing dependencies
28
+ if (!profitFlowData || !lossFlowData) {
29
+ logger.log('WARN', `[ProfitCohortDivergence] Missing computed dependency for ${dateStr}. Skipping.`);
30
+ return null;
31
+ }
32
+
33
+ const results = {};
34
+ const allTickers = new Set([...Object.keys(profitFlowData), ...Object.keys(lossFlowData)]);
35
+
36
+ // 4. Correlate
37
+ for (const ticker of allTickers) {
38
+ const profitFlow = profitFlowData[ticker]?.net_crowd_flow_pct || 0;
39
+ const lossFlow = lossFlowData[ticker]?.net_crowd_flow_pct || 0;
40
+
41
+ const profitSells = profitFlow <= -this.flowThreshold;
42
+ const profitBuys = profitFlow >= this.flowThreshold;
43
+ const lossSells = lossFlow <= -this.flowThreshold;
44
+ const lossBuys = lossFlow >= this.flowThreshold;
45
+
46
+ let status = 'No Divergence';
47
+ let detail = 'Both cohorts are acting similarly or flow is insignificant.';
48
+
49
+ if (profitSells && lossBuys) {
50
+ status = 'Profit Taking';
51
+ detail = 'The "in-profit" cohort is selling to the "in-loss" cohort, who are averaging down.';
52
+ } else if (profitBuys && lossSells) {
53
+ status = 'Capitulation';
54
+ detail = 'The "in-loss" cohort is panic-selling, and the "in-profit" cohort is buying the dip.';
55
+ } else if (profitBuys && lossBuys) {
56
+ status = 'High Conviction Buy';
57
+ detail = 'All cohorts are net-buying.';
58
+ } else if (profitSells && lossSells) {
59
+ status = 'High Conviction Sell';
60
+ detail = 'All cohorts are net-selling.';
61
+ }
62
+
63
+ results[ticker] = {
64
+ status: status,
65
+ detail: detail,
66
+ profit_cohort_flow: profitFlow,
67
+ loss_cohort_flow: lossFlow
68
+ };
69
+ }
70
+
71
+ return results;
72
+ }
73
+
74
+ async getResult() { return null; }
75
+ reset() {}
76
+ }
77
+
92
78
  module.exports = ProfitCohortDivergence;