bulltrackers-module 1.0.766 → 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 +559 -227
- package/functions/computation-system-v2/computations/GlobalAumPerAsset30D.js +103 -0
- package/functions/computation-system-v2/computations/NewSectorExposure.js +82 -35
- package/functions/computation-system-v2/computations/NewSocialPost.js +52 -24
- 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/PopularInvestorProfileMetrics.js +354 -641
- 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 +40 -126
- 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/core/Manifest.js +9 -16
- package/functions/computation-system-v2/framework/core/RunAnalyzer.js +2 -1
- package/functions/computation-system-v2/framework/data/DataFetcher.js +330 -126
- 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 +226 -153
- 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 +111 -83
- package/functions/computation-system-v2/framework/testing/ComputationTester.js +161 -66
- 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-computation-dag.js +109 -0
- package/functions/computation-system-v2/scripts/test-invalidation-scenarios.js +234 -0
- package/functions/task-engine/helpers/data_storage_helpers.js +6 -6
- package/package.json +1 -1
- package/functions/computation-system-v2/computations/PopularInvestorRiskAssessment.js +0 -176
- package/functions/computation-system-v2/computations/PopularInvestorRiskMetrics.js +0 -294
- package/functions/computation-system-v2/computations/UserPortfolioSummary.js +0 -172
- package/functions/computation-system-v2/scripts/migrate-sectors.js +0 -73
- package/functions/computation-system-v2/test/analyze-results.js +0 -238
- package/functions/computation-system-v2/test/other/test-dependency-cascade.js +0 -150
- package/functions/computation-system-v2/test/other/test-dispatcher.js +0 -317
- package/functions/computation-system-v2/test/other/test-framework.js +0 -500
- package/functions/computation-system-v2/test/other/test-real-execution.js +0 -166
- package/functions/computation-system-v2/test/other/test-real-integration.js +0 -194
- package/functions/computation-system-v2/test/other/test-refactor-e2e.js +0 -131
- package/functions/computation-system-v2/test/other/test-results.json +0 -31
- package/functions/computation-system-v2/test/other/test-risk-metrics-computation.js +0 -329
- package/functions/computation-system-v2/test/other/test-scheduler.js +0 -204
- package/functions/computation-system-v2/test/other/test-storage.js +0 -449
- package/functions/computation-system-v2/test/run-pipeline-test.js +0 -554
- package/functions/computation-system-v2/test/test-full-pipeline.js +0 -227
- package/functions/computation-system-v2/test/test-worker-pool.js +0 -266
|
@@ -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
|
|
@@ -39,12 +39,9 @@ class ManifestBuilder {
|
|
|
39
39
|
if (entry) {
|
|
40
40
|
manifestMap.set(entry.name, entry);
|
|
41
41
|
|
|
42
|
-
// CRITICAL FIX: Include conditional dependencies in the DAG for cycle detection and topological sort.
|
|
43
|
-
// Even if the dependency is conditional at runtime, the execution order (Pass) must respect it.
|
|
44
42
|
const graphDeps = [...entry.dependencies];
|
|
45
43
|
if (entry.conditionalDependencies) {
|
|
46
44
|
entry.conditionalDependencies.forEach(cd => {
|
|
47
|
-
// Ensure we use the normalized name for the graph
|
|
48
45
|
graphDeps.push(cd.computation);
|
|
49
46
|
});
|
|
50
47
|
}
|
|
@@ -60,7 +57,7 @@ class ManifestBuilder {
|
|
|
60
57
|
throw new Error(`[Manifest] Circular dependency detected: ${cycle}`);
|
|
61
58
|
}
|
|
62
59
|
|
|
63
|
-
// 3. Topological Sort
|
|
60
|
+
// 3. Topological Sort
|
|
64
61
|
const sortedItems = Graph.topologicalSort(nodes, adjacency);
|
|
65
62
|
|
|
66
63
|
// 4. Hydrate Sorted List
|
|
@@ -115,8 +112,6 @@ class ManifestBuilder {
|
|
|
115
112
|
compositeHash += `|RULE:${mod}:${h}`;
|
|
116
113
|
}
|
|
117
114
|
|
|
118
|
-
// Normalize conditional dependencies if they exist
|
|
119
|
-
// This ensures the Orchestrator can look them up by normalized name later
|
|
120
115
|
const conditionalDependencies = (config.conditionalDependencies || []).map(cd => ({
|
|
121
116
|
...cd,
|
|
122
117
|
computation: this._normalize(cd.computation)
|
|
@@ -128,16 +123,20 @@ class ManifestBuilder {
|
|
|
128
123
|
class: ComputationClass,
|
|
129
124
|
category: config.category || 'default',
|
|
130
125
|
type: config.type || 'global',
|
|
126
|
+
|
|
127
|
+
outputTable: config.outputTable || null,
|
|
128
|
+
// --------------------------------------
|
|
129
|
+
|
|
131
130
|
requires: config.requires || {},
|
|
132
131
|
dependencies: (config.dependencies || []).map(d => this._normalize(d)),
|
|
133
|
-
conditionalDependencies,
|
|
132
|
+
conditionalDependencies,
|
|
134
133
|
isHistorical: config.isHistorical || false,
|
|
135
134
|
isTest: config.isTest || false,
|
|
136
135
|
schedule: this.scheduleValidator.parseSchedule(config.schedule),
|
|
137
136
|
storage: this._parseStorageConfig(config.storage),
|
|
138
137
|
ttlDays: config.ttlDays,
|
|
139
|
-
pass: 0,
|
|
140
|
-
hash: this._hashCode(compositeHash),
|
|
138
|
+
pass: 0,
|
|
139
|
+
hash: this._hashCode(compositeHash),
|
|
141
140
|
weight: ComputationClass.getWeight ? ComputationClass.getWeight() : 1.0,
|
|
142
141
|
composition: {
|
|
143
142
|
epoch: this.epoch,
|
|
@@ -152,7 +151,6 @@ class ManifestBuilder {
|
|
|
152
151
|
_computeFinalHashes(sorted, manifestMap) {
|
|
153
152
|
for (const entry of sorted) {
|
|
154
153
|
let hashInput = entry.hash;
|
|
155
|
-
// Includes strict dependencies in the hash chain
|
|
156
154
|
if (entry.dependencies.length > 0) {
|
|
157
155
|
const depHashes = entry.dependencies.sort().map(d => {
|
|
158
156
|
const h = manifestMap.get(d)?.hash;
|
|
@@ -161,10 +159,6 @@ class ManifestBuilder {
|
|
|
161
159
|
});
|
|
162
160
|
hashInput += `|DEPS:${depHashes.join('|')}`;
|
|
163
161
|
}
|
|
164
|
-
// Note: Conditional dependencies are currently excluded from the hash chain
|
|
165
|
-
// because they might not be loaded. If strict versioning is required for them,
|
|
166
|
-
// they should be added here too.
|
|
167
|
-
|
|
168
162
|
entry.hash = this._hashCode(hashInput);
|
|
169
163
|
}
|
|
170
164
|
}
|
|
@@ -203,7 +197,7 @@ class ManifestBuilder {
|
|
|
203
197
|
const used = {};
|
|
204
198
|
for (const [name, exports] of Object.entries(this.sharedLayers)) {
|
|
205
199
|
const found = Object.keys(exports).filter(exp =>
|
|
206
|
-
code.includes(exp)
|
|
200
|
+
code.includes(exp)
|
|
207
201
|
);
|
|
208
202
|
if (found.length) used[name] = found;
|
|
209
203
|
}
|
|
@@ -240,7 +234,6 @@ class ManifestBuilder {
|
|
|
240
234
|
|
|
241
235
|
_log(l, m) { this.logger ? this.logger.log(l, `[Manifest] ${m}`) : console.log(`[Manifest] ${m}`); }
|
|
242
236
|
|
|
243
|
-
// Public alias for groupByPass matching the Interface
|
|
244
237
|
groupByPass(m) { return this._groupByPass(m); }
|
|
245
238
|
}
|
|
246
239
|
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* @fileoverview Run Analyzer
|
|
3
3
|
* * Pure logic component that determines which computations need to run.
|
|
4
4
|
* Decouples decision-making from execution and storage.
|
|
5
|
+
* * * UPDATE: Removed SQL bypass. All computations are now checked for data availability.
|
|
5
6
|
*/
|
|
6
7
|
|
|
7
8
|
class RunAnalyzer {
|
|
@@ -57,7 +58,7 @@ class RunAnalyzer {
|
|
|
57
58
|
}
|
|
58
59
|
|
|
59
60
|
// 2. Data Availability Check
|
|
60
|
-
//
|
|
61
|
+
// UPDATE: Removed isSql check. All computations must have their raw data available.
|
|
61
62
|
const availability = await this.dataFetcher.checkAvailability(requires, dateStr);
|
|
62
63
|
if (!availability.canRun) {
|
|
63
64
|
if (!isToday) {
|