bulltrackers-module 1.0.733 → 1.0.734

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.
Files changed (56) hide show
  1. package/functions/computation-system-v2/README.md +152 -0
  2. package/functions/computation-system-v2/computations/PopularInvestorProfileMetrics.js +720 -0
  3. package/functions/computation-system-v2/computations/PopularInvestorRiskAssessment.js +176 -0
  4. package/functions/computation-system-v2/computations/PopularInvestorRiskMetrics.js +294 -0
  5. package/functions/computation-system-v2/computations/TestComputation.js +46 -0
  6. package/functions/computation-system-v2/computations/UserPortfolioSummary.js +172 -0
  7. package/functions/computation-system-v2/config/bulltrackers.config.js +317 -0
  8. package/functions/computation-system-v2/framework/core/Computation.js +73 -0
  9. package/functions/computation-system-v2/framework/core/Manifest.js +223 -0
  10. package/functions/computation-system-v2/framework/core/RuleInjector.js +53 -0
  11. package/functions/computation-system-v2/framework/core/Rules.js +231 -0
  12. package/functions/computation-system-v2/framework/core/RunAnalyzer.js +163 -0
  13. package/functions/computation-system-v2/framework/cost/CostTracker.js +154 -0
  14. package/functions/computation-system-v2/framework/data/DataFetcher.js +399 -0
  15. package/functions/computation-system-v2/framework/data/QueryBuilder.js +232 -0
  16. package/functions/computation-system-v2/framework/data/SchemaRegistry.js +287 -0
  17. package/functions/computation-system-v2/framework/execution/Orchestrator.js +498 -0
  18. package/functions/computation-system-v2/framework/execution/TaskRunner.js +35 -0
  19. package/functions/computation-system-v2/framework/execution/middleware/CostTrackerMiddleware.js +32 -0
  20. package/functions/computation-system-v2/framework/execution/middleware/LineageMiddleware.js +32 -0
  21. package/functions/computation-system-v2/framework/execution/middleware/Middleware.js +14 -0
  22. package/functions/computation-system-v2/framework/execution/middleware/ProfilerMiddleware.js +47 -0
  23. package/functions/computation-system-v2/framework/index.js +45 -0
  24. package/functions/computation-system-v2/framework/lineage/LineageTracker.js +147 -0
  25. package/functions/computation-system-v2/framework/monitoring/Profiler.js +80 -0
  26. package/functions/computation-system-v2/framework/resilience/Checkpointer.js +66 -0
  27. package/functions/computation-system-v2/framework/scheduling/ScheduleValidator.js +327 -0
  28. package/functions/computation-system-v2/framework/storage/StateRepository.js +286 -0
  29. package/functions/computation-system-v2/framework/storage/StorageManager.js +469 -0
  30. package/functions/computation-system-v2/framework/storage/index.js +9 -0
  31. package/functions/computation-system-v2/framework/testing/ComputationTester.js +86 -0
  32. package/functions/computation-system-v2/framework/utils/Graph.js +205 -0
  33. package/functions/computation-system-v2/handlers/dispatcher.js +109 -0
  34. package/functions/computation-system-v2/handlers/index.js +23 -0
  35. package/functions/computation-system-v2/handlers/onDemand.js +289 -0
  36. package/functions/computation-system-v2/handlers/scheduler.js +327 -0
  37. package/functions/computation-system-v2/index.js +163 -0
  38. package/functions/computation-system-v2/rules/index.js +49 -0
  39. package/functions/computation-system-v2/rules/instruments.js +465 -0
  40. package/functions/computation-system-v2/rules/metrics.js +304 -0
  41. package/functions/computation-system-v2/rules/portfolio.js +534 -0
  42. package/functions/computation-system-v2/rules/rankings.js +655 -0
  43. package/functions/computation-system-v2/rules/social.js +562 -0
  44. package/functions/computation-system-v2/rules/trades.js +545 -0
  45. package/functions/computation-system-v2/scripts/migrate-sectors.js +73 -0
  46. package/functions/computation-system-v2/test/test-dispatcher.js +317 -0
  47. package/functions/computation-system-v2/test/test-framework.js +500 -0
  48. package/functions/computation-system-v2/test/test-real-execution.js +166 -0
  49. package/functions/computation-system-v2/test/test-real-integration.js +194 -0
  50. package/functions/computation-system-v2/test/test-refactor-e2e.js +131 -0
  51. package/functions/computation-system-v2/test/test-results.json +31 -0
  52. package/functions/computation-system-v2/test/test-risk-metrics-computation.js +329 -0
  53. package/functions/computation-system-v2/test/test-scheduler.js +204 -0
  54. package/functions/computation-system-v2/test/test-storage.js +449 -0
  55. package/functions/orchestrator/index.js +18 -26
  56. package/package.json +3 -2
