aiden-shared-calculations-unified 1.0.24 → 1.0.26
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,164 @@
|
|
|
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
|
+
|
|
164
|
+
module.exports = CapitalLiquidationPerformance;
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Meta-calculation (Pass 3) that tracks the performance
|
|
3
|
+
* of capital "vintages" by analyzing the market returns of assets
|
|
4
|
+
* that were bought following a crowd-wide deposit signal.
|
|
5
|
+
*
|
|
6
|
+
* This answers: "Does capital deployed from a fresh deposit event
|
|
7
|
+
* outperform capital deployed earlier?"
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
class CapitalVintagePerformance {
|
|
11
|
+
constructor() {
|
|
12
|
+
// How many days to look back/forward to measure performance
|
|
13
|
+
this.PERFORMANCE_WINDOW_DAYS = 7;
|
|
14
|
+
this.dependenciesLoaded = false;
|
|
15
|
+
this.priceMap = null;
|
|
16
|
+
this.tickerToIdMap = null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Helper to load all dependencies in parallel
|
|
21
|
+
*/
|
|
22
|
+
async _loadDependencies(calculationUtils) {
|
|
23
|
+
if (this.dependenciesLoaded) return;
|
|
24
|
+
|
|
25
|
+
const { loadAllPriceData, loadInstrumentMappings } = calculationUtils;
|
|
26
|
+
|
|
27
|
+
const [priceData, mappings] = await Promise.all([
|
|
28
|
+
loadAllPriceData(),
|
|
29
|
+
loadInstrumentMappings()
|
|
30
|
+
]);
|
|
31
|
+
|
|
32
|
+
this.priceMap = priceData;
|
|
33
|
+
|
|
34
|
+
// Create a reverse map for easy lookup
|
|
35
|
+
this.tickerToIdMap = {};
|
|
36
|
+
if (mappings && mappings.instrumentToTicker) {
|
|
37
|
+
for (const [id, ticker] of Object.entries(mappings.instrumentToTicker)) {
|
|
38
|
+
this.tickerToIdMap[ticker] = id;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
this.dependenciesLoaded = true;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Helper to get a date string X days from a base date
|
|
47
|
+
*/
|
|
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
|
+
/**
|
|
55
|
+
* Helper to calculate the average return of a basket of assets
|
|
56
|
+
* over a specified period.
|
|
57
|
+
*/
|
|
58
|
+
_calculateBasketPerformance(assets, startDateStr, endDateStr) {
|
|
59
|
+
if (!assets || assets.length === 0) {
|
|
60
|
+
return 0;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
let totalReturn = 0;
|
|
64
|
+
let validAssets = 0;
|
|
65
|
+
|
|
66
|
+
for (const asset of assets) {
|
|
67
|
+
const ticker = asset.ticker;
|
|
68
|
+
const instrumentId = this.tickerToIdMap[ticker];
|
|
69
|
+
|
|
70
|
+
if (!instrumentId || !this.priceMap[instrumentId]) {
|
|
71
|
+
continue; // No price data for this ticker
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const startPrice = this.priceMap[instrumentId][startDateStr];
|
|
75
|
+
const endPrice = this.priceMap[instrumentId][endDateStr];
|
|
76
|
+
|
|
77
|
+
if (startPrice && endPrice && startPrice > 0) {
|
|
78
|
+
const assetReturn = (endPrice - startPrice) / startPrice;
|
|
79
|
+
totalReturn += assetReturn;
|
|
80
|
+
validAssets++;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (validAssets === 0) return 0;
|
|
85
|
+
return (totalReturn / validAssets) * 100; // Return as percentage
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* @param {string} dateStr The date to run the analysis for (e.g., "2025-10-31").
|
|
90
|
+
* @param {object} dependencies The shared dependencies (db, logger, calculationUtils).
|
|
91
|
+
* @param {object} config The computation system configuration.
|
|
92
|
+
* @returns {Promise<object|null>} The analysis result or null.
|
|
93
|
+
*/
|
|
94
|
+
async process(dateStr, dependencies, config) {
|
|
95
|
+
const { db, logger, calculationUtils } = dependencies;
|
|
96
|
+
|
|
97
|
+
// 1. Load all price/mapping data
|
|
98
|
+
await this._loadDependencies(calculationUtils);
|
|
99
|
+
|
|
100
|
+
// 2. Define and fetch dependency: cash-flow-deployment
|
|
101
|
+
const depRef = db.collection(config.resultsCollection).doc(dateStr)
|
|
102
|
+
.collection('results').doc('meta')
|
|
103
|
+
.collection('computations').doc('cash-flow-deployment');
|
|
104
|
+
|
|
105
|
+
const snapshot = await depRef.get();
|
|
106
|
+
|
|
107
|
+
if (!snapshot.exists || snapshot.data().status !== 'analysis_complete') {
|
|
108
|
+
logger.log('WARN', `[CapitalVintage] Skipping ${dateStr}, no valid 'cash-flow-deployment' data found.`);
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const data = snapshot.data();
|
|
113
|
+
const topAssets = data.top_deployment_assets; // [{ ticker: 'AAPL', ... }]
|
|
114
|
+
const signalDate = data.signal_date; // The day the deposit signal occurred
|
|
115
|
+
const deploymentDate = data.analysis_date; // The day the capital was spent (dateStr)
|
|
116
|
+
|
|
117
|
+
if (!topAssets || topAssets.length === 0) {
|
|
118
|
+
logger.log('INFO', `[CapitalVintage] No top assets deployed on ${dateStr}.`);
|
|
119
|
+
return { status: 'no_deployment' };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// 3. Define performance windows
|
|
123
|
+
// "Before" window: 7 days leading up to the signal
|
|
124
|
+
const preSignalStart = this._getDateStr(signalDate, -this.PERFORMANCE_WINDOW_DAYS);
|
|
125
|
+
const preSignalEnd = signalDate;
|
|
126
|
+
|
|
127
|
+
// "After" window: 7 days starting from the deployment
|
|
128
|
+
const postDeployStart = deploymentDate;
|
|
129
|
+
const postDeployEnd = this._getDateStr(deploymentDate, this.PERFORMANCE_WINDOW_DAYS);
|
|
130
|
+
|
|
131
|
+
// 4. Calculate performance
|
|
132
|
+
const preSignalReturnPct = this._calculateBasketPerformance(
|
|
133
|
+
topAssets, preSignalStart, preSignalEnd
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
const postDeploymentReturnPct = this._calculateBasketPerformance(
|
|
137
|
+
topAssets, postDeployStart, postDeployEnd
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
const momentum = postDeploymentReturnPct - preSignalReturnPct;
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
status: 'analysis_complete',
|
|
144
|
+
signal_date: signalDate,
|
|
145
|
+
deployment_date: deploymentDate,
|
|
146
|
+
performance_window_days: this.PERFORMANCE_WINDOW_DAYS,
|
|
147
|
+
deployed_assets: topAssets.map(a => a.ticker),
|
|
148
|
+
pre_signal_return_pct: preSignalReturnPct,
|
|
149
|
+
post_deployment_return_pct: postDeploymentReturnPct,
|
|
150
|
+
return_momentum: momentum,
|
|
151
|
+
interpretation: "Measures the 7-day return of deployed assets *after* deployment vs. 7-days *before* the signal."
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async getResult() { return null; }
|
|
156
|
+
reset() {}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
module.exports = CapitalVintagePerformance;
|