bulltrackers-module 1.0.182 → 1.0.184

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.4: Adds Price Loading & 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,39 @@ 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
+ const historyMap = {};
61
+
62
+ snapshot.forEach(doc => {
63
+ const shardData = doc.data();
64
+ // Merge shard keys (instrumentIds) into main map
65
+ if (shardData) Object.assign(historyMap, shardData);
66
+ });
67
+
68
+ logger.log('INFO', `[DataLoader] Loaded prices for ${Object.keys(historyMap).length} instruments.`);
69
+
70
+ // Cache as an object with 'history' map to match priceExtractor expectations
71
+ this.cache.prices = { history: historyMap };
72
+ return this.cache.prices;
73
+
74
+ } catch (e) {
75
+ logger.log('ERROR', `[DataLoader] Failed to load prices: ${e.message}`);
76
+ return { history: {} };
77
+ }
78
+ }
46
79
  }
47
80
 
48
81
  class ContextBuilder {
@@ -76,7 +109,7 @@ class ContextBuilder {
76
109
  insights: { today: insights?.today, yesterday: insights?.yesterday },
77
110
  social: { today: socialData?.today, yesterday: socialData?.yesterday },
78
111
  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.
112
+ math: {
80
113
  extract: DataExtractor,
81
114
  history: HistoryExtractor,
82
115
  compute: MathPrimitives,
@@ -103,6 +136,7 @@ class ContextBuilder {
103
136
  mappings,
104
137
  insights,
105
138
  socialData,
139
+ prices, // <--- ADDED THIS
106
140
  computedDependencies,
107
141
  previousComputedDependencies,
108
142
  config,
@@ -113,8 +147,9 @@ class ContextBuilder {
113
147
  date: { today: dateStr },
114
148
  insights: { today: insights?.today, yesterday: insights?.yesterday },
115
149
  social: { today: socialData?.today, yesterday: socialData?.yesterday },
150
+ prices: prices || {}, // <--- INJECTED HERE
116
151
  mappings: mappings || {},
117
- math: { // Introduced a new computation class in the math primitives? Add it here.
152
+ math: {
118
153
  extract: DataExtractor,
119
154
  history: HistoryExtractor,
120
155
  compute: MathPrimitives,
@@ -142,22 +177,22 @@ class ComputationExecutor {
142
177
  this.loader = dataLoader;
143
178
  }
144
179
 
145
- /**
146
- * UPDATED: Accepts yesterdayPortfolioData AND historyData separately.
147
- */
148
180
  async executePerUser(calcInstance, metadata, dateStr, portfolioData, yesterdayPortfolioData, historyData, computedDeps, prevDeps) {
149
181
  const { logger } = this.deps;
150
182
  const targetUserType = metadata.userType;
151
183
  const mappings = await this.loader.loadMappings();
152
184
  const insights = metadata.rootDataDependencies?.includes('insights') ? { today: await this.loader.loadInsights(dateStr) } : null;
185
+
153
186
  for (const [userId, todayPortfolio] of Object.entries(portfolioData)) {
154
187
  const yesterdayPortfolio = yesterdayPortfolioData ? yesterdayPortfolioData[userId] : null;
155
188
  const todayHistory = historyData ? historyData[userId] : null;
156
189
  const actualUserType = todayPortfolio.PublicPositions ? SCHEMAS.USER_TYPES.SPECULATOR : SCHEMAS.USER_TYPES.NORMAL;
190
+
157
191
  if (targetUserType !== 'all') {
158
192
  const mappedTarget = (targetUserType === 'speculator') ? SCHEMAS.USER_TYPES.SPECULATOR : SCHEMAS.USER_TYPES.NORMAL;
159
193
  if (mappedTarget !== actualUserType) continue;
160
194
  }
195
+
161
196
  const context = ContextBuilder.buildPerUserContext({
162
197
  todayPortfolio, yesterdayPortfolio,
163
198
  todayHistory,
@@ -166,6 +201,7 @@ class ComputationExecutor {
166
201
  previousComputedDependencies: prevDeps,
167
202
  config: this.config, deps: this.deps
168
203
  });
204
+
169
205
  try { await calcInstance.process(context); }
170
206
  catch (e) { logger.log('WARN', `Calc ${metadata.name} failed for user ${userId}: ${e.message}`); }
171
207
  }
@@ -173,10 +209,20 @@ class ComputationExecutor {
173
209
 
174
210
  async executeOncePerDay(calcInstance, metadata, dateStr, computedDeps, prevDeps) {
175
211
  const mappings = await this.loader.loadMappings();
212
+
213
+ // Load standard dependencies
176
214
  const insights = metadata.rootDataDependencies?.includes('insights') ? { today: await this.loader.loadInsights(dateStr) } : null;
177
215
  const social = metadata.rootDataDependencies?.includes('social') ? { today: await this.loader.loadSocial(dateStr) } : null;
216
+
217
+ // NEW: Load Price dependencies if required
218
+ let prices = null;
219
+ if (metadata.rootDataDependencies?.includes('price')) {
220
+ prices = await this.loader.loadPrices();
221
+ }
222
+
178
223
  const context = ContextBuilder.buildMetaContext({
179
224
  dateStr, metadata, mappings, insights, socialData: social,
225
+ prices, // Pass prices to builder
180
226
  computedDependencies: computedDeps,
181
227
  previousComputedDependencies: prevDeps,
182
228
  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: Explicit Logging + Honest Status Updates
3
4
  */
4
5
 
5
6
  const { ComputationController } = require('../controllers/computation_controller');
@@ -35,7 +36,6 @@ function checkRootDependencies(calcManifest, rootDataStatus) {
35
36
  * Checks for the availability of all required root data for a specific date.
36
37
  */
37
38
  async function checkRootDataAvailability(dateStr, config, dependencies, earliestDates) {
38
- // ... [Unchanged content of checkRootDataAvailability] ...
39
39
  const { logger } = dependencies;
40
40
  const dateToProcess = new Date(dateStr + 'T00:00:00Z');
41
41
  let portfolioRefs = [], historyRefs = [];
@@ -64,10 +64,6 @@ async function checkRootDataAvailability(dateStr, config, dependencies, earliest
64
64
  }
65
65
  }
66
66
 
67
- /**
68
- * --- DEPRECATED: Old per-date fetch ---
69
- * Keeps compatibility but logic moves to fetchGlobalComputationStatus
70
- */
71
67
  async function fetchComputationStatus(dateStr, config, { db }) {
72
68
  const collection = config.computationStatusCollection || 'computation_status';
73
69
  const docRef = db.collection(collection).doc(dateStr);
@@ -75,10 +71,6 @@ async function fetchComputationStatus(dateStr, config, { db }) {
75
71
  return snap.exists ? snap.data() : {};
76
72
  }
77
73
 
78
- /**
79
- * --- NEW: Fetches the SINGLE GLOBAL status document ---
80
- * Loads the entire history of statuses in one read.
81
- */
82
74
  async function fetchGlobalComputationStatus(config, { db }) {
83
75
  const collection = config.computationStatusCollection || 'computation_status';
84
76
  const docRef = db.collection(collection).doc('global_status');
@@ -86,9 +78,6 @@ async function fetchGlobalComputationStatus(config, { db }) {
86
78
  return snap.exists ? snap.data() : {};
87
79
  }
88
80
 
89
- /**
90
- * --- DEPRECATED: Old per-date update ---
91
- */
92
81
  async function updateComputationStatus(dateStr, updates, config, { db }) {
93
82
  if (!updates || Object.keys(updates).length === 0) return;
94
83
  const collection = config.computationStatusCollection || 'computation_status';
@@ -96,17 +85,11 @@ async function updateComputationStatus(dateStr, updates, config, { db }) {
96
85
  await docRef.set(updates, { merge: true });
97
86
  }
98
87
 
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
88
  async function updateGlobalComputationStatus(updatesByDate, config, { db }) {
105
89
  if (!updatesByDate || Object.keys(updatesByDate).length === 0) return;
106
90
  const collection = config.computationStatusCollection || 'computation_status';
107
91
  const docRef = db.collection(collection).doc('global_status');
108
92
 
109
- // Flatten to dot notation for Firestore update: "2023-10-27.calcName": true
110
93
  const flattenUpdates = {};
111
94
  for (const [date, statuses] of Object.entries(updatesByDate)) {
112
95
  for (const [calc, status] of Object.entries(statuses)) {
@@ -117,7 +100,6 @@ async function updateGlobalComputationStatus(updatesByDate, config, { db }) {
117
100
  try {
118
101
  await docRef.update(flattenUpdates);
119
102
  } catch (err) {
120
- // If doc doesn't exist (first run), update fails. Use set({merge:true}).
121
103
  if (err.code === 5) { // NOT_FOUND
122
104
  const deepObj = {};
123
105
  for (const [date, statuses] of Object.entries(updatesByDate)) {
@@ -130,10 +112,6 @@ async function updateGlobalComputationStatus(updatesByDate, config, { db }) {
130
112
  }
131
113
  }
132
114
 
133
- /**
134
- * --- UPDATED: fetchExistingResults ---
135
- * (Unchanged, keeps fetching results per date as this is heavy data)
136
- */
137
115
  async function fetchExistingResults(dateStr, calcsInPass, fullManifest, config, { db }, includeSelf = false) {
138
116
  const manifestMap = new Map(fullManifest.map(c => [normalizeName(c.name), c]));
139
117
  const calcsToFetch = new Set();
@@ -158,10 +136,6 @@ async function fetchExistingResults(dateStr, calcsInPass, fullManifest, config,
158
136
  return fetched;
159
137
  }
160
138
 
161
- /**
162
- * --- UPDATED: streamAndProcess ---
163
- * (Unchanged)
164
- */
165
139
  async function streamAndProcess(dateStr, state, passName, config, deps, rootData, portfolioRefs, historyRefs, fetchedDeps, previousFetchedDeps) {
166
140
  const { logger } = deps;
167
141
  const controller = new ComputationController(config, deps);
@@ -211,10 +185,6 @@ async function streamAndProcess(dateStr, state, passName, config, deps, rootData
211
185
  logger.log('INFO', `[${passName}] Streaming complete.`);
212
186
  }
213
187
 
214
- /**
215
- * --- UPDATED: runStandardComputationPass ---
216
- * Now accepts `skipStatusWrite` and returns `successUpdates`
217
- */
218
188
  async function runStandardComputationPass(date, calcs, passName, config, deps, rootData, fetchedDeps, previousFetchedDeps, skipStatusWrite = false) {
219
189
  const dStr = date.toISOString().slice(0, 10);
220
190
  const logger = deps.logger;
@@ -232,6 +202,9 @@ async function runStandardComputationPass(date, calcs, passName, config, deps, r
232
202
  const inst = new c.class();
233
203
  inst.manifest = c;
234
204
  state[normalizeName(c.name)] = inst;
205
+
206
+ // LOG: Explicitly say what calculation is being processed (Initialized)
207
+ logger.log('INFO', `${c.name} calculation running for ${dStr}`);
235
208
  }
236
209
  catch(e) {
237
210
  logger.log('WARN', `Failed to init ${c.name}`);
@@ -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);
@@ -255,6 +223,9 @@ async function runMetaComputationPass(date, calcs, passName, config, deps, fetch
255
223
 
256
224
  for (const mCalc of calcs) {
257
225
  try {
226
+ // LOG: Explicitly say what calculation is being processed
227
+ deps.logger.log('INFO', `${mCalc.name} calculation running for ${dStr}`);
228
+
258
229
  const inst = new mCalc.class();
259
230
  inst.manifest = mCalc;
260
231
  await controller.executor.executeOncePerDay(inst, mCalc, dStr, fetchedDeps, previousFetchedDeps);
@@ -267,7 +238,7 @@ async function runMetaComputationPass(date, calcs, passName, config, deps, fetch
267
238
 
268
239
  /**
269
240
  * --- UPDATED: commitResults ---
270
- * Added `skipStatusWrite` parameter. Returns `successUpdates`.
241
+ * Includes Explicit Result Logging and Honest Status Reporting.
271
242
  */
272
243
  async function commitResults(stateObj, dStr, passName, config, deps, skipStatusWrite = false) {
273
244
  const writes = [], schemas = [], sharded = {};
@@ -277,14 +248,29 @@ async function commitResults(stateObj, dStr, passName, config, deps, skipStatusW
277
248
  const calc = stateObj[name];
278
249
  try {
279
250
  const result = await calc.getResult();
280
- if (!result) continue;
251
+
252
+ // If null/undefined, log as Failed/Unknown immediately
253
+ if (!result) {
254
+ deps.logger.log('INFO', `${name} calculation for ${dStr} ran, result : Failed (Empty Result)`);
255
+ continue;
256
+ }
257
+
281
258
  const standardRes = {};
259
+ let hasData = false;
260
+
282
261
  for (const key in result) {
283
262
  if (key.startsWith('sharded_')) {
284
263
  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];
264
+ for (const c in sData) {
265
+ sharded[c] = sharded[c] || {};
266
+ Object.assign(sharded[c], sData[c]);
267
+ }
268
+ if (Object.keys(sData).length > 0) hasData = true;
269
+ } else {
270
+ standardRes[key] = result[key];
271
+ }
287
272
  }
273
+
288
274
  if (Object.keys(standardRes).length) {
289
275
  standardRes._completed = true;
290
276
  writes.push({
@@ -293,7 +279,9 @@ async function commitResults(stateObj, dStr, passName, config, deps, skipStatusW
293
279
  .collection(config.computationsSubcollection).doc(name),
294
280
  data: standardRes
295
281
  });
282
+ hasData = true;
296
283
  }
284
+
297
285
  if (calc.manifest.class.getSchema) {
298
286
  const { class: _cls, ...safeMetadata } = calc.manifest;
299
287
  schemas.push({
@@ -304,9 +292,19 @@ async function commitResults(stateObj, dStr, passName, config, deps, skipStatusW
304
292
  });
305
293
  }
306
294
 
307
- successUpdates[name] = true;
295
+ // --- EXPLICIT LOGGING & STATUS UPDATE ---
296
+ if (hasData) {
297
+ successUpdates[name] = true;
298
+ deps.logger.log('INFO', `${name} calculation for ${dStr} ran, result : Succeeded`);
299
+ } else {
300
+ // It ran without error, but produced no content (e.g. no data met criteria)
301
+ deps.logger.log('INFO', `${name} calculation for ${dStr} ran, result : Unknown (No Data Written)`);
302
+ }
308
303
 
309
- } catch (e) { deps.logger.log('ERROR', `Commit failed ${name}: ${e.message}`); }
304
+ } catch (e) {
305
+ deps.logger.log('ERROR', `Commit failed ${name}: ${e.message}`);
306
+ deps.logger.log('INFO', `${name} calculation for ${dStr} ran, result : Failed (Exception)`);
307
+ }
310
308
  }
311
309
 
312
310
  if (schemas.length) batchStoreSchemas(deps, config, schemas).catch(()=>{});
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bulltrackers-module",
3
- "version": "1.0.182",
3
+ "version": "1.0.184",
4
4
  "description": "Helper Functions for Bulltrackers.",
5
5
  "main": "index.js",
6
6
  "files": [