bulltrackers-module 1.0.733 → 1.0.734

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.
Files changed (56) hide show
  1. package/functions/computation-system-v2/README.md +152 -0
  2. package/functions/computation-system-v2/computations/PopularInvestorProfileMetrics.js +720 -0
  3. package/functions/computation-system-v2/computations/PopularInvestorRiskAssessment.js +176 -0
  4. package/functions/computation-system-v2/computations/PopularInvestorRiskMetrics.js +294 -0
  5. package/functions/computation-system-v2/computations/TestComputation.js +46 -0
  6. package/functions/computation-system-v2/computations/UserPortfolioSummary.js +172 -0
  7. package/functions/computation-system-v2/config/bulltrackers.config.js +317 -0
  8. package/functions/computation-system-v2/framework/core/Computation.js +73 -0
  9. package/functions/computation-system-v2/framework/core/Manifest.js +223 -0
  10. package/functions/computation-system-v2/framework/core/RuleInjector.js +53 -0
  11. package/functions/computation-system-v2/framework/core/Rules.js +231 -0
  12. package/functions/computation-system-v2/framework/core/RunAnalyzer.js +163 -0
  13. package/functions/computation-system-v2/framework/cost/CostTracker.js +154 -0
  14. package/functions/computation-system-v2/framework/data/DataFetcher.js +399 -0
  15. package/functions/computation-system-v2/framework/data/QueryBuilder.js +232 -0
  16. package/functions/computation-system-v2/framework/data/SchemaRegistry.js +287 -0
  17. package/functions/computation-system-v2/framework/execution/Orchestrator.js +498 -0
  18. package/functions/computation-system-v2/framework/execution/TaskRunner.js +35 -0
  19. package/functions/computation-system-v2/framework/execution/middleware/CostTrackerMiddleware.js +32 -0
  20. package/functions/computation-system-v2/framework/execution/middleware/LineageMiddleware.js +32 -0
  21. package/functions/computation-system-v2/framework/execution/middleware/Middleware.js +14 -0
  22. package/functions/computation-system-v2/framework/execution/middleware/ProfilerMiddleware.js +47 -0
  23. package/functions/computation-system-v2/framework/index.js +45 -0
  24. package/functions/computation-system-v2/framework/lineage/LineageTracker.js +147 -0
  25. package/functions/computation-system-v2/framework/monitoring/Profiler.js +80 -0
  26. package/functions/computation-system-v2/framework/resilience/Checkpointer.js +66 -0
  27. package/functions/computation-system-v2/framework/scheduling/ScheduleValidator.js +327 -0
  28. package/functions/computation-system-v2/framework/storage/StateRepository.js +286 -0
  29. package/functions/computation-system-v2/framework/storage/StorageManager.js +469 -0
  30. package/functions/computation-system-v2/framework/storage/index.js +9 -0
  31. package/functions/computation-system-v2/framework/testing/ComputationTester.js +86 -0
  32. package/functions/computation-system-v2/framework/utils/Graph.js +205 -0
  33. package/functions/computation-system-v2/handlers/dispatcher.js +109 -0
  34. package/functions/computation-system-v2/handlers/index.js +23 -0
  35. package/functions/computation-system-v2/handlers/onDemand.js +289 -0
  36. package/functions/computation-system-v2/handlers/scheduler.js +327 -0
  37. package/functions/computation-system-v2/index.js +163 -0
  38. package/functions/computation-system-v2/rules/index.js +49 -0
  39. package/functions/computation-system-v2/rules/instruments.js +465 -0
  40. package/functions/computation-system-v2/rules/metrics.js +304 -0
  41. package/functions/computation-system-v2/rules/portfolio.js +534 -0
  42. package/functions/computation-system-v2/rules/rankings.js +655 -0
  43. package/functions/computation-system-v2/rules/social.js +562 -0
  44. package/functions/computation-system-v2/rules/trades.js +545 -0
  45. package/functions/computation-system-v2/scripts/migrate-sectors.js +73 -0
  46. package/functions/computation-system-v2/test/test-dispatcher.js +317 -0
  47. package/functions/computation-system-v2/test/test-framework.js +500 -0
  48. package/functions/computation-system-v2/test/test-real-execution.js +166 -0
  49. package/functions/computation-system-v2/test/test-real-integration.js +194 -0
  50. package/functions/computation-system-v2/test/test-refactor-e2e.js +131 -0
  51. package/functions/computation-system-v2/test/test-results.json +31 -0
  52. package/functions/computation-system-v2/test/test-risk-metrics-computation.js +329 -0
  53. package/functions/computation-system-v2/test/test-scheduler.js +204 -0
  54. package/functions/computation-system-v2/test/test-storage.js +449 -0
  55. package/functions/orchestrator/index.js +18 -26
  56. package/package.json +3 -2
