bulltrackers-module 1.0.766 → 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.
Files changed (32) hide show
  1. package/functions/computation-system-v2/computations/BehavioralAnomaly.js +298 -186
  2. package/functions/computation-system-v2/computations/NewSectorExposure.js +82 -35
  3. package/functions/computation-system-v2/computations/NewSocialPost.js +52 -24
  4. package/functions/computation-system-v2/computations/PopularInvestorProfileMetrics.js +354 -641
  5. package/functions/computation-system-v2/config/bulltrackers.config.js +26 -14
  6. package/functions/computation-system-v2/framework/core/Manifest.js +9 -16
  7. package/functions/computation-system-v2/framework/core/RunAnalyzer.js +2 -1
  8. package/functions/computation-system-v2/framework/data/DataFetcher.js +142 -4
  9. package/functions/computation-system-v2/framework/execution/Orchestrator.js +18 -31
  10. package/functions/computation-system-v2/framework/storage/StorageManager.js +7 -17
  11. package/functions/computation-system-v2/framework/testing/ComputationTester.js +155 -66
  12. package/functions/computation-system-v2/scripts/test-computation-dag.js +109 -0
  13. package/functions/task-engine/helpers/data_storage_helpers.js +6 -6
  14. package/package.json +1 -1
  15. package/functions/computation-system-v2/computations/PopularInvestorRiskAssessment.js +0 -176
  16. package/functions/computation-system-v2/computations/PopularInvestorRiskMetrics.js +0 -294
  17. package/functions/computation-system-v2/computations/UserPortfolioSummary.js +0 -172
  18. package/functions/computation-system-v2/scripts/migrate-sectors.js +0 -73
  19. package/functions/computation-system-v2/test/analyze-results.js +0 -238
  20. package/functions/computation-system-v2/test/other/test-dependency-cascade.js +0 -150
  21. package/functions/computation-system-v2/test/other/test-dispatcher.js +0 -317
  22. package/functions/computation-system-v2/test/other/test-framework.js +0 -500
  23. package/functions/computation-system-v2/test/other/test-real-execution.js +0 -166
  24. package/functions/computation-system-v2/test/other/test-real-integration.js +0 -194
  25. package/functions/computation-system-v2/test/other/test-refactor-e2e.js +0 -131
  26. package/functions/computation-system-v2/test/other/test-results.json +0 -31
  27. package/functions/computation-system-v2/test/other/test-risk-metrics-computation.js +0 -329
  28. package/functions/computation-system-v2/test/other/test-scheduler.js +0 -204
  29. package/functions/computation-system-v2/test/other/test-storage.js +0 -449
  30. package/functions/computation-system-v2/test/run-pipeline-test.js +0 -554
  31. package/functions/computation-system-v2/test/test-full-pipeline.js +0 -227
  32. package/functions/computation-system-v2/test/test-worker-pool.js +0 -266
@@ -1,17 +1,15 @@
1
+ /**
2
+ * @fileoverview New Sector Exposure Alert
3
+ * Refactored to map InstrumentID -> Ticker -> Sector (Industry)
4
+ * UPDATE: Uses "Last Known" logic for previous state (Lookback 14 days)
5
+ * Confirmed working 29/01/2026
6
+ */
1
7
  const { Computation } = require('../framework');
2
8
 
