bulltrackers-module 1.0.175 → 1.0.177

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 CHANGED
@@ -1,3 +1,1902 @@
1
- This is the directory for the cloud function entry points in the Bulltrackers Platform
1
+ # BullTrackers Platform - Comprehensive Developer Documentation
2
2
 
3
- Please refer to BullTrackers\Backend\documentation for the full documentation
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
+ ---