@@ -0,0 +1,194 @@
1
+ /**
2
+ * @fileoverview Real Integration Test (End-to-End)
3
+ * * 1. Auto-detects the LATEST date in BigQuery
4
+ * * 2. Fetches REAL data (which comes as a JSON string)
5
+ * * 3. Uses RULES to parse and analyze that string
6
+ * * 4. Saves the decoded output to test-results.json
7
+ */
8
+
9
+ const fs = require('fs');
10
+ const path = require('path');
11
+ const { BigQuery } = require('@google-cloud/bigquery');
12
+ const { Orchestrator } = require('../framework/execution/Orchestrator');
13
+ const { Computation } = require('../framework/core/Computation');
14
+ const realConfig = require('../config/bulltrackers.config');
15
+
16
+ // 1. DYNAMIC TEST COMPUTATION
17
+ class LiveTestComputation extends Computation {
18
+ static setConfig(tableName, entityField) {
19
+ this.targetTable = tableName;
20
+ this.entityField = entityField;
21
+ }
22
+
23
+ static getConfig() {
24
+ const tableName = this.targetTable;
25
+ return {
26
+ name: 'LiveTestComputation',
27
+ version: '1.0.0',
28
+ type: 'per-entity',
29
+ schedule: 'daily',
30
+ requires: {
31
+ [tableName]: {
32
+ mandatory: true
33
+ }
34
+ },
35
+ dependencies: []
36
+ };
37
+ }
38
+
39
+ async process(context) {
40
+ const { entityId, data, rules } = context;
41
+ const tableName = this.constructor.targetTable;
42
+
43
+ // This is the RAW JSON STRING from BigQuery
44
+ const rawData = data[tableName];
45
+
46
+ // 1. USE RULES TO PARSE DATA
47
+ // The rules engine handles the JSON parsing automatically
48
+ const positions = rules.portfolio.extractPositions(rawData);
49
+ const mirrors = rules.portfolio.extractMirrors(rawData);
50
+
51
+ // 2. CALCULATE METRICS (Using Rules)
52
+ const totalValue = rules.portfolio.calculateTotalValue(positions);
53
+ const invested = rules.portfolio.calculateTotalInvested(positions);
54
+ const profit = rules.metrics.round(totalValue - invested, 2);
55
+
56
+ // 3. GET TOP POSITIONS
57
+ const topHoldings = rules.portfolio.getTopPositions(positions, 3).map(pos => ({
58
+ symbol: rules.portfolio.getInstrumentId(pos),
59
+ value: rules.metrics.round(rules.portfolio.getValue(pos), 2),
60
+ direction: rules.portfolio.getDirection(pos)
61
+ }));
62
+
63
+ // 4. STORE RESULT
64
+ this.results[entityId] = {
65
+ entityId,
66
+ parsedSuccessfully: positions.length > 0 || mirrors.length > 0,
67
+ summary: {
68
+ positionCount: positions.length,
69
+ mirrorCount: mirrors.length,
70
+ totalValue: rules.metrics.round(totalValue, 2),
71
+ profit
72
+ },
73
+ topHoldings,
74
+ processedAt: new Date().toISOString(),
75
+ status: 'valid'
76
+ };
77
+ }
78
+ }
79
+
80
+ async function getLatestDate(config, tableName) {
81
+ const bq = new BigQuery({ projectId: config.bigquery.projectId });
82
+ const fullTable = `${config.bigquery.projectId}.${config.bigquery.dataset}.${tableName}`;
83
+
84
+ console.log(`🔎 Finding latest data in ${fullTable}...`);
85
+
86
+ try {
87
+ const query = `SELECT MAX(date) as latest_date FROM \`${fullTable}\``;
88
+ const [rows] = await bq.query(query);
89
+ if (rows.length && rows[0].latest_date) {
90
+ return rows[0].latest_date.value || rows[0].latest_date;
91
+ }
92
+ } catch (e) {
93
+ console.warn(` Warning: Could not query MAX(date). Using yesterday.`);
94
+ }
95
+
96
+ const d = new Date();
97
+ d.setDate(d.getDate() - 1);
98
+ return d.toISOString().split('T')[0];
99
+ }
100
+
101
+ async function runLiveTest() {
102
+ console.log('🔌 Connecting to Real Environment (BigQuery)...');
103
+
104
+ if (!realConfig.bigquery?.projectId) throw new Error('Missing projectId in config');
105
+
106
+ const tableName = 'portfolio_snapshots';
107
+ const tableConfig = realConfig.tables[tableName];
108
+
109
+ console.log(` Target Table: ${tableName}`);
110
+
111
+ // 3. AUTO-DETECT DATE
112
+ const latestDate = await getLatestDate(realConfig, tableName);
113
+ console.log(` Latest Available Date: ${latestDate}`);
114
+
115
+ LiveTestComputation.setConfig(tableName, tableConfig.entityField);
116
+
117
+ const testConfig = {
118
+ ...realConfig,
119
+ computations: [LiveTestComputation],
120
+ execution: {
121
+ batchSize: 5,
122
+ entityConcurrency: 2
123
+ }
124
+ };
125
+
126
+ // 4. INITIALIZE ORCHESTRATOR
127
+ const orchestrator = new Orchestrator(testConfig, console);
128
+ await orchestrator.initialize();
129
+
130
+ // =========================================================================
131
+ // 5. INTERCEPT STORAGE
132
+ // =========================================================================
133
+
134
+ const capturedResults = [];
135
+
136
+ orchestrator.storageManager.commitResults = async (date, entry, results) => {
137
+ const count = Object.keys(results).length;
138
+ console.log(` [MockStorage] Capturing batch of ${count} results...`);
139
+ Object.values(results).forEach(r => capturedResults.push(r));
140
+ return { rowCount: count };
141
+ };
142
+
143
+ // Silence other storage ops
144
+ orchestrator.storageManager.getLatestCheckpoint = async () => null;
145
+ orchestrator.storageManager.initCheckpoint = async () => {};
146
+ orchestrator.storageManager.updateCheckpoint = async () => {};
147
+ orchestrator.storageManager.completeCheckpoint = async () => {};
148
+ orchestrator.storageManager.savePerformanceReport = async () => {};
149
+
150
+ // =========================================================================
151
+ // 6. EXECUTION
152
+ // =========================================================================
153
+
154
+ const dateStr = typeof latestDate === 'string' ? latestDate : latestDate.toISOString().split('T')[0];
155
+ console.log(`🚀 Executing LiveTestComputation for ${dateStr}...`);
156
+
157
+ const entry = orchestrator.manifest.find(c => c.name === 'livetestcomputation');
158
+
159
+ const result = await orchestrator.runSingle(entry, dateStr, {
160
+ dryRun: false
161
+ });
162
+
163
+ // =========================================================================
164
+ // 7. OUTPUT & VALIDATION
165
+ // =========================================================================
166
+
167
+ console.log('\n📊 TEST COMPLETED');
168
+ console.log(` Status: ${result.status}`);
169
+ console.log(` Captured Rows: ${capturedResults.length}`);
170
+
171
+ if (capturedResults.length > 0) {
172
+ const sample = capturedResults[0];
173
+ console.log('\n🔍 Sample Result Inspection:');
174
+ console.log(` Entity ID: ${sample.entityId}`);
175
+ console.log(` Parsed OK: ${sample.parsedSuccessfully ? '✅ YES' : '❌ NO'}`);
176
+ console.log(` Positions: ${sample.summary.positionCount}`);
177
+ console.log(` Total Value: $${sample.summary.totalValue}`);
178
+
179
+ if (sample.topHoldings.length > 0) {
180
+ console.log(` Top Holding: ${sample.topHoldings[0].symbol} ($${sample.topHoldings[0].value})`);
181
+ }
182
+
183
+ const outputPath = path.join(__dirname, 'test-results.json');
184
+ fs.writeFileSync(outputPath, JSON.stringify(capturedResults, null, 2));
185
+ console.log(`\n✅ Full results saved to: ${outputPath}`);
186
+ } else {
187
+ console.warn('\n⚠️ No results were captured.');
188
+ }
189
+ }
190
+
191
+ runLiveTest().catch(err => {
192
+ console.error('\n❌ FATAL ERROR:', err);
193
+ process.exit(1);
194
+ });
@@ -0,0 +1,131 @@
1
+ /**
2
+ * @fileoverview End-to-End Test for Refactored Architecture
3
+ * Validates: Dispatcher -> Index -> Orchestrator -> RuleInjector -> TaskRunner
4
+ */
5
+
6
+ const assert = require('assert');
7
+ const { Orchestrator } = require('../framework/execution/Orchestrator');
8
+ const TestComputation = require('../computations/TestComputation');
9
+ const { RulesRegistry } = require('../framework/core/Rules');
10
+
11
+ // MOCK DATA
12
+ const MOCK_USERS = {
13
+ 'user_1': { id: 'user_1', status: 'active' },
14
+ 'user_2': { id: 'user_2', status: 'pending' }
15
+ };
16
+
17
+ const MOCK_TXS = [
18
+ { userId: 'user_1', amount: 100 },
19
+ { userId: 'user_1', amount: 50 },
20
+ { userId: 'user_2', amount: 200 }
21
+ ];
22
+
23
+ // MOCK RULES
24
+ class MockRulesRegistry extends RulesRegistry {
25
+ getContext() {
26
+ return {
27
+ math: {
28
+ double: (n) => n * 2
29
+ }
30
+ };
31
+ }
32
+ }
33
+
34
+ async function runTest() {
35
+ console.log('🧪 Starting End-to-End Refactor Test...\n');
36
+
37
+ // 1. SETUP: Initialize Orchestrator with Mocks
38
+ const config = {
39
+ computations: [TestComputation],
40
+ tables: {
41
+ users: { entityField: 'id' },
42
+ transactions: { entityField: 'userId' }
43
+ },
44
+ execution: { batchSize: 2 } // Small batch to test looping
45
+ };
46
+
47
+ const orchestrator = new Orchestrator(config, console);
48
+
49
+ // --- MONKEY PATCHING MOCKS (Simulating Infrastructure) ---
50
+
51
+ // Mock Data Fetcher (Batching)
52
+ orchestrator.dataFetcher.fetchComputationBatched = async function* (requires, date, batchSize) {
53
+ console.log(` [Mock] Fetching batch for ${date}...`);
54
+ yield {
55
+ entityIds: ['user_1', 'user_2'],
56
+ data: {
57
+ users: MOCK_USERS,
58
+ transactions: MOCK_TXS
59
+ }
60
+ };
61
+ };
62
+
63
+ // Mock Data Fetcher (Filter logic which usually runs in DB, we verify it runs in memory here)
64
+ orchestrator._filterDataForEntity = function(batchData, entityId) {
65
+ // Simple mock of the helper logic
66
+ return {
67
+ users: batchData.users[entityId],
68
+ transactions: batchData.transactions.filter(t => t.userId === entityId)
69
+ };
70
+ };
71
+
72
+ // Mock State Repository (Always fresh run)
73
+ orchestrator.stateRepository.getDailyStatus = async () => new Map();
74
+ orchestrator.stateRepository.updateStatusCache = async (d, n, s) => {
75
+ console.log(` [Mock] Status Updated: ${n} -> EntityCount: ${s.entityCount}`);
76
+ };
77
+
78
+ // Mock Storage Manager (Capture results)
79
+ const storedResults = {};
80
+ orchestrator.storageManager.commitResults = async (date, comp, results) => {
81
+ Object.assign(storedResults, results);
82
+ console.log(` [Mock] Storage Committed ${Object.keys(results).length} records.`);
83
+ };
84
+
85
+ // Mock Rule Registry (Inject our mock math rule)
86
+ orchestrator.ruleInjector.registry = new MockRulesRegistry(config);
87
+
88
+
89
+ // 2. EXECUTION: Run via the new "Worker" API
90
+ console.log('🚀 Triggering Execution...');
91
+
92
+ // We simulate what the new index.js runComputation does:
93
+ // It finds the manifest entry and calls runSingle.
94
+ await orchestrator.initialize();
95
+ const entry = orchestrator.manifest.find(c => c.name === 'testcomputation');
96
+
97
+ const result = await orchestrator.runSingle(entry, '2026-01-25', {
98
+ dryRun: false
99
+ });
100
+
101
+
102
+ // 3. VALIDATION: Assertions
103
+ console.log('\n📊 Validating Results...');
104
+
105
+ // Assertion 1: Execution Status
106
+ assert.strictEqual(result.status, 'completed', 'Status should be completed');
107
+ assert.strictEqual(result.resultCount, 2, 'Should process 2 entities');
108
+ console.log(' ✅ Execution Status OK');
109
+
110
+ // Assertion 2: User 1 Calculation
111
+ // Amount: 100 + 50 = 150. Multiplier (Rule): 2. Result: 300.
112
+ const r1 = storedResults['user_1'];
113
+ assert.ok(r1, 'User 1 result missing');
114
+ assert.strictEqual(r1.score, 300, `User 1 Score Wrong. Expected 300, got ${r1.score}`);
115
+ assert.strictEqual(r1.status, 'active', 'User 1 Status Wrong');
116
+ console.log(' ✅ User 1 Logic OK (Aggregation + Rule Injection)');
117
+
118
+ // Assertion 3: User 2 Calculation
119
+ // Amount: 200. Multiplier: 2. Result: 400.
120
+ const r2 = storedResults['user_2'];
121
+ assert.ok(r2, 'User 2 result missing');
122
+ assert.strictEqual(r2.score, 400, `User 2 Score Wrong. Expected 400, got ${r2.score}`);
123
+ console.log(' ✅ User 2 Logic OK');
124
+
125
+ console.log('\n🎉 ALL TESTS PASSED. The Orchestrator Refactor is FUNCTIONAL.');
126
+ }
127
+
128
+ runTest().catch(err => {
129
+ console.error('\n❌ TEST FAILED:', err);
130
+ process.exit(1);
131
+ });
@@ -0,0 +1,31 @@
1
+ [
2
+ {
3
+ "entityId": "24790725",
4
+ "parsedSuccessfully": true,
5
+ "summary": {
6
+ "positionCount": 29,
7
+ "mirrorCount": 0,
8
+ "totalValue": 99.54,
9
+ "profit": 0.16
10
+ },
11
+ "topHoldings": [
12
+ {
13
+ "symbol": 1002,
14
+ "value": 11.01,
15
+ "direction": "Buy"
16
+ },
17
+ {
18
+ "symbol": 4481,
19
+ "value": 7.95,
20
+ "direction": "Buy"
21
+ },
22
+ {
23
+ "symbol": 3006,
24
+ "value": 7.49,
25
+ "direction": "Buy"
26
+ }
27
+ ],
28
+ "processedAt": "2026-01-25T02:52:57.446Z",
29
+ "status": "valid"
30
+ }
31
+ ]
@@ -0,0 +1,329 @@
1
+ /**
2
+ * @fileoverview Test for PopularInvestorRiskMetrics Computation
3
+ *
4
+ * Tests the full computation pipeline including:
5
+ * - Rule-based calculations
6
+ * - BigQuery storage
7
+ * - Firestore storage
8
+ */
9
+
10
+ const { Executor, ManifestBuilder } = require('../framework');
11
+ const { StorageManager } = require('../framework/storage/StorageManager');
12
+ const PopularInvestorRiskMetrics = require('../computations/PopularInvestorRiskMetrics');
13
+ const config = require('../config/bulltrackers.config');
14
+
15
+ console.log('╔════════════════════════════════════════════════════════════╗');
16
+ console.log('║ PopularInvestorRiskMetrics - Full Integration Test ║');
17
+ console.log('╚════════════════════════════════════════════════════════════╝\n');
18
+
19
+ // Test logger
20
+ const logger = {
21
+ log: (level, msg) => console.log(`[${level}] ${msg}`)
22
+ };
23
+
24
+ // =============================================================================
25
+ // Test 1: Verify Manifest Entry
26
+ // =============================================================================
27
+
28
+ async function testManifestEntry() {
29
+ console.log('=== Test 1: Manifest Entry ===\n');
30
+
31
+ const builder = new ManifestBuilder(config, logger);
32
+ const manifest = builder.build([PopularInvestorRiskMetrics]);
33
+
34
+ const entry = manifest[0];
35
+
36
+ console.log('Computation Entry:');
37
+ console.log(` Name: ${entry.name}`);
38
+ console.log(` Category: ${entry.category}`);
39
+ console.log(` Type: ${entry.type}`);
40
+ console.log(` Hash: ${entry.hash}`);
41
+ console.log(` Schedule: ${JSON.stringify(entry.schedule)}`);
42
+ console.log('\nStorage Config:');
43
+ console.log(` BigQuery: ${entry.storage.bigquery}`);
44
+ console.log(` Firestore enabled: ${entry.storage.firestore.enabled}`);
45
+ console.log(` Firestore path: ${entry.storage.firestore.path}`);
46
+
47
+ const allCorrect =
48
+ entry.name === 'popularinvestorriskmetrics' &&
49
+ entry.category === 'risk_analytics' &&
50
+ entry.type === 'per-entity' &&
51
+ entry.storage.bigquery === true &&
52
+ entry.storage.firestore.enabled === true;
53
+
54
+ console.log(`\n✅ Manifest entry test ${allCorrect ? 'passed' : 'FAILED'}\n`);
55
+ return allCorrect;
56
+ }
57
+
58
+ // =============================================================================
59
+ // Test 2: Test Computation Logic with Mock Data
60
+ // =============================================================================
61
+
62
+ async function testComputationLogic() {
63
+ console.log('=== Test 2: Computation Logic (Mock Data) ===\n');
64
+
65
+ // Create a mock context with 30 days of portfolio history
66
+ const mockPortfolioHistory = [];
67
+ let baseValue = 100000;
68
+
69
+ // Generate 30 days of portfolio snapshots
70
+ for (let i = 0; i < 30; i++) {
71
+ const date = new Date('2026-01-01');
72
+ date.setDate(date.getDate() + i);
73
+
74
+ // Add some realistic variation (+/- 2% daily)
75
+ const dailyReturn = (Math.random() - 0.4) * 4; // Slight positive bias
76
+ baseValue = baseValue * (1 + dailyReturn / 100);
77
+
78
+ // Create positions with varying profits
79
+ const positions = [
80
+ { InstrumentID: '1', Amount: baseValue * 0.3, NetProfit: (Math.random() - 0.3) * 20, Value: baseValue * 0.3 },
81
+ { InstrumentID: '2', Amount: baseValue * 0.25, NetProfit: (Math.random() - 0.3) * 20, Value: baseValue * 0.25 },
82
+ { InstrumentID: '3', Amount: baseValue * 0.2, NetProfit: (Math.random() - 0.3) * 20, Value: baseValue * 0.2 },
83
+ { InstrumentID: '4', Amount: baseValue * 0.15, NetProfit: (Math.random() - 0.3) * 20, Value: baseValue * 0.15 },
84
+ { InstrumentID: '5', Amount: baseValue * 0.1, NetProfit: (Math.random() - 0.3) * 20, Value: baseValue * 0.1 },
85
+ ];
86
+
87
+ mockPortfolioHistory.push({
88
+ date: date.toISOString().split('T')[0],
89
+ AggregatedPositions: positions
90
+ });
91
+ }
92
+
93
+ // Build rules context
94
+ const rulesModules = config.rules || {};
95
+ const rulesContext = {};
96
+ for (const [name, moduleExports] of Object.entries(rulesModules)) {
97
+ rulesContext[name] = moduleExports;
98
+ }
99
+
100
+ // Create mock context
101
+ const context = {
102
+ date: '2026-01-30',
103
+ entityId: 'test_user_001',
104
+ data: {
105
+ 'portfolio_snapshots': mockPortfolioHistory
106
+ },
107
+ rules: rulesContext,
108
+ logger
109
+ };
110
+
111
+ // Create computation instance
112
+ const computation = new PopularInvestorRiskMetrics();
113
+ computation._meta = { logger, config };
114
+
115
+ // Run computation
116
+ await computation.process(context);
117
+
118
+ // Get result (results are stored in computation.results object)
119
+ const allResults = await computation.getResult();
120
+ const result = allResults['test_user_001'];
121
+
122
+ console.log('Computation Result:');
123
+ console.log(` User ID: ${result.userId}`);
124
+ console.log(` Portfolio Value: $${result.portfolioValue.toLocaleString()}`);
125
+ console.log(` Position Count: ${result.positionCount}`);
126
+ console.log(` Days Analyzed: ${result.daysAnalyzed}`);
127
+ console.log('\nRisk Metrics:');
128
+ console.log(` Sharpe Ratio: ${result.sharpeRatio}`);
129
+ console.log(` Sortino Ratio: ${result.sortinoRatio}`);
130
+ console.log(` Max Drawdown: ${result.maxDrawdown}%`);
131
+ console.log(` VaR (95%): ${result.valueAtRisk95}%`);
132
+ console.log('\nPerformance:');
133
+ console.log(` Win Ratio: ${(result.winRatio * 100).toFixed(1)}%`);
134
+ console.log(` Avg Daily Return: ${result.avgDailyReturn}%`);
135
+ console.log(` Return Volatility: ${result.returnVolatility}%`);
136
+ console.log(` Top 3 Concentration: ${result.top3Concentration}%`);
137
+ console.log(`\n Risk Grade: ${result.riskGrade}`);
138
+
139
+ const validResult =
140
+ result.userId === 'test_user_001' &&
141
+ result.portfolioValue > 0 &&
142
+ result.daysAnalyzed === 30 &&
143
+ result.riskGrade !== 'N/A';
144
+
145
+ console.log(`\n✅ Computation logic test ${validResult ? 'passed' : 'FAILED'}\n`);
146
+ return { passed: validResult, result };
147
+ }
148
+
149
+ // =============================================================================
150
+ // Test 3: Test Full Storage Pipeline (BigQuery + Firestore)
151
+ // =============================================================================
152
+
153
+ async function testStoragePipeline(computationResult) {
154
+ console.log('=== Test 3: Storage Pipeline (BQ + Firestore) ===\n');
155
+
156
+ const storageManager = new StorageManager(config, logger);
157
+
158
+ // Create a mock manifest entry with storage config
159
+ const entry = {
160
+ name: 'popularinvestorriskmetrics',
161
+ originalName: 'PopularInvestorRiskMetrics',
162
+ category: 'risk_analytics',
163
+ type: 'per-entity',
164
+ hash: 'test_hash_789',
165
+ storage: {
166
+ bigquery: false, // Skip BQ for this test
167
+ firestore: {
168
+ enabled: true,
169
+ path: '/test_users/{entityId}/computations/risk_metrics',
170
+ merge: false,
171
+ includeMetadata: true
172
+ }
173
+ }
174
+ };
175
+
176
+ const results = {
177
+ 'test_user_001': computationResult
178
+ };
179
+
180
+ const dateStr = '2026-01-30';
181
+
182
+ try {
183
+ console.log(' Writing computation results to Firestore...');
184
+ console.log(` Path template: ${entry.storage.firestore.path}`);
185
+
186
+ const writeResult = await storageManager.commitResults(dateStr, entry, results, {});
187
+
188
+ console.log('\n Write Results:');
189
+ console.log(` BigQuery: ${writeResult.bigquery ? `${writeResult.bigquery.rowCount} rows` : 'skipped'}`);
190
+ console.log(` Firestore: ${writeResult.firestore?.docCount || 0} documents`);
191
+
192
+ // Verify in Firestore
193
+ const firestore = storageManager.firestore;
194
+ const docPath = 'test_users/test_user_001/computations/risk_metrics';
195
+ const doc = await firestore.doc(docPath).get();
196
+
197
+ if (doc.exists) {
198
+ const data = doc.data();
199
+ console.log(`\n Verified document at ${docPath}:`);
200
+ console.log(` - sharpeRatio: ${data.sharpeRatio}`);
201
+ console.log(` - riskGrade: ${data.riskGrade}`);
202
+ console.log(` - _computedAt: ${data._computedAt ? 'present' : 'missing'}`);
203
+ console.log(` - _computationDate: ${data._computationDate}`);
204
+
205
+ // Cleanup
206
+ await firestore.doc(docPath).delete();
207
+ console.log('\n Cleanup complete');
208
+
209
+ console.log('\n✅ Storage pipeline test passed\n');
210
+ return true;
211
+ } else {
212
+ console.log(`\n Document not found at ${docPath}`);
213
+ console.log('\n❌ Storage pipeline test FAILED\n');
214
+ return false;
215
+ }
216
+
217
+ } catch (error) {
218
+ if (error.message.includes('Could not load the default credentials')) {
219
+ console.log(' Skipped: No GCP credentials');
220
+ return 'skipped';
221
+ }
222
+ console.log(` Error: ${error.message}`);
223
+ return false;
224
+ }
225
+ }
226
+
227
+ // =============================================================================
228
+ // Test 4: Test with Real BigQuery Data (if available)
229
+ // =============================================================================
230
+
231
+ async function testWithRealData() {
232
+ console.log('=== Test 4: Integration with Real BigQuery Data ===\n');
233
+
234
+ try {
235
+ // Create executor with real config
236
+ const executor = new Executor(config, logger);
237
+ await executor.initialize();
238
+
239
+ // Add our computation to the manifest
240
+ const builder = new ManifestBuilder(config, logger);
241
+ const manifest = builder.build([PopularInvestorRiskMetrics]);
242
+
243
+ console.log(` Manifest: ${manifest.length} computations`);
244
+ console.log(` Entry: ${manifest[0].name}`);
245
+
246
+ // Try to run for a recent date
247
+ const targetDate = '2026-01-24';
248
+ console.log(`\n Checking data availability for ${targetDate}...`);
249
+
250
+ // Check if we have data
251
+ const availability = await executor.dataFetcher.checkAvailability(
252
+ manifest[0].requires,
253
+ targetDate
254
+ );
255
+
256
+ console.log(` Data available: ${availability.canRun}`);
257
+
258
+ if (!availability.canRun) {
259
+ console.log(' Missing tables:', availability.missing?.join(', ') || 'unknown');
260
+ console.log('\n⏭️ Skipped: No real data available\n');
261
+ return 'skipped';
262
+ }
263
+
264
+ // Run computation
265
+ console.log('\n Running computation...');
266
+ const result = await executor.runSingle(manifest[0], targetDate, {
267
+ force: true // Force run even if cached
268
+ });
269
+
270
+ console.log('\n Execution Result:');
271
+ console.log(` Status: ${result.status}`);
272
+ console.log(` Entities: ${result.entityCount}`);
273
+ console.log(` Duration: ${result.duration}ms`);
274
+
275
+ console.log('\n✅ Real data integration test passed\n');
276
+ return true;
277
+
278
+ } catch (error) {
279
+ console.log(` Error: ${error.message}`);
280
+ if (error.message.includes('credentials') || error.message.includes('project')) {
281
+ console.log('\n⏭️ Skipped: GCP credentials not configured\n');
282
+ return 'skipped';
283
+ }
284
+ console.log('\n❌ Real data integration test FAILED\n');
285
+ return false;
286
+ }
287
+ }
288
+
289
+ // =============================================================================
290
+ // Run All Tests
291
+ // =============================================================================
292
+
293
+ async function runAllTests() {
294
+ const results = {};
295
+
296
+ // Test 1: Manifest
297
+ results.manifestEntry = await testManifestEntry();
298
+
299
+ // Test 2: Computation logic
300
+ const logicTest = await testComputationLogic();
301
+ results.computationLogic = logicTest.passed;
302
+
303
+ // Test 3: Storage pipeline (uses result from test 2)
304
+ if (logicTest.passed) {
305
+ results.storagePipeline = await testStoragePipeline(logicTest.result);
306
+ } else {
307
+ results.storagePipeline = 'skipped';
308
+ }
309
+
310
+ // Test 4: Real data (optional)
311
+ results.realDataIntegration = await testWithRealData();
312
+
313
+ // Summary
314
+ console.log('╔════════════════════════════════════════════════════════════╗');
315
+ console.log('║ Test Summary ║');
316
+ console.log('╚════════════════════════════════════════════════════════════╝\n');
317
+
318
+ let allPassed = true;
319
+ for (const [name, result] of Object.entries(results)) {
320
+ const status = result === true ? '✅ PASSED' :
321
+ result === 'skipped' ? '⏭️ SKIPPED' : '❌ FAILED';
322
+ console.log(` ${name}: ${status}`);
323
+ if (result === false) allPassed = false;
324
+ }
325
+
326
+ console.log('\n' + (allPassed ? '✅ All tests passed!' : '❌ Some tests failed'));
327
+ }
328
+
329
+ runAllTests().catch(console.error);