bulltrackers-module 1.0.768 → 1.0.769

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 (51) hide show
  1. package/functions/computation-system-v2/UserPortfolioMetrics.js +50 -0
  2. package/functions/computation-system-v2/computations/BehavioralAnomaly.js +557 -337
  3. package/functions/computation-system-v2/computations/GlobalAumPerAsset30D.js +103 -0
  4. package/functions/computation-system-v2/computations/PIDailyAssetAUM.js +134 -0
  5. package/functions/computation-system-v2/computations/PiFeatureVectors.js +227 -0
  6. package/functions/computation-system-v2/computations/PiRecommender.js +359 -0
  7. package/functions/computation-system-v2/computations/SignedInUserList.js +51 -0
  8. package/functions/computation-system-v2/computations/SignedInUserMirrorHistory.js +138 -0
  9. package/functions/computation-system-v2/computations/SignedInUserPIProfileMetrics.js +106 -0
  10. package/functions/computation-system-v2/computations/SignedInUserProfileMetrics.js +324 -0
  11. package/functions/computation-system-v2/config/bulltrackers.config.js +30 -128
  12. package/functions/computation-system-v2/core-api.js +17 -9
  13. package/functions/computation-system-v2/data_schema_reference.MD +108 -0
  14. package/functions/computation-system-v2/devtools/builder/builder.js +362 -0
  15. package/functions/computation-system-v2/devtools/builder/examples/user-metrics.yaml +26 -0
  16. package/functions/computation-system-v2/devtools/index.js +36 -0
  17. package/functions/computation-system-v2/devtools/shared/MockDataFactory.js +235 -0
  18. package/functions/computation-system-v2/devtools/shared/SchemaTemplates.js +475 -0
  19. package/functions/computation-system-v2/devtools/shared/SystemIntrospector.js +517 -0
  20. package/functions/computation-system-v2/devtools/shared/index.js +16 -0
  21. package/functions/computation-system-v2/devtools/simulation/DAGAnalyzer.js +243 -0
  22. package/functions/computation-system-v2/devtools/simulation/MockDataFetcher.js +306 -0
  23. package/functions/computation-system-v2/devtools/simulation/MockStorageManager.js +336 -0
  24. package/functions/computation-system-v2/devtools/simulation/SimulationEngine.js +525 -0
  25. package/functions/computation-system-v2/devtools/simulation/SimulationServer.js +581 -0
  26. package/functions/computation-system-v2/devtools/simulation/index.js +17 -0
  27. package/functions/computation-system-v2/devtools/simulation/simulate.js +324 -0
  28. package/functions/computation-system-v2/devtools/vscode-computation/package.json +90 -0
  29. package/functions/computation-system-v2/devtools/vscode-computation/snippets/computation.json +128 -0
  30. package/functions/computation-system-v2/devtools/vscode-computation/src/extension.ts +401 -0
  31. package/functions/computation-system-v2/devtools/vscode-computation/src/providers/codeActions.ts +152 -0
  32. package/functions/computation-system-v2/devtools/vscode-computation/src/providers/completions.ts +207 -0
  33. package/functions/computation-system-v2/devtools/vscode-computation/src/providers/diagnostics.ts +205 -0
  34. package/functions/computation-system-v2/devtools/vscode-computation/src/providers/hover.ts +205 -0
  35. package/functions/computation-system-v2/devtools/vscode-computation/tsconfig.json +22 -0
  36. package/functions/computation-system-v2/docs/HowToCreateComputations.MD +602 -0
  37. package/functions/computation-system-v2/framework/data/DataFetcher.js +250 -184
  38. package/functions/computation-system-v2/framework/data/MaterializedViewManager.js +84 -0
  39. package/functions/computation-system-v2/framework/data/QueryBuilder.js +38 -38
  40. package/functions/computation-system-v2/framework/execution/Orchestrator.js +215 -129
  41. package/functions/computation-system-v2/framework/scheduling/ScheduleValidator.js +17 -19
  42. package/functions/computation-system-v2/framework/storage/StateRepository.js +32 -2
  43. package/functions/computation-system-v2/framework/storage/StorageManager.js +105 -67
  44. package/functions/computation-system-v2/framework/testing/ComputationTester.js +12 -6
  45. package/functions/computation-system-v2/handlers/dispatcher.js +57 -29
  46. package/functions/computation-system-v2/legacy/PiAssetRecommender.js.old +115 -0
  47. package/functions/computation-system-v2/legacy/PiSimilarityMatrix.js +104 -0
  48. package/functions/computation-system-v2/legacy/PiSimilarityVector.js +71 -0
  49. package/functions/computation-system-v2/scripts/debug_aggregation.js +25 -0
  50. package/functions/computation-system-v2/scripts/test-invalidation-scenarios.js +234 -0
  51. package/package.json +1 -1
