bulltrackers-module 1.0.770 → 1.0.772
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/computations/GlobalAumPerAsset30D.js +11 -13
- package/functions/computation-system-v2/computations/PIDailyAssetAUM.js +17 -19
- package/functions/computation-system-v2/computations/PiFeatureVectors.js +44 -90
- package/functions/computation-system-v2/computations/PiRecommender.js +155 -226
- package/functions/computation-system-v2/computations/PopularInvestorProfileMetrics.js +20 -20
- package/functions/computation-system-v2/computations/SectorCorrelations.js +228 -0
- package/functions/computation-system-v2/computations/SignedInUserMirrorHistory.js +15 -32
- package/functions/computation-system-v2/computations/SignedInUserProfileMetrics.js +31 -31
- package/functions/computation-system-v2/framework/core/RunAnalyzer.js +3 -2
- package/functions/computation-system-v2/framework/data/DataFetcher.js +1 -1
- package/functions/computation-system-v2/framework/storage/StateRepository.js +13 -11
- package/functions/computation-system-v2/handlers/scheduler.js +43 -19
- package/package.json +1 -1
- package/functions/computation-system-v2/docs/Agents.MD +0 -964
|
@@ -1,964 +0,0 @@
|
|
|
1
|
-
# **Computation System v2**
|
|
2
|
-
|
|
3
|
-
Developer Manual
|
|
4
|
-
|
|
5
|
-
_A Domain-Agnostic DAG Framework for Data Computations_
|
|
6
|
-
|
|
7
|
-
# Table of Contents
|
|
8
|
-
|
|
9
|
-
This document covers:
|
|
10
|
-
|
|
11
|
-
- System Overview & Architecture
|
|
12
|
-
- Creating New Computations
|
|
13
|
-
- Configuration Reference (getConfig)
|
|
14
|
-
- Data Access & Context
|
|
15
|
-
- Business Rules System
|
|
16
|
-
- Testing & Debugging
|
|
17
|
-
- Best Practices & Common Patterns
|
|
18
|
-
|
|
19
|
-
# 1\. System Overview
|
|
20
|
-
|
|
21
|
-
The Computation System v2 is a domain-agnostic Directed Acyclic Graph (DAG) framework designed for managing complex data transformations and calculations. While the current implementation includes finance-specific business rules for the BullTrackers analytics platform, the framework itself is completely generic and can be adapted to any domain.
|
|
22
|
-
|
|
23
|
-
## 1.1 Core Principles
|
|
24
|
-
|
|
25
|
-
- Domain Agnostic: The framework has no built-in knowledge of finance or any specific domain
|
|
26
|
-
- Dependency-Driven: Computations declare their dependencies; the system manages execution order
|
|
27
|
-
- Smart Caching: Only re-runs when code, data, or dependencies change
|
|
28
|
-
- Scalable: Supports both in-memory and distributed (worker pool) execution
|
|
29
|
-
- Resilient: Automatic checkpointing and zombie detection for long-running tasks
|
|
30
|
-
|
|
31
|
-
## 1.2 Key Concepts
|
|
32
|
-
|
|
33
|
-
### Computations
|
|
34
|
-
|
|
35
|
-
A computation is a single unit of data processing. It:
|
|
36
|
-
|
|
37
|
-
- Declares what data it needs (requirements)
|
|
38
|
-
- Declares which other computations it depends on (dependencies)
|
|
39
|
-
- Implements a process() method containing the transformation logic
|
|
40
|
-
- Produces results that can be stored and used by other computations
|
|
41
|
-
|
|
42
|
-
### DAG (Directed Acyclic Graph)
|
|
43
|
-
|
|
44
|
-
The system automatically builds a dependency graph from computation declarations. This ensures:
|
|
45
|
-
|
|
46
|
-
- No circular dependencies (acyclic)
|
|
47
|
-
- Correct execution order (topological sort)
|
|
48
|
-
- Parallel execution of independent computations (passes)
|
|
49
|
-
|
|
50
|
-
### Passes
|
|
51
|
-
|
|
52
|
-
Computations are organized into execution passes based on their dependencies:
|
|
53
|
-
|
|
54
|
-
- Pass 1: Computations with no dependencies (root nodes)
|
|
55
|
-
- Pass 2: Computations that only depend on Pass 1
|
|
56
|
-
- Pass N: Computations whose longest dependency chain is N-1
|
|
57
|
-
|
|
58
|
-
All computations within a pass can run in parallel.
|
|
59
|
-
|
|
60
|
-
### Business Rules
|
|
61
|
-
|
|
62
|
-
Business rules are domain-specific functions that contain the actual business logic. They are:
|
|
63
|
-
|
|
64
|
-
- Organized into modules (e.g., portfolio, metrics, rankings)
|
|
65
|
-
- Automatically injected into computation contexts
|
|
66
|
-
- Version-tracked (changes trigger re-runs)
|
|
67
|
-
- Completely optional - framework works without them
|
|
68
|
-
|
|
69
|
-
# 2\. Creating New Computations
|
|
70
|
-
|
|
71
|
-
## 2.1 Basic Structure
|
|
72
|
-
|
|
73
|
-
Every computation follows this template:
|
|
74
|
-
|
|
75
|
-
const { Computation } = require('../framework');
|
|
76
|
-
|
|
77
|
-
class MyComputation extends Computation {
|
|
78
|
-
|
|
79
|
-
static getConfig() {
|
|
80
|
-
|
|
81
|
-
return {
|
|
82
|
-
|
|
83
|
-
name: 'MyComputation',
|
|
84
|
-
|
|
85
|
-
type: 'per-entity',
|
|
86
|
-
|
|
87
|
-
category: 'analytics',
|
|
88
|
-
|
|
89
|
-
requires: { /\* data requirements \*/ },
|
|
90
|
-
|
|
91
|
-
dependencies: \[\]
|
|
92
|
-
|
|
93
|
-
};
|
|
94
|
-
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
async process(context) {
|
|
98
|
-
|
|
99
|
-
const { data, entityId, rules } = context;
|
|
100
|
-
|
|
101
|
-
// Your logic here
|
|
102
|
-
|
|
103
|
-
this.setResult(entityId, result);
|
|
104
|
-
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
module.exports = MyComputation;
|
|
110
|
-
|
|
111
|
-
## 2.2 File Location
|
|
112
|
-
|
|
113
|
-
Save your computation file in:
|
|
114
|
-
|
|
115
|
-
computations/YourComputationName.js
|
|
116
|
-
|
|
117
|
-
The framework automatically discovers and loads all .js files in this directory (except those starting with underscore).
|
|
118
|
-
|
|
119
|
-
## 2.3 Computation Types
|
|
120
|
-
|
|
121
|
-
There are two computation types:
|
|
122
|
-
|
|
123
|
-
### Global Computations
|
|
124
|
-
|
|
125
|
-
Run once per date, processing all data together.
|
|
126
|
-
|
|
127
|
-
type: 'global'
|
|
128
|
-
|
|
129
|
-
Use cases: System-wide aggregations, rankings, summaries that need the full dataset.
|
|
130
|
-
|
|
131
|
-
async process(context) {
|
|
132
|
-
|
|
133
|
-
const { data } = context;
|
|
134
|
-
|
|
135
|
-
// Process all data at once
|
|
136
|
-
|
|
137
|
-
const result = analyzeAllData(data);
|
|
138
|
-
|
|
139
|
-
this.setGlobalResult(result);
|
|
140
|
-
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
### Per-Entity Computations
|
|
144
|
-
|
|
145
|
-
Run once for each entity (user, asset, etc.), enabling massive parallelization.
|
|
146
|
-
|
|
147
|
-
type: 'per-entity'
|
|
148
|
-
|
|
149
|
-
Use cases: User-specific calculations, per-asset analytics, individual reports.
|
|
150
|
-
|
|
151
|
-
async process(context) {
|
|
152
|
-
|
|
153
|
-
const { data, entityId, rules } = context;
|
|
154
|
-
|
|
155
|
-
// Process one entity's data
|
|
156
|
-
|
|
157
|
-
const result = calculateForEntity(data, entityId);
|
|
158
|
-
|
|
159
|
-
this.setResult(entityId, result);
|
|
160
|
-
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
# 3\. Configuration Reference (getConfig)
|
|
164
|
-
|
|
165
|
-
The getConfig() method returns an object defining the computation's behavior.
|
|
166
|
-
|
|
167
|
-
## 3.1 Required Fields
|
|
168
|
-
|
|
169
|
-
| **Field** | **Type** | **Description** | **Example** |
|
|
170
|
-
| --- | --- | --- | --- |
|
|
171
|
-
| name | string | Unique identifier for the computation | 'UserPortfolioSummary' |
|
|
172
|
-
| requires | object | Data requirements specification | See section 3.3 |
|
|
173
|
-
| type | string | Execution mode: 'global' or 'per-entity' | 'per-entity' |
|
|
174
|
-
|
|
175
|
-
## 3.2 Optional Fields
|
|
176
|
-
|
|
177
|
-
| **Field** | **Type** | **Default** | **Description** |
|
|
178
|
-
| --- | --- | --- | --- |
|
|
179
|
-
| category | string | 'default' | Organizational category |
|
|
180
|
-
| dependencies | array | \[\] | List of computation names this depends on |
|
|
181
|
-
| conditionalDependencies | array | \[\] | Dependencies loaded only when condition is true |
|
|
182
|
-
| schedule | object\|string | 'daily' | When to run: 'daily', 'weekly', 'monthly', or custom |
|
|
183
|
-
| isHistorical | boolean | false | Requires continuous daily execution (no gaps) |
|
|
184
|
-
| isTest | boolean | false | Always re-runs on today's date (for testing) |
|
|
185
|
-
| storage | object | see 3.8 | Where to store results (BigQuery, Firestore) |
|
|
186
|
-
| ttlDays | number | null | Auto-delete results after N days |
|
|
187
|
-
|
|
188
|
-
## 3.3 Data Requirements (requires)
|
|
189
|
-
|
|
190
|
-
The requires object declares which BigQuery tables the computation needs:
|
|
191
|
-
|
|
192
|
-
requires: {
|
|
193
|
-
|
|
194
|
-
'portfolio_snapshots': {
|
|
195
|
-
|
|
196
|
-
lookback: 30, // Days of historical data
|
|
197
|
-
|
|
198
|
-
mandatory: true, // Fail if missing
|
|
199
|
-
|
|
200
|
-
fields: \['user_id', 'portfolio_data'\],
|
|
201
|
-
|
|
202
|
-
filter: { user_type: 'POPULAR_INVESTOR' }
|
|
203
|
-
|
|
204
|
-
},
|
|
205
|
-
|
|
206
|
-
'asset_prices': {
|
|
207
|
-
|
|
208
|
-
lookback: 0, // Today only
|
|
209
|
-
|
|
210
|
-
mandatory: false // Optional data
|
|
211
|
-
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
### Requirement Fields
|
|
217
|
-
|
|
218
|
-
| **Field** | **Type** | **Default** | **Description** |
|
|
219
|
-
| --- | --- | --- | --- |
|
|
220
|
-
| lookback | number | 0 | How many days of historical data to fetch (0 = today only) |
|
|
221
|
-
| mandatory | boolean | false | If true, computation fails if data is missing |
|
|
222
|
-
| fields | array | null | Specific columns to fetch (null = all columns) |
|
|
223
|
-
| filter | object | {} | WHERE clause conditions to apply |
|
|
224
|
-
|
|
225
|
-
## 3.4 Dependencies
|
|
226
|
-
|
|
227
|
-
Dependencies are other computations that must complete first:
|
|
228
|
-
|
|
229
|
-
dependencies: \['UserPortfolioSummary', 'AssetPriceAnalysis'\]
|
|
230
|
-
|
|
231
|
-
The framework:
|
|
232
|
-
|
|
233
|
-
- Ensures dependencies run before this computation
|
|
234
|
-
- Detects circular dependencies at startup
|
|
235
|
-
- Re-runs this computation if a dependency's data changes
|
|
236
|
-
- Makes dependency results available via context.getDependency()
|
|
237
|
-
|
|
238
|
-
## 3.5 Conditional Dependencies
|
|
239
|
-
|
|
240
|
-
For dependencies that are only needed under certain conditions:
|
|
241
|
-
|
|
242
|
-
conditionalDependencies: \[
|
|
243
|
-
|
|
244
|
-
{
|
|
245
|
-
|
|
246
|
-
computation: 'WeekendAnalysis',
|
|
247
|
-
|
|
248
|
-
condition: ({ date }) => {
|
|
249
|
-
|
|
250
|
-
const day = new Date(date).getUTCDay();
|
|
251
|
-
|
|
252
|
-
return day === 0 || day === 6; // Weekend
|
|
253
|
-
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
\]
|
|
259
|
-
|
|
260
|
-
The condition function receives { date, config } and returns true/false.
|
|
261
|
-
|
|
262
|
-
## 3.6 Scheduling
|
|
263
|
-
|
|
264
|
-
Control when the computation runs:
|
|
265
|
-
|
|
266
|
-
// Simple schedules
|
|
267
|
-
|
|
268
|
-
schedule: 'daily' // Every day
|
|
269
|
-
|
|
270
|
-
schedule: 'weekly' // Every Sunday
|
|
271
|
-
|
|
272
|
-
schedule: 'monthly' // First of month
|
|
273
|
-
|
|
274
|
-
// Advanced schedule
|
|
275
|
-
|
|
276
|
-
schedule: {
|
|
277
|
-
|
|
278
|
-
frequency: 'weekly',
|
|
279
|
-
|
|
280
|
-
time: '02:00', // UTC time
|
|
281
|
-
|
|
282
|
-
dayOfWeek: 1, // Monday (0=Sunday)
|
|
283
|
-
|
|
284
|
-
timezone: 'UTC'
|
|
285
|
-
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
## 3.7 Historical Mode
|
|
289
|
-
|
|
290
|
-
For computations that must run continuously without gaps:
|
|
291
|
-
|
|
292
|
-
isHistorical: true
|
|
293
|
-
|
|
294
|
-
This enables:
|
|
295
|
-
|
|
296
|
-
- Access to previous day's results via context.previousResult
|
|
297
|
-
- Re-run if there's a gap in the daily sequence
|
|
298
|
-
- Version continuity checks (re-runs if code changed mid-sequence)
|
|
299
|
-
|
|
300
|
-
Use cases: Running balances, cumulative metrics, time-series that depend on previous state.
|
|
301
|
-
|
|
302
|
-
## 3.8 Storage Configuration
|
|
303
|
-
|
|
304
|
-
Control where results are stored:
|
|
305
|
-
|
|
306
|
-
storage: {
|
|
307
|
-
|
|
308
|
-
bigquery: true, // Default: Always store in BigQuery
|
|
309
|
-
|
|
310
|
-
firestore: {
|
|
311
|
-
|
|
312
|
-
enabled: true,
|
|
313
|
-
|
|
314
|
-
path: 'users/{entityId}/summary',
|
|
315
|
-
|
|
316
|
-
merge: true,
|
|
317
|
-
|
|
318
|
-
includeMetadata: true
|
|
319
|
-
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
Path variables: {entityId}, {date}, {computationName}, {category}
|
|
325
|
-
|
|
326
|
-
# 4\. Data Access & Context
|
|
327
|
-
|
|
328
|
-
The context object passed to process() contains everything the computation needs.
|
|
329
|
-
|
|
330
|
-
## 4.1 Context Properties
|
|
331
|
-
|
|
332
|
-
| **Property** | **Type** | **Description** |
|
|
333
|
-
| --- | --- | --- |
|
|
334
|
-
| data | object | Fetched table data based on requires configuration |
|
|
335
|
-
| entityId | string | Current entity ID (per-entity computations only) |
|
|
336
|
-
| date | string | Target date in YYYY-MM-DD format |
|
|
337
|
-
| rules | object | Injected business rules modules |
|
|
338
|
-
| references | object | Global reference data (lookups, mappings) |
|
|
339
|
-
| getDependency | function | Fetch dependency computation results |
|
|
340
|
-
| previousResult | any | Yesterday's result (isHistorical only) |
|
|
341
|
-
| computation | object | Metadata about this computation |
|
|
342
|
-
| config | object | System configuration (read-only) |
|
|
343
|
-
|
|
344
|
-
## 4.2 Accessing Table Data
|
|
345
|
-
|
|
346
|
-
Data is organized by table name, then by entity ID (for entity-keyed tables):
|
|
347
|
-
|
|
348
|
-
async process(context) {
|
|
349
|
-
|
|
350
|
-
const { data, entityId } = context;
|
|
351
|
-
|
|
352
|
-
// Per-entity table: Get current entity's data
|
|
353
|
-
|
|
354
|
-
const portfolio = data\['portfolio_snapshots'\];
|
|
355
|
-
|
|
356
|
-
// portfolio is already filtered to this entityId
|
|
357
|
-
|
|
358
|
-
// Global table: All entities
|
|
359
|
-
|
|
360
|
-
const prices = data\['asset_prices'\];
|
|
361
|
-
|
|
362
|
-
// prices = { instrumentId1: priceData, ... }
|
|
363
|
-
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
### Data Structures
|
|
367
|
-
|
|
368
|
-
With lookback = 0 (today only):
|
|
369
|
-
|
|
370
|
-
data\['table_name'\] = {
|
|
371
|
-
|
|
372
|
-
'entity1': { row data },
|
|
373
|
-
|
|
374
|
-
'entity2': { row data }
|
|
375
|
-
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
With lookback > 0 (historical data):
|
|
379
|
-
|
|
380
|
-
data\['table_name'\] = {
|
|
381
|
-
|
|
382
|
-
'entity1': \[
|
|
383
|
-
|
|
384
|
-
{ date: '2026-01-01', ...data },
|
|
385
|
-
|
|
386
|
-
{ date: '2026-01-02', ...data }
|
|
387
|
-
|
|
388
|
-
\]
|
|
389
|
-
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
## 4.3 Using getDependency()
|
|
393
|
-
|
|
394
|
-
Access results from dependency computations:
|
|
395
|
-
|
|
396
|
-
async process(context) {
|
|
397
|
-
|
|
398
|
-
const { getDependency, entityId } = context;
|
|
399
|
-
|
|
400
|
-
// Get this entity's result from dependency
|
|
401
|
-
|
|
402
|
-
const summary = getDependency('UserPortfolioSummary');
|
|
403
|
-
|
|
404
|
-
// Or explicitly specify entity ID
|
|
405
|
-
|
|
406
|
-
const summary = getDependency('UserPortfolioSummary', entityId);
|
|
407
|
-
|
|
408
|
-
// Cross-entity lookup
|
|
409
|
-
|
|
410
|
-
const otherUser = getDependency('UserPortfolioSummary', 'user-456');
|
|
411
|
-
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
## 4.4 Reference Data
|
|
415
|
-
|
|
416
|
-
Reference data is loaded once and shared across all entities:
|
|
417
|
-
|
|
418
|
-
async process(context) {
|
|
419
|
-
|
|
420
|
-
const { references } = context;
|
|
421
|
-
|
|
422
|
-
// Access pre-loaded lookup tables
|
|
423
|
-
|
|
424
|
-
const sectorMap = references\['sector_mappings'\];
|
|
425
|
-
|
|
426
|
-
const sector = sectorMap\['AAPL'\];
|
|
427
|
-
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
Configure in bulltrackers.config.js:
|
|
431
|
-
|
|
432
|
-
referenceData: \['sector_mappings', 'ticker_mappings'\]
|
|
433
|
-
|
|
434
|
-
# 5\. Business Rules System
|
|
435
|
-
|
|
436
|
-
Business rules separate domain logic from computation orchestration. They are completely optional - the framework works without them.
|
|
437
|
-
|
|
438
|
-
## 5.1 Using Rules
|
|
439
|
-
|
|
440
|
-
Rules are automatically injected into the computation context:
|
|
441
|
-
|
|
442
|
-
async process(context) {
|
|
443
|
-
|
|
444
|
-
const { data, rules, entityId } = context;
|
|
445
|
-
|
|
446
|
-
// Extract data using rules
|
|
447
|
-
|
|
448
|
-
const portfolioData = rules.portfolio.extractPortfolioData(
|
|
449
|
-
|
|
450
|
-
data\['portfolio_snapshots'\]
|
|
451
|
-
|
|
452
|
-
);
|
|
453
|
-
|
|
454
|
-
const positions = rules.portfolio.extractPositions(portfolioData);
|
|
455
|
-
|
|
456
|
-
// Calculate metrics using rules
|
|
457
|
-
|
|
458
|
-
const totalValue = rules.portfolio.calculateTotalValue(positions);
|
|
459
|
-
|
|
460
|
-
const exposure = rules.portfolio.getExposure(portfolioData);
|
|
461
|
-
|
|
462
|
-
this.setResult(entityId, { totalValue, exposure });
|
|
463
|
-
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
## 5.2 Available Rule Modules (BullTrackers)
|
|
467
|
-
|
|
468
|
-
The current implementation includes finance-specific rules:
|
|
469
|
-
|
|
470
|
-
| **Module** | **Purpose** | **Example Functions** |
|
|
471
|
-
| --- | --- | --- |
|
|
472
|
-
| rules.portfolio | Portfolio data extraction and analysis | extractPositions, calculateTotalValue, groupBySector |
|
|
473
|
-
| rules.metrics | Financial metrics calculations | calculateSharpeRatio, calculateMaxDrawdown, calculateVaR |
|
|
474
|
-
| rules.rankings | PI rankings data | getRiskScore, getAUM, calculateQualityScore |
|
|
475
|
-
| rules.trades | Trade history analysis | extractTrades, calculateTradeStats, filterProfitable |
|
|
476
|
-
| rules.social | Social engagement metrics | extractPosts, calculateEngagement, getAverageRating |
|
|
477
|
-
| rules.instruments | Asset and price data | getPrice, calculateSentimentScore, buildTickerMaps |
|
|
478
|
-
|
|
479
|
-
## 5.3 Creating Custom Rules
|
|
480
|
-
|
|
481
|
-
To add new business rules:
|
|
482
|
-
|
|
483
|
-
- Create a new file in rules/ directory
|
|
484
|
-
|
|
485
|
-
// rules/mymodule.js
|
|
486
|
-
|
|
487
|
-
function calculateSomething(data) {
|
|
488
|
-
|
|
489
|
-
// Your logic
|
|
490
|
-
|
|
491
|
-
return result;
|
|
492
|
-
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
module.exports = { calculateSomething };
|
|
496
|
-
|
|
497
|
-
- Register in rules/index.js
|
|
498
|
-
|
|
499
|
-
const mymodule = require('./mymodule');
|
|
500
|
-
|
|
501
|
-
module.exports = {
|
|
502
|
-
|
|
503
|
-
portfolio,
|
|
504
|
-
|
|
505
|
-
metrics,
|
|
506
|
-
|
|
507
|
-
mymodule // Add here
|
|
508
|
-
|
|
509
|
-
};
|
|
510
|
-
|
|
511
|
-
- Use in computations
|
|
512
|
-
|
|
513
|
-
const result = rules.mymodule.calculateSomething(data);
|
|
514
|
-
|
|
515
|
-
## 5.4 Rule Versioning
|
|
516
|
-
|
|
517
|
-
Rules are automatically version-tracked:
|
|
518
|
-
|
|
519
|
-
- Each rule function is hashed based on its source code
|
|
520
|
-
- Computations using a rule include its hash in their version
|
|
521
|
-
- Changing a rule triggers re-runs of dependent computations
|
|
522
|
-
- Only rules actually used (detected via code analysis) affect versioning
|
|
523
|
-
|
|
524
|
-
This ensures computation results stay fresh when business logic evolves.
|
|
525
|
-
|
|
526
|
-
# 6\. Testing & Debugging
|
|
527
|
-
|
|
528
|
-
## 6.1 Using the Admin Test Endpoint
|
|
529
|
-
|
|
530
|
-
The system includes a protected admin endpoint for testing:
|
|
531
|
-
|
|
532
|
-
curl -X POST <https://REGION-PROJECT.cloudfunctions.net/compute-admin-test> \\
|
|
533
|
-
|
|
534
|
-
\-H "Authorization: Bearer \$(gcloud auth print-identity-token)" \\
|
|
535
|
-
|
|
536
|
-
\-H "Content-Type: application/json" \\
|
|
537
|
-
|
|
538
|
-
\-d '{"action": "run", "computation": "MyComputation"}'
|
|
539
|
-
|
|
540
|
-
### Available Actions
|
|
541
|
-
|
|
542
|
-
| **Action** | **Purpose** | **Parameters** |
|
|
543
|
-
| --- | --- | --- |
|
|
544
|
-
| status | List all computations | None |
|
|
545
|
-
| analyze | Check what would run for a date | date |
|
|
546
|
-
| run | Execute a computation | computation, date, force, dryRun |
|
|
547
|
-
| run_limited | Run on sample entities only | computation, limit |
|
|
548
|
-
| test_worker | Test worker logic locally | computation, entityIds |
|
|
549
|
-
|
|
550
|
-
## 6.2 Development Workflow
|
|
551
|
-
|
|
552
|
-
- Create your computation file
|
|
553
|
-
- Deploy to Cloud Functions
|
|
554
|
-
- Test with admin endpoint
|
|
555
|
-
|
|
556
|
-
{
|
|
557
|
-
|
|
558
|
-
"action": "run_limited",
|
|
559
|
-
|
|
560
|
-
"computation": "MyComputation",
|
|
561
|
-
|
|
562
|
-
"limit": 5,
|
|
563
|
-
|
|
564
|
-
"dryRun": true
|
|
565
|
-
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
- Verify results in BigQuery
|
|
569
|
-
- Iterate as needed
|
|
570
|
-
|
|
571
|
-
## 6.3 Testing Strategies
|
|
572
|
-
|
|
573
|
-
### Test Mode
|
|
574
|
-
|
|
575
|
-
Mark computations as test to always re-run on today:
|
|
576
|
-
|
|
577
|
-
isTest: true
|
|
578
|
-
|
|
579
|
-
Perfect for development - runs every time without manual force flags.
|
|
580
|
-
|
|
581
|
-
### Dry Run
|
|
582
|
-
|
|
583
|
-
Execute logic without saving results:
|
|
584
|
-
|
|
585
|
-
{
|
|
586
|
-
|
|
587
|
-
"action": "run",
|
|
588
|
-
|
|
589
|
-
"computation": "MyComputation",
|
|
590
|
-
|
|
591
|
-
"dryRun": true
|
|
592
|
-
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
### Entity Filtering
|
|
596
|
-
|
|
597
|
-
Test on specific entities:
|
|
598
|
-
|
|
599
|
-
{
|
|
600
|
-
|
|
601
|
-
"action": "run",
|
|
602
|
-
|
|
603
|
-
"computation": "MyComputation",
|
|
604
|
-
|
|
605
|
-
"entityIds": \["user-123", "user-456"\]
|
|
606
|
-
|
|
607
|
-
}
|
|
608
|
-
|
|
609
|
-
## 6.4 Debugging Tips
|
|
610
|
-
|
|
611
|
-
- Use this.log() for logging (respects computation name prefix)
|
|
612
|
-
|
|
613
|
-
this.log('INFO', 'Processing entity: ' + entityId);
|
|
614
|
-
|
|
615
|
-
- Check BigQuery for stored results
|
|
616
|
-
- Verify dependencies completed successfully
|
|
617
|
-
- Use analyze action to understand scheduling
|
|
618
|
-
- Review Cloud Function logs for errors
|
|
619
|
-
|
|
620
|
-
# 7\. Best Practices & Common Patterns
|
|
621
|
-
|
|
622
|
-
## 7.1 Computation Design
|
|
623
|
-
|
|
624
|
-
### DO: Keep Computations Simple
|
|
625
|
-
|
|
626
|
-
Computations should be thin orchestration layers:
|
|
627
|
-
|
|
628
|
-
// GOOD: Simple orchestration
|
|
629
|
-
|
|
630
|
-
async process(context) {
|
|
631
|
-
|
|
632
|
-
const { data, rules, entityId } = context;
|
|
633
|
-
|
|
634
|
-
const portfolio = rules.portfolio.extractPortfolioData(data);
|
|
635
|
-
|
|
636
|
-
const positions = rules.portfolio.extractPositions(portfolio);
|
|
637
|
-
|
|
638
|
-
const summary = rules.portfolio.summarize(positions);
|
|
639
|
-
|
|
640
|
-
this.setResult(entityId, summary);
|
|
641
|
-
|
|
642
|
-
}
|
|
643
|
-
|
|
644
|
-
### DON'T: Implement Complex Logic
|
|
645
|
-
|
|
646
|
-
// BAD: Complex logic in computation
|
|
647
|
-
|
|
648
|
-
async process(context) {
|
|
649
|
-
|
|
650
|
-
const { data } = context;
|
|
651
|
-
|
|
652
|
-
// 100 lines of complex calculation logic here...
|
|
653
|
-
|
|
654
|
-
// This should be in business rules instead!
|
|
655
|
-
|
|
656
|
-
}
|
|
657
|
-
|
|
658
|
-
## 7.2 Dependency Management
|
|
659
|
-
|
|
660
|
-
### DO: Declare All Dependencies
|
|
661
|
-
|
|
662
|
-
Always list computations you depend on:
|
|
663
|
-
|
|
664
|
-
dependencies: \['UserPortfolioSummary', 'AssetPriceAnalysis'\]
|
|
665
|
-
|
|
666
|
-
### DON'T: Create Circular Dependencies
|
|
667
|
-
|
|
668
|
-
The framework will detect and reject these at startup:
|
|
669
|
-
|
|
670
|
-
// A depends on B
|
|
671
|
-
|
|
672
|
-
// B depends on C
|
|
673
|
-
|
|
674
|
-
// C depends on A <- CIRCULAR! Will fail
|
|
675
|
-
|
|
676
|
-
## 7.3 Data Requirements
|
|
677
|
-
|
|
678
|
-
### DO: Use Lookback Wisely
|
|
679
|
-
|
|
680
|
-
Only request the data you need:
|
|
681
|
-
|
|
682
|
-
requires: {
|
|
683
|
-
|
|
684
|
-
'portfolio_snapshots': {
|
|
685
|
-
|
|
686
|
-
lookback: 30 // Only if you need 30 days
|
|
687
|
-
|
|
688
|
-
}
|
|
689
|
-
|
|
690
|
-
}
|
|
691
|
-
|
|
692
|
-
Note: The system has a MAX_LOOKBACK_DAYS limit (currently 30) to prevent cost spirals.
|
|
693
|
-
|
|
694
|
-
### DO: Use Field Selection
|
|
695
|
-
|
|
696
|
-
Reduce data transfer by selecting only needed columns:
|
|
697
|
-
|
|
698
|
-
requires: {
|
|
699
|
-
|
|
700
|
-
'portfolio_snapshots': {
|
|
701
|
-
|
|
702
|
-
fields: \['user_id', 'portfolio_data', 'date'\]
|
|
703
|
-
|
|
704
|
-
}
|
|
705
|
-
|
|
706
|
-
}
|
|
707
|
-
|
|
708
|
-
## 7.4 Error Handling
|
|
709
|
-
|
|
710
|
-
### DO: Handle Missing Data Gracefully
|
|
711
|
-
|
|
712
|
-
async process(context) {
|
|
713
|
-
|
|
714
|
-
const { data, entityId } = context;
|
|
715
|
-
|
|
716
|
-
const portfolio = data\['portfolio_snapshots'\];
|
|
717
|
-
|
|
718
|
-
if (!portfolio) {
|
|
719
|
-
|
|
720
|
-
this.log('WARN', 'No portfolio data for ' + entityId);
|
|
721
|
-
|
|
722
|
-
return; // Skip this entity
|
|
723
|
-
|
|
724
|
-
}
|
|
725
|
-
|
|
726
|
-
// Process...
|
|
727
|
-
|
|
728
|
-
}
|
|
729
|
-
|
|
730
|
-
### DO: Use Mandatory for Critical Data
|
|
731
|
-
|
|
732
|
-
requires: {
|
|
733
|
-
|
|
734
|
-
'portfolio_snapshots': {
|
|
735
|
-
|
|
736
|
-
mandatory: true // Fail if missing
|
|
737
|
-
|
|
738
|
-
}
|
|
739
|
-
|
|
740
|
-
}
|
|
741
|
-
|
|
742
|
-
## 7.5 Performance
|
|
743
|
-
|
|
744
|
-
### DO: Use Per-Entity for Scalability
|
|
745
|
-
|
|
746
|
-
For user/entity-specific calculations, always use per-entity mode:
|
|
747
|
-
|
|
748
|
-
type: 'per-entity'
|
|
749
|
-
|
|
750
|
-
Benefits:
|
|
751
|
-
|
|
752
|
-
- Automatic parallelization
|
|
753
|
-
- Memory-efficient streaming
|
|
754
|
-
- Worker pool support
|
|
755
|
-
- Checkpointing and resume
|
|
756
|
-
|
|
757
|
-
### DON'T: Load Everything Into Memory
|
|
758
|
-
|
|
759
|
-
The framework handles batching - don't try to load all entities yourself.
|
|
760
|
-
|
|
761
|
-
## 7.6 Common Patterns
|
|
762
|
-
|
|
763
|
-
### Pattern: Aggregation
|
|
764
|
-
|
|
765
|
-
// Create per-entity computation first
|
|
766
|
-
|
|
767
|
-
class UserMetrics extends Computation {
|
|
768
|
-
|
|
769
|
-
static getConfig() {
|
|
770
|
-
|
|
771
|
-
return { name: 'UserMetrics', type: 'per-entity', ... };
|
|
772
|
-
|
|
773
|
-
}
|
|
774
|
-
|
|
775
|
-
}
|
|
776
|
-
|
|
777
|
-
// Then create global aggregation
|
|
778
|
-
|
|
779
|
-
class SystemSummary extends Computation {
|
|
780
|
-
|
|
781
|
-
static getConfig() {
|
|
782
|
-
|
|
783
|
-
return {
|
|
784
|
-
|
|
785
|
-
name: 'SystemSummary',
|
|
786
|
-
|
|
787
|
-
type: 'global',
|
|
788
|
-
|
|
789
|
-
dependencies: \['UserMetrics'\]
|
|
790
|
-
|
|
791
|
-
};
|
|
792
|
-
|
|
793
|
-
}
|
|
794
|
-
|
|
795
|
-
async process(context) {
|
|
796
|
-
|
|
797
|
-
const userMetrics = context.getDependency('UserMetrics');
|
|
798
|
-
|
|
799
|
-
// Aggregate all user results
|
|
800
|
-
|
|
801
|
-
}
|
|
802
|
-
|
|
803
|
-
}
|
|
804
|
-
|
|
805
|
-
### Pattern: Time Series
|
|
806
|
-
|
|
807
|
-
class RunningBalance extends Computation {
|
|
808
|
-
|
|
809
|
-
static getConfig() {
|
|
810
|
-
|
|
811
|
-
return {
|
|
812
|
-
|
|
813
|
-
name: 'RunningBalance',
|
|
814
|
-
|
|
815
|
-
type: 'per-entity',
|
|
816
|
-
|
|
817
|
-
isHistorical: true,
|
|
818
|
-
|
|
819
|
-
requires: { 'transactions': { lookback: 1 } }
|
|
820
|
-
|
|
821
|
-
};
|
|
822
|
-
|
|
823
|
-
}
|
|
824
|
-
|
|
825
|
-
async process(context) {
|
|
826
|
-
|
|
827
|
-
const { previousResult, data, entityId } = context;
|
|
828
|
-
|
|
829
|
-
const prevBalance = previousResult?.balance || 0;
|
|
830
|
-
|
|
831
|
-
const todayChange = calculateChange(data);
|
|
832
|
-
|
|
833
|
-
this.setResult(entityId, {
|
|
834
|
-
|
|
835
|
-
balance: prevBalance + todayChange
|
|
836
|
-
|
|
837
|
-
});
|
|
838
|
-
|
|
839
|
-
}
|
|
840
|
-
|
|
841
|
-
}
|
|
842
|
-
|
|
843
|
-
# Appendix A: Quick Reference
|
|
844
|
-
|
|
845
|
-
## Computation Template
|
|
846
|
-
|
|
847
|
-
const { Computation } = require('../framework');
|
|
848
|
-
|
|
849
|
-
class MyComputation extends Computation {
|
|
850
|
-
|
|
851
|
-
static getConfig() {
|
|
852
|
-
|
|
853
|
-
return {
|
|
854
|
-
|
|
855
|
-
name: 'MyComputation',
|
|
856
|
-
|
|
857
|
-
type: 'per-entity',
|
|
858
|
-
|
|
859
|
-
category: 'analytics',
|
|
860
|
-
|
|
861
|
-
requires: {
|
|
862
|
-
|
|
863
|
-
'table_name': {
|
|
864
|
-
|
|
865
|
-
lookback: 0,
|
|
866
|
-
|
|
867
|
-
mandatory: true
|
|
868
|
-
|
|
869
|
-
}
|
|
870
|
-
|
|
871
|
-
},
|
|
872
|
-
|
|
873
|
-
dependencies: \[\],
|
|
874
|
-
|
|
875
|
-
schedule: 'daily'
|
|
876
|
-
|
|
877
|
-
};
|
|
878
|
-
|
|
879
|
-
}
|
|
880
|
-
|
|
881
|
-
async process(context) {
|
|
882
|
-
|
|
883
|
-
const { data, entityId, rules } = context;
|
|
884
|
-
|
|
885
|
-
// Your logic here
|
|
886
|
-
|
|
887
|
-
const result = {};
|
|
888
|
-
|
|
889
|
-
this.setResult(entityId, result);
|
|
890
|
-
|
|
891
|
-
}
|
|
892
|
-
|
|
893
|
-
}
|
|
894
|
-
|
|
895
|
-
module.exports = MyComputation;
|
|
896
|
-
|
|
897
|
-
## Common Context Properties
|
|
898
|
-
|
|
899
|
-
context.data // Fetched table data
|
|
900
|
-
|
|
901
|
-
context.entityId // Current entity (per-entity only)
|
|
902
|
-
|
|
903
|
-
context.date // Target date (YYYY-MM-DD)
|
|
904
|
-
|
|
905
|
-
context.rules // Business rules modules
|
|
906
|
-
|
|
907
|
-
context.references // Global reference data
|
|
908
|
-
|
|
909
|
-
context.getDependency(name, id) // Get dependency result
|
|
910
|
-
|
|
911
|
-
context.previousResult // Yesterday (isHistorical only)
|
|
912
|
-
|
|
913
|
-
## Useful Helper Methods
|
|
914
|
-
|
|
915
|
-
this.setResult(entityId, result) // Save result
|
|
916
|
-
|
|
917
|
-
this.setGlobalResult(result) // Save (global only)
|
|
918
|
-
|
|
919
|
-
this.log(level, message) // Log message
|
|
920
|
-
|
|
921
|
-
this.get(obj, 'path.to.prop', default) // Safe access
|
|
922
|
-
|
|
923
|
-
# Appendix B: Framework Architecture
|
|
924
|
-
|
|
925
|
-
The system is built on a layered architecture that separates concerns:
|
|
926
|
-
|
|
927
|
-
## Layer 1: Core Framework
|
|
928
|
-
|
|
929
|
-
- Computation base class
|
|
930
|
-
- Manifest builder (DAG construction)
|
|
931
|
-
- Orchestrator (execution engine)
|
|
932
|
-
- Rules registry (business logic injection)
|
|
933
|
-
|
|
934
|
-
## Layer 2: Data Layer
|
|
935
|
-
|
|
936
|
-
- Schema registry (dynamic schema discovery)
|
|
937
|
-
- Query builder (SQL generation)
|
|
938
|
-
- Data fetcher (BigQuery interface)
|
|
939
|
-
- State repository (result caching)
|
|
940
|
-
|
|
941
|
-
## Layer 3: Storage Layer
|
|
942
|
-
|
|
943
|
-
- Storage manager (BigQuery & Firestore)
|
|
944
|
-
- Checkpointer (resume capability)
|
|
945
|
-
- Lineage tracker (data provenance)
|
|
946
|
-
|
|
947
|
-
## Layer 4: Execution Layer
|
|
948
|
-
|
|
949
|
-
- Task runner (middleware chain)
|
|
950
|
-
- Remote task runner (worker pool)
|
|
951
|
-
- Profiler middleware (performance tracking)
|
|
952
|
-
- Cost tracker middleware (billing)
|
|
953
|
-
|
|
954
|
-
## Layer 5: Scheduling Layer
|
|
955
|
-
|
|
956
|
-
- Scheduler (Cloud Tasks integration)
|
|
957
|
-
- Watchdog (zombie detection)
|
|
958
|
-
- Dispatcher (request routing)
|
|
959
|
-
|
|
960
|
-
All layers are domain-agnostic. Business logic lives entirely in the rules modules and computation implementations.
|
|
961
|
-
|
|
962
|
-
# End of Developer Manual
|
|
963
|
-
|
|
964
|
-
_For additional support, refer to the source code documentation or contact the development team._
|