aiden-shared-calculations-unified 1.0.44 → 1.0.45

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.
@@ -1,64 +1,128 @@
1
1
  /**
2
2
  * @fileoverview Tracks the investment flow of "smart money".
3
3
  *
4
- * --- FIX ---
5
- * The core logic (identifySmartMoney) has been moved from process() to
6
- * getResult(). This is critical to prevent a race condition where
7
- * this calculation tries to *read* the profitability shards
8
- * before user_profitability_tracker has *written* them.
9
- * --- END FIX ---
4
+ * --- META REFACTOR (v2) ---
5
+ * This is a streaming meta-calc. It reads historical shards (a side-effect
6
+ * from Pass 1) and streams root portfolio data to calculate its result.
7
+ * It has no direct computational dependencies passed to `process`.
10
8
  */
11
9
 
12
10
  const { Firestore } = require('@google-cloud/firestore');
13
11
  const firestore = new Firestore();
14
- // CORRECTED PATH: ../utils/ instead of ../../utils/
15
12
  const { getInstrumentSectorMap } = require('../../../utils/sector_mapping_provider');
13
+ // NOTE: Corrected relative path for data_loader
14
+ const { loadDataByRefs, getPortfolioPartRefs, loadFullDayMap } = require('../../../../bulltrackers-module/functions/computation-system/utils/data_loader');
15
+
16
16
 