@@ -1,6 +1,10 @@
1
1
  /**
2
2
  * @fileoverview Computation Dispatcher
3
- * ...
3
+ * Handles incoming HTTP requests to run computations.
4
+ * Supports:
5
+ * 1. Standard execution (Scheduled/On-Demand)
6
+ * 2. Deployment Events (Triggers History Backfill Fan-Out)
7
+ * 3. Stale Task Protection (Prevents running old code versions)
4
8
  */
5
9
 
6
10
  const crypto = require('crypto');
@@ -9,6 +13,7 @@ exports.dispatcherHandler = async (req, res) => {
9
13
  const startTime = Date.now();
10
14
 
11
15
  try {
16
+ // Load the system entry point (index.js)
12
17
  const system = require('../index');
13
18
 
14
19
  const {
@@ -30,56 +35,84 @@ exports.dispatcherHandler = async (req, res) => {
30
35
  });
31
36
  }
32
37
 
33
- // [FIXED LOGIC HERE] --------------------------------------------------
34
- // Stale Task Protection
38
+ console.log(`[Dispatcher] Received ${source} request: ${computationName}`);
39
+
40
+ // Safety check to ensure system is loaded correctly
41
+ if (!system) {
42
+ throw new Error('System not fully initialized. Check index.js exports.');
43
+ }
44
+
45
+ // =====================================================================
46
+ // SPECIAL HANDLING: DEPLOYMENT EVENTS (Fan-Out)
47
+ // =====================================================================
48
+ if (source === 'deployment') {
49
+ if (typeof system.triggerBackfill !== 'function') {
50
+ throw new Error('System does not support auto-backfill (triggerBackfill missing).');
51
+ }
52
+
53
+ console.log(`[Dispatcher] 🚀 Triggering Deployment Backfill for ${computationName}...`);
54
+ const stats = await system.triggerBackfill(computationName);
55
+
56
+ const duration = Date.now() - startTime;
57
+ console.log(`[Dispatcher] Deployment processed in ${duration}ms. Scheduled ${stats.scheduled} tasks.`);
58
+
59
+ return res.status(200).json({
60
+ status: 'triggered',
61
+ computation: computationName,
62
+ action: 'backfill_fan_out',
63
+ scheduledTasks: stats.scheduled,
64
+ duration
65
+ });
66
+ }
67
+ // =====================================================================
68
+
69
+ // 2. Stale Task Protection
70
+ // Prevents execution if the task was scheduled with an older version of the configuration
35
71
  if (configHash && !force) {
36
- // FIX: Use getManifest() as system.manifest is not exposed directly.
37
72
  const manifest = await system.getManifest();
38
73
 
39
- // Normalize name to match manifest keys (matches logic in core-api.js)
74
+ // Normalize name to match manifest keys
40
75
  const normalizedName = computationName.toLowerCase().replace(/[^a-z0-9]/g, '');
41
76
  const entry = manifest.find(c => c.name === normalizedName);
42
77
 
43
78
  if (entry) {
44
- // 1. Re-calculate the hash of the CURRENTLY DEPLOYED code
79
+ // Re-calculate the hash of the CURRENTLY DEPLOYED code
45
80
  const input = JSON.stringify(entry.schedule) + `|PASS:${entry.pass}`;
46
81
  const currentHash = crypto.createHash('md5').update(input).digest('hex').substring(0, 8);
47
82
 
48
- // 2. Compare
83
+ // Compare Task Hash vs Current Hash
49
84
  if (configHash !== currentHash) {
50
85
  console.warn(`[Dispatcher] ♻️ Skipped STALE task for ${computationName}. (Task Hash: ${configHash} != Current: ${currentHash})`);
51
86
 
52
87
  return res.status(200).json({
53
88
  status: 'skipped',
54
89
  reason: 'STALE_CONFIG',
55
- message: 'Task configuration (schedule/pass) is obsolete relative to current deployment.',
90
+ message: 'Task configuration is obsolete relative to current deployment.',
56
91
  hash: currentHash
57
92
  });
58
93
  }
59
94
  }
60
95
  }
61
- // ---------------------------------------------------------------------
62
96
 
97
+ // 3. Prepare Execution
63
98
  const date = targetDate || new Date().toISOString().split('T')[0];
64
- console.log(`[Dispatcher] Received ${source} request: ${computationName} for ${date}`);
65
-
66
- // Safety check to ensure system is loaded correctly
67
- if (!system || typeof system.runComputation !== 'function') {
68
- throw new Error('System not fully initialized (runComputation is missing). Check index.js exports.');
69
- }
70
99
 
71
- // 2. DELEGATE TO ORCHESTRATOR
100
+ // 4. DELEGATE TO ORCHESTRATOR
101
+ if (typeof system.runComputation !== 'function') {
102
+ throw new Error('system.runComputation is missing.');
103
+ }
104
+
72
105
  const result = await system.runComputation({
73
106
  date,
74
107
  computation: computationName,
75
108
  entityIds,
76
109
  dryRun,
77
- force // Pass force down to orchestrator if needed
110
+ force
78
111
  });
79
112
 
80
113
  const duration = Date.now() - startTime;
81
114
 
82
- // 3. HANDLE SUCCESS (Completed or Skipped/Up-to-date)
115
+ // 5. HANDLE SUCCESS (Completed or Skipped/Up-to-date)
83
116
  if (result.status === 'completed' || result.status === 'skipped') {
84
117
  console.log(`[Dispatcher] ${computationName} ${result.status}: ${result.resultCount || 0} entities in ${duration}ms`);
85
118
 
@@ -94,7 +127,7 @@ exports.dispatcherHandler = async (req, res) => {
94
127
  });
95
128
  }
