bulltrackers-module 1.0.175 → 1.0.176
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/README.MD +1901 -2
- package/functions/computation-system/controllers/computation_controller.js +1 -1
- package/functions/computation-system/helpers/computation_manifest_builder.js +9 -39
- package/functions/computation-system/helpers/computation_pass_runner.js +72 -66
- package/functions/computation-system/helpers/orchestration_helpers.js +136 -99
- package/functions/task-engine/utils/firestore_batch_manager.js +224 -180
- package/package.json +1 -1
package/README.MD
CHANGED
|
@@ -1,3 +1,1902 @@
|
|
|
1
|
-
|
|
1
|
+
# BullTrackers Platform - Comprehensive Developer Documentation
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
## Table of Contents
|
|
4
|
+
|
|
5
|
+
1. [System Overview](#1-system-overview)
|
|
6
|
+
2. [Architecture & Design Patterns](#2-architecture--design-patterns)
|
|
7
|
+
3. [Core Infrastructure](#3-core-infrastructure)
|
|
8
|
+
4. [Module Reference](#4-module-reference)
|
|
9
|
+
5. [Data Flow & Pipelines](#5-data-flow--pipelines)
|
|
10
|
+
6. [Computation System](#6-computation-system)
|
|
11
|
+
7. [Development Guidelines](#7-development-guidelines)
|
|
12
|
+
8. [Deployment & Operations](#8-deployment--operations)
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## 1. System Overview
|
|
17
|
+
|
|
18
|
+
### 1.1 What is BullTrackers?
|
|
19
|
+
|
|
20
|
+
BullTrackers is a sophisticated financial analytics platform that monitors and analyzes eToro trading portfolios at scale. The system processes millions of user portfolios daily, computing real-time market sentiment signals, risk metrics, and behavioral analytics.
|
|
21
|
+
|
|
22
|
+
### 1.2 Key Capabilities
|
|
23
|
+
|
|
24
|
+
- **User Discovery & Monitoring**: Automatically discovers and tracks ~5M+ eToro users
|
|
25
|
+
- **Portfolio Tracking**: Stores daily snapshots of user portfolios (both normal users and high-leverage "speculators")
|
|
26
|
+
- **Trading History**: Captures complete trade history for behavioral analysis
|
|
27
|
+
- **Real-Time Computations**: Runs 100+ mathematical computations daily to generate market insights
|
|
28
|
+
- **Social Sentiment Analysis**: Tracks and analyzes social posts from eToro's platform using AI
|
|
29
|
+
- **Public API**: Exposes computed insights via REST API with schema introspection
|
|
30
|
+
|
|
31
|
+
### 1.3 Technology Stack
|
|
32
|
+
|
|
33
|
+
- **Runtime**: Node.js 20+ (Google Cloud Functions Gen 2)
|
|
34
|
+
- **Database**: Google Firestore (NoSQL document store)
|
|
35
|
+
- **Messaging**: Google Cloud Pub/Sub
|
|
36
|
+
- **AI/ML**: Google Gemini API (sentiment analysis)
|
|
37
|
+
- **Proxy Infrastructure**: Google Apps Script (rate limit mitigation)
|
|
38
|
+
- **Package Management**: NPM private packages
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## 2. Architecture & Design Patterns
|
|
43
|
+
|
|
44
|
+
### 2.1 Modular Pipe Architecture
|
|
45
|
+
|
|
46
|
+
The entire codebase follows a **"pipe" pattern** where each module exports stateless functions that receive all dependencies explicitly:
|
|
47
|
+
|
|
48
|
+
```javascript
|
|
49
|
+
// Example pipe signature
|
|
50
|
+
async function handleRequest(message, context, config, dependencies) {
|
|
51
|
+
const { logger, db, pubsub } = dependencies;
|
|
52
|
+
// Function logic here
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
**Key Benefits**:
|
|
57
|
+
- **Testability**: Easy to mock dependencies
|
|
58
|
+
- **Reusability**: Functions are pure and composable
|
|
59
|
+
- **Maintainability**: Clear separation of concerns
|
|
60
|
+
|
|
61
|
+
### 2.2 Dependency Injection Pattern
|
|
62
|
+
|
|
63
|
+
All modules receive dependencies via a `dependencies` object:
|
|
64
|
+
|
|
65
|
+
```javascript
|
|
66
|
+
const dependencies = {
|
|
67
|
+
db: firestoreInstance,
|
|
68
|
+
pubsub: pubsubClient,
|
|
69
|
+
logger: loggerInstance,
|
|
70
|
+
headerManager: intelligentHeaderManager,
|
|
71
|
+
proxyManager: intelligentProxyManager,
|
|
72
|
+
batchManager: firestoreBatchManager,
|
|
73
|
+
calculationUtils: { withRetry, loadInstrumentMappings },
|
|
74
|
+
firestoreUtils: { getBlockCapacities, ... },
|
|
75
|
+
pubsubUtils: { batchPublishTasks }
|
|
76
|
+
};
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### 2.3 Configuration Objects
|
|
80
|
+
|
|
81
|
+
Each module has its own configuration namespace:
|
|
82
|
+
|
|
83
|
+
```javascript
|
|
84
|
+
const config = {
|
|
85
|
+
// Task Engine Config
|
|
86
|
+
ETORO_API_PORTFOLIO_URL: "https://...",
|
|
87
|
+
TASK_ENGINE_MAX_USERS_PER_SHARD: 500,
|
|
88
|
+
|
|
89
|
+
// Orchestrator Config
|
|
90
|
+
discoveryConfig: { normal: {...}, speculator: {...} },
|
|
91
|
+
updateConfig: { ... },
|
|
92
|
+
|
|
93
|
+
// Computation System Config
|
|
94
|
+
COMPUTATION_PASS_TO_RUN: "1",
|
|
95
|
+
resultsCollection: "unified_insights"
|
|
96
|
+
};
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### 2.4 Sharded Data Storage
|
|
100
|
+
|
|
101
|
+
To handle massive scale, user data is **sharded** across multiple Firestore documents:
|
|
102
|
+
|
|
103
|
+
```
|
|
104
|
+
Collection: NormalUserPortfolios
|
|
105
|
+
├── Block: 0M (users 0-999,999)
|
|
106
|
+
│ └── snapshots/
|
|
107
|
+
│ └── 2025-01-15/
|
|
108
|
+
│ └── parts/
|
|
109
|
+
│ ├── part_0 (users 0-499)
|
|
110
|
+
│ ├── part_1 (users 500-999)
|
|
111
|
+
│ └── ...
|
|
112
|
+
├── Block: 1M (users 1,000,000-1,999,999)
|
|
113
|
+
└── ...
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
**Why Sharding?**
|
|
117
|
+
- Firestore document size limit: 1MB
|
|
118
|
+
- Each "part" contains ~500 users
|
|
119
|
+
- Enables parallel processing
|
|
120
|
+
|
|
121
|
+
### 2.5 Batch Processing Pattern
|
|
122
|
+
|
|
123
|
+
All writes use a **FirestoreBatchManager** to aggregate operations and minimize API calls:
|
|
124
|
+
|
|
125
|
+
```javascript
|
|
126
|
+
// Instead of 1000 individual writes:
|
|
127
|
+
await batchManager.addToPortfolioBatch(userId, blockId, date, data, userType);
|
|
128
|
+
// ... (accumulates in memory)
|
|
129
|
+
await batchManager.flushBatches(); // Commits in bulk
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
## 3. Core Infrastructure
|
|
135
|
+
|
|
136
|
+
### 3.1 IntelligentProxyManager
|
|
137
|
+
|
|
138
|
+
**Purpose**: Manages a pool of Google Apps Script proxies to bypass eToro rate limits.
|
|
139
|
+
|
|
140
|
+
**Key Features**:
|
|
141
|
+
- **Automatic Proxy Rotation**: Selects available (unlocked) proxies
|
|
142
|
+
- **Rate Limit Detection**: Identifies HTML error pages (DOCTYPE checks)
|
|
143
|
+
- **Exponential Backoff**: Retries failed requests with different proxies
|
|
144
|
+
- **Locking Mechanism**: Temporarily locks proxies that hit rate limits
|
|
145
|
+
|
|
146
|
+
**Usage**:
|
|
147
|
+
```javascript
|
|
148
|
+
const response = await proxyManager.fetch(url, options);
|
|
149
|
+
// Handles: proxy selection, retries, rate limit detection
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
**Critical Configuration**:
|
|
153
|
+
```javascript
|
|
154
|
+
proxyUrls: [
|
|
155
|
+
"https://script.google.com/macros/s/.../exec",
|
|
156
|
+
// 10-20 different proxy URLs
|
|
157
|
+
],
|
|
158
|
+
proxyLockingEnabled: true,
|
|
159
|
+
PERFORMANCE_DOC_PATH: "system_state/proxy_performance"
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### 3.2 IntelligentHeaderManager
|
|
163
|
+
|
|
164
|
+
**Purpose**: Manages browser headers to mimic real user traffic.
|
|
165
|
+
|
|
166
|
+
**Key Features**:
|
|
167
|
+
- **Header Rotation**: Uses different User-Agent strings
|
|
168
|
+
- **Performance Tracking**: Records success rates per header
|
|
169
|
+
- **Weighted Selection**: Prefers high-success headers
|
|
170
|
+
- **Firestore Sync**: Persists performance data
|
|
171
|
+
|
|
172
|
+
**Usage**:
|
|
173
|
+
```javascript
|
|
174
|
+
const { id, header } = await headerManager.selectHeader();
|
|
175
|
+
// Use header in request
|
|
176
|
+
headerManager.updatePerformance(id, wasSuccessful);
|
|
177
|
+
await headerManager.flushPerformanceUpdates();
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### 3.3 FirestoreBatchManager
|
|
181
|
+
|
|
182
|
+
**Purpose**: Aggregates Firestore writes to minimize costs and improve performance.
|
|
183
|
+
|
|
184
|
+
**Features**:
|
|
185
|
+
- **Portfolio Batching**: Groups user portfolio updates
|
|
186
|
+
- **Trading History Batching**: Aggregates trade history
|
|
187
|
+
- **Timestamp Management**: Tracks last update times
|
|
188
|
+
- **Username Map Caching**: Reduces redundant lookups
|
|
189
|
+
- **10-Minute History Cache**: Prevents duplicate fetches
|
|
190
|
+
|
|
191
|
+
**Critical Methods**:
|
|
192
|
+
```javascript
|
|
193
|
+
// Add data (accumulates in memory)
|
|
194
|
+
await batchManager.addToPortfolioBatch(userId, blockId, date, data, userType);
|
|
195
|
+
await batchManager.addToTradingHistoryBatch(userId, blockId, date, historyData, userType);
|
|
196
|
+
await batchManager.updateUserTimestamp(userId, userType, instrumentId);
|
|
197
|
+
|
|
198
|
+
// Commit everything at once
|
|
199
|
+
await batchManager.flushBatches();
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
**Auto-Flush Logic**:
|
|
203
|
+
```javascript
|
|
204
|
+
_scheduleFlush() {
|
|
205
|
+
const totalOps = this._estimateBatchSize();
|
|
206
|
+
if (totalOps >= 400) {
|
|
207
|
+
this.flushBatches(); // Immediate flush if near limit
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
// Otherwise, schedule delayed flush
|
|
211
|
+
if (!this.batchTimeout) {
|
|
212
|
+
this.batchTimeout = setTimeout(
|
|
213
|
+
() => this.flushBatches(),
|
|
214
|
+
FLUSH_INTERVAL_MS
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
---
|
|
221
|
+
|
|
222
|
+
## 4. Module Reference
|
|
223
|
+
|
|
224
|
+
### 4.1 Orchestrator Module
|
|
225
|
+
|
|
226
|
+
**Entry Point**: `functions/orchestrator/index.js`
|
|
227
|
+
|
|
228
|
+
**Purpose**: High-level task coordination (discovery of new users, scheduling updates).
|
|
229
|
+
|
|
230
|
+
#### 4.1.1 Discovery Orchestration
|
|
231
|
+
|
|
232
|
+
**Function**: `runDiscoveryOrchestrator(config, deps)`
|
|
233
|
+
|
|
234
|
+
**Flow**:
|
|
235
|
+
1. Reset proxy locks (clear stale locks from previous runs)
|
|
236
|
+
2. Check if discovery is needed for normal users
|
|
237
|
+
3. Check if discovery is needed for speculators
|
|
238
|
+
4. Generate candidate CIDs (prioritized + random)
|
|
239
|
+
5. Publish discovery tasks to Pub/Sub
|
|
240
|
+
|
|
241
|
+
**Key Concepts**:
|
|
242
|
+
- **Blocks**: Users are grouped into 1M blocks (e.g., 0M = users 0-999,999)
|
|
243
|
+
- **Target Capacity**: Each block should have ~500 monitored users
|
|
244
|
+
- **Prioritized Discovery**: For speculators, existing normal users holding speculator assets are checked first
|
|
245
|
+
- **Random Discovery**: Random CIDs within blocks are tested
|
|
246
|
+
|
|
247
|
+
#### 4.1.2 Update Orchestration
|
|
248
|
+
|
|
249
|
+
**Function**: `runUpdateOrchestrator(config, deps)`
|
|
250
|
+
|
|
251
|
+
**Flow**:
|
|
252
|
+
1. Reset proxy locks
|
|
253
|
+
2. Query timestamp documents for stale users (normal users: updated >24h ago)
|
|
254
|
+
3. Query speculator blocks for stale users (verified >24h ago AND held assets recently)
|
|
255
|
+
4. Publish update tasks to dispatcher
|
|
256
|
+
|
|
257
|
+
**Thresholds**:
|
|
258
|
+
```javascript
|
|
259
|
+
const thresholds = {
|
|
260
|
+
dateThreshold: startOfTodayUTC, // Users updated before today
|
|
261
|
+
gracePeriodThreshold: 30DaysAgoUTC // Speculators who held assets in last 30 days
|
|
262
|
+
};
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
### 4.2 Dispatcher Module
|
|
266
|
+
|
|
267
|
+
**Entry Point**: `functions/dispatcher/index.js`
|
|
268
|
+
|
|
269
|
+
**Purpose**: Rate-limit task submission to prevent GCP scaling issues.
|
|
270
|
+
|
|
271
|
+
**Why Needed?**
|
|
272
|
+
- Orchestrator generates 10,000+ tasks instantly
|
|
273
|
+
- Directly publishing causes:
|
|
274
|
+
- GCP quota errors
|
|
275
|
+
- Firestore rate limit issues
|
|
276
|
+
- Cost spikes
|
|
277
|
+
|
|
278
|
+
**Solution**: Dispatcher batches tasks and publishes with delays:
|
|
279
|
+
|
|
280
|
+
```javascript
|
|
281
|
+
const config = {
|
|
282
|
+
batchSize: 50, // Tasks per Pub/Sub message
|
|
283
|
+
batchDelayMs: 100 // Delay between batches
|
|
284
|
+
};
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
**Flow**:
|
|
288
|
+
1. Receive batch of tasks from orchestrator
|
|
289
|
+
2. Group into sub-batches of 50 tasks
|
|
290
|
+
3. Publish 1 Pub/Sub message per sub-batch
|
|
291
|
+
4. Sleep 100ms between publishes
|
|
292
|
+
|
|
293
|
+
### 4.3 Task Engine Module
|
|
294
|
+
|
|
295
|
+
**Entry Point**: `functions/task-engine/handler_creator.js`
|
|
296
|
+
|
|
297
|
+
**Purpose**: Worker that executes individual user tasks (discover, verify, update).
|
|
298
|
+
|
|
299
|
+
#### 4.3.1 Task Types
|
|
300
|
+
|
|
301
|
+
##### Discover Task
|
|
302
|
+
**Purpose**: Test if random CIDs are public & active users
|
|
303
|
+
|
|
304
|
+
**Flow**:
|
|
305
|
+
1. Receive batch of CIDs (e.g., [123456, 234567, ...])
|
|
306
|
+
2. POST to eToro Rankings API (returns public users only)
|
|
307
|
+
3. Filter for active users (traded in last 30 days, non-zero exposure)
|
|
308
|
+
4. Apply speculator heuristic (if userType='speculator'):
|
|
309
|
+
```javascript
|
|
310
|
+
const isLikelySpeculator = (
|
|
311
|
+
(trades > 500) ||
|
|
312
|
+
(totalTradedInstruments > 50) ||
|
|
313
|
+
(mediumLeveragePct + highLeveragePct > 50) ||
|
|
314
|
+
(weeklyDrawdown < -25)
|
|
315
|
+
);
|
|
316
|
+
```
|
|
317
|
+
5. Chain to 'verify' task for active users
|
|
318
|
+
|
|
319
|
+
**Output**: Publishes 'verify' task with usernames
|
|
320
|
+
|
|
321
|
+
##### Verify Task
|
|
322
|
+
**Purpose**: Fetch full portfolios to confirm user type
|
|
323
|
+
|
|
324
|
+
**Flow**:
|
|
325
|
+
1. Receive users from discover task
|
|
326
|
+
2. Sequentially fetch each user's portfolio (to avoid rate limits)
|
|
327
|
+
3. For speculators: Check if they hold target instruments
|
|
328
|
+
4. Write to Firestore:
|
|
329
|
+
- Block document (`SpeculatorBlocks/{blockId}` or `NormalUserPortfolios/{blockId}`)
|
|
330
|
+
- Bronze status document (if user is Bronze tier)
|
|
331
|
+
- Username map shards (for future lookups)
|
|
332
|
+
- Block count increments
|
|
333
|
+
|
|
334
|
+
**Output**: Users stored in appropriate collections
|
|
335
|
+
|
|
336
|
+
##### Update Task
|
|
337
|
+
**Purpose**: Refresh existing user's portfolio & trade history
|
|
338
|
+
|
|
339
|
+
**Flow**:
|
|
340
|
+
1. Lookup username from cache (or fetch if missing)
|
|
341
|
+
2. **History Fetch** (once per user per invocation):
|
|
342
|
+
```javascript
|
|
343
|
+
if (!batchManager.checkAndSetHistoryFetched(userId)) {
|
|
344
|
+
// Fetch trade history from eToro
|
|
345
|
+
// Store in trading_history collection
|
|
346
|
+
}
|
|
347
|
+
```
|
|
348
|
+
3. **Portfolio Fetch** (per instrument for speculators):
|
|
349
|
+
```javascript
|
|
350
|
+
// Normal user: 1 fetch (full portfolio)
|
|
351
|
+
// Speculator: N fetches (1 per instrument they hold)
|
|
352
|
+
```
|
|
353
|
+
4. Accumulate in batch manager
|
|
354
|
+
5. Update timestamp documents
|
|
355
|
+
|
|
356
|
+
**Critical**: All fetches are **sequential** (concurrency = 1) to avoid AppScript rate limits.
|
|
357
|
+
|
|
358
|
+
#### 4.3.2 Fallback Mechanism
|
|
359
|
+
|
|
360
|
+
All API calls use a **two-tier fallback**:
|
|
361
|
+
|
|
362
|
+
```javascript
|
|
363
|
+
try {
|
|
364
|
+
// Tier 1: AppScript proxy (preferred)
|
|
365
|
+
response = await proxyManager.fetch(url, options);
|
|
366
|
+
} catch (proxyError) {
|
|
367
|
+
// Tier 2: Direct fetch via GCP IPs
|
|
368
|
+
response = await fetch(url, options);
|
|
369
|
+
}
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
**Why?**
|
|
373
|
+
- AppScript proxies have better rate limit tolerance
|
|
374
|
+
- Direct fetch used as emergency fallback
|
|
375
|
+
- Only AppScript failures penalize header performance
|
|
376
|
+
|
|
377
|
+
### 4.4 Computation System Module
|
|
378
|
+
|
|
379
|
+
**Entry Point**: `functions/computation-system/helpers/computation_pass_runner.js`
|
|
380
|
+
|
|
381
|
+
**Purpose**: Executes mathematical computations on stored portfolio data.
|
|
382
|
+
|
|
383
|
+
#### 4.4.1 Architecture
|
|
384
|
+
|
|
385
|
+
**Manifest Builder** (`computation_manifest_builder.js`):
|
|
386
|
+
- Introspects all calculation classes
|
|
387
|
+
- Validates dependencies
|
|
388
|
+
- Performs topological sort
|
|
389
|
+
- Determines execution passes
|
|
390
|
+
|
|
391
|
+
**Pass Runner** (`computation_pass_runner.js`):
|
|
392
|
+
- Loads manifest for specific pass
|
|
393
|
+
- Checks data availability
|
|
394
|
+
- Streams portfolio data in chunks
|
|
395
|
+
- Executes computations
|
|
396
|
+
- Writes results to Firestore
|
|
397
|
+
|
|
398
|
+
#### 4.4.2 Calculation Classes
|
|
399
|
+
|
|
400
|
+
All calculations extend this pattern:
|
|
401
|
+
|
|
402
|
+
```javascript
|
|
403
|
+
class MyCalculation {
|
|
404
|
+
constructor() {
|
|
405
|
+
this.results = {};
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
static getMetadata() {
|
|
409
|
+
return {
|
|
410
|
+
name: "my-calculation",
|
|
411
|
+
category: "core", // or 'gem', 'gauss', etc.
|
|
412
|
+
type: "standard", // or 'meta'
|
|
413
|
+
isHistorical: false, // true if needs yesterday's data
|
|
414
|
+
rootDataDependencies: ["portfolio"], // 'insights', 'social', 'history'
|
|
415
|
+
userType: "all" // 'normal', 'speculator', or 'all'
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
static getDependencies() {
|
|
420
|
+
return ["other-calculation"]; // Names of required computations
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
static getSchema() {
|
|
424
|
+
return {
|
|
425
|
+
"TICKER": {
|
|
426
|
+
"metric": 0.0,
|
|
427
|
+
"otherField": "string"
|
|
428
|
+
}
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
async process(context) {
|
|
433
|
+
const { user, math, computed, previousComputed } = context;
|
|
434
|
+
|
|
435
|
+
// Access current user's portfolio
|
|
436
|
+
const positions = math.extract.getPositions(
|
|
437
|
+
user.portfolio.today,
|
|
438
|
+
user.type
|
|
439
|
+
);
|
|
440
|
+
|
|
441
|
+
// Use math primitives
|
|
442
|
+
for (const pos of positions) {
|
|
443
|
+
const pnl = math.extract.getNetProfit(pos);
|
|
444
|
+
const ticker = context.mappings.instrumentToTicker[
|
|
445
|
+
math.extract.getInstrumentId(pos)
|
|
446
|
+
];
|
|
447
|
+
|
|
448
|
+
this.results[ticker] = { pnl };
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
async getResult() {
|
|
453
|
+
return this.results;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
#### 4.4.3 Math Primitives Layer
|
|
459
|
+
|
|
460
|
+
**File**: `functions/computation-system/layers/math_primitives.js`
|
|
461
|
+
|
|
462
|
+
**Purpose**: Single source of truth for data extraction and mathematical operations.
|
|
463
|
+
|
|
464
|
+
**Key Classes**:
|
|
465
|
+
|
|
466
|
+
##### DataExtractor
|
|
467
|
+
```javascript
|
|
468
|
+
// Schema-agnostic position access
|
|
469
|
+
const positions = DataExtractor.getPositions(portfolio, userType);
|
|
470
|
+
const instrumentId = DataExtractor.getInstrumentId(position);
|
|
471
|
+
const netProfit = DataExtractor.getNetProfit(position);
|
|
472
|
+
const weight = DataExtractor.getPositionWeight(position, userType);
|
|
473
|
+
|
|
474
|
+
// Speculator-specific
|
|
475
|
+
const leverage = DataExtractor.getLeverage(position);
|
|
476
|
+
const openRate = DataExtractor.getOpenRate(position);
|
|
477
|
+
const stopLoss = DataExtractor.getStopLossRate(position);
|
|
478
|
+
```
|
|
479
|
+
|
|
480
|
+
##### MathPrimitives
|
|
481
|
+
```javascript
|
|
482
|
+
// Statistical functions
|
|
483
|
+
const avg = MathPrimitives.average(values);
|
|
484
|
+
const median = MathPrimitives.median(values);
|
|
485
|
+
const stdDev = MathPrimitives.standardDeviation(values);
|
|
486
|
+
|
|
487
|
+
// Risk calculations
|
|
488
|
+
const hitProbability = MathPrimitives.calculateHitProbability(
|
|
489
|
+
currentPrice,
|
|
490
|
+
stopLossPrice,
|
|
491
|
+
volatility,
|
|
492
|
+
daysAhead
|
|
493
|
+
);
|
|
494
|
+
|
|
495
|
+
// Monte Carlo simulation
|
|
496
|
+
const futurePrices = MathPrimitives.simulateGBM(
|
|
497
|
+
currentPrice,
|
|
498
|
+
volatility,
|
|
499
|
+
days,
|
|
500
|
+
simulations
|
|
501
|
+
);
|
|
502
|
+
```
|
|
503
|
+
|
|
504
|
+
##### HistoryExtractor
|
|
505
|
+
```javascript
|
|
506
|
+
const history = HistoryExtractor.getDailyHistory(user);
|
|
507
|
+
const assets = HistoryExtractor.getTradedAssets(history);
|
|
508
|
+
const summary = HistoryExtractor.getSummary(history);
|
|
509
|
+
```
|
|
510
|
+
|
|
511
|
+
##### SignalPrimitives
|
|
512
|
+
```javascript
|
|
513
|
+
// Access dependency results
|
|
514
|
+
const metric = SignalPrimitives.getMetric(
|
|
515
|
+
dependencies,
|
|
516
|
+
"calc-name",
|
|
517
|
+
"TICKER",
|
|
518
|
+
"fieldName"
|
|
519
|
+
);
|
|
520
|
+
|
|
521
|
+
// Get previous state (for time-series)
|
|
522
|
+
const prevValue = SignalPrimitives.getPreviousState(
|
|
523
|
+
previousComputed,
|
|
524
|
+
"calc-name",
|
|
525
|
+
"TICKER",
|
|
526
|
+
"fieldName"
|
|
527
|
+
);
|
|
528
|
+
```
|
|
529
|
+
|
|
530
|
+
#### 4.4.4 Execution Flow
|
|
531
|
+
|
|
532
|
+
1. **Manifest Generation**:
|
|
533
|
+
```javascript
|
|
534
|
+
const manifest = buildManifest(
|
|
535
|
+
["core", "gem"], // Product lines to include
|
|
536
|
+
calculations // Imported calculation classes
|
|
537
|
+
);
|
|
538
|
+
```
|
|
539
|
+
|
|
540
|
+
2. **Pass Execution**:
|
|
541
|
+
```javascript
|
|
542
|
+
await runComputationPass(config, dependencies, manifest);
|
|
543
|
+
```
|
|
544
|
+
|
|
545
|
+
3. **Data Streaming**:
|
|
546
|
+
```javascript
|
|
547
|
+
// Stream portfolio data in chunks of 50 users
|
|
548
|
+
for await (const chunk of streamPortfolioData(...)) {
|
|
549
|
+
await Promise.all(calcs.map(c =>
|
|
550
|
+
controller.executor.executePerUser(c, metadata, dateStr, chunk, ...)
|
|
551
|
+
));
|
|
552
|
+
}
|
|
553
|
+
```
|
|
554
|
+
|
|
555
|
+
4. **Result Storage**:
|
|
556
|
+
```
|
|
557
|
+
Collection: unified_insights
|
|
558
|
+
└── Date: 2025-01-15
|
|
559
|
+
└── results/
|
|
560
|
+
└── Category: core
|
|
561
|
+
└── computations/
|
|
562
|
+
└── calculation-name: { data }
|
|
563
|
+
```
|
|
564
|
+
|
|
565
|
+
5. **Status Tracking**:
|
|
566
|
+
```javascript
|
|
567
|
+
// Global status document (NEW)
|
|
568
|
+
{
|
|
569
|
+
"2025-01-15": {
|
|
570
|
+
"calculation-a": true,
|
|
571
|
+
"calculation-b": false, // Failed or not run
|
|
572
|
+
...
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
```
|
|
576
|
+
|
|
577
|
+
#### 4.4.5 Computation Passes
|
|
578
|
+
|
|
579
|
+
**Why Passes?**
|
|
580
|
+
- Some calculations depend on others
|
|
581
|
+
- Must execute in dependency order
|
|
582
|
+
- Passes enable parallel execution within each level
|
|
583
|
+
|
|
584
|
+
**Example**:
|
|
585
|
+
```
|
|
586
|
+
Pass 1: [raw-data-extractors] (no dependencies)
|
|
587
|
+
Pass 2: [aggregators] (depends on Pass 1)
|
|
588
|
+
Pass 3: [signals] (depends on Pass 2)
|
|
589
|
+
```
|
|
590
|
+
|
|
591
|
+
**Configuration**:
|
|
592
|
+
```javascript
|
|
593
|
+
const config = {
|
|
594
|
+
COMPUTATION_PASS_TO_RUN: "1", // Set via environment variable
|
|
595
|
+
// Cloud Scheduler runs 3 separate functions:
|
|
596
|
+
// - computation-pass-1 (COMPUTATION_PASS_TO_RUN=1)
|
|
597
|
+
// - computation-pass-2 (COMPUTATION_PASS_TO_RUN=2)
|
|
598
|
+
// - computation-pass-3 (COMPUTATION_PASS_TO_RUN=3)
|
|
599
|
+
};
|
|
600
|
+
```
|
|
601
|
+
|
|
602
|
+
### 4.5 Generic API Module
|
|
603
|
+
|
|
604
|
+
**Entry Point**: `functions/generic-api/index.js`
|
|
605
|
+
|
|
606
|
+
**Purpose**: REST API for accessing computed insights.
|
|
607
|
+
|
|
608
|
+
#### 4.5.1 Endpoints
|
|
609
|
+
|
|
610
|
+
##### GET /
|
|
611
|
+
**Query Parameters**:
|
|
612
|
+
- `computations`: Comma-separated list of calculation names
|
|
613
|
+
- `startDate`: YYYY-MM-DD
|
|
614
|
+
- `endDate`: YYYY-MM-DD
|
|
615
|
+
|
|
616
|
+
**Example**:
|
|
617
|
+
```
|
|
618
|
+
GET /?computations=sentiment-score,momentum&startDate=2025-01-10&endDate=2025-01-15
|
|
619
|
+
```
|
|
620
|
+
|
|
621
|
+
**Response**:
|
|
622
|
+
```json
|
|
623
|
+
{
|
|
624
|
+
"status": "success",
|
|
625
|
+
"metadata": {
|
|
626
|
+
"computations": ["sentiment-score", "momentum"],
|
|
627
|
+
"startDate": "2025-01-10",
|
|
628
|
+
"endDate": "2025-01-15"
|
|
629
|
+
},
|
|
630
|
+
"data": {
|
|
631
|
+
"2025-01-10": {
|
|
632
|
+
"sentiment-score": { "AAPL": 0.75, ... },
|
|
633
|
+
"momentum": { "AAPL": 1.2, ... }
|
|
634
|
+
},
|
|
635
|
+
...
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
```
|
|
639
|
+
|
|
640
|
+
##### GET /manifest
|
|
641
|
+
**Purpose**: List all available computations with schemas
|
|
642
|
+
|
|
643
|
+
**Response**:
|
|
644
|
+
```json
|
|
645
|
+
{
|
|
646
|
+
"status": "success",
|
|
647
|
+
"summary": {
|
|
648
|
+
"totalComputations": 127,
|
|
649
|
+
"schemasAvailable": 127
|
|
650
|
+
},
|
|
651
|
+
"manifest": {
|
|
652
|
+
"calculation-name": {
|
|
653
|
+
"category": "core",
|
|
654
|
+
"structure": { "TICKER": { "field": 0 } },
|
|
655
|
+
"metadata": { ... },
|
|
656
|
+
"lastUpdated": "2025-01-15T..."
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
```
|
|
661
|
+
|
|
662
|
+
##### GET /manifest/:computationName
|
|
663
|
+
**Purpose**: Get detailed schema for specific computation
|
|
664
|
+
|
|
665
|
+
##### POST /manifest/generate/:computationName
|
|
666
|
+
**Purpose**: Manually trigger schema generation for a calculation
|
|
667
|
+
|
|
668
|
+
##### GET /structure/:computationName
|
|
669
|
+
**Purpose**: Get example data structure from latest stored result
|
|
670
|
+
|
|
671
|
+
##### GET /list-computations
|
|
672
|
+
**Purpose**: Simple list of all computation keys
|
|
673
|
+
|
|
674
|
+
#### 4.5.2 Caching
|
|
675
|
+
|
|
676
|
+
API responses are cached in-memory for 10 minutes:
|
|
677
|
+
|
|
678
|
+
```javascript
|
|
679
|
+
const CACHE = {};
|
|
680
|
+
const CACHE_TTL_MS = 10 * 60 * 1000;
|
|
681
|
+
|
|
682
|
+
// On cache HIT: Return from memory (no DB query)
|
|
683
|
+
// On cache MISS: Query DB, cache response
|
|
684
|
+
```
|
|
685
|
+
|
|
686
|
+
### 4.6 Maintenance Modules
|
|
687
|
+
|
|
688
|
+
#### 4.6.1 Speculator Cleanup
|
|
689
|
+
|
|
690
|
+
**Purpose**: Remove inactive speculators to free up monitoring slots
|
|
691
|
+
|
|
692
|
+
**Logic**:
|
|
693
|
+
1. Query `PendingSpeculators` for users older than grace period (12 hours)
|
|
694
|
+
2. Query `SpeculatorBlocks` for users who haven't held assets in 30 days
|
|
695
|
+
3. Batch delete stale users
|
|
696
|
+
4. Decrement block counts
|
|
697
|
+
|
|
698
|
+
#### 4.6.2 Invalid Speculator Handler
|
|
699
|
+
|
|
700
|
+
**Purpose**: Log CIDs that are private or don't meet speculator criteria
|
|
701
|
+
|
|
702
|
+
**Why?**
|
|
703
|
+
- Prevents re-discovery of known invalid users
|
|
704
|
+
- Used for analytics (discovery success rate)
|
|
705
|
+
|
|
706
|
+
#### 4.6.3 Fetch Insights
|
|
707
|
+
|
|
708
|
+
**Purpose**: Daily fetch of eToro's instrument insights
|
|
709
|
+
|
|
710
|
+
**Data Fetched**:
|
|
711
|
+
- Market trends
|
|
712
|
+
- Volatility metrics
|
|
713
|
+
- Trading volume
|
|
714
|
+
- Sentiment indicators
|
|
715
|
+
|
|
716
|
+
**Storage**:
|
|
717
|
+
```
|
|
718
|
+
Collection: daily_instrument_insights
|
|
719
|
+
└── Document: YYYY-MM-DD
|
|
720
|
+
└── insights: [ {...}, {...}, ... ]
|
|
721
|
+
```
|
|
722
|
+
|
|
723
|
+
#### 4.6.4 Price Fetcher
|
|
724
|
+
|
|
725
|
+
**Purpose**: Daily fetch of closing prices
|
|
726
|
+
|
|
727
|
+
**Storage**:
|
|
728
|
+
```
|
|
729
|
+
Collection: asset_prices
|
|
730
|
+
└── Document: shard_N (N = instrumentId % 40)
|
|
731
|
+
└── {
|
|
732
|
+
"10000": {
|
|
733
|
+
"ticker": "AAPL",
|
|
734
|
+
"prices": {
|
|
735
|
+
"2025-01-15": 150.25,
|
|
736
|
+
"2025-01-14": 149.80,
|
|
737
|
+
...
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
```
|
|
742
|
+
|
|
743
|
+
#### 4.6.5 Social Orchestrator & Task Handler
|
|
744
|
+
|
|
745
|
+
**Purpose**: Fetch and analyze social posts from eToro
|
|
746
|
+
|
|
747
|
+
**Flow**:
|
|
748
|
+
1. **Orchestrator**: Publishes 1 task per target ticker
|
|
749
|
+
2. **Task Handler**:
|
|
750
|
+
- Fetches posts since last run (12-hour window)
|
|
751
|
+
- Deduplicates via `processed_posts` collection
|
|
752
|
+
- Filters to English posts
|
|
753
|
+
- Calls Gemini AI for sentiment analysis
|
|
754
|
+
- Extracts topics (e.g., "FOMC", "Earnings", "CPI")
|
|
755
|
+
- Stores in `daily_social_insights/{date}/posts/{postId}`
|
|
756
|
+
|
|
757
|
+
**Gemini Analysis**:
|
|
758
|
+
```javascript
|
|
759
|
+
const prompt = `Analyze this post. Return JSON:
|
|
760
|
+
{
|
|
761
|
+
"overallSentiment": "Bullish|Bearish|Neutral",
|
|
762
|
+
"topics": ["FOMC", "Inflation", ...]
|
|
763
|
+
}`;
|
|
764
|
+
|
|
765
|
+
const result = await geminiModel.generateContent(prompt);
|
|
766
|
+
```
|
|
767
|
+
|
|
768
|
+
---
|
|
769
|
+
|
|
770
|
+
## 5. Data Flow & Pipelines
|
|
771
|
+
|
|
772
|
+
### 5.1 Daily Orchestration Schedule
|
|
773
|
+
|
|
774
|
+
```
|
|
775
|
+
00:00 UTC - Update Orchestrator (normal users)
|
|
776
|
+
00:30 UTC - Update Orchestrator (speculators)
|
|
777
|
+
01:00 UTC - Discovery Orchestrator (if needed)
|
|
778
|
+
02:00 UTC - Fetch Insights
|
|
779
|
+
02:30 UTC - Fetch Prices
|
|
780
|
+
03:00 UTC - Computation Pass 1
|
|
781
|
+
04:00 UTC - Computation Pass 2
|
|
782
|
+
05:00 UTC - Computation Pass 3
|
|
783
|
+
06:00 UTC - Social Orchestrator
|
|
784
|
+
12:00 UTC - Speculator Cleanup
|
|
785
|
+
```
|
|
786
|
+
|
|
787
|
+
### 5.2 User Discovery Pipeline
|
|
788
|
+
|
|
789
|
+
```
|
|
790
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
791
|
+
│ 1. Orchestrator: Check block capacities │
|
|
792
|
+
│ - Are there <500 users in any block? │
|
|
793
|
+
└────────────────┬────────────────────────────────────────────┘
|
|
794
|
+
│ YES
|
|
795
|
+
▼
|
|
796
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
797
|
+
│ 2. Orchestrator: Generate candidate CIDs │
|
|
798
|
+
│ - Prioritized: Normal users holding speculator assets │
|
|
799
|
+
│ - Random: Random CIDs within target blocks │
|
|
800
|
+
└────────────────┬────────────────────────────────────────────┘
|
|
801
|
+
│
|
|
802
|
+
▼
|
|
803
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
804
|
+
│ 3. Dispatcher: Batch tasks (50 per message, 100ms delay) │
|
|
805
|
+
└────────────────┬────────────────────────────────────────────┘
|
|
806
|
+
│
|
|
807
|
+
▼
|
|
808
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
809
|
+
│ 4. Task Engine (Discover): Test CIDs │
|
|
810
|
+
│ - POST to Rankings API │
|
|
811
|
+
│ - Filter for active users │
|
|
812
|
+
│ - Apply speculator heuristic (if applicable) │
|
|
813
|
+
└────────────────┬────────────────────────────────────────────┘
|
|
814
|
+
│
|
|
815
|
+
▼
|
|
816
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
817
|
+
│ 5. Task Engine (Verify): Fetch portfolios │
|
|
818
|
+
│ - Confirm user type │
|
|
819
|
+
│ - Check instrument holdings (speculators) │
|
|
820
|
+
│ - Write to Firestore blocks │
|
|
821
|
+
└─────────────────────────────────────────────────────────────┘
|
|
822
|
+
```
|
|
823
|
+
|
|
824
|
+
### 5.3 User Update Pipeline
|
|
825
|
+
|
|
826
|
+
```
|
|
827
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
828
|
+
│ 1. Orchestrator: Query timestamp documents │
|
|
829
|
+
│ - Normal: lastUpdated < startOfToday │
|
|
830
|
+
│ - Speculator: lastVerified < startOfToday │
|
|
831
|
+
│ AND lastHeldAsset > 30DaysAgo │
|
|
832
|
+
└────────────────┬────────────────────────────────────────────┘
|
|
833
|
+
│
|
|
834
|
+
▼
|
|
835
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
836
|
+
│ 2. Dispatcher: Batch update tasks │
|
|
837
|
+
└────────────────┬────────────────────────────────────────────┘
|
|
838
|
+
│
|
|
839
|
+
▼
|
|
840
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
841
|
+
│ 3. Task Engine: Prepare tasks │
|
|
842
|
+
│ - Lookup usernames from cache │
|
|
843
|
+
│ - If missing, bulk fetch from Rankings API │
|
|
844
|
+
└────────────────┬────────────────────────────────────────────┘
|
|
845
|
+
│
|
|
846
|
+
▼
|
|
847
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
848
|
+
│ 4. Task Engine (Update): Sequential execution │
|
|
849
|
+
│ - Fetch trade history (once per user) │
|
|
850
|
+
│ - Fetch portfolio (1x normal, Nx speculator) │
|
|
851
|
+
│ - Accumulate in batch manager │
|
|
852
|
+
└────────────────┬────────────────────────────────────────────┘
|
|
853
|
+
│
|
|
854
|
+
▼
|
|
855
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
856
|
+
│ 5. Task Engine: Flush batches │
|
|
857
|
+
│ - Write all data to Firestore │
|
|
858
|
+
│ - Update timestamps │
|
|
859
|
+
└─────────────────────────────────────────────────────────────┘
|
|
860
|
+
```
|
|
861
|
+
|
|
862
|
+
### 5.4 Computation Pipeline
|
|
863
|
+
|
|
864
|
+
```
|
|
865
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
866
|
+
│ 1. Build Manifest │
|
|
867
|
+
│ - Introspect calculation classes │
|
|
868
|
+
│ - Validate dependencies │
|
|
869
|
+
│ - Topological sort │
|
|
870
|
+
└────────────────┬────────────────────────────────────────────┘
|
|
871
|
+
│
|
|
872
|
+
▼
|
|
873
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
874
|
+
│ 2. Load Global Status │
|
|
875
|
+
│ - Check which calcs completed yesterday │
|
|
876
|
+
└────────────────┬────────────────────────────────────────────┘
|
|
877
|
+
│
|
|
878
|
+
▼
|
|
879
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
880
|
+
│ 3. For each date (from earliest data to yesterday): │
|
|
881
|
+
│ ┌───────────────────────────────────────────────────┐ │
|
|
882
|
+
│ │ 3a. Check root data availability │ │
|
|
883
|
+
│ │ - Portfolio snapshots exist? │ │
|
|
884
|
+
│ │ - Insights fetched? │ │
|
|
885
|
+
│ │ - Social data available? │ │
|
|
886
|
+
│ └────────────────┬──────────────────────────────────┘ │
|
|
887
|
+
│ │ │
|
|
888
|
+
│ ▼ │
|
|
889
|
+
│ ┌───────────────────────────────────────────────────┐ │
|
|
890
|
+
│ │ 3b. Filter calculations │ │
|
|
891
|
+
│ │ - Skip if already completed │ │
|
|
892
|
+
│ │ - Skip if dependencies not met │ │
|
|
893
|
+
│ │ - Skip if root data missing │ │
|
|
894
|
+
│ └────────────────┬──────────────────────────────────┘ │
|
|
895
|
+
│ │ │
|
|
896
|
+
│ ▼ │
|
|
897
|
+
│ ┌───────────────────────────────────────────────────┐ │
|
|
898
|
+
│ │ 3c. Stream portfolio data │ │
|
|
899
|
+
│ │ - Load in chunks of 50 users │ │
|
|
900
|
+
│ │ - Execute computations on each chunk │ │
|
|
901
|
+
│ └────────────────┬──────────────────────────────────┘ │
|
|
902
|
+
│ │ │
|
|
903
|
+
│ ▼ │
|
|
904
|
+
│ ┌───────────────────────────────────────────────────┐ │
|
|
905
|
+
│ │ 3d. Commit results │ │
|
|
906
|
+
│ │ - Store to unified_insights │ │
|
|
907
|
+
│ │ - Update status document │ │
|
|
908
|
+
│ └───────────────────────────────────────────────────┘ │
|
|
909
|
+
└─────────────────────────────────────────────────────────────┘
|
|
910
|
+
```
|
|
911
|
+
|
|
912
|
+
---
|
|
913
|
+
|
|
914
|
+
## 6. Computation System
|
|
915
|
+
|
|
916
|
+
### 6.1 Adding a New Calculation
|
|
917
|
+
|
|
918
|
+
**Step 1**: Create calculation class file
|
|
919
|
+
|
|
920
|
+
```javascript
|
|
921
|
+
// backend/core/calculations/category/newcalc.js
|
|
922
|
+
|
|
923
|
+
class MyNewCalculation {
|
|
924
|
+
constructor() {
|
|
925
|
+
this.results = {};
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
static getMetadata() {
|
|
929
|
+
return {
|
|
930
|
+
name: "my-new-calc",
|
|
931
|
+
category: "core",
|
|
932
|
+
type: "standard",
|
|
933
|
+
isHistorical: false,
|
|
934
|
+
rootDataDependencies: ["portfolio"],
|
|
935
|
+
userType: "all"
|
|
936
|
+
};
|
|
937
|
+
}
|
|
938
|
+
static getDependencies() {
|
|
939
|
+
return []; // Or list other calculation names
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
static getSchema() {
|
|
943
|
+
return {
|
|
944
|
+
"TICKER": {
|
|
945
|
+
"myMetric": 0.0,
|
|
946
|
+
"confidence": 0.0
|
|
947
|
+
}
|
|
948
|
+
};
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
async process(context) {
|
|
952
|
+
const { user, math, mappings } = context;
|
|
953
|
+
|
|
954
|
+
// Get positions
|
|
955
|
+
const positions = math.extract.getPositions(
|
|
956
|
+
user.portfolio.today,
|
|
957
|
+
user.type
|
|
958
|
+
);
|
|
959
|
+
|
|
960
|
+
// Process each position
|
|
961
|
+
for (const pos of positions) {
|
|
962
|
+
const instId = math.extract.getInstrumentId(pos);
|
|
963
|
+
const ticker = mappings.instrumentToTicker[instId];
|
|
964
|
+
|
|
965
|
+
if (!ticker) continue;
|
|
966
|
+
|
|
967
|
+
// Your calculation logic here
|
|
968
|
+
const netProfit = math.extract.getNetProfit(pos);
|
|
969
|
+
const weight = math.extract.getPositionWeight(pos);
|
|
970
|
+
|
|
971
|
+
this.results[ticker] = {
|
|
972
|
+
myMetric: netProfit * weight,
|
|
973
|
+
confidence: weight > 10 ? 0.9 : 0.5
|
|
974
|
+
};
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
async getResult() {
|
|
979
|
+
return this.results;
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
```
|
|
983
|
+
|
|
984
|
+
```
|
|
985
|
+
|
|
986
|
+
**Step 3**: The manifest builder will automatically:
|
|
987
|
+
- Discover your calculation
|
|
988
|
+
- Validate its metadata
|
|
989
|
+
- Determine its execution pass based on dependencies
|
|
990
|
+
- Include it in the manifest
|
|
991
|
+
|
|
992
|
+
**Step 4**: Deploy and run
|
|
993
|
+
|
|
994
|
+
```bash
|
|
995
|
+
npm version patch
|
|
996
|
+
npm publish
|
|
997
|
+
# Update calculations package version number in core package.json
|
|
998
|
+
# Deploy cloud function
|
|
999
|
+
Main computation system will automatically discover the new computation.
|
|
1000
|
+
```
|
|
1001
|
+
|
|
1002
|
+
### 6.2 Using Dependencies
|
|
1003
|
+
|
|
1004
|
+
**Example**: Calculation that depends on another
|
|
1005
|
+
|
|
1006
|
+
```javascript
|
|
1007
|
+
static getDependencies() {
|
|
1008
|
+
return ["sentiment-score", "momentum"];
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
async process(context) {
|
|
1012
|
+
const { computed, math } = context;
|
|
1013
|
+
|
|
1014
|
+
// Access dependency results
|
|
1015
|
+
const sentimentScore = math.signals.getMetric(
|
|
1016
|
+
computed,
|
|
1017
|
+
"sentiment-score",
|
|
1018
|
+
"AAPL",
|
|
1019
|
+
"score"
|
|
1020
|
+
);
|
|
1021
|
+
|
|
1022
|
+
const momentum = math.signals.getMetric(
|
|
1023
|
+
computed,
|
|
1024
|
+
"momentum",
|
|
1025
|
+
"AAPL",
|
|
1026
|
+
"value"
|
|
1027
|
+
);
|
|
1028
|
+
|
|
1029
|
+
// Combine signals
|
|
1030
|
+
this.results["AAPL"] = {
|
|
1031
|
+
combinedSignal: (sentimentScore * 0.6) + (momentum * 0.4)
|
|
1032
|
+
};
|
|
1033
|
+
}
|
|
1034
|
+
```
|
|
1035
|
+
|
|
1036
|
+
### 6.3 Historical Calculations
|
|
1037
|
+
|
|
1038
|
+
**Purpose**: Compare today's data with yesterday's
|
|
1039
|
+
|
|
1040
|
+
```javascript
|
|
1041
|
+
class PortfolioChangeCalculation {
|
|
1042
|
+
static getMetadata() {
|
|
1043
|
+
return {
|
|
1044
|
+
name: "portfolio-change",
|
|
1045
|
+
isHistorical: true, // Requires yesterday's data
|
|
1046
|
+
// ...
|
|
1047
|
+
};
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
async process(context) {
|
|
1051
|
+
const { user, math } = context;
|
|
1052
|
+
|
|
1053
|
+
// Access today's portfolio
|
|
1054
|
+
const todayPositions = math.extract.getPositions(
|
|
1055
|
+
user.portfolio.today,
|
|
1056
|
+
user.type
|
|
1057
|
+
);
|
|
1058
|
+
|
|
1059
|
+
// Access yesterday's portfolio
|
|
1060
|
+
const yesterdayPositions = math.extract.getPositions(
|
|
1061
|
+
user.portfolio.yesterday,
|
|
1062
|
+
user.type
|
|
1063
|
+
);
|
|
1064
|
+
|
|
1065
|
+
// Compare positions
|
|
1066
|
+
// ...
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
```
|
|
1070
|
+
|
|
1071
|
+
### 6.4 Meta Calculations
|
|
1072
|
+
|
|
1073
|
+
**Purpose**: Aggregate across all users (not per-user processing)
|
|
1074
|
+
|
|
1075
|
+
```javascript
|
|
1076
|
+
class MarketWideSentimentCalculation {
|
|
1077
|
+
static getMetadata() {
|
|
1078
|
+
return {
|
|
1079
|
+
type: "meta", // Not per-user
|
|
1080
|
+
category: "core",
|
|
1081
|
+
rootDataDependencies: ["insights", "social"]
|
|
1082
|
+
};
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
static getDependencies() {
|
|
1086
|
+
return ["user-level-sentiment"]; // Depends on per-user calc
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
async process(context) {
|
|
1090
|
+
const { computed, insights, social } = context;
|
|
1091
|
+
|
|
1092
|
+
// Access results from per-user calculations
|
|
1093
|
+
const userSentiments = computed["user-level-sentiment"];
|
|
1094
|
+
|
|
1095
|
+
// Access insights data
|
|
1096
|
+
const marketData = insights.today;
|
|
1097
|
+
|
|
1098
|
+
// Access social data
|
|
1099
|
+
const socialPosts = social.today;
|
|
1100
|
+
|
|
1101
|
+
// Perform market-wide aggregation
|
|
1102
|
+
const tickers = new Set();
|
|
1103
|
+
for (const ticker in userSentiments) {
|
|
1104
|
+
tickers.add(ticker);
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
for (const ticker of tickers) {
|
|
1108
|
+
// Aggregate logic
|
|
1109
|
+
this.results[ticker] = {
|
|
1110
|
+
aggregatedSentiment: // ...
|
|
1111
|
+
};
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
```
|
|
1116
|
+
|
|
1117
|
+
### 6.5 Using Time Series
|
|
1118
|
+
|
|
1119
|
+
**Purpose**: Track moving averages, trends, etc.
|
|
1120
|
+
|
|
1121
|
+
```javascript
|
|
1122
|
+
class MovingAverageSentiment {
|
|
1123
|
+
static getDependencies() {
|
|
1124
|
+
return ["sentiment-score"];
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
async process(context) {
|
|
1128
|
+
const { computed, previousComputed, math } = context;
|
|
1129
|
+
|
|
1130
|
+
const currentScore = math.signals.getMetric(
|
|
1131
|
+
computed,
|
|
1132
|
+
"sentiment-score",
|
|
1133
|
+
"AAPL",
|
|
1134
|
+
"score"
|
|
1135
|
+
);
|
|
1136
|
+
|
|
1137
|
+
// Get previous state (EMA parameters)
|
|
1138
|
+
const prevState = math.signals.getPreviousState(
|
|
1139
|
+
previousComputed,
|
|
1140
|
+
"moving-average-sentiment",
|
|
1141
|
+
"AAPL",
|
|
1142
|
+
"_state"
|
|
1143
|
+
);
|
|
1144
|
+
|
|
1145
|
+
// Update EMA
|
|
1146
|
+
const newState = math.TimeSeries.updateEMAState(
|
|
1147
|
+
currentScore,
|
|
1148
|
+
prevState || { mean: 0, variance: 1 },
|
|
1149
|
+
0.1 // alpha (decay factor)
|
|
1150
|
+
);
|
|
1151
|
+
|
|
1152
|
+
this.results["AAPL"] = {
|
|
1153
|
+
ema: newState.mean,
|
|
1154
|
+
volatility: Math.sqrt(newState.variance),
|
|
1155
|
+
_state: newState // Store for next day
|
|
1156
|
+
};
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
```
|
|
1160
|
+
|
|
1161
|
+
### 6.6 Advanced: Monte Carlo Risk Analysis
|
|
1162
|
+
|
|
1163
|
+
```javascript
|
|
1164
|
+
class StopLossRiskCalculation {
|
|
1165
|
+
async process(context) {
|
|
1166
|
+
const { user, math, prices } = context;
|
|
1167
|
+
|
|
1168
|
+
const positions = math.extract.getPositions(
|
|
1169
|
+
user.portfolio.today,
|
|
1170
|
+
user.type
|
|
1171
|
+
);
|
|
1172
|
+
|
|
1173
|
+
for (const pos of positions) {
|
|
1174
|
+
const instId = math.extract.getInstrumentId(pos);
|
|
1175
|
+
const ticker = context.mappings.instrumentToTicker[instId];
|
|
1176
|
+
|
|
1177
|
+
// Get current price and stop loss
|
|
1178
|
+
const currentRate = math.extract.getCurrentRate(pos);
|
|
1179
|
+
const stopLoss = math.extract.getStopLossRate(pos);
|
|
1180
|
+
|
|
1181
|
+
if (!stopLoss || stopLoss === 0) continue;
|
|
1182
|
+
|
|
1183
|
+
// Get historical prices for volatility calculation
|
|
1184
|
+
const priceHistory = math.priceExtractor.getHistory(
|
|
1185
|
+
prices,
|
|
1186
|
+
ticker
|
|
1187
|
+
);
|
|
1188
|
+
|
|
1189
|
+
// Calculate historical volatility
|
|
1190
|
+
const returns = [];
|
|
1191
|
+
for (let i = 1; i < priceHistory.length; i++) {
|
|
1192
|
+
const ret = Math.log(
|
|
1193
|
+
priceHistory[i].price / priceHistory[i-1].price
|
|
1194
|
+
);
|
|
1195
|
+
returns.push(ret);
|
|
1196
|
+
}
|
|
1197
|
+
const volatility = math.compute.standardDeviation(returns) * Math.sqrt(252);
|
|
1198
|
+
|
|
1199
|
+
// Calculate probability of hitting stop loss in next 3 days
|
|
1200
|
+
const hitProbability = math.compute.calculateHitProbability(
|
|
1201
|
+
currentRate,
|
|
1202
|
+
stopLoss,
|
|
1203
|
+
volatility,
|
|
1204
|
+
3, // days
|
|
1205
|
+
0 // drift (risk-neutral)
|
|
1206
|
+
);
|
|
1207
|
+
|
|
1208
|
+
// Run Monte Carlo simulation
|
|
1209
|
+
const simulations = math.compute.simulateGBM(
|
|
1210
|
+
currentRate,
|
|
1211
|
+
volatility,
|
|
1212
|
+
3,
|
|
1213
|
+
1000 // number of paths
|
|
1214
|
+
);
|
|
1215
|
+
|
|
1216
|
+
// Count how many simulations hit stop loss
|
|
1217
|
+
let hitCount = 0;
|
|
1218
|
+
for (const simPrice of simulations) {
|
|
1219
|
+
if (simPrice <= stopLoss) hitCount++;
|
|
1220
|
+
}
|
|
1221
|
+
const empiricalProbability = hitCount / 1000;
|
|
1222
|
+
|
|
1223
|
+
this.results[ticker] = {
|
|
1224
|
+
theoreticalHitProb: hitProbability,
|
|
1225
|
+
empiricalHitProb: empiricalProbability,
|
|
1226
|
+
currentRate,
|
|
1227
|
+
stopLoss,
|
|
1228
|
+
volatility
|
|
1229
|
+
};
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
```
|
|
1234
|
+
|
|
1235
|
+
---
|
|
1236
|
+
|
|
1237
|
+
## 7. Development Guidelines
|
|
1238
|
+
|
|
1239
|
+
### 7.1 Code Style
|
|
1240
|
+
|
|
1241
|
+
#### 7.1.1 Naming Conventions
|
|
1242
|
+
|
|
1243
|
+
```javascript
|
|
1244
|
+
// Calculation names: kebab-case
|
|
1245
|
+
"sentiment-score"
|
|
1246
|
+
"moving-average-momentum"
|
|
1247
|
+
|
|
1248
|
+
// File names: snake_case
|
|
1249
|
+
my_calculation.js
|
|
1250
|
+
data_loader.js
|
|
1251
|
+
|
|
1252
|
+
// Class names: PascalCase
|
|
1253
|
+
class SentimentScoreCalculation { }
|
|
1254
|
+
class IntelligentProxyManager { }
|
|
1255
|
+
|
|
1256
|
+
// Functions: camelCase
|
|
1257
|
+
async function handleRequest() { }
|
|
1258
|
+
function getPositions() { }
|
|
1259
|
+
|
|
1260
|
+
// Constants: SCREAMING_SNAKE_CASE
|
|
1261
|
+
const MAX_RETRIES = 3;
|
|
1262
|
+
const ETORO_API_URL = "https://...";
|
|
1263
|
+
```
|
|
1264
|
+
|
|
1265
|
+
#### 7.1.2 Logging Standards
|
|
1266
|
+
|
|
1267
|
+
```javascript
|
|
1268
|
+
// Always use structured logging
|
|
1269
|
+
logger.log('INFO', '[ModuleName] Descriptive message', {
|
|
1270
|
+
contextKey: contextValue,
|
|
1271
|
+
errorMessage: error.message // if error
|
|
1272
|
+
});
|
|
1273
|
+
|
|
1274
|
+
// Log levels
|
|
1275
|
+
logger.log('TRACE', 'Very detailed debug info');
|
|
1276
|
+
logger.log('INFO', 'General information');
|
|
1277
|
+
logger.log('WARN', 'Warning condition');
|
|
1278
|
+
logger.log('ERROR', 'Error condition');
|
|
1279
|
+
logger.log('SUCCESS', 'Operation completed successfully');
|
|
1280
|
+
|
|
1281
|
+
// Task-specific logging pattern
|
|
1282
|
+
logger.log('INFO', `[TaskType/${taskId}/${userId}] Message`);
|
|
1283
|
+
// Example: [UPDATE/batch-123/456789] Portfolio fetch successful
|
|
1284
|
+
```
|
|
1285
|
+
|
|
1286
|
+
#### 7.1.3 Error Handling
|
|
1287
|
+
|
|
1288
|
+
```javascript
|
|
1289
|
+
// Always wrap API calls in try-catch
|
|
1290
|
+
try {
|
|
1291
|
+
const response = await proxyManager.fetch(url, options);
|
|
1292
|
+
if (!response.ok) {
|
|
1293
|
+
throw new Error(`API error ${response.status}`);
|
|
1294
|
+
}
|
|
1295
|
+
wasSuccess = true;
|
|
1296
|
+
// Process response
|
|
1297
|
+
} catch (error) {
|
|
1298
|
+
logger.log('ERROR', '[Module] Operation failed', {
|
|
1299
|
+
errorMessage: error.message,
|
|
1300
|
+
errorStack: error.stack,
|
|
1301
|
+
context: { userId, url }
|
|
1302
|
+
});
|
|
1303
|
+
wasSuccess = false;
|
|
1304
|
+
} finally {
|
|
1305
|
+
// Always update performance tracking
|
|
1306
|
+
if (selectedHeader) {
|
|
1307
|
+
headerManager.updatePerformance(selectedHeader.id, wasSuccess);
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
```
|
|
1311
|
+
|
|
1312
|
+
#### 7.1.4 Async/Await Best Practices
|
|
1313
|
+
|
|
1314
|
+
```javascript
|
|
1315
|
+
// BAD: Uncontrolled concurrency
|
|
1316
|
+
const promises = users.map(user => fetchUser(user));
|
|
1317
|
+
await Promise.all(promises); // Can overwhelm APIs
|
|
1318
|
+
|
|
1319
|
+
// GOOD: Controlled concurrency
|
|
1320
|
+
const limit = pLimit(1); // Sequential
|
|
1321
|
+
const promises = users.map(user =>
|
|
1322
|
+
limit(() => fetchUser(user))
|
|
1323
|
+
);
|
|
1324
|
+
await Promise.all(promises);
|
|
1325
|
+
|
|
1326
|
+
// GOOD: Batch processing
|
|
1327
|
+
for (let i = 0; i < items.length; i += BATCH_SIZE) {
|
|
1328
|
+
const batch = items.slice(i, i + BATCH_SIZE);
|
|
1329
|
+
await processBatch(batch);
|
|
1330
|
+
await sleep(DELAY_MS); // Rate limiting
|
|
1331
|
+
}
|
|
1332
|
+
```
|
|
1333
|
+
|
|
1334
|
+
### 7.2 Testing Strategy
|
|
1335
|
+
|
|
1336
|
+
#### 7.2.1 Local Testing Setup
|
|
1337
|
+
|
|
1338
|
+
```javascript
|
|
1339
|
+
// test-setup.js
|
|
1340
|
+
const admin = require('firebase-admin');
|
|
1341
|
+
const { pipe } = require('bulltrackers-module');
|
|
1342
|
+
|
|
1343
|
+
// Initialize Firebase Admin
|
|
1344
|
+
admin.initializeApp({
|
|
1345
|
+
credential: admin.credential.applicationDefault(),
|
|
1346
|
+
projectId: 'your-project-id'
|
|
1347
|
+
});
|
|
1348
|
+
|
|
1349
|
+
const db = admin.firestore();
|
|
1350
|
+
const logger = {
|
|
1351
|
+
log: (level, msg, data) => console.log(`[${level}] ${msg}`, data)
|
|
1352
|
+
};
|
|
1353
|
+
|
|
1354
|
+
const dependencies = {
|
|
1355
|
+
db,
|
|
1356
|
+
logger,
|
|
1357
|
+
// ... other dependencies
|
|
1358
|
+
};
|
|
1359
|
+
|
|
1360
|
+
const config = {
|
|
1361
|
+
// Your config
|
|
1362
|
+
};
|
|
1363
|
+
|
|
1364
|
+
// Test a specific function
|
|
1365
|
+
async function testUpdateTask() {
|
|
1366
|
+
const task = {
|
|
1367
|
+
type: 'update',
|
|
1368
|
+
userId: '123456',
|
|
1369
|
+
userType: 'normal'
|
|
1370
|
+
};
|
|
1371
|
+
|
|
1372
|
+
await pipe.taskEngine.handleUpdate(
|
|
1373
|
+
task,
|
|
1374
|
+
'test-task-id',
|
|
1375
|
+
dependencies,
|
|
1376
|
+
config,
|
|
1377
|
+
'testuser'
|
|
1378
|
+
);
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
testUpdateTask().catch(console.error);
|
|
1382
|
+
```
|
|
1383
|
+
|
|
1384
|
+
#### 7.2.2 Unit Test Example
|
|
1385
|
+
|
|
1386
|
+
```javascript
|
|
1387
|
+
// test/math-primitives.test.js
|
|
1388
|
+
const { MathPrimitives } = require('../functions/computation-system/layers/math_primitives');
|
|
1389
|
+
|
|
1390
|
+
describe('MathPrimitives', () => {
|
|
1391
|
+
describe('calculateHitProbability', () => {
|
|
1392
|
+
it('should return 0 for invalid inputs', () => {
|
|
1393
|
+
const result = MathPrimitives.calculateHitProbability(
|
|
1394
|
+
0, 100, 0.3, 5
|
|
1395
|
+
);
|
|
1396
|
+
expect(result).toBe(0);
|
|
1397
|
+
});
|
|
1398
|
+
|
|
1399
|
+
it('should return probability between 0 and 1', () => {
|
|
1400
|
+
const result = MathPrimitives.calculateHitProbability(
|
|
1401
|
+
100, 90, 0.3, 5
|
|
1402
|
+
);
|
|
1403
|
+
expect(result).toBeGreaterThanOrEqual(0);
|
|
1404
|
+
expect(result).toBeLessThanOrEqual(1);
|
|
1405
|
+
});
|
|
1406
|
+
|
|
1407
|
+
it('should return higher probability for closer barriers', () => {
|
|
1408
|
+
const far = MathPrimitives.calculateHitProbability(
|
|
1409
|
+
100, 80, 0.3, 5
|
|
1410
|
+
);
|
|
1411
|
+
const close = MathPrimitives.calculateHitProbability(
|
|
1412
|
+
100, 95, 0.3, 5
|
|
1413
|
+
);
|
|
1414
|
+
expect(close).toBeGreaterThan(far);
|
|
1415
|
+
});
|
|
1416
|
+
});
|
|
1417
|
+
});
|
|
1418
|
+
```
|
|
1419
|
+
|
|
1420
|
+
#### 7.2.3 Integration Test Pattern
|
|
1421
|
+
|
|
1422
|
+
```javascript
|
|
1423
|
+
// test/integration/computation-system.test.js
|
|
1424
|
+
async function testComputationPipeline() {
|
|
1425
|
+
// 1. Setup test data
|
|
1426
|
+
await setupTestPortfolios();
|
|
1427
|
+
|
|
1428
|
+
// 2. Build manifest
|
|
1429
|
+
const calculations = require('aiden-shared-calculations-unified');
|
|
1430
|
+
const manifest = pipe.computationSystem.buildManifest(
|
|
1431
|
+
['core'],
|
|
1432
|
+
calculations
|
|
1433
|
+
);
|
|
1434
|
+
|
|
1435
|
+
// 3. Run computation
|
|
1436
|
+
const config = {
|
|
1437
|
+
COMPUTATION_PASS_TO_RUN: "1",
|
|
1438
|
+
resultsCollection: 'test_unified_insights'
|
|
1439
|
+
};
|
|
1440
|
+
|
|
1441
|
+
await pipe.computationSystem.runComputationPass(
|
|
1442
|
+
config,
|
|
1443
|
+
dependencies,
|
|
1444
|
+
manifest
|
|
1445
|
+
);
|
|
1446
|
+
|
|
1447
|
+
// 4. Verify results
|
|
1448
|
+
const results = await db
|
|
1449
|
+
.collection('test_unified_insights')
|
|
1450
|
+
.doc('2025-01-15')
|
|
1451
|
+
.collection('results')
|
|
1452
|
+
.doc('core')
|
|
1453
|
+
.collection('computations')
|
|
1454
|
+
.doc('test-calculation')
|
|
1455
|
+
.get();
|
|
1456
|
+
|
|
1457
|
+
expect(results.exists).toBe(true);
|
|
1458
|
+
expect(results.data()).toHaveProperty('AAPL');
|
|
1459
|
+
}
|
|
1460
|
+
```
|
|
1461
|
+
|
|
1462
|
+
### 7.3 Performance Optimization
|
|
1463
|
+
|
|
1464
|
+
#### 7.3.1 Firestore Query Optimization
|
|
1465
|
+
|
|
1466
|
+
```javascript
|
|
1467
|
+
// BAD: Multiple individual reads
|
|
1468
|
+
for (const userId of userIds) {
|
|
1469
|
+
const doc = await db.collection('users').doc(userId).get();
|
|
1470
|
+
// Process doc
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
// GOOD: Batch read (up to 500 docs)
|
|
1474
|
+
const docRefs = userIds.map(id => db.collection('users').doc(id));
|
|
1475
|
+
const snapshots = await db.getAll(...docRefs);
|
|
1476
|
+
snapshots.forEach(doc => {
|
|
1477
|
+
// Process doc
|
|
1478
|
+
});
|
|
1479
|
+
```
|
|
1480
|
+
|
|
1481
|
+
#### 7.3.2 Computation Optimization
|
|
1482
|
+
|
|
1483
|
+
```javascript
|
|
1484
|
+
// BAD: Repeated calculations
|
|
1485
|
+
for (const position of positions) {
|
|
1486
|
+
const instrumentId = DataExtractor.getInstrumentId(position);
|
|
1487
|
+
const ticker = mappings.instrumentToTicker[instrumentId];
|
|
1488
|
+
const netProfit = DataExtractor.getNetProfit(position);
|
|
1489
|
+
// ... repeated work
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
// GOOD: Cache and reuse
|
|
1493
|
+
const tickerCache = new Map();
|
|
1494
|
+
for (const position of positions) {
|
|
1495
|
+
const instrumentId = DataExtractor.getInstrumentId(position);
|
|
1496
|
+
|
|
1497
|
+
let ticker = tickerCache.get(instrumentId);
|
|
1498
|
+
if (!ticker) {
|
|
1499
|
+
ticker = mappings.instrumentToTicker[instrumentId];
|
|
1500
|
+
tickerCache.set(instrumentId, ticker);
|
|
1501
|
+
}
|
|
1502
|
+
|
|
1503
|
+
// ... use cached ticker
|
|
1504
|
+
}
|
|
1505
|
+
```
|
|
1506
|
+
|
|
1507
|
+
#### 7.3.3 Memory Management
|
|
1508
|
+
|
|
1509
|
+
```javascript
|
|
1510
|
+
// BAD: Loading all data into memory
|
|
1511
|
+
const allUsers = await loadFullDayMap(config, deps, refs);
|
|
1512
|
+
// Process 1M+ users at once
|
|
1513
|
+
|
|
1514
|
+
// GOOD: Streaming
|
|
1515
|
+
for await (const chunk of streamPortfolioData(config, deps, dateStr, refs)) {
|
|
1516
|
+
// Process 50 users at a time
|
|
1517
|
+
await processChunk(chunk);
|
|
1518
|
+
// Chunk is garbage collected after processing
|
|
1519
|
+
}
|
|
1520
|
+
```
|
|
1521
|
+
|
|
1522
|
+
### 7.4 Common Patterns
|
|
1523
|
+
|
|
1524
|
+
#### 7.4.1 Retry with Exponential Backoff
|
|
1525
|
+
|
|
1526
|
+
```javascript
|
|
1527
|
+
async function fetchWithRetry(url, options, maxRetries = 3) {
|
|
1528
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
1529
|
+
try {
|
|
1530
|
+
const response = await fetch(url, options);
|
|
1531
|
+
if (response.ok) return response;
|
|
1532
|
+
|
|
1533
|
+
if (attempt < maxRetries) {
|
|
1534
|
+
const backoffMs = Math.pow(2, attempt) * 1000;
|
|
1535
|
+
await sleep(backoffMs);
|
|
1536
|
+
}
|
|
1537
|
+
} catch (error) {
|
|
1538
|
+
if (attempt === maxRetries) throw error;
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
1542
|
+
```
|
|
1543
|
+
|
|
1544
|
+
#### 7.4.2 Rate Limiting
|
|
1545
|
+
|
|
1546
|
+
```javascript
|
|
1547
|
+
class RateLimiter {
|
|
1548
|
+
constructor(maxRequests, windowMs) {
|
|
1549
|
+
this.maxRequests = maxRequests;
|
|
1550
|
+
this.windowMs = windowMs;
|
|
1551
|
+
this.requests = [];
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1554
|
+
async acquire() {
|
|
1555
|
+
const now = Date.now();
|
|
1556
|
+
|
|
1557
|
+
// Remove old requests
|
|
1558
|
+
this.requests = this.requests.filter(
|
|
1559
|
+
time => now - time < this.windowMs
|
|
1560
|
+
);
|
|
1561
|
+
|
|
1562
|
+
// Check if we can proceed
|
|
1563
|
+
if (this.requests.length >= this.maxRequests) {
|
|
1564
|
+
const oldestRequest = this.requests[0];
|
|
1565
|
+
const waitTime = this.windowMs - (now - oldestRequest);
|
|
1566
|
+
await sleep(waitTime);
|
|
1567
|
+
return this.acquire(); // Recursive retry
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1570
|
+
this.requests.push(now);
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1574
|
+
// Usage
|
|
1575
|
+
const limiter = new RateLimiter(10, 60000); // 10 requests per minute
|
|
1576
|
+
|
|
1577
|
+
for (const task of tasks) {
|
|
1578
|
+
await limiter.acquire();
|
|
1579
|
+
await processTask(task);
|
|
1580
|
+
}
|
|
1581
|
+
```
|
|
1582
|
+
|
|
1583
|
+
#### 7.4.3 Batch Aggregation Pattern
|
|
1584
|
+
|
|
1585
|
+
```javascript
|
|
1586
|
+
class BatchAggregator {
|
|
1587
|
+
constructor(flushCallback, maxSize = 500, maxWaitMs = 5000) {
|
|
1588
|
+
this.flushCallback = flushCallback;
|
|
1589
|
+
this.maxSize = maxSize;
|
|
1590
|
+
this.maxWaitMs = maxWaitMs;
|
|
1591
|
+
this.items = [];
|
|
1592
|
+
this.timer = null;
|
|
1593
|
+
}
|
|
1594
|
+
|
|
1595
|
+
add(item) {
|
|
1596
|
+
this.items.push(item);
|
|
1597
|
+
|
|
1598
|
+
if (this.items.length >= this.maxSize) {
|
|
1599
|
+
this.flush();
|
|
1600
|
+
} else if (!this.timer) {
|
|
1601
|
+
this.timer = setTimeout(() => this.flush(), this.maxWaitMs);
|
|
1602
|
+
}
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1605
|
+
async flush() {
|
|
1606
|
+
if (this.timer) {
|
|
1607
|
+
clearTimeout(this.timer);
|
|
1608
|
+
this.timer = null;
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
if (this.items.length === 0) return;
|
|
1612
|
+
|
|
1613
|
+
const itemsToFlush = this.items;
|
|
1614
|
+
this.items = [];
|
|
1615
|
+
|
|
1616
|
+
await this.flushCallback(itemsToFlush);
|
|
1617
|
+
}
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1620
|
+
// Usage
|
|
1621
|
+
const aggregator = new BatchAggregator(async (items) => {
|
|
1622
|
+
const batch = db.batch();
|
|
1623
|
+
for (const item of items) {
|
|
1624
|
+
batch.set(item.ref, item.data);
|
|
1625
|
+
}
|
|
1626
|
+
await batch.commit();
|
|
1627
|
+
});
|
|
1628
|
+
|
|
1629
|
+
// Add items
|
|
1630
|
+
aggregator.add({ ref: docRef1, data: data1 });
|
|
1631
|
+
aggregator.add({ ref: docRef2, data: data2 });
|
|
1632
|
+
// ... auto-flushes when full or after timeout
|
|
1633
|
+
```
|
|
1634
|
+
|
|
1635
|
+
---
|
|
1636
|
+
|
|
1637
|
+
## 8. Deployment & Operations
|
|
1638
|
+
|
|
1639
|
+
### 8.1 Deployment Process
|
|
1640
|
+
|
|
1641
|
+
#### 8.1.1 Module Updates
|
|
1642
|
+
|
|
1643
|
+
```bash
|
|
1644
|
+
# 1. Make changes to bulltrackers-module
|
|
1645
|
+
cd Backend/Core/bulltrackers-module
|
|
1646
|
+
|
|
1647
|
+
# 2. Update version
|
|
1648
|
+
npm version patch # or minor, or major
|
|
1649
|
+
|
|
1650
|
+
# 3. Publish to NPM
|
|
1651
|
+
npm publish
|
|
1652
|
+
|
|
1653
|
+
# 4. Update dependent packages
|
|
1654
|
+
cd ../your-cloud-function
|
|
1655
|
+
npm install bulltrackers-module@latest
|
|
1656
|
+
|
|
1657
|
+
# 5. Deploy cloud function
|
|
1658
|
+
gcloud functions deploy your-function \
|
|
1659
|
+
--gen2 \
|
|
1660
|
+
--runtime=nodejs20 \
|
|
1661
|
+
--region=us-central1 \
|
|
1662
|
+
--source=. \
|
|
1663
|
+
--entry-point=yourEntryPoint \
|
|
1664
|
+
--trigger-topic=your-topic
|
|
1665
|
+
```
|
|
1666
|
+
|
|
1667
|
+
#### 8.1.2 Calculation Updates
|
|
1668
|
+
|
|
1669
|
+
```bash
|
|
1670
|
+
# 1. Update calculation in aiden-shared-calculations-unified
|
|
1671
|
+
cd aiden-shared-calculations-unified
|
|
1672
|
+
|
|
1673
|
+
# 2. Version bump
|
|
1674
|
+
npm version patch
|
|
1675
|
+
|
|
1676
|
+
# 3. Publish
|
|
1677
|
+
npm publish
|
|
1678
|
+
|
|
1679
|
+
# 4. Update bulltrackers-module dependency
|
|
1680
|
+
cd ../bulltrackers-module
|
|
1681
|
+
npm install aiden-shared-calculations-unified@latest
|
|
1682
|
+
npm version patch
|
|
1683
|
+
npm publish
|
|
1684
|
+
|
|
1685
|
+
# 5. Redeploy computation functions
|
|
1686
|
+
# (They will pick up new calculations automatically)
|
|
1687
|
+
```
|
|
1688
|
+
|
|
1689
|
+
#### 8.1.3 Environment Variables
|
|
1690
|
+
|
|
1691
|
+
```bash
|
|
1692
|
+
# Set via Cloud Console or gcloud CLI
|
|
1693
|
+
gcloud functions deploy computation-pass-1 \
|
|
1694
|
+
--set-env-vars="COMPUTATION_PASS_TO_RUN=1" \
|
|
1695
|
+
--set-env-vars="FIRESTORE_PROJECT_ID=your-project" \
|
|
1696
|
+
--set-env-vars="ETORO_API_URL=https://..."
|
|
1697
|
+
```
|
|
1698
|
+
|
|
1699
|
+
### 8.2 Monitoring
|
|
1700
|
+
|
|
1701
|
+
#### 8.2.1 Key Metrics
|
|
1702
|
+
|
|
1703
|
+
**Cloud Functions**:
|
|
1704
|
+
- Invocation count per function
|
|
1705
|
+
- Execution time (p50, p95, p99)
|
|
1706
|
+
- Error rate
|
|
1707
|
+
- Memory usage
|
|
1708
|
+
- Active instances
|
|
1709
|
+
|
|
1710
|
+
**Firestore**:
|
|
1711
|
+
- Read operations per day
|
|
1712
|
+
- Write operations per day
|
|
1713
|
+
- Document count per collection
|
|
1714
|
+
- Storage usage
|
|
1715
|
+
|
|
1716
|
+
**Pub/Sub**:
|
|
1717
|
+
- Messages published per topic
|
|
1718
|
+
- Messages consumed
|
|
1719
|
+
- Oldest unacked message age
|
|
1720
|
+
- Dead letter queue size
|
|
1721
|
+
|
|
1722
|
+
#### 8.2.2 Logging Queries
|
|
1723
|
+
|
|
1724
|
+
```
|
|
1725
|
+
# Find errors in last 24 hours
|
|
1726
|
+
resource.type="cloud_function"
|
|
1727
|
+
severity="ERROR"
|
|
1728
|
+
timestamp>="2025-01-15T00:00:00Z"
|
|
1729
|
+
|
|
1730
|
+
# Find specific user task
|
|
1731
|
+
resource.type="cloud_function"
|
|
1732
|
+
jsonPayload.message=~"userId.*123456"
|
|
1733
|
+
|
|
1734
|
+
# Find rate limit errors
|
|
1735
|
+
resource.type="cloud_function"
|
|
1736
|
+
jsonPayload.message=~"rate limit|429|too many"
|
|
1737
|
+
|
|
1738
|
+
# Computation pass performance
|
|
1739
|
+
resource.type="cloud_function"
|
|
1740
|
+
resource.labels.function_name="computation-pass-1"
|
|
1741
|
+
jsonPayload.message=~"Pass.*finished"
|
|
1742
|
+
```
|
|
1743
|
+
|
|
1744
|
+
#### 8.2.3 Alerting Rules
|
|
1745
|
+
|
|
1746
|
+
```yaml
|
|
1747
|
+
# Example alert policy
|
|
1748
|
+
displayName: "Task Engine Error Rate High"
|
|
1749
|
+
conditions:
|
|
1750
|
+
- displayName: "Error rate > 5%"
|
|
1751
|
+
conditionThreshold:
|
|
1752
|
+
filter: |
|
|
1753
|
+
resource.type="cloud_function"
|
|
1754
|
+
resource.labels.function_name="task-engine"
|
|
1755
|
+
severity="ERROR"
|
|
1756
|
+
aggregations:
|
|
1757
|
+
- alignmentPeriod: 300s
|
|
1758
|
+
perSeriesAligner: ALIGN_RATE
|
|
1759
|
+
comparison: COMPARISON_GT
|
|
1760
|
+
thresholdValue: 0.05
|
|
1761
|
+
duration: 600s
|
|
1762
|
+
|
|
1763
|
+
notificationChannels:
|
|
1764
|
+
- "projects/YOUR_PROJECT/notificationChannels/YOUR_CHANNEL"
|
|
1765
|
+
```
|
|
1766
|
+
|
|
1767
|
+
### 8.3 Cost Optimization
|
|
1768
|
+
|
|
1769
|
+
#### 8.3.1 Firestore Costs
|
|
1770
|
+
|
|
1771
|
+
**Current Architecture** (Optimized):
|
|
1772
|
+
- Sharded writes: ~50 writes per 500 users = 10¢ per 1M users
|
|
1773
|
+
- Status tracking: Single global document = $0
|
|
1774
|
+
- Batch commits: 1 batch per flush = 10× cost reduction
|
|
1775
|
+
|
|
1776
|
+
**Anti-Patterns to Avoid**:
|
|
1777
|
+
```javascript
|
|
1778
|
+
// BAD: Individual writes
|
|
1779
|
+
for (const user of users) {
|
|
1780
|
+
await db.collection('data').doc(user.id).set(data);
|
|
1781
|
+
}
|
|
1782
|
+
// Cost: N writes = $0.18 per 1000 writes
|
|
1783
|
+
|
|
1784
|
+
// GOOD: Batched writes
|
|
1785
|
+
const batch = db.batch();
|
|
1786
|
+
for (const user of users) {
|
|
1787
|
+
batch.set(db.collection('data').doc(user.id), data);
|
|
1788
|
+
}
|
|
1789
|
+
await batch.commit();
|
|
1790
|
+
// Cost: 1 write = $0.00018
|
|
1791
|
+
```
|
|
1792
|
+
|
|
1793
|
+
#### 8.3.2 Cloud Function Costs
|
|
1794
|
+
|
|
1795
|
+
**Optimization Strategies**:
|
|
1796
|
+
- **Memory Allocation**: Use 512MB for most functions (256MB often causes cold starts)
|
|
1797
|
+
- **Timeout**: Set realistic timeouts (5min for task engine, 1min for API)
|
|
1798
|
+
- **Concurrency**: Limit concurrent instances to avoid quota exhaustion
|
|
1799
|
+
|
|
1800
|
+
```yaml
|
|
1801
|
+
# function-config.yaml
|
|
1802
|
+
availableMemoryMb: 512
|
|
1803
|
+
timeout: 300s
|
|
1804
|
+
maxInstances: 100
|
|
1805
|
+
minInstances: 0 # Set to 1-2 for critical functions
|
|
1806
|
+
```
|
|
1807
|
+
|
|
1808
|
+
#### 8.3.3 API Call Optimization
|
|
1809
|
+
|
|
1810
|
+
**Current Strategy**:
|
|
1811
|
+
- AppScript proxies: Free (within Google's generous limits)
|
|
1812
|
+
- Direct calls: Fallback only (minimize usage)
|
|
1813
|
+
- Batch API calls: 50-100 users per request
|
|
1814
|
+
|
|
1815
|
+
**Cost Comparison**:
|
|
1816
|
+
```
|
|
1817
|
+
AppScript Proxy:
|
|
1818
|
+
- Cost: $0
|
|
1819
|
+
- Rate Limit: ~100 requests/min per proxy
|
|
1820
|
+
- Solution: 10+ proxies = 1000 req/min
|
|
1821
|
+
|
|
1822
|
+
Direct GCP IP:
|
|
1823
|
+
- Cost: $0
|
|
1824
|
+
- Rate Limit: ~20 requests/min (eToro throttles)
|
|
1825
|
+
- Solution: Use only as fallback
|
|
1826
|
+
```
|
|
1827
|
+
|
|
1828
|
+
### 8.4 Disaster Recovery
|
|
1829
|
+
|
|
1830
|
+
#### 8.4.1 Backup Strategy
|
|
1831
|
+
|
|
1832
|
+
```javascript
|
|
1833
|
+
// Automated daily backup (Cloud Scheduler)
|
|
1834
|
+
const {Firestore} = require('@google-cloud/firestore');
|
|
1835
|
+
|
|
1836
|
+
async function backupFirestore() {
|
|
1837
|
+
const firestore = new Firestore();
|
|
1838
|
+
|
|
1839
|
+
const bucket = 'gs://your-backup-bucket';
|
|
1840
|
+
const timestamp = new Date().toISOString().split('T')[0];
|
|
1841
|
+
|
|
1842
|
+
await firestore.export({
|
|
1843
|
+
collectionIds: [
|
|
1844
|
+
'NormalUserPortfolios',
|
|
1845
|
+
'SpeculatorBlocks',
|
|
1846
|
+
'unified_insights'
|
|
1847
|
+
],
|
|
1848
|
+
outputUriPrefix: `${bucket}/firestore-backups/${timestamp}`
|
|
1849
|
+
});
|
|
1850
|
+
}
|
|
1851
|
+
```
|
|
1852
|
+
|
|
1853
|
+
#### 8.4.2 Data Corruption Recovery
|
|
1854
|
+
|
|
1855
|
+
```javascript
|
|
1856
|
+
// Rebuild computation results from historical data
|
|
1857
|
+
async function rebuildComputations(startDate, endDate) {
|
|
1858
|
+
const config = {
|
|
1859
|
+
COMPUTATION_PASS_TO_RUN: "1",
|
|
1860
|
+
// Force recompute by clearing status
|
|
1861
|
+
};
|
|
1862
|
+
|
|
1863
|
+
// Clear status for date range
|
|
1864
|
+
const statusRef = db.collection('computation_status').doc('global_status');
|
|
1865
|
+
const updates = {};
|
|
1866
|
+
|
|
1867
|
+
for (let date = startDate; date <= endDate; date.setDate(date.getDate() + 1)) {
|
|
1868
|
+
const dateStr = date.toISOString().slice(0, 10);
|
|
1869
|
+
// Delete all calculation statuses for this date
|
|
1870
|
+
for (const calcName of allCalculationNames) {
|
|
1871
|
+
updates[`${dateStr}.${calcName}`] = FieldValue.delete();
|
|
1872
|
+
}
|
|
1873
|
+
}
|
|
1874
|
+
|
|
1875
|
+
await statusRef.update(updates);
|
|
1876
|
+
|
|
1877
|
+
// Trigger computation passes
|
|
1878
|
+
// They will recompute all dates with missing status
|
|
1879
|
+
}
|
|
1880
|
+
```
|
|
1881
|
+
|
|
1882
|
+
#### 8.4.3 Rollback Procedure
|
|
1883
|
+
|
|
1884
|
+
```bash
|
|
1885
|
+
# 1. Identify problematic version
|
|
1886
|
+
git log --oneline
|
|
1887
|
+
|
|
1888
|
+
# 2. Revert to previous version
|
|
1889
|
+
git revert <commit-hash>
|
|
1890
|
+
|
|
1891
|
+
# 3. Republish packages
|
|
1892
|
+
npm version patch
|
|
1893
|
+
npm publish
|
|
1894
|
+
|
|
1895
|
+
# 4. Redeploy functions
|
|
1896
|
+
gcloud functions deploy all-functions --source=.
|
|
1897
|
+
|
|
1898
|
+
# 5. Verify in logs
|
|
1899
|
+
gcloud logging read "resource.type=cloud_function" --limit=100
|
|
1900
|
+
```
|
|
1901
|
+
|
|
1902
|
+
---
|