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,324 @@
1
+ /**
2
+ * @fileoverview Signed-In User Profile Metrics (v2 Refactor)
3
+ * * TARGETS: 'SIGNED_IN_USER' only (via BQ filters).
4
+ * * STRATEGY: Iterates Portfolio Mirrors to enrich copied PI data.
5
+ */
6
+ const { Computation } = require('../framework');
7
+
8
+ class SignedInUserProfileMetrics extends Computation {
9
+
10
+ static getConfig() {
11
+ return {
12
+ name: 'SignedInUserProfileMetrics',
13
+ type: 'per-entity',
14
+ category: 'signed_in_user',
15
+ isHistorical: true,
16
+
17
+ requires: {
18
+ // --- Core Data (Drivers) ---
19
+ // [CRITICAL] Filters restrict execution to Signed-In Users only
20
+ 'portfolio_snapshots': {
21
+ lookback: 30,
22
+ mandatory: true,
23
+ fields: ['user_id', 'portfolio_data', 'date'],
24
+ filter: { user_type: 'SIGNED_IN_USER' }
25
+ },
26
+ 'trade_history_snapshots': {
27
+ lookback: 30,
28
+ mandatory: false,
29
+ fields: ['user_id', 'history_data', 'date'],
30
+ filter: { user_type: 'SIGNED_IN_USER' }
31
+ },
32
+ 'social_post_snapshots': {
33
+ lookback: 7,
34
+ mandatory: false,
35
+ fields: ['user_id', 'posts_data', 'date'],
36
+ filter: { user_type: 'SIGNED_IN_USER' }
37
+ },
38
+
39
+ // --- Reference Data (Lookups) ---
40
+ // Used only to enrich PIs found in the user's copy list
41
+ 'pi_rankings': {
42
+ lookback: 7,
43
+ mandatory: false,
44
+ fields: ['pi_id', 'rankings_data', 'username', 'date']
45
+ },
46
+ 'pi_master_list': {
47
+ lookback: 1,
48
+ mandatory: false,
49
+ fields: ['cid', 'username']
50
+ },
51
+ 'pi_ratings': {
52
+ lookback: 30,
53
+ mandatory: false,
54
+ fields: ['pi_id', 'average_rating', 'date']
55
+ },
56
+
57
+ // --- Mappings ---
58
+ 'ticker_mappings': { mandatory: false },
59
+ 'sector_mappings': { mandatory: false }
60
+ },
61
+
62
+ storage: {
63
+ bigquery: true,
64
+ firestore: {
65
+ enabled: true,
66
+ path: 'user_profiles/{entityId}/metrics/{date}', // Standardized path
67
+ merge: true
68
+ }
69
+ },
70
+
71
+ userType: 'SIGNED_IN_USER'
72
+ };
73
+ }
74
+
75
+ async process(context) {
76
+ const { data, entityId, rules } = context;
77
+
78
+ // ==========================================================================================
79
+ // 0. HELPERS & REFERENCE DATA
80
+ // ==========================================================================================
81
+ const toDateStr = (d) => (d?.value || (d instanceof Date ? d.toISOString().slice(0, 10) : String(d)));
82
+ const toArray = (input) => (Array.isArray(input) ? input : (input ? Object.values(input) : []));
83
+
84
+ // Helper to handle V2 Map-based data structure
85
+ const getEntityRows = (dataset) => {
86
+ if (!dataset) return [];
87
+ // Direct entity lookup (fastest)
88
+ if (dataset[entityId]) return Array.isArray(dataset[entityId]) ? dataset[entityId] : [dataset[entityId]];
89
+ // Fallback for flat arrays (slower, but safe)
90
+ if (Array.isArray(dataset)) return dataset.filter(r => String(r.user_id || r.cid) === String(entityId));
91
+ return [];
92
+ };
93
+
94
+ const sortAsc = (input) => toArray(input).sort((a, b) => toDateStr(a.date).localeCompare(toDateStr(b.date)));
95
+
96
+ // Mappings
97
+ const tickerMap = new Map();
98
+ toArray(data['ticker_mappings']).forEach(row => { if (row.instrument_id) tickerMap.set(Number(row.instrument_id), row.ticker); });
99
+ const resolveTicker = (id) => tickerMap.get(Number(id)) || `ID:${id}`;
100
+
101
+ const sectorMap = new Map();
102
+ toArray(data['sector_mappings']).forEach(row => { if (row.symbol) sectorMap.set(row.symbol.toUpperCase(), row.sector); });
103
+ const resolveSector = (id) => {
104
+ const ticker = resolveTicker(id);
105
+ return ticker ? (sectorMap.get(ticker) || 'Unknown') : 'Unknown';
106
+ };
107
+
108
+ // Global Reference Data (Optimized for Lookups)
109
+ // We convert these arrays to Maps/Finders only once if this were a global calc,
110
+ // but here we just do simple lookups for the specific copied PIs.
111
+ const globalRankings = toArray(data['pi_rankings']);
112
+ const piMasterList = toArray(data['pi_master_list']);
113
+
114
+ const resolveUsername = (cid) => {
115
+ const rank = globalRankings.find(r => String(r.pi_id || r.CustomerId) === String(cid));
116
+ if (rank?.username || rank?.UserName) return rank.username || rank.UserName;
117
+ const master = piMasterList.find(m => String(m.cid) === String(cid));
118
+ return master?.username || 'Unknown';
119
+ };
120
+
121
+ // Initialize Result Structure
122
+ const result = {
123
+ socialEngagement: { chartType: 'line', data: [] },
124
+ myPosts: { chartType: 'feed', data: [] },
125
+ profitablePositions: { chartType: 'bar', data: [] },
126
+ topWinningPositions: { chartType: 'table', data: [] },
127
+ sectorPerformance: { bestSector: null, worstSector: null, bestSectorProfit: 0, worstSectorProfit: 0 },
128
+ sectorExposure: { chartType: 'pie', data: {} },
129
+ assetExposure: { chartType: 'pie', data: {} },
130
+ portfolioSummary: { totalInvested: 0, totalProfit: 0, profitPercent: 0 },
131
+ copiedPIs: { chartType: 'cards', data: [] }, // Populated from Mirrors
132
+ recommendedPIs: { chartType: 'cards', data: [] }, // (Optional: derived from similarity)
133
+ recentlyViewedPages: { chartType: 'list', data: [] },
134
+ alertGraphs: { chartType: 'line', data: [] },
135
+ performanceGraphics: { chartType: 'composite', data: {} },
136
+ exposureGraphics: { chartType: 'composite', data: {} }
137
+ };
138
+
139
+ // ==========================================================================================
140
+ // 1. DATA PREPARATION
141
+ // ==========================================================================================
142
+ const portfolios = sortAsc(getEntityRows(data['portfolio_snapshots']));
143
+ const historyData = sortAsc(getEntityRows(data['trade_history_snapshots']));
144
+ const socialData = sortAsc(getEntityRows(data['social_post_snapshots']));
145
+
146
+ const currentPortfolio = portfolios.length > 0 ? portfolios[portfolios.length - 1] : null;
147
+
148
+ // ==========================================================================================
149
+ // 2. SOCIAL FEED (My Posts)
150
+ // ==========================================================================================
151
+ const socialFeed = [];
152
+ socialData.forEach(day => {
153
+ const posts = rules.social.extractPosts(day);
154
+ const dStr = toDateStr(day.date);
155
+ let dayLikes = 0, dayComments = 0;
156
+
157
+ posts.forEach(p => {
158
+ const likes = rules.social.getPostLikes(p) || 0;
159
+ const comments = rules.social.getPostComments(p) || 0;
160
+ dayLikes += likes;
161
+ dayComments += comments;
162
+
163
+ socialFeed.push({
164
+ id: p.id,
165
+ date: rules.social.getPostDate(p),
166
+ text: rules.social.getPostText(p),
167
+ likes,
168
+ comments,
169
+ type: 'post'
170
+ });
171
+ });
172
+
173
+ if (dayLikes > 0 || dayComments > 0) {
174
+ result.socialEngagement.data.push({ date: dStr, likes: dayLikes, comments: dayComments });
175
+ }
176
+ });
177
+ // Limit feed size
178
+ result.myPosts.data = socialFeed.sort((a, b) => new Date(b.date) - new Date(a.date)).slice(0, 20);
179
+
180
+ // ==========================================================================================
181
+ // 3. TRADES & PERFORMANCE
182
+ // ==========================================================================================
183
+ const dailyPnL = new Map();
184
+ const winLoss = { wins: 0, losses: 0, profit: 0, loss: 0 };
185
+ const tradeStats = new Map();
186
+
187
+ historyData.forEach(dayDoc => {
188
+ const trades = rules.trades.extractTrades(dayDoc);
189
+ trades.forEach(t => {
190
+ const closeDate = rules.trades.getCloseDate(t);
191
+ if (!closeDate) return;
192
+
193
+ const dKey = closeDate.toISOString().split('T')[0];
194
+ const profit = rules.trades.getNetProfit(t);
195
+
196
+ // Stats for Bar Chart
197
+ const entry = tradeStats.get(dKey) || { date: dKey, profitableCount: 0, totalCount: 0 };
198
+ entry.totalCount++;
199
+ if (profit > 0) entry.profitableCount++;
200
+ tradeStats.set(dKey, entry);
201
+
202
+ // Stats for Aggregate Graphics
203
+ if (profit > 0) { winLoss.wins++; winLoss.profit += profit; }
204
+ else if (profit < 0) { winLoss.losses++; winLoss.loss += profit; }
205
+
206
+ dailyPnL.set(dKey, (dailyPnL.get(dKey) || 0) + profit);
207
+ });
208
+ });
209
+
210
+ result.profitablePositions.data = Array.from(tradeStats.values())
211
+ .sort((a,b) => a.date.localeCompare(b.date))
212
+ .slice(-30);
213
+
214
+ result.performanceGraphics.data = {
215
+ winRate: (winLoss.wins + winLoss.losses) > 0 ? Number(((winLoss.wins / (winLoss.wins + winLoss.losses)) * 100).toFixed(2)) : 0,
216
+ avgWin: winLoss.wins > 0 ? Number((winLoss.profit / winLoss.wins).toFixed(2)) : 0,
217
+ avgLoss: winLoss.losses > 0 ? Number((winLoss.loss / winLoss.losses).toFixed(2)) : 0,
218
+ profitFactor: winLoss.losses !== 0 ? Number((Math.abs(winLoss.profit / winLoss.loss)).toFixed(2)) : (winLoss.profit > 0 ? Infinity : 0),
219
+ dailyPnL: Array.from(dailyPnL.entries()).map(([date, pnl]) => ({ date, pnl: Number(pnl.toFixed(2)) })).sort((a,b) => a.date.localeCompare(b.date)).slice(-20)
220
+ };
221
+
222
+ // ==========================================================================================
223
+ // 4. PORTFOLIO & COPIED PIs (The Core Requirement)
224
+ // ==========================================================================================
225
+ if (currentPortfolio) {
226
+ const pData = rules.portfolio.extractPortfolioData(currentPortfolio);
227
+ const positions = rules.portfolio.extractPositions(pData);
228
+
229
+ // Extract Mirrors using Rules (assumes rule exists, or manual extraction if needed)
230
+ // If rules.portfolio doesn't have extractMirrors, we can look at pData.AggregatedMirrors directly
231
+ const mirrors = pData.AggregatedMirrors || [];
232
+
233
+ let totalInv = 0, totalProf = 0;
234
+ const secExp = {}, assetExp = {}, secProfits = {};
235
+
236
+ // A. Process Direct Positions
237
+ positions.forEach(pos => {
238
+ const id = rules.portfolio.getInstrumentId(pos);
239
+ const inv = rules.portfolio.getInvested(pos);
240
+ const profPct = rules.portfolio.getNetProfit(pos);
241
+ const profVal = inv * (profPct / 100);
242
+
243
+ totalInv += inv;
244
+ totalProf += profVal;
245
+
246
+ const ticker = resolveTicker(id);
247
+ const sector = resolveSector(id);
248
+
249
+ secExp[sector] = (secExp[sector] || 0) + inv;
250
+ assetExp[ticker] = (assetExp[ticker] || 0) + inv;
251
+
252
+ if (!secProfits[sector]) secProfits[sector] = { profit: 0, weight: 0 };
253
+ secProfits[sector].profit += profVal;
254
+ secProfits[sector].weight += inv;
255
+ });
256
+
257
+ // B. Process Copied PIs (Mirrors)
258
+ // * We iterate the USER'S mirrors, then lookup the PI data.
259
+ // * This avoids fetching all PIs as primary data.
260
+ mirrors.forEach(m => {
261
+ const cid = String(m.ParentCID || m.CID); // Adjust based on exact schema
262
+ const inv = m.Invested || m.Amount || 0;
263
+ const profPct = m.NetProfit || 0;
264
+
265
+ totalInv += inv;
266
+ totalProf += (inv * (profPct / 100));
267
+
268
+ // LOOKUP: Get PI details from the global reference data
269
+ const rank = globalRankings.find(r => String(r.pi_id || r.CustomerId) === cid);
270
+
271
+ result.copiedPIs.data.push({
272
+ cid,
273
+ username: resolveUsername(cid),
274
+ invested: inv,
275
+ netProfit: m.NetProfit || 0,
276
+ value: m.Value || 0,
277
+ pendingClosure: m.PendingForClosure === true,
278
+ isRanked: !!rank,
279
+ // Enrich with Reference Data
280
+ rankData: rank ? {
281
+ riskScore: rules.rankings.getRiskScore(rank),
282
+ gain: rules.rankings.getTotalGain(rank),
283
+ aum: rules.rankings.getAUMTier(rank)
284
+ } : null
285
+ });
286
+ });
287
+
288
+ // Summary Stats
289
+ result.portfolioSummary = {
290
+ totalInvested: Number(totalInv.toFixed(2)),
291
+ totalProfit: Number(totalProf.toFixed(2)),
292
+ profitPercent: totalInv > 0 ? Number(((totalProf / totalInv) * 100).toFixed(2)) : 0
293
+ };
294
+
295
+ // Calculate Best/Worst Sectors
296
+ let bestS = null, worstS = null, bestP = -Infinity, worstP = Infinity;
297
+ Object.entries(secProfits).forEach(([sec, d]) => {
298
+ if (d.weight <= 0) return;
299
+ const p = (d.profit / d.weight) * 100;
300
+ if (p > bestP) { bestP = p; bestS = sec; }
301
+ if (p < worstP) { worstP = p; worstS = sec; }
302
+ });
303
+ result.sectorPerformance = {
304
+ bestSector: bestS,
305
+ worstSector: worstS,
306
+ bestSectorProfit: bestS ? Number(bestP.toFixed(2)) : 0,
307
+ worstSectorProfit: worstS ? Number(worstP.toFixed(2)) : 0
308
+ };
309
+
310
+ // Exposure Charts
311
+ if (totalInv > 0) {
312
+ Object.keys(secExp).forEach(k => result.sectorExposure.data[k] = Number(((secExp[k]/totalInv)*100).toFixed(2)));
313
+ Object.entries(assetExp)
314
+ .sort((a,b) => b[1] - a[1])
315
+ .slice(0, 10)
316
+ .forEach(([k,v]) => result.assetExposure.data[k] = Number(((v/totalInv)*100).toFixed(2)));
317
+ }
318
+ }
319
+
320
+ this.setResult(entityId, result);
321
+ }
322
+ }
323
+
324
+ module.exports = SignedInUserProfileMetrics;