bulltrackers-module 1.0.766 → 1.0.769
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/UserPortfolioMetrics.js +50 -0
- package/functions/computation-system-v2/computations/BehavioralAnomaly.js +559 -227
- package/functions/computation-system-v2/computations/GlobalAumPerAsset30D.js +103 -0
- 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/PIDailyAssetAUM.js +134 -0
- package/functions/computation-system-v2/computations/PiFeatureVectors.js +227 -0
- package/functions/computation-system-v2/computations/PiRecommender.js +359 -0
- package/functions/computation-system-v2/computations/PopularInvestorProfileMetrics.js +354 -641
- package/functions/computation-system-v2/computations/SignedInUserList.js +51 -0
- package/functions/computation-system-v2/computations/SignedInUserMirrorHistory.js +138 -0
- package/functions/computation-system-v2/computations/SignedInUserPIProfileMetrics.js +106 -0
- package/functions/computation-system-v2/computations/SignedInUserProfileMetrics.js +324 -0
- package/functions/computation-system-v2/config/bulltrackers.config.js +40 -126
- package/functions/computation-system-v2/core-api.js +17 -9
- package/functions/computation-system-v2/data_schema_reference.MD +108 -0
- package/functions/computation-system-v2/devtools/builder/builder.js +362 -0
- package/functions/computation-system-v2/devtools/builder/examples/user-metrics.yaml +26 -0
- package/functions/computation-system-v2/devtools/index.js +36 -0
- package/functions/computation-system-v2/devtools/shared/MockDataFactory.js +235 -0
- package/functions/computation-system-v2/devtools/shared/SchemaTemplates.js +475 -0
- package/functions/computation-system-v2/devtools/shared/SystemIntrospector.js +517 -0
- package/functions/computation-system-v2/devtools/shared/index.js +16 -0
- package/functions/computation-system-v2/devtools/simulation/DAGAnalyzer.js +243 -0
- package/functions/computation-system-v2/devtools/simulation/MockDataFetcher.js +306 -0
- package/functions/computation-system-v2/devtools/simulation/MockStorageManager.js +336 -0
- package/functions/computation-system-v2/devtools/simulation/SimulationEngine.js +525 -0
- package/functions/computation-system-v2/devtools/simulation/SimulationServer.js +581 -0
- package/functions/computation-system-v2/devtools/simulation/index.js +17 -0
- package/functions/computation-system-v2/devtools/simulation/simulate.js +324 -0
- package/functions/computation-system-v2/devtools/vscode-computation/package.json +90 -0
- package/functions/computation-system-v2/devtools/vscode-computation/snippets/computation.json +128 -0
- package/functions/computation-system-v2/devtools/vscode-computation/src/extension.ts +401 -0
- package/functions/computation-system-v2/devtools/vscode-computation/src/providers/codeActions.ts +152 -0
- package/functions/computation-system-v2/devtools/vscode-computation/src/providers/completions.ts +207 -0
- package/functions/computation-system-v2/devtools/vscode-computation/src/providers/diagnostics.ts +205 -0
- package/functions/computation-system-v2/devtools/vscode-computation/src/providers/hover.ts +205 -0
- package/functions/computation-system-v2/devtools/vscode-computation/tsconfig.json +22 -0
- package/functions/computation-system-v2/docs/HowToCreateComputations.MD +602 -0
- 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 +330 -126
- package/functions/computation-system-v2/framework/data/MaterializedViewManager.js +84 -0
- package/functions/computation-system-v2/framework/data/QueryBuilder.js +38 -38
- package/functions/computation-system-v2/framework/execution/Orchestrator.js +226 -153
- package/functions/computation-system-v2/framework/scheduling/ScheduleValidator.js +17 -19
- package/functions/computation-system-v2/framework/storage/StateRepository.js +32 -2
- package/functions/computation-system-v2/framework/storage/StorageManager.js +111 -83
- package/functions/computation-system-v2/framework/testing/ComputationTester.js +161 -66
- package/functions/computation-system-v2/handlers/dispatcher.js +57 -29
- package/functions/computation-system-v2/legacy/PiAssetRecommender.js.old +115 -0
- package/functions/computation-system-v2/legacy/PiSimilarityMatrix.js +104 -0
- package/functions/computation-system-v2/legacy/PiSimilarityVector.js +71 -0
- package/functions/computation-system-v2/scripts/debug_aggregation.js +25 -0
- package/functions/computation-system-v2/scripts/test-computation-dag.js +109 -0
- package/functions/computation-system-v2/scripts/test-invalidation-scenarios.js +234 -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
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Declarative Computation Builder
|
|
3
|
+
*
|
|
4
|
+
* Generates computation code from YAML specifications.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* node builder.js <spec.yaml> [--output <dir>]
|
|
8
|
+
* node builder.js --interactive
|
|
9
|
+
*
|
|
10
|
+
* YAML Schema:
|
|
11
|
+
* name: ComputationName
|
|
12
|
+
* description: What it does
|
|
13
|
+
* type: per-entity | global
|
|
14
|
+
* category: signed_in_user | popular_investor | global
|
|
15
|
+
* isHistorical: true | false
|
|
16
|
+
* requires:
|
|
17
|
+
* table_name:
|
|
18
|
+
* lookback: 30
|
|
19
|
+
* mandatory: true
|
|
20
|
+
* fields: [field1, field2]
|
|
21
|
+
* dependencies: [OtherComputation]
|
|
22
|
+
* storage:
|
|
23
|
+
* bigquery: true
|
|
24
|
+
* firestore:
|
|
25
|
+
* enabled: true
|
|
26
|
+
* path: 'collection/{entityId}'
|
|
27
|
+
* logic:
|
|
28
|
+
* extract:
|
|
29
|
+
* - variable: positions
|
|
30
|
+
* from: portfolio_snapshots
|
|
31
|
+
* using: rules.portfolio.extractPositions
|
|
32
|
+
* compute:
|
|
33
|
+
* - variable: result
|
|
34
|
+
* expression: positions.length
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
const fs = require('fs');
|
|
38
|
+
const path = require('path');
|
|
39
|
+
const readline = require('readline');
|
|
40
|
+
|
|
41
|
+
// Simple YAML parser (no external deps)
|
|
42
|
+
function parseYAML(content) {
|
|
43
|
+
const lines = content.split('\n');
|
|
44
|
+
const result = {};
|
|
45
|
+
const stack = [{ indent: -2, obj: result }];
|
|
46
|
+
let currentKey = null;
|
|
47
|
+
let listContext = null;
|
|
48
|
+
|
|
49
|
+
for (const rawLine of lines) {
|
|
50
|
+
const line = rawLine.replace(/\r$/, '');
|
|
51
|
+
if (line.trim() === '' || line.trim().startsWith('#')) continue;
|
|
52
|
+
|
|
53
|
+
const indent = line.search(/\S/);
|
|
54
|
+
const trimmed = line.trim();
|
|
55
|
+
|
|
56
|
+
// Pop stack to find parent
|
|
57
|
+
while (stack.length > 1 && indent <= stack[stack.length - 1].indent) {
|
|
58
|
+
stack.pop();
|
|
59
|
+
}
|
|
60
|
+
const parent = stack[stack.length - 1].obj;
|
|
61
|
+
|
|
62
|
+
// Handle list item
|
|
63
|
+
if (trimmed.startsWith('- ')) {
|
|
64
|
+
const value = trimmed.slice(2).trim();
|
|
65
|
+
if (!Array.isArray(parent[currentKey])) {
|
|
66
|
+
parent[currentKey] = [];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (value.includes(':')) {
|
|
70
|
+
// Object in list
|
|
71
|
+
const obj = {};
|
|
72
|
+
const [k, v] = value.split(':').map(s => s.trim());
|
|
73
|
+
obj[k] = parseValue(v);
|
|
74
|
+
parent[currentKey].push(obj);
|
|
75
|
+
stack.push({ indent, obj });
|
|
76
|
+
} else {
|
|
77
|
+
parent[currentKey].push(parseValue(value));
|
|
78
|
+
}
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Handle key: value
|
|
83
|
+
const colonIdx = trimmed.indexOf(':');
|
|
84
|
+
if (colonIdx > 0) {
|
|
85
|
+
const key = trimmed.slice(0, colonIdx).trim();
|
|
86
|
+
const value = trimmed.slice(colonIdx + 1).trim();
|
|
87
|
+
|
|
88
|
+
if (value === '') {
|
|
89
|
+
// Nested object
|
|
90
|
+
parent[key] = {};
|
|
91
|
+
currentKey = key;
|
|
92
|
+
stack.push({ indent, obj: parent[key] });
|
|
93
|
+
} else if (value.startsWith('[') && value.endsWith(']')) {
|
|
94
|
+
// Inline array
|
|
95
|
+
parent[key] = value.slice(1, -1).split(',').map(s => parseValue(s.trim()));
|
|
96
|
+
currentKey = key;
|
|
97
|
+
} else {
|
|
98
|
+
parent[key] = parseValue(value);
|
|
99
|
+
currentKey = key;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return result;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function parseValue(str) {
|
|
108
|
+
if (str === 'true') return true;
|
|
109
|
+
if (str === 'false') return false;
|
|
110
|
+
if (str === 'null') return null;
|
|
111
|
+
if (/^\d+$/.test(str)) return parseInt(str);
|
|
112
|
+
if (/^\d+\.\d+$/.test(str)) return parseFloat(str);
|
|
113
|
+
return str.replace(/^['"]|['"]$/g, '');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Template engine
|
|
117
|
+
function generateCode(spec) {
|
|
118
|
+
const { name, description, type, category, isHistorical, requires, dependencies, storage, logic } = spec;
|
|
119
|
+
|
|
120
|
+
// Build requires block
|
|
121
|
+
const requiresCode = Object.entries(requires || {}).map(([table, config]) => {
|
|
122
|
+
const fields = config.fields ? `fields: [${config.fields.map(f => `'${f}'`).join(', ')}]` : '';
|
|
123
|
+
return ` '${table}': {
|
|
124
|
+
lookback: ${config.lookback || 0},
|
|
125
|
+
mandatory: ${config.mandatory || false}${fields ? `,\n ${fields}` : ''}
|
|
126
|
+
}`;
|
|
127
|
+
}).join(',\n');
|
|
128
|
+
|
|
129
|
+
// Build dependencies
|
|
130
|
+
const depsCode = dependencies && dependencies.length > 0
|
|
131
|
+
? `\n dependencies: [${dependencies.map(d => `'${d}'`).join(', ')}],`
|
|
132
|
+
: '';
|
|
133
|
+
|
|
134
|
+
// Build storage config
|
|
135
|
+
let storageCode = '';
|
|
136
|
+
if (storage) {
|
|
137
|
+
const firestoreConfig = storage.firestore?.enabled
|
|
138
|
+
? `\n firestore: {
|
|
139
|
+
enabled: true,
|
|
140
|
+
path: '${storage.firestore.path || 'collection/{entityId}'}',
|
|
141
|
+
merge: ${storage.firestore.merge || true}
|
|
142
|
+
}`
|
|
143
|
+
: '\n firestore: { enabled: false }';
|
|
144
|
+
|
|
145
|
+
storageCode = `
|
|
146
|
+
storage: {
|
|
147
|
+
bigquery: ${storage.bigquery !== false},${firestoreConfig}
|
|
148
|
+
}`;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Build process method
|
|
152
|
+
const processCode = buildProcessCode(logic, requires, type);
|
|
153
|
+
|
|
154
|
+
return `const { Computation } = require('../framework');
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* ${name}
|
|
158
|
+
* ${description || 'Auto-generated computation'}
|
|
159
|
+
*
|
|
160
|
+
* Generated: ${new Date().toISOString()}
|
|
161
|
+
*/
|
|
162
|
+
class ${name} extends Computation {
|
|
163
|
+
static getConfig() {
|
|
164
|
+
return {
|
|
165
|
+
name: '${name}',
|
|
166
|
+
description: '${description || ''}',
|
|
167
|
+
type: '${type || 'per-entity'}',
|
|
168
|
+
category: '${category || 'signed_in_user'}',
|
|
169
|
+
isHistorical: ${isHistorical || false},${depsCode}
|
|
170
|
+
requires: {
|
|
171
|
+
${requiresCode}
|
|
172
|
+
},${storageCode}
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
${processCode}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
module.exports = ${name};
|
|
180
|
+
`;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function buildProcessCode(logic, requires, type) {
|
|
184
|
+
const lines = [];
|
|
185
|
+
const tables = Object.keys(requires || {});
|
|
186
|
+
|
|
187
|
+
if (type === 'global') {
|
|
188
|
+
lines.push(' async process(data, { targetDate, rules }) {');
|
|
189
|
+
} else {
|
|
190
|
+
lines.push(' async process(data, { entityId, targetDate, rules, getDependency }) {');
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Extract data from tables
|
|
194
|
+
for (const table of tables) {
|
|
195
|
+
if (type === 'global') {
|
|
196
|
+
lines.push(` const ${toCamelCase(table)} = data.${table} || {};`);
|
|
197
|
+
} else {
|
|
198
|
+
lines.push(` const ${toCamelCase(table)} = data.${table}[entityId] || [];`);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
lines.push('');
|
|
203
|
+
|
|
204
|
+
// Generate extraction logic
|
|
205
|
+
if (logic?.extract) {
|
|
206
|
+
for (const ext of logic.extract) {
|
|
207
|
+
if (ext.using) {
|
|
208
|
+
lines.push(` const ${ext.variable} = ${ext.using}(${toCamelCase(ext.from)});`);
|
|
209
|
+
} else {
|
|
210
|
+
lines.push(` const ${ext.variable} = ${toCamelCase(ext.from)};`);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
lines.push('');
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Generate computation logic
|
|
217
|
+
if (logic?.compute) {
|
|
218
|
+
for (const comp of logic.compute) {
|
|
219
|
+
lines.push(` const ${comp.variable} = ${comp.expression};`);
|
|
220
|
+
}
|
|
221
|
+
lines.push('');
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Build return statement
|
|
225
|
+
if (logic?.compute) {
|
|
226
|
+
const resultVars = logic.compute.map(c => c.variable);
|
|
227
|
+
lines.push(' return {');
|
|
228
|
+
for (const v of resultVars) {
|
|
229
|
+
lines.push(` ${v},`);
|
|
230
|
+
}
|
|
231
|
+
lines.push(' };');
|
|
232
|
+
} else {
|
|
233
|
+
lines.push(' // TODO: Implement computation logic');
|
|
234
|
+
lines.push(' return {};');
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
lines.push(' }');
|
|
238
|
+
|
|
239
|
+
return lines.join('\n');
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function toCamelCase(str) {
|
|
243
|
+
return str.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Interactive CLI
|
|
247
|
+
async function runInteractive() {
|
|
248
|
+
const rl = readline.createInterface({
|
|
249
|
+
input: process.stdin,
|
|
250
|
+
output: process.stdout
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
const ask = (q) => new Promise(resolve => rl.question(q, resolve));
|
|
254
|
+
|
|
255
|
+
console.log('\n🔧 Computation Builder - Interactive Mode\n');
|
|
256
|
+
|
|
257
|
+
const spec = {
|
|
258
|
+
requires: {},
|
|
259
|
+
storage: { bigquery: true, firestore: { enabled: false } }
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
spec.name = await ask('Computation name (PascalCase): ');
|
|
263
|
+
spec.description = await ask('Description: ');
|
|
264
|
+
|
|
265
|
+
const typeChoice = await ask('Type (1=per-entity, 2=global): ');
|
|
266
|
+
spec.type = typeChoice === '2' ? 'global' : 'per-entity';
|
|
267
|
+
|
|
268
|
+
const catChoice = await ask('Category (1=signed_in_user, 2=popular_investor, 3=global): ');
|
|
269
|
+
spec.category = { '1': 'signed_in_user', '2': 'popular_investor', '3': 'global' }[catChoice] || 'signed_in_user';
|
|
270
|
+
|
|
271
|
+
const historical = await ask('Is historical? (y/N): ');
|
|
272
|
+
spec.isHistorical = historical.toLowerCase() === 'y';
|
|
273
|
+
|
|
274
|
+
// Add tables
|
|
275
|
+
console.log('\nAdd required tables (empty name to finish):');
|
|
276
|
+
while (true) {
|
|
277
|
+
const table = await ask(' Table name: ');
|
|
278
|
+
if (!table) break;
|
|
279
|
+
|
|
280
|
+
const lookback = await ask(' Lookback days (0): ');
|
|
281
|
+
const mandatory = await ask(' Mandatory? (Y/n): ');
|
|
282
|
+
|
|
283
|
+
spec.requires[table] = {
|
|
284
|
+
lookback: parseInt(lookback) || 0,
|
|
285
|
+
mandatory: mandatory.toLowerCase() !== 'n'
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Dependencies
|
|
290
|
+
const deps = await ask('\nDependencies (comma-separated, or empty): ');
|
|
291
|
+
if (deps) {
|
|
292
|
+
spec.dependencies = deps.split(',').map(d => d.trim());
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Firestore
|
|
296
|
+
const useFirestore = await ask('Enable Firestore? (y/N): ');
|
|
297
|
+
if (useFirestore.toLowerCase() === 'y') {
|
|
298
|
+
spec.storage.firestore.enabled = true;
|
|
299
|
+
spec.storage.firestore.path = await ask(' Firestore path: ');
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
rl.close();
|
|
303
|
+
|
|
304
|
+
const code = generateCode(spec);
|
|
305
|
+
const filename = `${spec.name}.js`;
|
|
306
|
+
|
|
307
|
+
console.log(`\n📝 Generated ${filename}\n`);
|
|
308
|
+
console.log(code);
|
|
309
|
+
|
|
310
|
+
const outputPath = path.join(process.cwd(), filename);
|
|
311
|
+
fs.writeFileSync(outputPath, code);
|
|
312
|
+
console.log(`\n✅ Saved to ${outputPath}`);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// CLI Entry
|
|
316
|
+
async function main() {
|
|
317
|
+
const args = process.argv.slice(2);
|
|
318
|
+
|
|
319
|
+
if (args.includes('--interactive') || args.includes('-i')) {
|
|
320
|
+
await runInteractive();
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
if (args.length === 0) {
|
|
325
|
+
console.log(`
|
|
326
|
+
Computation Builder
|
|
327
|
+
|
|
328
|
+
Usage:
|
|
329
|
+
node builder.js <spec.yaml> Generate from YAML spec
|
|
330
|
+
node builder.js --interactive Interactive wizard
|
|
331
|
+
|
|
332
|
+
Options:
|
|
333
|
+
--output <dir> Output directory (default: current)
|
|
334
|
+
`);
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const specFile = args[0];
|
|
339
|
+
if (!fs.existsSync(specFile)) {
|
|
340
|
+
console.error(`File not found: ${specFile}`);
|
|
341
|
+
process.exit(1);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const content = fs.readFileSync(specFile, 'utf8');
|
|
345
|
+
const spec = parseYAML(content);
|
|
346
|
+
const code = generateCode(spec);
|
|
347
|
+
|
|
348
|
+
const outputIdx = args.indexOf('--output');
|
|
349
|
+
const outputDir = outputIdx >= 0 && args[outputIdx + 1]
|
|
350
|
+
? args[outputIdx + 1]
|
|
351
|
+
: process.cwd();
|
|
352
|
+
|
|
353
|
+
const filename = `${spec.name}.js`;
|
|
354
|
+
const outputPath = path.join(outputDir, filename);
|
|
355
|
+
|
|
356
|
+
fs.writeFileSync(outputPath, code);
|
|
357
|
+
console.log(`✅ Generated ${outputPath}`);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
main().catch(console.error);
|
|
361
|
+
|
|
362
|
+
module.exports = { parseYAML, generateCode };
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Simple Example - No logic block (parser handles well)
|
|
2
|
+
name: UserPortfolioMetrics
|
|
3
|
+
description: Calculate portfolio metrics for signed-in users
|
|
4
|
+
|
|
5
|
+
type: per-entity
|
|
6
|
+
category: signed_in_user
|
|
7
|
+
isHistorical: true
|
|
8
|
+
|
|
9
|
+
requires:
|
|
10
|
+
portfolio_snapshots:
|
|
11
|
+
lookback: 30
|
|
12
|
+
mandatory: true
|
|
13
|
+
fields: [user_id, date, portfolio_data, cash, total_value]
|
|
14
|
+
trade_history_snapshots:
|
|
15
|
+
lookback: 30
|
|
16
|
+
mandatory: false
|
|
17
|
+
fields: [user_id, date, trade_history]
|
|
18
|
+
|
|
19
|
+
dependencies: [SignedInUserList]
|
|
20
|
+
|
|
21
|
+
storage:
|
|
22
|
+
bigquery: true
|
|
23
|
+
firestore:
|
|
24
|
+
enabled: true
|
|
25
|
+
path: users/{entityId}/metrics
|
|
26
|
+
merge: true
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview DevTools Main Index
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
// Shared foundation
|
|
6
|
+
const shared = require('./shared');
|
|
7
|
+
const { SystemIntrospector, MockDataFactory, SchemaTemplates } = shared;
|
|
8
|
+
|
|
9
|
+
// Simulation engine
|
|
10
|
+
const simulation = require('./simulation');
|
|
11
|
+
const { SimulationEngine, SimulationServer, MockDataFetcher, MockStorageManager, DAGAnalyzer } = simulation;
|
|
12
|
+
|
|
13
|
+
// Builder
|
|
14
|
+
const { parseYAML, generateCode } = require('./builder/builder');
|
|
15
|
+
|
|
16
|
+
module.exports = {
|
|
17
|
+
// Foundation
|
|
18
|
+
SystemIntrospector,
|
|
19
|
+
MockDataFactory,
|
|
20
|
+
SchemaTemplates,
|
|
21
|
+
|
|
22
|
+
// Simulation
|
|
23
|
+
SimulationEngine,
|
|
24
|
+
SimulationServer,
|
|
25
|
+
MockDataFetcher,
|
|
26
|
+
MockStorageManager,
|
|
27
|
+
DAGAnalyzer,
|
|
28
|
+
|
|
29
|
+
// Builder
|
|
30
|
+
parseYAML,
|
|
31
|
+
generateCode,
|
|
32
|
+
|
|
33
|
+
// Re-export sub-modules
|
|
34
|
+
shared,
|
|
35
|
+
simulation
|
|
36
|
+
};
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Mock Data Factory
|
|
3
|
+
*
|
|
4
|
+
* Generates realistic mock data for the simulation engine.
|
|
5
|
+
* Uses SchemaTemplates to create data that matches actual BigQuery structures.
|
|
6
|
+
*
|
|
7
|
+
* Features:
|
|
8
|
+
* - Schema-aware row generation
|
|
9
|
+
* - Entity-keyed data for per-entity computations
|
|
10
|
+
* - Date range support for lookback simulation
|
|
11
|
+
* - Reproducible output with seeding
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const { SchemaTemplates, helpers } = require('./SchemaTemplates');
|
|
15
|
+
const { randomInt, randomFloat, randomChoice, randomPastDate, randomUUID, randomHex } = helpers;
|
|
16
|
+
|
|
17
|
+
class MockDataFactory {
|
|
18
|
+
/**
|
|
19
|
+
* @param {Object} options
|
|
20
|
+
* @param {number} [options.seed] - Random seed for reproducibility
|
|
21
|
+
* @param {Object} [options.introspector] - SystemIntrospector for metadata
|
|
22
|
+
*/
|
|
23
|
+
constructor(options = {}) {
|
|
24
|
+
this.introspector = options.introspector || null;
|
|
25
|
+
this.seed = options.seed || null;
|
|
26
|
+
|
|
27
|
+
// If seeded, we'd implement a seeded RNG here
|
|
28
|
+
// For now, using Math.random()
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Generate mock data for a table.
|
|
33
|
+
*
|
|
34
|
+
* @param {string} tableName - Table to generate data for
|
|
35
|
+
* @param {Object} options
|
|
36
|
+
* @param {number} [options.entityCount=10] - Number of entities
|
|
37
|
+
* @param {number} [options.daysBack=7] - Days of history to generate
|
|
38
|
+
* @param {string[]} [options.entityIds] - Specific entity IDs to use
|
|
39
|
+
* @param {Date|string} [options.asOfDate] - Reference date (defaults to today)
|
|
40
|
+
* @returns {Object} { rows: [], byEntity: Map<entityId, rows[]> }
|
|
41
|
+
*/
|
|
42
|
+
generate(tableName, options = {}) {
|
|
43
|
+
const template = SchemaTemplates[tableName];
|
|
44
|
+
if (!template) {
|
|
45
|
+
throw new Error(`No schema template for table: ${tableName}`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const entityCount = options.entityCount || 10;
|
|
49
|
+
const daysBack = options.daysBack || 7;
|
|
50
|
+
const asOfDate = options.asOfDate ? new Date(options.asOfDate) : new Date();
|
|
51
|
+
|
|
52
|
+
// Generate entity IDs
|
|
53
|
+
const entityIds = options.entityIds || this._generateEntityIds(tableName, entityCount);
|
|
54
|
+
|
|
55
|
+
// Get table metadata
|
|
56
|
+
const meta = this.introspector?.getTableMetadata(tableName) || this._inferMetadata(tableName, template);
|
|
57
|
+
|
|
58
|
+
const rows = [];
|
|
59
|
+
const byEntity = new Map();
|
|
60
|
+
|
|
61
|
+
// Generate rows
|
|
62
|
+
for (const entityId of entityIds) {
|
|
63
|
+
const entityRows = [];
|
|
64
|
+
|
|
65
|
+
if (meta.dateField) {
|
|
66
|
+
// Date-partitioned table: generate one row per day
|
|
67
|
+
for (let d = 0; d <= daysBack; d++) {
|
|
68
|
+
const date = new Date(asOfDate);
|
|
69
|
+
date.setDate(date.getDate() - d);
|
|
70
|
+
|
|
71
|
+
const row = this._generateRow(tableName, template, entityId, date, meta);
|
|
72
|
+
rows.push(row);
|
|
73
|
+
entityRows.push(row);
|
|
74
|
+
}
|
|
75
|
+
} else {
|
|
76
|
+
// Non-partitioned table: single row per entity
|
|
77
|
+
const row = this._generateRow(tableName, template, entityId, null, meta);
|
|
78
|
+
rows.push(row);
|
|
79
|
+
entityRows.push(row);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
byEntity.set(entityId, entityRows);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return { rows, byEntity };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Generate data mimicking the DataFetcher response format.
|
|
90
|
+
* This is what computations actually receive.
|
|
91
|
+
*
|
|
92
|
+
* @param {Object} requires - The computation's requires config
|
|
93
|
+
* @param {Object} options
|
|
94
|
+
* @param {number} [options.entityCount=10]
|
|
95
|
+
* @param {string[]} [options.entityIds]
|
|
96
|
+
* @param {Date|string} [options.asOfDate]
|
|
97
|
+
* @returns {Object} Data object keyed by table name
|
|
98
|
+
*/
|
|
99
|
+
generateForComputation(requires, options = {}) {
|
|
100
|
+
const data = {};
|
|
101
|
+
const entityCount = options.entityCount || 10;
|
|
102
|
+
const entityIds = options.entityIds || this._generateEntityIds('default', entityCount);
|
|
103
|
+
|
|
104
|
+
for (const [tableName, tableConfig] of Object.entries(requires)) {
|
|
105
|
+
const lookback = tableConfig.lookback || 0;
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
const { byEntity } = this.generate(tableName, {
|
|
109
|
+
entityCount,
|
|
110
|
+
entityIds,
|
|
111
|
+
daysBack: lookback,
|
|
112
|
+
asOfDate: options.asOfDate
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// DataFetcher returns data keyed by entityId (for per-entity fetches)
|
|
116
|
+
// or as flat array (for global fetches)
|
|
117
|
+
data[tableName] = Object.fromEntries(byEntity);
|
|
118
|
+
} catch (e) {
|
|
119
|
+
// Table not in templates, return empty
|
|
120
|
+
data[tableName] = {};
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return data;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Generate computation results for dependency simulation.
|
|
129
|
+
*
|
|
130
|
+
* @param {string} computationName
|
|
131
|
+
* @param {Object} results - Map of entityId -> result
|
|
132
|
+
* @param {Date|string} date
|
|
133
|
+
* @returns {Object[]} Rows in computation_results format
|
|
134
|
+
*/
|
|
135
|
+
generateComputationResults(computationName, results, date) {
|
|
136
|
+
const rows = [];
|
|
137
|
+
const dateStr = (date instanceof Date ? date : new Date(date)).toISOString().split('T')[0];
|
|
138
|
+
|
|
139
|
+
for (const [entityId, result] of Object.entries(results)) {
|
|
140
|
+
rows.push({
|
|
141
|
+
date: dateStr,
|
|
142
|
+
computation_name: computationName,
|
|
143
|
+
category: 'mock',
|
|
144
|
+
entity_id: entityId,
|
|
145
|
+
result: result,
|
|
146
|
+
hash: randomHex(16),
|
|
147
|
+
created_at: new Date().toISOString()
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return rows;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// =========================================================================
|
|
155
|
+
// PRIVATE METHODS
|
|
156
|
+
// =========================================================================
|
|
157
|
+
|
|
158
|
+
_generateRow(tableName, template, entityId, date, meta) {
|
|
159
|
+
const row = {};
|
|
160
|
+
|
|
161
|
+
for (const col of template.columns) {
|
|
162
|
+
const colName = col.name;
|
|
163
|
+
|
|
164
|
+
// Handle special fields
|
|
165
|
+
if (colName === meta.entityField) {
|
|
166
|
+
row[colName] = entityId;
|
|
167
|
+
} else if (colName === meta.dateField && date) {
|
|
168
|
+
row[colName] = date.toISOString().split('T')[0];
|
|
169
|
+
} else if (colName === 'fetched_at' || colName === 'last_updated') {
|
|
170
|
+
row[colName] = new Date().toISOString();
|
|
171
|
+
} else if (col.type === 'JSON') {
|
|
172
|
+
// Use JSON template
|
|
173
|
+
row[colName] = this._generateJsonField(tableName, colName, entityId, template);
|
|
174
|
+
} else if (template.generators && template.generators[colName]) {
|
|
175
|
+
// Use custom generator
|
|
176
|
+
row[colName] = template.generators[colName](entityId);
|
|
177
|
+
} else {
|
|
178
|
+
// Default by type
|
|
179
|
+
row[colName] = this._generateByType(col.type);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return row;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
_generateJsonField(tableName, colName, entityId, template) {
|
|
187
|
+
const jsonTemplate = template.jsonTemplates?.[colName];
|
|
188
|
+
if (!jsonTemplate) return {};
|
|
189
|
+
|
|
190
|
+
const result = {};
|
|
191
|
+
for (const [key, generator] of Object.entries(jsonTemplate)) {
|
|
192
|
+
if (typeof generator === 'function') {
|
|
193
|
+
result[key] = generator(entityId);
|
|
194
|
+
} else {
|
|
195
|
+
result[key] = generator;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return result;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
_generateByType(type) {
|
|
203
|
+
switch (type) {
|
|
204
|
+
case 'STRING': return randomHex(8);
|
|
205
|
+
case 'INTEGER': return randomInt(1, 1000);
|
|
206
|
+
case 'FLOAT': return randomFloat(0, 100);
|
|
207
|
+
case 'BOOLEAN': return randomChoice([true, false]);
|
|
208
|
+
case 'DATE': return new Date().toISOString().split('T')[0];
|
|
209
|
+
case 'TIMESTAMP': return new Date().toISOString();
|
|
210
|
+
case 'JSON': return {};
|
|
211
|
+
default: return null;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
_generateEntityIds(tableName, count) {
|
|
216
|
+
// Generate realistic entity IDs based on table type
|
|
217
|
+
const ids = [];
|
|
218
|
+
for (let i = 0; i < count; i++) {
|
|
219
|
+
ids.push(String(randomInt(10000000, 50000000)));
|
|
220
|
+
}
|
|
221
|
+
return ids;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
_inferMetadata(tableName, template) {
|
|
225
|
+
// Infer metadata from column names
|
|
226
|
+
const columns = template.columns.map(c => c.name);
|
|
227
|
+
|
|
228
|
+
return {
|
|
229
|
+
dateField: columns.includes('date') ? 'date' : null,
|
|
230
|
+
entityField: columns.find(c => ['user_id', 'pi_id', 'cid', 'entity_id', 'instrument_id'].includes(c)) || null
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
module.exports = { MockDataFactory };
|