bulltrackers-module 1.0.766 → 1.0.768

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 (32) hide show
  1. package/functions/computation-system-v2/computations/BehavioralAnomaly.js +298 -186
  2. package/functions/computation-system-v2/computations/NewSectorExposure.js +82 -35
  3. package/functions/computation-system-v2/computations/NewSocialPost.js +52 -24
  4. package/functions/computation-system-v2/computations/PopularInvestorProfileMetrics.js +354 -641
  5. package/functions/computation-system-v2/config/bulltrackers.config.js +26 -14
  6. package/functions/computation-system-v2/framework/core/Manifest.js +9 -16
  7. package/functions/computation-system-v2/framework/core/RunAnalyzer.js +2 -1
  8. package/functions/computation-system-v2/framework/data/DataFetcher.js +142 -4
  9. package/functions/computation-system-v2/framework/execution/Orchestrator.js +18 -31
  10. package/functions/computation-system-v2/framework/storage/StorageManager.js +7 -17
  11. package/functions/computation-system-v2/framework/testing/ComputationTester.js +155 -66
  12. package/functions/computation-system-v2/scripts/test-computation-dag.js +109 -0
  13. package/functions/task-engine/helpers/data_storage_helpers.js +6 -6
  14. package/package.json +1 -1
  15. package/functions/computation-system-v2/computations/PopularInvestorRiskAssessment.js +0 -176
  16. package/functions/computation-system-v2/computations/PopularInvestorRiskMetrics.js +0 -294
  17. package/functions/computation-system-v2/computations/UserPortfolioSummary.js +0 -172
  18. package/functions/computation-system-v2/scripts/migrate-sectors.js +0 -73
  19. package/functions/computation-system-v2/test/analyze-results.js +0 -238
  20. package/functions/computation-system-v2/test/other/test-dependency-cascade.js +0 -150
  21. package/functions/computation-system-v2/test/other/test-dispatcher.js +0 -317
  22. package/functions/computation-system-v2/test/other/test-framework.js +0 -500
  23. package/functions/computation-system-v2/test/other/test-real-execution.js +0 -166
  24. package/functions/computation-system-v2/test/other/test-real-integration.js +0 -194
  25. package/functions/computation-system-v2/test/other/test-refactor-e2e.js +0 -131
  26. package/functions/computation-system-v2/test/other/test-results.json +0 -31
  27. package/functions/computation-system-v2/test/other/test-risk-metrics-computation.js +0 -329
  28. package/functions/computation-system-v2/test/other/test-scheduler.js +0 -204
  29. package/functions/computation-system-v2/test/other/test-storage.js +0 -449
  30. package/functions/computation-system-v2/test/run-pipeline-test.js +0 -554
  31. package/functions/computation-system-v2/test/test-full-pipeline.js +0 -227
  32. package/functions/computation-system-v2/test/test-worker-pool.js +0 -266