17
17
  class SmartMoneyFlow {
18
+
19
+ /**
20
+ * (NEW) Statically declare dependencies.
21
+ * This calc reads shards from Pass 1, but doesn't take a computed
22
+ * result as an argument. The manifest ensures it runs in Pass 2.
23
+ */
24
+ static getDependencies() {
25
+ return ['user-profitability-tracker']; // This is just for ordering, not for `fetchedDependencies`
26
+ }
27
+
18
28
  constructor() {
19
29
  this.smartMoneyUsers = new Set();
20
- this.sectorMap = null; // Cache for the sector map
21
-
22
- // --- NEW ---
30
+ this.sectorMap = null;
31
+
23
32
  // We now store the portfolio data during process()
33
+ // (This state is reset for each date by the runner)
24
34
  this.todayPortfolios = {};
25
35
  this.yesterdayPortfolios = {};
26
- // --- END NEW ---
27
36
  }
28
37
 
29
- async process(todayPortfolio, yesterdayPortfolio, userId) {
30
- // --- FIX: process() NO LONGER calls identifySmartMoney ---
31
- // It just caches the portfolios we need for getResult()
32
-
33
- // Load sector map if not already loaded
38
+ /**
39
+ * REFACTORED PROCESS METHOD
40
+ * This is now the *first* method called by the runner.
41
+ * It caches the portfolios.
42
+ */
43
+ async process(dateStr, dependencies, config, fetchedDependencies) {
44
+ const { logger, db, rootData } = dependencies;
45
+ const { portfolioRefs } = rootData;
46
+ logger.log('INFO', '[SmartMoneyFlow] Starting meta-process...');
47
+
48
+ // 1. Load external dependencies (sectors)
34
49
  if (!this.sectorMap) {
35
50
  try {
36
51
  this.sectorMap = await getInstrumentSectorMap();
37
- if (!this.sectorMap || Object.keys(this.sectorMap).length === 0) {
38
- console.warn('Sector map loaded but is empty.');
39
- }
40
52
  } catch (error) {
41
- console.error('Failed to load sector map:', error);
42
- return; // Stop processing if sector map fails to load
53
+ logger.log('ERROR', '[SmartMoneyFlow] Failed to load sector map.', { err: error.message });
54
+ return null; // Abort
43
55
  }
44
56
  }
45
-
46
- if (todayPortfolio) {
47
- this.todayPortfolios[userId] = todayPortfolio;
57
+
58
+ // 2. Load "yesterday's" portfolio data for comparison
59
+ const yesterdayDate = new Date(dateStr + 'T00:00:00Z');
60
+ yesterdayDate.setUTCDate(yesterdayDate.getUTCDate() - 1);
61
+ const yesterdayStr = yesterdayDate.toISOString().slice(0, 10);
62
+ const yesterdayRefs = await getPortfolioPartRefs(config, dependencies, yesterdayStr);
63
+ const yesterdayPortfolios = await loadFullDayMap(config, dependencies, yesterdayRefs);
64
+ logger.log('INFO', `[SmartMoneyFlow] Loaded ${yesterdayRefs.length} part refs for yesterday.`);
65
+
66
+ // 3. Identify smart money users by reading Pass 1 shards
67
+ // This is the dependency.
68
+ await this.identifySmartMoney(dependencies);
69
+ if (this.smartMoneyUsers.size === 0) {
70
+ logger.log('WARN', '[SmartMoneyFlow] No smart money users identified. Aborting.');
71
+ return { smart_money_flow: {} };
48
72
  }
49
- if (yesterdayPortfolio) {
50
- this.yesterdayPortfolios[userId] = yesterdayPortfolio;
73
+
74
+ // 4. Stream "today's" portfolio data and process *only smart users*
75
+ const batchSize = config.partRefBatchSize || 10;
76
+ const sectorFlow = {};
77
+
78
+ for (let i = 0; i < portfolioRefs.length; i += batchSize) {
79
+ const batchRefs = portfolioRefs.slice(i, i + batchSize);
80
+ const todayPortfoliosChunk = await loadDataByRefs(config, dependencies, batchRefs);
81
+
82
+ for (const uid in todayPortfoliosChunk) {
83
+ // --- Filter user ---
84
+ if (!this.smartMoneyUsers.has(uid)) {
85
+ continue;
86
+ }
87
+
88
+ const pToday = todayPortfoliosChunk[uid];
89
+ const pYesterday = yesterdayPortfolios[uid];
90
+
91
+ if (!pToday || !pYesterday) continue;
92
+
93
+ // --- User is in cohort, run logic ---
94
+ const todaySectorInvestment = this.calculateSectorInvestment(pToday);
95
+ const yesterdaySectorInvestment = this.calculateSectorInvestment(pYesterday);
96
+
97
+ const allSectors = new Set([...Object.keys(todaySectorInvestment), ...Object.keys(yesterdaySectorInvestment)]);
98
+ for (const sector of allSectors) {
99
+ const todayAmount = todaySectorInvestment[sector] || 0;
100
+ const yesterdayAmount = yesterdaySectorInvestment[sector] || 0;
101
+ const change = todayAmount - yesterdayAmount;
102
+
103
+ if (change !== 0) {
104
+ if (!sectorFlow[sector]) {
105
+ sectorFlow[sector] = 0;
106
+ }
107
+ sectorFlow[sector] += change;
108
+ }
109
+ }
110
+ }
51
111
  }
112
+
113
+ logger.log('INFO', `[SmartMoneyFlow] Processed ${this.smartMoneyUsers.size} smart users.`);
114
+
115
+ // 5. Return final result
116
+ return { smart_money_flow: sectorFlow };
52
117
  }
53
118
 
54
- async identifySmartMoney() {
55
- console.log("Attempting to identify smart money users...");
119
+ async identifySmartMoney(dependencies) {
120
+ const { logger } = dependencies;
121
+ logger.log('INFO', "[SmartMoneyFlow] Attempting to identify smart money users by reading shards...");
56
122
  try {
57
- // Fetching sharded profitability data
58
123
  const shardPromises = [];
59
- const NUM_SHARDS = 50; // Ensure this matches the value used in user_profitability_tracker
124
+ const NUM_SHARDS = 50;
60
125
  for (let i = 0; i < NUM_SHARDS; i++) {
61
- // Corrected document path for shards
62
126
  const docRef = firestore.collection('historical_insights').doc(`user_profitability_shard_${i}`);
63
127
  shardPromises.push(docRef.get());
64
128
  }
@@ -70,10 +134,7 @@ class SmartMoneyFlow {
70
134
  const shardData = snap.data() || {};
71
135
  for (const userId in shardData) {
72
136
  const history = shardData[userId];
73
- // Check if history is an array and has enough entries
74
137
  if (Array.isArray(history) && history.length >= 5) {
75
- // "Smart money" definition: profitable for at least 5 of the last 7 days
76
- // Filter based on the 'pnl' property of each history entry
77
138
  const profitableDays = history.slice(-7).filter(d => d && typeof d.pnl === 'number' && d.pnl > 0).length;
78
139
  if (profitableDays >= 5) {
79
140
  this.smartMoneyUsers.add(userId);
@@ -82,32 +143,25 @@ class SmartMoneyFlow {
82
143
  }
83
144
  }
84
145
  } else {
85
- console.warn(`Profitability shard user_profitability_shard_${index} does not exist.`);
146
+ logger.log('WARN', `[SmartMoneyFlow] Profitability shard user_profitability_shard_${index} does not exist.`);
86
147
  }
87
148
  });
88
- console.log(`Found ${profitableUsersFound} potentially smart money users across all shards.`);
149
+ logger.log('INFO', `[SmartMoneyFlow] Found ${profitableUsersFound} smart money users across all shards.`);
89
150
 
90
151
  } catch (error) {
91
- console.error('Error identifying smart money users:', error);
92
- // Decide how to handle error: maybe retry, or proceed without smart money data
93
- this.smartMoneyUsers.clear(); // Ensure set is empty on error
94
- }
95
- // Final check after attempt
96
- if (this.smartMoneyUsers.size === 0) {
97
- console.warn("No smart money users identified. Smart money flow calculation might be empty.");
152
+ logger.log('ERROR', '[SmartMoneyFlow] Error identifying smart money users', { err: error.message });
153
+ this.smartMoneyUsers.clear();
98
154
  }
99
155
  }
100
156
 
101
157
 
102
158
  calculateSectorInvestment(portfolio) {
103
159
  const sectorInvestment = {};
104
- // Ensure sectorMap is loaded
105
160
  if (!this.sectorMap) {
106
- console.warn("Cannot calculate sector investment: Sector map not loaded.");
107
- return sectorInvestment; // Return empty object
161
+ console.warn("[SmartMoneyFlow] Cannot calculate sector investment: Sector map not loaded.");
162
+ return sectorInvestment;
108
163
  }
109
164
 
110
- // Use AggregatedPositions as it contains 'Invested' amount
111
165
  if (portfolio && portfolio.AggregatedPositions && Array.isArray(portfolio.AggregatedPositions)) {
112
166
  for (const pos of portfolio.AggregatedPositions) {
113
167
  if (pos && typeof pos.InstrumentID !== 'undefined' && typeof pos.Invested === 'number') {
@@ -116,70 +170,20 @@ class SmartMoneyFlow {
116
170
  sectorInvestment[sector] = 0;
117
171
  }
118
172
  sectorInvestment[sector] += pos.Invested;
119
- } else {
120
- // Log potentially malformed position data
121
- // console.warn('Skipping position due to missing InstrumentID or Invested amount:', pos);
122
173
  }
123
174
  }
124
- } else {
125
- // Log if AggregatedPositions are missing or not an array
126
- // console.warn('AggregatedPositions missing or invalid in portfolio for sector investment calculation.');
127
175
  }
128
176
  return sectorInvestment;
129
177
  }
130
178
 
131
179
 
132
- async getResult() {
133
- // --- FIX: MOVED FROM process() ---
134
- // 1. Identify smart money *now*, after Pass 1's getResult() has run
135
- await this.identifySmartMoney();
136
-
137
- // 2. Ensure sector map is loaded (it should be from process())
138
- if (!this.sectorMap) {
139
- console.error('SmartMoneyFlow: Sector map not loaded, cannot get result.');
140
- return { smart_money_flow: {} };
141
- }
142
- // --- END FIX ---
143
-
144
- const sectorFlow = {};
145
-
146
- // 3. Loop through the cached portfolios
147
- for (const userId of this.smartMoneyUsers) {
148
- const todayPortfolio = this.todayPortfolios[userId];
149
- const yesterdayPortfolio = this.yesterdayPortfolios[userId];
150
-
151
- if (todayPortfolio && yesterdayPortfolio) {
152
- const todaySectorInvestment = this.calculateSectorInvestment(todayPortfolio);
153
- const yesterdaySectorInvestment = this.calculateSectorInvestment(yesterdayPortfolio);
154
-
155
- // Calculate change in investment per sector
156
- const allSectors = new Set([...Object.keys(todaySectorInvestment), ...Object.keys(yesterdaySectorInvestment)]);
157
- for (const sector of allSectors) {
158
- const todayAmount = todaySectorInvestment[sector] || 0;
159
- const yesterdayAmount = yesterdaySectorInvestment[sector] || 0;
160
- const change = todayAmount - yesterdayAmount;
161
-
162
- // Only record if there is a change
163
- if (change !== 0) {
164
- if (!sectorFlow[sector]) {
165
- sectorFlow[sector] = 0;
166
- }
167
- sectorFlow[sector] += change;
168
- }
169
- }
170
- }
171
- }
172
-
173
- // Return only sectors with non-zero flow if desired, or the full object
174
- const filteredFlow = {};
175
- for (const sector in sectorFlow) {
176
- if (sectorFlow[sector] !== 0) {
177
- filteredFlow[sector] = sectorFlow[sector];
178
- }
179
- }
180
- console.log("Final Smart Money Flow:", filteredFlow);
181
- return { smart_money_flow: filteredFlow };
180
+ async getResult() { return null; }
181
+ reset() {
182
+ this.smartMoneyUsers.clear();
183
+ this.sectorMap = null;
184
+ this.todayPortfolios = {};
185
+ this.yesterdayPortfolios = {};
182
186
  }
183
187
  }
184
188
 
185
- module.exports = SmartMoneyFlow;
189
+ module.exports = SmartMoneyFlow;
@@ -1,23 +1,25 @@
1
1
  /**
2
2
  * @fileoverview Calculates a rolling 90-day "Investor Score" (IS) for each normal user.
3
3
  *
4
- * --- META REFACTOR ---
5
- * This calculation is now `type: "meta"` to consume in-memory dependencies.
6
- * It runs ONCE per day, receives the in-memory cache, and must
7
- * perform its own user data streaming.
4
+ * --- META REFACTOR (v2) ---
5
+ * This calculation is now `type: "meta"` and expects its dependencies
6
+ * to be fetched by the pass runner and provided to its `process` method.
7
+ * It also dynamically fetches its own historical user data.
8
8
  */
9
9
 
10
10
  const { Firestore } = require('@google-cloud/firestore');
11
11
  const firestore = new Firestore();
12
- const { loadAllPriceData, getDailyPriceChange } = require('../../../utils/price_data_provider');
13
- const { loadInstrumentMappings, getInstrumentSectorMap } = require('../../../utils/sector_mapping_provider');
14
- const { loadDataByRefs, getPortfolioPartRefs, loadFullDayMap } = require('../../../../bulltrackers-module/functions/computation-system/utils/data_loader');// Config
15
- const NUM_SHARDS = 50; // Must match the number of shards to read/write
12
+ // NOTE: Corrected relative path for data_loader
13
+ const { loadDataByRefs, getPortfolioPartRefs, loadFullDayMap } = require('../../../../bulltrackers-module/functions/computation-system/utils/data_loader');
14
+ const { loadAllPriceData, getInstrumentSectorMap } = require('../../../utils/price_data_provider');
15
+
16
+ // Config
17
+ const NUM_SHARDS = 50;
16
18
  const ROLLING_DAYS = 90;
17
- const SHARD_COLLECTION_NAME = 'user_profile_history'; // The collection to store sharded history
18
- const PNL_TRACKER_CALC_ID = 'user-profitability-tracker'; // The calc to read PNL from
19
+ const SHARD_COLLECTION_NAME = 'user_profile_history';
20
+ const PNL_TRACKER_CALC_ID = 'user-profitability-tracker';
19
21
 
20
- // Helper: stable shard index for numeric or string IDs
22
+ // Helper: stable shard index
21
23
  function getShardIndex(id) {
22
24
  const n = parseInt(id, 10);
23
25
  if (!Number.isNaN(n)) return Math.abs(n) % NUM_SHARDS;
@@ -30,16 +32,15 @@ function getShardIndex(id) {
30
32
  }
31
33
 
32
34
  class UserInvestmentProfile {
33
- constructor() {
34
- // --- META REFACTOR ---
35
- // All state is now managed inside the `process` function.
36
- // The constructor, getResult, and reset methods are no longer used
37
- // by the meta-runner, but we leave them for compatibility.
38
- // --- END REFACTOR ---
39
- }
40
35
 
41
- // ... [Heuristic calculation functions _calculateRiskAndDivScore, _calculateDisciplineScore, _calculateMarketTimingScore] ...
42
- // These helper functions remain identical to your original file.
36
+ /**
37
+ * (NEW) Statically declare dependencies.
38
+ * The runner will fetch these results for the *current date*
39
+ * and pass them to `process`.
40
+ */
41
+ static getDependencies() {
42
+ return [PNL_TRACKER_CALC_ID];
43
+ }
43
44
 
44
45
  /**
45
46
  * HEURISTIC 1: Risk & Diversification Score (0-10).
@@ -59,7 +60,7 @@ class UserInvestmentProfile {
59
60
  for (const pos of positions) {
60
61
  const invested = pos.InvestedAmount || pos.Amount || 0;
61
62
  const netProfit = ('NetProfit' in pos) ? pos.NetProfit : (pos.ProfitAndLoss || 0); // decimal % return
62
- const ret = invested > 0 ? (netProfit) : netProfit; // if invested==0 we still include ret but weight 0
63
+ const ret = invested > 0 ? (netProfit) : netProfit;
63
64
 
64
65
  weightedRetSum += ret * invested;
65
66
  weightedRetSqSum += (ret * ret) * invested;
@@ -69,29 +70,19 @@ class UserInvestmentProfile {
69
70
  sectors.add(sectorMap[pos.InstrumentID] || 'N/A');
70
71
  }
71
72
 
72
- // Weighted mean & variance of returns
73
73
  const meanReturn = totalInvested > 0 ? (weightedRetSum / totalInvested) : 0;
74
74
  const meanReturnSq = totalInvested > 0 ? (weightedRetSqSum / totalInvested) : (meanReturn * meanReturn);
75
75
  const variance = Math.max(0, meanReturnSq - (meanReturn * meanReturn));
76
76
  const stdReturn = Math.sqrt(variance);
77
-
78
- // dispersion proxy: mean / std (if std is zero we treat as neutral 0)
79
77
  let dispersionRiskProxy = stdReturn > 0 ? meanReturn / stdReturn : 0;
80
-
81
- // cap and map dispersion proxy to [0..10].
82
- // dispersionRiskProxy can be outside [-2..4], clamp to reasonable bounds first.
83
78
  const capped = Math.max(-2, Math.min(4, dispersionRiskProxy));
84
- const scoreSharpe = ((capped + 2) / 6) * 10; // maps [-2..4] -> [0..10]
85
-
86
- // Sector diversification (monotonic - diminishing returns)
79
+ const scoreSharpe = ((capped + 2) / 6) * 10;
87
80
  const sectorCount = sectors.size;
88
81
  let scoreDiversification = 0;
89
82
  if (sectorCount === 1) scoreDiversification = 0;
90
83
  else if (sectorCount <= 4) scoreDiversification = 5;
91
84
  else if (sectorCount <= 7) scoreDiversification = 8;
92
85
  else scoreDiversification = 10;
93
-
94
- // Position sizing / concentration penalty
95
86
  const concentrationRatio = totalInvested > 0 ? (maxPosition / totalInvested) : 0;
96
87
  let scoreSizing = 0;
97
88
  if (concentrationRatio > 0.8) scoreSizing = 0;
@@ -99,7 +90,6 @@ class UserInvestmentProfile {
99
90
  else if (concentrationRatio > 0.3) scoreSizing = 5;
100
91
  else if (concentrationRatio > 0.15) scoreSizing = 8;
101
92
  else scoreSizing = 10;
102
-
103
93
  const final = (scoreSharpe * 0.4) + (scoreDiversification * 0.3) + (scoreSizing * 0.3);
104
94
  return Math.max(0, Math.min(10, final));
105
95
  }
@@ -110,42 +100,34 @@ class UserInvestmentProfile {
110
100
  _calculateDisciplineScore(yesterdayPortfolio = {}, todayPortfolio = {}) {
111
101
  const yPositions = yesterdayPortfolio.AggregatedPositions || [];
112
102
  const tPositions = new Map((todayPortfolio.AggregatedPositions || []).map(p => [p.PositionID, p]));
113
-
114
103
  if (yPositions.length === 0) {
115
- return 5; // neutral if nothing to judge
104
+ return 5; // neutral
116
105
  }
117
-
118
106
  let eventPoints = 0;
119
107
  let eventCount = 0;
120
-
121
108
  for (const yPos of yPositions) {
122
109
  const profitAndLoss = ('ProfitAndLoss' in yPos) ? yPos.ProfitAndLoss : (yPos.NetProfit || 0);
123
110
  const invested = yPos.InvestedAmount || yPos.Amount || 0;
124
- const pnlPercent = profitAndLoss; // This is already the decimal % return
125
-
111
+ const pnlPercent = profitAndLoss;
126
112
  const tPos = tPositions.get(yPos.PositionID);
127
-
128
113
  if (!tPos) {
129
- // Closed position
130
114
  eventCount++;
131
- if (pnlPercent < -0.05) eventPoints += 10; // cut loser (good)
132
- else if (pnlPercent > 0.20) eventPoints += 8; // took big profit (good)
133
- else if (pnlPercent > 0 && pnlPercent < 0.05) eventPoints += 2; // paper hands (bad)
134
- else eventPoints += 5; // neutral close
115
+ if (pnlPercent < -0.05) eventPoints += 10;
116
+ else if (pnlPercent > 0.20) eventPoints += 8;
117
+ else if (pnlPercent > 0 && pnlPercent < 0.05) eventPoints += 2;
118
+ else eventPoints += 5;
135
119
  } else {
136
- // Held or modified
137
120
  if (pnlPercent < -0.10) {
138
121
  eventCount++;
139
122
  const tInvested = tPos.InvestedAmount || tPos.Amount || 0;
140
- if (tInvested > invested) eventPoints += 0; // averaged down (very poor)
141
- else eventPoints += 3; // held loser (poor)
123
+ if (tInvested > invested) eventPoints += 0;
124
+ else eventPoints += 3;
142
125
  } else if (pnlPercent > 0.15) {
143
126
  eventCount++;
144
- eventPoints += 10; // held/added to winner (good)
127
+ eventPoints += 10;
145
128
  }
146
129
  }
147
130
  }
148
-
149
131
  const avg = (eventCount > 0) ? (eventPoints / eventCount) : 5;
150
132
  return Math.max(0, Math.min(10, avg));
151
133
  }
@@ -156,44 +138,33 @@ class UserInvestmentProfile {
156
138
  _calculateMarketTimingScore(yesterdayPortfolio = {}, todayPortfolio = {}, priceMap) {
157
139
  const yIds = new Set((yesterdayPortfolio.AggregatedPositions || []).map(p => p.PositionID));
158
140
  const newPositions = (todayPortfolio.AggregatedPositions || []).filter(p => !yIds.has(p.PositionID));
159
-
160
141
  if (newPositions.length === 0) return 5;
161
-
162
142
  let timingPoints = 0;
163
143
  let timingCount = 0;
164
-
165
144
  for (const tPos of newPositions) {
166
145
  const prices = priceMap[tPos.InstrumentID];
167
146
  if (!prices) continue;
168
-
169
- // Accept prices as either array or {date:price} map; build sorted array of prices
170
147
  let historyPrices = [];
171
148
  if (Array.isArray(prices)) {
172
- // assume array of numbers or objects with .price/.close
173
149
  historyPrices = prices
174
150
  .map(p => (typeof p === 'number' ? p : (p.price || p.close || null)))
175
151
  .filter(v => v != null);
176
152
  } else {
177
- // object keyed by date -> price
178
153
  const entries = Object.keys(prices)
179
154
  .map(d => ({ d, p: prices[d] }))
180
155
  .filter(e => e.p != null)
181
156
  .sort((a, b) => new Date(a.d) - new Date(b.d));
182
157
  historyPrices = entries.map(e => e.p);
183
158
  }
184
-
185
159
  const last30 = historyPrices.slice(-30);
186
160
  if (last30.length < 2) continue;
187
-
188
161
  const minPrice = Math.min(...last30);
189
162
  const maxPrice = Math.max(...last30);
190
163
  const openRate = tPos.OpenRate;
191
164
  const range = maxPrice - minPrice;
192
165
  if (!isFinite(range) || range === 0) continue;
193
-
194
- let proximity = (openRate - minPrice) / range; // 0 = at low, 1 = at high
195
- proximity = Math.max(0, Math.min(1, proximity)); // clamp to [0,1]
196
-
166
+ let proximity = (openRate - minPrice) / range;
167
+ proximity = Math.max(0, Math.min(1, proximity));
197
168
  timingCount++;
198
169
  if (proximity < 0.2) timingPoints += 10;
199
170
  else if (proximity < 0.4) timingPoints += 8;
@@ -201,30 +172,32 @@ class UserInvestmentProfile {
201
172
  else if (proximity > 0.7) timingPoints += 3;
202
173
  else timingPoints += 5;
203
174
  }
204
-
205
175
  const avg = (timingCount > 0) ? (timingPoints / timingCount) : 5;
206
176
  return Math.max(0, Math.min(10, avg));
207
177
  }
208
-
209
-
178
+
210
179
  /**
211
- * PROCESS: META REFACTOR
212
- * This now runs ONCE, loads all data, streams users, and returns one big result.
180
+ * REFACTORED PROCESS METHOD
181
+ * @param {string} dateStr - The date being processed (YYYY-MM-DD).
182
+ * @param {object} dependencies - Shared dependencies (db, logger, rootData, etc.).
183
+ * @param {object} config - The computation system configuration.
184
+ * @param {object} fetchedDependencies - An object containing the results of this
185
+ * calc's dependencies (e.g., { 'user-profitability-tracker': ... }).
213
186
  */
214
- async process(dateStr, dependencies, config, computedDependencies) {
215
- const { logger, db, rootData, calculationUtils } = dependencies;
187
+ async process(dateStr, dependencies, config, fetchedDependencies) {
188
+ const { logger, db, rootData } = dependencies;
216
189
  const { portfolioRefs } = rootData;
217
190
 
218
191
  logger.log('INFO', '[UserInvestmentProfile] Starting meta-process...');
219
192
 
220
- // 1. Get Pass 1 dependency from in-memory cache
221
- const pnlTrackerResult = computedDependencies[PNL_TRACKER_CALC_ID];
193
+ // 1. Get Pass 1 dependency from the `fetchedDependencies` argument
194
+ const pnlTrackerResult = fetchedDependencies[PNL_TRACKER_CALC_ID];
222
195
  if (!pnlTrackerResult || !pnlTrackerResult.daily_pnl_map) {
223
- logger.log('WARN', `[UserInvestmentProfile] Missing in-memory dependency '${PNL_TRACKER_CALC_ID}'. Aborting.`);
196
+ logger.log('WARN', `[UserInvestmentProfile] Dependency '${PNL_TRACKER_CALC_ID}' not found in fetchedDependencies. Aborting.`);
224
197
  return null; // Return null to signal failure
225
198
  }
226
199
  const pnlScores = pnlTrackerResult.daily_pnl_map;
227
- logger.log('INFO', `[UserInvestmentProfile] Received ${Object.keys(pnlScores).length} PNL scores in-memory.`);
200
+ logger.log('INFO', `[UserInvestmentProfile] Received ${Object.keys(pnlScores).length} PNL scores from dependency.`);
228
201
 
229
202
  // 2. Load external dependencies (prices, sectors)
230
203
  const [priceMap, sectorMap] = await Promise.all([
@@ -254,11 +227,10 @@ class UserInvestmentProfile {
254
227
 
255
228
  for (const uid in todayPortfoliosChunk) {
256
229
  const pToday = todayPortfoliosChunk[uid];
257
- if (!pToday || !pToday.AggregatedPositions) continue; // Skip speculators or empty
230
+ if (!pToday || !pToday.AggregatedPositions) continue; // Skip
258
231
 
259
232
  const pYesterday = yesterdayPortfolios[uid] || {};
260
233
 
261
- // Run the heuristic calculations
262
234
  const score_rd = this._calculateRiskAndDivScore(pToday, sectorMap);
263
235
  const score_disc = this._calculateDisciplineScore(pYesterday, pToday);
264
236
  const score_time = this._calculateMarketTimingScore(pYesterday, pToday, priceMap);
@@ -272,15 +244,12 @@ class UserInvestmentProfile {
272
244
  }
273
245
  logger.log('INFO', `[UserInvestmentProfile] Calculated daily scores for ${Object.keys(dailyUserScores).length} users.`);
274
246
 
275
- // --- 5. GETRESULT LOGIC IS NOW INSIDE PROCESS ---
276
- // (This is the logic from your original getResult())
277
-
247
+ // 5. GETRESULT LOGIC (Final aggregation)
278
248
  const shardedResults = {};
279
249
  for (let i = 0; i < NUM_SHARDS; i++) {
280
250
  const shardKey = `${SHARD_COLLECTION_NAME}_shard_${i}`;
281
251
  shardedResults[shardKey] = { profiles: {}, lastUpdated: dateStr };
282
252
  }
283
-
284
253
  const dailyInvestorScoreMap = {};
285
254
 
286
255
  // Fetch existing shards in parallel
@@ -296,16 +265,13 @@ class UserInvestmentProfile {
296
265
  for (const userId of Object.keys(dailyUserScores)) {
297
266
  const shardIndex = getShardIndex(userId);
298
267
  const scores = dailyUserScores[userId];
299
-
300
268
  const existingProfiles = existingShards[shardIndex] || {};
301
269
  const history = (existingProfiles[userId] || []).slice(); // clone
302
-
303
270
  history.push({
304
271
  date: dateStr,
305
272
  ...scores,
306
273
  pnl: (pnlScores[userId] || 0)
307
274
  });
308
-
309
275
  const newHistory = history.slice(-ROLLING_DAYS);
310
276
 
311
277
  // compute rolling averages
@@ -340,18 +306,8 @@ class UserInvestmentProfile {
340
306
  };
341
307
  }
342
308
 
343
- /**
344
- * getResult is no longer used by the meta-runner.
345
- */
346
- async getResult() {
347
- return null;
348
- }
349
-
350
- /**
351
- * reset is no longer used by the meta-runner.
352
- */
353
- reset() {
354
- }
309
+ async getResult() { return null; }
310
+ reset() {}
355
311
  }
356
312
 
357
313
  module.exports = UserInvestmentProfile;