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.
- package/functions/computation-system-v2/README.md +152 -0
- package/functions/computation-system-v2/computations/PopularInvestorProfileMetrics.js +720 -0
- package/functions/computation-system-v2/computations/PopularInvestorRiskAssessment.js +176 -0
- package/functions/computation-system-v2/computations/PopularInvestorRiskMetrics.js +294 -0
- package/functions/computation-system-v2/computations/TestComputation.js +46 -0
- package/functions/computation-system-v2/computations/UserPortfolioSummary.js +172 -0
- package/functions/computation-system-v2/config/bulltrackers.config.js +317 -0
- package/functions/computation-system-v2/framework/core/Computation.js +73 -0
- package/functions/computation-system-v2/framework/core/Manifest.js +223 -0
- package/functions/computation-system-v2/framework/core/RuleInjector.js +53 -0
- package/functions/computation-system-v2/framework/core/Rules.js +231 -0
- package/functions/computation-system-v2/framework/core/RunAnalyzer.js +163 -0
- package/functions/computation-system-v2/framework/cost/CostTracker.js +154 -0
- package/functions/computation-system-v2/framework/data/DataFetcher.js +399 -0
- package/functions/computation-system-v2/framework/data/QueryBuilder.js +232 -0
- package/functions/computation-system-v2/framework/data/SchemaRegistry.js +287 -0
- package/functions/computation-system-v2/framework/execution/Orchestrator.js +498 -0
- package/functions/computation-system-v2/framework/execution/TaskRunner.js +35 -0
- package/functions/computation-system-v2/framework/execution/middleware/CostTrackerMiddleware.js +32 -0
- package/functions/computation-system-v2/framework/execution/middleware/LineageMiddleware.js +32 -0
- package/functions/computation-system-v2/framework/execution/middleware/Middleware.js +14 -0
- package/functions/computation-system-v2/framework/execution/middleware/ProfilerMiddleware.js +47 -0
- package/functions/computation-system-v2/framework/index.js +45 -0
- package/functions/computation-system-v2/framework/lineage/LineageTracker.js +147 -0
- package/functions/computation-system-v2/framework/monitoring/Profiler.js +80 -0
- package/functions/computation-system-v2/framework/resilience/Checkpointer.js +66 -0
- package/functions/computation-system-v2/framework/scheduling/ScheduleValidator.js +327 -0
- package/functions/computation-system-v2/framework/storage/StateRepository.js +286 -0
- package/functions/computation-system-v2/framework/storage/StorageManager.js +469 -0
- package/functions/computation-system-v2/framework/storage/index.js +9 -0
- package/functions/computation-system-v2/framework/testing/ComputationTester.js +86 -0
- package/functions/computation-system-v2/framework/utils/Graph.js +205 -0
- package/functions/computation-system-v2/handlers/dispatcher.js +109 -0
- package/functions/computation-system-v2/handlers/index.js +23 -0
- package/functions/computation-system-v2/handlers/onDemand.js +289 -0
- package/functions/computation-system-v2/handlers/scheduler.js +327 -0
- package/functions/computation-system-v2/index.js +163 -0
- package/functions/computation-system-v2/rules/index.js +49 -0
- package/functions/computation-system-v2/rules/instruments.js +465 -0
- package/functions/computation-system-v2/rules/metrics.js +304 -0
- package/functions/computation-system-v2/rules/portfolio.js +534 -0
- package/functions/computation-system-v2/rules/rankings.js +655 -0
- package/functions/computation-system-v2/rules/social.js +562 -0
- package/functions/computation-system-v2/rules/trades.js +545 -0
- package/functions/computation-system-v2/scripts/migrate-sectors.js +73 -0
- package/functions/computation-system-v2/test/test-dispatcher.js +317 -0
- package/functions/computation-system-v2/test/test-framework.js +500 -0
- package/functions/computation-system-v2/test/test-real-execution.js +166 -0
- package/functions/computation-system-v2/test/test-real-integration.js +194 -0
- package/functions/computation-system-v2/test/test-refactor-e2e.js +131 -0
- package/functions/computation-system-v2/test/test-results.json +31 -0
- package/functions/computation-system-v2/test/test-risk-metrics-computation.js +329 -0
- package/functions/computation-system-v2/test/test-scheduler.js +204 -0
- package/functions/computation-system-v2/test/test-storage.js +449 -0
- package/functions/orchestrator/index.js +18 -26
- 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);
|