bulltrackers-module 1.0.158 → 1.0.160
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.
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview
|
|
3
|
+
* Dynamic Manifest Builder (v3 - Refactored)
|
|
4
|
+
*
|
|
5
|
+
* This script builds the computation manifest dynamically by introspecting all
|
|
6
|
+
* calculation classes from the new product-line structure (core, gem, gauss, etc.).
|
|
7
|
+
* It validates metadata, resolves dependencies, and performs a topological sort
|
|
8
|
+
* to determine execution passes.
|
|
9
|
+
*
|
|
10
|
+
* It explicitly skips the 'legacy' package.
|
|
11
|
+
* It has removed all logic for deprecated folder structures (e.g., /historical/).
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const { calculations } = require('aiden-shared-calculations-unified');
|
|
15
|
+
const fs = require('fs');
|
|
16
|
+
const path = require('path');
|
|
17
|
+
const Viz = require('viz.js');
|
|
18
|
+
const { Module, render } = require('viz.js/full.render.js');
|
|
19
|
+
|
|
20
|
+
/* --------------------------------------------------
|
|
21
|
+
* Pretty Console Helpers
|
|
22
|
+
* -------------------------------------------------- */
|
|
23
|
+
const log = {
|
|
24
|
+
info: (msg) => console.log('ℹ︎ ' + msg),
|
|
25
|
+
step: (msg) => console.log('› ' + msg),
|
|
26
|
+
warn: (msg) => console.warn('⚠︎ ' + msg),
|
|
27
|
+
success: (msg) => console.log('✔︎ ' + msg),
|
|
28
|
+
error: (msg) => console.error('✖ ' + msg),
|
|
29
|
+
fatal: (msg) => { console.error('✖ FATAL ✖ ' + msg); console.error('✖ FATAL ✖ Manifest build FAILED.'); },
|
|
30
|
+
divider: (label) => { const line = ''.padEnd(60, '─'); console.log(`\n${line}\n${label}\n${line}\n`); },
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/* --------------------------------------------------
|
|
34
|
+
* Helper Utilities
|
|
35
|
+
* -------------------------------------------------- */
|
|
36
|
+
|
|
37
|
+
const normalizeName = (name) => { if (typeof name !== 'string') return name; return name.trim().replace(/,$/, '').replace(/_/g, '-').toLowerCase(); };
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Finds the closest string matches for a typo.
|
|
41
|
+
*/
|
|
42
|
+
function suggestClosest(name, candidates, n = 3) {
|
|
43
|
+
const levenshtein = (a = '', b = '') => {
|
|
44
|
+
const m = a.length, n = b.length;
|
|
45
|
+
if (!m) return n; if (!n) return m;
|
|
46
|
+
const dp = Array.from({ length: m + 1 }, (_, i) => Array(n + 1).fill(i));
|
|
47
|
+
for (let j = 0; j <= n; j++) dp[0][j] = j;
|
|
48
|
+
for (let i = 1; i <= m; i++)
|
|
49
|
+
for (let j = 1; j <= n; j++)
|
|
50
|
+
dp[i][j] = a[i - 1] === b[j - 1] ? dp[i - 1][j - 1] : Math.min(dp[i - 1][j - 1], dp[i][j - 1], dp[i - 1][j]) + 1;
|
|
51
|
+
return dp[m][n];
|
|
52
|
+
};
|
|
53
|
+
const scores = candidates.map(c => [c, levenshtein(name, c)]);
|
|
54
|
+
scores.sort((a, b) => a[1] - b[1]);
|
|
55
|
+
return scores.slice(0, n).map(s => s[0]);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Checks for a cycle using Depth First Search.
|
|
60
|
+
*/
|
|
61
|
+
function findCycles(manifestMap, adjacencyList) {
|
|
62
|
+
const visited = new Set(), stack = new Set(), cycles = [];
|
|
63
|
+
const dfs = (node, path) => {
|
|
64
|
+
if (stack.has(node)) { const idx = path.indexOf(node); cycles.push([...path.slice(idx), node]); return; }
|
|
65
|
+
if (visited.has(node)) return;
|
|
66
|
+
visited.add(node); stack.add(node);
|
|
67
|
+
for (const nb of adjacencyList.get(node) || []) dfs(nb, [...path, nb]);
|
|
68
|
+
stack.delete(node); };
|
|
69
|
+
for (const name of manifestMap.keys()) dfs(name, [name]);
|
|
70
|
+
return cycles;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Recursively traces all dependencies for a given set of "endpoint" calcs.
|
|
75
|
+
*/
|
|
76
|
+
function getDependencySet(endpoints, adjacencyList) {
|
|
77
|
+
const required = new Set(endpoints);
|
|
78
|
+
const queue = [...endpoints];
|
|
79
|
+
while (queue.length > 0) { const calcName = queue.shift(); const dependencies = adjacencyList.get(calcName);
|
|
80
|
+
if (dependencies) { for (const dep of dependencies) { if (!required.has(dep)) { required.add(dep); queue.push(dep); } } } }
|
|
81
|
+
return required;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/* --------------------------------------------------
|
|
85
|
+
* Core Manifest Builder
|
|
86
|
+
* -------------------------------------------------- */
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Builds the full computation manifest.
|
|
90
|
+
* @param {string[]} productLinesToRun - Array of product line categories (folder names) to build for.
|
|
91
|
+
* @returns {object[]} The final sorted manifest array.
|
|
92
|
+
*/
|
|
93
|
+
function buildManifest(productLinesToRun = []) {
|
|
94
|
+
log.divider('Building Dynamic Manifest');
|
|
95
|
+
log.info(`Target Product Lines: [${productLinesToRun.join(', ')}]`);
|
|
96
|
+
const manifestMap = new Map();
|
|
97
|
+
const adjacency = new Map();
|
|
98
|
+
const reverseAdjacency = new Map();
|
|
99
|
+
const inDegree = new Map();
|
|
100
|
+
let hasFatalError = false;
|
|
101
|
+
/* ---------------- 1. Load All Calculations ---------------- */
|
|
102
|
+
log.step('Loading and validating all calculation classes…');
|
|
103
|
+
const allCalculationClasses = new Map();
|
|
104
|
+
/**
|
|
105
|
+
* Processes a single calculation class, validates it, and adds to maps.
|
|
106
|
+
* --- REFACTOR: Simplified signature, isHistorical is now read from metadata ---
|
|
107
|
+
*/
|
|
108
|
+
function processCalc(Class, name, category) {
|
|
109
|
+
if (!Class || typeof Class !== 'function') return;
|
|
110
|
+
const normalizedName = normalizeName(name);
|
|
111
|
+
allCalculationClasses.set(normalizedName, Class);
|
|
112
|
+
// --- RULE 1: Check for static getMetadata() ---
|
|
113
|
+
if (typeof Class.getMetadata !== 'function') { log.fatal(`Calculation "${normalizedName}" is missing the static getMetadata() method. Build FAILED.`); hasFatalError = true; return; }
|
|
114
|
+
// --- RULE 2: Check for static getDependencies() ---
|
|
115
|
+
if (typeof Class.getDependencies !== 'function') { log.fatal(`Calculation "${normalizedName}" is missing the static getDependencies() method. Build FAILED.`); hasFatalError = true;return; }
|
|
116
|
+
// --- RULE 3: Check for static getSchema() ---
|
|
117
|
+
if (typeof Class.getSchema !== 'function') {log.warn(`Calculation "${normalizedName}" is missing the static getSchema() method. (Recommended)`); }
|
|
118
|
+
const metadata = Class.getMetadata();
|
|
119
|
+
const dependencies = Class.getDependencies().map(normalizeName);
|
|
120
|
+
// --- RULE 4: Check for isHistorical mismatch ---
|
|
121
|
+
if (metadata.isHistorical === true && !Class.toString().includes('yesterdayPortfolio')) { log.warn(`Calculation "${normalizedName}" is marked 'isHistorical: true' but does not seem to use 'yesterdayPortfolio'.`); }
|
|
122
|
+
const manifestEntry = {
|
|
123
|
+
name: normalizedName,
|
|
124
|
+
class: Class,
|
|
125
|
+
category: metadata.category || category,
|
|
126
|
+
type: metadata.type,
|
|
127
|
+
isHistorical: metadata.isHistorical,
|
|
128
|
+
rootDataDependencies: metadata.rootDataDependencies || [],
|
|
129
|
+
userType: metadata.userType,
|
|
130
|
+
dependencies: dependencies,
|
|
131
|
+
pass: 0,
|
|
132
|
+
};
|
|
133
|
+
manifestMap.set(normalizedName, manifestEntry);
|
|
134
|
+
adjacency.set(normalizedName, dependencies);
|
|
135
|
+
inDegree.set(normalizedName, dependencies.length);
|
|
136
|
+
dependencies.forEach(dep => { if (!reverseAdjacency.has(dep)) reverseAdjacency.set(dep, []); reverseAdjacency.get(dep).push(normalizedName); }); }
|
|
137
|
+
for (const category in calculations) {
|
|
138
|
+
if (category === 'legacy') { log.info('Skipping "legacy" calculations package.'); continue; }
|
|
139
|
+
const group = calculations[category];
|
|
140
|
+
for (const key in group) {
|
|
141
|
+
const entry = group[key];
|
|
142
|
+
if (typeof entry === 'function') { processCalc(entry, key, category); } } }
|
|
143
|
+
|
|
144
|
+
if (hasFatalError) { throw new Error('Manifest build failed due to missing static methods in calculations.'); }
|
|
145
|
+
log.success(`Loaded and validated ${manifestMap.size} total calculations.`);
|
|
146
|
+
/* ---------------- 2. Validate Dependency Links ---------------- */
|
|
147
|
+
log.divider('Validating Dependency Links');
|
|
148
|
+
const allNames = new Set(manifestMap.keys());
|
|
149
|
+
let invalidLinks = false;
|
|
150
|
+
for (const [name, entry] of manifestMap) {
|
|
151
|
+
for (const dep of entry.dependencies) {
|
|
152
|
+
if (!allNames.has(dep)) {
|
|
153
|
+
invalidLinks = true;
|
|
154
|
+
const guesses = suggestClosest(dep, Array.from(allNames));
|
|
155
|
+
log.error(`${name} depends on unknown calculation "${dep}"`);
|
|
156
|
+
if (guesses.length) log.info(`Did you mean: ${guesses.join(', ')} ?`);
|
|
157
|
+
}
|
|
158
|
+
if (dep === name) {
|
|
159
|
+
invalidLinks = true;
|
|
160
|
+
log.error(`${name} has a circular dependency on *itself*!`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
if (invalidLinks) { throw new Error('Manifest validation failed. Fix missing or self-referencing dependencies.'); }
|
|
165
|
+
log.success('All dependency links are valid.');
|
|
166
|
+
/* ---------------- 3. Filter for Product Lines ---------------- */
|
|
167
|
+
log.divider('Filtering by Product Line');
|
|
168
|
+
// 1. Find all "endpoint" calculations (the final signals) in the target product lines.
|
|
169
|
+
const productLineEndpoints = [];
|
|
170
|
+
for (const [name, entry] of manifestMap.entries()) { if (productLinesToRun.includes(entry.category)) { productLineEndpoints.push(name); } }
|
|
171
|
+
// 2. Add 'core' calculations as they are always included.
|
|
172
|
+
for (const [name, entry] of manifestMap.entries()) { if (entry.category === 'core') { productLineEndpoints.push(name); } }
|
|
173
|
+
// 3. Trace all dependencies upwards from these endpoints.
|
|
174
|
+
const requiredCalcs = getDependencySet(productLineEndpoints, adjacency);
|
|
175
|
+
log.info(`Identified ${productLineEndpoints.length} endpoint/core calculations.`);
|
|
176
|
+
log.info(`Traced dependencies: ${requiredCalcs.size} total calculations are required.`);
|
|
177
|
+
// 4. Create the final, filtered maps for sorting.
|
|
178
|
+
const filteredManifestMap = new Map();
|
|
179
|
+
const filteredInDegree = new Map();
|
|
180
|
+
const filteredReverseAdjacency = new Map();
|
|
181
|
+
for (const name of requiredCalcs) { filteredManifestMap.set(name, manifestMap.get(name)); filteredInDegree.set(name, inDegree.get(name));
|
|
182
|
+
const consumers = (reverseAdjacency.get(name) || []).filter(consumer => requiredCalcs.has(consumer)); filteredReverseAdjacency.set(name, consumers); }
|
|
183
|
+
log.success(`Filtered manifest to ${filteredManifestMap.size} calculations.`);
|
|
184
|
+
/* ---------------- 4. Topological Sort (Kahn's Algorithm) ---------------- */
|
|
185
|
+
log.divider('Topological Sorting (Kahn\'s Algorithm)');
|
|
186
|
+
const sortedManifest = [];
|
|
187
|
+
const queue = [];
|
|
188
|
+
let maxPass = 0;
|
|
189
|
+
for (const [name, degree] of filteredInDegree) { if (degree === 0) { queue.push(name); filteredManifestMap.get(name).pass = 1; maxPass = 1; } }
|
|
190
|
+
queue.sort();
|
|
191
|
+
while (queue.length) {
|
|
192
|
+
const currentName = queue.shift();
|
|
193
|
+
const currentEntry = filteredManifestMap.get(currentName);
|
|
194
|
+
sortedManifest.push(currentEntry);
|
|
195
|
+
for (const neighborName of (filteredReverseAdjacency.get(currentName) || [])) { const newDegree = filteredInDegree.get(neighborName) - 1; filteredInDegree.set(neighborName, newDegree);
|
|
196
|
+
const neighborEntry = filteredManifestMap.get(neighborName);
|
|
197
|
+
if (neighborEntry.pass <= currentEntry.pass) { neighborEntry.pass = currentEntry.pass + 1; if (neighborEntry.pass > maxPass) maxPass = neighborEntry.pass; }
|
|
198
|
+
if (newDegree === 0) { queue.push(neighborName); } }
|
|
199
|
+
queue.sort(); }
|
|
200
|
+
if (sortedManifest.length !== filteredManifestMap.size) {
|
|
201
|
+
log.divider('Circular Dependency Detected');
|
|
202
|
+
const cycles = findCycles(filteredManifestMap, adjacency);
|
|
203
|
+
for (const c of cycles) log.error('Cycle: ' + c.join(' → '));
|
|
204
|
+
throw new Error('Circular dependency detected. Manifest build failed.'); }
|
|
205
|
+
/* ---------------- 5. Summary ---------------- */
|
|
206
|
+
log.divider('Manifest Summary');
|
|
207
|
+
log.success(`Total calculations in build: ${sortedManifest.length}`);
|
|
208
|
+
log.success(`Total passes required: ${maxPass}\n`);
|
|
209
|
+
const grouped = [...sortedManifest.reduce((m, e) => { if (!m.has(e.pass)) m.set(e.pass, []); m.get(e.pass).push(e); return m; }, new Map())].sort((a, b) => a[0] - b[0]);
|
|
210
|
+
for (const [pass, list] of grouped) { console.log(`Pass ${pass} (${list.length} calcs):`); console.log(' ' + list.map(l => l.name).join(', ')); }
|
|
211
|
+
log.divider('Build Complete');
|
|
212
|
+
return sortedManifest;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Generates an SVG dependency graph for a given manifest.
|
|
217
|
+
* @param {object[]} manifest - The sorted manifest array.
|
|
218
|
+
* @param {string} filename - The output filename (e.g., "full-dependency-tree.svg").
|
|
219
|
+
*/
|
|
220
|
+
async function generateSvgGraph(manifest, filename = 'dependency-tree.svg') {
|
|
221
|
+
log.divider(`Generating SVG Graph: ${filename}`);
|
|
222
|
+
const viz = new Viz({ Module, render });
|
|
223
|
+
const categories = [...new Set(manifest.map(e => e.category))];
|
|
224
|
+
const colors = ['#e6194b', '#3cb44b', '#ffe119', '#4363d8', '#f58231', '#911eb4', '#46f0f0', '#f032e6', '#bcf60c', '#fabebe', '#008080', '#e6beff', '#9a6324', '#fffac8', '#800000', '#aaffc3', '#808000', '#ffd8b1', '#000075', '#808080'];
|
|
225
|
+
const colorMap = new Map(categories.map((cat, i) => [cat, colors[i % colors.length]]));
|
|
226
|
+
colorMap.set('core', '#a9a9a9');
|
|
227
|
+
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`;
|
|
228
|
+
for (const category of categories) {
|
|
229
|
+
if (category === 'core') continue;
|
|
230
|
+
dot += ` subgraph "cluster_${category}" {\n label="${category.toUpperCase()} Product Line";\n style=filled;\n color="#f0f0f0";\n`;
|
|
231
|
+
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`; }
|
|
232
|
+
dot += ` }\n`; }
|
|
233
|
+
dot += ` subgraph "cluster_core" {\n label="CORE";\n style=filled;\n color="#e0e0e0";\n`;
|
|
234
|
+
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`; }
|
|
235
|
+
dot += ` }\n`;
|
|
236
|
+
for (const entry of manifest) { for (const dep of entry.dependencies || []) { dot += ` "${dep}" -> "${entry.name}";\n`; } }
|
|
237
|
+
dot += '}\n';
|
|
238
|
+
try {
|
|
239
|
+
const svg = await viz.renderString(dot, { format: 'svg' });
|
|
240
|
+
const out = path.join(__dirname, '..', '..', '..', '..', 'config', filename);
|
|
241
|
+
fs.writeFileSync(out, svg);
|
|
242
|
+
log.success(`Dependency tree generated at ${out}`);
|
|
243
|
+
} catch (e) { log.error(`SVG generation failed: ${e.message}`); }
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Main entry point for building and exporting the manifest.
|
|
249
|
+
* @param {string[]} productLinesToRun - Array of product line categories (folder names) to build for.
|
|
250
|
+
*/
|
|
251
|
+
async function build(productLinesToRun) {
|
|
252
|
+
try {
|
|
253
|
+
const manifest = buildManifest(productLinesToRun);
|
|
254
|
+
await generateSvgGraph(manifest, `dependency-tree-filtered.svg`);
|
|
255
|
+
const allProductLines = Object.keys(calculations).filter(c => c !== 'legacy');
|
|
256
|
+
const fullManifest = buildManifest(allProductLines);
|
|
257
|
+
await generateSvgGraph(fullManifest, 'dependency-tree-full.svg');
|
|
258
|
+
return manifest;
|
|
259
|
+
} catch (error) { log.error(error.message); return null; }
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
module.exports = { build };
|
|
263
|
+
|
|
264
|
+
if (require.main === module) { (async () => { log.info('Running manifest builder in local debug mode...'); const productLines = Object.keys(calculations).filter(c => c !== 'legacy');await build(productLines); })(); }
|
|
@@ -55,17 +55,12 @@ async function checkRootDataAvailability(dateStr, config, dependencies, earliest
|
|
|
55
55
|
*/
|
|
56
56
|
async function fetchExistingResults(dateStr, calcsInPass, fullManifest, config, { db, logger }) {
|
|
57
57
|
const manifestMap = new Map(fullManifest.map(c => [normalizeName(c.name), c]));
|
|
58
|
-
|
|
59
|
-
// --- FIX: Create a Set of *all* calcs we need to fetch ---
|
|
60
|
-
// This includes the calcs in this pass AND their dependencies.
|
|
61
58
|
const calcsToFetch = new Set();
|
|
62
59
|
|
|
63
60
|
for (const calc of calcsInPass) {
|
|
64
61
|
const calcName = normalizeName(calc.name);
|
|
65
|
-
// Add the calc itself (for skipping)
|
|
66
62
|
calcsToFetch.add(calcName);
|
|
67
63
|
|
|
68
|
-
// Add all its dependencies (for processing)
|
|
69
64
|
if (calc.dependencies && calc.dependencies.length > 0) {
|
|
70
65
|
for (const depName of calc.dependencies) {
|
|
71
66
|
calcsToFetch.add(normalizeName(depName));
|
|
@@ -104,7 +99,13 @@ async function fetchExistingResults(dateStr, calcsInPass, fullManifest, config,
|
|
|
104
99
|
const fetched = {};
|
|
105
100
|
if (docRefs.length) {
|
|
106
101
|
(await db.getAll(...docRefs)).forEach((doc, i) => {
|
|
107
|
-
|
|
102
|
+
// --- FIX [PROBLEM 8]: Add completion marker check ---
|
|
103
|
+
const data = doc.exists ? doc.data() : null;
|
|
104
|
+
if (data && data._completed === true) {
|
|
105
|
+
fetched[depNames[i]] = data;
|
|
106
|
+
} else {
|
|
107
|
+
fetched[depNames[i]] = null; // Treat as not existing if incomplete
|
|
108
|
+
}
|
|
108
109
|
});
|
|
109
110
|
}
|
|
110
111
|
|
|
@@ -144,19 +145,18 @@ function filterCalculations(standardCalcs, metaCalcs, rootDataStatus, existingRe
|
|
|
144
145
|
return earliestRunDate;
|
|
145
146
|
};
|
|
146
147
|
const filterCalc = (calc) => {
|
|
147
|
-
|
|
148
|
+
// --- FIX [PROBLEM 8]: The check for existingResults is now correct ---
|
|
149
|
+
if (existingResults[calc.name]) {logger.log('TRACE', `[Pass ${passToRun}] Skipping ${calc.name} for ${dateStr}. Result already exists (and is complete).`); skipped.add(calc.name); return false;}
|
|
150
|
+
|
|
148
151
|
const earliestRunDate = getTrueEarliestRunDate(calc);
|
|
149
152
|
if (dateToProcess < earliestRunDate) {logger.log('TRACE', `[Pass ${passToRun}] Skipping ${calc.name} for ${dateStr}. Date is before true earliest run date (${earliestRunDate.toISOString().slice(0, 10)}).`); skipped.add(calc.name); return false; }
|
|
150
153
|
const { canRun, missing: missingRoot } = checkRootDependencies(calc, rootDataStatus);
|
|
151
154
|
if (!canRun) {logger.log('INFO', `[Pass ${passToRun}] Skipping ${calc.name} for ${dateStr}. Data missing for this date: [${missingRoot.join(', ')}]`);skipped.add(calc.name); return false;}
|
|
152
155
|
|
|
153
|
-
// --- FIX: This check is now robust ---
|
|
154
|
-
// 'existingResults' now contains all dependencies, so this check
|
|
155
|
-
// will correctly find 'pnl_distribution_per_stock' and *not* skip.
|
|
156
156
|
if (calc.type === 'meta') {
|
|
157
157
|
const missingDeps = (calc.dependencies || [])
|
|
158
158
|
.map(normalizeName)
|
|
159
|
-
.filter(d => !existingResults[d]);
|
|
159
|
+
.filter(d => !existingResults[d]); // This check is now robust
|
|
160
160
|
if (missingDeps.length > 0) {
|
|
161
161
|
logger.log('WARN', `[Pass ${passToRun} Meta] Skipping ${calc.name} for ${dateStr}. Missing computed deps: [${missingDeps.join(', ')}]`);
|
|
162
162
|
skipped.add(calc.name);
|
|
@@ -182,15 +182,20 @@ async function loadHistoricalData(date, calcs, config, deps, rootData) {
|
|
|
182
182
|
const needsYesterdayInsights = calcs.some(c => c.isHistorical && c.rootDataDependencies.includes('insights'));
|
|
183
183
|
const needsYesterdaySocial = calcs.some(c => c.isHistorical && c.rootDataDependencies.includes('social'));
|
|
184
184
|
const needsYesterdayPortfolio = calcs.some(c => c.isHistorical && c.rootDataDependencies.includes('portfolio'));
|
|
185
|
+
|
|
186
|
+
// --- FIX: Add T-1 COMPUTED dependency loading ---
|
|
187
|
+
const needsYesterdayDependencies = calcs.some(c => c.isHistorical && c.dependencies && c.dependencies.length > 0);
|
|
188
|
+
|
|
185
189
|
const prev = new Date(date);
|
|
186
190
|
prev.setUTCDate(prev.getUTCDate() - 1);
|
|
187
191
|
const prevStr = prev.toISOString().slice(0, 10);
|
|
192
|
+
|
|
188
193
|
if(needsYesterdayInsights) {
|
|
189
|
-
tasks.push((async()=>{
|
|
194
|
+
tasks.push((async()=>{
|
|
190
195
|
logger.log('INFO', `[PassRunner] Loading YESTERDAY insights data for ${prevStr}`);
|
|
191
196
|
updated.yesterdayInsights=await loadDailyInsights(config,deps,prevStr); })());}
|
|
192
197
|
if(needsYesterdaySocial) {
|
|
193
|
-
tasks.push((async()=>{
|
|
198
|
+
tasks.push((async()=>{
|
|
194
199
|
logger.log('INFO', `[PassRunner] Loading YESTERDAY social data for ${prevStr}`);
|
|
195
200
|
updated.yesterdaySocialPostInsights=await loadDailySocialPostInsights(config,deps,prevStr); })());}
|
|
196
201
|
|
|
@@ -200,6 +205,17 @@ async function loadHistoricalData(date, calcs, config, deps, rootData) {
|
|
|
200
205
|
updated.yesterdayPortfolioRefs = await getPortfolioPartRefs(config, deps, prevStr);
|
|
201
206
|
})());
|
|
202
207
|
}
|
|
208
|
+
|
|
209
|
+
// --- FIX: Load T-1 COMPUTED dependencies ---
|
|
210
|
+
if(needsYesterdayDependencies) {
|
|
211
|
+
tasks.push((async()=>{
|
|
212
|
+
logger.log('INFO', `[PassRunner] Loading YESTERDAY computed dependencies for ${prevStr}`);
|
|
213
|
+
// This is a simplified fetch, assuming all calcs in this pass share the same T-1 deps
|
|
214
|
+
// A more robust solution would aggregate all unique T-1 deps.
|
|
215
|
+
updated.yesterdayDependencyData = await fetchExistingResults(prevStr, calcs, calcs.map(c => c.manifest), config, deps);
|
|
216
|
+
})());
|
|
217
|
+
}
|
|
218
|
+
|
|
203
219
|
await Promise.all(tasks);
|
|
204
220
|
return updated;
|
|
205
221
|
}
|
|
@@ -209,9 +225,12 @@ async function loadHistoricalData(date, calcs, config, deps, rootData) {
|
|
|
209
225
|
*/
|
|
210
226
|
async function streamAndProcess(dateStr, state, passName, config, deps, rootData, portfolioRefs, historyRefs) {
|
|
211
227
|
const { logger, calculationUtils } = deps;
|
|
212
|
-
const { todayInsights, yesterdayInsights, todaySocialPostInsights, yesterdaySocialPostInsights } = rootData;
|
|
228
|
+
const { todayInsights, yesterdayInsights, todaySocialPostInsights, yesterdaySocialPostInsights, yesterdayDependencyData } = rootData;
|
|
213
229
|
const calcsThatStreamPortfolio = Object.values(state).filter(calc => calc && calc.manifest && (calc.manifest.rootDataDependencies.includes('portfolio') || calc.manifest.category === 'speculators'));
|
|
214
|
-
|
|
230
|
+
|
|
231
|
+
// --- FIX: Add yesterday's computed data to context ---
|
|
232
|
+
const context={instrumentMappings:(await calculationUtils.loadInstrumentMappings()).instrumentToTicker, sectorMapping:(await calculationUtils.loadInstrumentMappings()).instrumentToSector, todayDateStr:dateStr, dependencies:deps, config, yesterdaysDependencyData: yesterdayDependencyData};
|
|
233
|
+
|
|
215
234
|
let firstUser=true;
|
|
216
235
|
for(const name in state){ const calc=state[name]; if(!calc||typeof calc.process!=='function') continue;
|
|
217
236
|
const cat=calc.manifest.category;
|
|
@@ -254,6 +273,11 @@ async function streamAndProcess(dateStr, state, passName, config, deps, rootData
|
|
|
254
273
|
firstUser=false;
|
|
255
274
|
if (pY) { delete yesterdayPortfolios[uid]; }
|
|
256
275
|
if (hT) { delete todayHistoryData[uid]; } } }
|
|
276
|
+
|
|
277
|
+
// --- FIX [PROBLEM 7]: Clear stale data to prevent memory leak ---
|
|
278
|
+
yesterdayPortfolios = {};
|
|
279
|
+
todayHistoryData = {};
|
|
280
|
+
|
|
257
281
|
logger.log('INFO', `[${passName}] Finished streaming data for ${dateStr}.`);
|
|
258
282
|
}
|
|
259
283
|
|
|
@@ -284,6 +308,8 @@ async function runStandardComputationPass(date, calcs, passName, config, deps, r
|
|
|
284
308
|
} else { standardResult[key] = result[key]; }}
|
|
285
309
|
if (Object.keys(standardResult).length > 0) {
|
|
286
310
|
const docRef = deps.db.collection(config.resultsCollection).doc(dStr) .collection(config.resultsSubcollection).doc(calc.manifest.category) .collection(config.computationsSubcollection).doc(name);
|
|
311
|
+
// --- FIX [PROBLEM 8]: Add completion marker ---
|
|
312
|
+
standardResult._completed = true;
|
|
287
313
|
standardWrites.push({ ref: docRef, data: standardResult });}
|
|
288
314
|
const calcClass = calc.manifest.class;
|
|
289
315
|
let staticSchema = null;
|
|
@@ -302,7 +328,10 @@ async function runStandardComputationPass(date, calcs, passName, config, deps, r
|
|
|
302
328
|
if (docPath.includes('/')) { docRef = deps.db.doc(docPath); } else {
|
|
303
329
|
const collection = (docPath.startsWith('user_profile_history')) ? config.shardedUserProfileCollection : config.shardedProfitabilityCollection;
|
|
304
330
|
docRef = deps.db.collection(collection).doc(docPath); }
|
|
305
|
-
if (docData && typeof docData === 'object' && !Array.isArray(docData)) {
|
|
331
|
+
if (docData && typeof docData === 'object' && !Array.isArray(docData)) {
|
|
332
|
+
// --- FIX [PROBLEM 8]: Add completion marker to sharded writes ---
|
|
333
|
+
docData._completed = true;
|
|
334
|
+
shardedDocWrites.push({ ref: docRef, data: docData });
|
|
306
335
|
} else { logger.log('ERROR', `[${passName}] Invalid sharded document data for ${docPath}. Not an object.`, { data: docData }); }
|
|
307
336
|
if (shardedDocWrites.length > 0) { await commitBatchInChunks(config, deps, shardedDocWrites, `${passName} Sharded ${docPath} ${dStr}`); } }
|
|
308
337
|
const logMetadata = {};
|
|
@@ -327,25 +356,8 @@ async function runMetaComputationPass(date, calcs, passName, config, deps, fetch
|
|
|
327
356
|
const name = normalizeName(mCalc.name), Cl = mCalc.class;
|
|
328
357
|
if (typeof Cl !== 'function') { logger.log('ERROR', `Invalid class ${name}`); failedCalcs.push(name); continue; }
|
|
329
358
|
const inst = new Cl();
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
// The original code assumed a 'meta' calc *only* has a .process() method.
|
|
333
|
-
// We now check for .process() first, and if it's not there,
|
|
334
|
-
// we fall back to calling .getResult() and pass it the dependencies.
|
|
335
|
-
//
|
|
336
|
-
// This file, however, expects `crowd_sharpe_ratio_proxy` to be
|
|
337
|
-
// refactored to use `process()`. See the update to that file.
|
|
338
|
-
// This `runMetaComputationPass` function remains as-is,
|
|
339
|
-
// as the fix is in refactoring the calculation file itself.
|
|
340
|
-
|
|
341
|
-
try {
|
|
342
|
-
|
|
343
|
-
// --- FIX: Refactored `crowd_sharpe_ratio_proxy` will now have this method ---
|
|
344
|
-
if (typeof inst.process !== 'function') {
|
|
345
|
-
logger.log('ERROR', `Meta-calc ${name} is missing a 'process' method.`);
|
|
346
|
-
failedCalcs.push(name);
|
|
347
|
-
continue;
|
|
348
|
-
}
|
|
359
|
+
|
|
360
|
+
try { if (typeof inst.process !== 'function') { logger.log('ERROR', `Meta-calc ${name} is missing a 'process' method.`); failedCalcs.push(name); continue; }
|
|
349
361
|
|
|
350
362
|
const result = await Promise.resolve(inst.process(dStr, { ...deps, rootData: fullRoot }, config, fetchedDeps));
|
|
351
363
|
|
|
@@ -358,6 +370,8 @@ async function runMetaComputationPass(date, calcs, passName, config, deps, fetch
|
|
|
358
370
|
}
|
|
359
371
|
if (Object.keys(standardResult).length > 0) {
|
|
360
372
|
const docRef = deps.db.collection(config.resultsCollection).doc(dStr) .collection(config.resultsSubcollection).doc(mCalc.category) .collection(config.computationsSubcollection).doc(name);
|
|
373
|
+
// --- FIX [PROBLEM 8]: Add completion marker ---
|
|
374
|
+
standardResult._completed = true;
|
|
361
375
|
standardWrites.push({ ref: docRef, data: standardResult });
|
|
362
376
|
}
|
|
363
377
|
const calcClass = mCalc.class;
|
|
@@ -375,7 +389,13 @@ async function runMetaComputationPass(date, calcs, passName, config, deps, fetch
|
|
|
375
389
|
for (const collectionName in shardedWrites) {
|
|
376
390
|
const docs = shardedWrites[collectionName];
|
|
377
391
|
const shardedDocWrites = [];
|
|
378
|
-
for (const docId in docs) {
|
|
392
|
+
for (const docId in docs) {
|
|
393
|
+
const docRef = docId.includes('/') ? deps.db.doc(docId) : deps.db.collection(collectionName).doc(docId);
|
|
394
|
+
// --- FIX [PROBLEM 8]: Add completion marker to sharded writes ---
|
|
395
|
+
const docData = docs[docId];
|
|
396
|
+
docData._completed = true;
|
|
397
|
+
shardedDocWrites.push({ ref: docRef, data: docData });
|
|
398
|
+
}
|
|
379
399
|
if (shardedDocWrites.length > 0) { await commitBatchInChunks(config, deps, shardedDocWrites, `${passName} Sharded ${collectionName} ${dStr}`); } }
|
|
380
400
|
const logMetadata = {};
|
|
381
401
|
if (failedCalcs.length > 0) { logMetadata.failedComputations = failedCalcs; }
|
package/index.js
CHANGED
|
@@ -29,10 +29,15 @@ const taskEngine = { handleRequest : require('./functions/task
|
|
|
29
29
|
handleVerify : require('./functions/task-engine/helpers/verify_helpers') .handleVerify ,
|
|
30
30
|
handleUpdate : require('./functions/task-engine/helpers/update_helpers') .handleUpdate };
|
|
31
31
|
|
|
32
|
+
// --- NEW IMPORT ---
|
|
33
|
+
const { build: buildManifestFunc } = require('./functions/computation-system/helpers/computation_manifest_builder');
|
|
34
|
+
|
|
32
35
|
// Computation System
|
|
33
36
|
const computationSystem = { runComputationPass : require('./functions/computation-system/helpers/computation_pass_runner') .runComputationPass,
|
|
34
37
|
dataLoader : require('./functions/computation-system/utils/data_loader'),
|
|
35
|
-
computationUtils : require('./functions/computation-system/utils/utils')
|
|
38
|
+
computationUtils : require('./functions/computation-system/utils/utils'),
|
|
39
|
+
buildManifest : buildManifestFunc // <--- NEW PIPE EXPORT
|
|
40
|
+
};
|
|
36
41
|
|
|
37
42
|
// API
|
|
38
43
|
const api = { createApiApp : require('./functions/generic-api/index') .createApiApp,
|
|
@@ -50,4 +55,4 @@ const maintenance = { runSpeculatorCleanup : require('./functions/spec
|
|
|
50
55
|
// Proxy
|
|
51
56
|
const proxy = { handlePost : require('./functions/appscript-api/index') .handlePost };
|
|
52
57
|
|
|
53
|
-
module.exports = { pipe: { core, orchestrator, dispatcher, taskEngine, computationSystem, api, maintenance, proxy } };
|
|
58
|
+
module.exports = { pipe: { core, orchestrator, dispatcher, taskEngine, computationSystem, api, maintenance, proxy } };
|