bulltrackers-module 1.0.766 → 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 (76) hide show
  1. package/functions/computation-system-v2/UserPortfolioMetrics.js +50 -0
  2. package/functions/computation-system-v2/computations/BehavioralAnomaly.js +559 -227
  3. package/functions/computation-system-v2/computations/GlobalAumPerAsset30D.js +103 -0
  4. package/functions/computation-system-v2/computations/NewSectorExposure.js +82 -35
  5. package/functions/computation-system-v2/computations/NewSocialPost.js +52 -24
  6. package/functions/computation-system-v2/computations/PIDailyAssetAUM.js +134 -0
  7. package/functions/computation-system-v2/computations/PiFeatureVectors.js +227 -0
  8. package/functions/computation-system-v2/computations/PiRecommender.js +359 -0
  9. package/functions/computation-system-v2/computations/PopularInvestorProfileMetrics.js +354 -641
  10. package/functions/computation-system-v2/computations/SignedInUserList.js +51 -0
  11. package/functions/computation-system-v2/computations/SignedInUserMirrorHistory.js +138 -0
  12. package/functions/computation-system-v2/computations/SignedInUserPIProfileMetrics.js +106 -0
  13. package/functions/computation-system-v2/computations/SignedInUserProfileMetrics.js +324 -0
  14. package/functions/computation-system-v2/config/bulltrackers.config.js +40 -126
  15. package/functions/computation-system-v2/core-api.js +17 -9
  16. package/functions/computation-system-v2/data_schema_reference.MD +108 -0
  17. package/functions/computation-system-v2/devtools/builder/builder.js +362 -0
  18. package/functions/computation-system-v2/devtools/builder/examples/user-metrics.yaml +26 -0
  19. package/functions/computation-system-v2/devtools/index.js +36 -0
  20. package/functions/computation-system-v2/devtools/shared/MockDataFactory.js +235 -0
  21. package/functions/computation-system-v2/devtools/shared/SchemaTemplates.js +475 -0
  22. package/functions/computation-system-v2/devtools/shared/SystemIntrospector.js +517 -0
  23. package/functions/computation-system-v2/devtools/shared/index.js +16 -0
  24. package/functions/computation-system-v2/devtools/simulation/DAGAnalyzer.js +243 -0
  25. package/functions/computation-system-v2/devtools/simulation/MockDataFetcher.js +306 -0
  26. package/functions/computation-system-v2/devtools/simulation/MockStorageManager.js +336 -0
  27. package/functions/computation-system-v2/devtools/simulation/SimulationEngine.js +525 -0
  28. package/functions/computation-system-v2/devtools/simulation/SimulationServer.js +581 -0
  29. package/functions/computation-system-v2/devtools/simulation/index.js +17 -0
  30. package/functions/computation-system-v2/devtools/simulation/simulate.js +324 -0
  31. package/functions/computation-system-v2/devtools/vscode-computation/package.json +90 -0
  32. package/functions/computation-system-v2/devtools/vscode-computation/snippets/computation.json +128 -0
  33. package/functions/computation-system-v2/devtools/vscode-computation/src/extension.ts +401 -0
  34. package/functions/computation-system-v2/devtools/vscode-computation/src/providers/codeActions.ts +152 -0
  35. package/functions/computation-system-v2/devtools/vscode-computation/src/providers/completions.ts +207 -0
  36. package/functions/computation-system-v2/devtools/vscode-computation/src/providers/diagnostics.ts +205 -0
  37. package/functions/computation-system-v2/devtools/vscode-computation/src/providers/hover.ts +205 -0
  38. package/functions/computation-system-v2/devtools/vscode-computation/tsconfig.json +22 -0
  39. package/functions/computation-system-v2/docs/HowToCreateComputations.MD +602 -0
  40. package/functions/computation-system-v2/framework/core/Manifest.js +9 -16
  41. package/functions/computation-system-v2/framework/core/RunAnalyzer.js +2 -1
  42. package/functions/computation-system-v2/framework/data/DataFetcher.js +330 -126
  43. package/functions/computation-system-v2/framework/data/MaterializedViewManager.js +84 -0
  44. package/functions/computation-system-v2/framework/data/QueryBuilder.js +38 -38
  45. package/functions/computation-system-v2/framework/execution/Orchestrator.js +226 -153
  46. package/functions/computation-system-v2/framework/scheduling/ScheduleValidator.js +17 -19
  47. package/functions/computation-system-v2/framework/storage/StateRepository.js +32 -2
  48. package/functions/computation-system-v2/framework/storage/StorageManager.js +111 -83
  49. package/functions/computation-system-v2/framework/testing/ComputationTester.js +161 -66
  50. package/functions/computation-system-v2/handlers/dispatcher.js +57 -29
  51. package/functions/computation-system-v2/legacy/PiAssetRecommender.js.old +115 -0
  52. package/functions/computation-system-v2/legacy/PiSimilarityMatrix.js +104 -0
  53. package/functions/computation-system-v2/legacy/PiSimilarityVector.js +71 -0
  54. package/functions/computation-system-v2/scripts/debug_aggregation.js +25 -0
  55. package/functions/computation-system-v2/scripts/test-computation-dag.js +109 -0
  56. package/functions/computation-system-v2/scripts/test-invalidation-scenarios.js +234 -0
  57. package/functions/task-engine/helpers/data_storage_helpers.js +6 -6
  58. package/package.json +1 -1
  59. package/functions/computation-system-v2/computations/PopularInvestorRiskAssessment.js +0 -176
  60. package/functions/computation-system-v2/computations/PopularInvestorRiskMetrics.js +0 -294
  61. package/functions/computation-system-v2/computations/UserPortfolioSummary.js +0 -172
  62. package/functions/computation-system-v2/scripts/migrate-sectors.js +0 -73
  63. package/functions/computation-system-v2/test/analyze-results.js +0 -238
  64. package/functions/computation-system-v2/test/other/test-dependency-cascade.js +0 -150
  65. package/functions/computation-system-v2/test/other/test-dispatcher.js +0 -317
  66. package/functions/computation-system-v2/test/other/test-framework.js +0 -500
  67. package/functions/computation-system-v2/test/other/test-real-execution.js +0 -166
  68. package/functions/computation-system-v2/test/other/test-real-integration.js +0 -194
  69. package/functions/computation-system-v2/test/other/test-refactor-e2e.js +0 -131
  70. package/functions/computation-system-v2/test/other/test-results.json +0 -31
  71. package/functions/computation-system-v2/test/other/test-risk-metrics-computation.js +0 -329
  72. package/functions/computation-system-v2/test/other/test-scheduler.js +0 -204
  73. package/functions/computation-system-v2/test/other/test-storage.js +0 -449
  74. package/functions/computation-system-v2/test/run-pipeline-test.js +0 -554
  75. package/functions/computation-system-v2/test/test-full-pipeline.js +0 -227
  76. package/functions/computation-system-v2/test/test-worker-pool.js +0 -266
