bulltrackers-module 1.0.770 → 1.0.772

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.
@@ -13,27 +13,25 @@ class GlobalAumPerAsset extends Computation {
13
13
  name: 'GlobalAumPerAsset',
14
14
  type: 'global',
15
15
  category: 'market_insights',
16
-
16
+
17
17
  requires: {
18
18
  'computation_results': {
19
- lookback: 0,
19
+ lookback: 0,
20
20
  mandatory: true,
21
21
  fields: ['result_data', 'computation_name', 'date'],
22
22
  // Lowercase to match BigQuery storage
23
- filter: {
24
- computation_name: 'pidailyassetaum'
23
+ filter: {
24
+ computation_name: 'pidailyassetaum'
25
25
  }
26
26
  }
27
27
  },
28
-
28
+
29
29
  dependencies: ['PIDailyAssetAUM'],
30
30
 
31
31
  storage: {
32
32
  bigquery: true,
33
33
  firestore: {
34
- enabled: true,
35
- path: 'global_metrics/aum_per_asset_{date}',
36
- merge: true
34
+ enabled: false,
37
35
  }
38
36
  }
39
37
  };
@@ -44,8 +42,8 @@ class GlobalAumPerAsset extends Computation {
44
42
 
45
43
  let rows = data['computation_results'];
46
44
  if (!rows) {
47
- console.log('[GlobalAumPerAsset] No input data found.');
48
- return;
45
+ console.log('[GlobalAumPerAsset] No input data found.');
46
+ return;
49
47
  }
50
48
 
51
49
  if (!Array.isArray(rows) && typeof rows === 'object') {
@@ -64,15 +62,15 @@ class GlobalAumPerAsset extends Computation {
64
62
 
65
63
  for (const row of rows) {
66
64
  let assetMap = row.result_data;
67
-
65
+
68
66
  if (typeof assetMap === 'string') {
69
67
  try { assetMap = JSON.parse(assetMap); } catch (e) { continue; }
70
68
  }
71
-
69
+
72
70
  if (!assetMap) continue;
73
71
 
74
72
  piCount++;
75
-
73
+
76
74
  for (const [ticker, value] of Object.entries(assetMap)) {
77
75
  if (typeof value === 'number') {
78
76
  const current = globalTotals.get(ticker) || 0;
@@ -8,7 +8,7 @@ class PIDailyAssetAUM extends Computation {
8
8
  type: 'per-entity',
9
9
  category: 'popular_investor',
10
10
  isHistorical: true,
11
-
11
+
12
12
  requires: {
13
13
  // Driver: Only process Popular Investors
14
14
  'portfolio_snapshots': {
@@ -22,22 +22,20 @@ class PIDailyAssetAUM extends Computation {
22
22
  mandatory: true,
23
23
  fields: ['pi_id', 'rankings_data', 'date']
24
24
  },
25
- 'ticker_mappings': {
25
+ 'ticker_mappings': {
26
26
  mandatory: false,
27
- fields: ['instrument_id', 'ticker']
28
- },
29
- 'pi_master_list': {
27
+ fields: ['instrument_id', 'ticker']
28
+ },
29
+ 'pi_master_list': {
30
30
  mandatory: false,
31
- fields: ['cid', 'username']
31
+ fields: ['cid', 'username']
32
32
  }
33
33
  },
34
34
 
35
35
  storage: {
36
36
  bigquery: true,
37
37
  firestore: {
38
- enabled: true,
39
- path: 'popular_investors/{entityId}/metrics/asset_aum_{date}',
40
- merge: true
38
+ enabled: false
41
39
  }
42
40
  }
43
41
  };
@@ -62,7 +60,7 @@ class PIDailyAssetAUM extends Computation {
62
60
  if (Array.isArray(dataset)) return dataset.filter(r => String(r.user_id || r.pi_id || r.cid) === String(entityId));
63
61
  return [];
64
62
  };
65
-
63
+
66
64
  // Mappings
67
65
  const tickerMap = new Map();
68
66
  const mappingsList = Array.isArray(context.data['ticker_mappings']) ? context.data['ticker_mappings'] : Object.values(context.data['ticker_mappings'] || {});
@@ -77,8 +75,8 @@ class PIDailyAssetAUM extends Computation {
77
75
  let validRanking = null;
78
76
 
79
77
  // Sort descending using safe date parsing
80
- portfolios.sort((a,b) => parseDate(b.date) - parseDate(a.date));
81
- rankings.sort((a,b) => parseDate(b.date) - parseDate(a.date));
78
+ portfolios.sort((a, b) => parseDate(b.date) - parseDate(a.date));
79
+ rankings.sort((a, b) => parseDate(b.date) - parseDate(a.date));
82
80
 
83
81
  if (portfolios.length > 0) {
84
82
  validPortfolio = portfolios[0];
@@ -88,7 +86,7 @@ class PIDailyAssetAUM extends Computation {
88
86
  const rDate = parseDate(r.date);
89
87
  const diffTime = Math.abs(pDate - rDate);
90
88
  const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
91
-
89
+
92
90
  // Allow up to 14 days gap (matches your data reality)
93
91
  return diffDays <= 14;
94
92
  });
@@ -101,10 +99,10 @@ class PIDailyAssetAUM extends Computation {
101
99
  // 3. Calculation
102
100
  const pData = rules.portfolio.extractPortfolioData(validPortfolio);
103
101
  const rData = rules.rankings.extractRankingsData(validRanking);
104
-
102
+
105
103
  // FIX: Use 'getAUM' (checked from rules/rankings.js)
106
- const totalAum = rules.rankings.getAUM(rData);
107
-
104
+ const totalAum = rules.rankings.getAUM(rData);
105
+
108
106
  if (!totalAum || totalAum <= 0) return;
109
107
 
110
108
  const positions = rules.portfolio.extractPositions(pData);
@@ -113,10 +111,10 @@ class PIDailyAssetAUM extends Computation {
113
111
  positions.forEach(pos => {
114
112
  const id = rules.portfolio.getInstrumentId(pos);
115
113
  const ticker = resolveTicker(id);
116
-
114
+
117
115
  // FIX: Use 'getInvested' (checked from rules/portfolio.js)
118
- const weight = rules.portfolio.getInvested(pos);
119
-
116
+ const weight = rules.portfolio.getInvested(pos);
117
+
120
118
  if (weight > 0) {
121
119
  // weight is a percentage (e.g., 5.5 for 5.5%), so divide by 100
122
120
  const dollarValue = (weight / 100) * totalAum;
@@ -1,14 +1,11 @@
1
1
  /**
2
- * @fileoverview PI Feature Vectors
3
- *
4
- * Global computation that builds normalized feature vectors for all Popular Investors.
5
- * Used by PiRecommender to find balanced recommendations.
6
- *
7
- * Features computed per PI:
8
- * - Sector exposure (normalized %)
9
- * - Risk profile (volatility, risk score)
10
- * - Performance metrics (gain %, win ratio)
11
- * - Quality indicators (AUM tier, copiers)
2
+ * @fileoverview PI Feature Vectors (v2 - Direction Aware)
3
+ * * Global computation that builds normalized feature vectors for all Popular Investors.
4
+ * Calculates Net Exposure (Long - Short) based on Portfolio Value %.
5
+ * * Features computed per PI:
6
+ * - Net Sector exposure (Signed %: + for Long, - for Short)
7
+ * - Leverage utilization
8
+ * - Quality indicators (Risk, Gain, Copiers)
12
9
  */
13
10
  const { Computation } = require('../framework');
14
11
 
@@ -16,7 +13,7 @@ class PiFeatureVectors extends Computation {
16
13
  static getConfig() {
17
14
  return {
18
15
  name: 'PiFeatureVectors',
19
- description: 'Computes feature vectors for all PIs for recommendation matching',
16
+ description: 'Computes signed feature vectors for all PIs (Net Exposure)',
20
17
  type: 'global',
21
18
  category: 'popular_investor',
22
19
  isHistorical: false,
@@ -36,10 +33,10 @@ class PiFeatureVectors extends Computation {
36
33
  'pi_master_list': {
37
34
  lookback: 1,
38
35
  mandatory: false,
39
- fields: ['cid', 'username', 'verified_date']
36
+ fields: ['cid', 'username', 'last_updated']
40
37
  },
41
- 'ticker_mappings': { mandatory: false },
42
- 'sector_mappings': { mandatory: false }
38
+ 'ticker_mappings': { lookback: 0, mandatory: false, fields: ['instrument_id', 'ticker'] },
39
+ 'sector_mappings': { lookback: 0, mandatory: false, fields: ['symbol', 'sector'] }
43
40
  },
44
41
 
45
42
  storage: {
@@ -53,16 +50,10 @@ class PiFeatureVectors extends Computation {
53
50
  const { data, rules, targetDate } = context;
54
51
 
55
52
  // ===========================================================================
56
- // SETUP: Helpers and reference data
53
+ // SETUP: Mappings
57
54
  // ===========================================================================
58
- const toArray = (input) => {
59
- if (!input) return [];
60
- if (Array.isArray(input)) return input;
61
- // Handle Map-like structure from DataFetcher
62
- return Object.values(input).flat();
63
- };
55
+ const toArray = (input) => Array.isArray(input) ? input : Object.values(input).flat();
64
56
 
65
- // Build sector lookup
66
57
  const tickerMap = new Map();
67
58
  toArray(data['ticker_mappings']).forEach(row => {
68
59
  if (row.instrument_id && row.ticker) {
@@ -82,7 +73,6 @@ class PiFeatureVectors extends Computation {
82
73
  return ticker ? (sectorMap.get(ticker) || 'Other') : 'Other';
83
74
  };
84
75
 
85
- // All known sectors for consistent vector dimensions
86
76
  const ALL_SECTORS = [
87
77
  'Technology', 'Healthcare', 'Financial Services', 'Consumer Cyclical',
88
78
  'Industrials', 'Energy', 'Utilities', 'Real Estate', 'Communication Services',
@@ -90,28 +80,22 @@ class PiFeatureVectors extends Computation {
90
80
  ];
91
81
 
92
82
  // ===========================================================================
93
- // GATHER: PI portfolio and rankings data
83
+ // GATHER DATA
94
84
  // ===========================================================================
95
85
  const rankings = toArray(data['pi_rankings']);
96
86
  const portfolios = data['portfolio_snapshots']; // Map { piId: [rows] }
97
87
  const masterList = toArray(data['pi_master_list']);
98
88
 
99
- // Build username lookup
100
89
  const usernameMap = new Map();
101
- rankings.forEach(r => {
102
- const id = String(r.pi_id || r.CustomerId);
103
- usernameMap.set(id, r.username || r.UserName);
104
- });
90
+ rankings.forEach(r => usernameMap.set(String(r.pi_id || r.CustomerId), r.username || r.UserName));
105
91
  masterList.forEach(m => {
106
92
  if (m.cid && m.username) usernameMap.set(String(m.cid), m.username);
107
93
  });
108
94
 
109
95
  // ===========================================================================
110
- // COMPUTE: Feature vectors for each PI
96
+ // COMPUTE VECTORS
111
97
  // ===========================================================================
112
98
  const featureVectors = {};
113
-
114
- // Process each PI's portfolio
115
99
  const piIds = Object.keys(portfolios || {});
116
100
 
117
101
  for (const piId of piIds) {
@@ -119,55 +103,37 @@ class PiFeatureVectors extends Computation {
119
103
  const latestRow = rows[rows.length - 1];
120
104
  if (!latestRow) continue;
121
105
 
122
- // Extract positions
123
106
  const pData = rules.portfolio.extractPortfolioData(latestRow);
124
107
  const positions = rules.portfolio.extractPositions(pData);
125
108
 
126
109
  if (positions.length === 0) continue;
127
110
 
128
- // Calculate sector exposure
129
- const sectorWeights = {};
130
- let totalValue = 0;
111
+ // 1. Calculate Signed Sector Exposure (Net Exposure)
112
+ // Value is % of Total Portfolio Value.
113
+ const sectorNetValue = {};
114
+ let totalGrossExposure = 0; // Sum of abs(value)
131
115
 
132
116
  positions.forEach(pos => {
133
117
  const instrumentId = rules.portfolio.getInstrumentId(pos);
134
- const invested = rules.portfolio.getInvested(pos) || 0;
118
+ const value = rules.portfolio.getValue(pos) || 0; // Already a %
135
119
  const sector = resolveSector(instrumentId);
120
+ const isShort = rules.portfolio.isShort(pos);
121
+
122
+ // Signed Value: Long is +, Short is -
123
+ const signedValue = isShort ? -value : value;
136
124
 
137
- sectorWeights[sector] = (sectorWeights[sector] || 0) + invested;
138
- totalValue += invested;
125
+ sectorNetValue[sector] = (sectorNetValue[sector] || 0) + signedValue;
126
+ totalGrossExposure += value; // Utilization of equity
139
127
  });
140
128
 
141
- // Normalize sector exposure to percentages
129
+ // Populate vector with Net Exposure % (e.g., -15.5 for Short Tech)
142
130
  const sectorVector = {};
143
131
  ALL_SECTORS.forEach(s => {
144
- sectorVector[s] = totalValue > 0
145
- ? Number(((sectorWeights[s] || 0) / totalValue).toFixed(4))
146
- : 0;
132
+ sectorVector[s] = Number((sectorNetValue[s] || 0).toFixed(4));
147
133
  });
148
134
 
149
- // Get ranking data
150
- if (piIds.indexOf(piId) === 0) {
151
- const availableIds = rankings.map(r => r.pi_id || 'undefined').join(',');
152
- console.log(`[DEBUG] Available IDs in rankings: ${availableIds}`);
153
- console.log(`[DEBUG] First ranking row keys: ${Object.keys(rankings[0] || {}).join(',')}`);
154
- if (rankings[0]) console.log(`[DEBUG] First ranking row pi_id: ${rankings[0].pi_id}`);
155
- }
135
+ // 2. Extract Quality Metrics
156
136
  const rankRow = rankings.find(r => String(r.pi_id || r.CustomerId) === String(piId));
157
-
158
- if (piIds.indexOf(piId) === 0) {
159
- console.log(`[DEBUG] PiId: ${piId}`);
160
- console.log(`[DEBUG] RankRow found: ${!!rankRow}`);
161
- if (rankRow) {
162
- console.log(`[DEBUG] RankRow keys: ${Object.keys(rankRow).join(',')}`);
163
- console.log(`[DEBUG] RankingsData type: ${typeof rankRow.rankings_data}`);
164
- if (typeof rankRow.rankings_data === 'object') {
165
- console.log(`[DEBUG] RankingsData keys: ${Object.keys(rankRow.rankings_data).join(',')}`);
166
- console.log(`[DEBUG] RankingsData.Gain: ${rankRow.rankings_data.Gain}`);
167
- }
168
- }
169
- }
170
-
171
137
  let riskScore = 5, gain = 0, copiers = 0, winRatio = 0, aumTier = 0;
172
138
 
173
139
  if (rankRow) {
@@ -181,40 +147,28 @@ class PiFeatureVectors extends Computation {
181
147
  }
182
148
  }
183
149
 
184
- // Calculate concentration (Herfindahl index) - higher = more concentrated
185
- const weights = Object.values(sectorWeights).filter(w => w > 0);
186
- const hhi = totalValue > 0
187
- ? weights.reduce((sum, w) => sum + Math.pow(w / totalValue, 2), 0)
150
+ // Concentration (HHI based on gross exposure per sector)
151
+ const grossWeights = Object.values(sectorNetValue).map(Math.abs);
152
+ const sumGross = grossWeights.reduce((a, b) => a + b, 0);
153
+ const hhi = sumGross > 0
154
+ ? grossWeights.reduce((sum, w) => sum + Math.pow(w / sumGross, 2), 0)
188
155
  : 1;
189
156
 
190
- // Assemble feature vector
191
157
  featureVectors[piId] = {
192
158
  piId: String(piId),
193
159
  username: usernameMap.get(String(piId)) || 'Unknown',
194
-
195
- // Sector exposure (normalized)
196
- sectors: sectorVector,
197
-
198
- // Risk profile
199
- riskScore: riskScore,
200
- concentration: Number(hhi.toFixed(4)), // 0-1, higher = less diversified
201
-
202
- // Performance
203
- gain: gain,
204
- winRatio: winRatio,
205
-
206
- // Quality
207
- copiers: copiers,
208
- aumTier: aumTier,
209
- positionCount: positions.length,
210
-
211
- // Metadata
212
- totalValue: Number(totalValue.toFixed(2)),
160
+ sectors: sectorVector, // Signed Net Exposure
161
+ riskScore,
162
+ concentration: Number(hhi.toFixed(4)),
163
+ gain,
164
+ winRatio,
165
+ copiers,
166
+ aumTier,
167
+ grossExposure: Number(totalGrossExposure.toFixed(2)),
213
168
  computedAt: targetDate
214
169
  };
215
170
  }
216
171
 
217
- // Store all vectors as a single global result
218
172
  this.setResult('_global', {
219
173
  vectors: featureVectors,
220
174
  piCount: Object.keys(featureVectors).length,
@@ -224,4 +178,4 @@ class PiFeatureVectors extends Computation {
224
178
  }
225
179
  }
226
180
 
227
- module.exports = PiFeatureVectors;
181
+ module.exports = PiFeatureVectors;