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,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
|