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,138 +1,130 @@
1
- /**
2
- * @fileoverview Meta-calculation (Pass 3) that analyzes "what" the crowd does
3
- * following a net deposit signal. It determines if the new capital is used to
4
- * buy *new* assets or *add* to existing ones.
5
- */
6
-
7
- const { FieldValue } = require('@google-cloud/firestore');
8
-
9
- class CapitalDeploymentStrategy {
10
- constructor() {
11
- this.lookbackDays = 7;
12
- this.correlationWindow = 3; // How many days after a signal to link behavior
13
- this.depositSignalThreshold = -0.005; // Formerly -1.0
14
- }
15
-
16
- _getDateStr(baseDate, daysAgo) {
17
- const date = new Date(baseDate + 'T00:00:00Z');
18
- date.setUTCDate(date.getUTCDate() - daysAgo);
19
- return date.toISOString().slice(0, 10);
20
- }
21
-
22
- /**
23
- * @param {string} dateStr The date to run the analysis for (e.g., "2025-10-31").
24
- * @param {object} dependencies The shared dependencies (db, logger).
25
- * @param {object} config The computation system configuration.
26
- * @returns {Promise<object|null>} The analysis result or null.
27
- */
28
- async process(dateStr, dependencies, config) {
29
- const { db, logger } = dependencies;
30
- const collection = config.resultsCollection;
31
- const resultsSub = config.resultsSubcollection || 'results';
32
- const compsSub = config.computationsSubcollection || 'computations';
33
-
34
- // 1. Find the most recent deposit signal
35
- let depositSignal = null;
36
- let depositSignalDay = null;
37
- let refsToGet = [];
38
-
39
- for (let i = 1; i <= this.lookbackDays; i++) {
40
- const checkDate = this._getDateStr(dateStr, i);
41
- refsToGet.push({
42
- date: checkDate,
43
- key: `signal_${checkDate}`,
44
- ref: db.collection(collection).doc(checkDate).collection(resultsSub).doc('capital_flow').collection(compsSub).doc('crowd-cash-flow-proxy')
45
- });
46
- }
47
-
48
- const signalSnapshots = await db.getAll(...refsToGet.map(r => r.ref));
49
- const dataMap = new Map();
50
- signalSnapshots.forEach((snap, idx) => {
51
- if (snap.exists) dataMap.set(refsToGet[idx].key, snap.data());
52
- });
53
-
54
- for (let i = 1; i <= this.lookbackDays; i++) {
55
- const checkDate = this._getDateStr(dateStr, i);
56
- const flowData = dataMap.get(`signal_${checkDate}`);
57
- if (flowData && flowData.cash_flow_effect_proxy < this.depositSignalThreshold) {
58
- depositSignal = flowData;
59
- depositSignalDay = checkDate;
60
- break; // Found the most recent signal
61
- }
62
- }
63
-
64
- if (!depositSignal) {
65
- return {
66
- status: 'no_deposit_signal_found',
67
- lookback_days: this.lookbackDays
68
- };
69
- }
70
-
71
- // 2. Check if today is within the correlation window
72
- const daysSinceSignal = (new Date(dateStr) - new Date(depositSignalDay)) / (1000 * 60 * 60 * 24);
73
-
74
- if (daysSinceSignal <= 0 || daysSinceSignal > this.correlationWindow) {
75
- return {
76
- status: 'outside_correlation_window',
77
- signal_day: depositSignalDay,
78
- days_since_signal: daysSinceSignal
79
- };
80
- }
81
-
82
- // 3. Fetch deployment data for *today*
83
- // We are correlating the *past signal* with *today's action*
84
- refsToGet = [
85
- {
86
- key: 'new_alloc',
87
- ref: db.collection(collection).doc(dateStr).collection(resultsSub).doc('capital_flow').collection(compsSub).doc('new-allocation-percentage')
88
- },
89
- {
90
- key: 're_alloc',
91
- ref: db.collection(collection).doc(dateStr).collection(resultsSub).doc('capital_flow').collection(compsSub).doc('reallocation-increase-percentage')
92
- }
93
- ];
94
-
95
- const deploymentSnapshots = await db.getAll(...refsToGet.map(r => r.ref));
96
- const newAllocData = deploymentSnapshots[0].exists ? deploymentSnapshots[0].data() : null;
97
- const reAllocData = deploymentSnapshots[1].exists ? deploymentSnapshots[1].data() : null;
98
-
99
- // 4. Handle "day-delay" for *this* data
100
- if (!newAllocData || !reAllocData) {
101
- logger.log('WARN', `[CapitalDeploymentStrategy] Missing deployment data for ${dateStr}. Allowing backfill.`);
102
- // This is a "same-day" meta-calc, so we return null to let backfill run
103
- return null;
104
- }
105
-
106
- // 5. Calculate deployment bias
107
- const newAlloc = newAllocData.average_new_allocation_percentage || 0;
108
- const reAlloc = reAllocData.average_reallocation_increase_percentage || 0;
109
-
110
- const totalDeployment = newAlloc + reAlloc;
111
- let newAssetBias = 0;
112
- let existingAssetBias = 0;
113
-
114
- if (totalDeployment > 0) {
115
- newAssetBias = (newAlloc / totalDeployment) * 100;
116
- existingAssetBias = (reAlloc / totalDeployment) * 100;
117
- }
118
-
119
- return {
120
- status: 'analysis_complete',
121
- analysis_date: dateStr,
122
- signal_date: depositSignalDay,
123
- days_since_signal: daysSinceSignal,
124
- signal_deposit_proxy_pct: Math.abs(depositSignal.cash_flow_effect_proxy),
125
- deployment_new_alloc_pct: newAlloc,
126
- deployment_existing_alloc_pct: reAlloc,
127
- total_deployment_pct: totalDeployment,
128
- new_asset_bias: newAssetBias,
129
- existing_asset_bias: existingAssetBias
130
- };
131
- }
132
-
133
- // Must exist for the meta-computation runner
134
- async getResult() { return null; }
135
- reset() {}
136
- }
137
-
1
+ /**
2
+ * @fileoverview Meta-calculation (Pass 3) that analyzes "what" the crowd does
3
+ * following a net deposit signal. It determines if the new capital is used to
4
+ * buy *new* assets or *add* to existing ones.
5
+ */
6
+
7
+ // Note: This calculation still needs to read *historical* data for the signal.
8
+ // Only same-day dependencies can be passed in-memory.
9
+ // A full refactor would involve the orchestrator passing historical results
10
+ // into the cache, but for now we leave the historical lookback.
11
+
12
+ const { FieldValue } = require('@google-cloud/firestore');
13
+
14
+ class CapitalDeploymentStrategy {
15
+ constructor() {
16
+ this.lookbackDays = 7;
17
+ this.correlationWindow = 3; // How many days after a signal to link behavior
18
+ this.depositSignalThreshold = -0.005; // Formerly -1.0
19
+ }
20
+
21
+ _getDateStr(baseDate, daysAgo) {
22
+ const date = new Date(baseDate + 'T00:00:00Z');
23
+ date.setUTCDate(date.getUTCDate() - daysAgo);
24
+ return date.toISOString().slice(0, 10);
25
+ }
26
+
27
+ /**
28
+ * @param {string} dateStr The date to run the analysis for (e.g., "2025-10-31").
29
+ * @param {object} dependencies The shared dependencies (db, logger).
30
+ * @param {object} config The computation system configuration.
31
+ * @param {object} computedDependencies In-memory results from previous passes.
32
+ * @returns {Promise<object|null>} The analysis result or null.
33
+ */
34
+ async process(dateStr, dependencies, config, computedDependencies) {
35
+ const { db, logger } = dependencies;
36
+ const collection = config.resultsCollection;
37
+ const resultsSub = config.resultsSubcollection || 'results';
38
+ const compsSub = config.computationsSubcollection || 'computations';
39
+
40
+ // 1. Find the most recent deposit signal (Still requires historical lookback)
41
+ let depositSignal = null;
42
+ let depositSignalDay = null;
43
+ let refsToGet = [];
44
+
45
+ for (let i = 1; i <= this.lookbackDays; i++) {
46
+ const checkDate = this._getDateStr(dateStr, i);
47
+ refsToGet.push({
48
+ date: checkDate,
49
+ key: `signal_${checkDate}`,
50
+ ref: db.collection(collection).doc(checkDate).collection(resultsSub).doc('capital_flow').collection(compsSub).doc('crowd-cash-flow-proxy')
51
+ });
52
+ }
53
+
54
+ const signalSnapshots = await db.getAll(...refsToGet.map(r => r.ref));
55
+ const dataMap = new Map();
56
+ signalSnapshots.forEach((snap, idx) => {
57
+ if (snap.exists) dataMap.set(refsToGet[idx].key, snap.data());
58
+ });
59
+
60
+ for (let i = 1; i <= this.lookbackDays; i++) {
61
+ const checkDate = this._getDateStr(dateStr, i);
62
+ const flowData = dataMap.get(`signal_${checkDate}`);
63
+ if (flowData && flowData.cash_flow_effect_proxy < this.depositSignalThreshold) {
64
+ depositSignal = flowData;
65
+ depositSignalDay = checkDate;
66
+ break; // Found the most recent signal
67
+ }
68
+ }
69
+
70
+ if (!depositSignal) {
71
+ return {
72
+ status: 'no_deposit_signal_found',
73
+ lookback_days: this.lookbackDays
74
+ };
75
+ }
76
+
77
+ // 2. Check if today is within the correlation window
78
+ const daysSinceSignal = (new Date(dateStr) - new Date(depositSignalDay)) / (1000 * 60 * 60 * 24);
79
+
80
+ if (daysSinceSignal <= 0 || daysSinceSignal > this.correlationWindow) {
81
+ return {
82
+ status: 'outside_correlation_window',
83
+ signal_day: depositSignalDay,
84
+ days_since_signal: daysSinceSignal
85
+ };
86
+ }
87
+
88
+ // 3. Fetch deployment data for *today* FROM IN-MEMORY CACHE
89
+ const newAllocData = computedDependencies['new-allocation-percentage'];
90
+ const reAllocData = computedDependencies['reallocation-increase-percentage'];
91
+
92
+ // 4. Handle missing dependencies
93
+ if (!newAllocData || !reAllocData) {
94
+ logger.log('WARN', `[CapitalDeploymentStrategy] Missing in-memory deployment data for ${dateStr}. Skipping.`);
95
+ return null;
96
+ }
97
+
98
+ // 5. Calculate deployment bias
99
+ const newAlloc = newAllocData.average_new_allocation_percentage || 0;
100
+ const reAlloc = reAllocData.average_reallocation_increase_percentage || 0;
101
+
102
+ const totalDeployment = newAlloc + reAlloc;
103
+ let newAssetBias = 0;
104
+ let existingAssetBias = 0;
105
+
106
+ if (totalDeployment > 0) {
107
+ newAssetBias = (newAlloc / totalDeployment) * 100;
108
+ existingAssetBias = (reAlloc / totalDeployment) * 100;
109
+ }
110
+
111
+ return {
112
+ status: 'analysis_complete',
113
+ analysis_date: dateStr,
114
+ signal_date: depositSignalDay,
115
+ days_since_signal: daysSinceSignal,
116
+ signal_deposit_proxy_pct: Math.abs(depositSignal.cash_flow_effect_proxy),
117
+ deployment_new_alloc_pct: newAlloc,
118
+ deployment_existing_alloc_pct: reAlloc,
119
+ total_deployment_pct: totalDeployment,
120
+ new_asset_bias: newAssetBias,
121
+ existing_asset_bias: existingAssetBias
122
+ };
123
+ }
124
+
125
+ // Must exist for the meta-computation runner
126
+ async getResult() { return null; }
127
+ reset() {}
128
+ }
129
+
138
130
  module.exports = CapitalDeploymentStrategy;