96
129
 
97
- // 4. HANDLE NON-RUNNABLE STATES (Blocked / Impossible)
130
+ // 6. HANDLE NON-RUNNABLE STATES (Blocked / Impossible)
98
131
  if (result.status === 'blocked' || result.status === 'impossible') {
99
132
  console.log(`[Dispatcher] ${computationName} ${result.status}: ${result.reason}`);
100
133
 
@@ -105,24 +138,19 @@ exports.dispatcherHandler = async (req, res) => {
105
138
  });
106
139
  }
107
140
 
108
- // 5. Fallback for other statuses
141
+ // 7. Fallback
109
142
  return res.status(200).json(result);
110
143
 
111
144
  } catch (error) {
112
145
  const duration = Date.now() - startTime;
113
146
  console.error(`[Dispatcher] Error after ${duration}ms:`, error);
114
147
 
115
- if (error.message && error.message.includes('Computation not found')) {
116
- return res.status(400).json({
117
- status: 'error',
118
- reason: 'UNKNOWN_COMPUTATION',
119
- message: error.message
120
- });
121
- }
148
+ const statusCode = (error.message && error.message.includes('Computation not found')) ? 400 : 500;
149
+ const reason = statusCode === 400 ? 'UNKNOWN_COMPUTATION' : 'EXECUTION_FAILED';
122
150
 
123
- return res.status(500).json({
151
+ return res.status(statusCode).json({
124
152
  status: 'error',
125
- reason: 'EXECUTION_FAILED',
153
+ reason: reason,
126
154
  message: error.message
127
155
  });
128
156
  }
@@ -0,0 +1,115 @@
1
+ const { Computation } = require('../framework');
2
+
3
+ class PiAssetRecommender extends Computation {
4
+ static getConfig() {
5
+ return {
6
+ name: 'PiAssetRecommender',
7
+ type: 'per-entity', // Recommendation is specific to THIS user
8
+ category: 'recommendations',
9
+
10
+ // 1. We rely on Global computations that pre-calculate the similarity graph
11
+ dependencies: ['PiSimilarityMatrix', 'PiTopHoldings'],
12
+
13
+ requires: {
14
+ 'portfolio_snapshots': {
15
+ lookback: 0,
16
+ mandatory: true,
17
+ fields: ['user_id', 'portfolio_data'] // Needed to get Mirrors
18
+ },
19
+ 'user_watchlists': { // Assuming table existence
20
+ lookback: 0,
21
+ mandatory: false,
22
+ fields: ['user_id', 'watched_pi_ids']
23
+ }
24
+ },
25
+
26
+ storage: {
27
+ bigquery: true,
28
+ firestore: { enabled: true, path: 'users/{entityId}/recommendations' }
29
+ }
30
+ };
31
+ }
32
+
33
+ async process(context) {
34
+ const { data, entityId, rules, getDependency } = context;
35
+
36
+ // 1. Get User's Signals (Mirrors + Watchlist)
37
+ const seedPIs = new Set();
38
+
39
+ // A. From Mirrors
40
+ const portfolioRow = data['portfolio_snapshots'];
41
+ if (portfolioRow) {
42
+ const pData = rules.portfolio.extractPortfolioData(portfolioRow);
43
+ const mirrors = rules.portfolio.extractMirrors(pData);
44
+ mirrors.forEach(m => {
45
+ if (m.ParentCID) seedPIs.add(String(m.ParentCID));
46
+ });
47
+ }
48
+
49
+ // B. From Watchlist
50
+ const watchlistRow = data['user_watchlists'];
51
+ if (watchlistRow && watchlistRow.watched_pi_ids) {
52
+ // Assuming array of IDs in column
53
+ const ids = Array.isArray(watchlistRow.watched_pi_ids)
54
+ ? watchlistRow.watched_pi_ids
55
+ : JSON.parse(watchlistRow.watched_pi_ids);
56
+ ids.forEach(id => seedPIs.add(String(id)));
57
+ }
58
+
59
+ if (seedPIs.size === 0) return; // No signals, no recommendations
60
+
61
+ // 2. Load Global Context (The Graph) via Dependencies
62
+ // These are large objects, so typically we fetch specific parts or the whole thing
63
+ // Assuming getDependency returns a Map of PI_ID -> Data
64
+ const similarityGraph = getDependency('PiSimilarityMatrix');
65
+ const piHoldings = getDependency('PiTopHoldings');
66
+
67
+ if (!similarityGraph || !piHoldings) return;
68
+
69
+ // 3. Find Similar PIs (Collaborative Filtering)
70
+ const candidateAssets = new Map(); // AssetID -> Score
71
+
72
+ seedPIs.forEach(seedId => {
73
+ // Find PIs similar to the ones I already copy/watch
74
+ const neighbors = similarityGraph[seedId] || []; // List of { id, score }
75
+
76
+ neighbors.forEach(neighbor => {
77
+ const neighborId = String(neighbor.id);
78
+ const similarityScore = neighbor.score;
79
+
80
+ // What does this neighbor hold?
81
+ const holdings = piHoldings[neighborId] || []; // List of { assetId, weight }
82
+
83
+ holdings.forEach(holding => {
84
+ const currentScore = candidateAssets.get(holding.assetId) || 0;
85
+ // Boost score based on PI similarity and holding weight
86
+ candidateAssets.set(holding.assetId, currentScore + (similarityScore * holding.weight));
87
+ });
88
+ });
89
+ });
90
+
91
+ // 4. Filter Assets User Already Owns
92
+ // (Re-use portfolio logic)
93
+ const myAssets = new Set();
94
+ if (portfolioRow) {
95
+ const pData = rules.portfolio.extractPortfolioData(portfolioRow);
96
+ const positions = rules.portfolio.extractPositions(pData);
97
+ positions.forEach(p => myAssets.add(String(rules.portfolio.getInstrumentId(p))));
98
+ }
99
+
100
+ // 5. Final Ranking
101
+ const recommendations = Array.from(candidateAssets.entries())
102
+ .filter(([assetId]) => !myAssets.has(String(assetId))) // Exclude owned
103
+ .sort((a, b) => b[1] - a[1]) // Sort by score DESC
104
+ .slice(0, 5) // Top 5
105
+ .map(([assetId, score]) => ({
106
+ assetId: Number(assetId),
107
+ score: Number(score.toFixed(4)),
108
+ reason: 'Held by investors similar to those you copy'
109
+ }));
110
+
111
+ this.setResult(entityId, { recommendations });
112
+ }
113
+ }
114
+
115
+ module.exports = PiAssetRecommender;
@@ -0,0 +1,104 @@
1
+ /**
2
+ * @fileoverview Similarity Matrix Meta-Computation.
3
+ * Calculates cosine similarity between all PI vectors to find nearest neighbors.
4
+ */
5
+ class PiSimilarityMatrix {
6
+ constructor() { this.results = {}; }
7
+
8
+ static getMetadata() {
9
+ return {
10
+ type: 'meta', // Runs once per date
11
+ category: 'popular_investor_meta',
12
+ userType: 'POPULAR_INVESTOR'
13
+ };
14
+ }
15
+
16
+ // DEPENDENCY: Forces this to run in Pass 2
17
+ static getDependencies() { return ['PiSimilarityVector']; }
18
+
19
+ static getSchema() {
20
+ return {
21
+ type: 'object', // Map<UserId, Array<SimilarUser>>
22
+ description: 'Top 5 similar users for each PI'
23
+ };
24
+ }
25
+
26
+ async process(context) {
27
+ const vectorMap = context.computed['PiSimilarityVector'];
28
+
29
+ // [FIX] Ensure we have enough peers to actually compare
30
+ if (!vectorMap || Object.keys(vectorMap).length < 2) {
31
+ this.results = {}; // Explicitly set empty
32
+ return this.results;
33
+ }
34
+
35
+ const userIds = Object.keys(vectorMap);
36
+ const matrix = {};
37
+
38
+ for (let i = 0; i < userIds.length; i++) {
39
+ const u1 = userIds[i];
40
+ const v1 = vectorMap[u1];
41
+
42
+ // Skip empty vectors
43
+ if (!v1 || Object.keys(v1).length === 0) continue;
44
+
45
+ const matches = [];
46
+
47
+ // Compare against every other user
48
+ for (let j = 0; j < userIds.length; j++) {
49
+ if (i === j) continue; // Don't compare self
50
+
51
+ const u2 = userIds[j];
52
+ const v2 = vectorMap[u2];
53
+
54
+ if (!v2 || Object.keys(v2).length === 0) continue;
55
+
56
+ // Calculate Cosine Similarity
57
+ let dotProduct = 0;
58
+ let mag1 = 0;
59
+ let mag2 = 0;
60
+
61
+ // v1 magnitude & dot product
62
+ for (const [k, val] of Object.entries(v1)) {
63
+ mag1 += (val * val);
64
+ if (v2[k]) dotProduct += (val * v2[k]);
65
+ }
66
+
67
+ // v2 magnitude
68
+ for (const val of Object.values(v2)) {
69
+ mag2 += (val * val);
70
+ }
71
+
72
+ mag1 = Math.sqrt(mag1);
73
+ mag2 = Math.sqrt(mag2);
74
+
75
+ if (mag1 > 0 && mag2 > 0) {
76
+ const similarity = dotProduct / (mag1 * mag2);
77
+ if (similarity > 0.1) { // Threshold to filter noise
78
+ matches.push({
79
+ id: u2,
80
+ score: Number(similarity.toFixed(4)),
81
+ reason: "Portfolio Composition Match"
82
+ });
83
+ }
84
+ }
85
+ }
86
+
87
+ // Sort by score desc and take top 5
88
+ matrix[u1] = matches.sort((a,b) => b.score - a.score).slice(0, 5);
89
+ }
90
+
91
+ // --- CRITICAL FIX ---
92
+ // 1. Assign to class property (for getResult)
93
+ this.results = matrix;
94
+
95
+ // 2. Return to MetaExecutor (for wrapping)
96
+ return this.results;
97
+ }
98
+
99
+ async getResult() {
100
+ return this.results;
101
+ }
102
+ }
103
+
104
+ module.exports = PiSimilarityMatrix;
@@ -0,0 +1,71 @@
1
+ /**
2
+ * @fileoverview Similarity Vector for Popular Investors.
3
+ * Generates a normalized asset allocation vector.
4
+ */
5
+
6
+ class PiSimilarityVector {
7
+ constructor() {
8
+ this.results = {};
9
+ }
10
+
11
+ static getMetadata() {
12
+ return {
13
+ type: 'standard', // MUST be 'standard'
14
+ category: 'popular_investor',
15
+ rootDataDependencies: ['portfolio'],
16
+ userType: 'POPULAR_INVESTOR'
17
+ };
18
+ }
19
+
20
+ static getDependencies() { return []; }
21
+
22
+ static getSchema() {
23
+ return {
24
+ type: 'object',
25
+ patternProperties: {
26
+ "^[0-9]+$": { type: 'number' } // InstrumentID -> Weight
27
+ },
28
+ description: 'Normalized Asset Allocation Vector'
29
+ };
30
+ }
31
+
32
+ process(context) {
33
+ const { DataExtractor } = context.math;
34
+ const userId = context.user.id;
35
+ const portfolio = context.user.portfolio.today;
36
+
37
+ if (!portfolio) {
38
+ this.results[userId] = {};
39
+ return;
40
+ }
41
+
42
+ const positions = DataExtractor.getPositions(portfolio, 'POPULAR_INVESTOR');
43
+ const vector = {};
44
+ let totalValue = 0;
45
+
46
+ for (const pos of positions) {
47
+ const id = DataExtractor.getInstrumentId(pos);
48
+ const weight = DataExtractor.getPositionWeight(pos);
49
+
50
+ if (id && weight > 0) {
51
+ vector[id] = (vector[id] || 0) + weight;
52
+ totalValue += weight;
53
+ }
54
+ }
55
+
56
+ // Normalize
57
+ if (totalValue > 0) {
58
+ for (const key in vector) {
59
+ vector[key] = Number((vector[key] / totalValue).toFixed(6));
60
+ }
61
+ }
62
+
63
+ this.results[userId] = vector;
64
+ }
65
+
66
+ async getResult() {
67
+ return this.results;
68
+ }
69
+ }
70
+
71
+ module.exports = PiSimilarityVector;
@@ -0,0 +1,25 @@
1
+ const { BigQuery } = require('@google-cloud/bigquery');
2
+ const config = require('../config/bulltrackers.config');
3
+
4
+ async function checkAggregation() {
5
+ const bq = new BigQuery({ projectId: config.bigquery.projectId });
6
+ const date = '2026-01-30';
7
+
8
+ console.log(`Checking Aggregation on ${date}`);
9
+
10
+ const query = `
11
+ SELECT user_type, COUNT(*) as count
12
+ FROM \`${config.bigquery.projectId}.${config.bigquery.dataset}.portfolio_snapshots\`
13
+ WHERE date = @date
14
+ GROUP BY 1
15
+ `;
16
+
17
+ const [rows] = await bq.query({
18
+ query,
19
+ params: { date }
20
+ });
21
+
22
+ console.log('Result:', rows);
23
+ }
24
+
25
+ checkAggregation().catch(console.error);