aiden-shared-calculations-unified 1.0.28 → 1.0.30

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.
@@ -81,64 +81,85 @@ class AssetCrowdFlow {
81
81
 
82
82
  async getResult() {
83
83
  if (this.user_count === 0 || !this.dates.today) {
84
- return {}; // No users processed or dates not found
84
+ console.warn('[AssetCrowdFlow] No users processed or dates missing.');
85
+ return {};
85
86
  }
86
87
 
87
- // Load dependencies (prices and mappings) in parallel
88
+ // Load priceMap and mappings if not loaded
88
89
  if (!this.priceMap || !this.mappings) {
89
- const [priceData, mappingData] = await Promise.all([
90
- loadAllPriceData(),
91
- loadInstrumentMappings()
92
- ]);
93
- this.priceMap = priceData;
94
- this.mappings = mappingData;
90
+ try {
91
+ const [priceData, mappingData] = await Promise.all([
92
+ loadAllPriceData(),
93
+ loadInstrumentMappings()
94
+ ]);
95
+ this.priceMap = priceData;
96
+ this.mappings = mappingData;
97
+ } catch (err) {
98
+ console.error('[AssetCrowdFlow] Failed to load dependencies:', err);
99
+ return {};
100
+ }
95
101
  }
96
-
102
+
97
103
  const finalResults = {};
98
104
  const todayStr = this.dates.today;
99
105
  const yesterdayStr = this.dates.yesterday;
100
106
 
101
- for (const instrumentId in this.asset_values) {
102
- const ticker = this.mappings.instrumentToTicker[instrumentId] || `id_${instrumentId}`;
103
-
104
- // 1. Calculate average % values
105
- const avg_day1_value = this.asset_values[instrumentId].day1_value_sum / this.user_count;
106
- const avg_day2_value = this.asset_values[instrumentId].day2_value_sum / this.user_count;
107
-
108
- // 2. Get the actual price change
109
- const priceChangePct = getDailyPriceChange(instrumentId, yesterdayStr, todayStr, this.priceMap);
107
+ for (const rawInstrumentId in this.asset_values) {
108
+ const instrumentId = String(rawInstrumentId); // normalize
109
+ const ticker = this.mappings.instrumentToTicker?.[instrumentId] || `id_${instrumentId}`;
110
+
111
+ const avg_day1_value = this.asset_values[rawInstrumentId].day1_value_sum / this.user_count;
112
+ const avg_day2_value = this.asset_values[rawInstrumentId].day2_value_sum / this.user_count;
113
+
114
+ let priceChangePct = null;
115
+
116
+ // Check priceMap presence
117
+ if (!this.priceMap || !this.priceMap[instrumentId]) {
118
+ console.debug(`[AssetCrowdFlow] Missing priceMap entry for instrumentId ${instrumentId} (${ticker})`);
119
+ } else {
120
+ const priceDay1 = this.priceMap[instrumentId][yesterdayStr];
121
+ const priceDay2 = this.priceMap[instrumentId][todayStr];
122
+
123
+ if (priceDay1 == null) console.debug(`[AssetCrowdFlow] Missing price for ${instrumentId} (${ticker}) on ${yesterdayStr}`);
124
+ if (priceDay2 == null) console.debug(`[AssetCrowdFlow] Missing price for ${instrumentId} (${ticker}) on ${todayStr}`);
125
+
126
+ if (priceDay1 != null && priceDay2 != null && priceDay1 > 0) {
127
+ priceChangePct = (priceDay2 - priceDay1) / priceDay1;
128
+ }
129
+ }
110
130
 
111
131
  if (priceChangePct === null) {
112
- // Cannot calculate if price data is missing for either day
113
132
  finalResults[ticker] = {
114
133
  net_crowd_flow_pct: 0,
115
- error: "Missing price data for calculation."
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'
116
139
  };
117
140
  continue;
118
141
  }
119
142
 
120
- // 3. Calculate the expected value (the "price-move" effect)
121
- // We use avg_day1_value as the base. The cash flow proxy calculation
122
- // uses (avg_value - avg_invested) because it's solving for a different unknown.
123
- // Here, we are solving for flow *relative to the asset itself*.
143
+ // Calculate expected day2 value from price movement
124
144
  const expected_day2_value = avg_day1_value * (1 + priceChangePct);
125
145
 
126
- // 4. Find the signal (the "crowd-flow" effect)
146
+ // Net crowd flow = actual minus expected
127
147
  const net_crowd_flow_pct = avg_day2_value - expected_day2_value;
128
148
 
129
149
  finalResults[ticker] = {
130
- net_crowd_flow_pct: net_crowd_flow_pct,
131
- avg_value_day1_pct: avg_day1_value,
132
- avg_value_day2_pct: avg_day2_value,
133
- expected_value_day2_pct: expected_day2_value,
150
+ net_crowd_flow_pct,
151
+ avg_value_day1,
152
+ avg_value_day2,
153
+ expected_day2_value,
134
154
  price_change_pct: priceChangePct,
135
155
  user_sample_size: this.user_count
136
156
  };
137
157
  }
138
-
158
+
139
159
  return finalResults;
140
160
  }
141
161
 
162
+
142
163
  reset() {
143
164
  this.asset_values = {};
144
165
  this.user_count = 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aiden-shared-calculations-unified",
3
- "version": "1.0.28",
3
+ "version": "1.0.30",
4
4
  "description": "Shared calculation modules for the BullTrackers Computation System.",
5
5
  "main": "index.js",
6
6
  "files": [
@@ -1,71 +1,151 @@
1
+ // utils/price_data_provider.js
1
2
  const { Firestore } = require('@google-cloud/firestore');
2
3
  const firestore = new Firestore();
3
4
 
4
- // Config
5
5
  const PRICE_COLLECTION = 'asset_prices';
6
6
  const CACHE_DURATION_MS = 3600000; // 1 hour
7
7
 
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
8
+ let cache = { timestamp: null, priceMap: null, meta: null };
15
9
  let fetchPromise = null;
16
10
 
11
+ function isPlainObject(v) {
12
+ return v && typeof v === 'object' && !Array.isArray(v);
13
+ }
14
+
17
15
  /**
18
- * Loads all sharded price data from the `asset_prices` collection.
19
- * This is a heavy operation and should be cached.
16
+ * Load all price data from 'asset_prices' collection.
17
+ * This loader is resilient to two common shapes:
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 } }
20
25
  */
21
26
  async function loadAllPriceData() {
22
27
  const now = Date.now();
23
28
  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
+ });
24
33
  return cache.priceMap;
25
34
  }
26
35
 
27
- if (fetchPromise) {
28
- return fetchPromise;
29
- }
36
+ if (fetchPromise) return fetchPromise;
30
37
 
31
38
  fetchPromise = (async () => {
32
- console.log('Fetching and caching all asset price data...');
39
+ console.log('[price_data_provider] fetching asset_prices collection from Firestore...');
33
40
  const masterPriceMap = {};
34
-
41
+ let docCount = 0;
42
+ let shardDocs = 0;
43
+ let singleInstrumentDocs = 0;
35
44
  try {
36
45
  const snapshot = await firestore.collection(PRICE_COLLECTION).get();
37
-
38
46
  if (snapshot.empty) {
39
- throw new Error(`Price collection '${PRICE_COLLECTION}' is empty.`);
47
+ console.warn(`[price_data_provider] collection "${PRICE_COLLECTION}" is empty`);
48
+ return {};
40
49
  }
41
50
 
42
- // Loop through each shard document (e.g., "shard_0", "shard_1")
43
51
  snapshot.forEach(doc => {
44
- const shardData = doc.data();
45
-
46
- // Loop through each instrumentId in the shard
47
- for (const instrumentId in shardData) {
48
- // Check if it's a valid instrument entry
49
- if (shardData[instrumentId] && shardData[instrumentId].prices) {
50
- masterPriceMap[instrumentId] = shardData[instrumentId].prices;
52
+ docCount++;
53
+ const data = doc.data();
54
+
55
+ // Heuristic 1: if doc.data() has a 'prices' key and it's an object of date->number,
56
+ // treat this doc as a per-instrument document (doc.id = instrumentId).
57
+ if (isPlainObject(data) && 'prices' in data && isPlainObject(data.prices)) {
58
+ singleInstrumentDocs++;
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
+ }
51
99
  }
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
+ };
52
119
  }
53
120
  });
54
121
 
55
122
  cache = {
56
123
  timestamp: now,
57
124
  priceMap: masterPriceMap,
125
+ meta: {
126
+ instrumentCount: Object.keys(masterPriceMap).length,
127
+ docCount,
128
+ shardDocs,
129
+ singleInstrumentDocs
130
+ }
58
131
  };
59
-
60
- console.log(`Successfully cached prices for ${Object.keys(masterPriceMap).length} instruments.`);
61
- return masterPriceMap;
62
132
 
133
+ console.log('[price_data_provider] load complete', cache.meta);
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
+
144
+ return masterPriceMap;
63
145
  } catch (err) {
64
- console.error('CRITICAL: Error loading price data:', err);
65
- // On error, return an empty map but don't cache, so a future call can retry.
146
+ console.error('[price_data_provider] error loading price data:', err);
66
147
  return {};
67
148
  } finally {
68
- // Clear the promise so the next call (if cache is stale) triggers a new fetch
69
149
  fetchPromise = null;
70
150
  }
71
151
  })();
@@ -74,30 +154,40 @@ async function loadAllPriceData() {
74
154
  }
75
155
 
76
156
  /**
77
- * A helper to safely get the price change percentage between two dates.
78
- * @param {string} instrumentId - The instrument ID.
79
- * @param {string} yesterdayStr - YYYY-MM-DD date string for yesterday.
80
- * @param {string} todayStr - YYYY-MM-DD date string for today.
81
- * @param {object} priceMap - The master price map from loadAllPriceData().
82
- * @returns {number|null} The percentage change (e.g., 0.10 for +10%), or null if data is missing.
157
+ * Get daily price change percentage. Accepts priceMap with shape produced by loadAllPriceData().
158
+ * Returns number (e.g., 0.1 for +10%) or null if not available.
83
159
  */
84
160
  function getDailyPriceChange(instrumentId, yesterdayStr, todayStr, priceMap) {
85
- if (!priceMap || !priceMap[instrumentId]) {
86
- return null; // No price data for this instrument
87
- }
88
-
89
- const priceDay1 = priceMap[instrumentId][yesterdayStr];
90
- const priceDay2 = priceMap[instrumentId][todayStr];
161
+ if (!priceMap) return null;
162
+ const id = String(instrumentId);
163
+ const entry = priceMap[id];
91
164
 
92
- if (priceDay1 && priceDay2 && priceDay1 > 0) {
93
- return (priceDay2 - priceDay1) / priceDay1;
94
- }
95
-
96
- return null; // Missing one or both dates, or division by zero
97
- }
165
+ if (!entry) return null;
166
+
167
+ // entry may be { prices: {...}, ticker, lastUpdated } OR legacy style where entry is the raw prices map
168
+ const prices = (isPlainObject(entry) && 'prices' in entry) ? entry.prices : entry;
98
169
 
170
+ if (!isPlainObject(prices)) return null;
171
+
172
+ const p1 = prices[yesterdayStr];
173
+ const p2 = prices[todayStr];
174
+
175
+ // log missing specifics for traceability (not too spammy)
176
+ if (p1 == null || p2 == null) {
177
+ // caller should already log, but this helps track date mismatches
178
+ console.debug('[price_data_provider] missing date price', { instrumentId: id, yesterdayStr, todayStr, hasP1: p1 != null, hasP2: p2 != null });
179
+ return null;
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;
188
+ }
99
189
 
100
190
  module.exports = {
101
- loadAllPriceData,
102
- getDailyPriceChange
191
+ loadAllPriceData,
192
+ getDailyPriceChange
103
193
  };