bulltrackers-module 1.0.175 → 1.0.177

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.
@@ -147,37 +147,25 @@ class ComputationExecutor {
147
147
  */
148
148
  async executePerUser(calcInstance, metadata, dateStr, portfolioData, yesterdayPortfolioData, historyData, computedDeps, prevDeps) {
149
149
  const { logger } = this.deps;
150
-
151
- // Fix for the 'all' userType discrepancy:
152
- const targetUserType = metadata.userType; // 'all', 'normal', or 'speculator'
153
-
150
+ const targetUserType = metadata.userType;
154
151
  const mappings = await this.loader.loadMappings();
155
152
  const insights = metadata.rootDataDependencies?.includes('insights') ? { today: await this.loader.loadInsights(dateStr) } : null;
156
-
157
153
  for (const [userId, todayPortfolio] of Object.entries(portfolioData)) {
158
- // 1. Get Yesterday's Portfolio (if available)
159
154
  const yesterdayPortfolio = yesterdayPortfolioData ? yesterdayPortfolioData[userId] : null;
160
-
161
- // 2. Get Today's Trading History (if available)
162
155
  const todayHistory = historyData ? historyData[userId] : null;
163
-
164
156
  const actualUserType = todayPortfolio.PublicPositions ? SCHEMAS.USER_TYPES.SPECULATOR : SCHEMAS.USER_TYPES.NORMAL;
165
-
166
- // Filtering Logic
167
157
  if (targetUserType !== 'all') {
168
158
  const mappedTarget = (targetUserType === 'speculator') ? SCHEMAS.USER_TYPES.SPECULATOR : SCHEMAS.USER_TYPES.NORMAL;
169
159
  if (mappedTarget !== actualUserType) continue;
170
160
  }
171
-
172
161
  const context = ContextBuilder.buildPerUserContext({
173
162
  todayPortfolio, yesterdayPortfolio,
174
- todayHistory, // Passed to context
163
+ todayHistory,
175
164
  userId, userType: actualUserType, dateStr, metadata, mappings, insights,
176
165
  computedDependencies: computedDeps,
177
166
  previousComputedDependencies: prevDeps,
178
167
  config: this.config, deps: this.deps
179
168
  });
180
-
181
169
  try { await calcInstance.process(context); }
182
170
  catch (e) { logger.log('WARN', `Calc ${metadata.name} failed for user ${userId}: ${e.message}`); }
183
171
  }
@@ -187,14 +175,12 @@ class ComputationExecutor {
187
175
  const mappings = await this.loader.loadMappings();
188
176
  const insights = metadata.rootDataDependencies?.includes('insights') ? { today: await this.loader.loadInsights(dateStr) } : null;
189
177
  const social = metadata.rootDataDependencies?.includes('social') ? { today: await this.loader.loadSocial(dateStr) } : null;
190
-
191
178
  const context = ContextBuilder.buildMetaContext({
192
179
  dateStr, metadata, mappings, insights, socialData: social,
193
180
  computedDependencies: computedDeps,
194
181
  previousComputedDependencies: prevDeps,
195
182
  config: this.config, deps: this.deps
196
183
  });
197
-
198
184
  return await calcInstance.process(context);
199
185
  }
200
186
  }
@@ -11,17 +11,6 @@
11
11
  * It has removed all logic for deprecated folder structures (e.g., /historical/).
12
12
  */
13
13
 
14
- const fs = require('fs');
15
- //Hacky solution to force tmp writes TODO : This Tmp write is really dodgy, not ideal but works, consider less hacky solutions to writing to filesystem
16
- process.env.TMPDIR = '/tmp';
17
- process.env.TMP = '/tmp';
18
- process.env.TEMP = '/tmp';
19
- const os = require('os');
20
- os.tmpdir = () => '/tmp';
21
- try { const temp = require('temp'); const path = require('path'); const fs = require('fs'); const tmp = '/tmp';
22
- if (!fs.existsSync(tmp)) fs.mkdirSync(tmp); temp.dir = tmp; temp.path = () => path.join(tmp, 'temp-' + Math.random().toString(36).slice(2)); } catch {}
23
- const Viz = require('graphviz');
24
-
25
14
  /* --------------------------------------------------
26
15
  * Pretty Console Helpers
27
16
  * -------------------------------------------------- */
@@ -42,7 +31,7 @@ const log = {
42
31
  const normalizeName = (name) => { if (typeof name !== 'string') return name; return name.trim().replace(/,$/, '').replace(/_/g, '-').toLowerCase(); };
43
32
 
44
33
  /**
45
- * Finds the closest string matches for a typo.
34
+ * Finds the closest string matches for a typo. // https://medium.com/@ethannam/understanding-the-levenshtein-distance-equation-for-beginners-c4285a5604f0
46
35
  */
47
36
  function suggestClosest(name, candidates, n = 3) {
48
37
  const levenshtein = (a = '', b = '') => {
@@ -56,7 +45,7 @@ function suggestClosest(name, candidates, n = 3) {
56
45
  return dp[m][n];
57
46
  };
58
47
  const scores = candidates.map(c => [c, levenshtein(name, c)]);
59
- scores.sort((a, b) => a[1] - b[1]);
48
+ scores.sort((a, b) => a[1] - b[1]);
60
49
  return scores.slice(0, n).map(s => s[0]);
61
50
  }
62
51
 
@@ -125,7 +114,7 @@ function buildManifest(productLinesToRun = [], calculations) {
125
114
  const metadata = Class.getMetadata();
126
115
  const dependencies = Class.getDependencies().map(normalizeName);
127
116
  // --- RULE 4: Check for isHistorical mismatch ---
128
- if (metadata.isHistorical === true && !Class.toString().includes('yesterday')) { // UPDATED FOR MATH LAYER, THIS IS A LITTLE BRITTLE, BUT FOR NOW FINE...
117
+ if (metadata.isHistorical === true && !Class.toString().includes('yesterday')) { // UPDATED FOR MATH LAYER, TODO THIS IS A LITTLE BRITTLE, BUT FOR NOW FINE...
129
118
  log.warn(`Calculation "${normalizedName}" is marked 'isHistorical: true' but does not seem to reference 'yesterday' data.`);
130
119
  }
131
120
  const manifestEntry = {
@@ -144,8 +133,6 @@ function buildManifest(productLinesToRun = [], calculations) {
144
133
  inDegree.set(normalizedName, dependencies.length);
145
134
  dependencies.forEach(dep => { if (!reverseAdjacency.has(dep)) reverseAdjacency.set(dep, []); reverseAdjacency.get(dep).push(normalizedName); }); }
146
135
 
147
- // --- UPDATED ---
148
- // This 'calculations' object is now the one passed in as an argument.
149
136
  if (!calculations || typeof calculations !== 'object') {
150
137
  log.fatal('Calculations object was not provided or is invalid.');
151
138
  throw new Error('Manifest build failed: Invalid calculations object.');
@@ -185,14 +172,18 @@ function buildManifest(productLinesToRun = [], calculations) {
185
172
  /* ---------------- 3. Filter for Product Lines ---------------- */
186
173
  log.divider('Filtering by Product Line');
187
174
  // 1. Find all "endpoint" calculations (the final signals) in the target product lines.
175
+
188
176
  const productLineEndpoints = [];
189
177
  for (const [name, entry] of manifestMap.entries()) { if (productLinesToRun.includes(entry.category)) { productLineEndpoints.push(name); } }
178
+
190
179
  // 2. Add 'core' calculations as they are always included.
191
180
  for (const [name, entry] of manifestMap.entries()) { if (entry.category === 'core') { productLineEndpoints.push(name); } }
181
+
192
182
  // 3. Trace all dependencies upwards from these endpoints.
193
183
  const requiredCalcs = getDependencySet(productLineEndpoints, adjacency);
194
184
  log.info(`Identified ${productLineEndpoints.length} endpoint/core calculations.`);
195
185
  log.info(`Traced dependencies: ${requiredCalcs.size} total calculations are required.`);
186
+
196
187
  // 4. Create the final, filtered maps for sorting.
197
188
  const filteredManifestMap = new Map();
198
189
  const filteredInDegree = new Map();
@@ -206,17 +197,20 @@ function buildManifest(productLinesToRun = [], calculations) {
206
197
  const sortedManifest = [];
207
198
  const queue = [];
208
199
  let maxPass = 0;
200
+
209
201
  for (const [name, degree] of filteredInDegree) { if (degree === 0) { queue.push(name); filteredManifestMap.get(name).pass = 1; maxPass = 1; } }
210
202
  queue.sort();
211
203
  while (queue.length) {
212
204
  const currentName = queue.shift();
213
205
  const currentEntry = filteredManifestMap.get(currentName);
214
206
  sortedManifest.push(currentEntry);
207
+
215
208
  for (const neighborName of (filteredReverseAdjacency.get(currentName) || [])) { const newDegree = filteredInDegree.get(neighborName) - 1; filteredInDegree.set(neighborName, newDegree);
216
209
  const neighborEntry = filteredManifestMap.get(neighborName);
217
210
  if (neighborEntry.pass <= currentEntry.pass) { neighborEntry.pass = currentEntry.pass + 1; if (neighborEntry.pass > maxPass) maxPass = neighborEntry.pass; }
218
211
  if (newDegree === 0) { queue.push(neighborName); } }
219
212
  queue.sort(); }
213
+
220
214
  if (sortedManifest.length !== filteredManifestMap.size) {
221
215
  log.divider('Circular Dependency Detected');
222
216
  const cycles = findCycles(filteredManifestMap, adjacency);
@@ -233,37 +227,7 @@ function buildManifest(productLinesToRun = [], calculations) {
233
227
  return sortedManifest;
234
228
  }
235
229
 
236
- /**
237
- * Generates an SVG dependency graph for a given manifest.
238
- * @param {object[]} manifest - The sorted manifest array.
239
- * @param {string} filename - The output filename (e.g., "full-dependency-tree.svg").
240
- */
241
- async function generateSvgGraph(manifest, filename = 'dependency-tree.svg') {
242
- log.divider(`Generating SVG Graph: ${filename}`);
243
- const viz = new Viz({ worker: false });
244
- const categories = [...new Set(manifest.map(e => e.category))];
245
- const colors = ['#e6194b', '#3cb44b', '#ffe119', '#4363d8', '#f58231', '#911eb4', '#46f0f0', '#f032e6', '#bcf60c', '#fabebe', '#008080', '#e6beff', '#9a6324', '#fffac8', '#800000', '#aaffc3', '#808000', '#ffd8b1', '#000075', '#808080'];
246
- const colorMap = new Map(categories.map((cat, i) => [cat, colors[i % colors.length]]));
247
- colorMap.set('core', '#a9a9a9');
248
- let dot = `digraph Manifest {\n rankdir=LR;\n node [shape=box, style=filled, fontname="Helvetica"];\n layout=dot;\n overlap=false;\n splines=true;\n`;
249
- for (const category of categories) {
250
- if (category === 'core') continue;
251
- dot += ` subgraph "cluster_${category}" {\n label="${category.toUpperCase()} Product Line";\n style=filled;\n color="#f0f0f0";\n`;
252
- for (const entry of manifest.filter(e => e.category === category)) { dot += ` "${entry.name}" [label="${entry.name}\\n(Pass ${entry.pass})", fillcolor="${colorMap.get(category)}"];\n`; }
253
- dot += ` }\n`; }
254
- dot += ` subgraph "cluster_core" {\n label="CORE";\n style=filled;\n color="#e0e0e0";\n`;
255
- for (const entry of manifest.filter(e => e.category === 'core')) { dot += ` "${entry.name}" [label="${entry.name}\\n(Pass ${entry.pass})", fillcolor="${colorMap.get('core')}"];\n`; }
256
- dot += ` }\n`;
257
- for (const entry of manifest) { for (const dep of entry.dependencies || []) { dot += ` "${dep}" -> "${entry.name}";\n`; } }
258
- dot += '}\n';
259
- try {
260
- const svg = await viz.renderString(dot, { format: 'svg' });
261
- const out = path.join('/tmp', filename);
262
- fs.writeFileSync(out, svg);
263
- log.success(`Dependency tree generated at ${out}`);
264
230
 
265
- } catch (e) { log.error(`SVG generation failed: ${e.message}`); }
266
- }
267
231
 
268
232
 
269
233
  /**
@@ -282,4 +246,4 @@ function build(productLinesToRun, calculations) {
282
246
  }
283
247
 
284
248
 
285
- module.exports = { build, generateSvgGraph };
249
+ module.exports = { build};
@@ -1,17 +1,13 @@
1
1
  /**
2
- * FIXED: computation_pass_runner.js
3
- * V3.4: Optimized Orchestration using Status Document (Single Source of Truth).
4
- * - Reads one 'computation_status' doc per day to decide what to run.
5
- * - Reduces Firestore reads significantly.
6
- * - explicitly marks failures as false in the status doc.
2
+ * FILENAME: bulltrackers-module/functions/computation-system/helpers/computation_pass_runner.js
7
3
  */
8
4
 
9
5
  const {
10
6
  groupByPass,
11
7
  checkRootDataAvailability,
12
8
  fetchExistingResults,
13
- fetchComputationStatus,
14
- updateComputationStatus,
9
+ fetchGlobalComputationStatus, // <--- New Import
10
+ updateGlobalComputationStatus, // <--- New Import
15
11
  runStandardComputationPass,
16
12
  runMetaComputationPass,
17
13
  checkRootDependencies
@@ -25,113 +21,117 @@ async function runComputationPass(config, dependencies, computationManifest) {
25
21
  if (!passToRun)
26
22
  return logger.log('ERROR', '[PassRunner] No pass defined. Aborting.');
27
23
 
28
- logger.log('INFO', `🚀 Starting PASS ${passToRun} with Optimized Status Check...`);
24
+ logger.log('INFO', `🚀 Starting PASS ${passToRun} with Global Status Check...`);
29
25
 
30
- // Hardcoded earliest dates for global availability checks
31
- const earliestDates = { portfolio: new Date('2025-09-25T00:00:00Z'), history: new Date('2025-11-05T00:00:00Z'), social: new Date('2025-10-30T00:00:00Z'), insights: new Date('2025-08-26T00:00:00Z') };
26
+ // Hardcoded earliest dates
27
+ const earliestDates = { portfolio: new Date('2025-09-25T00:00:00Z'), history: new Date('2025-11-05T00:00:00Z'), social: new Date('2025-10-30T00:00:00Z'), insights: new Date('2025-08-26T00:00:00Z') };
32
28
  earliestDates.absoluteEarliest = Object.values(earliestDates).reduce((a,b) => a < b ? a : b);
33
29
 
34
- const passes = groupByPass(computationManifest);
30
+ const passes = groupByPass(computationManifest);
35
31
  const calcsInThisPass = passes[passToRun] || [];
36
32
 
37
33
  if (!calcsInThisPass.length)
38
34
  return logger.log('WARN', `[PassRunner] No calcs for Pass ${passToRun}. Exiting.`);
39
35
 
40
- // Determine date range to process
41
- // We no longer need complex per-calc date derivation since the status doc handles "done-ness".
42
- // We just iterate from absolute earliest to yesterday.
43
36
  const passEarliestDate = earliestDates.absoluteEarliest;
44
- const endDateUTC = new Date(Date.UTC(new Date().getUTCFullYear(), new Date().getUTCMonth(), new Date().getUTCDate() - 1));
37
+ const endDateUTC = new Date(Date.UTC(new Date().getUTCFullYear(), new Date().getUTCMonth(), new Date().getUTCDate() - 1));
45
38
  const allExpectedDates = getExpectedDateStrings(passEarliestDate, endDateUTC);
46
39
 
47
- const standardCalcs = calcsInThisPass.filter(c => c.type === 'standard');
48
- const metaCalcs = calcsInThisPass.filter(c => c.type === 'meta');
40
+ const standardCalcs = calcsInThisPass.filter(c => c.type === 'standard');
41
+ const metaCalcs = calcsInThisPass.filter(c => c.type === 'meta');
49
42
 
50
- // Helper: Decide if a calculation should run based on the status document
51
- const shouldRun = (calc, statusData) => {
52
- // 1. If explicitly TRUE, it ran fine. Ignore.
53
- if (statusData[calc.name] === true) return false;
43
+ // 1. Fetch Global Status ONCE (Memory Cache)
44
+ // Returns { "2023-10-27": { calcA: true, calcB: false }, ... }
45
+ const globalStatusData = await fetchGlobalComputationStatus(config, dependencies);
54
46
 
55
- // 2. If missing or FALSE, it needs to run.
56
- // But first, check if its dependencies are ready (if any).
57
- // Note: Dependencies from previous passes must be TRUE.
47
+ // Helper: Check status using in-memory data
48
+ const shouldRun = (calc, dateStr) => {
49
+ const dailyStatus = globalStatusData[dateStr] || {};
50
+
51
+ // 1. If explicitly TRUE, ignore.
52
+ if (dailyStatus[calc.name] === true) return false;
53
+
54
+ // 2. Check dependencies (using same in-memory status)
58
55
  if (calc.dependencies && calc.dependencies.length > 0) {
59
- const depsMet = calc.dependencies.every(depName => statusData[depName] === true);
60
- if (!depsMet) return false; // Dependency not ready yet
56
+ const depsMet = calc.dependencies.every(depName => dailyStatus[depName] === true);
57
+ if (!depsMet) return false;
61
58
  }
62
-
63
59
  return true;
64
60
  };
65
61
 
62
+ // Process a single date and RETURN updates (do not write)
66
63
  const processDate = async (dateStr) => {
67
- // 1. Fetch the Single Status Document (Cheap Read)
68
- const statusData = await fetchComputationStatus(dateStr, config, dependencies);
69
64
  const dateToProcess = new Date(dateStr + 'T00:00:00Z');
70
-
71
- // 2. Filter calculations based on Status Doc
72
- const standardToRun = standardCalcs.filter(c => shouldRun(c, statusData));
73
- const metaToRun = metaCalcs.filter(c => shouldRun(c, statusData));
65
+ const standardToRun = standardCalcs.filter(c => shouldRun(c, dateStr));
66
+ const metaToRun = metaCalcs.filter(c => shouldRun(c, dateStr));
74
67
 
75
- // Optimization: If nothing needs to run, stop here.
76
- // No checking root data, no loading results.
77
- if (!standardToRun.length && !metaToRun.length) {
78
- return; // logger.log('INFO', `[PassRunner] ${dateStr} complete or waiting for deps.`);
79
- }
68
+ if (!standardToRun.length && !metaToRun.length) return null; // No work
80
69
 
81
- // 3. Check Root Data Availability (Only done if we have work to do)
82
- // We filter standardToRun further based on root data requirements
83
- // (e.g. if calc needs history but history isn't ready for this date)
84
70
  const rootData = await checkRootDataAvailability(dateStr, config, dependencies, earliestDates);
85
-
86
- if (!rootData) {
87
- // If root data is completely missing for the day, we can't run anything.
88
- // We do NOT mark as false, because it's not a failure, just data unavailability.
89
- // We just skip.
90
- return;
91
- }
71
+ if (!rootData) return null;
92
72
 
93
- // Further filter based on specific root data needs (e.g. checks "hasHistory")
94
73
  const finalStandardToRun = standardToRun.filter(c => checkRootDependencies(c, rootData.status).canRun);
95
74
  const finalMetaToRun = metaToRun.filter(c => checkRootDependencies(c, rootData.status).canRun);
96
75
 
97
- if (!finalStandardToRun.length && !finalMetaToRun.length) return;
76
+ if (!finalStandardToRun.length && !finalMetaToRun.length) return null;
98
77
 
99
- logger.log('INFO', `[PassRunner] Running ${dateStr}: ${finalStandardToRun.length} standard, ${finalMetaToRun.length} meta`);
78
+ logger.log('INFO', `[PassRunner] Running ${dateStr}: ${finalStandardToRun.length} std, ${finalMetaToRun.length} meta`);
79
+
80
+ const dateUpdates = {}; // { calcName: true/false }
100
81
 
101
82
  try {
102
- // 4. Fetch Data for Execution (Dependencies)
103
- // We only fetch data required by the calcs that are actually running.
104
83
  const calcsRunning = [...finalStandardToRun, ...finalMetaToRun];
105
84
  const existingResults = await fetchExistingResults(dateStr, calcsRunning, computationManifest, config, dependencies, false);
106
-
107
- // Fetch Previous Day's Results (for State Persistence)
108
- const prevDate = new Date(dateToProcess); prevDate.setUTCDate(prevDate.getUTCDate() - 1);
109
- const prevDateStr = prevDate.toISOString().slice(0, 10);
85
+ const prevDate = new Date(dateToProcess); prevDate.setUTCDate(prevDate.getUTCDate() - 1);
86
+ const prevDateStr = prevDate.toISOString().slice(0, 10);
110
87
  const previousResults = await fetchExistingResults(prevDateStr, calcsRunning, computationManifest, config, dependencies, true);
111
-
112
- // 5. Execute
113
- if (finalStandardToRun.length)
114
- await runStandardComputationPass(dateToProcess, finalStandardToRun, `Pass ${passToRun} (Standard)`, config, dependencies, rootData, existingResults, previousResults);
115
-
116
- if (finalMetaToRun.length)
117
- await runMetaComputationPass(dateToProcess, finalMetaToRun, `Pass ${passToRun} (Meta)`, config, dependencies, existingResults, previousResults, rootData);
118
-
88
+ if (finalStandardToRun.length) {
89
+ const updates = await runStandardComputationPass(dateToProcess, finalStandardToRun, `Pass ${passToRun} (Std)`, config, dependencies, rootData, existingResults, previousResults, true); // skipStatusWrite=true
90
+ Object.assign(dateUpdates, updates);
91
+ }
92
+ if (finalMetaToRun.length) {
93
+ const updates = await runMetaComputationPass(dateToProcess, finalMetaToRun, `Pass ${passToRun} (Meta)`, config, dependencies, existingResults, previousResults, rootData, true); // skipStatusWrite=true
94
+ Object.assign(dateUpdates, updates);
95
+ }
119
96
  } catch (err) {
120
97
  logger.log('ERROR', `[PassRunner] FAILED Pass ${passToRun} for ${dateStr}`, { errorMessage: err.message });
121
-
122
- // 6. Explicitly Mark Failures as FALSE
123
- const failedUpdates = {};
124
- [...finalStandardToRun, ...finalMetaToRun].forEach(c => failedUpdates[c.name] = false);
125
- await updateComputationStatus(dateStr, failedUpdates, config, dependencies);
98
+ // Mark failures
99
+ [...finalStandardToRun, ...finalMetaToRun].forEach(c => dateUpdates[c.name] = false);
126
100
  }
101
+
102
+ // Return the updates for this date
103
+ return { date: dateStr, updates: dateUpdates };
127
104
  };
128
105
 
129
106
  // Batch process dates
130
107
  for (let i = 0; i < allExpectedDates.length; i += PARALLEL_BATCH_SIZE) {
131
108
  const batch = allExpectedDates.slice(i, i + PARALLEL_BATCH_SIZE);
132
- await Promise.all(batch.map(processDate));
109
+
110
+ // Run batch in parallel
111
+ const results = await Promise.all(batch.map(processDate));
112
+
113
+ // Aggregate updates from the batch
114
+ const batchUpdates = {};
115
+ let hasUpdates = false;
116
+
117
+ results.forEach(res => {
118
+ if (res && res.updates && Object.keys(res.updates).length > 0) {
119
+ batchUpdates[res.date] = res.updates;
120
+ hasUpdates = true;
121
+
122
+ // Also update our local in-memory copy so subsequent logic in this run sees it (though passes usually rely on prev days)
123
+ if (!globalStatusData[res.date]) globalStatusData[res.date] = {};
124
+ Object.assign(globalStatusData[res.date], res.updates);
125
+ }
126
+ });
127
+
128
+ // Write status ONCE per batch
129
+ if (hasUpdates) {
130
+ await updateGlobalComputationStatus(batchUpdates, config, dependencies);
131
+ logger.log('INFO', `[PassRunner] Batched status update for ${Object.keys(batchUpdates).length} dates.`);
132
+ }
133
133
  }
134
134
  logger.log('INFO', `[PassRunner] Pass ${passToRun} orchestration finished.`);
135
135
  }
136
136
 
137
- module.exports = { runComputationPass };
137
+ module.exports = { runComputationPass };