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
|
@@ -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;
|