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,562 @@
1
+ /**
2
+ * @fileoverview Social & Engagement Business Rules
3
+ *
4
+ * For extracting and analyzing social data:
5
+ * - social_post_snapshots (posts, engagement)
6
+ * - pi_ratings (reviews, ratings)
7
+ * - pi_page_views (profile views)
8
+ *
9
+ * Usage in computation:
10
+ * ```javascript
11
+ * const posts = rules.social.extractPosts(row);
12
+ * const engagement = rules.social.calculateEngagement(posts);
13
+ * const rating = rules.social.getAverageRating(ratingsRow);
14
+ * ```
15
+ */
16
+
17
+ // =============================================================================
18
+ // POSTS - social_post_snapshots
19
+ // =============================================================================
20
+
21
+ /**
22
+ * Extract posts from a social_post_snapshots row.
23
+ * @param {Object} row - social_post_snapshots row
24
+ * @returns {Array} Array of post objects
25
+ */
26
+ function extractPosts(row) {
27
+ if (!row) return [];
28
+
29
+ let data = row.posts_data;
30
+
31
+ if (typeof data === 'string') {
32
+ try {
33
+ data = JSON.parse(data);
34
+ } catch (e) {
35
+ return [];
36
+ }
37
+ }
38
+
39
+ // Posts are in a map keyed by post ID
40
+ const postsMap = data?.posts;
41
+ if (!postsMap || typeof postsMap !== 'object') return [];
42
+
43
+ return Object.values(postsMap);
44
+ }
45
+
46
+ /**
47
+ * Get user ID from posts row.
48
+ * @param {Object} row - social_post_snapshots row
49
+ * @returns {string|null}
50
+ */
51
+ function getPostsUserId(row) {
52
+ if (!row) return null;
53
+
54
+ if (row.user_id) return String(row.user_id);
55
+
56
+ let data = row.posts_data;
57
+ if (typeof data === 'string') {
58
+ try {
59
+ data = JSON.parse(data);
60
+ } catch (e) {
61
+ return null;
62
+ }
63
+ }
64
+
65
+ return data?.cid ? String(data.cid) : null;
66
+ }
67
+
68
+ /**
69
+ * Get post count from row.
70
+ * @param {Object} row - social_post_snapshots row
71
+ * @returns {number}
72
+ */
73
+ function getPostCount(row) {
74
+ if (!row) return 0;
75
+
76
+ let data = row.posts_data;
77
+ if (typeof data === 'string') {
78
+ try {
79
+ data = JSON.parse(data);
80
+ } catch (e) {
81
+ return 0;
82
+ }
83
+ }
84
+
85
+ return data?.postCount ?? 0;
86
+ }
87
+
88
+ /**
89
+ * Get post text.
90
+ * @param {Object} post - Post object
91
+ * @returns {string}
92
+ */
93
+ function getPostText(post) {
94
+ return post?.text || '';
95
+ }
96
+
97
+ /**
98
+ * Get post creation date.
99
+ * @param {Object} post - Post object
100
+ * @returns {Date|null}
101
+ */
102
+ function getPostDate(post) {
103
+ return post?.createdAt ? new Date(post.createdAt) : null;
104
+ }
105
+
106
+ /**
107
+ * Get post likes count.
108
+ * @param {Object} post - Post object
109
+ * @returns {number}
110
+ */
111
+ function getPostLikes(post) {
112
+ return post?.stats?.likes ?? 0;
113
+ }
114
+
115
+ /**
116
+ * Get post comments count.
117
+ * @param {Object} post - Post object
118
+ * @returns {number}
119
+ */
120
+ function getPostComments(post) {
121
+ return post?.stats?.comments ?? 0;
122
+ }
123
+
124
+ /**
125
+ * Get total engagement (likes + comments).
126
+ * @param {Object} post - Post object
127
+ * @returns {number}
128
+ */
129
+ function getPostEngagement(post) {
130
+ return getPostLikes(post) + getPostComments(post);
131
+ }
132
+
133
+ /**
134
+ * Get post ID.
135
+ * @param {Object} post - Post object
136
+ * @returns {string|null}
137
+ */
138
+ function getPostId(post) {
139
+ return post?.id || null;
140
+ }
141
+
142
+ /**
143
+ * Extract mentioned tickers from post text.
144
+ * Looks for $TICKER patterns.
145
+ * @param {Object} post - Post object
146
+ * @returns {string[]}
147
+ */
148
+ function extractMentionedTickers(post) {
149
+ const text = getPostText(post);
150
+ const matches = text.match(/\$([A-Z0-9.]+)/g) || [];
151
+ return [...new Set(matches.map(m => m.substring(1)))];
152
+ }
153
+
154
+ /**
155
+ * Calculate total engagement for all posts.
156
+ * @param {Array} posts - Array of posts
157
+ * @returns {Object}
158
+ */
159
+ function calculateEngagement(posts) {
160
+ if (!posts || posts.length === 0) {
161
+ return {
162
+ totalPosts: 0,
163
+ totalLikes: 0,
164
+ totalComments: 0,
165
+ totalEngagement: 0,
166
+ avgLikesPerPost: 0,
167
+ avgCommentsPerPost: 0,
168
+ avgEngagementPerPost: 0
169
+ };
170
+ }
171
+
172
+ const totalLikes = posts.reduce((sum, p) => sum + getPostLikes(p), 0);
173
+ const totalComments = posts.reduce((sum, p) => sum + getPostComments(p), 0);
174
+
175
+ return {
176
+ totalPosts: posts.length,
177
+ totalLikes,
178
+ totalComments,
179
+ totalEngagement: totalLikes + totalComments,
180
+ avgLikesPerPost: totalLikes / posts.length,
181
+ avgCommentsPerPost: totalComments / posts.length,
182
+ avgEngagementPerPost: (totalLikes + totalComments) / posts.length
183
+ };
184
+ }
185
+
186
+ /**
187
+ * Get most engaged posts.
188
+ * @param {Array} posts - Array of posts
189
+ * @param {number} count - Number to return
190
+ * @returns {Array}
191
+ */
192
+ function getMostEngagedPosts(posts, count = 5) {
193
+ return [...posts]
194
+ .sort((a, b) => getPostEngagement(b) - getPostEngagement(a))
195
+ .slice(0, count);
196
+ }
197
+
198
+ /**
199
+ * Get recent posts.
200
+ * @param {Array} posts - Array of posts
201
+ * @param {number} count - Number to return
202
+ * @returns {Array}
203
+ */
204
+ function getRecentPosts(posts, count = 5) {
205
+ return [...posts]
206
+ .sort((a, b) => {
207
+ const dateA = getPostDate(a);
208
+ const dateB = getPostDate(b);
209
+ return (dateB || 0) - (dateA || 0);
210
+ })
211
+ .slice(0, count);
212
+ }
213
+
214
+ /**
215
+ * Calculate posting frequency (posts per week).
216
+ * @param {Array} posts - Array of posts
217
+ * @returns {number}
218
+ */
219
+ function calculatePostFrequency(posts) {
220
+ if (!posts || posts.length < 2) return 0;
221
+
222
+ const dates = posts
223
+ .map(getPostDate)
224
+ .filter(d => d)
225
+ .sort((a, b) => a - b);
226
+
227
+ if (dates.length < 2) return 0;
228
+
229
+ const daySpan = (dates[dates.length - 1] - dates[0]) / (1000 * 60 * 60 * 24);
230
+ if (daySpan === 0) return posts.length; // All same day
231
+
232
+ return (posts.length / daySpan) * 7;
233
+ }
234
+
235
+ // =============================================================================
236
+ // RATINGS - pi_ratings
237
+ // =============================================================================
238
+
239
+ /**
240
+ * Get average rating from pi_ratings row.
241
+ * @param {Object} row - pi_ratings row
242
+ * @returns {number}
243
+ */
244
+ function getAverageRating(row) {
245
+ return row?.average_rating ?? 0;
246
+ }
247
+
248
+ /**
249
+ * Get total ratings count.
250
+ * @param {Object} row - pi_ratings row
251
+ * @returns {number}
252
+ */
253
+ function getTotalRatings(row) {
254
+ return row?.total_ratings ?? 0;
255
+ }
256
+
257
+ /**
258
+ * Extract reviews from pi_ratings row.
259
+ * @param {Object} row - pi_ratings row
260
+ * @returns {Array}
261
+ */
262
+ function extractReviews(row) {
263
+ if (!row) return [];
264
+
265
+ let reviews = row.reviews;
266
+
267
+ if (typeof reviews === 'string') {
268
+ try {
269
+ reviews = JSON.parse(reviews);
270
+ } catch (e) {
271
+ return [];
272
+ }
273
+ }
274
+
275
+ return Array.isArray(reviews) ? reviews : [];
276
+ }
277
+
278
+ /**
279
+ * Get ratings by user map.
280
+ * @param {Object} row - pi_ratings row
281
+ * @returns {Object} Map of userId -> rating
282
+ */
283
+ function getRatingsByUser(row) {
284
+ if (!row) return {};
285
+
286
+ let ratings = row.ratings_by_user;
287
+
288
+ if (typeof ratings === 'string') {
289
+ try {
290
+ ratings = JSON.parse(ratings);
291
+ } catch (e) {
292
+ return {};
293
+ }
294
+ }
295
+
296
+ return ratings || {};
297
+ }
298
+
299
+ /**
300
+ * Get review comment.
301
+ * @param {Object} review - Review object
302
+ * @returns {string}
303
+ */
304
+ function getReviewComment(review) {
305
+ return review?.comment || '';
306
+ }
307
+
308
+ /**
309
+ * Get review rating (1-5).
310
+ * @param {Object} review - Review object
311
+ * @returns {number}
312
+ */
313
+ function getReviewRating(review) {
314
+ return review?.rating ?? 0;
315
+ }
316
+
317
+ /**
318
+ * Get review date.
319
+ * @param {Object} review - Review object
320
+ * @returns {Date|null}
321
+ */
322
+ function getReviewDate(review) {
323
+ return review?.createdAt ? new Date(review.createdAt) : null;
324
+ }
325
+
326
+ /**
327
+ * Get reviewer username.
328
+ * @param {Object} review - Review object
329
+ * @returns {string|null}
330
+ */
331
+ function getReviewerUsername(review) {
332
+ return review?.reviewerUsername || null;
333
+ }
334
+
335
+ /**
336
+ * Check if review is anonymous.
337
+ * @param {Object} review - Review object
338
+ * @returns {boolean}
339
+ */
340
+ function isReviewAnonymous(review) {
341
+ return review?.isAnonymous === true;
342
+ }
343
+
344
+ /**
345
+ * Calculate rating distribution.
346
+ * @param {Array} reviews - Array of reviews
347
+ * @returns {Object} Map of rating -> count
348
+ */
349
+ function calculateRatingDistribution(reviews) {
350
+ const distribution = { 1: 0, 2: 0, 3: 0, 4: 0, 5: 0 };
351
+
352
+ for (const review of reviews) {
353
+ const rating = getReviewRating(review);
354
+ if (rating >= 1 && rating <= 5) {
355
+ distribution[rating]++;
356
+ }
357
+ }
358
+
359
+ return distribution;
360
+ }
361
+
362
+ /**
363
+ * Get recent reviews.
364
+ * @param {Array} reviews - Array of reviews
365
+ * @param {number} count - Number to return
366
+ * @returns {Array}
367
+ */
368
+ function getRecentReviews(reviews, count = 5) {
369
+ return [...reviews]
370
+ .sort((a, b) => {
371
+ const dateA = getReviewDate(a);
372
+ const dateB = getReviewDate(b);
373
+ return (dateB || 0) - (dateA || 0);
374
+ })
375
+ .slice(0, count);
376
+ }
377
+
378
+ // =============================================================================
379
+ // PAGE VIEWS - pi_page_views
380
+ // =============================================================================
381
+
382
+ /**
383
+ * Get total views from pi_page_views row.
384
+ * @param {Object} row - pi_page_views row
385
+ * @returns {number}
386
+ */
387
+ function getTotalViews(row) {
388
+ return row?.total_views ?? 0;
389
+ }
390
+
391
+ /**
392
+ * Get unique viewers count.
393
+ * @param {Object} row - pi_page_views row
394
+ * @returns {number}
395
+ */
396
+ function getUniqueViewers(row) {
397
+ return row?.unique_viewers ?? 0;
398
+ }
399
+
400
+ /**
401
+ * Extract views by user map.
402
+ * @param {Object} row - pi_page_views row
403
+ * @returns {Object} Map of userId -> viewData
404
+ */
405
+ function extractViewsByUser(row) {
406
+ if (!row) return {};
407
+
408
+ let views = row.views_by_user;
409
+
410
+ if (typeof views === 'string') {
411
+ try {
412
+ views = JSON.parse(views);
413
+ } catch (e) {
414
+ return {};
415
+ }
416
+ }
417
+
418
+ return views || {};
419
+ }
420
+
421
+ /**
422
+ * Get view count for a specific user.
423
+ * @param {Object} viewsByUser - views_by_user map
424
+ * @param {string} userId - User ID
425
+ * @returns {number}
426
+ */
427
+ function getViewCountForUser(viewsByUser, userId) {
428
+ return viewsByUser?.[userId]?.viewCount ?? 0;
429
+ }
430
+
431
+ /**
432
+ * Get last viewed timestamp for a specific user.
433
+ * @param {Object} viewsByUser - views_by_user map
434
+ * @param {string} userId - User ID
435
+ * @returns {Date|null}
436
+ */
437
+ function getLastViewedForUser(viewsByUser, userId) {
438
+ const lastViewed = viewsByUser?.[userId]?.lastViewed;
439
+ if (!lastViewed) return null;
440
+
441
+ // Handle Firestore timestamp format
442
+ if (lastViewed._seconds) {
443
+ return new Date(lastViewed._seconds * 1000 + (lastViewed._nanoseconds || 0) / 1000000);
444
+ }
445
+
446
+ return new Date(lastViewed);
447
+ }
448
+
449
+ /**
450
+ * Calculate view metrics.
451
+ * @param {Object} row - pi_page_views row
452
+ * @returns {Object}
453
+ */
454
+ function calculateViewMetrics(row) {
455
+ const total = getTotalViews(row);
456
+ const unique = getUniqueViewers(row);
457
+ const viewsByUser = extractViewsByUser(row);
458
+
459
+ const userIds = Object.keys(viewsByUser);
460
+ const viewCounts = userIds.map(id => getViewCountForUser(viewsByUser, id));
461
+
462
+ return {
463
+ totalViews: total,
464
+ uniqueViewers: unique,
465
+ avgViewsPerViewer: unique > 0 ? total / unique : 0,
466
+ maxViewsByUser: viewCounts.length > 0 ? Math.max(...viewCounts) : 0,
467
+ repeatViewers: viewCounts.filter(c => c > 1).length
468
+ };
469
+ }
470
+
471
+ // =============================================================================
472
+ // COMBINED SOCIAL SCORE
473
+ // =============================================================================
474
+
475
+ /**
476
+ * Calculate a social engagement score (0-100).
477
+ * @param {Object} options - Data sources
478
+ * @param {Object} [options.postsRow] - social_post_snapshots row
479
+ * @param {Object} [options.ratingsRow] - pi_ratings row
480
+ * @param {Object} [options.viewsRow] - pi_page_views row
481
+ * @returns {number}
482
+ */
483
+ function calculateSocialScore(options = {}) {
484
+ let score = 50; // Base score
485
+
486
+ // Posts engagement (+/- 15)
487
+ if (options.postsRow) {
488
+ const posts = extractPosts(options.postsRow);
489
+ const engagement = calculateEngagement(posts);
490
+
491
+ if (engagement.avgEngagementPerPost >= 5) score += 15;
492
+ else if (engagement.avgEngagementPerPost >= 2) score += 7;
493
+ else if (engagement.totalPosts === 0) score -= 10;
494
+ }
495
+
496
+ // Ratings (+/- 20)
497
+ if (options.ratingsRow) {
498
+ const avgRating = getAverageRating(options.ratingsRow);
499
+ const totalRatings = getTotalRatings(options.ratingsRow);
500
+
501
+ if (avgRating >= 4.5 && totalRatings >= 5) score += 20;
502
+ else if (avgRating >= 4.0 && totalRatings >= 3) score += 10;
503
+ else if (avgRating < 3.0 && totalRatings >= 3) score -= 15;
504
+ else if (totalRatings === 0) score -= 5;
505
+ }
506
+
507
+ // Page views (+/- 15)
508
+ if (options.viewsRow) {
509
+ const metrics = calculateViewMetrics(options.viewsRow);
510
+
511
+ if (metrics.uniqueViewers >= 50) score += 15;
512
+ else if (metrics.uniqueViewers >= 20) score += 7;
513
+ else if (metrics.uniqueViewers < 5) score -= 5;
514
+
515
+ // Repeat viewers bonus
516
+ if (metrics.repeatViewers >= 10) score += 5;
517
+ }
518
+
519
+ return Math.max(0, Math.min(100, score));
520
+ }
521
+
522
+ module.exports = {
523
+ // Posts
524
+ extractPosts,
525
+ getPostsUserId,
526
+ getPostCount,
527
+ getPostText,
528
+ getPostDate,
529
+ getPostLikes,
530
+ getPostComments,
531
+ getPostEngagement,
532
+ getPostId,
533
+ extractMentionedTickers,
534
+ calculateEngagement,
535
+ getMostEngagedPosts,
536
+ getRecentPosts,
537
+ calculatePostFrequency,
538
+
539
+ // Ratings
540
+ getAverageRating,
541
+ getTotalRatings,
542
+ extractReviews,
543
+ getRatingsByUser,
544
+ getReviewComment,
545
+ getReviewRating,
546
+ getReviewDate,
547
+ getReviewerUsername,
548
+ isReviewAnonymous,
549
+ calculateRatingDistribution,
550
+ getRecentReviews,
551
+
552
+ // Page views
553
+ getTotalViews,
554
+ getUniqueViewers,
555
+ extractViewsByUser,
556
+ getViewCountForUser,
557
+ getLastViewedForUser,
558
+ calculateViewMetrics,
559
+
560
+ // Combined
561
+ calculateSocialScore
562
+ };