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.
- package/functions/computation-system-v2/UserPortfolioMetrics.js +50 -0
- package/functions/computation-system-v2/computations/BehavioralAnomaly.js +559 -227
- package/functions/computation-system-v2/computations/GlobalAumPerAsset30D.js +103 -0
- package/functions/computation-system-v2/computations/NewSectorExposure.js +82 -35
- package/functions/computation-system-v2/computations/NewSocialPost.js +52 -24
- package/functions/computation-system-v2/computations/PIDailyAssetAUM.js +134 -0
- package/functions/computation-system-v2/computations/PiFeatureVectors.js +227 -0
- package/functions/computation-system-v2/computations/PiRecommender.js +359 -0
- package/functions/computation-system-v2/computations/PopularInvestorProfileMetrics.js +354 -641
- package/functions/computation-system-v2/computations/SignedInUserList.js +51 -0
- package/functions/computation-system-v2/computations/SignedInUserMirrorHistory.js +138 -0
- package/functions/computation-system-v2/computations/SignedInUserPIProfileMetrics.js +106 -0
- package/functions/computation-system-v2/computations/SignedInUserProfileMetrics.js +324 -0
- package/functions/computation-system-v2/config/bulltrackers.config.js +40 -126
- package/functions/computation-system-v2/core-api.js +17 -9
- package/functions/computation-system-v2/data_schema_reference.MD +108 -0
- package/functions/computation-system-v2/devtools/builder/builder.js +362 -0
- package/functions/computation-system-v2/devtools/builder/examples/user-metrics.yaml +26 -0
- package/functions/computation-system-v2/devtools/index.js +36 -0
- package/functions/computation-system-v2/devtools/shared/MockDataFactory.js +235 -0
- package/functions/computation-system-v2/devtools/shared/SchemaTemplates.js +475 -0
- package/functions/computation-system-v2/devtools/shared/SystemIntrospector.js +517 -0
- package/functions/computation-system-v2/devtools/shared/index.js +16 -0
- package/functions/computation-system-v2/devtools/simulation/DAGAnalyzer.js +243 -0
- package/functions/computation-system-v2/devtools/simulation/MockDataFetcher.js +306 -0
- package/functions/computation-system-v2/devtools/simulation/MockStorageManager.js +336 -0
- package/functions/computation-system-v2/devtools/simulation/SimulationEngine.js +525 -0
- package/functions/computation-system-v2/devtools/simulation/SimulationServer.js +581 -0
- package/functions/computation-system-v2/devtools/simulation/index.js +17 -0
- package/functions/computation-system-v2/devtools/simulation/simulate.js +324 -0
- package/functions/computation-system-v2/devtools/vscode-computation/package.json +90 -0
- package/functions/computation-system-v2/devtools/vscode-computation/snippets/computation.json +128 -0
- package/functions/computation-system-v2/devtools/vscode-computation/src/extension.ts +401 -0
- package/functions/computation-system-v2/devtools/vscode-computation/src/providers/codeActions.ts +152 -0
- package/functions/computation-system-v2/devtools/vscode-computation/src/providers/completions.ts +207 -0
- package/functions/computation-system-v2/devtools/vscode-computation/src/providers/diagnostics.ts +205 -0
- package/functions/computation-system-v2/devtools/vscode-computation/src/providers/hover.ts +205 -0
- package/functions/computation-system-v2/devtools/vscode-computation/tsconfig.json +22 -0
- package/functions/computation-system-v2/docs/HowToCreateComputations.MD +602 -0
- package/functions/computation-system-v2/framework/core/Manifest.js +9 -16
- package/functions/computation-system-v2/framework/core/RunAnalyzer.js +2 -1
- package/functions/computation-system-v2/framework/data/DataFetcher.js +330 -126
- package/functions/computation-system-v2/framework/data/MaterializedViewManager.js +84 -0
- package/functions/computation-system-v2/framework/data/QueryBuilder.js +38 -38
- package/functions/computation-system-v2/framework/execution/Orchestrator.js +226 -153
- package/functions/computation-system-v2/framework/scheduling/ScheduleValidator.js +17 -19
- package/functions/computation-system-v2/framework/storage/StateRepository.js +32 -2
- package/functions/computation-system-v2/framework/storage/StorageManager.js +111 -83
- package/functions/computation-system-v2/framework/testing/ComputationTester.js +161 -66
- package/functions/computation-system-v2/handlers/dispatcher.js +57 -29
- package/functions/computation-system-v2/legacy/PiAssetRecommender.js.old +115 -0
- package/functions/computation-system-v2/legacy/PiSimilarityMatrix.js +104 -0
- package/functions/computation-system-v2/legacy/PiSimilarityVector.js +71 -0
- package/functions/computation-system-v2/scripts/debug_aggregation.js +25 -0
- package/functions/computation-system-v2/scripts/test-computation-dag.js +109 -0
- package/functions/computation-system-v2/scripts/test-invalidation-scenarios.js +234 -0
- package/functions/task-engine/helpers/data_storage_helpers.js +6 -6
- package/package.json +1 -1
- package/functions/computation-system-v2/computations/PopularInvestorRiskAssessment.js +0 -176
- package/functions/computation-system-v2/computations/PopularInvestorRiskMetrics.js +0 -294
- package/functions/computation-system-v2/computations/UserPortfolioSummary.js +0 -172
- package/functions/computation-system-v2/scripts/migrate-sectors.js +0 -73
- package/functions/computation-system-v2/test/analyze-results.js +0 -238
- package/functions/computation-system-v2/test/other/test-dependency-cascade.js +0 -150
- package/functions/computation-system-v2/test/other/test-dispatcher.js +0 -317
- package/functions/computation-system-v2/test/other/test-framework.js +0 -500
- package/functions/computation-system-v2/test/other/test-real-execution.js +0 -166
- package/functions/computation-system-v2/test/other/test-real-integration.js +0 -194
- package/functions/computation-system-v2/test/other/test-refactor-e2e.js +0 -131
- package/functions/computation-system-v2/test/other/test-results.json +0 -31
- package/functions/computation-system-v2/test/other/test-risk-metrics-computation.js +0 -329
- package/functions/computation-system-v2/test/other/test-scheduler.js +0 -204
- package/functions/computation-system-v2/test/other/test-storage.js +0 -449
- package/functions/computation-system-v2/test/run-pipeline-test.js +0 -554
- package/functions/computation-system-v2/test/test-full-pipeline.js +0 -227
- package/functions/computation-system-v2/test/test-worker-pool.js +0 -266
|
@@ -1,86 +1,181 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @fileoverview
|
|
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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
this.
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
24
|
-
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
41
|
-
return allResults[entityId] || allResults;
|
|
114
|
+
throw new Error(`No runnable date found in last ${lookbackDays} days.`);
|
|
42
115
|
}
|
|
43
|
-
|
|
44
|
-
async
|
|
45
|
-
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
62
|
-
|
|
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
|
-
|
|
70
|
-
return path.split('.').reduce((curr, key) => curr?.[key], obj);
|
|
71
|
-
},
|
|
161
|
+
const config = compClass.getConfig();
|
|
72
162
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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 = {
|
|
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
|
-
|
|
34
|
-
|
|
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
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
|
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
|
-
//
|
|
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
|
|
110
|
+
force
|
|
78
111
|
});
|
|
79
112
|
|
|
80
113
|
const duration = Date.now() - startTime;
|
|
81
114
|
|
|
82
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
|
|
116
|
-
|
|
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(
|
|
151
|
+
return res.status(statusCode).json({
|
|
124
152
|
status: 'error',
|
|
125
|
-
reason:
|
|
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;
|