bulltrackers-module 1.0.768 → 1.0.770

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 (52) 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/RiskScoreIncrease.js +13 -13
  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/handlers/scheduler.js +172 -203
  47. package/functions/computation-system-v2/legacy/PiAssetRecommender.js.old +115 -0
  48. package/functions/computation-system-v2/legacy/PiSimilarityMatrix.js +104 -0
  49. package/functions/computation-system-v2/legacy/PiSimilarityVector.js +71 -0
  50. package/functions/computation-system-v2/scripts/debug_aggregation.js +25 -0
  51. package/functions/computation-system-v2/scripts/test-invalidation-scenarios.js +234 -0
  52. package/package.json +1 -1
@@ -0,0 +1,103 @@
1
+ /**
2
+ * @fileoverview Aggregates $ AUM per asset across ALL Popular Investors.
3
+ * * STRATEGY: Global Computation (Reduce Step).
4
+ * * INPUT: Reads 'computation_results' for 'pidailyassetaum'.
5
+ * * OUTPUT: List of { symbol, amount } for Materialized View generation.
6
+ */
7
+ const { Computation } = require('../framework');
8
+
9
+ class GlobalAumPerAsset extends Computation {
10
+
11
+ static getConfig() {
12
+ return {
13
+ name: 'GlobalAumPerAsset',
14
+ type: 'global',
15
+ category: 'market_insights',
16
+
17
+ requires: {
18
+ 'computation_results': {
19
+ lookback: 0,
20
+ mandatory: true,
21
+ fields: ['result_data', 'computation_name', 'date'],
22
+ // Lowercase to match BigQuery storage
23
+ filter: {
24
+ computation_name: 'pidailyassetaum'
25
+ }
26
+ }
27
+ },
28
+
29
+ dependencies: ['PIDailyAssetAUM'],
30
+
31
+ storage: {
32
+ bigquery: true,
33
+ firestore: {
34
+ enabled: true,
35
+ path: 'global_metrics/aum_per_asset_{date}',
36
+ merge: true
37
+ }
38
+ }
39
+ };
40
+ }
41
+
42
+ async process(context) {
43
+ const { data } = context;
44
+
45
+ let rows = data['computation_results'];
46
+ if (!rows) {
47
+ console.log('[GlobalAumPerAsset] No input data found.');
48
+ return;
49
+ }
50
+
51
+ if (!Array.isArray(rows) && typeof rows === 'object') {
52
+ rows = Object.values(rows);
53
+ }
54
+
55
+ if (rows.length === 0) {
56
+ console.log('[GlobalAumPerAsset] Input array is empty.');
57
+ return;
58
+ }
59
+
60
+ console.log(`[GlobalAumPerAsset] Aggregating ${rows.length} PI portfolios...`);
61
+
62
+ const globalTotals = new Map();
63
+ let piCount = 0;
64
+
65
+ for (const row of rows) {
66
+ let assetMap = row.result_data;
67
+
68
+ if (typeof assetMap === 'string') {
69
+ try { assetMap = JSON.parse(assetMap); } catch (e) { continue; }
70
+ }
71
+
72
+ if (!assetMap) continue;
73
+
74
+ piCount++;
75
+
76
+ for (const [ticker, value] of Object.entries(assetMap)) {
77
+ if (typeof value === 'number') {
78
+ const current = globalTotals.get(ticker) || 0;
79
+ globalTotals.set(ticker, current + value);
80
+ }
81
+ }
82
+ }
83
+
84
+ const materializedViewData = [];
85
+ for (const [symbol, amount] of globalTotals.entries()) {
86
+ if (amount > 0) {
87
+ materializedViewData.push({
88
+ symbol: symbol,
89
+ amount: Number(amount.toFixed(2))
90
+ });
91
+ }
92
+ }
93
+
94
+ materializedViewData.sort((a, b) => b.amount - a.amount);
95
+
96
+ console.log(`[GlobalAumPerAsset] Generated stats for ${materializedViewData.length} unique assets from ${piCount} PIs.`);
97
+
98
+ // FIX: Use '_global' to match Orchestrator context (was 'global')
99
+ this.setResult('_global', materializedViewData);
100
+ }
101
+ }
102
+
103
+ module.exports = GlobalAumPerAsset;
@@ -0,0 +1,134 @@
1
+ const { Computation } = require('../framework');
2
+
3
+ class PIDailyAssetAUM extends Computation {
4
+
5
+ static getConfig() {
6
+ return {
7
+ name: 'PIDailyAssetAUM',
8
+ type: 'per-entity',
9
+ category: 'popular_investor',
10
+ isHistorical: true,
11
+
12
+ requires: {
13
+ // Driver: Only process Popular Investors
14
+ 'portfolio_snapshots': {
15
+ lookback: 30, // Extended lookback to ensure we find snapshots
16
+ mandatory: true,
17
+ fields: ['user_id', 'portfolio_data', 'date', 'user_type'],
18
+ filter: { user_type: 'POPULAR_INVESTOR' }
19
+ },
20
+ 'pi_rankings': {
21
+ lookback: 30, // Extended lookback to match portfolios
22
+ mandatory: true,
23
+ fields: ['pi_id', 'rankings_data', 'date']
24
+ },
25
+ 'ticker_mappings': {
26
+ mandatory: false,
27
+ fields: ['instrument_id', 'ticker']
28
+ },
29
+ 'pi_master_list': {
30
+ mandatory: false,
31
+ fields: ['cid', 'username']
32
+ }
33
+ },
34
+
35
+ storage: {
36
+ bigquery: true,
37
+ firestore: {
38
+ enabled: true,
39
+ path: 'popular_investors/{entityId}/metrics/asset_aum_{date}',
40
+ merge: true
41
+ }
42
+ }
43
+ };
44
+ }
45
+
46
+ async process(context) {
47
+ const { data, entityId, rules } = context;
48
+
49
+ // --- HELPER: Safe Date Parser ---
50
+ // Handles BigQuery { value: '2023-01-01' } objects to prevent NaN errors
51
+ const parseDate = (d) => {
52
+ if (!d) return null;
53
+ if (d instanceof Date) return d;
54
+ if (typeof d === 'object' && d.value) return new Date(d.value);
55
+ return new Date(d);
56
+ };
57
+
58
+ const getEntityRows = (dataset) => {
59
+ if (!dataset) return [];
60
+ if (dataset[entityId]) return Array.isArray(dataset[entityId]) ? dataset[entityId] : [dataset[entityId]];
61
+ // Handle mismatched ID columns (user_id vs pi_id)
62
+ if (Array.isArray(dataset)) return dataset.filter(r => String(r.user_id || r.pi_id || r.cid) === String(entityId));
63
+ return [];
64
+ };
65
+
66
+ // Mappings
67
+ const tickerMap = new Map();
68
+ const mappingsList = Array.isArray(context.data['ticker_mappings']) ? context.data['ticker_mappings'] : Object.values(context.data['ticker_mappings'] || {});
69
+ mappingsList.forEach(row => { if (row.instrument_id) tickerMap.set(Number(row.instrument_id), row.ticker); });
70
+ const resolveTicker = (id) => tickerMap.get(Number(id)) || `ID:${id}`;
71
+
72
+ // 2. Get Valid Snapshots
73
+ const portfolios = getEntityRows(data['portfolio_snapshots']);
74
+ const rankings = getEntityRows(data['pi_rankings']);
75
+
76
+ let validPortfolio = null;
77
+ let validRanking = null;
78
+
79
+ // Sort descending using safe date parsing
80
+ portfolios.sort((a,b) => parseDate(b.date) - parseDate(a.date));
81
+ rankings.sort((a,b) => parseDate(b.date) - parseDate(a.date));
82
+
83
+ if (portfolios.length > 0) {
84
+ validPortfolio = portfolios[0];
85
+ const pDate = parseDate(validPortfolio.date);
86
+
87
+ validRanking = rankings.find(r => {
88
+ const rDate = parseDate(r.date);
89
+ const diffTime = Math.abs(pDate - rDate);
90
+ const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
91
+
92
+ // Allow up to 14 days gap (matches your data reality)
93
+ return diffDays <= 14;
94
+ });
95
+ }
96
+
97
+ if (!validPortfolio || !validRanking) {
98
+ return;
99
+ }
100
+
101
+ // 3. Calculation
102
+ const pData = rules.portfolio.extractPortfolioData(validPortfolio);
103
+ const rData = rules.rankings.extractRankingsData(validRanking);
104
+
105
+ // FIX: Use 'getAUM' (checked from rules/rankings.js)
106
+ const totalAum = rules.rankings.getAUM(rData);
107
+
108
+ if (!totalAum || totalAum <= 0) return;
109
+
110
+ const positions = rules.portfolio.extractPositions(pData);
111
+ const assetAumMap = {};
112
+
113
+ positions.forEach(pos => {
114
+ const id = rules.portfolio.getInstrumentId(pos);
115
+ const ticker = resolveTicker(id);
116
+
117
+ // FIX: Use 'getInvested' (checked from rules/portfolio.js)
118
+ const weight = rules.portfolio.getInvested(pos);
119
+
120
+ if (weight > 0) {
121
+ // weight is a percentage (e.g., 5.5 for 5.5%), so divide by 100
122
+ const dollarValue = (weight / 100) * totalAum;
123
+ assetAumMap[ticker] = (assetAumMap[ticker] || 0) + dollarValue;
124
+ }
125
+ });
126
+
127
+ // 4. Formatting
128
+ Object.keys(assetAumMap).forEach(k => assetAumMap[k] = Number(assetAumMap[k].toFixed(2)));
129
+
130
+ this.setResult(entityId, assetAumMap);
131
+ }
132
+ }
133
+
134
+ module.exports = PIDailyAssetAUM;
@@ -0,0 +1,227 @@
1
+ /**
2
+ * @fileoverview PI Feature Vectors
3
+ *
4
+ * Global computation that builds normalized feature vectors for all Popular Investors.
5
+ * Used by PiRecommender to find balanced recommendations.
6
+ *
7
+ * Features computed per PI:
8
+ * - Sector exposure (normalized %)
9
+ * - Risk profile (volatility, risk score)
10
+ * - Performance metrics (gain %, win ratio)
11
+ * - Quality indicators (AUM tier, copiers)
12
+ */
13
+ const { Computation } = require('../framework');
14
+
15
+ class PiFeatureVectors extends Computation {
16
+ static getConfig() {
17
+ return {
18
+ name: 'PiFeatureVectors',
19
+ description: 'Computes feature vectors for all PIs for recommendation matching',
20
+ type: 'global',
21
+ category: 'popular_investor',
22
+ isHistorical: false,
23
+
24
+ requires: {
25
+ 'portfolio_snapshots': {
26
+ lookback: 0,
27
+ mandatory: true,
28
+ fields: ['user_id', 'portfolio_data', 'date'],
29
+ filter: { user_type: 'POPULAR_INVESTOR' }
30
+ },
31
+ 'pi_rankings': {
32
+ lookback: 1,
33
+ mandatory: true,
34
+ fields: ['pi_id', 'rankings_data', 'username', 'date']
35
+ },
36
+ 'pi_master_list': {
37
+ lookback: 1,
38
+ mandatory: false,
39
+ fields: ['cid', 'username', 'verified_date']
40
+ },
41
+ 'ticker_mappings': { mandatory: false },
42
+ 'sector_mappings': { mandatory: false }
43
+ },
44
+
45
+ storage: {
46
+ bigquery: true,
47
+ firestore: { enabled: false }
48
+ }
49
+ };
50
+ }
51
+
52
+ async process(context) {
53
+ const { data, rules, targetDate } = context;
54
+
55
+ // ===========================================================================
56
+ // SETUP: Helpers and reference data
57
+ // ===========================================================================
58
+ const toArray = (input) => {
59
+ if (!input) return [];
60
+ if (Array.isArray(input)) return input;
61
+ // Handle Map-like structure from DataFetcher
62
+ return Object.values(input).flat();
63
+ };
64
+
65
+ // Build sector lookup
66
+ const tickerMap = new Map();
67
+ toArray(data['ticker_mappings']).forEach(row => {
68
+ if (row.instrument_id && row.ticker) {
69
+ tickerMap.set(Number(row.instrument_id), row.ticker.toUpperCase());
70
+ }
71
+ });
72
+
73
+ const sectorMap = new Map();
74
+ toArray(data['sector_mappings']).forEach(row => {
75
+ if (row.symbol && row.sector) {
76
+ sectorMap.set(row.symbol.toUpperCase(), row.sector);
77
+ }
78
+ });
79
+
80
+ const resolveSector = (instrumentId) => {
81
+ const ticker = tickerMap.get(Number(instrumentId));
82
+ return ticker ? (sectorMap.get(ticker) || 'Other') : 'Other';
83
+ };
84
+
85
+ // All known sectors for consistent vector dimensions
86
+ const ALL_SECTORS = [
87
+ 'Technology', 'Healthcare', 'Financial Services', 'Consumer Cyclical',
88
+ 'Industrials', 'Energy', 'Utilities', 'Real Estate', 'Communication Services',
89
+ 'Consumer Defensive', 'Basic Materials', 'Crypto', 'Commodities', 'ETF', 'Other'
90
+ ];
91
+
92
+ // ===========================================================================
93
+ // GATHER: PI portfolio and rankings data
94
+ // ===========================================================================
95
+ const rankings = toArray(data['pi_rankings']);
96
+ const portfolios = data['portfolio_snapshots']; // Map { piId: [rows] }
97
+ const masterList = toArray(data['pi_master_list']);
98
+
99
+ // Build username lookup
100
+ const usernameMap = new Map();
101
+ rankings.forEach(r => {
102
+ const id = String(r.pi_id || r.CustomerId);
103
+ usernameMap.set(id, r.username || r.UserName);
104
+ });
105
+ masterList.forEach(m => {
106
+ if (m.cid && m.username) usernameMap.set(String(m.cid), m.username);
107
+ });
108
+
109
+ // ===========================================================================
110
+ // COMPUTE: Feature vectors for each PI
111
+ // ===========================================================================
112
+ const featureVectors = {};
113
+
114
+ // Process each PI's portfolio
115
+ const piIds = Object.keys(portfolios || {});
116
+
117
+ for (const piId of piIds) {
118
+ const rows = Array.isArray(portfolios[piId]) ? portfolios[piId] : [portfolios[piId]];
119
+ const latestRow = rows[rows.length - 1];
120
+ if (!latestRow) continue;
121
+
122
+ // Extract positions
123
+ const pData = rules.portfolio.extractPortfolioData(latestRow);
124
+ const positions = rules.portfolio.extractPositions(pData);
125
+
126
+ if (positions.length === 0) continue;
127
+
128
+ // Calculate sector exposure
129
+ const sectorWeights = {};
130
+ let totalValue = 0;
131
+
132
+ positions.forEach(pos => {
133
+ const instrumentId = rules.portfolio.getInstrumentId(pos);
134
+ const invested = rules.portfolio.getInvested(pos) || 0;
135
+ const sector = resolveSector(instrumentId);
136
+
137
+ sectorWeights[sector] = (sectorWeights[sector] || 0) + invested;
138
+ totalValue += invested;
139
+ });
140
+
141
+ // Normalize sector exposure to percentages
142
+ const sectorVector = {};
143
+ ALL_SECTORS.forEach(s => {
144
+ sectorVector[s] = totalValue > 0
145
+ ? Number(((sectorWeights[s] || 0) / totalValue).toFixed(4))
146
+ : 0;
147
+ });
148
+
149
+ // Get ranking data
150
+ if (piIds.indexOf(piId) === 0) {
151
+ const availableIds = rankings.map(r => r.pi_id || 'undefined').join(',');
152
+ console.log(`[DEBUG] Available IDs in rankings: ${availableIds}`);
153
+ console.log(`[DEBUG] First ranking row keys: ${Object.keys(rankings[0] || {}).join(',')}`);
154
+ if (rankings[0]) console.log(`[DEBUG] First ranking row pi_id: ${rankings[0].pi_id}`);
155
+ }
156
+ const rankRow = rankings.find(r => String(r.pi_id || r.CustomerId) === String(piId));
157
+
158
+ if (piIds.indexOf(piId) === 0) {
159
+ console.log(`[DEBUG] PiId: ${piId}`);
160
+ console.log(`[DEBUG] RankRow found: ${!!rankRow}`);
161
+ if (rankRow) {
162
+ console.log(`[DEBUG] RankRow keys: ${Object.keys(rankRow).join(',')}`);
163
+ console.log(`[DEBUG] RankingsData type: ${typeof rankRow.rankings_data}`);
164
+ if (typeof rankRow.rankings_data === 'object') {
165
+ console.log(`[DEBUG] RankingsData keys: ${Object.keys(rankRow.rankings_data).join(',')}`);
166
+ console.log(`[DEBUG] RankingsData.Gain: ${rankRow.rankings_data.Gain}`);
167
+ }
168
+ }
169
+ }
170
+
171
+ let riskScore = 5, gain = 0, copiers = 0, winRatio = 0, aumTier = 0;
172
+
173
+ if (rankRow) {
174
+ const rData = rules.rankings.extractRankingsData(rankRow);
175
+ if (rData) {
176
+ riskScore = rules.rankings.getRiskScore(rData) || 5;
177
+ gain = rules.rankings.getTotalGain(rData) || 0;
178
+ copiers = rules.rankings.getCopiers(rData) || 0;
179
+ winRatio = rules.rankings.getWinRatio(rData) || 0;
180
+ aumTier = rules.rankings.getAUMTier(rData) || 0;
181
+ }
182
+ }
183
+
184
+ // Calculate concentration (Herfindahl index) - higher = more concentrated
185
+ const weights = Object.values(sectorWeights).filter(w => w > 0);
186
+ const hhi = totalValue > 0
187
+ ? weights.reduce((sum, w) => sum + Math.pow(w / totalValue, 2), 0)
188
+ : 1;
189
+
190
+ // Assemble feature vector
191
+ featureVectors[piId] = {
192
+ piId: String(piId),
193
+ username: usernameMap.get(String(piId)) || 'Unknown',
194
+
195
+ // Sector exposure (normalized)
196
+ sectors: sectorVector,
197
+
198
+ // Risk profile
199
+ riskScore: riskScore,
200
+ concentration: Number(hhi.toFixed(4)), // 0-1, higher = less diversified
201
+
202
+ // Performance
203
+ gain: gain,
204
+ winRatio: winRatio,
205
+
206
+ // Quality
207
+ copiers: copiers,
208
+ aumTier: aumTier,
209
+ positionCount: positions.length,
210
+
211
+ // Metadata
212
+ totalValue: Number(totalValue.toFixed(2)),
213
+ computedAt: targetDate
214
+ };
215
+ }
216
+
217
+ // Store all vectors as a single global result
218
+ this.setResult('_global', {
219
+ vectors: featureVectors,
220
+ piCount: Object.keys(featureVectors).length,
221
+ sectorDimensions: ALL_SECTORS,
222
+ computedAt: targetDate
223
+ });
224
+ }
225
+ }
226
+
227
+ module.exports = PiFeatureVectors;