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.
- package/functions/computation-system-v2/UserPortfolioMetrics.js +50 -0
- package/functions/computation-system-v2/computations/BehavioralAnomaly.js +557 -337
- package/functions/computation-system-v2/computations/GlobalAumPerAsset30D.js +103 -0
- 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/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 +30 -128
- 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/data/DataFetcher.js +250 -184
- 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 +215 -129
- 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 +105 -67
- package/functions/computation-system-v2/framework/testing/ComputationTester.js +12 -6
- 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-invalidation-scenarios.js +234 -0
- 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
|
-
|
|
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;
|
|
@@ -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);
|