bulltrackers-module 1.0.765 → 1.0.768
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/computations/BehavioralAnomaly.js +298 -186
- package/functions/computation-system-v2/computations/NewSectorExposure.js +82 -35
- package/functions/computation-system-v2/computations/NewSocialPost.js +52 -24
- package/functions/computation-system-v2/computations/PopularInvestorProfileMetrics.js +354 -641
- package/functions/computation-system-v2/config/bulltrackers.config.js +26 -14
- package/functions/computation-system-v2/framework/core/Manifest.js +9 -16
- package/functions/computation-system-v2/framework/core/RunAnalyzer.js +2 -1
- package/functions/computation-system-v2/framework/data/DataFetcher.js +142 -4
- package/functions/computation-system-v2/framework/execution/Orchestrator.js +119 -122
- package/functions/computation-system-v2/framework/storage/StorageManager.js +16 -18
- package/functions/computation-system-v2/framework/testing/ComputationTester.js +155 -66
- package/functions/computation-system-v2/handlers/scheduler.js +15 -5
- package/functions/computation-system-v2/scripts/test-computation-dag.js +109 -0
- package/functions/task-engine/helpers/data_storage_helpers.js +6 -6
- package/package.json +1 -1
- package/functions/computation-system-v2/computations/PopularInvestorRiskAssessment.js +0 -176
- package/functions/computation-system-v2/computations/PopularInvestorRiskMetrics.js +0 -294
- package/functions/computation-system-v2/computations/UserPortfolioSummary.js +0 -172
- package/functions/computation-system-v2/scripts/migrate-sectors.js +0 -73
- package/functions/computation-system-v2/test/analyze-results.js +0 -238
- package/functions/computation-system-v2/test/other/test-dependency-cascade.js +0 -150
- package/functions/computation-system-v2/test/other/test-dispatcher.js +0 -317
- package/functions/computation-system-v2/test/other/test-framework.js +0 -500
- package/functions/computation-system-v2/test/other/test-real-execution.js +0 -166
- package/functions/computation-system-v2/test/other/test-real-integration.js +0 -194
- package/functions/computation-system-v2/test/other/test-refactor-e2e.js +0 -131
- package/functions/computation-system-v2/test/other/test-results.json +0 -31
- package/functions/computation-system-v2/test/other/test-risk-metrics-computation.js +0 -329
- package/functions/computation-system-v2/test/other/test-scheduler.js +0 -204
- package/functions/computation-system-v2/test/other/test-storage.js +0 -449
- package/functions/computation-system-v2/test/run-pipeline-test.js +0 -554
- package/functions/computation-system-v2/test/test-full-pipeline.js +0 -227
- package/functions/computation-system-v2/test/test-worker-pool.js +0 -266
|
@@ -1,86 +1,175 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @fileoverview
|
|
2
|
+
* @fileoverview Integration Tester
|
|
3
|
+
* Wraps the Orchestrator to run computations in a test environment.
|
|
4
|
+
* - Redirects output to a test table.
|
|
5
|
+
* - Resolves and executes dependency chains.
|
|
6
|
+
* - Finds valid runnable dates based on raw data availability.
|
|
7
|
+
* * * UPDATE: Removed SQL output fetching support.
|
|
3
8
|
*/
|
|
4
9
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
this.
|
|
15
|
-
return this;
|
|
10
|
+
const fs = require('fs');
|
|
11
|
+
const path = require('path');
|
|
12
|
+
const { Orchestrator } = require('../execution/Orchestrator');
|
|
13
|
+
const { ManifestBuilder } = require('../core/Manifest');
|
|
14
|
+
|
|
15
|
+
class IntegrationTester {
|
|
16
|
+
constructor(baseConfig, logger = console) {
|
|
17
|
+
this.baseConfig = baseConfig;
|
|
18
|
+
this.logger = logger;
|
|
19
|
+
this.testConfig = this._createTestConfig(baseConfig);
|
|
16
20
|
}
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Create a modified config for testing
|
|
24
|
+
* - Writes to computation_results_test
|
|
25
|
+
* - Disables Cloud Tasks
|
|
26
|
+
* - Forces local execution (no worker pool)
|
|
27
|
+
*/
|
|
28
|
+
_createTestConfig(config) {
|
|
29
|
+
const testConfig = JSON.parse(JSON.stringify(config)); // Deep clone
|
|
30
|
+
|
|
31
|
+
// 1. Redirect Storage
|
|
32
|
+
if (!testConfig.resultStore) testConfig.resultStore = {};
|
|
33
|
+
testConfig.resultStore.table = 'computation_results_test';
|
|
34
|
+
|
|
35
|
+
// 2. Disable Side Effects
|
|
36
|
+
testConfig.cloudTasks = null; // Don't trigger downstream
|
|
37
|
+
|
|
38
|
+
// 3. Force Local Execution for simplified debugging
|
|
39
|
+
if (testConfig.workerPool) {
|
|
40
|
+
testConfig.workerPool.enabled = false;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Preserve the original computation classes (JSON.stringify lost them)
|
|
44
|
+
testConfig.computations = config.computations;
|
|
45
|
+
// Preserve rules (functions)
|
|
46
|
+
testConfig.rules = config.rules;
|
|
47
|
+
|
|
48
|
+
return testConfig;
|
|
21
49
|
}
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Build a standalone Orchestrator instance restricted to the specific dependency chain
|
|
53
|
+
*/
|
|
54
|
+
async setupForComputation(targetName) {
|
|
55
|
+
// 1. Identify Dependency Chain
|
|
56
|
+
const allComputations = this.testConfig.computations;
|
|
57
|
+
const chain = this._resolveDependencyChain(targetName, allComputations);
|
|
25
58
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
config: this.config,
|
|
33
|
-
logger: { log: () => {} }
|
|
34
|
-
};
|
|
59
|
+
this.logger.log('INFO', `[Tester] execution chain: ${chain.map(c => c.getConfig().name).join(' -> ')}`);
|
|
60
|
+
|
|
61
|
+
// 2. Configure Orchestrator with ONLY this chain
|
|
62
|
+
const runConfig = { ...this.testConfig, computations: chain };
|
|
63
|
+
this.orchestrator = new Orchestrator(runConfig, this.logger);
|
|
64
|
+
await this.orchestrator.initialize();
|
|
35
65
|
|
|
36
|
-
|
|
66
|
+
return chain;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Find the most recent date where Raw Data requirements are met
|
|
71
|
+
*/
|
|
72
|
+
async findLatestRunnableDate(targetName, lookbackDays = 7) {
|
|
73
|
+
if (!this.orchestrator) await this.setupForComputation(targetName);
|
|
74
|
+
|
|
75
|
+
const manifest = this.orchestrator.manifest;
|
|
76
|
+
const chainNames = new Set(manifest.map(c => c.name));
|
|
37
77
|
|
|
38
|
-
|
|
78
|
+
// Collect all Raw Data requirements (exclude requirements that are other computations in the chain)
|
|
79
|
+
const rawRequirements = {};
|
|
39
80
|
|
|
40
|
-
const
|
|
41
|
-
|
|
81
|
+
for (const entry of manifest) {
|
|
82
|
+
for (const [reqName, reqSpec] of Object.entries(entry.requires)) {
|
|
83
|
+
// If the requirement is NOT a computation in our chain, it's raw data
|
|
84
|
+
rawRequirements[reqName] = reqSpec;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
this.logger.log('INFO', `[Tester] Checking data availability for: ${Object.keys(rawRequirements).join(', ')}`);
|
|
89
|
+
|
|
90
|
+
// Iterate backwards from today
|
|
91
|
+
const today = new Date();
|
|
92
|
+
for (let i = 1; i <= lookbackDays; i++) {
|
|
93
|
+
const d = new Date(today);
|
|
94
|
+
d.setDate(today.getDate() - i);
|
|
95
|
+
const dateStr = d.toISOString().slice(0, 10);
|
|
96
|
+
|
|
97
|
+
const availability = await this.orchestrator.dataFetcher.checkAvailability(rawRequirements, dateStr);
|
|
98
|
+
|
|
99
|
+
if (availability.canRun) {
|
|
100
|
+
this.logger.log('INFO', `[Tester] Found runnable date: ${dateStr}`);
|
|
101
|
+
return dateStr;
|
|
102
|
+
} else {
|
|
103
|
+
this.logger.log('DEBUG', `[Tester] Date ${dateStr} missing: ${availability.missing.join(', ')}`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
throw new Error(`No runnable date found in last ${lookbackDays} days.`);
|
|
42
108
|
}
|
|
43
|
-
|
|
44
|
-
async
|
|
45
|
-
|
|
109
|
+
|
|
110
|
+
async run(targetName, dateStr) {
|
|
111
|
+
if (!this.orchestrator) await this.setupForComputation(targetName);
|
|
112
|
+
|
|
113
|
+
// Ensure test table exists
|
|
114
|
+
await this.orchestrator.storageManager._ensureBigQueryTable('computation_results_test');
|
|
46
115
|
|
|
116
|
+
this.logger.log('INFO', `[Tester] Running chain for ${targetName} on ${dateStr}`);
|
|
117
|
+
|
|
118
|
+
// Run the DAG
|
|
119
|
+
// Orchestrator will handle passes automatically (Deps first, then Target)
|
|
120
|
+
const summary = await this.orchestrator.execute({
|
|
121
|
+
date: dateStr,
|
|
122
|
+
force: true // Force run even if "up to date" in test table
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// UPDATE: Removed SQL output fetching logic.
|
|
126
|
+
// Always fetch from the standard State Repository (JSON result store).
|
|
127
|
+
const result = await this.orchestrator.stateRepository.getResult(dateStr, targetName);
|
|
128
|
+
|
|
47
129
|
return {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
130
|
+
date: dateStr,
|
|
131
|
+
summary,
|
|
132
|
+
result
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Recursively find all dependencies for the target computation
|
|
138
|
+
*/
|
|
139
|
+
_resolveDependencyChain(targetName, allComputations) {
|
|
140
|
+
const compMap = new Map(allComputations.map(c => {
|
|
141
|
+
try {
|
|
142
|
+
const conf = c.getConfig();
|
|
143
|
+
return [conf.name.toLowerCase(), c];
|
|
144
|
+
} catch (e) { return [null, null]; }
|
|
145
|
+
}));
|
|
146
|
+
|
|
147
|
+
const chain = new Set();
|
|
148
|
+
const visit = (name) => {
|
|
149
|
+
const key = name.toLowerCase();
|
|
150
|
+
if (chain.has(key)) return;
|
|
60
151
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
if (errors.length > 0) {
|
|
64
|
-
throw new Error(`Schema validation failed: ${errors.join(', ')}`);
|
|
65
|
-
}
|
|
66
|
-
return this;
|
|
67
|
-
},
|
|
152
|
+
const compClass = compMap.get(key);
|
|
153
|
+
if (!compClass) throw new Error(`Computation '${name}' not found.`);
|
|
68
154
|
|
|
69
|
-
|
|
70
|
-
return path.split('.').reduce((curr, key) => curr?.[key], obj);
|
|
71
|
-
},
|
|
155
|
+
const config = compClass.getConfig();
|
|
72
156
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
if (typeof data[key] !== type) {
|
|
77
|
-
errors.push(`${key}: expected ${type}, got ${typeof data[key]}`);
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
return errors;
|
|
157
|
+
// Recurse dependencies
|
|
158
|
+
if (config.dependencies) {
|
|
159
|
+
config.dependencies.forEach(dep => visit(dep));
|
|
81
160
|
}
|
|
161
|
+
// Recurse conditional dependencies
|
|
162
|
+
if (config.conditionalDependencies) {
|
|
163
|
+
config.conditionalDependencies.forEach(cd => visit(cd.computation));
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
chain.add(key);
|
|
82
167
|
};
|
|
168
|
+
|
|
169
|
+
visit(targetName);
|
|
170
|
+
|
|
171
|
+
return Array.from(chain).map(k => compMap.get(k));
|
|
83
172
|
}
|
|
84
173
|
}
|
|
85
174
|
|
|
86
|
-
module.exports = {
|
|
175
|
+
module.exports = { IntegrationTester };
|
|
@@ -121,17 +121,27 @@ async function runWatchdog(req, res) {
|
|
|
121
121
|
// 1. Find Zombies
|
|
122
122
|
const zombies = await storageManager.findZombies(ZOMBIE_THRESHOLD_MINUTES);
|
|
123
123
|
|
|
124
|
-
|
|
125
|
-
|
|
124
|
+
// Filter out excessive attempts
|
|
125
|
+
const actionableZombies = [];
|
|
126
|
+
for (const z of zombies) {
|
|
127
|
+
if ((z.attempts || 0) >= 3) {
|
|
128
|
+
console.warn(`[Watchdog] Ignoring zombie ${z.name} (Checkpoint: ${z.checkpointId}) - Max attempts reached (${z.attempts})`);
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
actionableZombies.push(z);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (actionableZombies.length === 0) {
|
|
135
|
+
return res.status(200).send('No recoverable zombies.');
|
|
126
136
|
}
|
|
127
137
|
|
|
128
|
-
console.log(`[Watchdog] 🧟 Found ${
|
|
138
|
+
console.log(`[Watchdog] 🧟 Found ${actionableZombies.length} zombies. Initiating recovery...`);
|
|
129
139
|
|
|
130
140
|
// 2. Claim & Recover
|
|
131
141
|
// We claim them first so the next watchdog doesn't grab them while we are dispatching
|
|
132
|
-
await Promise.all(
|
|
142
|
+
await Promise.all(actionableZombies.map(z => storageManager.claimZombie(z.checkpointId)));
|
|
133
143
|
|
|
134
|
-
const recoveryTasks =
|
|
144
|
+
const recoveryTasks = actionableZombies.map(z => {
|
|
135
145
|
const entry = manifest.find(m => m.name === z.name);
|
|
136
146
|
if (!entry) {
|
|
137
147
|
console.error(`[Watchdog] Computation ${z.name} no longer exists in manifest. Cannot recover.`);
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Test Computation DAG CLI
|
|
3
|
+
* Usage: node scripts/test-computation-dag.js <ComputationName> [Date]
|
|
4
|
+
* Example: node scripts/test-computation-dag.js UserPortfolioSummary
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
const { IntegrationTester } = require('../framework/testing/ComputationTester');
|
|
10
|
+
const config = require('../config/bulltrackers.config');
|
|
11
|
+
|
|
12
|
+
// Setup logging
|
|
13
|
+
const logger = {
|
|
14
|
+
log: (level, msg) => console.log(`[${new Date().toISOString()}] ${level}: ${msg}`),
|
|
15
|
+
error: (msg) => console.error(`[ERROR] ${msg}`)
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
async function main() {
|
|
19
|
+
const args = process.argv.slice(2);
|
|
20
|
+
if (args.length < 1) {
|
|
21
|
+
console.log('Usage: node scripts/test-computation-dag.js <ComputationName> [YYYY-MM-DD]');
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const targetName = args[0];
|
|
26
|
+
let dateStr = args[1]; // Optional
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
console.log('┌────────────────────────────────────────────────────────┐');
|
|
30
|
+
console.log('│ COMPUTATION INTEGRATION TEST │');
|
|
31
|
+
console.log('└────────────────────────────────────────────────────────┘');
|
|
32
|
+
console.log(`Target: ${targetName}`);
|
|
33
|
+
console.log(`Mode: DAG Integration (Production Read / Test Write)`);
|
|
34
|
+
|
|
35
|
+
const tester = new IntegrationTester(config, logger);
|
|
36
|
+
|
|
37
|
+
// 1. Setup & Resolve Chain
|
|
38
|
+
await tester.setupForComputation(targetName);
|
|
39
|
+
|
|
40
|
+
// 2. Find Date if not provided
|
|
41
|
+
if (!dateStr) {
|
|
42
|
+
console.log('\n🔍 Auto-detecting latest runnable date...');
|
|
43
|
+
dateStr = await tester.findLatestRunnableDate(targetName);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// 3. Execute
|
|
47
|
+
console.log(`\n🚀 Executing chain on ${dateStr}...`);
|
|
48
|
+
const output = await tester.run(targetName, dateStr);
|
|
49
|
+
|
|
50
|
+
// 4. Report
|
|
51
|
+
const { summary, result } = output;
|
|
52
|
+
|
|
53
|
+
console.log('\n┌────────────────────────────────────────────────────────┐');
|
|
54
|
+
console.log('│ TEST RESULTS │');
|
|
55
|
+
console.log('└────────────────────────────────────────────────────────┘');
|
|
56
|
+
|
|
57
|
+
if (summary.summary.errors > 0) {
|
|
58
|
+
console.log('❌ Execution FAILED');
|
|
59
|
+
summary.errors.forEach(e => console.log(` - ${e.name}: ${e.error}`));
|
|
60
|
+
} else {
|
|
61
|
+
console.log('✅ Execution SUCCESS');
|
|
62
|
+
|
|
63
|
+
// SQL computations: result is { _sqlOutput, outputTable, rowCount, rows }
|
|
64
|
+
if (result && result._sqlOutput) {
|
|
65
|
+
console.log(`\n📊 SQL output table: \`${config.bigquery.dataset}.${result.outputTable}\``);
|
|
66
|
+
console.log(` - Rows for ${dateStr}: ${result.rowCount}`);
|
|
67
|
+
if (result._error) {
|
|
68
|
+
console.log(` - ⚠️ Read error: ${result._error}`);
|
|
69
|
+
} else if (result.rowCount > 0) {
|
|
70
|
+
const sample = result.rows[0];
|
|
71
|
+
console.log(` - Sample row: ${JSON.stringify(sample).substring(0, 120)}...`);
|
|
72
|
+
const filename = `test-results-${targetName}-${dateStr}.json`;
|
|
73
|
+
const outputPath = path.join(__dirname, '..', filename);
|
|
74
|
+
fs.writeFileSync(outputPath, JSON.stringify(result.rows, null, 2));
|
|
75
|
+
console.log(`\n💾 Full rows saved to: ${outputPath}`);
|
|
76
|
+
} else {
|
|
77
|
+
console.log(' - No rows for this date. Check source table has data: portfolio_snapshots for this date.');
|
|
78
|
+
}
|
|
79
|
+
console.log(`\nQuery in BQ: SELECT * FROM \`${config.bigquery.projectId}.${config.bigquery.dataset}.${result.outputTable}\` WHERE date = '${dateStr}'`);
|
|
80
|
+
} else if (result) {
|
|
81
|
+
// Non-SQL: result from computation_results_test
|
|
82
|
+
const resultKeys = Object.keys(result);
|
|
83
|
+
const sampleKey = resultKeys[0];
|
|
84
|
+
const sampleData = resultKeys.length > 0 ? result[sampleKey] : null;
|
|
85
|
+
|
|
86
|
+
console.log(`\n📊 Result Summary for ${targetName}:`);
|
|
87
|
+
console.log(` - Count: ${resultKeys.length} entities/records`);
|
|
88
|
+
console.log(` - Sample ID: ${sampleKey || 'N/A'}`);
|
|
89
|
+
if (sampleData) {
|
|
90
|
+
console.log(` - Sample Data Preview: ${JSON.stringify(sampleData).substring(0, 100)}...`);
|
|
91
|
+
}
|
|
92
|
+
const filename = `test-results-${targetName}-${dateStr}.json`;
|
|
93
|
+
const outputPath = path.join(__dirname, '..', filename);
|
|
94
|
+
fs.writeFileSync(outputPath, JSON.stringify(result, null, 2));
|
|
95
|
+
console.log(`\n💾 Full results saved to: ${outputPath}`);
|
|
96
|
+
console.log(`Query results in BQ: SELECT * FROM \`${config.bigquery.dataset}.computation_results_test\` WHERE computation_name = '${targetName}' AND date = '${dateStr}'`);
|
|
97
|
+
} else {
|
|
98
|
+
console.log('⚠️ No results produced (Computation returned empty set).');
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
} catch (e) {
|
|
103
|
+
console.error('\n❌ FATAL ERROR:');
|
|
104
|
+
console.error(e);
|
|
105
|
+
process.exit(1);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
main();
|
|
@@ -43,7 +43,7 @@ async function storeSignedInUserPortfolio({ db, logger, collectionRegistry, cid,
|
|
|
43
43
|
date: date,
|
|
44
44
|
user_id: Number(cid),
|
|
45
45
|
user_type: 'SIGNED_IN_USER',
|
|
46
|
-
portfolio_data:
|
|
46
|
+
portfolio_data: portfolioData,
|
|
47
47
|
fetched_at: new Date().toISOString()
|
|
48
48
|
};
|
|
49
49
|
|
|
@@ -103,7 +103,7 @@ async function storeSignedInUserTradeHistory({ db, logger, collectionRegistry, c
|
|
|
103
103
|
date: date,
|
|
104
104
|
user_id: Number(cid),
|
|
105
105
|
user_type: 'SIGNED_IN_USER',
|
|
106
|
-
history_data:
|
|
106
|
+
history_data: historyData,
|
|
107
107
|
fetched_at: new Date().toISOString()
|
|
108
108
|
};
|
|
109
109
|
|
|
@@ -172,7 +172,7 @@ async function storeSignedInUserSocialPosts({ db, logger, collectionRegistry, ci
|
|
|
172
172
|
date: date,
|
|
173
173
|
user_id: Number(cid),
|
|
174
174
|
user_type: 'SIGNED_IN_USER',
|
|
175
|
-
posts_data:
|
|
175
|
+
posts_data: { posts: postsMap, postCount: posts.length },
|
|
176
176
|
fetched_at: new Date().toISOString()
|
|
177
177
|
};
|
|
178
178
|
|
|
@@ -257,7 +257,7 @@ async function storePopularInvestorPortfolio({ db, logger, collectionRegistry, c
|
|
|
257
257
|
date: date,
|
|
258
258
|
user_id: Number(cid),
|
|
259
259
|
user_type: 'POPULAR_INVESTOR',
|
|
260
|
-
portfolio_data:
|
|
260
|
+
portfolio_data: portfolioDoc,
|
|
261
261
|
fetched_at: new Date().toISOString()
|
|
262
262
|
};
|
|
263
263
|
|
|
@@ -316,7 +316,7 @@ async function storePopularInvestorTradeHistory({ db, logger, collectionRegistry
|
|
|
316
316
|
date: date,
|
|
317
317
|
user_id: Number(cid),
|
|
318
318
|
user_type: 'POPULAR_INVESTOR',
|
|
319
|
-
history_data:
|
|
319
|
+
history_data: historyData,
|
|
320
320
|
fetched_at: new Date().toISOString()
|
|
321
321
|
};
|
|
322
322
|
|
|
@@ -383,7 +383,7 @@ async function storePopularInvestorSocialPosts({ db, logger, collectionRegistry,
|
|
|
383
383
|
date: date,
|
|
384
384
|
user_id: Number(cid),
|
|
385
385
|
user_type: 'POPULAR_INVESTOR',
|
|
386
|
-
posts_data:
|
|
386
|
+
posts_data: { posts: postsMap, postCount: posts.length },
|
|
387
387
|
fetched_at: new Date().toISOString()
|
|
388
388
|
};
|
|
389
389
|
|
package/package.json
CHANGED
|
@@ -1,176 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @fileoverview Popular Investor Risk Assessment - v2
|
|
3
|
-
*
|
|
4
|
-
* This computation DEPENDS on PopularInvestorProfileMetrics.
|
|
5
|
-
* It uses the profile metrics to calculate additional risk assessments.
|
|
6
|
-
*
|
|
7
|
-
* This tests:
|
|
8
|
-
* 1. Dependency declaration works
|
|
9
|
-
* 2. Pass assignment is incremented (should be Pass 2)
|
|
10
|
-
* 3. Dependency data is loaded and passed to process()
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
const { Computation } = require('../framework');
|
|
14
|
-
|
|
15
|
-
class PopularInvestorRiskAssessment extends Computation {
|
|
16
|
-
|
|
17
|
-
static getConfig() {
|
|
18
|
-
return {
|
|
19
|
-
name: 'PopularInvestorRiskAssessment',
|
|
20
|
-
category: 'popular_investor',
|
|
21
|
-
|
|
22
|
-
// Only needs rankings for additional risk factors
|
|
23
|
-
requires: {
|
|
24
|
-
'pi_rankings': {
|
|
25
|
-
lookback: 0,
|
|
26
|
-
mandatory: true,
|
|
27
|
-
filter: null
|
|
28
|
-
}
|
|
29
|
-
},
|
|
30
|
-
|
|
31
|
-
// DEPENDS ON PopularInvestorProfileMetrics!
|
|
32
|
-
dependencies: ['PopularInvestorProfileMetrics'],
|
|
33
|
-
|
|
34
|
-
type: 'per-entity',
|
|
35
|
-
isHistorical: false,
|
|
36
|
-
ttlDays: 30
|
|
37
|
-
};
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
static getSchema() {
|
|
41
|
-
return {
|
|
42
|
-
type: 'object',
|
|
43
|
-
properties: {
|
|
44
|
-
riskScore: { type: 'number' },
|
|
45
|
-
riskLevel: { type: 'string' },
|
|
46
|
-
diversificationScore: { type: 'number' },
|
|
47
|
-
volatilityScore: { type: 'number' },
|
|
48
|
-
concentrationRisk: { type: 'number' },
|
|
49
|
-
sectorConcentration: { type: 'object' },
|
|
50
|
-
recommendations: { type: 'array' }
|
|
51
|
-
}
|
|
52
|
-
};
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
static getWeight() {
|
|
56
|
-
return 3.0;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
async process(context) {
|
|
60
|
-
const { date, entityId: userId, data, dependencies } = context;
|
|
61
|
-
|
|
62
|
-
// Get the profile metrics from our dependency
|
|
63
|
-
const profileMetrics = dependencies?.['popularinvestorprofilemetrics']?.[userId];
|
|
64
|
-
|
|
65
|
-
// Get rankings data
|
|
66
|
-
const rankingsData = data['pi_rankings'];
|
|
67
|
-
const rankEntry = this._findRankEntry(rankingsData, userId);
|
|
68
|
-
|
|
69
|
-
// Initialize result
|
|
70
|
-
const result = {
|
|
71
|
-
riskScore: 0,
|
|
72
|
-
riskLevel: 'Unknown',
|
|
73
|
-
diversificationScore: 0,
|
|
74
|
-
volatilityScore: 0,
|
|
75
|
-
concentrationRisk: 0,
|
|
76
|
-
sectorConcentration: {},
|
|
77
|
-
recommendations: []
|
|
78
|
-
};
|
|
79
|
-
|
|
80
|
-
// If we don't have profile metrics, we can still calculate basic risk from rankings
|
|
81
|
-
if (rankEntry) {
|
|
82
|
-
const riskScore = rankEntry.RiskScore || rankEntry.riskScore || 0;
|
|
83
|
-
result.riskScore = riskScore;
|
|
84
|
-
result.riskLevel = this._getRiskLevel(riskScore);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// If we have profile metrics, enhance the assessment
|
|
88
|
-
if (profileMetrics) {
|
|
89
|
-
// Use sector exposure for diversification analysis
|
|
90
|
-
const sectorExposure = profileMetrics.sectorExposure?.data || {};
|
|
91
|
-
const sectors = Object.keys(sectorExposure);
|
|
92
|
-
|
|
93
|
-
if (sectors.length > 0) {
|
|
94
|
-
// Diversification score (more sectors = better diversification)
|
|
95
|
-
result.diversificationScore = Math.min(100, sectors.length * 15);
|
|
96
|
-
|
|
97
|
-
// Concentration risk (highest sector weight)
|
|
98
|
-
const maxExposure = Math.max(...Object.values(sectorExposure), 0);
|
|
99
|
-
result.concentrationRisk = maxExposure;
|
|
100
|
-
|
|
101
|
-
// Copy sector breakdown
|
|
102
|
-
result.sectorConcentration = sectorExposure;
|
|
103
|
-
|
|
104
|
-
// Generate recommendations
|
|
105
|
-
if (maxExposure > 50) {
|
|
106
|
-
result.recommendations.push(`High concentration in single sector (${maxExposure.toFixed(1)}%). Consider diversifying.`);
|
|
107
|
-
}
|
|
108
|
-
if (sectors.length < 3) {
|
|
109
|
-
result.recommendations.push('Portfolio is concentrated in few sectors. Consider adding exposure to other sectors.');
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
// Use portfolio summary for additional risk factors
|
|
114
|
-
const portfolioSummary = profileMetrics.portfolioSummary || {};
|
|
115
|
-
if (portfolioSummary.profitPercent < -10) {
|
|
116
|
-
result.recommendations.push('Portfolio showing significant losses. Review position sizing.');
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
// Use rankings data from profile for win ratio analysis
|
|
120
|
-
const rankingsFromProfile = profileMetrics.rankingsData || {};
|
|
121
|
-
if (rankingsFromProfile.winRatio < 0.5) {
|
|
122
|
-
result.volatilityScore = 80; // Lower win ratio = higher volatility risk
|
|
123
|
-
result.recommendations.push('Win ratio below 50%. Trading strategy may need review.');
|
|
124
|
-
} else {
|
|
125
|
-
result.volatilityScore = Math.max(0, 100 - (rankingsFromProfile.winRatio * 100));
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
// Adjust overall risk score based on analysis
|
|
129
|
-
const adjustedRisk = (
|
|
130
|
-
(result.riskScore * 0.4) +
|
|
131
|
-
(result.concentrationRisk * 0.3) +
|
|
132
|
-
(result.volatilityScore * 0.3)
|
|
133
|
-
);
|
|
134
|
-
result.riskScore = Math.round(adjustedRisk);
|
|
135
|
-
result.riskLevel = this._getRiskLevel(result.riskScore);
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
this.setResult(userId, result);
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
_findRankEntry(rankings, userId) {
|
|
142
|
-
if (!rankings) return null;
|
|
143
|
-
|
|
144
|
-
// Rankings might be date-keyed or an array
|
|
145
|
-
if (typeof rankings === 'object' && !Array.isArray(rankings)) {
|
|
146
|
-
const dates = Object.keys(rankings).sort().reverse();
|
|
147
|
-
for (const dateStr of dates) {
|
|
148
|
-
const dayRankings = rankings[dateStr];
|
|
149
|
-
if (Array.isArray(dayRankings)) {
|
|
150
|
-
const entry = dayRankings.find(r =>
|
|
151
|
-
String(r.CustomerId || r.pi_id || r.userId) === String(userId)
|
|
152
|
-
);
|
|
153
|
-
if (entry) return entry;
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
if (Array.isArray(rankings)) {
|
|
159
|
-
return rankings.find(r =>
|
|
160
|
-
String(r.CustomerId || r.pi_id || r.userId) === String(userId)
|
|
161
|
-
);
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
return null;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
_getRiskLevel(score) {
|
|
168
|
-
if (score <= 2) return 'Very Low';
|
|
169
|
-
if (score <= 4) return 'Low';
|
|
170
|
-
if (score <= 6) return 'Medium';
|
|
171
|
-
if (score <= 8) return 'High';
|
|
172
|
-
return 'Very High';
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
module.exports = PopularInvestorRiskAssessment;
|