bulltrackers-module 1.0.215 → 1.0.217
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.
|
@@ -0,0 +1,716 @@
|
|
|
1
|
+
# BullTrackers Computation System - Comprehensive Onboarding Guide
|
|
2
|
+
|
|
3
|
+
## Table of Contents
|
|
4
|
+
1. [System Overview](#system-overview)
|
|
5
|
+
2. [Context Architecture](#context-architecture)
|
|
6
|
+
3. [Data Loading & Routing](#data-loading--routing)
|
|
7
|
+
4. [Sharding System](#sharding-system)
|
|
8
|
+
5. [Computation Types & Execution](#computation-types--execution)
|
|
9
|
+
6. [Dependency Management](#dependency-management)
|
|
10
|
+
7. [Versioning & Smart Hashing](#versioning--smart-hashing)
|
|
11
|
+
8. [Execution Modes](#execution-modes)
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## System Overview
|
|
16
|
+
|
|
17
|
+
The BullTrackers Computation System is a **dependency-aware, auto-sharding, distributed calculation engine** designed to process massive datasets across user portfolios, trading histories, market insights, and price data. The system automatically handles:
|
|
18
|
+
|
|
19
|
+
- **Smart data loading** (only loads what's needed)
|
|
20
|
+
- **Transparent sharding** (handles Firestore's 1MB document limit)
|
|
21
|
+
- **Dependency injection** (calculations receive exactly what they declare)
|
|
22
|
+
- **Historical state management** (access to yesterday's data when needed)
|
|
23
|
+
- **Incremental recomputation** (only reruns when code or dependencies change)
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Context Architecture
|
|
28
|
+
|
|
29
|
+
### The Context Object
|
|
30
|
+
|
|
31
|
+
Every computation receives a **context object** that contains all the data and tools it needs. The context is built dynamically based on the computation's declared dependencies.
|
|
32
|
+
|
|
33
|
+
### Context Structure by Computation Type
|
|
34
|
+
|
|
35
|
+
#### **Standard (Per-User) Context**
|
|
36
|
+
```javascript
|
|
37
|
+
{
|
|
38
|
+
user: {
|
|
39
|
+
id: "user_123",
|
|
40
|
+
type: "speculator", // or "normal"
|
|
41
|
+
portfolio: {
|
|
42
|
+
today: { /* Portfolio snapshot for today */ },
|
|
43
|
+
yesterday: { /* Portfolio snapshot for yesterday (if isHistorical: true) */ }
|
|
44
|
+
},
|
|
45
|
+
history: {
|
|
46
|
+
today: { /* Trading history for today */ },
|
|
47
|
+
yesterday: { /* Trading history for yesterday (if needed) */ }
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
date: {
|
|
51
|
+
today: "2024-12-07"
|
|
52
|
+
},
|
|
53
|
+
insights: {
|
|
54
|
+
today: { /* Daily instrument insights */ },
|
|
55
|
+
yesterday: { /* Yesterday's insights (if needed) */ }
|
|
56
|
+
},
|
|
57
|
+
social: {
|
|
58
|
+
today: { /* Social post insights */ },
|
|
59
|
+
yesterday: { /* Yesterday's social data (if needed) */ }
|
|
60
|
+
},
|
|
61
|
+
mappings: {
|
|
62
|
+
tickerToInstrument: { "AAPL": 123, ... },
|
|
63
|
+
instrumentToTicker: { 123: "AAPL", ... }
|
|
64
|
+
},
|
|
65
|
+
math: {
|
|
66
|
+
// All mathematical layers (extractors, primitives, signals, etc.)
|
|
67
|
+
extract: DataExtractor,
|
|
68
|
+
compute: MathPrimitives,
|
|
69
|
+
signals: SignalPrimitives,
|
|
70
|
+
// ... and more
|
|
71
|
+
},
|
|
72
|
+
computed: {
|
|
73
|
+
// Results from dependency calculations (current day)
|
|
74
|
+
"risk-metrics": { "AAPL": { volatility: 0.25 }, ... },
|
|
75
|
+
"sentiment-score": { "AAPL": { score: 0.8 }, ... }
|
|
76
|
+
},
|
|
77
|
+
previousComputed: {
|
|
78
|
+
// Results from dependency calculations (previous day, if isHistorical: true)
|
|
79
|
+
"risk-metrics": { "AAPL": { volatility: 0.23 }, ... }
|
|
80
|
+
},
|
|
81
|
+
meta: { /* Calculation metadata */ },
|
|
82
|
+
config: { /* System configuration */ },
|
|
83
|
+
deps: { /* System dependencies (db, logger, etc.) */ }
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
#### **Meta (Once-Per-Day) Context**
|
|
88
|
+
```javascript
|
|
89
|
+
{
|
|
90
|
+
date: {
|
|
91
|
+
today: "2024-12-07"
|
|
92
|
+
},
|
|
93
|
+
insights: {
|
|
94
|
+
today: { /* Daily instrument insights */ },
|
|
95
|
+
yesterday: { /* If needed */ }
|
|
96
|
+
},
|
|
97
|
+
social: {
|
|
98
|
+
today: { /* Social post insights */ },
|
|
99
|
+
yesterday: { /* If needed */ }
|
|
100
|
+
},
|
|
101
|
+
prices: {
|
|
102
|
+
history: {
|
|
103
|
+
// Price data for all instruments (or batched shards)
|
|
104
|
+
"123": {
|
|
105
|
+
ticker: "AAPL",
|
|
106
|
+
prices: {
|
|
107
|
+
"2024-12-01": 150.25,
|
|
108
|
+
"2024-12-02": 151.30,
|
|
109
|
+
// ...
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
mappings: { /* Same as Standard */ },
|
|
115
|
+
math: { /* Same as Standard */ },
|
|
116
|
+
computed: { /* Same as Standard */ },
|
|
117
|
+
previousComputed: { /* Same as Standard */ },
|
|
118
|
+
meta: { /* Calculation metadata */ },
|
|
119
|
+
config: { /* System configuration */ },
|
|
120
|
+
deps: { /* System dependencies */ }
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### How Context is Auto-Populated
|
|
125
|
+
|
|
126
|
+
The system uses a **declaration-based approach**. When you define a calculation, you declare what data you need:
|
|
127
|
+
|
|
128
|
+
```javascript
|
|
129
|
+
class MyCalculation {
|
|
130
|
+
static getMetadata() {
|
|
131
|
+
return {
|
|
132
|
+
type: 'standard', // or 'meta'
|
|
133
|
+
isHistorical: true, // Do I need yesterday's data?
|
|
134
|
+
rootDataDependencies: ['portfolio', 'insights'], // What root data do I need?
|
|
135
|
+
userType: 'all' // 'all', 'speculator', or 'normal'
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
static getDependencies() {
|
|
140
|
+
return ['risk-metrics', 'sentiment-score']; // What other calculations do I depend on?
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
The `ContextBuilder` then:
|
|
146
|
+
|
|
147
|
+
1. **Checks `rootDataDependencies`** → Loads portfolio, insights, social, history, or price data
|
|
148
|
+
2. **Checks `isHistorical`** → If true, loads yesterday's portfolio and previous computation results
|
|
149
|
+
3. **Checks `getDependencies()`** → Fetches results from other calculations
|
|
150
|
+
4. **Injects math layers** → Automatically includes all extractors, primitives, and utilities
|
|
151
|
+
5. **Adds mappings** → Provides ticker ↔ instrument ID conversion
|
|
152
|
+
|
|
153
|
+
**You only get what you ask for.** This keeps memory usage efficient and prevents unnecessary data loading.
|
|
154
|
+
|
|
155
|
+
---
|
|
156
|
+
|
|
157
|
+
## Data Loading & Routing
|
|
158
|
+
|
|
159
|
+
### The Data Loading Pipeline
|
|
160
|
+
|
|
161
|
+
```
|
|
162
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
163
|
+
│ DataLoader (Cached) │
|
|
164
|
+
├─────────────────────────────────────────────────────────────┤
|
|
165
|
+
│ • loadMappings() → Ticker/Instrument maps │
|
|
166
|
+
│ • loadInsights(date) → Daily instrument insights │
|
|
167
|
+
│ • loadSocial(date) → Social post insights │
|
|
168
|
+
│ • loadPriceShard(ref) → Asset price data │
|
|
169
|
+
│ • getPriceShardRefs() → All price shards │
|
|
170
|
+
│ • getSpecificPriceShardReferences(ids) → Targeted shards │
|
|
171
|
+
└─────────────────────────────────────────────────────────────┘
|
|
172
|
+
↓
|
|
173
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
174
|
+
│ ComputationExecutor │
|
|
175
|
+
├─────────────────────────────────────────────────────────────┤
|
|
176
|
+
│ • executePerUser() → Streams portfolio data │
|
|
177
|
+
│ • executeOncePerDay() → Loads global/meta data │
|
|
178
|
+
└─────────────────────────────────────────────────────────────┘
|
|
179
|
+
↓
|
|
180
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
181
|
+
│ ContextBuilder │
|
|
182
|
+
├─────────────────────────────────────────────────────────────┤
|
|
183
|
+
│ Assembles context based on metadata & dependencies │
|
|
184
|
+
└─────────────────────────────────────────────────────────────┘
|
|
185
|
+
↓
|
|
186
|
+
Your Calculation.process()
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
### Streaming vs Batch Loading
|
|
190
|
+
|
|
191
|
+
#### **Standard Computations: Streaming**
|
|
192
|
+
Standard (per-user) computations use **streaming** to process users in chunks:
|
|
193
|
+
|
|
194
|
+
```javascript
|
|
195
|
+
// System streams portfolio data in batches of 50 users
|
|
196
|
+
for await (const userBatch of streamPortfolioData()) {
|
|
197
|
+
// Each batch is processed in parallel
|
|
198
|
+
for (const [userId, portfolio] of Object.entries(userBatch)) {
|
|
199
|
+
const context = buildPerUserContext({ userId, portfolio, ... });
|
|
200
|
+
await calculation.process(context);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
**Why streaming?**
|
|
206
|
+
- Portfolio data is sharded across multiple documents
|
|
207
|
+
- Loading all users at once would exceed memory limits
|
|
208
|
+
- Streaming allows processing millions of users efficiently
|
|
209
|
+
|
|
210
|
+
#### **Meta Computations: Batch or Shard**
|
|
211
|
+
Meta computations have two modes:
|
|
212
|
+
|
|
213
|
+
1. **Standard Meta** (No price dependency):
|
|
214
|
+
```javascript
|
|
215
|
+
const context = buildMetaContext({ insights, social, ... });
|
|
216
|
+
await calculation.process(context);
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
2. **Price-Dependent Meta** (Batched Shard Processing):
|
|
220
|
+
```javascript
|
|
221
|
+
// System loads price data in shard batches
|
|
222
|
+
for (const shardRef of priceShardRefs) {
|
|
223
|
+
const shardData = await loadPriceShard(shardRef);
|
|
224
|
+
const context = buildMetaContext({ prices: { history: shardData } });
|
|
225
|
+
await calculation.process(context);
|
|
226
|
+
// Memory is cleared between shards
|
|
227
|
+
}
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
---
|
|
231
|
+
|
|
232
|
+
## Sharding System
|
|
233
|
+
|
|
234
|
+
### The Problem: Firestore's 1MB Limit
|
|
235
|
+
|
|
236
|
+
Firestore has a **1MB hard limit** per document. When computation results contain thousands of tickers (e.g., momentum scores for every asset), the document exceeds this limit.
|
|
237
|
+
|
|
238
|
+
### The Solution: Auto-Sharding
|
|
239
|
+
|
|
240
|
+
The system **automatically detects** when a result is too large and splits it into a subcollection.
|
|
241
|
+
|
|
242
|
+
### How Auto-Sharding Works
|
|
243
|
+
|
|
244
|
+
```javascript
|
|
245
|
+
// When saving results:
|
|
246
|
+
const result = {
|
|
247
|
+
"AAPL": { score: 0.8, volatility: 0.25 },
|
|
248
|
+
"GOOGL": { score: 0.7, volatility: 0.22 },
|
|
249
|
+
// ... 5,000+ tickers
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
// System calculates size:
|
|
253
|
+
const totalSize = calculateFirestoreBytes(result); // ~1.2 MB
|
|
254
|
+
|
|
255
|
+
// IF size > 900KB (safety threshold):
|
|
256
|
+
// 1. Splits data into chunks < 900KB each
|
|
257
|
+
// 2. Writes chunks to: /results/{date}/{category}/{calc}/_shards/shard_0
|
|
258
|
+
// /results/{date}/{category}/{calc}/_shards/shard_1
|
|
259
|
+
// 3. Writes pointer: /results/{date}/{category}/{calc}
|
|
260
|
+
// → { _sharded: true, _shardCount: 2, _completed: true }
|
|
261
|
+
|
|
262
|
+
// IF size < 900KB:
|
|
263
|
+
// Writes normally: /results/{date}/{category}/{calc}
|
|
264
|
+
// → { "AAPL": {...}, "GOOGL": {...}, _completed: true, _sharded: false }
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
### Reading Sharded Data
|
|
268
|
+
|
|
269
|
+
The system **transparently reassembles** sharded data when loading dependencies:
|
|
270
|
+
|
|
271
|
+
```javascript
|
|
272
|
+
// When loading a dependency:
|
|
273
|
+
const result = await fetchExistingResults(dateStr, ['momentum-score']);
|
|
274
|
+
|
|
275
|
+
// System checks: Is this document sharded?
|
|
276
|
+
if (doc.data()._sharded === true) {
|
|
277
|
+
// 1. Fetch all docs from _shards subcollection
|
|
278
|
+
// 2. Merge them back into a single object
|
|
279
|
+
// 3. Return as if it was never sharded
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Your calculation receives complete data, regardless of storage method
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
### Handling Mixed Storage Scenarios
|
|
286
|
+
|
|
287
|
+
**Question:** What if I need data from 2 days, where Day 1 is sharded and Day 2 is not?
|
|
288
|
+
|
|
289
|
+
**Answer:** The system handles this automatically:
|
|
290
|
+
|
|
291
|
+
```javascript
|
|
292
|
+
// Your calculation declares:
|
|
293
|
+
static getMetadata() {
|
|
294
|
+
return {
|
|
295
|
+
isHistorical: true, // I need yesterday's data
|
|
296
|
+
// ...
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// System loads BOTH days:
|
|
301
|
+
const computed = await fetchExistingResults(todayDate, ['momentum-score']);
|
|
302
|
+
// → Auto-detects if sharded, reassembles if needed
|
|
303
|
+
|
|
304
|
+
const previousComputed = await fetchExistingResults(yesterdayDate, ['momentum-score']);
|
|
305
|
+
// → Auto-detects if sharded, reassembles if needed
|
|
306
|
+
|
|
307
|
+
// Context now has both:
|
|
308
|
+
{
|
|
309
|
+
computed: { "momentum-score": { /* today's data, reassembled if sharded */ } },
|
|
310
|
+
previousComputed: { "momentum-score": { /* yesterday's data, reassembled if sharded */ } }
|
|
311
|
+
}
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
You never need to know or care whether data is sharded. The system guarantees you receive complete, reassembled data.
|
|
315
|
+
|
|
316
|
+
---
|
|
317
|
+
|
|
318
|
+
## Computation Types & Execution
|
|
319
|
+
|
|
320
|
+
### Standard Computations (`type: 'standard'`)
|
|
321
|
+
|
|
322
|
+
**Purpose:** Per-user calculations (risk profiles, P&L analysis, behavioral scoring)
|
|
323
|
+
|
|
324
|
+
**Execution:**
|
|
325
|
+
- Runs **once per user** per day
|
|
326
|
+
- Receives individual user portfolio and history
|
|
327
|
+
- Streams data in batches for memory efficiency
|
|
328
|
+
|
|
329
|
+
**Example:**
|
|
330
|
+
```javascript
|
|
331
|
+
class UserRiskProfile {
|
|
332
|
+
static getMetadata() {
|
|
333
|
+
return {
|
|
334
|
+
type: 'standard',
|
|
335
|
+
rootDataDependencies: ['portfolio', 'history'],
|
|
336
|
+
userType: 'speculator'
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
async process(context) {
|
|
341
|
+
const { user, math } = context;
|
|
342
|
+
const portfolio = user.portfolio.today;
|
|
343
|
+
const positions = math.extract.getPositions(portfolio, user.type);
|
|
344
|
+
|
|
345
|
+
// Calculate risk per user
|
|
346
|
+
this.results[user.id] = { riskScore: /* ... */ };
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
**Result Structure:**
|
|
352
|
+
```javascript
|
|
353
|
+
{
|
|
354
|
+
"user_123": { riskScore: 0.75 },
|
|
355
|
+
"user_456": { riskScore: 0.45 },
|
|
356
|
+
// ... millions of users
|
|
357
|
+
}
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
### Meta Computations (`type: 'meta'`)
|
|
361
|
+
|
|
362
|
+
**Purpose:** Platform-wide calculations (aggregate metrics, market analysis, global trends)
|
|
363
|
+
|
|
364
|
+
**Execution:**
|
|
365
|
+
- Runs **once per day** (not per user)
|
|
366
|
+
- Processes all data holistically
|
|
367
|
+
- Can access price history for all instruments
|
|
368
|
+
|
|
369
|
+
**Example:**
|
|
370
|
+
```javascript
|
|
371
|
+
class MarketMomentum {
|
|
372
|
+
static getMetadata() {
|
|
373
|
+
return {
|
|
374
|
+
type: 'meta',
|
|
375
|
+
rootDataDependencies: ['price', 'insights']
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
async process(context) {
|
|
380
|
+
const { prices, insights, math } = context;
|
|
381
|
+
|
|
382
|
+
// Calculate momentum for every ticker
|
|
383
|
+
for (const [instId, data] of Object.entries(prices.history)) {
|
|
384
|
+
const ticker = data.ticker;
|
|
385
|
+
const priceData = math.priceExtractor.getHistory(prices, ticker);
|
|
386
|
+
|
|
387
|
+
this.results[ticker] = { momentum: /* ... */ };
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
**Result Structure:**
|
|
394
|
+
```javascript
|
|
395
|
+
{
|
|
396
|
+
"AAPL": { momentum: 0.65 },
|
|
397
|
+
"GOOGL": { momentum: 0.82 },
|
|
398
|
+
// ... all tickers
|
|
399
|
+
}
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
### Price-Dependent Meta Computations
|
|
403
|
+
|
|
404
|
+
When a meta computation declares `rootDataDependencies: ['price']`, it enters **batched shard processing mode**:
|
|
405
|
+
|
|
406
|
+
```javascript
|
|
407
|
+
// Instead of loading ALL price data at once (would crash):
|
|
408
|
+
for (const shardRef of priceShardRefs) {
|
|
409
|
+
const shardData = await loadPriceShard(shardRef); // ~50-100 instruments per shard
|
|
410
|
+
|
|
411
|
+
const context = buildMetaContext({
|
|
412
|
+
prices: { history: shardData } // Only this shard's data
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
await calculation.process(context);
|
|
416
|
+
|
|
417
|
+
// Results accumulate across shards
|
|
418
|
+
// Memory is cleared between iterations
|
|
419
|
+
}
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
**Your calculation receives partial data** and processes it incrementally. The system ensures all shards are eventually processed.
|
|
423
|
+
|
|
424
|
+
---
|
|
425
|
+
|
|
426
|
+
## Dependency Management
|
|
427
|
+
|
|
428
|
+
### Declaring Dependencies
|
|
429
|
+
|
|
430
|
+
```javascript
|
|
431
|
+
static getDependencies() {
|
|
432
|
+
return ['risk-metrics', 'sentiment-score', 'momentum-analysis'];
|
|
433
|
+
}
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
This tells the system: "Before you run me, make sure these 3 calculations have completed."
|
|
437
|
+
|
|
438
|
+
### How Dependencies Are Loaded
|
|
439
|
+
|
|
440
|
+
When your calculation runs:
|
|
441
|
+
|
|
442
|
+
1. System fetches results from all declared dependencies
|
|
443
|
+
2. Checks if data is sharded → reassembles if needed
|
|
444
|
+
3. Injects into `context.computed`:
|
|
445
|
+
|
|
446
|
+
```javascript
|
|
447
|
+
{
|
|
448
|
+
computed: {
|
|
449
|
+
"risk-metrics": { "AAPL": { volatility: 0.25 }, ... },
|
|
450
|
+
"sentiment-score": { "AAPL": { score: 0.8 }, ... },
|
|
451
|
+
"momentum-analysis": { "AAPL": { momentum: 0.65 }, ... }
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
### Accessing Dependency Results
|
|
457
|
+
|
|
458
|
+
```javascript
|
|
459
|
+
async process(context) {
|
|
460
|
+
const { computed, math } = context;
|
|
461
|
+
|
|
462
|
+
// Access results from dependencies
|
|
463
|
+
const volatility = math.signals.getMetric(
|
|
464
|
+
computed,
|
|
465
|
+
'risk-metrics',
|
|
466
|
+
'AAPL',
|
|
467
|
+
'volatility'
|
|
468
|
+
);
|
|
469
|
+
|
|
470
|
+
const sentiment = math.signals.getMetric(
|
|
471
|
+
computed,
|
|
472
|
+
'sentiment-score',
|
|
473
|
+
'AAPL',
|
|
474
|
+
'score'
|
|
475
|
+
);
|
|
476
|
+
|
|
477
|
+
// Use them in your calculation
|
|
478
|
+
const combinedScore = volatility * sentiment;
|
|
479
|
+
}
|
|
480
|
+
```
|
|
481
|
+
|
|
482
|
+
### Historical Dependencies
|
|
483
|
+
|
|
484
|
+
If your calculation needs **yesterday's dependency results**:
|
|
485
|
+
|
|
486
|
+
```javascript
|
|
487
|
+
static getMetadata() {
|
|
488
|
+
return {
|
|
489
|
+
isHistorical: true, // ← Enable historical mode
|
|
490
|
+
// ...
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
async process(context) {
|
|
495
|
+
const { computed, previousComputed } = context;
|
|
496
|
+
|
|
497
|
+
// Today's risk
|
|
498
|
+
const todayRisk = computed['risk-metrics']['AAPL'].volatility;
|
|
499
|
+
|
|
500
|
+
// Yesterday's risk
|
|
501
|
+
const yesterdayRisk = previousComputed['risk-metrics']['AAPL'].volatility;
|
|
502
|
+
|
|
503
|
+
// Calculate change
|
|
504
|
+
const riskChange = todayRisk - yesterdayRisk;
|
|
505
|
+
}
|
|
506
|
+
```
|
|
507
|
+
|
|
508
|
+
---
|
|
509
|
+
|
|
510
|
+
## Versioning & Smart Hashing
|
|
511
|
+
|
|
512
|
+
### The Problem: When to Recompute?
|
|
513
|
+
|
|
514
|
+
If you fix a bug in a calculation, how does the system know to re-run it for all past dates?
|
|
515
|
+
|
|
516
|
+
### The Solution: Merkle Tree Dependency Hashing
|
|
517
|
+
|
|
518
|
+
Every calculation gets a **smart hash** that includes:
|
|
519
|
+
|
|
520
|
+
1. **Its own code** (SHA-256 of the class definition)
|
|
521
|
+
2. **Layer dependencies** (Hashes of math layers it uses)
|
|
522
|
+
3. **Calculation dependencies** (Hashes of calculations it depends on)
|
|
523
|
+
|
|
524
|
+
```javascript
|
|
525
|
+
// Example hash composition:
|
|
526
|
+
const intrinsicHash = hash(calculation.toString() + layerHashes);
|
|
527
|
+
const dependencyHashes = dependencies.map(dep => dep.hash).join('|');
|
|
528
|
+
const finalHash = hash(intrinsicHash + '|DEPS:' + dependencyHashes);
|
|
529
|
+
|
|
530
|
+
// Result: "a3f9c2e1..." (SHA-256)
|
|
531
|
+
```
|
|
532
|
+
|
|
533
|
+
### Cascading Invalidation
|
|
534
|
+
|
|
535
|
+
If **Calculation A** changes, **Calculation B** (which depends on A) automatically gets a new hash:
|
|
536
|
+
|
|
537
|
+
```
|
|
538
|
+
Risk Metrics (v1) → hash: abc123
|
|
539
|
+
↓
|
|
540
|
+
Sentiment Score → hash: def456 (includes abc123)
|
|
541
|
+
(depends on Risk)
|
|
542
|
+
```
|
|
543
|
+
|
|
544
|
+
If you update Risk Metrics:
|
|
545
|
+
|
|
546
|
+
```
|
|
547
|
+
Risk Metrics (v2) → hash: xyz789 (NEW!)
|
|
548
|
+
↓
|
|
549
|
+
Sentiment Score → hash: ghi012 (NEW! Because dependency changed)
|
|
550
|
+
```
|
|
551
|
+
|
|
552
|
+
### Recomputation Logic
|
|
553
|
+
|
|
554
|
+
For each date, the system checks:
|
|
555
|
+
|
|
556
|
+
```javascript
|
|
557
|
+
// Stored in Firestore:
|
|
558
|
+
computationStatus['2024-12-07'] = {
|
|
559
|
+
'risk-metrics': 'abc123', // Last run hash
|
|
560
|
+
'sentiment-score': 'def456'
|
|
561
|
+
};
|
|
562
|
+
|
|
563
|
+
// Current manifest:
|
|
564
|
+
manifest['risk-metrics'].hash = 'xyz789'; // NEW HASH!
|
|
565
|
+
manifest['sentiment-score'].hash = 'ghi012';
|
|
566
|
+
|
|
567
|
+
// Decision:
|
|
568
|
+
// - Risk Metrics: Hash mismatch → RERUN
|
|
569
|
+
// - Sentiment Score: Hash mismatch → RERUN (cascaded)
|
|
570
|
+
```
|
|
571
|
+
|
|
572
|
+
This ensures **incremental recomputation**: only changed calculations (and their dependents) re-run.
|
|
573
|
+
|
|
574
|
+
---
|
|
575
|
+
|
|
576
|
+
## Execution Modes
|
|
577
|
+
|
|
578
|
+
### Mode 1: Legacy (Orchestrator)
|
|
579
|
+
|
|
580
|
+
**Single-process execution** for all dates and calculations.
|
|
581
|
+
|
|
582
|
+
```bash
|
|
583
|
+
COMPUTATION_PASS_TO_RUN=1 npm run computation-orchestrator
|
|
584
|
+
```
|
|
585
|
+
|
|
586
|
+
- Loads manifest
|
|
587
|
+
- Iterates through all dates
|
|
588
|
+
- Runs all calculations in Pass 1 sequentially
|
|
589
|
+
- Good for: Development, debugging
|
|
590
|
+
|
|
591
|
+
### Mode 2: Dispatcher + Workers (Production)
|
|
592
|
+
|
|
593
|
+
**Distributed execution** using Pub/Sub.
|
|
594
|
+
|
|
595
|
+
#### Step 1: Dispatch Tasks
|
|
596
|
+
```bash
|
|
597
|
+
COMPUTATION_PASS_TO_RUN=1 npm run computation-dispatcher
|
|
598
|
+
```
|
|
599
|
+
|
|
600
|
+
Publishes messages to Pub/Sub:
|
|
601
|
+
```json
|
|
602
|
+
{
|
|
603
|
+
"action": "RUN_COMPUTATION_DATE",
|
|
604
|
+
"date": "2024-12-07",
|
|
605
|
+
"pass": "1"
|
|
606
|
+
}
|
|
607
|
+
```
|
|
608
|
+
|
|
609
|
+
#### Step 2: Workers Consume Tasks
|
|
610
|
+
```bash
|
|
611
|
+
# Cloud Function triggered by Pub/Sub
|
|
612
|
+
# Or: Local consumer for testing
|
|
613
|
+
npm run computation-worker
|
|
614
|
+
```
|
|
615
|
+
|
|
616
|
+
Each worker:
|
|
617
|
+
1. Receives a date + pass
|
|
618
|
+
2. Loads manifest
|
|
619
|
+
3. Runs calculations for that date only
|
|
620
|
+
4. Updates status document
|
|
621
|
+
|
|
622
|
+
**Benefits:**
|
|
623
|
+
- Parallel execution (100+ workers)
|
|
624
|
+
- Fault tolerance (failed dates retry automatically)
|
|
625
|
+
- Scales to millions of dates
|
|
626
|
+
|
|
627
|
+
### Pass System
|
|
628
|
+
|
|
629
|
+
Calculations are grouped into **passes** based on dependencies:
|
|
630
|
+
|
|
631
|
+
```
|
|
632
|
+
Pass 1: Base calculations (no dependencies)
|
|
633
|
+
- risk-metrics
|
|
634
|
+
- price-momentum
|
|
635
|
+
|
|
636
|
+
Pass 2: Depends on Pass 1
|
|
637
|
+
- sentiment-score (needs risk-metrics)
|
|
638
|
+
- trend-analysis (needs price-momentum)
|
|
639
|
+
|
|
640
|
+
Pass 3: Depends on Pass 2
|
|
641
|
+
- combined-signal (needs sentiment-score + trend-analysis)
|
|
642
|
+
```
|
|
643
|
+
|
|
644
|
+
**You run passes sequentially:**
|
|
645
|
+
```bash
|
|
646
|
+
COMPUTATION_PASS_TO_RUN=1 npm run computation-dispatcher # Wait for completion
|
|
647
|
+
COMPUTATION_PASS_TO_RUN=2 npm run computation-dispatcher # Wait for completion
|
|
648
|
+
COMPUTATION_PASS_TO_RUN=3 npm run computation-dispatcher
|
|
649
|
+
```
|
|
650
|
+
|
|
651
|
+
The manifest builder automatically assigns pass numbers via topological sort.
|
|
652
|
+
|
|
653
|
+
---
|
|
654
|
+
|
|
655
|
+
## Summary: The Complete Flow
|
|
656
|
+
|
|
657
|
+
### For a Standard Calculation
|
|
658
|
+
|
|
659
|
+
```
|
|
660
|
+
1. Manifest Builder
|
|
661
|
+
├─ Scans your calculation class
|
|
662
|
+
├─ Generates smart hash (code + layers + dependencies)
|
|
663
|
+
├─ Assigns to a pass based on dependency graph
|
|
664
|
+
└─ Validates all dependencies exist
|
|
665
|
+
|
|
666
|
+
2. Dispatcher/Orchestrator
|
|
667
|
+
├─ Loads manifest
|
|
668
|
+
├─ Iterates through all dates
|
|
669
|
+
└─ For each date:
|
|
670
|
+
├─ Checks if calculation needs to run (hash mismatch?)
|
|
671
|
+
├─ Checks if root data exists (portfolio, history, etc.)
|
|
672
|
+
└─ Dispatches task (or runs directly)
|
|
673
|
+
|
|
674
|
+
3. Worker/Executor
|
|
675
|
+
├─ Receives task for specific date
|
|
676
|
+
├─ Loads dependency results (auto-reassembles if sharded)
|
|
677
|
+
├─ Streams portfolio data in batches
|
|
678
|
+
└─ For each user batch:
|
|
679
|
+
├─ Builds per-user context
|
|
680
|
+
├─ Injects math layers, mappings, computed dependencies
|
|
681
|
+
├─ Calls your calculation.process(context)
|
|
682
|
+
└─ Accumulates results
|
|
683
|
+
|
|
684
|
+
4. Result Committer
|
|
685
|
+
├─ Calculates total result size
|
|
686
|
+
├─ IF size > 900KB:
|
|
687
|
+
│ ├─ Splits into chunks
|
|
688
|
+
│ ├─ Writes to _shards subcollection
|
|
689
|
+
│ └─ Writes pointer document
|
|
690
|
+
└─ ELSE:
|
|
691
|
+
└─ Writes single document
|
|
692
|
+
|
|
693
|
+
5. Status Updater
|
|
694
|
+
└─ Updates computation_status/{date} with new hash
|
|
695
|
+
```
|
|
696
|
+
|
|
697
|
+
### For a Meta Calculation
|
|
698
|
+
|
|
699
|
+
Same as above, except:
|
|
700
|
+
|
|
701
|
+
- **Step 3**: Loads all data once (or iterates through price shards)
|
|
702
|
+
- **Context**: Global data, not per-user
|
|
703
|
+
- **Result**: One document per date (e.g., all tickers' momentum scores)
|
|
704
|
+
|
|
705
|
+
---
|
|
706
|
+
|
|
707
|
+
## Key Takeaways
|
|
708
|
+
|
|
709
|
+
1. **Context is Auto-Built**: Declare what you need in metadata; the system handles the rest
|
|
710
|
+
2. **Sharding is Transparent**: Read and write as if documents have no size limit
|
|
711
|
+
3. **Dependencies Just Work**: Results are automatically fetched and reassembled
|
|
712
|
+
4. **Versioning is Smart**: Change code → system knows what to rerun
|
|
713
|
+
5. **Streaming is Automatic**: Standard computations stream data; you don't manage batches
|
|
714
|
+
6. **Execution is Flexible**: Run locally for dev, distributed for production
|
|
715
|
+
|
|
716
|
+
---
|