@@ -1,86 +1,181 @@
1
1
  /**
2
- * @fileoverview Unit Testing Utility for Computations
2
+ * @fileoverview Integration Tester
3
+ * Wraps the Orchestrator to run computations in a test environment.
4
+ * - Redirects output to a test table.
5
+ * - Resolves and executes dependency chains.
6
+ * - Finds valid runnable dates based on raw data availability.
7
+ * * * UPDATE: Fixed dependency reading to look at test table instead of production.
3
8
  */
4
9
 
5
- class ComputationTester {
6
- constructor(ComputationClass, config) {
7
- this.ComputationClass = ComputationClass;
8
- this.config = config;
9
- this.mockData = {};
10
- this.mockDependencies = {};
11
- }
12
-
13
- withMockData(tableName, data) {
14
- this.mockData[tableName] = data;
15
- return this;
10
+ const fs = require('fs');
11
+ const path = require('path');
12
+ const { Orchestrator } = require('../execution/Orchestrator');
13
+ const { ManifestBuilder } = require('../core/Manifest');
14
+
15
+ class IntegrationTester {
16
+ constructor(baseConfig, logger = console) {
17
+ this.baseConfig = baseConfig;
18
+ this.logger = logger;
19
+ this.testConfig = this._createTestConfig(baseConfig);
16
20
  }
17
-
18
- withMockDependency(computationName, results) {
19
- this.mockDependencies[computationName] = results;
20
- return this;
21
+
22
+ /**
23
+ * Create a modified config for testing
24
+ * - Writes to computation_results_test
25
+ * - Disables Cloud Tasks
26
+ * - Forces local execution (no worker pool)
27
+ */
28
+ _createTestConfig(config) {
29
+ const testConfig = JSON.parse(JSON.stringify(config)); // Deep clone
30
+
31
+ // 1. Redirect Storage (Writes)
32
+ if (!testConfig.resultStore) testConfig.resultStore = {};
33
+ testConfig.resultStore.table = 'computation_results_test';
34
+
35
+ // 2. Redirect Reads for Intermediate Results (Crucial for Chained Computations)
36
+ // We must ensure that when a computation asks for 'computation_results',
37
+ // it reads from the TEST table where the upstream dependency just wrote.
38
+ if (testConfig.tables && testConfig.tables['computation_results']) {
39
+ testConfig.tables['computation_results'].tableName = 'computation_results_test';
40
+ }
41
+
42
+ // 3. Disable Side Effects
43
+ testConfig.cloudTasks = null; // Don't trigger downstream
44
+
45
+ // 4. Force Local Execution for simplified debugging
46
+ if (testConfig.workerPool) {
47
+ testConfig.workerPool.enabled = false;
48
+ }
49
+
50
+ // Preserve the original computation classes (JSON.stringify lost them)
51
+ testConfig.computations = config.computations;
52
+ // Preserve rules (functions)
53
+ testConfig.rules = config.rules;
54
+
55
+ return testConfig;
21
56
  }
22
-
23
- async run(entityId = 'test_entity') {
24
- const instance = new this.ComputationClass();
57
+
58
+ /**
59
+ * Build a standalone Orchestrator instance restricted to the specific dependency chain
60
+ */
61
+ async setupForComputation(targetName) {
62
+ // 1. Identify Dependency Chain
63
+ const allComputations = this.testConfig.computations;
64
+ const chain = this._resolveDependencyChain(targetName, allComputations);
25
65
 
26
- const context = {
27
- date: '2026-01-24',
28
- entityId,
29
- data: this.mockData,
30
- dependencies: this.mockDependencies,
31
- rules: this.config.rules,
32
- config: this.config,
33
- logger: { log: () => {} }
34
- };
66
+ this.logger.log('INFO', `[Tester] execution chain: ${chain.map(c => c.getConfig().name).join(' -> ')}`);
67
+
68
+ // 2. Configure Orchestrator with ONLY this chain
69
+ const runConfig = { ...this.testConfig, computations: chain };
70
+ this.orchestrator = new Orchestrator(runConfig, this.logger);
71
+ await this.orchestrator.initialize();
35
72
 
36
- instance._meta = { logger: context.logger, config: this.config };
73
+ return chain;
74
+ }
75
+
76
+ /**
77
+ * Find the most recent date where Raw Data requirements are met
78
+ */
79
+ async findLatestRunnableDate(targetName, lookbackDays = 7) {
80
+ if (!this.orchestrator) await this.setupForComputation(targetName);
81
+
82
+ const manifest = this.orchestrator.manifest;
83
+ const chainNames = new Set(manifest.map(c => c.name));
84
+
85
+ // Collect all Raw Data requirements (exclude requirements that are other computations in the chain)
86
+ const rawRequirements = {};
37
87
 
38
- await instance.process(context);
88
+ for (const entry of manifest) {
89
+ for (const [reqName, reqSpec] of Object.entries(entry.requires)) {
90
+ // If the requirement is NOT a computation in our chain, it's raw data
91
+ rawRequirements[reqName] = reqSpec;
92
+ }
93
+ }
94
+
95
+ this.logger.log('INFO', `[Tester] Checking data availability for: ${Object.keys(rawRequirements).join(', ')}`);
96
+
97
+ // Iterate backwards from today
98
+ const today = new Date();
99
+ for (let i = 1; i <= lookbackDays; i++) {
100
+ const d = new Date(today);
101
+ d.setDate(today.getDate() - i);
102
+ const dateStr = d.toISOString().slice(0, 10);
103
+
104
+ const availability = await this.orchestrator.dataFetcher.checkAvailability(rawRequirements, dateStr);
105
+
106
+ if (availability.canRun) {
107
+ this.logger.log('INFO', `[Tester] Found runnable date: ${dateStr}`);
108
+ return dateStr;
109
+ } else {
110
+ this.logger.log('DEBUG', `[Tester] Date ${dateStr} missing: ${availability.missing.join(', ')}`);
111
+ }
112
+ }
39
113
 
40
- const allResults = await instance.getResult();
41
- return allResults[entityId] || allResults;
114
+ throw new Error(`No runnable date found in last ${lookbackDays} days.`);
42
115
  }
43
-
44
- async expect(entityId = 'test_entity') {
45
- const result = await this.run(entityId);
116
+
117
+ async run(targetName, dateStr) {
118
+ if (!this.orchestrator) await this.setupForComputation(targetName);
46
119
 
120
+ // Ensure test table exists
121
+ await this.orchestrator.storageManager._ensureBigQueryTable('computation_results_test');
122
+
123
+ this.logger.log('INFO', `[Tester] Running chain for ${targetName} on ${dateStr}`);
124
+
125
+ // Run the DAG
126
+ // Orchestrator will handle passes automatically (Deps first, then Target)
127
+ const summary = await this.orchestrator.execute({
128
+ date: dateStr,
129
+ force: true // Force run even if "up to date" in test table
130
+ });
131
+
132
+ // Always fetch from the standard State Repository (JSON result store)
133
+ const result = await this.orchestrator.stateRepository.getResult(dateStr, targetName);
134
+
47
135
  return {
48
- result,
49
-
50
- toHaveProperty(prop, value) {
51
- const actual = this._getNestedProperty(result, prop);
52
- if (value !== undefined && actual !== value) {
53
- throw new Error(`Expected ${prop} to be ${value}, got ${actual}`);
54
- }
55
- if (actual === undefined) {
56
- throw new Error(`Expected ${prop} to exist`);
57
- }
58
- return this;
59
- },
136
+ date: dateStr,
137
+ summary,
138
+ result
139
+ };
140
+ }
141
+
142
+ /**
143
+ * Recursively find all dependencies for the target computation
144
+ */
145
+ _resolveDependencyChain(targetName, allComputations) {
146
+ const compMap = new Map(allComputations.map(c => {
147
+ try {
148
+ const conf = c.getConfig();
149
+ return [conf.name.toLowerCase(), c];
150
+ } catch (e) { return [null, null]; }
151
+ }));
152
+
153
+ const chain = new Set();
154
+ const visit = (name) => {
155
+ const key = name.toLowerCase();
156
+ if (chain.has(key)) return;
60
157
 
61
- toMatchSchema(schema) {
62
- const errors = this._validateSchema(result, schema);
63
- if (errors.length > 0) {
64
- throw new Error(`Schema validation failed: ${errors.join(', ')}`);
65
- }
66
- return this;
67
- },
158
+ const compClass = compMap.get(key);
159
+ if (!compClass) throw new Error(`Computation '${name}' not found.`);
68
160
 
69
- _getNestedProperty(obj, path) {
70
- return path.split('.').reduce((curr, key) => curr?.[key], obj);
71
- },
161
+ const config = compClass.getConfig();
72
162
 
73
- _validateSchema(data, schema) {
74
- const errors = [];
75
- for (const [key, type] of Object.entries(schema)) {
76
- if (typeof data[key] !== type) {
77
- errors.push(`${key}: expected ${type}, got ${typeof data[key]}`);
78
- }
79
- }
80
- return errors;
163
+ // Recurse dependencies
164
+ if (config.dependencies) {
165
+ config.dependencies.forEach(dep => visit(dep));
81
166
  }
167
+ // Recurse conditional dependencies
168
+ if (config.conditionalDependencies) {
169
+ config.conditionalDependencies.forEach(cd => visit(cd.computation));
170
+ }
171
+
172
+ chain.add(key);
82
173
  };
174
+
175
+ visit(targetName);
176
+
177
+ return Array.from(chain).map(k => compMap.get(k));
83
178
  }
84
179
  }
85
180
 
86
- module.exports = { ComputationTester };
181
+ module.exports = { IntegrationTester };
@@ -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;