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.
Files changed (51) 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/SignedInUserList.js +51 -0
  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/legacy/PiAssetRecommender.js.old +115 -0
  47. package/functions/computation-system-v2/legacy/PiSimilarityMatrix.js +104 -0
  48. package/functions/computation-system-v2/legacy/PiSimilarityVector.js +71 -0
  49. package/functions/computation-system-v2/scripts/debug_aggregation.js +25 -0
  50. package/functions/computation-system-v2/scripts/test-invalidation-scenarios.js +234 -0
  51. package/package.json +1 -1
@@ -0,0 +1,602 @@
1
+ # Developing Computations for the BullTrackers DAG System
2
+
3
+ ## Overview
4
+
5
+ The computation system is a **directed acyclic graph (DAG)** of data transformations that run on BigQuery data. Each computation is a self-contained class that declares its data requirements, dependencies, and processing logic.
6
+
7
+ ---
8
+
9
+ ## Core Concepts
10
+
11
+ ### 1. **Computation Types**
12
+
13
+ ```javascript
14
+ type: 'per-entity' // Runs once per entity (user, asset, etc.)
15
+ type: 'global' // Runs once for all data (aggregations, rankings)
16
+ ```
17
+
18
+ ### 2. **Result Keys**
19
+
20
+ ```javascript
21
+ // Per-entity computations
22
+ this.setResult(entityId, { ...data });
23
+
24
+ // Global computations
25
+ this.setResult('_global', { ...data });
26
+ ```
27
+
28
+ **Critical:** Global computations must use `'_global'` as the key, not `'global'`.
29
+
30
+ ---
31
+
32
+ ## Basic Computation Structure
33
+
34
+ ```javascript
35
+ const { Computation } = require('../framework');
36
+
37
+ class MyComputation extends Computation {
38
+ static getConfig() {
39
+ return {
40
+ name: 'MyComputation',
41
+ type: 'per-entity', // or 'global'
42
+ category: 'analytics',
43
+ isHistorical: false, // true if needs previous day's data
44
+
45
+ requires: {
46
+ 'table_name': {
47
+ lookback: 0, // days to fetch (0 = today only)
48
+ mandatory: true,
49
+ fields: ['col1', 'col2'], // REQUIRED for cost control
50
+ filter: { user_type: 'POPULAR_INVESTOR' }
51
+ }
52
+ },
53
+
54
+ dependencies: [], // Other computations needed
55
+
56
+ storage: {
57
+ bigquery: true,
58
+ firestore: {
59
+ enabled: true,
60
+ path: 'results/{entityId}',
61
+ merge: true
62
+ }
63
+ }
64
+ };
65
+ }
66
+
67
+ async process(context) {
68
+ const { data, entityId, date, rules, references } = context;
69
+
70
+ // Your logic here
71
+ const result = { /* ... */ };
72
+
73
+ this.setResult(entityId, result);
74
+ }
75
+ }
76
+
77
+ module.exports = MyComputation;
78
+ ```
79
+
80
+ ---
81
+
82
+ ## Cost Optimization Rules
83
+
84
+ ### **1. Always Specify Fields**
85
+
86
+ ```javascript
87
+ // ❌ BAD - Will throw LAZY SELECT BLOCKED error
88
+ requires: {
89
+ 'portfolio_snapshots': {
90
+ fields: [] // or null or '*'
91
+ }
92
+ }
93
+
94
+ // ✅ GOOD - Only fetch what you need
95
+ requires: {
96
+ 'portfolio_snapshots': {
97
+ fields: ['user_id', 'portfolio_data', 'date']
98
+ }
99
+ }
100
+ ```
101
+
102
+ ### **2. Always Use Partitioning**
103
+
104
+ ```javascript
105
+ // ❌ BAD - Full table scan
106
+ requires: {
107
+ 'portfolio_snapshots': {
108
+ lookback: 0 // Missing! Will throw UNSAFE QUERY error
109
+ }
110
+ }
111
+
112
+ // ✅ GOOD - Date partition filter
113
+ requires: {
114
+ 'portfolio_snapshots': {
115
+ lookback: 0, // Today only
116
+ fields: ['user_id', 'portfolio_data', 'date']
117
+ }
118
+ }
119
+ ```
120
+
121
+ ### **3. Limit Lookback**
122
+
123
+ ```javascript
124
+ // Maximum enforced: 60 days
125
+ lookback: 30 // 30 days of history (max enforced: 60)
126
+ ```
127
+
128
+ ### **4. Use Filters to Reduce Data**
129
+
130
+ ```javascript
131
+ requires: {
132
+ 'portfolio_snapshots': {
133
+ lookback: 0,
134
+ fields: ['user_id', 'portfolio_data', 'date'],
135
+ filter: {
136
+ user_type: 'POPULAR_INVESTOR' // Reduces rows significantly
137
+ }
138
+ }
139
+ }
140
+ ```
141
+
142
+ ---
143
+
144
+ ## Materialized Views
145
+
146
+ ### **Overview**
147
+
148
+ Materialized views enable efficient **metric-based queries** without requiring custom SQL. The system **automatically creates and manages** them.
149
+
150
+ ### **Requesting a Materialized View**
151
+
152
+ ```javascript
153
+ requires: {
154
+ 'aum_metric': {
155
+ type: 'metric', // Triggers MV creation
156
+ source: 'pi_rankings', // Base table
157
+ func: 'SUM', // Aggregation function
158
+ field: 'rankings_data', // Column to aggregate
159
+ jsonPath: '$.AUMValue', // JSON path (if needed)
160
+ lookback: 7,
161
+ series: true // true = time series, false = single value
162
+ }
163
+ }
164
+ ```
165
+
166
+ ### **What Happens**
167
+
168
+ 1. System generates MV name: `mv_pi_rankings_sum_rankings_data_by_day`
169
+ 2. Automatically creates MV with schema:
170
+ ```sql
171
+ CREATE MATERIALIZED VIEW mv_...
172
+ AS SELECT
173
+ entity_id,
174
+ date,
175
+ SUM(CAST(JSON_VALUE(field, '$.path') AS FLOAT64)) as value
176
+ FROM base_table
177
+ GROUP BY entity_id, date
178
+ ```
179
+ 3. Auto-refreshes every 60 minutes
180
+
181
+ ### **Accessing Results**
182
+
183
+ ```javascript
184
+ async process(context) {
185
+ const { data } = context;
186
+
187
+ // Series mode (series: true)
188
+ // Returns: { "entityId": { "2024-01-01": 123, "2024-01-02": 456 } }
189
+ const aumTimeSeries = data['aum_metric'];
190
+ const todayAum = aumTimeSeries[entityId][date];
191
+
192
+ // Single value mode (series: false)
193
+ // Returns: { "entityId": 123 }
194
+ const totalAum = data['aum_metric'][entityId];
195
+ }
196
+ ```
197
+
198
+ ### **Supported Functions**
199
+
200
+ - `SUM`, `AVG`, `MIN`, `MAX`, `COUNT`
201
+
202
+ ---
203
+
204
+ ## Dependencies Between Computations
205
+
206
+ ### **Basic Dependency**
207
+
208
+ ```javascript
209
+ class DownstreamComputation extends Computation {
210
+ static getConfig() {
211
+ return {
212
+ name: 'DownstreamComputation',
213
+ dependencies: ['UpstreamComputation'],
214
+ requires: {
215
+ 'computation_results': {
216
+ lookback: 0,
217
+ filter: {
218
+ computation_name: 'upstreamcomputation' // lowercase!
219
+ }
220
+ }
221
+ }
222
+ };
223
+ }
224
+
225
+ async process(context) {
226
+ const { getDependency, entityId } = context;
227
+
228
+ // Access dependency result
229
+ const upstreamResult = getDependency('UpstreamComputation', entityId);
230
+
231
+ // Use it
232
+ const value = upstreamResult.someField;
233
+ }
234
+ }
235
+ ```
236
+
237
+ ### **Conditional Dependencies**
238
+
239
+ ```javascript
240
+ conditionalDependencies: [
241
+ {
242
+ computation: 'SeasonalAdjustment',
243
+ condition: ({ date }) => {
244
+ const month = new Date(date).getMonth();
245
+ return month === 0; // Only in January
246
+ }
247
+ }
248
+ ]
249
+ ```
250
+
251
+ ---
252
+
253
+ ## Example Computations
254
+
255
+ ### **Example 1: Per-Entity Alert**
256
+
257
+ ```javascript
258
+ class RiskScoreIncrease extends Computation {
259
+ static getConfig() {
260
+ return {
261
+ name: 'RiskScoreIncrease',
262
+ type: 'per-entity',
263
+ category: 'alerts',
264
+ isHistorical: true, // Needs yesterday's data
265
+
266
+ requires: {
267
+ 'pi_rankings': {
268
+ lookback: 1, // Today + yesterday
269
+ mandatory: true,
270
+ fields: ['pi_id', 'rankings_data', 'date'],
271
+ filter: { user_type: 'POPULAR_INVESTOR' }
272
+ }
273
+ },
274
+
275
+ storage: {
276
+ bigquery: true,
277
+ firestore: {
278
+ enabled: true,
279
+ path: 'alerts/{date}/RiskScoreIncrease/{entityId}',
280
+ merge: true
281
+ }
282
+ }
283
+ };
284
+ }
285
+
286
+ async process(context) {
287
+ const { data, entityId, date, rules } = context;
288
+
289
+ const history = data['pi_rankings'] || [];
290
+ const todayRow = history.find(d => d.date === date);
291
+ const yesterdayRow = history.find(d => d.date === this._yesterday(date));
292
+
293
+ const todayRisk = rules.rankings.getRiskScore(
294
+ rules.rankings.extractRankingsData(todayRow)
295
+ );
296
+ const yesterdayRisk = rules.rankings.getRiskScore(
297
+ rules.rankings.extractRankingsData(yesterdayRow)
298
+ );
299
+
300
+ const change = todayRisk - (yesterdayRisk || 0);
301
+
302
+ this.setResult(entityId, {
303
+ currentRisk: todayRisk,
304
+ previousRisk: yesterdayRisk,
305
+ change,
306
+ triggered: change > 0
307
+ });
308
+ }
309
+
310
+ _yesterday(dateStr) {
311
+ const d = new Date(dateStr);
312
+ d.setDate(d.getDate() - 1);
313
+ return d.toISOString().slice(0, 10);
314
+ }
315
+ }
316
+ ```
317
+
318
+ ### **Example 2: Global Aggregation**
319
+
320
+ ```javascript
321
+ class GlobalAumPerAsset extends Computation {
322
+ static getConfig() {
323
+ return {
324
+ name: 'GlobalAumPerAsset',
325
+ type: 'global',
326
+ category: 'market_insights',
327
+
328
+ requires: {
329
+ 'computation_results': {
330
+ lookback: 0,
331
+ mandatory: true,
332
+ fields: ['result_data', 'computation_name', 'date'],
333
+ filter: {
334
+ computation_name: 'pidailyassetaum'
335
+ }
336
+ }
337
+ },
338
+
339
+ dependencies: ['PIDailyAssetAUM'],
340
+
341
+ storage: {
342
+ bigquery: true,
343
+ firestore: {
344
+ enabled: true,
345
+ path: 'global_metrics/aum_per_asset_{date}',
346
+ merge: true
347
+ }
348
+ }
349
+ };
350
+ }
351
+
352
+ async process(context) {
353
+ const { data } = context;
354
+
355
+ let rows = data['computation_results'];
356
+ if (!Array.isArray(rows)) {
357
+ rows = Object.values(rows);
358
+ }
359
+
360
+ const globalTotals = new Map();
361
+
362
+ for (const row of rows) {
363
+ let assetMap = row.result_data;
364
+ if (typeof assetMap === 'string') {
365
+ assetMap = JSON.parse(assetMap);
366
+ }
367
+
368
+ for (const [ticker, value] of Object.entries(assetMap)) {
369
+ const current = globalTotals.get(ticker) || 0;
370
+ globalTotals.set(ticker, current + value);
371
+ }
372
+ }
373
+
374
+ const result = Array.from(globalTotals.entries())
375
+ .map(([symbol, amount]) => ({ symbol, amount }))
376
+ .sort((a, b) => b.amount - a.amount);
377
+
378
+ this.setResult('_global', result); // Note: _global, not global
379
+ }
380
+ }
381
+ ```
382
+
383
+ ### **Example 3: Using Materialized Views**
384
+
385
+ ```javascript
386
+ class PortfolioVelocity extends Computation {
387
+ static getConfig() {
388
+ return {
389
+ name: 'PortfolioVelocity',
390
+ type: 'per-entity',
391
+ category: 'analytics',
392
+
393
+ requires: {
394
+ 'total_exposure_metric': {
395
+ type: 'metric',
396
+ source: 'portfolio_snapshots',
397
+ func: 'SUM',
398
+ field: 'portfolio_data',
399
+ jsonPath: '$.exposure',
400
+ lookback: 7,
401
+ series: true // Get 7 days of data
402
+ }
403
+ },
404
+
405
+ storage: { bigquery: true }
406
+ };
407
+ }
408
+
409
+ async process(context) {
410
+ const { data, entityId, date } = context;
411
+
412
+ const exposureSeries = data['total_exposure_metric'][entityId] || {};
413
+ const dates = Object.keys(exposureSeries).sort();
414
+
415
+ if (dates.length < 2) {
416
+ return this.setResult(entityId, { velocity: 0 });
417
+ }
418
+
419
+ // Calculate rate of change
420
+ const values = dates.map(d => exposureSeries[d]);
421
+ const diffs = [];
422
+ for (let i = 1; i < values.length; i++) {
423
+ diffs.push(values[i] - values[i-1]);
424
+ }
425
+
426
+ const avgVelocity = diffs.reduce((a, b) => a + b, 0) / diffs.length;
427
+
428
+ this.setResult(entityId, {
429
+ velocity: avgVelocity,
430
+ current: values[values.length - 1],
431
+ trend: avgVelocity > 0 ? 'increasing' : 'decreasing'
432
+ });
433
+ }
434
+ }
435
+ ```
436
+
437
+ ### **Example 4: Using Rules**
438
+
439
+ ```javascript
440
+ class PortfolioSummary extends Computation {
441
+ static getConfig() {
442
+ return {
443
+ name: 'PortfolioSummary',
444
+ type: 'per-entity',
445
+ category: 'analytics',
446
+
447
+ requires: {
448
+ 'portfolio_snapshots': {
449
+ lookback: 0,
450
+ mandatory: true,
451
+ fields: ['user_id', 'portfolio_data', 'date']
452
+ },
453
+ 'ticker_mappings': {
454
+ mandatory: false,
455
+ fields: ['instrument_id', 'ticker']
456
+ },
457
+ 'sector_mappings': {
458
+ mandatory: false,
459
+ fields: ['symbol', 'sector']
460
+ }
461
+ },
462
+
463
+ storage: { bigquery: true }
464
+ };
465
+ }
466
+
467
+ async process(context) {
468
+ const { data, entityId, rules, references } = context;
469
+
470
+ const portfolioRow = data['portfolio_snapshots'];
471
+ const portfolioData = rules.portfolio.extractPortfolioData(portfolioRow);
472
+ const positions = rules.portfolio.extractPositions(portfolioData);
473
+
474
+ // Use business rules for calculations
475
+ const totalValue = rules.portfolio.calculateTotalValue(positions);
476
+ const exposure = rules.portfolio.getExposure(portfolioData);
477
+ const winRatio = rules.portfolio.calculateWinRatio(positions);
478
+
479
+ // Access reference data
480
+ const tickerMap = this._buildTickerMap(references['ticker_mappings']);
481
+ const sectorMap = this._buildSectorMap(references['sector_mappings']);
482
+
483
+ // Calculate sector exposure
484
+ const sectorExposure = rules.portfolio.calculateSectorExposure(
485
+ positions,
486
+ this._buildInstrumentToSectorMap(tickerMap, sectorMap)
487
+ );
488
+
489
+ this.setResult(entityId, {
490
+ totalValue,
491
+ exposure,
492
+ winRatio,
493
+ sectorExposure,
494
+ positionCount: positions.length
495
+ });
496
+ }
497
+
498
+ _buildTickerMap(rows) {
499
+ const map = {};
500
+ Object.values(rows || {}).forEach(r => {
501
+ map[r.instrument_id] = r.ticker;
502
+ });
503
+ return map;
504
+ }
505
+
506
+ _buildSectorMap(rows) {
507
+ const map = {};
508
+ Object.values(rows || {}).forEach(r => {
509
+ map[r.symbol] = r.sector;
510
+ });
511
+ return map;
512
+ }
513
+
514
+ _buildInstrumentToSectorMap(tickerMap, sectorMap) {
515
+ const map = {};
516
+ for (const [instId, ticker] of Object.entries(tickerMap)) {
517
+ map[instId] = sectorMap[ticker] || 'Unknown';
518
+ }
519
+ return map;
520
+ }
521
+ }
522
+ ```
523
+
524
+ ---
525
+
526
+ ## Common Patterns
527
+
528
+ ### **Pattern 1: Date Comparison**
529
+
530
+ ```javascript
531
+ const toDateStr = (d) => {
532
+ if (d?.value) return d.value;
533
+ if (d instanceof Date) return d.toISOString().slice(0, 10);
534
+ return String(d);
535
+ };
536
+
537
+ const todayRow = history.find(d => toDateStr(d.date) === date);
538
+ ```
539
+
540
+ ### **Pattern 2: Safe Entity Row Extraction**
541
+
542
+ ```javascript
543
+ const getEntityRows = (dataset) => {
544
+ if (!dataset) return [];
545
+ if (dataset[entityId]) {
546
+ return Array.isArray(dataset[entityId])
547
+ ? dataset[entityId]
548
+ : [dataset[entityId]];
549
+ }
550
+ if (Array.isArray(dataset)) {
551
+ return dataset.filter(r =>
552
+ String(r.user_id || r.pi_id || r.cid) === String(entityId)
553
+ );
554
+ }
555
+ return [];
556
+ };
557
+ ```
558
+
559
+ ### **Pattern 3: JSON Parsing**
560
+
561
+ ```javascript
562
+ let data = row.data_field;
563
+ if (typeof data === 'string') {
564
+ try {
565
+ data = JSON.parse(data);
566
+ } catch (e) {
567
+ data = null;
568
+ }
569
+ }
570
+ ```
571
+
572
+ ---
573
+
574
+ ## Testing
575
+
576
+ ```javascript
577
+ const { IntegrationTester } = require('../framework/testing/ComputationTester');
578
+
579
+ async function testComputation() {
580
+ const tester = new IntegrationTester(config);
581
+
582
+ // Automatically finds runnable date
583
+ const date = await tester.findLatestRunnableDate('MyComputation');
584
+
585
+ // Runs computation + dependencies
586
+ const { result } = await tester.run('MyComputation', date);
587
+
588
+ console.log(result);
589
+ }
590
+ ```
591
+
592
+ ---
593
+
594
+ ## Key Takeaways
595
+
596
+ 1. **Always specify `fields`** - Prevents cost explosions
597
+ 2. **Always use date partitioning** - Required for safety
598
+ 3. **Use `_global` not `'global'`** - Critical for global computations
599
+ 4. **Leverage materialized views** - Automatic metric aggregation
600
+ 5. **Use business rules** - Centralized, tested logic
601
+ 6. **Reference data** - Use `references` for mappings
602
+ 7. **Test incrementally** - Use IntegrationTester