@@ -1,294 +0,0 @@
1
- /**
2
- * @fileoverview Popular Investor Risk Metrics Computation
3
- *
4
- * Calculates comprehensive risk metrics for each popular investor:
5
- * - Sharpe Ratio (risk-adjusted returns)
6
- * - Sortino Ratio (downside risk)
7
- * - Max Drawdown
8
- * - Win Ratio
9
- * - Portfolio concentration
10
- *
11
- * This computation demonstrates:
12
- * - Using both metrics and portfolio rules
13
- * - Per-entity computation pattern
14
- * - Dual storage (BigQuery + Firestore)
15
- * - User-centric Firestore path
16
- */
17
-
18
- const { Computation } = require('../framework');
19
-
20
- class PopularInvestorRiskMetrics extends Computation {
21
- static getConfig() {
22
- return {
23
- name: 'PopularInvestorRiskMetrics',
24
- category: 'risk_analytics',
25
-
26
- // Data requirements
27
- requires: {
28
- 'portfolio_snapshots': {
29
- lookback: 30, // Need 30 days of history for risk metrics
30
- mandatory: true,
31
- filter: { user_type: 'POPULAR_INVESTOR' }
32
- },
33
- 'trade_history_snapshots': {
34
- lookback: 30,
35
- mandatory: false // Optional, used for trade-based metrics
36
- }
37
- },
38
-
39
- // No computation dependencies
40
- dependencies: [],
41
-
42
- // Per-entity means we process each user separately
43
- type: 'per-entity',
44
-
45
- // Keep 90 days of history
46
- ttlDays: 90,
47
-
48
- // Schedule: Run daily at 3 AM
49
- schedule: {
50
- frequency: 'daily',
51
- time: '03:00'
52
- },
53
-
54
- // =========================================================================
55
- // STORAGE: Write to both BigQuery and Firestore
56
- // =========================================================================
57
- storage: {
58
- // BigQuery: for analytics queries and as dependency for other computations
59
- bigquery: true,
60
-
61
- // Firestore: for real-time frontend access
62
- // Each user gets their risk metrics in their user document
63
- firestore: {
64
- enabled: true,
65
- path: '/users/{entityId}/computations/risk_metrics',
66
- merge: false, // Overwrite the entire document
67
- includeMetadata: true // Include _computedAt, _codeHash, etc.
68
- }
69
- }
70
- };
71
- }
72
-
73
- static getSchema() {
74
- return {
75
- type: 'object',
76
- properties: {
77
- userId: { type: 'string' },
78
- date: { type: 'string' },
79
-
80
- // Portfolio overview
81
- portfolioValue: { type: 'number' },
82
- positionCount: { type: 'number' },
83
-
84
- // Risk metrics
85
- sharpeRatio: { type: 'number' },
86
- sortinoRatio: { type: 'number' },
87
- maxDrawdown: { type: 'number' },
88
- valueAtRisk95: { type: 'number' },
89
-
90
- // Performance metrics
91
- winRatio: { type: 'number' },
92
- avgDailyReturn: { type: 'number' },
93
- returnVolatility: { type: 'number' },
94
-
95
- // Concentration metrics
96
- top3Concentration: { type: 'number' },
97
-
98
- // Risk grade (A-F)
99
- riskGrade: { type: 'string' }
100
- },
101
- required: ['userId', 'date', 'riskGrade']
102
- };
103
- }
104
-
105
- static getWeight() {
106
- return 2.0; // Higher weight = more expensive
107
- }
108
-
109
- /**
110
- * Process a single investor's risk metrics.
111
- */
112
- async process(context) {
113
- const { date, entityId, data, rules } = context;
114
-
115
- const portfolioHistory = data['portfolio_snapshots'];
116
-
117
- // Need portfolio history to calculate risk metrics
118
- if (!portfolioHistory || !Array.isArray(portfolioHistory) || portfolioHistory.length === 0) {
119
- this.log('DEBUG', `No portfolio history for user ${entityId}`);
120
- this.setResult(entityId, this._emptyResult(entityId, date));
121
- return;
122
- }
123
-
124
- // Sort by date ascending
125
- const sortedHistory = [...portfolioHistory].sort((a, b) =>
126
- new Date(a.date || a.snapshot_date) - new Date(b.date || b.snapshot_date)
127
- );
128
-
129
- // =====================================================================
130
- // STEP 1: Extract daily portfolio values and calculate returns
131
- // =====================================================================
132
-
133
- const dailyValues = sortedHistory.map(snapshot => {
134
- const positions = rules.portfolio.extractPositions(snapshot);
135
- return rules.portfolio.calculateTotalValue(positions);
136
- });
137
-
138
- // Calculate daily returns (percentage change)
139
- const dailyReturns = [];
140
- for (let i = 1; i < dailyValues.length; i++) {
141
- if (dailyValues[i - 1] > 0) {
142
- const returnPct = ((dailyValues[i] - dailyValues[i - 1]) / dailyValues[i - 1]) * 100;
143
- dailyReturns.push(returnPct);
144
- }
145
- }
146
-
147
- // Need at least a few returns for meaningful metrics
148
- if (dailyReturns.length < 5) {
149
- this.log('DEBUG', `Insufficient return history for user ${entityId}: ${dailyReturns.length} days`);
150
- this.setResult(entityId, this._emptyResult(entityId, date));
151
- return;
152
- }
153
-
154
- // =====================================================================
155
- // STEP 2: Calculate risk metrics using rules.metrics
156
- // =====================================================================
157
-
158
- // Sharpe Ratio (risk-free rate = 4% annualized)
159
- const sharpeRatio = rules.metrics.calculateSharpeRatio(dailyReturns, 4, 252);
160
-
161
- // Sortino Ratio (target return = 0)
162
- const sortinoRatio = rules.metrics.calculateSortinoRatio(dailyReturns, 0, 252);
163
-
164
- // Max Drawdown
165
- const { maxDrawdown } = rules.metrics.calculateMaxDrawdown(dailyValues);
166
-
167
- // Value at Risk (95% confidence)
168
- const valueAtRisk95 = rules.metrics.calculateVaR(dailyReturns, 0.95);
169
-
170
- // Basic statistics
171
- const avgDailyReturn = rules.metrics.mean(dailyReturns);
172
- const returnVolatility = rules.metrics.standardDeviation(dailyReturns);
173
-
174
- // =====================================================================
175
- // STEP 3: Calculate portfolio metrics using rules.portfolio
176
- // =====================================================================
177
-
178
- // Get latest portfolio snapshot
179
- const latestSnapshot = sortedHistory[sortedHistory.length - 1];
180
- const positions = rules.portfolio.extractPositions(latestSnapshot);
181
-
182
- const portfolioValue = rules.portfolio.calculateTotalValue(positions);
183
- const positionCount = positions.length;
184
- const winRatio = rules.portfolio.calculateWinRatio(positions);
185
-
186
- // Calculate concentration (top 3 positions as % of portfolio)
187
- const top3 = rules.portfolio.getTopPositions(positions, 3);
188
- const top3Value = top3.reduce((sum, pos) => sum + rules.portfolio.getValue(pos), 0);
189
- const top3Concentration = portfolioValue > 0 ? (top3Value / portfolioValue) * 100 : 0;
190
-
191
- // =====================================================================
192
- // STEP 4: Calculate risk grade (A-F based on combined metrics)
193
- // =====================================================================
194
-
195
- const riskGrade = this._calculateRiskGrade({
196
- sharpeRatio,
197
- sortinoRatio,
198
- maxDrawdown,
199
- winRatio,
200
- top3Concentration
201
- }, rules);
202
-
203
- // =====================================================================
204
- // STEP 5: Build and store result
205
- // =====================================================================
206
-
207
- this.setResult(entityId, {
208
- userId: entityId,
209
- date,
210
-
211
- // Portfolio overview
212
- portfolioValue: rules.metrics.round(portfolioValue, 2),
213
- positionCount,
214
-
215
- // Risk metrics (annualized where applicable)
216
- sharpeRatio: rules.metrics.round(sharpeRatio, 3),
217
- sortinoRatio: rules.metrics.round(sortinoRatio, 3),
218
- maxDrawdown: rules.metrics.round(maxDrawdown, 2),
219
- valueAtRisk95: rules.metrics.round(valueAtRisk95, 2),
220
-
221
- // Performance
222
- winRatio: rules.metrics.round(winRatio, 4),
223
- avgDailyReturn: rules.metrics.round(avgDailyReturn, 4),
224
- returnVolatility: rules.metrics.round(returnVolatility, 4),
225
-
226
- // Concentration
227
- top3Concentration: rules.metrics.round(top3Concentration, 2),
228
-
229
- // Overall grade
230
- riskGrade,
231
-
232
- // Metadata for debugging
233
- daysAnalyzed: dailyReturns.length + 1
234
- });
235
- }
236
-
237
- /**
238
- * Calculate a risk grade from A (best) to F (worst).
239
- */
240
- _calculateRiskGrade(metrics, rules) {
241
- let score = 50; // Start at middle
242
-
243
- // Sharpe Ratio: > 2 is excellent, < 0.5 is poor
244
- if (metrics.sharpeRatio > 2) score += 20;
245
- else if (metrics.sharpeRatio > 1) score += 10;
246
- else if (metrics.sharpeRatio < 0.5) score -= 10;
247
- else if (metrics.sharpeRatio < 0) score -= 20;
248
-
249
- // Max Drawdown: < 10% is good, > 30% is poor
250
- if (metrics.maxDrawdown < 10) score += 15;
251
- else if (metrics.maxDrawdown < 20) score += 5;
252
- else if (metrics.maxDrawdown > 30) score -= 15;
253
- else if (metrics.maxDrawdown > 50) score -= 25;
254
-
255
- // Win Ratio: > 60% is good, < 40% is poor
256
- if (metrics.winRatio > 0.6) score += 10;
257
- else if (metrics.winRatio < 0.4) score -= 10;
258
-
259
- // Concentration: > 50% in top 3 is risky
260
- if (metrics.top3Concentration > 70) score -= 15;
261
- else if (metrics.top3Concentration > 50) score -= 5;
262
- else if (metrics.top3Concentration < 30) score += 5;
263
-
264
- // Clamp and convert to grade
265
- score = rules.metrics.clamp(score, 0, 100);
266
-
267
- if (score >= 80) return 'A';
268
- if (score >= 65) return 'B';
269
- if (score >= 50) return 'C';
270
- if (score >= 35) return 'D';
271
- return 'F';
272
- }
273
-
274
- _emptyResult(entityId, date) {
275
- return {
276
- userId: entityId,
277
- date,
278
- portfolioValue: 0,
279
- positionCount: 0,
280
- sharpeRatio: 0,
281
- sortinoRatio: 0,
282
- maxDrawdown: 0,
283
- valueAtRisk95: 0,
284
- winRatio: 0,
285
- avgDailyReturn: 0,
286
- returnVolatility: 0,
287
- top3Concentration: 0,
288
- riskGrade: 'N/A',
289
- daysAnalyzed: 0
290
- };
291
- }
292
- }
293
-
294
- module.exports = PopularInvestorRiskMetrics;
@@ -1,172 +0,0 @@
1
- /**
2
- * @fileoverview User Portfolio Summary - v2 Computation
3
- *
4
- * This computation demonstrates the "simple recipe" pattern:
5
- * - No complex calculations inline
6
- * - Uses injected rules for all business logic
7
- * - The computation itself just orchestrates
8
- *
9
- * When rules.portfolio changes, this computation automatically re-runs
10
- * because the rules hash is included in the computation hash.
11
- */
12
-
13
- const { Computation } = require('../framework');
14
-
15
- class UserPortfolioSummary extends Computation {
16
- /**
17
- * Configuration - What the framework needs to know.
18
- */
19
- static getConfig() {
20
- return {
21
- name: 'UserPortfolioSummary',
22
- category: 'analytics',
23
-
24
- requires: {
25
- 'portfolio_snapshots': {
26
- lookback: 0,
27
- mandatory: true,
28
- filter: { user_type: 'POPULAR_INVESTOR' }
29
- },
30
- 'asset_prices': {
31
- lookback: 0,
32
- mandatory: true
33
- }
34
- },
35
-
36
- dependencies: [],
37
- type: 'per-entity',
38
- ttlDays: 30,
39
-
40
- // =========================================================================
41
- // STORAGE CONFIGURATION
42
- // =========================================================================
43
- // Controls where computation results are persisted.
44
- // BigQuery is always enabled unless explicitly set to false.
45
- // Firestore is opt-in for frontend-facing, user-centric data.
46
- // =========================================================================
47
- storage: {
48
- // BigQuery: always enabled by default (for analytics, dependency resolution)
49
- bigquery: true,
50
-
51
- // Firestore: user-centric storage for real-time frontend access
52
- firestore: {
53
- enabled: true,
54
- // Path template - {entityId} replaced with user ID, {date} with computation date
55
- // This stores each user's portfolio summary in their user document
56
- path: '/users/{entityId}/computations/portfolio_summary',
57
-
58
- // merge: false = overwrite entire document (default)
59
- // merge: true = merge with existing fields
60
- merge: false,
61
-
62
- // Include metadata (_computedAt, _computationDate, _codeHash)
63
- includeMetadata: true
64
- }
65
- }
66
- };
67
- }
68
-
69
- static getSchema() {
70
- return {
71
- type: 'object',
72
- properties: {
73
- userId: { type: 'string' },
74
- date: { type: 'string' },
75
- totalValue: { type: 'number' },
76
- totalInvested: { type: 'number' },
77
- profitLoss: { type: 'number' },
78
- profitLossPercent: { type: 'number' },
79
- positionCount: { type: 'number' },
80
- winRatio: { type: 'number' },
81
- topPositions: { type: 'array' }
82
- },
83
- required: ['userId', 'date', 'totalValue']
84
- };
85
- }
86
-
87
- static getWeight() {
88
- return 1.5;
89
- }
90
-
91
- /**
92
- * Process a single user's portfolio.
93
- *
94
- * NOTE: This is a "simple recipe" - it uses injected rules for all
95
- * business logic. The computation just orchestrates the rules.
96
- *
97
- * Benefits:
98
- * - Rules are the single source of truth
99
- * - When rules change, this computation re-runs automatically
100
- * - Easy to understand what calculations are being done
101
- * - Easy to test rules in isolation
102
- */
103
- async process(context) {
104
- const { date, entityId, data, rules } = context;
105
-
106
- const portfolio = data['portfolio_snapshots'];
107
- const prices = data['asset_prices'];
108
-
109
- // No data = no result
110
- if (!portfolio) {
111
- this.log('DEBUG', `No portfolio for user ${entityId}`);
112
- return;
113
- }
114
-
115
- // =====================================================================
116
- // USE INJECTED RULES - No inline business logic!
117
- // =====================================================================
118
-
119
- // Extract positions using portfolio rules
120
- const positions = rules.portfolio.extractPositions(portfolio);
121
-
122
- if (!positions || positions.length === 0) {
123
- this.setResult(entityId, this._emptyResult(entityId, date));
124
- return;
125
- }
126
-
127
- // Calculate portfolio metrics using portfolio rules
128
- const totalValue = rules.portfolio.calculateTotalValue(positions);
129
- const totalInvested = rules.portfolio.calculateTotalInvested(positions);
130
- const profitLossPercent = rules.portfolio.calculateWeightedProfitPercent(positions);
131
- const winRatio = rules.portfolio.calculateWinRatio(positions);
132
- const topPositions = rules.portfolio.getTopPositions(positions, 5);
133
-
134
- // Use metrics rules for rounding
135
- const profitLoss = rules.metrics.round(totalValue - totalInvested, 2);
136
-
137
- // Build result
138
- this.setResult(entityId, {
139
- userId: entityId,
140
- date,
141
- totalValue: rules.metrics.round(totalValue, 2),
142
- totalInvested: rules.metrics.round(totalInvested, 2),
143
- profitLoss,
144
- profitLossPercent: rules.metrics.round(profitLossPercent, 2),
145
- positionCount: positions.length,
146
- winRatio: rules.metrics.round(winRatio, 4),
147
- topPositions: topPositions.map(pos => ({
148
- instrumentId: rules.portfolio.getInstrumentId(pos),
149
- value: rules.metrics.round(rules.portfolio.getValue(pos), 2),
150
- percentOfPortfolio: totalValue > 0
151
- ? rules.metrics.round((rules.portfolio.getValue(pos) / totalValue) * 100, 2)
152
- : 0
153
- }))
154
- });
155
- }
156
-
157
- _emptyResult(entityId, date) {
158
- return {
159
- userId: entityId,
160
- date,
161
- totalValue: 0,
162
- totalInvested: 0,
163
- profitLoss: 0,
164
- profitLossPercent: 0,
165
- positionCount: 0,
166
- winRatio: 0,
167
- topPositions: []
168
- };
169
- }
170
- }
171
-
172
- module.exports = UserPortfolioSummary;
@@ -1,73 +0,0 @@
1
- const admin = require('firebase-admin');
2
- const { BigQuery } = require('@google-cloud/bigquery');
3
-
4
- // Configuration
5
- const PROJECT_ID = process.env.GCP_PROJECT_ID || 'stocks-12345';
6
- const DATASET_ID = 'bulltrackers_data'; // Check your config
7
- const TABLE_ID = 'sector_mappings';
8
- const FIRESTORE_PATH = 'instrument_sector_mappings/sector_mappings';
9
-
10
- async function migrate() {
11
- console.log('🚀 Starting Sector Migration...');
12
-
13
- // 1. Initialize Firestore
14
- // Note: Assuming Application Default Credentials work.
15
- // If running locally, ensure you've done: gcloud auth application-default login
16
- admin.initializeApp({ projectId: PROJECT_ID });
17
- const db = admin.firestore();
18
-
19
- // 2. Fetch Firestore Data
20
- console.log(`Reading Firestore doc: ${FIRESTORE_PATH}...`);
21
- const docRef = db.doc(FIRESTORE_PATH);
22
- const doc = await docRef.get();
23
-
24
- if (!doc.exists) {
25
- throw new Error('Firestore document not found!');
26
- }
27
-
28
- const data = doc.data();
29
- const rows = Object.entries(data).map(([ticker, sector]) => ({
30
- symbol: ticker,
31
- sector: sector || 'N/A'
32
- }));
33
-
34
- console.log(`✅ Found ${rows.length} mappings.`);
35
-
36
- // 3. Initialize BigQuery
37
- const bigquery = new BigQuery({ projectId: PROJECT_ID });
38
- const dataset = bigquery.dataset(DATASET_ID);
39
- const table = dataset.table(TABLE_ID);
40
-
41
- // 4. Create Table (if not exists)
42
- const schema = [
43
- { name: 'symbol', type: 'STRING', mode: 'REQUIRED' },
44
- { name: 'sector', type: 'STRING', mode: 'NULLABLE' }
45
- ];
46
-
47
- const [tableExists] = await table.exists();
48
-
49
- if (!tableExists) {
50
- console.log(`Creating table ${TABLE_ID}...`);
51
- await table.create({ schema });
52
- } else {
53
- console.log(`Table ${TABLE_ID} exists. Truncating...`);
54
- // We truncate to ensure a clean slate (idempotent migration)
55
- await bigquery.query(`TRUNCATE TABLE \`${PROJECT_ID}.${DATASET_ID}.${TABLE_ID}\``);
56
- }
57
-
58
- // 5. Insert Data
59
- if (rows.length > 0) {
60
- console.log('Inserting rows into BigQuery...');
61
- // Insert in chunks to be safe
62
- const chunkSize = 1000;
63
- for (let i = 0; i < rows.length; i += chunkSize) {
64
- const chunk = rows.slice(i, i + chunkSize);
65
- await table.insert(chunk);
66
- console.log(` Inserted rows ${i + 1} to ${i + chunk.length}`);
67
- }
68
- }
69
-
70
- console.log('🎉 Migration complete!');
71
- }
72
-
73
- migrate().catch(console.error);