aiden-shared-calculations-unified 1.0.30 → 1.0.32
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.
|
@@ -82,7 +82,7 @@ class AssetCrowdFlow {
|
|
|
82
82
|
async getResult() {
|
|
83
83
|
if (this.user_count === 0 || !this.dates.today) {
|
|
84
84
|
console.warn('[AssetCrowdFlow] No users processed or dates missing.');
|
|
85
|
-
return
|
|
85
|
+
return null; // <--- MODIFICATION
|
|
86
86
|
}
|
|
87
87
|
|
|
88
88
|
// Load priceMap and mappings if not loaded
|
|
@@ -94,9 +94,19 @@ class AssetCrowdFlow {
|
|
|
94
94
|
]);
|
|
95
95
|
this.priceMap = priceData;
|
|
96
96
|
this.mappings = mappingData;
|
|
97
|
+
|
|
98
|
+
// --- START NEW CHECK ---
|
|
99
|
+
// If priceData failed to load (e.g., returned {} from provider)
|
|
100
|
+
// this.priceMap will be empty. We must abort.
|
|
101
|
+
if (!this.priceMap || Object.keys(this.priceMap).length === 0) {
|
|
102
|
+
console.error('[AssetCrowdFlow] CRITICAL: Price map is empty or failed to load. Aborting calculation to allow backfill.');
|
|
103
|
+
return null; // Return null to trigger backfill
|
|
104
|
+
}
|
|
105
|
+
// --- END NEW CHECK ---
|
|
106
|
+
|
|
97
107
|
} catch (err) {
|
|
98
108
|
console.error('[AssetCrowdFlow] Failed to load dependencies:', err);
|
|
99
|
-
return
|
|
109
|
+
return null; // <--- MODIFICATION: Return null on error
|
|
100
110
|
}
|
|
101
111
|
}
|
|
102
112
|
|
|
@@ -129,14 +139,8 @@ class AssetCrowdFlow {
|
|
|
129
139
|
}
|
|
130
140
|
|
|
131
141
|
if (priceChangePct === null) {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
avg_value_day1: avg_day1_value,
|
|
135
|
-
avg_value_day2: avg_day2_value,
|
|
136
|
-
price_change_pct: null,
|
|
137
|
-
user_sample_size: this.user_count,
|
|
138
|
-
warning: 'Missing price data for calculation'
|
|
139
|
-
};
|
|
142
|
+
// <--- MODIFICATION: We no longer add a warning object. We just skip it.
|
|
143
|
+
// If it's missing price data, it won't be in finalResults.
|
|
140
144
|
continue;
|
|
141
145
|
}
|
|
142
146
|
|
|
@@ -156,6 +160,15 @@ class AssetCrowdFlow {
|
|
|
156
160
|
};
|
|
157
161
|
}
|
|
158
162
|
|
|
163
|
+
// --- START NEW CHECK ---
|
|
164
|
+
// If all tickers were skipped due to missing price data,
|
|
165
|
+
// finalResults will be empty. Return null to trigger backfill.
|
|
166
|
+
if (Object.keys(finalResults).length === 0) {
|
|
167
|
+
console.warn(`[AssetCrowdFlow] No results generated for ${this.dates.today}. This likely means all price data was missing. Returning null for backfill.`);
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
// --- END NEW CHECK ---
|
|
171
|
+
|
|
159
172
|
return finalResults;
|
|
160
173
|
}
|
|
161
174
|
|
|
@@ -169,4 +182,4 @@ class AssetCrowdFlow {
|
|
|
169
182
|
}
|
|
170
183
|
}
|
|
171
184
|
|
|
172
|
-
module.exports = AssetCrowdFlow;
|
|
185
|
+
module.exports = AssetCrowdFlow;
|
|
@@ -45,6 +45,18 @@ class CashFlowDeployment {
|
|
|
45
45
|
if (snap.exists) dataMap.set(idx, snap.data());
|
|
46
46
|
});
|
|
47
47
|
|
|
48
|
+
// --- START MODIFICATION ---
|
|
49
|
+
// Check for THIS DATE's dependencies (cash-flow and asset-flow) FIRST.
|
|
50
|
+
// These are at the end of the 'dates' array.
|
|
51
|
+
const cashFlowData = dataMap.get(this.lookbackDays);
|
|
52
|
+
const assetFlowData = dataMap.get(this.lookbackDays + 1);
|
|
53
|
+
|
|
54
|
+
if (!cashFlowData || !assetFlowData) {
|
|
55
|
+
logger.log('WARN', `[CashFlowDeployment] Missing critical dependency data (cash-flow or asset-flow) for ${dateStr}. Skipping to allow backfill.`);
|
|
56
|
+
return null; // This allows backfill
|
|
57
|
+
}
|
|
58
|
+
// --- END MODIFICATION ---
|
|
59
|
+
|
|
48
60
|
// find deposit signal
|
|
49
61
|
let depositSignal = null;
|
|
50
62
|
let depositSignalDay = null;
|
|
@@ -77,14 +89,10 @@ class CashFlowDeployment {
|
|
|
77
89
|
};
|
|
78
90
|
}
|
|
79
91
|
|
|
80
|
-
//
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
if (!cashFlowData || !assetFlowData) {
|
|
85
|
-
logger.log('WARN', `[CashFlowDeployment] Missing dependency data for ${dateStr}. Skipping.`);
|
|
86
|
-
return null;
|
|
87
|
-
}
|
|
92
|
+
// --- REMOVED ---
|
|
93
|
+
// The check for cashFlowData and assetFlowData was here
|
|
94
|
+
// but has been moved up.
|
|
95
|
+
// --- END REMOVED ---
|
|
88
96
|
|
|
89
97
|
const netSpendPct = cashFlowData.components?.trading_effect || 0;
|
|
90
98
|
const netDepositPct = Math.abs(depositSignal.cash_flow_effect_proxy);
|
|
@@ -114,4 +122,4 @@ class CashFlowDeployment {
|
|
|
114
122
|
reset() {}
|
|
115
123
|
}
|
|
116
124
|
|
|
117
|
-
module.exports = CashFlowDeployment;
|
|
125
|
+
module.exports = CashFlowDeployment;
|
|
@@ -59,6 +59,18 @@ class CashFlowLiquidation {
|
|
|
59
59
|
if (snap.exists) dataMap.set(idx, snap.data());
|
|
60
60
|
});
|
|
61
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
|
+
|
|
62
74
|
// 2. Find the withdrawal signal
|
|
63
75
|
let withdrawalSignal = null;
|
|
64
76
|
let withdrawalSignalDay = null;
|
|
@@ -92,14 +104,10 @@ class CashFlowLiquidation {
|
|
|
92
104
|
};
|
|
93
105
|
}
|
|
94
106
|
|
|
95
|
-
//
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
if (!cashFlowData || !assetFlowData) {
|
|
100
|
-
logger.log('WARN', `[CashFlowLiquidation] Missing dependency data for ${dateStr}. Skipping.`);
|
|
101
|
-
return null;
|
|
102
|
-
}
|
|
107
|
+
// --- REMOVED ---
|
|
108
|
+
// The check for cashFlowData and assetFlowData was here
|
|
109
|
+
// but has been moved up.
|
|
110
|
+
// --- END REMOVED ---
|
|
103
111
|
|
|
104
112
|
// 'trading_effect' will be negative if the crowd is net-selling
|
|
105
113
|
const netSellPct = cashFlowData.components?.trading_effect || 0;
|
package/package.json
CHANGED
|
@@ -1,151 +1,99 @@
|
|
|
1
|
-
// utils/price_data_provider.js
|
|
2
1
|
const { Firestore } = require('@google-cloud/firestore');
|
|
3
2
|
const firestore = new Firestore();
|
|
4
3
|
|
|
4
|
+
// Config
|
|
5
5
|
const PRICE_COLLECTION = 'asset_prices';
|
|
6
6
|
const CACHE_DURATION_MS = 3600000; // 1 hour
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
// Cache
|
|
9
|
+
let cache = {
|
|
10
|
+
timestamp: null,
|
|
11
|
+
priceMap: null, // Will be { instrumentId: { "YYYY-MM-DD": price, ... } }
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
// In-progress fetch promise
|
|
9
15
|
let fetchPromise = null;
|
|
10
16
|
|
|
11
|
-
|
|
12
|
-
|
|
17
|
+
/**
|
|
18
|
+
* Finds the most recent available price on or before a given date.
|
|
19
|
+
* @param {object} priceHistory - The map of { "YYYY-MM-DD": price }
|
|
20
|
+
* @param {string} dateStr - The target date string to start searching from.
|
|
21
|
+
* @param {number} [maxLookback=5] - Max days to look back (to skip weekends/holidays).
|
|
22
|
+
* @returns {number|null} The price, or null if not found.
|
|
23
|
+
*/
|
|
24
|
+
function _findPreviousAvailablePrice(priceHistory, dateStr, maxLookback = 5) {
|
|
25
|
+
if (!priceHistory) return null;
|
|
26
|
+
|
|
27
|
+
let checkDate = new Date(dateStr + 'T00:00:00Z');
|
|
28
|
+
|
|
29
|
+
for (let i = 0; i < maxLookback; i++) {
|
|
30
|
+
const checkDateStr = checkDate.toISOString().slice(0, 10);
|
|
31
|
+
const price = priceHistory[checkDateStr];
|
|
32
|
+
|
|
33
|
+
if (price !== undefined && price !== null && price > 0) {
|
|
34
|
+
return price; // Found it
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// If not found, look back one more day
|
|
38
|
+
checkDate.setUTCDate(checkDate.getUTCDate() - 1);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// console.warn(`No price found for instrument within ${maxLookback} days of ${dateStr}`);
|
|
42
|
+
return null; // No price found within lookback
|
|
13
43
|
}
|
|
14
44
|
|
|
15
45
|
/**
|
|
16
|
-
*
|
|
17
|
-
* This
|
|
18
|
-
* 1) Sharded docs: each doc contains many instrumentId keys:
|
|
19
|
-
* { "360": { prices: {...}, ticker: "NG.JUL23" }, "1028": { ... }, ... }
|
|
20
|
-
* 2) Per-instrument docs: each doc id is an instrumentId and doc contains prices:
|
|
21
|
-
* doc.id = "360", doc.data() = { prices: {...}, ticker: "NG.JUL23", lastUpdated: ... }
|
|
22
|
-
*
|
|
23
|
-
* The returned priceMap will always be:
|
|
24
|
-
* { "<instrumentId>": { prices: { "YYYY-MM-DD": number, ... }, ticker: string|null, lastUpdated: any|null } }
|
|
46
|
+
* Loads all sharded price data from the `asset_prices` collection.
|
|
47
|
+
* This is a heavy operation and should be cached.
|
|
25
48
|
*/
|
|
26
49
|
async function loadAllPriceData() {
|
|
27
50
|
const now = Date.now();
|
|
28
51
|
if (cache.timestamp && (now - cache.timestamp < CACHE_DURATION_MS)) {
|
|
29
|
-
console.debug('[price_data_provider] returning cached priceMap', {
|
|
30
|
-
cachedAt: new Date(cache.timestamp).toISOString(),
|
|
31
|
-
instrumentCount: Object.keys(cache.priceMap || {}).length
|
|
32
|
-
});
|
|
33
52
|
return cache.priceMap;
|
|
34
53
|
}
|
|
35
54
|
|
|
36
|
-
if (fetchPromise)
|
|
55
|
+
if (fetchPromise) {
|
|
56
|
+
return fetchPromise;
|
|
57
|
+
}
|
|
37
58
|
|
|
38
59
|
fetchPromise = (async () => {
|
|
39
|
-
console.log('
|
|
60
|
+
console.log('Fetching and caching all asset price data...');
|
|
40
61
|
const masterPriceMap = {};
|
|
41
|
-
|
|
42
|
-
let shardDocs = 0;
|
|
43
|
-
let singleInstrumentDocs = 0;
|
|
62
|
+
|
|
44
63
|
try {
|
|
45
64
|
const snapshot = await firestore.collection(PRICE_COLLECTION).get();
|
|
65
|
+
|
|
46
66
|
if (snapshot.empty) {
|
|
47
|
-
|
|
48
|
-
return {};
|
|
67
|
+
throw new Error(`Price collection '${PRICE_COLLECTION}' is empty.`);
|
|
49
68
|
}
|
|
50
69
|
|
|
70
|
+
// Loop through each shard document (e.g., "shard_0", "shard_1")
|
|
51
71
|
snapshot.forEach(doc => {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
const instrumentId = String(doc.id);
|
|
60
|
-
masterPriceMap[instrumentId] = {
|
|
61
|
-
prices: data.prices || {},
|
|
62
|
-
ticker: data.ticker || null,
|
|
63
|
-
lastUpdated: data.lastUpdated || null
|
|
64
|
-
};
|
|
65
|
-
return; // continue to next doc
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// Heuristic 2: otherwise, check whether doc contains multiple instrumentId keys
|
|
69
|
-
// e.g., doc.data() looks like { "360": { prices: {...}, ticker: "NG.JUL23" }, "1028": { ... } }
|
|
70
|
-
const keys = Object.keys(data || {});
|
|
71
|
-
// detect shard-style by checking if at least one value is an object with .prices
|
|
72
|
-
const looksLikeShard = keys.length > 0 && keys.some(k => {
|
|
73
|
-
const v = data[k];
|
|
74
|
-
return isPlainObject(v) && ('prices' in v) && isPlainObject(v.prices);
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
if (looksLikeShard) {
|
|
78
|
-
shardDocs++;
|
|
79
|
-
for (const instrumentIdRaw of keys) {
|
|
80
|
-
try {
|
|
81
|
-
const entry = data[instrumentIdRaw];
|
|
82
|
-
if (!entry || !isPlainObject(entry)) continue;
|
|
83
|
-
if (!('prices' in entry) || !isPlainObject(entry.prices)) continue;
|
|
84
|
-
|
|
85
|
-
const instrumentId = String(instrumentIdRaw);
|
|
86
|
-
masterPriceMap[instrumentId] = {
|
|
87
|
-
prices: entry.prices || {},
|
|
88
|
-
ticker: entry.ticker || null,
|
|
89
|
-
lastUpdated: entry.lastUpdated || null
|
|
90
|
-
};
|
|
91
|
-
} catch (e) {
|
|
92
|
-
// resilience: skip malformed instrument entry
|
|
93
|
-
console.debug('[price_data_provider] skipping malformed entry in shard', {
|
|
94
|
-
docId: doc.id,
|
|
95
|
-
instrumentIdRaw,
|
|
96
|
-
err: e && e.message
|
|
97
|
-
});
|
|
98
|
-
}
|
|
72
|
+
const shardData = doc.data();
|
|
73
|
+
|
|
74
|
+
// Loop through each instrumentId in the shard
|
|
75
|
+
for (const instrumentId in shardData) {
|
|
76
|
+
// Check if it's a valid instrument entry
|
|
77
|
+
if (shardData[instrumentId] && shardData[instrumentId].prices) {
|
|
78
|
+
masterPriceMap[instrumentId] = shardData[instrumentId].prices;
|
|
99
79
|
}
|
|
100
|
-
return;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// If we get here, doc shape is unexpected. Log it for debugging and attempt
|
|
104
|
-
// a best-effort extraction: if doc keys look numeric and values are numbers
|
|
105
|
-
// or nested simple maps, we may try to treat doc.id as instrumentId fallback.
|
|
106
|
-
console.debug('[price_data_provider] unexpected doc shape in asset_prices', {
|
|
107
|
-
docId: doc.id,
|
|
108
|
-
sampleKeys: Object.keys(data || {}).slice(0, 5)
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
// Fallback: if doc has top-level numeric-like id and a single 'prices' like nested object, try doc.id
|
|
112
|
-
if (isPlainObject(data) && Object.keys(data).length > 0 && 'prices' in data) {
|
|
113
|
-
const instrumentId = String(doc.id);
|
|
114
|
-
masterPriceMap[instrumentId] = {
|
|
115
|
-
prices: data.prices || {},
|
|
116
|
-
ticker: data.ticker || null,
|
|
117
|
-
lastUpdated: data.lastUpdated || null
|
|
118
|
-
};
|
|
119
80
|
}
|
|
120
81
|
});
|
|
121
82
|
|
|
122
83
|
cache = {
|
|
123
84
|
timestamp: now,
|
|
124
85
|
priceMap: masterPriceMap,
|
|
125
|
-
meta: {
|
|
126
|
-
instrumentCount: Object.keys(masterPriceMap).length,
|
|
127
|
-
docCount,
|
|
128
|
-
shardDocs,
|
|
129
|
-
singleInstrumentDocs
|
|
130
|
-
}
|
|
131
86
|
};
|
|
132
|
-
|
|
133
|
-
console.log(
|
|
134
|
-
|
|
135
|
-
// also print a small sample to help debugging quickly
|
|
136
|
-
const sampleIds = Object.keys(masterPriceMap).slice(0, 10);
|
|
137
|
-
const samplePreview = sampleIds.map(id => ({
|
|
138
|
-
id,
|
|
139
|
-
ticker: masterPriceMap[id].ticker || null,
|
|
140
|
-
sampleDates: Object.keys(masterPriceMap[id].prices || {}).slice(0, 3)
|
|
141
|
-
}));
|
|
142
|
-
console.debug('[price_data_provider] sample preview:', samplePreview);
|
|
143
|
-
|
|
87
|
+
|
|
88
|
+
console.log(`Successfully cached prices for ${Object.keys(masterPriceMap).length} instruments.`);
|
|
144
89
|
return masterPriceMap;
|
|
90
|
+
|
|
145
91
|
} catch (err) {
|
|
146
|
-
console.error('
|
|
92
|
+
console.error('CRITICAL: Error loading price data:', err);
|
|
93
|
+
// On error, return an empty map but don't cache, so a future call can retry.
|
|
147
94
|
return {};
|
|
148
95
|
} finally {
|
|
96
|
+
// Clear the promise so the next call (if cache is stale) triggers a new fetch
|
|
149
97
|
fetchPromise = null;
|
|
150
98
|
}
|
|
151
99
|
})();
|
|
@@ -154,40 +102,41 @@ async function loadAllPriceData() {
|
|
|
154
102
|
}
|
|
155
103
|
|
|
156
104
|
/**
|
|
157
|
-
*
|
|
158
|
-
*
|
|
105
|
+
* A helper to safely get the price change percentage between two dates.
|
|
106
|
+
* --- THIS IS THE MODIFIED FUNCTION ---
|
|
107
|
+
* @param {string} instrumentId - The instrument ID.
|
|
108
|
+
* @param {string} yesterdayStr - YYYY-MM-DD date string for yesterday.
|
|
109
|
+
* @param {string} todayStr - YYYY-MM-DD date string for today.
|
|
110
|
+
* @param {object} priceMap - The master price map from loadAllPriceData().
|
|
111
|
+
* @returns {number|null} The percentage change (e.g., 0.10 for +10%), or null if data is missing.
|
|
159
112
|
*/
|
|
160
113
|
function getDailyPriceChange(instrumentId, yesterdayStr, todayStr, priceMap) {
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
if (typeof p1 !== 'number' || typeof p2 !== 'number' || p1 <= 0) {
|
|
183
|
-
console.debug('[price_data_provider] invalid price values', { instrumentId: id, p1, p2 });
|
|
184
|
-
return null;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
return (p2 - p1) / p1;
|
|
114
|
+
if (!priceMap || !priceMap[instrumentId]) {
|
|
115
|
+
return null; // No price data for this instrument
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const instrumentPrices = priceMap[instrumentId];
|
|
119
|
+
|
|
120
|
+
// Find the most recent available price *on or before* the target dates
|
|
121
|
+
const priceDay1 = _findPreviousAvailablePrice(instrumentPrices, yesterdayStr, 5);
|
|
122
|
+
const priceDay2 = _findPreviousAvailablePrice(instrumentPrices, todayStr, 5);
|
|
123
|
+
|
|
124
|
+
if (priceDay1 && priceDay2) {
|
|
125
|
+
// We found prices, now check if they are the *same* price
|
|
126
|
+
// (e.g., if today is Sunday, priceDay2 would be Friday's price.
|
|
127
|
+
// If yesterday was Saturday, priceDay1 would *also* be Friday's price).
|
|
128
|
+
if (priceDay1 === priceDay2) {
|
|
129
|
+
return 0.0; // No change between the two most recent *available* dates
|
|
130
|
+
}
|
|
131
|
+
return (priceDay2 - priceDay1) / priceDay1;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return null; // Missing one or both dates, even after lookback
|
|
188
135
|
}
|
|
189
136
|
|
|
137
|
+
|
|
190
138
|
module.exports = {
|
|
191
|
-
|
|
192
|
-
|
|
139
|
+
loadAllPriceData,
|
|
140
|
+
getDailyPriceChange
|
|
141
|
+
// Add the helper to exports if you want, but it's not required
|
|
193
142
|
};
|