3
9
  class NewSectorExposure extends Computation {
4
10
 
5
11
  constructor() {
6
12
  super();
7
- this.SECTOR_MAPPING = {
8
- 1: 'Currencies',
9
- 2: 'Commodities',
10
- 4: 'Indices',
11
- 5: 'Stocks',
12
- 6: 'ETF',
13
- 10: 'Cryptocurrencies'
14
- };
15
13
  }
16
14
 
17
15
  static getConfig() {
@@ -23,9 +21,15 @@ class NewSectorExposure extends Computation {
23
21
 
24
22
  requires: {
25
23
  'portfolio_snapshots': {
26
- lookback: 1, // Today + Yesterday
24
+ lookback: 14, // INCREASED: Look back 2 weeks to find last known state
27
25
  mandatory: true,
28
26
  fields: ['user_id', 'portfolio_data', 'date']
27
+ },
28
+ 'ticker_mappings': {
29
+ mandatory: false
30
+ },
31
+ 'sector_mappings': {
32
+ mandatory: false
29
33
  }
30
34
  },
31
35
 
@@ -38,12 +42,11 @@ class NewSectorExposure extends Computation {
38
42
  }
39
43
  },
40
44
 
41
- // LEGACY METADATA
42
45
  userType: 'POPULAR_INVESTOR',
43
46
  alert: {
44
47
  id: 'newSector',
45
48
  frontendName: 'New Sector Entry',
46
- description: 'Alert when a Popular Investor enters a new sector category',
49
+ description: 'Alert when a Popular Investor enters a new industry sector',
47
50
  messageTemplate: '{piUsername} entered new sector(s): {sectorName}',
48
51
  severity: 'low',
49
52
  configKey: 'newSector',
@@ -55,7 +58,7 @@ class NewSectorExposure extends Computation {
55
58
  type: 'multiselect',
56
59
  label: 'Watch specific sectors',
57
60
  description: 'Only alert for these sectors',
58
- options: ['Stocks', 'Cryptocurrencies', 'ETF', 'Indices', 'Commodities', 'Currencies'],
61
+ options: ['Technology', 'Basic Materials', 'Consumer Cyclical', 'Financial Services', 'Real Estate', 'Healthcare', 'Energy', 'Industrials'],
59
62
  default: []
60
63
  }
61
64
  ],
@@ -70,49 +73,90 @@ class NewSectorExposure extends Computation {
70
73
  async process(context) {
71
74
  const { data, entityId, date, rules } = context;
72
75
 
73
- // 1. Get Data
76
+ // 1. Get Data Sources
74
77
  const history = data['portfolio_snapshots'] || [];
75
- const todayRow = history.find(d => d.date === date);
78
+ // FIX: Handle DataFetcher returning Objects (keyed by ID) or Arrays.
79
+ const tickerRows = Object.values(data['ticker_mappings'] || {});
80
+ const sectorRows = Object.values(data['sector_mappings'] || {});
81
+
82
+ // 2. Build Mapping Dictionaries
83
+ const tickerMap = new Map();
84
+ tickerRows.forEach(row => {
85
+ if (row.instrument_id && row.ticker) {
86
+ tickerMap.set(Number(row.instrument_id), row.ticker.toUpperCase());
87
+ }
88
+ });
89
+
90
+ const sectorMap = new Map();
91
+ sectorRows.forEach(row => {
92
+ if (row.symbol && row.sector && row.sector !== 'N/A') {
93
+ sectorMap.set(row.symbol.toUpperCase(), row.sector);
94
+ }
95
+ });
96
+
97
+ const resolveSector = (instrumentId) => {
98
+ const ticker = tickerMap.get(Number(instrumentId));
99
+ if (!ticker) return null;
100
+ return sectorMap.get(ticker);
101
+ };
102
+
103
+ // 3. Helper to handle BigQuery Dates
104
+ const toDateStr = (d) => {
105
+ if (!d) return null;
106
+ if (d.value) return d.value;
107
+ return d;
108
+ };
109
+
110
+ // 4. Find Today and Last Known Previous Row
111
+ // Sort history descending by date to find the most recent previous entry
112
+ history.sort((a, b) => {
113
+ const dateA = toDateStr(a.date);
114
+ const dateB = toDateStr(b.date);
115
+ return dateB.localeCompare(dateA); // Descending (Newest First)
116
+ });
117
+
118
+ const todayRow = history.find(d => toDateStr(d.date) === date);
76
119
 
77
- // Calc yesterday date
78
- const yDate = new Date(date);
79
- yDate.setDate(yDate.getDate() - 1);
80
- const yDateStr = yDate.toISOString().split('T')[0];
81
- const yesterdayRow = history.find(d => d.date === yDateStr);
120
+ // Find the first row where date is strictly less than target date
121
+ // Since we sorted descending, this is the "Last Known" date
122
+ const previousRow = history.find(d => toDateStr(d.date) < date);
82
123
 
83
- // 2. Extract Data
124
+ // 5. Extract Portfolio Data
84
125
  const todayData = rules.portfolio.extractPortfolioData(todayRow);
85
- const yesterdayData = rules.portfolio.extractPortfolioData(yesterdayRow);
126
+ const previousData = rules.portfolio.extractPortfolioData(previousRow);
86
127
 
87
128
  if (!todayData) return;
88
129
 
89
- // 3. Helper to get Sector Map
130
+ // 6. Calculate Sector Exposures
90
131
  const getSectorMap = (pData) => {
91
132
  const map = new Map();
92
- // Using Rule: extractPositionsByType gives AggregatedPositionsByInstrumentTypeID
93
- const aggs = rules.portfolio.extractPositionsByType(pData);
133
+ const positions = rules.portfolio.extractPositions(pData);
94
134
 
95
- aggs.forEach(agg => {
96
- const typeId = agg.InstrumentTypeID;
97
- const invested = agg.Invested || 0;
135
+ positions.forEach(pos => {
136
+ const instrumentId = rules.portfolio.getInstrumentId(pos);
137
+ const invested = rules.portfolio.getInvested(pos);
98
138
 
99
- if (typeId && invested > 0) {
100
- const sectorName = this.SECTOR_MAPPING[typeId];
101
- if (sectorName) map.set(sectorName, invested);
139
+ if (instrumentId && invested > 0) {
140
+ const sectorName = resolveSector(instrumentId);
141
+ if (sectorName) {
142
+ const current = map.get(sectorName) || 0;
143
+ map.set(sectorName, current + invested);
144
+ }
102
145
  }
103
146
  });
104
147
  return map;
105
148
  };
106
149
 
107
150
  const todaySectors = getSectorMap(todayData);
108
- const yesterdaySectors = getSectorMap(yesterdayData);
151
+ const previousSectors = getSectorMap(previousData);
109
152
 
153
+ // 7. Compare
110
154
  const newExposures = [];
111
- const isBaselineReset = (!yesterdayData);
155
+ const isBaselineReset = (!previousData); // True if no previous history found
112
156
 
113
157
  if (!isBaselineReset) {
114
158
  for (const [sectorName, invested] of todaySectors) {
115
- const prevInvested = yesterdaySectors.get(sectorName) || 0;
159
+ const prevInvested = previousSectors.get(sectorName) || 0;
116
160
  if (prevInvested === 0 && invested > 0) {
117
161
  newExposures.push(sectorName);
118
162
  }
@@ -121,11 +165,14 @@ class NewSectorExposure extends Computation {
121
165
 
122
166
  const result = {
123
167
  currentSectors: Array.from(todaySectors.keys()),
124
- previousSectors: isBaselineReset ? [] : Array.from(yesterdaySectors.keys()),
168
+ previousSectors: isBaselineReset ? [] : Array.from(previousSectors.keys()),
125
169
  newExposures,
126
170
  sectorName: newExposures.join(', '),
127
171
  isBaselineReset,
128
- triggered: newExposures.length > 0
172
+ triggered: newExposures.length > 0,
173
+ _metadata: {
174
+ previousDate: previousRow ? toDateStr(previousRow.date) : null
175
+ }
129
176
  };
130
177
 
131
178
  this.setResult(entityId, result);
@@ -1,3 +1,8 @@
1
+ /**
2
+ * @fileoverview New Social Post Alert
3
+ * Logic: Alerts ONLY if a post exists with a creation date matching the current run date.
4
+ * Relies on DataFetcher V2.8+ to handle JSON parsing/double-encoding.
5
+ */
1
6
  const { Computation } = require('../framework');
2
7
 
3
8
  class NewSocialPost extends Computation {
@@ -7,11 +12,11 @@ class NewSocialPost extends Computation {
7
12
  name: 'NewSocialPost',
8
13
  type: 'per-entity',
9
14
  category: 'alerts',
10
- isHistorical: false,
15
+ isHistorical: true,
11
16
 
12
17
  requires: {
13
18
  'social_post_snapshots': {
14
- lookback: 0,
19
+ lookback: 5, // Short lookback just to ensure we catch the specific row
15
20
  mandatory: true,
16
21
  fields: ['user_id', 'posts_data', 'date']
17
22
  }
@@ -26,19 +31,15 @@ class NewSocialPost extends Computation {
26
31
  }
27
32
  },
28
33
 
29
- // LEGACY METADATA
30
34
  userType: 'POPULAR_INVESTOR',
31
35
  alert: {
32
36
  id: 'newSocialPost',
33
37
  frontendName: 'New Social Post',
34
- description: 'Alert when a Popular Investor makes a new social post',
38
+ description: 'Alert when a Popular Investor makes a new social post today',
35
39
  messageTemplate: '{piUsername} posted a new update: {title}',
36
40
  severity: 'low',
37
41
  configKey: 'newSocialPost',
38
- isDynamic: false,
39
- thresholds: [],
40
- conditions: [],
41
- resultKeys: ['hasNewPost', 'latestPostDate', 'postCount', 'title']
42
+ isDynamic: false
42
43
  }
43
44
  };
44
45
  }
@@ -46,50 +47,77 @@ class NewSocialPost extends Computation {
46
47
  async process(context) {
47
48
  const { data, entityId, date, rules } = context;
48
49
 
49
- // 1. Get Data using Rules
50
- // V2 data structure: data['table'][entityId] is the row (when lookback=0)
51
- const row = data['social_post_snapshots'];
52
- const posts = rules.social.extractPosts(row);
50
+ // 1. Get History
51
+ const history = data['social_post_snapshots'] || [];
52
+
53
+ // Helper: Handle BigQuery Date Objects
54
+ const toDateStr = (d) => {
55
+ if (!d) return null;
56
+ if (d.value) return d.value;
57
+ return d;
58
+ };
59
+
60
+ // 2. Find the Snapshot for "Today" (The Execution Date)
61
+ // We strictly want to know if they posted *on this specific date*.
62
+ const todayRow = history.find(d => toDateStr(d.date) === date);
63
+
64
+ if (!todayRow) {
65
+ // No snapshot exists for this date yet, so we can't determine if they posted.
66
+ this.setResult(entityId, {
67
+ hasNewPost: false,
68
+ triggered: false,
69
+ reason: "No snapshot found for date"
70
+ });
71
+ return;
72
+ }
73
+
74
+ // 3. Extract Posts
75
+ // (DataFetcher V2.8 automatically handles the double-encoded JSON)
76
+ const posts = rules.social.extractPosts(todayRow);
53
77
 
54
78
  let hasNewPost = false;
55
79
  let latestPost = null;
56
80
 
57
- // 2. Logic
81
+ // 4. Check for posts created "Today"
58
82
  for (const post of posts) {
59
- const postDate = rules.social.getPostDate(post);
60
- if (!postDate) continue;
83
+ const postDateObj = rules.social.getPostDate(post);
84
+ if (!postDateObj) continue;
85
+
86
+ // Format YYYY-MM-DD
87
+ const postDateStr = postDateObj.toISOString().slice(0, 10);
61
88
 
62
- // Check matches today (YYYY-MM-DD)
63
- if (postDate.toISOString().slice(0, 10) === date) {
89
+ if (postDateStr === date) {
64
90
  hasNewPost = true;
65
91
 
66
- const latestDate = latestPost ? rules.social.getPostDate(latestPost) : new Date(0);
67
- if (postDate > latestDate) {
92
+ // Track the latest one for the alert title
93
+ if (!latestPost || postDateObj > rules.social.getPostDate(latestPost)) {
68
94
  latestPost = post;
69
95
  }
70
96
  }
71
97
  }
72
98
 
73
- // 3. Title Extraction
99
+ // 5. Format Title for Alert
74
100
  let displayTitle = 'New Update';
75
101
  if (latestPost) {
76
102
  const text = rules.social.getPostText(latestPost);
77
- const rawTitle = latestPost.Title; // Fallback if rule doesn't cover Title specifically
103
+ const rawTitle = latestPost.Title;
78
104
 
79
105
  if (rawTitle) {
80
106
  displayTitle = rawTitle;
81
107
  } else if (text) {
82
- displayTitle = text.substring(0, 50).replace(/\n/g, ' ');
83
- if (text.length > 50) displayTitle += '...';
108
+ // Truncate text for display
109
+ displayTitle = text.substring(0, 60).replace(/\n/g, ' ');
110
+ if (text.length > 60) displayTitle += '...';
84
111
  }
85
112
  }
86
113
 
114
+ // 6. Set Result
87
115
  const result = {
88
116
  hasNewPost,
89
117
  latestPostDate: latestPost ? rules.social.getPostDate(latestPost) : null,
90
118
  postCount: posts.length,
91
119
  title: displayTitle,
92
- triggered: hasNewPost
120
+ triggered: hasNewPost // Only triggers if a post matched today's date
93
121
  };
94
122
 
95
123
  this.setResult(entityId, result);