bulltrackers-module 1.0.765 → 1.0.768

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 (33) hide show
  1. package/functions/computation-system-v2/computations/BehavioralAnomaly.js +298 -186
  2. package/functions/computation-system-v2/computations/NewSectorExposure.js +82 -35
  3. package/functions/computation-system-v2/computations/NewSocialPost.js +52 -24
  4. package/functions/computation-system-v2/computations/PopularInvestorProfileMetrics.js +354 -641
  5. package/functions/computation-system-v2/config/bulltrackers.config.js +26 -14
  6. package/functions/computation-system-v2/framework/core/Manifest.js +9 -16
  7. package/functions/computation-system-v2/framework/core/RunAnalyzer.js +2 -1
  8. package/functions/computation-system-v2/framework/data/DataFetcher.js +142 -4
  9. package/functions/computation-system-v2/framework/execution/Orchestrator.js +119 -122
  10. package/functions/computation-system-v2/framework/storage/StorageManager.js +16 -18
  11. package/functions/computation-system-v2/framework/testing/ComputationTester.js +155 -66
  12. package/functions/computation-system-v2/handlers/scheduler.js +15 -5
  13. package/functions/computation-system-v2/scripts/test-computation-dag.js +109 -0
  14. package/functions/task-engine/helpers/data_storage_helpers.js +6 -6
  15. package/package.json +1 -1
  16. package/functions/computation-system-v2/computations/PopularInvestorRiskAssessment.js +0 -176
  17. package/functions/computation-system-v2/computations/PopularInvestorRiskMetrics.js +0 -294
  18. package/functions/computation-system-v2/computations/UserPortfolioSummary.js +0 -172
  19. package/functions/computation-system-v2/scripts/migrate-sectors.js +0 -73
  20. package/functions/computation-system-v2/test/analyze-results.js +0 -238
  21. package/functions/computation-system-v2/test/other/test-dependency-cascade.js +0 -150
  22. package/functions/computation-system-v2/test/other/test-dispatcher.js +0 -317
  23. package/functions/computation-system-v2/test/other/test-framework.js +0 -500
  24. package/functions/computation-system-v2/test/other/test-real-execution.js +0 -166
  25. package/functions/computation-system-v2/test/other/test-real-integration.js +0 -194
  26. package/functions/computation-system-v2/test/other/test-refactor-e2e.js +0 -131
  27. package/functions/computation-system-v2/test/other/test-results.json +0 -31
  28. package/functions/computation-system-v2/test/other/test-risk-metrics-computation.js +0 -329
  29. package/functions/computation-system-v2/test/other/test-scheduler.js +0 -204
  30. package/functions/computation-system-v2/test/other/test-storage.js +0 -449
  31. package/functions/computation-system-v2/test/run-pipeline-test.js +0 -554
  32. package/functions/computation-system-v2/test/test-full-pipeline.js +0 -227
  33. package/functions/computation-system-v2/test/test-worker-pool.js +0 -266
