bulltrackers-module 1.0.182 → 1.0.183

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.
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * FIXED: computation_controller.js
3
- * V3.2: Supports Streaming Trading History (todayHistory) separate from Yesterday's Portfolio.
3
+ * V3.3: Adds Price Loading for Meta Context & Fixes Context Injection
4
4
  */
5
5
 
6
6
  const { DataExtractor,
@@ -23,7 +23,7 @@ class DataLoader {
23
23
  constructor(config, dependencies) {
24
24
  this.config = config;
25
25
  this.deps = dependencies;
26
- this.cache = { mappings: null, insights: new Map(), social: new Map() };
26
+ this.cache = { mappings: null, insights: new Map(), social: new Map(), prices: null };
27
27
  }
28
28
  async loadMappings() {
29
29
  if (this.cache.mappings) return this.cache.mappings;
@@ -43,6 +43,48 @@ class DataLoader {
43
43
  this.cache.social.set(dateStr, social);
44
44
  return social;
45
45
  }
46
+ /**
47
+ * NEW: Loads sharded price data for Meta calculations
48
+ */
49
+ async loadPrices() {
50
+ if (this.cache.prices) return this.cache.prices;
51
+ const { db, logger } = this.deps;
52
+ const collection = this.config.priceCollection || 'asset_prices';
53
+
54
+ logger.log('INFO', `[DataLoader] Loading all price shards from ${collection}...`);
55
+
56
+ try {
57
+ const snapshot = await db.collection(collection).get();
58
+ if (snapshot.empty) return { history: [] };
59
+
60
+ // Flatten shards into a single array for the context
61
+ // Structure expected by calculation: Array of { instrumentId, prices: {...} }
62
+ const allPrices = [];
63
+
64
+ snapshot.forEach(doc => {
65
+ const shardData = doc.data();
66
+ // Iterate keys in shard (instrumentIds)
67
+ for (const [instId, data] of Object.entries(shardData)) {
68
+ if (data && data.prices) {
69
+ allPrices.push({
70
+ instrumentId: instId,
71
+ ...data
72
+ });
73
+ }
74
+ }
75
+ });
76
+
77
+ logger.log('INFO', `[DataLoader] Loaded prices for ${allPrices.length} instruments.`);
78
+
79
+ // Cache as an object with 'history' array to match context expectations
80
+ this.cache.prices = { history: allPrices };
81
+ return this.cache.prices;
82
+
83
+ } catch (e) {
84
+ logger.log('ERROR', `[DataLoader] Failed to load prices: ${e.message}`);
85
+ return { history: [] };
86
+ }
87
+ }
46
88
  }
47
89
 
48
90
  class ContextBuilder {
@@ -76,7 +118,7 @@ class ContextBuilder {
76
118
  insights: { today: insights?.today, yesterday: insights?.yesterday },
77
119
  social: { today: socialData?.today, yesterday: socialData?.yesterday },
78
120
  mappings: mappings || {},
79
- math: { // Introduced a new computation class in the math primitives? Add it here. Then add it to meta context a little further down.
121
+ math: {
80
122
  extract: DataExtractor,
81
123
  history: HistoryExtractor,
82
124
  compute: MathPrimitives,
@@ -103,6 +145,7 @@ class ContextBuilder {
103
145
  mappings,
104
146
  insights,
105
147
  socialData,
148
+ prices, // <--- ADDED THIS
106
149
  computedDependencies,
107
150
  previousComputedDependencies,
108
151
  config,
@@ -113,8 +156,9 @@ class ContextBuilder {
113
156
  date: { today: dateStr },
114
157
  insights: { today: insights?.today, yesterday: insights?.yesterday },
115
158
  social: { today: socialData?.today, yesterday: socialData?.yesterday },
159
+ prices: prices || {}, // <--- INJECTED HERE
116
160
  mappings: mappings || {},
117
- math: { // Introduced a new computation class in the math primitives? Add it here.
161
+ math: {
118
162
  extract: DataExtractor,
119
163
  history: HistoryExtractor,
120
164
  compute: MathPrimitives,
@@ -142,22 +186,22 @@ class ComputationExecutor {
142
186
  this.loader = dataLoader;
143
187
  }
144
188
 
145
- /**
146
- * UPDATED: Accepts yesterdayPortfolioData AND historyData separately.
147
- */
148
189
  async executePerUser(calcInstance, metadata, dateStr, portfolioData, yesterdayPortfolioData, historyData, computedDeps, prevDeps) {
149
190
  const { logger } = this.deps;
150
191
  const targetUserType = metadata.userType;
151
192
  const mappings = await this.loader.loadMappings();
152
193
  const insights = metadata.rootDataDependencies?.includes('insights') ? { today: await this.loader.loadInsights(dateStr) } : null;
194
+
153
195
  for (const [userId, todayPortfolio] of Object.entries(portfolioData)) {
154
196
  const yesterdayPortfolio = yesterdayPortfolioData ? yesterdayPortfolioData[userId] : null;
155
197
  const todayHistory = historyData ? historyData[userId] : null;
156
198
  const actualUserType = todayPortfolio.PublicPositions ? SCHEMAS.USER_TYPES.SPECULATOR : SCHEMAS.USER_TYPES.NORMAL;
199
+
157
200
  if (targetUserType !== 'all') {
158
201
  const mappedTarget = (targetUserType === 'speculator') ? SCHEMAS.USER_TYPES.SPECULATOR : SCHEMAS.USER_TYPES.NORMAL;
159
202
  if (mappedTarget !== actualUserType) continue;
160
203
  }
204
+
161
205
  const context = ContextBuilder.buildPerUserContext({
162
206
  todayPortfolio, yesterdayPortfolio,
163
207
  todayHistory,
@@ -166,6 +210,7 @@ class ComputationExecutor {
166
210
  previousComputedDependencies: prevDeps,
167
211
  config: this.config, deps: this.deps
168
212
  });
213
+
169
214
  try { await calcInstance.process(context); }
170
215
  catch (e) { logger.log('WARN', `Calc ${metadata.name} failed for user ${userId}: ${e.message}`); }
171
216
  }
@@ -173,10 +218,20 @@ class ComputationExecutor {
173
218
 
174
219
  async executeOncePerDay(calcInstance, metadata, dateStr, computedDeps, prevDeps) {
175
220
  const mappings = await this.loader.loadMappings();
221
+
222
+ // Load standard dependencies
176
223
  const insights = metadata.rootDataDependencies?.includes('insights') ? { today: await this.loader.loadInsights(dateStr) } : null;
177
224
  const social = metadata.rootDataDependencies?.includes('social') ? { today: await this.loader.loadSocial(dateStr) } : null;
225
+
226
+ // NEW: Load Price dependencies if required
227
+ let prices = null;
228
+ if (metadata.rootDataDependencies?.includes('price')) {
229
+ prices = await this.loader.loadPrices();
230
+ }
231
+
178
232
  const context = ContextBuilder.buildMetaContext({
179
233
  dateStr, metadata, mappings, insights, socialData: social,
234
+ prices, // Pass prices to builder
180
235
  computedDependencies: computedDeps,
181
236
  previousComputedDependencies: prevDeps,
182
237
  config: this.config, deps: this.deps
@@ -7,7 +7,7 @@ const {
7
7
  checkRootDataAvailability,
8
8
  fetchExistingResults,
9
9
  fetchComputationStatus,
10
- updateComputationStatus,
10
+ updateComputationStatus,
11
11
  runStandardComputationPass,
12
12
  runMetaComputationPass,
13
13
  checkRootDependencies
@@ -36,7 +36,7 @@ async function runComputationPass(config, dependencies, computationManifest) {
36
36
  return logger.log('WARN', `[PassRunner] No calcs for Pass ${passToRun}. Exiting.`);
37
37
 
38
38
  const passEarliestDate = earliestDates.absoluteEarliest;
39
- const endDateUTC = new Date(Date.UTC(new Date().getUTCFullYear(), new Date().getUTCMonth(), new Date().getUTCDate() - 1));
39
+ const endDateUTC = new Date(Date.UTC(new Date().getUTCFullYear(), new Date().getUTCMonth(), new Date().getUTCDate() - 1));
40
40
  const allExpectedDates = getExpectedDateStrings(passEarliestDate, endDateUTC);
41
41
 
42
42
  const standardCalcs = calcsInThisPass.filter(c => c.type === 'standard');
@@ -1,5 +1,6 @@
1
1
  /**
2
2
  * FILENAME: bulltrackers-module/functions/computation-system/helpers/orchestration_helpers.js
3
+ * FIXED: Only marks computations as TRUE if they actually store results.
3
4
  */
4
5
 
5
6
  const { ComputationController } = require('../controllers/computation_controller');
@@ -27,6 +28,8 @@ function checkRootDependencies(calcManifest, rootDataStatus) {
27
28
  else if (dep === 'insights' && !rootDataStatus.hasInsights) missing.push('insights');
28
29
  else if (dep === 'social' && !rootDataStatus.hasSocial) missing.push('social');
29
30
  else if (dep === 'history' && !rootDataStatus.hasHistory) missing.push('history');
31
+ // Note: 'price' is typically not a blocking root check in this specific function logic unless added,
32
+ // but usually prices are treated as auxiliary. If you want to block on prices, add it here.
30
33
  }
31
34
  return { canRun: missing.length === 0, missing };
32
35
  }
@@ -35,7 +38,6 @@ function checkRootDependencies(calcManifest, rootDataStatus) {
35
38
  * Checks for the availability of all required root data for a specific date.
36
39
  */
37
40
  async function checkRootDataAvailability(dateStr, config, dependencies, earliestDates) {
38
- // ... [Unchanged content of checkRootDataAvailability] ...
39
41
  const { logger } = dependencies;
40
42
  const dateToProcess = new Date(dateStr + 'T00:00:00Z');
41
43
  let portfolioRefs = [], historyRefs = [];
@@ -50,6 +52,7 @@ async function checkRootDataAvailability(dateStr, config, dependencies, earliest
50
52
 
51
53
  await Promise.all(tasks);
52
54
 
55
+ // We allow running if ANY data is present. Specific calcs filter themselves using checkRootDependencies.
53
56
  if (!(hasPortfolio || hasInsights || hasSocial || hasHistory)) return null;
54
57
 
55
58
  return {
@@ -64,10 +67,6 @@ async function checkRootDataAvailability(dateStr, config, dependencies, earliest
64
67
  }
65
68
  }
66
69
 
67
- /**
68
- * --- DEPRECATED: Old per-date fetch ---
69
- * Keeps compatibility but logic moves to fetchGlobalComputationStatus
70
- */
71
70
  async function fetchComputationStatus(dateStr, config, { db }) {
72
71
  const collection = config.computationStatusCollection || 'computation_status';
73
72
  const docRef = db.collection(collection).doc(dateStr);
@@ -75,10 +74,6 @@ async function fetchComputationStatus(dateStr, config, { db }) {
75
74
  return snap.exists ? snap.data() : {};
76
75
  }
77
76
 
78
- /**
79
- * --- NEW: Fetches the SINGLE GLOBAL status document ---
80
- * Loads the entire history of statuses in one read.
81
- */
82
77
  async function fetchGlobalComputationStatus(config, { db }) {
83
78
  const collection = config.computationStatusCollection || 'computation_status';
84
79
  const docRef = db.collection(collection).doc('global_status');
@@ -86,9 +81,6 @@ async function fetchGlobalComputationStatus(config, { db }) {
86
81
  return snap.exists ? snap.data() : {};
87
82
  }
88
83
 
89
- /**
90
- * --- DEPRECATED: Old per-date update ---
91
- */
92
84
  async function updateComputationStatus(dateStr, updates, config, { db }) {
93
85
  if (!updates || Object.keys(updates).length === 0) return;
94
86
  const collection = config.computationStatusCollection || 'computation_status';
@@ -96,17 +88,11 @@ async function updateComputationStatus(dateStr, updates, config, { db }) {
96
88
  await docRef.set(updates, { merge: true });
97
89
  }
98
90
 
99
- /**
100
- * --- NEW: Batch Updates to Global Document ---
101
- * Accepts a map of { "YYYY-MM-DD": { calcName: true, ... } }
102
- * and writes them using dot notation to avoid overwriting other dates.
103
- */
104
91
  async function updateGlobalComputationStatus(updatesByDate, config, { db }) {
105
92
  if (!updatesByDate || Object.keys(updatesByDate).length === 0) return;
106
93
  const collection = config.computationStatusCollection || 'computation_status';
107
94
  const docRef = db.collection(collection).doc('global_status');
108
95
 
109
- // Flatten to dot notation for Firestore update: "2023-10-27.calcName": true
110
96
  const flattenUpdates = {};
111
97
  for (const [date, statuses] of Object.entries(updatesByDate)) {
112
98
  for (const [calc, status] of Object.entries(statuses)) {
@@ -117,7 +103,6 @@ async function updateGlobalComputationStatus(updatesByDate, config, { db }) {
117
103
  try {
118
104
  await docRef.update(flattenUpdates);
119
105
  } catch (err) {
120
- // If doc doesn't exist (first run), update fails. Use set({merge:true}).
121
106
  if (err.code === 5) { // NOT_FOUND
122
107
  const deepObj = {};
123
108
  for (const [date, statuses] of Object.entries(updatesByDate)) {
@@ -130,10 +115,6 @@ async function updateGlobalComputationStatus(updatesByDate, config, { db }) {
130
115
  }
131
116
  }
132
117
 
133
- /**
134
- * --- UPDATED: fetchExistingResults ---
135
- * (Unchanged, keeps fetching results per date as this is heavy data)
136
- */
137
118
  async function fetchExistingResults(dateStr, calcsInPass, fullManifest, config, { db }, includeSelf = false) {
138
119
  const manifestMap = new Map(fullManifest.map(c => [normalizeName(c.name), c]));
139
120
  const calcsToFetch = new Set();
@@ -158,10 +139,6 @@ async function fetchExistingResults(dateStr, calcsInPass, fullManifest, config,
158
139
  return fetched;
159
140
  }
160
141
 
161
- /**
162
- * --- UPDATED: streamAndProcess ---
163
- * (Unchanged)
164
- */
165
142
  async function streamAndProcess(dateStr, state, passName, config, deps, rootData, portfolioRefs, historyRefs, fetchedDeps, previousFetchedDeps) {
166
143
  const { logger } = deps;
167
144
  const controller = new ComputationController(config, deps);
@@ -211,10 +188,6 @@ async function streamAndProcess(dateStr, state, passName, config, deps, rootData
211
188
  logger.log('INFO', `[${passName}] Streaming complete.`);
212
189
  }
213
190
 
214
- /**
215
- * --- UPDATED: runStandardComputationPass ---
216
- * Now accepts `skipStatusWrite` and returns `successUpdates`
217
- */
218
191
  async function runStandardComputationPass(date, calcs, passName, config, deps, rootData, fetchedDeps, previousFetchedDeps, skipStatusWrite = false) {
219
192
  const dStr = date.toISOString().slice(0, 10);
220
193
  const logger = deps.logger;
@@ -240,14 +213,9 @@ async function runStandardComputationPass(date, calcs, passName, config, deps, r
240
213
 
241
214
  await streamAndProcess(dStr, state, passName, config, deps, fullRoot, rootData.portfolioRefs, rootData.historyRefs, fetchedDeps, previousFetchedDeps);
242
215
 
243
- // Return the updates instead of just writing them
244
216
  return await commitResults(state, dStr, passName, config, deps, skipStatusWrite);
245
217
  }
246
218
 
247
- /**
248
- * --- UPDATED: runMetaComputationPass ---
249
- * Now accepts `skipStatusWrite` and returns `successUpdates`
250
- */
251
219
  async function runMetaComputationPass(date, calcs, passName, config, deps, fetchedDeps, previousFetchedDeps, rootData, skipStatusWrite = false) {
252
220
  const controller = new ComputationController(config, deps);
253
221
  const dStr = date.toISOString().slice(0, 10);
@@ -266,8 +234,8 @@ async function runMetaComputationPass(date, calcs, passName, config, deps, fetch
266
234
  }
267
235
 
268
236
  /**
269
- * --- UPDATED: commitResults ---
270
- * Added `skipStatusWrite` parameter. Returns `successUpdates`.
237
+ * --- FIXED: commitResults ---
238
+ * Only marks 'successUpdates' if data is actually written.
271
239
  */
272
240
  async function commitResults(stateObj, dStr, passName, config, deps, skipStatusWrite = false) {
273
241
  const writes = [], schemas = [], sharded = {};
@@ -278,13 +246,23 @@ async function commitResults(stateObj, dStr, passName, config, deps, skipStatusW
278
246
  try {
279
247
  const result = await calc.getResult();
280
248
  if (!result) continue;
249
+
281
250
  const standardRes = {};
251
+ let hasData = false; // Track if this calc produced data
252
+
282
253
  for (const key in result) {
283
254
  if (key.startsWith('sharded_')) {
284
255
  const sData = result[key];
285
- for (const c in sData) { sharded[c] = sharded[c] || {}; Object.assign(sharded[c], sData[c]); }
286
- } else standardRes[key] = result[key];
256
+ for (const c in sData) {
257
+ sharded[c] = sharded[c] || {};
258
+ Object.assign(sharded[c], sData[c]);
259
+ }
260
+ if (Object.keys(sData).length > 0) hasData = true;
261
+ } else {
262
+ standardRes[key] = result[key];
263
+ }
287
264
  }
265
+
288
266
  if (Object.keys(standardRes).length) {
289
267
  standardRes._completed = true;
290
268
  writes.push({
@@ -293,7 +271,9 @@ async function commitResults(stateObj, dStr, passName, config, deps, skipStatusW
293
271
  .collection(config.computationsSubcollection).doc(name),
294
272
  data: standardRes
295
273
  });
274
+ hasData = true;
296
275
  }
276
+
297
277
  if (calc.manifest.class.getSchema) {
298
278
  const { class: _cls, ...safeMetadata } = calc.manifest;
299
279
  schemas.push({
@@ -304,7 +284,13 @@ async function commitResults(stateObj, dStr, passName, config, deps, skipStatusW
304
284
  });
305
285
  }
306
286
 
307
- successUpdates[name] = true;
287
+ // FIX: Only mark as successful if we actually had data to write.
288
+ if (hasData) {
289
+ successUpdates[name] = true;
290
+ } else {
291
+ // Optional: Log that it produced no data
292
+ // deps.logger.log('INFO', `Calc ${name} produced no data. Skipping status update.`);
293
+ }
308
294
 
309
295
  } catch (e) { deps.logger.log('ERROR', `Commit failed ${name}: ${e.message}`); }
310
296
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bulltrackers-module",
3
- "version": "1.0.182",
3
+ "version": "1.0.183",
4
4
  "description": "Helper Functions for Bulltrackers.",
5
5
  "main": "index.js",
6
6
  "files": [