@@ -0,0 +1,231 @@
1
+ /**
2
+ * @fileoverview Business Rules Registry
3
+ *
4
+ * Manages business rules that are injected into computations:
5
+ * 1. Loads all rule modules from config
6
+ * 2. Hashes each rule for version tracking
7
+ * 3. Detects which rules each computation uses
8
+ * 4. Injects rules into computation context
9
+ *
10
+ * Design Philosophy:
11
+ * - Computations are "simple recipes" - they orchestrate, not calculate
12
+ * - Business logic (math, extraction, validation) lives in rules
13
+ * - Single point of reference for all shared functions
14
+ * - Rule changes automatically trigger re-runs of dependent computations
15
+ *
16
+ * Example Usage:
17
+ * ```javascript
18
+ * // In computation process():
19
+ * async process(context) {
20
+ * const { rules, data, entityId } = context;
21
+ *
22
+ * // Use injected rules - no require() needed
23
+ * const portfolio = rules.portfolio.extractPositions(data['portfolio_snapshots']);
24
+ * const sharpeRatio = rules.metrics.calculateSharpe(returns, riskFreeRate);
25
+ * const riskScore = rules.risk.calculateRiskScore(portfolio, volatility);
26
+ *
27
+ * this.setResult(entityId, { sharpeRatio, riskScore });
28
+ * }
29
+ * ```
30
+ */
31
+
32
+ const crypto = require('crypto');
33
+
34
+ /**
35
+ * @typedef {Object} RuleModule
36
+ * @property {string} name - Module name (e.g., 'portfolio', 'metrics')
37
+ * @property {Object} exports - The exported functions/objects
38
+ * @property {string} hash - Hash of the module for version tracking
39
+ * @property {Object} functionHashes - Hash of each exported function
40
+ */
41
+
42
+ class RulesRegistry {
43
+ /**
44
+ * @param {Object} config - Configuration containing rule modules
45
+ * @param {Object} [logger] - Logger instance
46
+ */
47
+ constructor(config, logger = null) {
48
+ this.config = config;
49
+ this.logger = logger;
50
+
51
+ // Storage
52
+ this.modules = new Map(); // name -> RuleModule
53
+ this.context = {}; // The injected context object
54
+ this.functionIndex = new Map(); // functionName -> { module, hash }
55
+
56
+ // Load rules from config
57
+ this._loadRules();
58
+ }
59
+
60
+ /**
61
+ * Get the rules context to inject into computations.
62
+ * @returns {Object} Rules context with all modules
63
+ */
64
+ getContext() {
65
+ return this.context;
66
+ }
67
+
68
+ /**
69
+ * Get all rule hashes for a computation based on which rules it uses.
70
+ * @param {string} codeString - Computation source code
71
+ * @returns {Object} { usedRules: { moduleName: [functionNames] }, hashes: { moduleName: hash } }
72
+ */
73
+ detectUsage(codeString) {
74
+ const usedRules = {};
75
+ const hashes = {};
76
+
77
+ for (const [moduleName, module] of this.modules) {
78
+ const usedFunctions = [];
79
+
80
+ // Check each exported function
81
+ for (const [funcName, funcHash] of Object.entries(module.functionHashes)) {
82
+ // Look for patterns like: rules.moduleName.funcName or just funcName
83
+ const patterns = [
84
+ new RegExp(`rules\\.${moduleName}\\.${funcName}\\b`), // rules.portfolio.extractPositions
85
+ new RegExp(`\\.${funcName}\\(`), // .extractPositions(
86
+ ];
87
+
88
+ for (const pattern of patterns) {
89
+ if (pattern.test(codeString)) {
90
+ if (!usedFunctions.includes(funcName)) {
91
+ usedFunctions.push(funcName);
92
+ }
93
+ break;
94
+ }
95
+ }
96
+ }
97
+
98
+ if (usedFunctions.length > 0) {
99
+ usedRules[moduleName] = usedFunctions;
100
+
101
+ // Combine hashes of used functions
102
+ const usedHashes = usedFunctions
103
+ .map(f => module.functionHashes[f])
104
+ .sort()
105
+ .join('|');
106
+ hashes[moduleName] = this._hash(usedHashes);
107
+ }
108
+ }
109
+
110
+ return { usedRules, hashes };
111
+ }
112
+
113
+ /**
114
+ * Get the combined hash of all rules (for safe mode).
115
+ * @returns {string} Combined hash
116
+ */
117
+ getAllHash() {
118
+ const allHashes = Array.from(this.modules.values())
119
+ .map(m => m.hash)
120
+ .sort()
121
+ .join('|');
122
+ return this._hash(allHashes);
123
+ }
124
+
125
+ /**
126
+ * Get module hash by name.
127
+ * @param {string} moduleName - Module name
128
+ * @returns {string|null} Module hash or null
129
+ */
130
+ getModuleHash(moduleName) {
131
+ return this.modules.get(moduleName)?.hash || null;
132
+ }
133
+
134
+ /**
135
+ * Get stats about loaded rules.
136
+ * @returns {Object} Stats
137
+ */
138
+ getStats() {
139
+ const stats = {
140
+ moduleCount: this.modules.size,
141
+ functionCount: this.functionIndex.size,
142
+ modules: {}
143
+ };
144
+
145
+ for (const [name, module] of this.modules) {
146
+ stats.modules[name] = {
147
+ functionCount: Object.keys(module.functionHashes).length,
148
+ hash: module.hash
149
+ };
150
+ }
151
+
152
+ return stats;
153
+ }
154
+
155
+ // =========================================================================
156
+ // PRIVATE METHODS
157
+ // =========================================================================
158
+
159
+ _loadRules() {
160
+ const rulesConfig = this.config.rules || {};
161
+
162
+ for (const [moduleName, moduleExports] of Object.entries(rulesConfig)) {
163
+ this._registerModule(moduleName, moduleExports);
164
+ }
165
+
166
+ this._log('INFO', `Loaded ${this.modules.size} rule modules with ${this.functionIndex.size} functions`);
167
+ }
168
+
169
+ _registerModule(name, exports) {
170
+ const functionHashes = {};
171
+ const cleanExports = {};
172
+
173
+ // Process each export
174
+ for (const [key, value] of Object.entries(exports)) {
175
+ if (typeof value === 'function') {
176
+ // Hash the function source
177
+ functionHashes[key] = this._hashFunction(value);
178
+ cleanExports[key] = value;
179
+
180
+ // Index for quick lookup
181
+ this.functionIndex.set(key, { module: name, hash: functionHashes[key] });
182
+ } else if (typeof value === 'object' && value !== null) {
183
+ // For objects (like constants or nested modules), hash the JSON
184
+ functionHashes[key] = this._hash(JSON.stringify(value));
185
+ cleanExports[key] = value;
186
+ this.functionIndex.set(key, { module: name, hash: functionHashes[key] });
187
+ }
188
+ }
189
+
190
+ // Create module hash from all function hashes
191
+ const moduleHash = this._hash(
192
+ Object.values(functionHashes).sort().join('|')
193
+ );
194
+
195
+ // Store module
196
+ this.modules.set(name, {
197
+ name,
198
+ exports: cleanExports,
199
+ hash: moduleHash,
200
+ functionHashes
201
+ });
202
+
203
+ // Add to context
204
+ this.context[name] = cleanExports;
205
+ }
206
+
207
+ _hashFunction(fn) {
208
+ const source = fn.toString();
209
+ // Strip comments and normalize whitespace for loose equality
210
+ const cleaned = source
211
+ .replace(/\/\/.*$/gm, '')
212
+ .replace(/\/\*[\s\S]*?\*\//g, '')
213
+ .replace(/\s+/g, ' ')
214
+ .trim();
215
+ return this._hash(cleaned);
216
+ }
217
+
218
+ _hash(str) {
219
+ return crypto.createHash('sha256').update(str).digest('hex').substring(0, 16);
220
+ }
221
+
222
+ _log(level, message) {
223
+ if (this.logger && typeof this.logger.log === 'function') {
224
+ this.logger.log(level, `[Rules] ${message}`);
225
+ } else {
226
+ console.log(`[Rules] ${message}`);
227
+ }
228
+ }
229
+ }
230
+
231
+ module.exports = { RulesRegistry };
@@ -0,0 +1,163 @@
1
+ /**
2
+ * @fileoverview Run Analyzer
3
+ * * Pure logic component that determines which computations need to run.
4
+ * Decouples decision-making from execution and storage.
5
+ */
6
+
7
+ class RunAnalyzer {
8
+ constructor(manifest, dataFetcher, logger = null) {
9
+ this.manifest = manifest;
10
+ this.dataFetcher = dataFetcher;
11
+ this.logger = logger || console;
12
+ this.manifestMap = new Map(manifest.map(e => [e.name, e]));
13
+ }
14
+
15
+ /**
16
+ * Analyze the system state to determine runnable computations.
17
+ * @param {string} dateStr - Target date
18
+ * @param {Map} dailyStatus - Current day's status map
19
+ * @param {Map} prevDayStatus - Previous day's status map
20
+ * @returns {Promise<Object>} The Analysis Report
21
+ */
22
+ async analyze(dateStr, dailyStatus, prevDayStatus) {
23
+ const report = {
24
+ runnable: [],
25
+ skipped: [],
26
+ blocked: [],
27
+ impossible: [],
28
+ reRuns: []
29
+ };
30
+
31
+ const isToday = dateStr === new Date().toISOString().slice(0, 10);
32
+
33
+ for (const entry of this.manifest) {
34
+ const decision = await this._evaluateEntry(
35
+ entry,
36
+ dateStr,
37
+ isToday,
38
+ dailyStatus,
39
+ prevDayStatus
40
+ );
41
+
42
+ report[decision.type].push(decision.payload);
43
+ }
44
+
45
+ // Merge reRuns into runnable for backward compatibility if needed by consumers
46
+ // But keeping them distinct in the report is better for logging
47
+ return report;
48
+ }
49
+
50
+ async _evaluateEntry(entry, dateStr, isToday, dailyStatus, prevDayStatus) {
51
+ const { name, requires, dependencies, isHistorical, isTest, schedule } = entry;
52
+ const stored = dailyStatus.get(name);
53
+
54
+ // 1. Schedule Check
55
+ if (!this._shouldRunOnDate(dateStr, schedule)) {
56
+ return { type: 'skipped', payload: { name, reason: `Not scheduled (${schedule})` } };
57
+ }
58
+
59
+ // 2. Data Availability Check
60
+ // Note: This is the only async IO part (calls DataFetcher)
61
+ const availability = await this.dataFetcher.checkAvailability(requires, dateStr);
62
+ if (!availability.canRun) {
63
+ if (!isToday) {
64
+ return { type: 'impossible', payload: { name, reason: `Missing data: ${availability.missing.join(', ')}` } };
65
+ } else {
66
+ return { type: 'blocked', payload: { name, reason: `Waiting for data: ${availability.missing.join(', ')}` } };
67
+ }
68
+ }
69
+
70
+ // 3. Force Test Re-run
71
+ if (isTest && isToday) {
72
+ return {
73
+ type: 'reRuns',
74
+ payload: {
75
+ name, pass: entry.pass, reason: 'Test computation (always runs today)',
76
+ oldHash: stored?.hash, newHash: entry.hash
77
+ }
78
+ };
79
+ }
80
+
81
+ // 4. Code Change Detection
82
+ let needsRun = false;
83
+ let runReason = '';
84
+
85
+ if (!stored) {
86
+ needsRun = true;
87
+ runReason = 'New computation';
88
+ } else if (stored.hash !== entry.hash) {
89
+ needsRun = true;
90
+ runReason = 'Code changed';
91
+ }
92
+
93
+ // 5. Dependency Checks
94
+ const missingDeps = [];
95
+ let hasDataDrift = false;
96
+
97
+ for (const dep of dependencies) {
98
+ const depEntry = this.manifestMap.get(dep);
99
+ const depStatus = dailyStatus.get(dep);
100
+
101
+ if (!depStatus) {
102
+ missingDeps.push(dep);
103
+ } else if (depEntry && depStatus.hash !== depEntry.hash) {
104
+ // Dependency is present but old (needs update first)
105
+ missingDeps.push(dep);
106
+ } else if (stored?.dependencyResultHashes) {
107
+ // Check for data drift
108
+ const lastSeenResultHash = stored.dependencyResultHashes[dep];
109
+ if (lastSeenResultHash && depStatus.resultHash !== lastSeenResultHash) {
110
+ hasDataDrift = true;
111
+ if (!needsRun) {
112
+ needsRun = true;
113
+ runReason = `Dependency data changed: ${dep}`;
114
+ }
115
+ }
116
+ }
117
+ }
118
+
119
+ if (missingDeps.length > 0) {
120
+ return { type: 'blocked', payload: { name, reason: `Waiting for: ${missingDeps.join(', ')}` } };
121
+ }
122
+
123
+ // 6. Historical Check
124
+ if (isHistorical && !needsRun) {
125
+ const prevStatus = prevDayStatus.get(name);
126
+ if (!prevStatus) {
127
+ needsRun = true;
128
+ runReason = 'Historical: no previous day result';
129
+ } else if (prevStatus.hash !== entry.hash) {
130
+ needsRun = true;
131
+ runReason = 'Historical: code version discontinuity';
132
+ }
133
+ }
134
+
135
+ // 7. Final Decision
136
+ if (needsRun) {
137
+ const type = stored ? 'reRuns' : 'runnable';
138
+ return {
139
+ type,
140
+ payload: {
141
+ name, pass: entry.pass, reason: runReason,
142
+ oldHash: stored?.hash, newHash: entry.hash
143
+ }
144
+ };
145
+ }
146
+
147
+ return { type: 'skipped', payload: { name, reason: 'Already up to date' } };
148
+ }
149
+
150
+ _shouldRunOnDate(dateStr, schedule) {
151
+ if (!schedule || schedule === 'daily') return true;
152
+
153
+ const date = new Date(dateStr + 'T00:00:00Z');
154
+ const dayOfWeek = date.getUTCDay(); // 0=Sun
155
+ const dayOfMonth = date.getUTCDate();
156
+
157
+ if (schedule === 'weekly') return dayOfWeek === 0;
158
+ if (schedule === 'monthly') return dayOfMonth === 1;
159
+ return true;
160
+ }
161
+ }
162
+
163
+ module.exports = { RunAnalyzer };
@@ -0,0 +1,154 @@
1
+ /**
2
+ * @fileoverview Cost Attribution Tracker for BigQuery
3
+ * * Tracks query costs per computation by monitoring bytes processed.
4
+ * * Manages its own storage table in BigQuery.
5
+ */
6
+
7
+ const { BigQuery } = require('@google-cloud/bigquery');
8
+
9
+ class CostTracker {
10
+ /**
11
+ * @param {Object} config - System configuration
12
+ * @param {Object} [logger] - Logger instance
13
+ */
14
+ constructor(config, logger = null) {
15
+ this.config = config;
16
+ this.logger = logger || console;
17
+
18
+ this.bigquery = new BigQuery({
19
+ projectId: config.bigquery.projectId,
20
+ location: config.bigquery.location || 'US'
21
+ });
22
+
23
+ this.datasetId = config.bigquery.dataset;
24
+ this.tableName = 'computation_costs';
25
+ this._tableChecked = false;
26
+ }
27
+
28
+ /**
29
+ * Record the cost of a computation execution.
30
+ * @param {string} computationName
31
+ * @param {string} dateStr - The execution business date
32
+ * @param {number} bytesProcessed - Total bytes processed by BigQuery
33
+ */
34
+ async trackCost(computationName, dateStr, bytesProcessed) {
35
+ // If no bytes processed (cached or no-op), don't record
36
+ if (!bytesProcessed || bytesProcessed <= 0) return;
37
+
38
+ try {
39
+ await this._ensureTable();
40
+
41
+ // Pricing: ~$5.00 per TB
42
+ const COST_PER_TB = 5.0;
43
+ const BYTES_PER_TB = 1099511627776; // 1024^4
44
+ const estimatedCost = (bytesProcessed / BYTES_PER_TB) * COST_PER_TB;
45
+
46
+ const row = {
47
+ date: dateStr,
48
+ computation_name: computationName,
49
+ bytes_processed: bytesProcessed,
50
+ estimated_cost_usd: parseFloat(estimatedCost.toFixed(6)), // Keep reasonable precision
51
+ timestamp: new Date().toISOString()
52
+ };
53
+
54
+ await this.bigquery
55
+ .dataset(this.datasetId)
56
+ .table(this.tableName)
57
+ .insert([row]);
58
+
59
+ this._log('DEBUG', `Recorded cost for ${computationName}: $${row.estimated_cost_usd} (${bytesProcessed} bytes)`);
60
+
61
+ } catch (e) {
62
+ // Swallow errors to prevent blocking the main execution pipeline
63
+ this._log('ERROR', `Failed to track cost for ${computationName}: ${e.message}`);
64
+ }
65
+ }
66
+
67
+ /**
68
+ * Generate a cost report for a date range.
69
+ * @param {string} startDate - YYYY-MM-DD
70
+ * @param {string} endDate - YYYY-MM-DD
71
+ */
72
+ async getCostReport(startDate, endDate) {
73
+ try {
74
+ const query = `
75
+ SELECT
76
+ computation_name,
77
+ SUM(bytes_processed) as total_bytes,
78
+ SUM(estimated_cost_usd) as total_cost,
79
+ COUNT(*) as execution_count,
80
+ AVG(estimated_cost_usd) as avg_cost_per_run
81
+ FROM \`${this.config.bigquery.projectId}.${this.datasetId}.${this.tableName}\`
82
+ WHERE date BETWEEN @startDate AND @endDate
83
+ GROUP BY computation_name
84
+ ORDER BY total_cost DESC
85
+ `;
86
+
87
+ const [rows] = await this.bigquery.query({
88
+ query,
89
+ params: { startDate, endDate },
90
+ location: this.config.bigquery.location
91
+ });
92
+
93
+ const totalCost = rows.reduce((sum, r) => sum + (r.total_cost || 0), 0);
94
+
95
+ return {
96
+ period: { start: startDate, end: endDate },
97
+ totalCost,
98
+ breakdown: rows.map(r => ({
99
+ computation: r.computation_name,
100
+ cost: r.total_cost,
101
+ runs: r.execution_count,
102
+ avgCost: r.avg_cost_per_run,
103
+ bytes: r.total_bytes
104
+ }))
105
+ };
106
+
107
+ } catch (e) {
108
+ this._log('ERROR', `Failed to generate cost report: ${e.message}`);
109
+ return { error: e.message };
110
+ }
111
+ }
112
+
113
+ async _ensureTable() {
114
+ if (this._tableChecked) return;
115
+
116
+ const dataset = this.bigquery.dataset(this.datasetId);
117
+ const table = dataset.table(this.tableName);
118
+
119
+ const [exists] = await table.exists();
120
+
121
+ if (!exists) {
122
+ this._log('INFO', `Creating cost tracking table: ${this.tableName}`);
123
+ await dataset.createTable(this.tableName, {
124
+ schema: [
125
+ { name: 'date', type: 'DATE', mode: 'REQUIRED' },
126
+ { name: 'computation_name', type: 'STRING', mode: 'REQUIRED' },
127
+ { name: 'bytes_processed', type: 'INTEGER', mode: 'REQUIRED' },
128
+ { name: 'estimated_cost_usd', type: 'FLOAT', mode: 'REQUIRED' },
129
+ { name: 'timestamp', type: 'TIMESTAMP', mode: 'REQUIRED' }
130
+ ],
131
+ timePartitioning: {
132
+ type: 'DAY',
133
+ field: 'date'
134
+ },
135
+ clustering: {
136
+ fields: ['computation_name']
137
+ }
138
+ });
139
+ }
140
+
141
+ this._tableChecked = true;
142
+ }
143
+
144
+ _log(level, message) {
145
+ const prefix = '[CostTracker]';
146
+ if (this.logger && typeof this.logger.log === 'function') {
147
+ this.logger.log(level, `${prefix} ${message}`);
148
+ } else {
149
+ console.log(`${level}: ${prefix} ${message}`);
150
+ }
151
+ }
152
+ }
153
+
154
+ module.exports = { CostTracker };