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,327 @@
1
+ /**
2
+ * @fileoverview Unified Computation Scheduler
3
+ *
4
+ * Single Cloud Function triggered every minute by Cloud Scheduler.
5
+ * Checks all computations, dispatches those that are due to Cloud Tasks.
6
+ *
7
+ * Architecture:
8
+ *
9
+ * Cloud Scheduler (every minute, * * * * *)
10
+ * │
11
+ * ▼
12
+ * ┌─────────────────────────────────────────────┐
13
+ * │ Scheduler Cloud Function (this file) │
14
+ * │ 1. Floor current time to minute boundary │
15
+ * │ 2. Check each computation's schedule │
16
+ * │ 3. Enqueue due computations to Cloud Tasks │
17
+ * └─────────────────────────────────────────────┘
18
+ * │
19
+ * ▼ (via Cloud Tasks queue)
20
+ * ┌─────────────────────────────────────────────┐
21
+ * │ Dispatcher Cloud Function │
22
+ * │ - Validates dependencies │
23
+ * │ - Executes computation │
24
+ * │ - Returns 503 if blocked (Cloud Tasks │
25
+ * │ will retry with backoff) │
26
+ * └─────────────────────────────────────────────┘
27
+ *
28
+ * Clock Drift Handling:
29
+ * - Scheduler might run at 14:00:58 instead of 14:00:00
30
+ * - We floor to minute boundary: 14:00:58 → 14:00
31
+ * - Schedule check uses 14:00, payload uses 14:00
32
+ * - System behaves as if it ran exactly on time
33
+ *
34
+ * Rate Limiting:
35
+ * - Uses p-limit to control concurrent Cloud Tasks API calls
36
+ * - Prevents hitting GCP API quotas
37
+ */
38
+
39
+ const { CloudTasksClient } = require('@google-cloud/tasks');
40
+ const pLimit = require('p-limit');
41
+ const { ManifestBuilder, ScheduleValidator } = require('../framework');
42
+ const config = require('../config/bulltrackers.config');
43
+
44
+ // Concurrency limit for Cloud Tasks API calls
45
+ const CLOUD_TASKS_CONCURRENCY = 10;
46
+
47
+ // Singleton instances
48
+ let manifest = null;
49
+ let scheduleValidator = null;
50
+ let tasksClient = null;
51
+
52
+ /**
53
+ * Initialize manifest and schedule validator.
54
+ */
55
+ async function initialize() {
56
+ if (manifest) return;
57
+
58
+ console.log('[Scheduler] Initializing...');
59
+
60
+ const builder = new ManifestBuilder(config, { log: (l, m) => console.log(`[${l}] ${m}`) });
61
+ manifest = builder.build(config.computations || []);
62
+ scheduleValidator = builder.getScheduleValidator();
63
+ tasksClient = new CloudTasksClient();
64
+
65
+ console.log(`[Scheduler] Initialized with ${manifest.length} computations`);
66
+ }
67
+
68
+ /**
69
+ * Main scheduler handler.
70
+ * Triggered by Cloud Scheduler every minute.
71
+ *
72
+ * @param {Object} req - HTTP request
73
+ * @param {Object} res - HTTP response
74
+ */
75
+ async function schedulerHandler(req, res) {
76
+ const startTime = Date.now();
77
+
78
+ try {
79
+ await initialize();
80
+
81
+ // Get current time, floored to minute boundary
82
+ // This handles clock drift - if we run at 14:00:58, we treat it as 14:00:00
83
+ const now = floorToMinute(new Date());
84
+ const targetDate = formatDate(now);
85
+ const currentTime = formatTime(now);
86
+
87
+ console.log(`[Scheduler] Running for ${targetDate} ${currentTime}`);
88
+
89
+ // Find computations due at this time
90
+ const dueComputations = findDueComputations(now);
91
+
92
+ if (dueComputations.length === 0) {
93
+ console.log(`[Scheduler] No computations due at ${currentTime}`);
94
+ return res.status(200).json({
95
+ status: 'ok',
96
+ time: currentTime,
97
+ dispatched: 0,
98
+ message: 'No computations due'
99
+ });
100
+ }
101
+
102
+ console.log(`[Scheduler] ${dueComputations.length} computations due: ${dueComputations.map(c => c.name).join(', ')}`);
103
+
104
+ // Dispatch to Cloud Tasks with rate limiting
105
+ // Pass 'now' for idempotent task naming (retry-safe)
106
+ const results = await dispatchComputations(dueComputations, targetDate, now);
107
+
108
+ const duration = Date.now() - startTime;
109
+ const succeeded = results.filter(r => r.status === 'dispatched').length;
110
+ const failed = results.filter(r => r.status === 'error').length;
111
+
112
+ console.log(`[Scheduler] Dispatched ${succeeded}/${dueComputations.length} in ${duration}ms`);
113
+
114
+ return res.status(200).json({
115
+ status: 'ok',
116
+ time: currentTime,
117
+ date: targetDate,
118
+ dispatched: succeeded,
119
+ failed,
120
+ duration,
121
+ results
122
+ });
123
+
124
+ } catch (error) {
125
+ console.error('[Scheduler] Error:', error);
126
+ return res.status(500).json({
127
+ status: 'error',
128
+ message: error.message
129
+ });
130
+ }
131
+ }
132
+
133
+ /**
134
+ * Find all computations that are due at the given time.
135
+ *
136
+ * @param {Date} now - Current time (floored to minute)
137
+ * @returns {Array} Array of manifest entries that are due
138
+ */
139
+ function findDueComputations(now) {
140
+ const due = [];
141
+ const currentHour = now.getUTCHours();
142
+ const currentMinute = now.getUTCMinutes();
143
+ const currentTime = `${String(currentHour).padStart(2, '0')}:${String(currentMinute).padStart(2, '0')}`;
144
+
145
+ const dayOfWeek = now.getUTCDay(); // 0 = Sunday
146
+ const dayOfMonth = now.getUTCDate(); // 1-31
147
+
148
+ for (const entry of manifest) {
149
+ const schedule = entry.schedule;
150
+
151
+ // Check if this computation is due now
152
+ if (isScheduleDue(schedule, currentTime, dayOfWeek, dayOfMonth)) {
153
+ due.push(entry);
154
+ }
155
+ }
156
+
157
+ return due;
158
+ }
159
+
160
+ /**
161
+ * Check if a schedule is due at the given time.
162
+ *
163
+ * @param {Object} schedule - Schedule object
164
+ * @param {string} currentTime - Current time in HH:MM format
165
+ * @param {number} dayOfWeek - Day of week (0-6, Sunday=0)
166
+ * @param {number} dayOfMonth - Day of month (1-31)
167
+ * @returns {boolean}
168
+ */
169
+ function isScheduleDue(schedule, currentTime, dayOfWeek, dayOfMonth) {
170
+ const scheduleTime = schedule.time || '02:00';
171
+ const [scheduleHour, scheduleMinute] = scheduleTime.split(':').map(Number);
172
+ const [currentHour, currentMinuteNum] = currentTime.split(':').map(Number);
173
+
174
+ // Check frequency-specific conditions
175
+ switch (schedule.frequency) {
176
+ case 'hourly':
177
+ // Hourly runs every hour at the specified minute
178
+ // e.g., time: '00:30' means run at XX:30
179
+ // Only check the minute portion matches
180
+ return scheduleMinute === currentMinuteNum;
181
+
182
+ case 'daily':
183
+ // Daily runs at exact time (hour:minute must match)
184
+ return scheduleTime === currentTime;
185
+
186
+ case 'weekly':
187
+ // Weekly runs at exact time on specified day
188
+ if (scheduleTime !== currentTime) return false;
189
+ const targetDay = schedule.dayOfWeek ?? 0; // Default Sunday
190
+ return dayOfWeek === targetDay;
191
+
192
+ case 'monthly':
193
+ // Monthly runs at exact time on specified day of month
194
+ if (scheduleTime !== currentTime) return false;
195
+ const targetDayOfMonth = schedule.dayOfMonth ?? 1; // Default 1st
196
+ return dayOfMonth === targetDayOfMonth;
197
+
198
+ default:
199
+ // Unknown frequency, default to daily behavior
200
+ return scheduleTime === currentTime;
201
+ }
202
+ }
203
+
204
+ /**
205
+ * Dispatch computations to Cloud Tasks queue.
206
+ * Uses p-limit for rate limiting.
207
+ *
208
+ * @param {Array} computations - Array of manifest entries
209
+ * @param {string} targetDate - Target date (YYYY-MM-DD)
210
+ * @param {Date} scheduledTime - The floored time this scheduler run represents (for idempotent task names)
211
+ * @returns {Promise<Array>} Results for each dispatch
212
+ */
213
+ async function dispatchComputations(computations, targetDate, scheduledTime) {
214
+ const limit = pLimit(CLOUD_TASKS_CONCURRENCY);
215
+
216
+ const { projectId, location, queueName, dispatcherUrl } = config.cloudTasks;
217
+ const queuePath = tasksClient.queuePath(projectId, location, queueName);
218
+
219
+ // Use the floored scheduledTime for idempotent task naming
220
+ // This ensures retries or slow loops don't create duplicate tasks
221
+ const timeSlot = formatTimeCompact(scheduledTime);
222
+
223
+ const tasks = computations.map(entry => limit(async () => {
224
+ try {
225
+ const taskPayload = {
226
+ computationName: entry.originalName,
227
+ targetDate,
228
+ source: 'scheduled',
229
+ scheduledAt: scheduledTime.toISOString()
230
+ };
231
+
232
+ const task = {
233
+ httpRequest: {
234
+ httpMethod: 'POST',
235
+ url: dispatcherUrl,
236
+ headers: {
237
+ 'Content-Type': 'application/json'
238
+ },
239
+ body: Buffer.from(JSON.stringify(taskPayload)).toString('base64'),
240
+ // OIDC token for authenticated Cloud Function invocation
241
+ // The Dispatcher should be deployed with "Require authentication"
242
+ oidcToken: {
243
+ serviceAccountEmail: config.cloudTasks.serviceAccountEmail,
244
+ audience: dispatcherUrl
245
+ }
246
+ },
247
+ // Task name uses the floored time slot - idempotent across retries
248
+ // If scheduler runs twice for the same minute, Cloud Tasks deduplicates
249
+ name: `${queuePath}/tasks/${entry.name}-${targetDate}-${timeSlot}`
250
+ };
251
+
252
+ await tasksClient.createTask({ parent: queuePath, task });
253
+
254
+ return {
255
+ computation: entry.originalName,
256
+ status: 'dispatched',
257
+ targetDate
258
+ };
259
+
260
+ } catch (error) {
261
+ // Handle "already exists" gracefully (duplicate prevention)
262
+ if (error.code === 6) { // ALREADY_EXISTS
263
+ return {
264
+ computation: entry.originalName,
265
+ status: 'skipped',
266
+ reason: 'Task already exists (duplicate prevention)'
267
+ };
268
+ }
269
+
270
+ console.error(`[Scheduler] Failed to dispatch ${entry.originalName}:`, error.message);
271
+ return {
272
+ computation: entry.originalName,
273
+ status: 'error',
274
+ error: error.message
275
+ };
276
+ }
277
+ }));
278
+
279
+ return Promise.all(tasks);
280
+ }
281
+
282
+ /**
283
+ * Floor a date to the nearest minute boundary.
284
+ * 14:00:58 → 14:00:00
285
+ */
286
+ function floorToMinute(date) {
287
+ const floored = new Date(date);
288
+ floored.setUTCSeconds(0);
289
+ floored.setUTCMilliseconds(0);
290
+ return floored;
291
+ }
292
+
293
+ /**
294
+ * Format date as YYYY-MM-DD.
295
+ */
296
+ function formatDate(date) {
297
+ return date.toISOString().split('T')[0];
298
+ }
299
+
300
+ /**
301
+ * Format time as HH:MM.
302
+ */
303
+ function formatTime(date) {
304
+ const hours = String(date.getUTCHours()).padStart(2, '0');
305
+ const minutes = String(date.getUTCMinutes()).padStart(2, '0');
306
+ return `${hours}:${minutes}`;
307
+ }
308
+
309
+ /**
310
+ * Format time as HHMM (compact, for task names).
311
+ */
312
+ function formatTimeCompact(date) {
313
+ const hours = String(date.getUTCHours()).padStart(2, '0');
314
+ const minutes = String(date.getUTCMinutes()).padStart(2, '0');
315
+ return `${hours}${minutes}`;
316
+ }
317
+
318
+ // Export for Cloud Functions
319
+ module.exports = {
320
+ schedulerHandler,
321
+ initialize,
322
+
323
+ // For testing
324
+ _findDueComputations: findDueComputations,
325
+ _isScheduleDue: isScheduleDue,
326
+ _floorToMinute: floorToMinute
327
+ };
@@ -0,0 +1,163 @@
1
+ /**
2
+ * @fileoverview Computation System v2 Entry Point
3
+ * * Usage:
4
+ * const { execute, analyze, runComputation } = require('./computation-system-v2');
5
+ * * // Analyze what can run
6
+ * const report = await analyze({ date: '2026-01-24' });
7
+ * * // Execute computations (Monolith/Scheduler mode)
8
+ * const result = await execute({ date: '2026-01-24' });
9
+ * * // Run single computation (Worker mode)
10
+ * const task = await runComputation({
11
+ * date: '2026-01-24',
12
+ * computation: 'UserPortfolioSummary'
13
+ * });
14
+ */
15
+
16
+ const { Orchestrator } = require('./framework');
17
+ const config = require('./config/bulltrackers.config');
18
+
19
+ // Add computations to config
20
+ config.computations = [
21
+ require('./computations/UserPortfolioSummary'),
22
+ require('./computations/PopularInvestorProfileMetrics'),
23
+ require('./computations/PopularInvestorRiskAssessment'),
24
+ // Add more computations here as they're migrated
25
+ ];
26
+
27
+ // Singleton orchestrator instance
28
+ let orchestrator = null;
29
+
30
+ /**
31
+ * Get or create the orchestrator instance.
32
+ * @param {Object} [customConfig] - Override default config
33
+ * @param {Object} [logger] - Custom logger
34
+ * @returns {Promise<Orchestrator>}
35
+ */
36
+ async function getOrchestrator(customConfig = null, logger = null) {
37
+ if (!orchestrator || customConfig) {
38
+ const cfg = customConfig || config;
39
+ orchestrator = new Orchestrator(cfg, logger);
40
+ // Note: Orchestrator initializes lazily on first execute/analyze call,
41
+ // but we can force it here if needed.
42
+ }
43
+ return orchestrator;
44
+ }
45
+
46
+ /**
47
+ * Analyze what can run for a given date.
48
+ * @param {Object} options
49
+ * @param {string} options.date - Target date (YYYY-MM-DD)
50
+ * @param {Object} [options.config] - Override config
51
+ * @param {Object} [options.logger] - Custom logger
52
+ * @returns {Promise<Object>} Analysis report
53
+ */
54
+ async function analyze(options) {
55
+ const { date, config: customConfig = null, logger = null } = options;
56
+ const orch = await getOrchestrator(customConfig, logger);
57
+ return orch.analyze({ date });
58
+ }
59
+
60
+ /**
61
+ * Execute computations for a given date.
62
+ * (Used by Scheduler or Monolithic runs)
63
+ */
64
+ async function execute(options) {
65
+ const {
66
+ date,
67
+ pass = null,
68
+ computation = null,
69
+ dryRun = false,
70
+ entities = null,
71
+ config: customConfig = null,
72
+ logger = null
73
+ } = options;
74
+
75
+ const orch = await getOrchestrator(customConfig, logger);
76
+ return orch.execute({ date, pass, computation, dryRun, entities });
77
+ }
78
+
79
+ /**
80
+ * WORKER ENTRY POINT: Run a single computation.
81
+ * (Used by Cloud Functions / Dispatcher)
82
+ */
83
+ async function runComputation(options) {
84
+ const {
85
+ date,
86
+ computation,
87
+ entityIds = null,
88
+ dryRun = false,
89
+ config: customConfig = null,
90
+ logger = null
91
+ } = options;
92
+
93
+ const orch = await getOrchestrator(customConfig, logger);
94
+
95
+ // 1. Ensure manifest is built
96
+ if (!orch.manifest) await orch.initialize();
97
+
98
+ // 2. Find the specific entry
99
+ // We normalize to handle case-insensitivity
100
+ const normalizedName = computation.toLowerCase().replace(/[^a-z0-9]/g, '');
101
+ const entry = orch.manifest.find(c => c.name === normalizedName);
102
+
103
+ if (!entry) {
104
+ throw new Error(`Computation not found: ${computation}`);
105
+ }
106
+
107
+ // 3. Delegate to Orchestrator's runSingle
108
+ // This handles dependencies, data fetching, middleware, etc.
109
+ return orch.runSingle(entry, date, {
110
+ entityIds,
111
+ dryRun
112
+ });
113
+ }
114
+
115
+ /**
116
+ * Get the manifest (list of all computations with metadata).
117
+ */
118
+ async function getManifest(options = {}) {
119
+ const { config: customConfig = null, logger = null } = options;
120
+ const orch = await getOrchestrator(customConfig, logger);
121
+ if (!orch.manifest) await orch.initialize();
122
+ return orch.manifest;
123
+ }
124
+
125
+ /**
126
+ * Warm the schema cache for all tables.
127
+ */
128
+ async function warmCache(options = {}) {
129
+ const { config: customConfig = null, logger = null } = options;
130
+ const orch = await getOrchestrator(customConfig, logger);
131
+ const allTables = Object.keys(orch.config.tables);
132
+ return orch.schemaRegistry.warmCache(allTables);
133
+ }
134
+
135
+ /**
136
+ * Reset the singleton (useful for testing).
137
+ */
138
+ function reset() {
139
+ orchestrator = null;
140
+ }
141
+
142
+ // Import handlers for Cloud Functions
143
+ const handlers = require('./handlers');
144
+
145
+ module.exports = {
146
+ // Main API
147
+ analyze,
148
+ execute,
149
+ runComputation, // <--- New Method
150
+ getManifest,
151
+ warmCache,
152
+ reset,
153
+
154
+ // For advanced usage
155
+ getOrchestrator,
156
+ config,
157
+
158
+ // Cloud Function handlers
159
+ ...handlers,
160
+
161
+ // Re-export framework components
162
+ ...require('./framework')
163
+ };
@@ -0,0 +1,49 @@
1
+ /**
2
+ * @fileoverview Business Rules Index
3
+ *
4
+ * Central registry of all business rules.
5
+ * These are automatically injected into computations.
6
+ *
7
+ * Available rule modules:
8
+ * - portfolio: Portfolio snapshots (positions, value, exposure)
9
+ * - metrics: Financial metrics (Sharpe, Sortino, VaR, etc.)
10
+ * - rankings: PI rankings data (AUM, copiers, risk scores)
11
+ * - trades: Trade history (closed positions, stats)
12
+ * - social: Social data (posts, ratings, page views)
13
+ * - instruments: Instrument data (prices, sentiment, tickers)
14
+ *
15
+ * Rules are available in computations as:
16
+ * rules.portfolio.extractPositions(...)
17
+ * rules.metrics.calculateSharpeRatio(...)
18
+ * rules.rankings.getRiskScore(...)
19
+ * rules.trades.calculateTradeStats(...)
20
+ * rules.social.calculateEngagement(...)
21
+ * rules.instruments.getPrice(...)
22
+ */
23
+
24
+ const portfolio = require('./portfolio');
25
+ const metrics = require('./metrics');
26
+ const rankings = require('./rankings');
27
+ const trades = require('./trades');
28
+ const social = require('./social');
29
+ const instruments = require('./instruments');
30
+
31
+ module.exports = {
32
+ // Portfolio snapshots - positions, value, exposure, direction
33
+ portfolio,
34
+
35
+ // Financial metrics - Sharpe, Sortino, drawdown, VaR
36
+ metrics,
37
+
38
+ // PI rankings - AUM, copiers, risk scores, trading style
39
+ rankings,
40
+
41
+ // Trade history - closed trades, win rate, P&L stats
42
+ trades,
43
+
44
+ // Social data - posts, ratings, page views, engagement
45
+ social,
46
+
47
+ // Instruments - prices, sentiment, tickers
48
+ instruments
49
+ };