@@ -1,164 +1,122 @@
1
- /**
2
- * @fileoverview Meta-calculation (Pass 3) that tracks the performance
3
- * of assets that were heavily liquidated (sold) by the crowd to fund
4
- * a withdrawal event.
5
- *
6
- * This answers: "After the crowd sold an asset to withdraw cash,
7
- * did that asset recover (implying a panic-sell) or
8
- * continue to fall (implying a smart exit)?"
9
- */
10
-
11
- class CapitalLiquidationPerformance {
12
- constructor() {
13
- // How many days to look back/forward to measure performance
14
- this.PERFORMANCE_WINDOW_DAYS = 7;
15
- this.dependenciesLoaded = false;
16
- this.priceMap = null;
17
- this.tickerToIdMap = null;
18
- }
19
-
20
- /**
21
- * Helper to load all dependencies in parallel
22
- */
23
- async _loadDependencies(calculationUtils) {
24
- if (this.dependenciesLoaded) return;
25
-
26
- const { loadAllPriceData, loadInstrumentMappings } = calculationUtils;
27
-
28
- const [priceData, mappings] = await Promise.all([
29
- loadAllPriceData(),
30
- loadInstrumentMappings()
31
- ]);
32
-
33
- this.priceMap = priceData;
34
-
35
- // Create a reverse map for easy lookup
36
- this.tickerToIdMap = {};
37
- if (mappings && mappings.instrumentToTicker) {
38
- for (const [id, ticker] of Object.entries(mappings.instrumentToTicker)) {
39
- this.tickerToIdMap[ticker] = id;
40
- }
41
- }
42
-
43
- this.dependenciesLoaded = true;
44
- }
45
-
46
- /**
47
- * Helper to get a date string X days from a base date
48
- */
49
- _getDateStr(baseDateStr, daysOffset) {
50
- const date = new Date(baseDateStr + 'T00:00:00Z');
51
- date.setUTCDate(date.getUTCDate() + daysOffset);
52
- return date.toISOString().slice(0, 10);
53
- }
54
-
55
- /**
56
- * Helper to calculate the average return of a basket of assets
57
- * over a specified period.
58
- */
59
- _calculateBasketPerformance(assets, startDateStr, endDateStr) {
60
- if (!assets || assets.length === 0) {
61
- return 0;
62
- }
63
-
64
- let totalReturn = 0;
65
- let validAssets = 0;
66
-
67
- for (const asset of assets) {
68
- const ticker = asset.ticker;
69
- const instrumentId = this.tickerToIdMap[ticker];
70
-
71
- if (!instrumentId || !this.priceMap[instrumentId]) {
72
- continue; // No price data for this ticker
73
- }
74
-
75
- const startPrice = this.priceMap[instrumentId][startDateStr];
76
- const endPrice = this.priceMap[instrumentId][endDateStr];
77
-
78
- if (startPrice && endPrice && startPrice > 0) {
79
- const assetReturn = (endPrice - startPrice) / startPrice;
80
- totalReturn += assetReturn;
81
- validAssets++;
82
- }
83
- }
84
-
85
- if (validAssets === 0) return 0;
86
- return (totalReturn / validAssets) * 100; // Return as percentage
87
- }
88
-
89
- /**
90
- * @param {string} dateStr The date to run the analysis for (e.g., "2025-10-31").
91
- * @param {object} dependencies The shared dependencies (db, logger, calculationUtils).
92
- * @param {object} config The computation system configuration.
93
- * @returns {Promise<object|null>} The analysis result or null.
94
- */
95
- async process(dateStr, dependencies, config) {
96
- const { db, logger, calculationUtils } = dependencies;
97
-
98
- // 1. Load all price/mapping data
99
- await this._loadDependencies(calculationUtils);
100
-
101
- // 2. Define and fetch dependency: cash-flow-liquidation
102
- const depRef = db.collection(config.resultsCollection).doc(dateStr)
103
- .collection('results').doc('meta')
104
- .collection('computations').doc('cash-flow-liquidation'); // <-- This is the dependency
105
-
106
- const snapshot = await depRef.get();
107
-
108
- if (!snapshot.exists || snapshot.data().status !== 'analysis_complete') {
109
- logger.log('WARN', `[CapitalLiquidation] Skipping ${dateStr}, no valid 'cash-flow-liquidation' data found.`);
110
- return null;
111
- }
112
-
113
- const data = snapshot.data();
114
- // This is the key change: we get the list of *sold* assets
115
- const topAssets = data.top_liquidation_assets; // [{ ticker: 'MSFT', ... }]
116
- const signalDate = data.signal_date; // The day the withdrawal signal occurred
117
- const liquidationDate = data.analysis_date; // The day the capital was sold (dateStr)
118
-
119
- if (!topAssets || topAssets.length === 0) {
120
- logger.log('INFO', `[CapitalLiquidation] No top assets liquidated on ${dateStr}.`);
121
- return { status: 'no_liquidation' };
122
- }
123
-
124
- // 3. Define performance windows
125
- // "Before" window: 7 days leading up to the signal
126
- const preSignalStart = this._getDateStr(signalDate, -this.PERFORMANCE_WINDOW_DAYS);
127
- const preSignalEnd = signalDate;
128
-
129
- // "After" window: 7 days starting from the liquidation
130
- const postLiquidationStart = liquidationDate;
131
- const postLiquidationEnd = this._getDateStr(liquidationDate, this.PERFORMANCE_WINDOW_DAYS);
132
-
133
- // 4. Calculate performance
134
- const preSignalReturnPct = this._calculateBasketPerformance(
135
- topAssets, preSignalStart, preSignalEnd
136
- );
137
-
138
- const postLiquidationReturnPct = this._calculateBasketPerformance(
139
- topAssets, postLiquidationStart, postLiquidationEnd
140
- );
141
-
142
- // This answers the question:
143
- // A positive value means the assets RECOVERED (crowd panic-sold at the bottom).
144
- // A negative value means the assets CONTINUED TO FALL (crowd made a good exit).
145
- const crowdTimingError = postLiquidationReturnPct;
146
-
147
- return {
148
- status: 'analysis_complete',
149
- signal_date: signalDate,
150
- liquidation_date: liquidationDate,
151
- performance_window_days: this.PERFORMANCE_WINDOW_DAYS,
152
- liquidated_assets: topAssets.map(a => a.ticker),
153
- pre_signal_return_pct: preSignalReturnPct,
154
- post_liquidation_return_pct: postLiquidationReturnPct,
155
- crowd_timing_error: crowdTimingError,
156
- interpretation: "Measures the 7-day return of liquidated assets *after* being sold. Positive = asset recovered (bad timing). Negative = asset kept falling (good timing)."
157
- };
158
- }
159
-
160
- async getResult() { return null; }
161
- reset() {}
162
- }
163
-
1
+ /**
2
+ * @fileoverview Meta-calculation (Pass 3) that tracks the performance
3
+ * of assets that were heavily liquidated (sold) by the crowd to fund
4
+ * a withdrawal event.
5
+ */
6
+
7
+ class CapitalLiquidationPerformance {
8
+ constructor() {
9
+ this.PERFORMANCE_WINDOW_DAYS = 7;
10
+ this.dependenciesLoaded = false;
11
+ this.priceMap = null;
12
+ this.tickerToIdMap = null;
13
+ }
14
+
15
+ async _loadDependencies(calculationUtils) {
16
+ if (this.dependenciesLoaded) return;
17
+ const { loadAllPriceData, loadInstrumentMappings } = calculationUtils;
18
+ const [priceData, mappings] = await Promise.all([
19
+ loadAllPriceData(),
20
+ loadInstrumentMappings()
21
+ ]);
22
+ this.priceMap = priceData;
23
+ this.tickerToIdMap = {};
24
+ if (mappings && mappings.instrumentToTicker) {
25
+ for (const [id, ticker] of Object.entries(mappings.instrumentToTicker)) {
26
+ this.tickerToIdMap[ticker] = id;
27
+ }
28
+ }
29
+ this.dependenciesLoaded = true;
30
+ }
31
+
32
+ _getDateStr(baseDateStr, daysOffset) {
33
+ const date = new Date(baseDateStr + 'T00:00:00Z');
34
+ date.setUTCDate(date.getUTCDate() + daysOffset);
35
+ return date.toISOString().slice(0, 10);
36
+ }
37
+
38
+ _calculateBasketPerformance(assets, startDateStr, endDateStr) {
39
+ if (!assets || assets.length === 0) return 0;
40
+ let totalReturn = 0;
41
+ let validAssets = 0;
42
+ for (const asset of assets) {
43
+ const ticker = asset.ticker;
44
+ const instrumentId = this.tickerToIdMap[ticker];
45
+ if (!instrumentId || !this.priceMap[instrumentId]) continue;
46
+ const startPrice = this.priceMap[instrumentId][startDateStr];
47
+ const endPrice = this.priceMap[instrumentId][endDateStr];
48
+ if (startPrice && endPrice && startPrice > 0) {
49
+ const assetReturn = (endPrice - startPrice) / startPrice;
50
+ totalReturn += assetReturn;
51
+ validAssets++;
52
+ }
53
+ }
54
+ if (validAssets === 0) return 0;
55
+ return (totalReturn / validAssets) * 100;
56
+ }
57
+
58
+ /**
59
+ * @param {string} dateStr The date to run the analysis for (e.g., "2025-10-31").
60
+ * @param {object} dependencies The shared dependencies (db, logger, calculationUtils).
61
+ * @param {object} config The computation system configuration.
62
+ * @param {object} computedDependencies In-memory results from previous passes.
63
+ * @returns {Promise<object|null>} The analysis result or null.
64
+ */
65
+ async process(dateStr, dependencies, config, computedDependencies) {
66
+ const { db, logger, calculationUtils } = dependencies;
67
+
68
+ // 1. Load all price/mapping data
69
+ await this._loadDependencies(calculationUtils);
70
+
71
+ // 2. Get dependency from in-memory cache
72
+ const data = computedDependencies['cash-flow-liquidation'];
73
+
74
+ if (!data || data.status !== 'analysis_complete') {
75
+ logger.log('WARN', `[CapitalLiquidation] Skipping ${dateStr}, no valid 'cash-flow-liquidation' data found.`);
76
+ return null;
77
+ }
78
+
79
+ const topAssets = data.top_liquidation_assets; // [{ ticker: 'MSFT', ... }]
80
+ const signalDate = data.signal_date;
81
+ const liquidationDate = data.analysis_date;
82
+
83
+ if (!topAssets || topAssets.length === 0) {
84
+ logger.log('INFO', `[CapitalLiquidation] No top assets liquidated on ${dateStr}.`);
85
+ return { status: 'no_liquidation' };
86
+ }
87
+
88
+ // 3. Define performance windows
89
+ const preSignalStart = this._getDateStr(signalDate, -this.PERFORMANCE_WINDOW_DAYS);
90
+ const preSignalEnd = signalDate;
91
+ const postLiquidationStart = liquidationDate;
92
+ const postLiquidationEnd = this._getDateStr(liquidationDate, this.PERFORMANCE_WINDOW_DAYS);
93
+
94
+ // 4. Calculate performance
95
+ const preSignalReturnPct = this._calculateBasketPerformance(
96
+ topAssets, preSignalStart, preSignalEnd
97
+ );
98
+
99
+ const postLiquidationReturnPct = this._calculateBasketPerformance(
100
+ topAssets, postLiquidationStart, postLiquidationEnd
101
+ );
102
+
103
+ const crowdTimingError = postLiquidationReturnPct;
104
+
105
+ return {
106
+ status: 'analysis_complete',
107
+ signal_date: signalDate,
108
+ liquidation_date: liquidationDate,
109
+ performance_window_days: this.PERFORMANCE_WINDOW_DAYS,
110
+ liquidated_assets: topAssets.map(a => a.ticker),
111
+ pre_signal_return_pct: preSignalReturnPct,
112
+ post_liquidation_return_pct: postLiquidationReturnPct,
113
+ crowd_timing_error: crowdTimingError,
114
+ interpretation: "Measures the 7-day return of liquidated assets *after* being sold. Positive = asset recovered (bad timing). Negative = asset kept falling (good timing)."
115
+ };
116
+ }
117
+
118
+ async getResult() { return null; }
119
+ reset() {}
120
+ }
121
+
164
122
  module.exports = CapitalLiquidationPerformance;