@@ -84,7 +84,7 @@ module.exports = {
84
84
  'portfolio_snapshots': {
85
85
  dateField: 'date',
86
86
  entityField: 'user_id',
87
- dataField: 'portfolio_data',
87
+ //dataField: 'portfolio_data',
88
88
  description: 'Daily portfolio snapshots for all users'
89
89
  },
90
90
 
@@ -92,7 +92,7 @@ module.exports = {
92
92
  'trade_history_snapshots': {
93
93
  dateField: 'date',
94
94
  entityField: 'user_id',
95
- dataField: 'history_data',
95
+ //dataField: 'history_data',
96
96
  description: 'Daily trade history snapshots'
97
97
  },
98
98
 
@@ -100,7 +100,7 @@ module.exports = {
100
100
  'social_post_snapshots': {
101
101
  dateField: 'date',
102
102
  entityField: 'user_id',
103
- dataField: 'posts_data',
103
+ //dataField: 'posts_data',
104
104
  description: 'Daily social post snapshots'
105
105
  },
106
106
 
@@ -108,7 +108,7 @@ module.exports = {
108
108
  'asset_prices': {
109
109
  dateField: 'date',
110
110
  entityField: 'instrument_id',
111
- dataField: null, // Flat table
111
+ //dataField: null, // Flat table
112
112
  description: 'Daily asset prices'
113
113
  },
114
114
 
@@ -116,7 +116,7 @@ module.exports = {
116
116
  'pi_rankings': {
117
117
  dateField: 'date',
118
118
  entityField: 'pi_id',
119
- dataField: 'rankings_data',
119
+ //dataField: 'rankings_data',
120
120
  description: 'Daily PI rankings snapshot'
121
121
  },
122
122
 
@@ -124,7 +124,7 @@ module.exports = {
124
124
  'pi_master_list': {
125
125
  dateField: null, // Not date-partitioned
126
126
  entityField: 'cid',
127
- dataField: null,
127
+ //dataField: null,
128
128
  description: 'Master list of all Popular Investors'
129
129
  },
130
130
 
@@ -132,7 +132,7 @@ module.exports = {
132
132
  'pi_ratings': {
133
133
  dateField: 'date',
134
134
  entityField: 'pi_id',
135
- dataField: null,
135
+ //dataField: null,
136
136
  description: 'Daily PI ratings'
137
137
  },
138
138
 
@@ -140,7 +140,7 @@ module.exports = {
140
140
  'pi_page_views': {
141
141
  dateField: 'date',
142
142
  entityField: 'pi_id',
143
- dataField: null,
143
+ //dataField: null,
144
144
  description: 'Daily PI page view metrics'
145
145
  },
146
146
 
@@ -148,7 +148,7 @@ module.exports = {
148
148
  'watchlist_membership': {
149
149
  dateField: 'date',
150
150
  entityField: 'pi_id',
151
- dataField: null,
151
+ //dataField: null,
152
152
  description: 'Daily watchlist membership counts'
153
153
  },
154
154
 
@@ -156,7 +156,7 @@ module.exports = {
156
156
  'pi_alert_history': {
157
157
  dateField: 'date',
158
158
  entityField: 'pi_id',
159
- dataField: 'metadata',
159
+ //dataField: 'metadata',
160
160
  description: 'Daily alert trigger history'
161
161
  },
162
162
 
@@ -164,7 +164,7 @@ module.exports = {
164
164
  'instrument_insights': {
165
165
  dateField: 'date',
166
166
  entityField: 'instrument_id',
167
- dataField: 'insights_data',
167
+ //dataField: 'insights_data',
168
168
  description: 'Daily instrument insights'
169
169
  },
170
170
 
@@ -172,7 +172,7 @@ module.exports = {
172
172
  'ticker_mappings': {
173
173
  dateField: null,
174
174
  entityField: 'instrument_id',
175
- dataField: null,
175
+ //dataField: null,
176
176
  description: 'Instrument ID to ticker symbol mappings'
177
177
  },
178
178
 
@@ -180,15 +180,27 @@ module.exports = {
180
180
  'computation_results': {
181
181
  dateField: 'date',
182
182
  entityField: null, // Keyed by computation_name
183
- dataField: 'result_data',
183
+ //dataField: 'result_data',
184
184
  description: 'Stored computation results'
185
185
  },
186
186
  // NEW: Sector Mappings Table
187
187
  'sector_mappings': {
188
188
  dateField: null, // Static data
189
189
  entityField: 'symbol', // Key the data by symbol for fast lookup
190
- dataField: null,
190
+ //dataField: null,
191
191
  description: 'Ticker to Sector mappings migrated from Firestore'
192
+ },
193
+
194
+ // NEW: Map the abstract requirement 'behavioral_features' to the actual BQ table
195
+ 'behavioral_features': {
196
+ tableName: 'daily_behavioral_features',
197
+ dateField: 'date',
198
+ entityField: 'user_id', // Important for per-entity fetching
199
+ schema: [
200
+ { name: 'user_id', type: 'STRING' },
201
+ { name: 'hhi_score', type: 'FLOAT' },
202
+ { name: 'martingale_events', type: 'INTEGER' }
203
+ ]
192
204
  }
193
205
  },
194
206
 
@@ -39,12 +39,9 @@ class ManifestBuilder {
39
39
  if (entry) {
40
40
  manifestMap.set(entry.name, entry);
41
41
 
42
- // CRITICAL FIX: Include conditional dependencies in the DAG for cycle detection and topological sort.
43
- // Even if the dependency is conditional at runtime, the execution order (Pass) must respect it.
44
42
  const graphDeps = [...entry.dependencies];
45
43
  if (entry.conditionalDependencies) {
46
44
  entry.conditionalDependencies.forEach(cd => {
47
- // Ensure we use the normalized name for the graph
48
45
  graphDeps.push(cd.computation);
49
46
  });
50
47
  }
@@ -60,7 +57,7 @@ class ManifestBuilder {
60
57
  throw new Error(`[Manifest] Circular dependency detected: ${cycle}`);
61
58
  }
62
59
 
63
- // 3. Topological Sort (calculates passes)
60
+ // 3. Topological Sort
64
61
  const sortedItems = Graph.topologicalSort(nodes, adjacency);
65
62
 
66
63
  // 4. Hydrate Sorted List
@@ -115,8 +112,6 @@ class ManifestBuilder {
115
112
  compositeHash += `|RULE:${mod}:${h}`;
116
113
  }
117
114
 
118
- // Normalize conditional dependencies if they exist
119
- // This ensures the Orchestrator can look them up by normalized name later
120
115
  const conditionalDependencies = (config.conditionalDependencies || []).map(cd => ({
121
116
  ...cd,
122
117
  computation: this._normalize(cd.computation)
@@ -128,16 +123,20 @@ class ManifestBuilder {
128
123
  class: ComputationClass,
129
124
  category: config.category || 'default',
130
125
  type: config.type || 'global',
126
+
127
+ outputTable: config.outputTable || null,
128
+ // --------------------------------------
129
+
131
130
  requires: config.requires || {},
132
131
  dependencies: (config.dependencies || []).map(d => this._normalize(d)),
133
- conditionalDependencies, // FIX: Pass this through to the manifest entry
132
+ conditionalDependencies,
134
133
  isHistorical: config.isHistorical || false,
135
134
  isTest: config.isTest || false,
136
135
  schedule: this.scheduleValidator.parseSchedule(config.schedule),
137
136
  storage: this._parseStorageConfig(config.storage),
138
137
  ttlDays: config.ttlDays,
139
- pass: 0, // Set later by Graph.js
140
- hash: this._hashCode(compositeHash), // Intrinsic hash
138
+ pass: 0,
139
+ hash: this._hashCode(compositeHash),
141
140
  weight: ComputationClass.getWeight ? ComputationClass.getWeight() : 1.0,
142
141
  composition: {
143
142
  epoch: this.epoch,
@@ -152,7 +151,6 @@ class ManifestBuilder {
152
151
  _computeFinalHashes(sorted, manifestMap) {
153
152
  for (const entry of sorted) {
154
153
  let hashInput = entry.hash;
155
- // Includes strict dependencies in the hash chain
156
154
  if (entry.dependencies.length > 0) {
157
155
  const depHashes = entry.dependencies.sort().map(d => {
158
156
  const h = manifestMap.get(d)?.hash;
@@ -161,10 +159,6 @@ class ManifestBuilder {
161
159
  });
162
160
  hashInput += `|DEPS:${depHashes.join('|')}`;
163
161
  }
164
- // Note: Conditional dependencies are currently excluded from the hash chain
165
- // because they might not be loaded. If strict versioning is required for them,
166
- // they should be added here too.
167
-
168
162
  entry.hash = this._hashCode(hashInput);
169
163
  }
170
164
  }
@@ -203,7 +197,7 @@ class ManifestBuilder {
203
197
  const used = {};
204
198
  for (const [name, exports] of Object.entries(this.sharedLayers)) {
205
199
  const found = Object.keys(exports).filter(exp =>
206
- code.includes(exp) // Simple include check, similar to original regex
200
+ code.includes(exp)
207
201
  );
208
202
  if (found.length) used[name] = found;
209
203
  }
@@ -240,7 +234,6 @@ class ManifestBuilder {
240
234
 
241
235
  _log(l, m) { this.logger ? this.logger.log(l, `[Manifest] ${m}`) : console.log(`[Manifest] ${m}`); }
242
236
 
243
- // Public alias for groupByPass matching the Interface
244
237
  groupByPass(m) { return this._groupByPass(m); }
245
238
  }
246
239
 
@@ -2,6 +2,7 @@
2
2
  * @fileoverview Run Analyzer
3
3
  * * Pure logic component that determines which computations need to run.
4
4
  * Decouples decision-making from execution and storage.
5
+ * * * UPDATE: Removed SQL bypass. All computations are now checked for data availability.
5
6
  */
6
7
 
7
8
  class RunAnalyzer {
@@ -57,7 +58,7 @@ class RunAnalyzer {
57
58
  }
58
59
 
59
60
  // 2. Data Availability Check
60
- // Note: This is the only async IO part (calls DataFetcher)
61
+ // UPDATE: Removed isSql check. All computations must have their raw data available.
61
62
  const availability = await this.dataFetcher.checkAvailability(requires, dateStr);
62
63
  if (!availability.canRun) {
63
64
  if (!isToday) {
@@ -9,9 +9,14 @@
9
9
  * * V2.4 FIX: Runaway Query Cost Prevention [Fix #3].
10
10
  * * V2.5 UPDATE: Super-Entity Monitoring [Safety Valve for Fix #6].
11
11
  * - Warns if a single entity exceeds reasonable batch limits (Memory Risk).
12
+ * * V2.6 UPDATE: Query Result Caching.
13
+ * - Implemented in-memory LRU cache to prevent redundant BigQuery costs for reference data.
14
+ * * V2.7 FIX: Double-Encoded JSON Normalization.
15
+ * - Automatically detects and recursively parses JSON strings (e.g. posts_data) to prevent downstream parsing errors.
12
16
  */
13
17
 
14
18
  const { BigQuery } = require('@google-cloud/bigquery');
19
+ const crypto = require('crypto');
15
20
 
16
21
  // FIX #3: Hard limit to prevent cost spirals
17
22
  const MAX_LOOKBACK_DAYS = 30;
@@ -29,11 +34,24 @@ class DataFetcher {
29
34
 
30
35
  this.client = new BigQuery({ projectId: this.projectId });
31
36
 
37
+ // Cache Configuration (V2.6)
38
+ this.cacheConfig = config.queryCache || {
39
+ enabled: true,
40
+ ttlMs: 300000, // 5 minutes default
41
+ maxSize: 1000 // Max unique queries to cache
42
+ };
43
+
44
+ // Use Map as LRU cache (insertion order preserved)
45
+ this.cache = new Map();
46
+
32
47
  this.stats = {
33
48
  queries: 0,
34
49
  rowsFetched: 0,
35
50
  errors: 0,
36
- bytesProcessed: 0
51
+ bytesProcessed: 0,
52
+ cacheHits: 0,
53
+ cacheMisses: 0,
54
+ cacheEvictions: 0
37
55
  };
38
56
  }
39
57
 
@@ -273,21 +291,74 @@ class DataFetcher {
273
291
 
274
292
  return { canRun: missing.length === 0, available, missing };
275
293
  }
294
+
276
295
 
277
296
  getStats() { return { ...this.stats }; }
278
- resetStats() { this.stats = { queries: 0, rowsFetched: 0, errors: 0, bytesProcessed: 0 }; }
297
+
298
+ resetStats() {
299
+ this.stats = {
300
+ queries: 0,
301
+ rowsFetched: 0,
302
+ errors: 0,
303
+ bytesProcessed: 0,
304
+ cacheHits: 0,
305
+ cacheMisses: 0,
306
+ cacheEvictions: 0
307
+ };
308
+ this.cache.clear();
309
+ }
310
+
311
+ clearCache() {
312
+ this.cache.clear();
313
+ this._log('DEBUG', 'Query cache cleared');
314
+ }
315
+
316
+ // =========================================================================
317
+ // PRIVATE METHODS
318
+ // =========================================================================
279
319
 
280
320
  async _execute(query) {
321
+ // V2.6: Query Caching
322
+ if (this.cacheConfig.enabled) {
323
+ const cacheKey = this._generateCacheKey(query);
324
+ const cached = this.cache.get(cacheKey);
325
+
326
+ if (cached) {
327
+ if (Date.now() - cached.timestamp < this.cacheConfig.ttlMs) {
328
+ this.stats.cacheHits++;
329
+ // Refresh LRU position (delete and re-set moves to end)
330
+ this.cache.delete(cacheKey);
331
+ this.cache.set(cacheKey, cached);
332
+ // Return cached rows immediately - no BigQuery cost
333
+ return cached.rows;
334
+ } else {
335
+ this.cache.delete(cacheKey); // Expired
336
+ }
337
+ }
338
+ this.stats.cacheMisses++;
339
+ }
340
+
281
341
  this.stats.queries++;
342
+
282
343
  try {
283
344
  const [job] = await this.client.createQueryJob({
284
345
  query: query.sql, params: query.params, location: this.location
285
346
  });
286
347
  const [rows] = await job.getQueryResults();
287
348
  const [metadata] = await job.getMetadata();
349
+
288
350
  this.stats.rowsFetched += rows.length;
289
351
  this.stats.bytesProcessed += parseInt(metadata.statistics?.totalBytesProcessed || 0, 10);
290
- return rows;
352
+
353
+ // FIX V2.7: Normalize Rows (Recursive JSON Parse) BEFORE caching
354
+ const normalizedRows = rows.map(r => this._normalizeRow(r));
355
+
356
+ // Store in cache if enabled
357
+ if (this.cacheConfig.enabled) {
358
+ this._addToCache(query, normalizedRows);
359
+ }
360
+
361
+ return normalizedRows;
291
362
  } catch (e) {
292
363
  this.stats.errors++;
293
364
  this._log('ERROR', `Query failed: ${e.message}`);
@@ -296,6 +367,8 @@ class DataFetcher {
296
367
  }
297
368
 
298
369
  async *_executeStream(query) {
370
+ // NOTE: We do NOT cache streams. They are typically massive datasets (batch processing)
371
+ // and caching them in memory would cause OOM.
299
372
  this.stats.queries++;
300
373
  try {
301
374
  const [job] = await this.client.createQueryJob({
@@ -304,7 +377,8 @@ class DataFetcher {
304
377
  const stream = job.getQueryResultsStream();
305
378
  for await (const row of stream) {
306
379
  this.stats.rowsFetched++;
307
- yield row;
380
+ // FIX V2.7: Normalize Rows (Recursive JSON Parse)
381
+ yield this._normalizeRow(row);
308
382
  }
309
383
  } catch (e) {
310
384
  this.stats.errors++;
@@ -312,6 +386,70 @@ class DataFetcher {
312
386
  throw e;
313
387
  }
314
388
  }
389
+
390
+ /**
391
+ * V2.8 FIX: JSON Detection Logic
392
+ */
393
+ _normalizeRow(row) {
394
+ const normalized = { ...row };
395
+ for (const [key, value] of Object.entries(normalized)) {
396
+ if (typeof value === 'string') {
397
+ const trimmed = value.trim();
398
+ // FIX: Check for " (Double Encoded JSON) in addition to { and [
399
+ if (trimmed.startsWith('{') || trimmed.startsWith('[') || trimmed.startsWith('"')) {
400
+ normalized[key] = this._safeRecursiveParse(value);
401
+ }
402
+ }
403
+ }
404
+ return normalized;
405
+ }
406
+
407
+ /**
408
+ * V2.7 FIX: Helper to safely recursively parse JSON.
409
+ * Handles: Double-Encoded JSON Strings (parsed recursively)
410
+ */
411
+ _safeRecursiveParse(input) {
412
+ if (!input) return null;
413
+ if (typeof input === 'object') return input;
414
+ try {
415
+ const parsed = JSON.parse(input);
416
+ // Recursion for double-encoded strings
417
+ if (typeof parsed === 'string') return this._safeRecursiveParse(parsed);
418
+ return parsed;
419
+ } catch (e) {
420
+ return input; // Not JSON, return original
421
+ }
422
+ }
423
+
424
+ /**
425
+ * V2.6: Generate a unique cache key for a query
426
+ */
427
+ _generateCacheKey(query) {
428
+ // Hash the SQL + Params to ensure uniqueness
429
+ const str = query.sql + JSON.stringify(query.params || {});
430
+ return crypto.createHash('md5').update(str).digest('hex');
431
+ }
432
+
433
+ /**
434
+ * V2.6: Add to cache with LRU eviction
435
+ */
436
+ _addToCache(query, rows) {
437
+ // Generate key
438
+ const key = this._generateCacheKey(query);
439
+
440
+ // Eviction Logic
441
+ if (this.cache.size >= this.cacheConfig.maxSize) {
442
+ // Map iterator yields in insertion order. First item is oldest.
443
+ const oldestKey = this.cache.keys().next().value;
444
+ this.cache.delete(oldestKey);
445
+ this.stats.cacheEvictions++;
446
+ }
447
+
448
+ this.cache.set(key, {
449
+ rows: rows,
450
+ timestamp: Date.now()
451
+ });
452
+ }
315
453
 
316
454
  /**
317
455
  * Transforms raw rows into a structured object.