aiden-shared-calculations-unified 1.0.36 → 1.0.38

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,19 +1,17 @@
1
1
  /**
2
2
  * @fileoverview Calculates a rolling 90-day "Investor Score" (IS) for each normal user.
3
- * Heuristic engine (not an academic finance model). Outputs:
4
- * - sharded_user_profile: { <shardKey>: { profiles: { userId: [history...] }, lastUpdated } }
5
- * - daily_investor_scores: { userId: finalIS }
6
3
  *
7
- * Notes:
8
- * - NetProfit / ProfitAndLoss fields are assumed to be percent returns in decimal (e.g. 0.03 = +3%).
9
- * - The "Sharpe" used here is a cross-sectional dispersion proxy computed over position returns,
10
- * weighted by invested amounts. It's renamed/treated as a dispersionRiskProxy in comments.
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.
11
8
  */
12
9
 
13
10
  const { Firestore } = require('@google-cloud/firestore');
14
11
  const firestore = new Firestore();
15
- const { loadAllPriceData } = require('../../../utils/price_data_provider');
16
- const { getInstrumentSectorMap, loadInstrumentMappings } = require('../../../utils/sector_mapping_provider');
12
+ const { loadAllPriceData, getDailyPriceChange } = require('../../../utils/price_data_provider');
13
+ const { loadInstrumentMappings, getInstrumentSectorMap } = require('../../../utils/sector_mapping_provider');
14
+ const { loadDataByRefs } = require('../../../../bulltrackers-module/functions/computation-system/utils/data_loader'); // Adjust path as needed
17
15
 
18
16
  // Config
19
17
  const NUM_SHARDS = 50; // Must match the number of shards to read/write
@@ -25,88 +23,30 @@ const PNL_TRACKER_CALC_ID = 'user-profitability-tracker'; // The calc to read PN
25
23
  function getShardIndex(id) {
26
24
  const n = parseInt(id, 10);
27
25
  if (!Number.isNaN(n)) return Math.abs(n) % NUM_SHARDS;
28
- // simple deterministic string hash fallback for non-numeric IDs (UUIDs)
29
26
  let h = 0;
30
27
  for (let i = 0; i < id.length; i++) {
31
28
  h = ((h << 5) - h) + id.charCodeAt(i);
32
- h |= 0; // keep 32-bit
29
+ h |= 0;
33
30
  }
34
31
  return Math.abs(h) % NUM_SHARDS;
35
32
  }
36
33
 
37
34
  class UserInvestmentProfile {
38
35
  constructor() {
39
- // will hold today's per-user raw heuristic scores
40
- this.dailyUserScores = {}; // { userId: { score_rd, score_disc, score_time } }
41
-
42
- // cached dependencies
43
- this.priceMap = null;
44
- this.sectorMap = null;
45
- this.pnlScores = null; // { userId: dailyPnlDecimal }
46
- this.dates = {};
47
- this.dependenciesLoaded = false;
48
-
49
- // --- START MODIFICATION ---
50
- // Flag to track if dependencies loaded successfully
51
- this.dependencyLoadedSuccess = false;
52
- // --- END MODIFICATION ---
36
+ // --- META REFACTOR ---
37
+ // All state is now managed inside the `process` function.
38
+ // The constructor, getResult, and reset methods are no longer used
39
+ // by the meta-runner, but we leave them for compatibility.
40
+ // --- END REFACTOR ---
53
41
  }
54
42
 
55
- /**
56
- * Loads external dependencies once per run.
57
- */
58
- async _loadDependencies(context, dependencies) {
59
- if (this.dependenciesLoaded) return;
60
-
61
- const { db, logger } = dependencies;
62
- const { todayDateStr } = context;
63
-
64
- if (logger) logger.log('INFO', '[UserInvestmentProfile] Loading dependencies...');
65
-
66
- // load price data and sector mapping in parallel
67
- const [priceData, sectorData] = await Promise.all([
68
- loadAllPriceData(),
69
- getInstrumentSectorMap()
70
- ]);
71
- this.priceMap = priceData || {};
72
- this.sectorMap = sectorData || {};
73
-
74
- // load PNL map (daily percent returns per user) from PNL calc
75
- this.pnlScores = {};
76
- try {
77
- const pnlCalcRef = db.collection(context.config.resultsCollection).doc(todayDateStr)
78
- .collection(context.config.resultsSubcollection).doc('pnl')
79
- .collection(context.config.computationsSubcollection).doc(PNL_TRACKER_CALC_ID);
80
-
81
- const pnlSnap = await pnlCalcRef.get();
82
-
83
- // --- START MODIFICATION ---
84
- // Check for existence of the doc AND the data within it
85
- if (pnlSnap.exists && pnlSnap.data().daily_pnl_map) {
86
- this.pnlScores = pnlSnap.data().daily_pnl_map || {};
87
- if (logger) logger.log('INFO', `[UserInvestmentProfile] Loaded ${Object.keys(this.pnlScores).length} PNL scores.`);
88
- this.dependencyLoadedSuccess = true; // Set success flag
89
- } else {
90
- if (logger) logger.log('WARN', `[UserInvestmentProfile] Could not find PNL scores dependency for ${todayDateStr}. PNL score will be 0. Aborting profile calculation.`);
91
- this.dependencyLoadedSuccess = false; // Set failure flag
92
- }
93
- // --- END MODIFICATION ---
94
-
95
- } catch (e) {
96
- if (logger) logger.log('ERROR', `[UserInvestmentProfile] Failed to load PNL scores.`, { error: e.message });
97
- this.dependencyLoadedSuccess = false; // Set failure flag on error
98
- }
99
-
100
- this.dependenciesLoaded = true;
101
- if (logger) logger.log('INFO', '[UserInvestmentProfile] All dependencies loaded.');
102
- }
103
-
104
- // ... [Heuristic calculation functions _calculateRiskAndDivScore, _calculateDisciplineScore, _calculateMarketTimingScore are unchanged] ...
43
+ // ... [Heuristic calculation functions _calculateRiskAndDivScore, _calculateDisciplineScore, _calculateMarketTimingScore] ...
44
+ // These helper functions remain identical to your original file.
105
45
 
106
46
  /**
107
47
  * HEURISTIC 1: Risk & Diversification Score (0-10).
108
48
  */
109
- _calculateRiskAndDivScore(todayPortfolio) {
49
+ _calculateRiskAndDivScore(todayPortfolio, sectorMap) {
110
50
  if (!todayPortfolio.AggregatedPositions || todayPortfolio.AggregatedPositions.length === 0) {
111
51
  return 5; // neutral
112
52
  }
@@ -128,7 +68,7 @@ class UserInvestmentProfile {
128
68
  totalInvested += invested;
129
69
  if (invested > maxPosition) maxPosition = invested;
130
70
 
131
- sectors.add(this.sectorMap[pos.InstrumentID] || 'N/A');
71
+ sectors.add(sectorMap[pos.InstrumentID] || 'N/A');
132
72
  }
133
73
 
134
74
  // Weighted mean & variance of returns
@@ -215,7 +155,7 @@ class UserInvestmentProfile {
215
155
  /**
216
156
  * HEURISTIC 3: Market Timing Score (0-10).
217
157
  */
218
- _calculateMarketTimingScore(yesterdayPortfolio = {}, todayPortfolio = {}) {
158
+ _calculateMarketTimingScore(yesterdayPortfolio = {}, todayPortfolio = {}, priceMap) {
219
159
  const yIds = new Set((yesterdayPortfolio.AggregatedPositions || []).map(p => p.PositionID));
220
160
  const newPositions = (todayPortfolio.AggregatedPositions || []).filter(p => !yIds.has(p.PositionID));
221
161
 
@@ -225,7 +165,7 @@ class UserInvestmentProfile {
225
165
  let timingCount = 0;
226
166
 
227
167
  for (const tPos of newPositions) {
228
- const prices = this.priceMap[tPos.InstrumentID];
168
+ const prices = priceMap[tPos.InstrumentID];
229
169
  if (!prices) continue;
230
170
 
231
171
  // Accept prices as either array or {date:price} map; build sorted array of prices
@@ -267,98 +207,105 @@ class UserInvestmentProfile {
267
207
  const avg = (timingCount > 0) ? (timingPoints / timingCount) : 5;
268
208
  return Math.max(0, Math.min(10, avg));
269
209
  }
270
-
271
- /**
272
- * PROCESS: called per-user per-day to compute and store today's heuristics.
273
- */
274
- async process(todayPortfolio, yesterdayPortfolio, userId, context, todayInsights, yesterdayInsights, todaySocial, yesterdaySocial) {
275
- // run only for normal users with portfolios
276
- if (!todayPortfolio || !todayPortfolio.AggregatedPositions) return;
277
210
 
278
- if (!this.dependenciesLoaded) {
279
- await this._loadDependencies(context, context.dependencies);
280
- this.dates.today = context.todayDateStr;
281
- }
282
-
283
- // --- START MODIFICATION ---
284
- // If dependencies failed to load (e.g., PNL doc was missing), stop processing.
285
- if (!this.dependencyLoadedSuccess) {
286
- return;
287
- }
288
- // --- END MODIFICATION ---
289
211
 
290
- const yPort = yesterdayPortfolio || {};
212
+ /**
213
+ * PROCESS: META REFACTOR
214
+ * This now runs ONCE, loads all data, streams users, and returns one big result.
215
+ */
216
+ async process(dateStr, dependencies, config, computedDependencies) {
217
+ const { logger, db, rootData, calculationUtils } = dependencies;
218
+ const { portfolioRefs } = rootData;
291
219
 
292
- const score_rd = this._calculateRiskAndDivScore(todayPortfolio);
293
- const score_disc = this._calculateDisciplineScore(yPort, todayPortfolio);
294
- const score_time = this._calculateMarketTimingScore(yPort, todayPortfolio);
220
+ logger.log('INFO', '[UserInvestmentProfile] Starting meta-process...');
295
221
 
296
- this.dailyUserScores[userId] = {
297
- score_rd,
298
- score_disc,
299
- score_time
300
- };
301
- }
222
+ // 1. Get Pass 1 dependency from in-memory cache
223
+ const pnlTrackerResult = computedDependencies[PNL_TRACKER_CALC_ID];
224
+ if (!pnlTrackerResult || !pnlTrackerResult.daily_pnl_map) {
225
+ logger.log('WARN', `[UserInvestmentProfile] Missing in-memory dependency '${PNL_TRACKER_CALC_ID}'. Aborting.`);
226
+ return null; // Return null to signal failure
227
+ }
228
+ const pnlScores = pnlTrackerResult.daily_pnl_map;
229
+ logger.log('INFO', `[UserInvestmentProfile] Received ${Object.keys(pnlScores).length} PNL scores in-memory.`);
302
230
 
303
- /**
304
- * GETRESULT: Aggregate into rolling 90-day history, compute avg components and final IS.
305
- */
306
- async getResult() {
307
- // --- START MODIFICATION ---
308
- // If dependencies failed, return null to trigger backfill.
309
- if (!this.dependencyLoadedSuccess) {
310
- // Logger might not be available here, use console.warn
311
- console.warn('[UserInvestmentProfile] Skipping getResult as dependency (pnl-tracker) failed.');
231
+ // 2. Load external dependencies (prices, sectors)
232
+ const [priceMap, sectorMap] = await Promise.all([
233
+ loadAllPriceData(),
234
+ getInstrumentSectorMap()
235
+ ]);
236
+ if (!priceMap || !sectorMap) {
237
+ logger.log('ERROR', '[UserInvestmentProfile] Failed to load priceMap or sectorMap.');
312
238
  return null;
313
239
  }
314
240
 
315
- // If no users were processed (e.g., all were filtered out), return null.
316
- if (Object.keys(this.dailyUserScores).length === 0) {
317
- console.warn('[UserInvestmentProfile] No daily user scores were calculated. Returning null for backfill.');
318
- return null;
241
+ // 3. Load "yesterday's" portfolio data for comparison
242
+ const yesterdayDate = new Date(dateStr + 'T00:00:00Z');
243
+ yesterdayDate.setUTCDate(yesterdayDate.getUTCDate() - 1);
244
+ const yesterdayStr = yesterdayDate.toISOString().slice(0, 10);
245
+ const yesterdayRefs = await calculationUtils.getPortfolioPartRefs(config, dependencies, yesterdayStr);
246
+ const yesterdayPortfolios = await loadFullDayMap(config, dependencies, yesterdayRefs);
247
+ logger.log('INFO', `[UserInvestmentProfile] Loaded ${yesterdayRefs.length} part refs for yesterday.`);
248
+
249
+ // 4. Stream "today's" portfolio data and process
250
+ const batchSize = config.partRefBatchSize || 10;
251
+ const dailyUserScores = {}; // Local state for this run
252
+
253
+ for (let i = 0; i < portfolioRefs.length; i += batchSize) {
254
+ const batchRefs = portfolioRefs.slice(i, i + batchSize);
255
+ const todayPortfoliosChunk = await loadDataByRefs(config, dependencies, batchRefs);
256
+
257
+ for (const uid in todayPortfoliosChunk) {
258
+ const pToday = todayPortfoliosChunk[uid];
259
+ if (!pToday || !pToday.AggregatedPositions) continue; // Skip speculators or empty
260
+
261
+ const pYesterday = yesterdayPortfolios[uid] || {};
262
+
263
+ // Run the heuristic calculations
264
+ const score_rd = this._calculateRiskAndDivScore(pToday, sectorMap);
265
+ const score_disc = this._calculateDisciplineScore(pYesterday, pToday);
266
+ const score_time = this._calculateMarketTimingScore(pYesterday, pToday, priceMap);
267
+
268
+ dailyUserScores[uid] = {
269
+ score_rd,
270
+ score_disc,
271
+ score_time
272
+ };
273
+ }
319
274
  }
320
- // --- END MODIFICATION ---
321
-
322
- const todayStr = this.dates.today || (new Date()).toISOString().slice(0, 10);
275
+ logger.log('INFO', `[UserInvestmentProfile] Calculated daily scores for ${Object.keys(dailyUserScores).length} users.`);
276
+
277
+ // --- 5. GETRESULT LOGIC IS NOW INSIDE PROCESS ---
278
+ // (This is the logic from your original getResult())
323
279
 
324
- // prepare sharded output objects with profiles container (Option A)
325
280
  const shardedResults = {};
326
281
  for (let i = 0; i < NUM_SHARDS; i++) {
327
282
  const shardKey = `${SHARD_COLLECTION_NAME}_shard_${i}`;
328
- shardedResults[shardKey] = { profiles: {}, lastUpdated: todayStr };
283
+ shardedResults[shardKey] = { profiles: {}, lastUpdated: dateStr };
329
284
  }
330
285
 
331
286
  const dailyInvestorScoreMap = {};
332
287
 
333
- // fetch existing shards in parallel
288
+ // Fetch existing shards in parallel
334
289
  const shardPromises = [];
335
290
  for (let i = 0; i < NUM_SHARDS; i++) {
336
291
  const docRef = firestore.collection(SHARD_COLLECTION_NAME).doc(`${SHARD_COLLECTION_NAME}_shard_${i}`);
337
292
  shardPromises.push(docRef.get());
338
293
  }
339
294
  const shardSnapshots = await Promise.all(shardPromises);
340
-
341
- // Build existingShards map of profiles for quick access
342
- const existingShards = shardSnapshots.map((snap, idx) => {
343
- if (!snap.exists) return {}; // no profiles
344
- const data = snap.data() || {};
345
- return data.profiles || {};
346
- });
347
-
348
- // process users
349
- for (const userId of Object.keys(this.dailyUserScores)) {
295
+ const existingShards = shardSnapshots.map((snap) => (snap.exists ? snap.data().profiles : {}));
296
+
297
+ // Process users
298
+ for (const userId of Object.keys(dailyUserScores)) {
350
299
  const shardIndex = getShardIndex(userId);
351
- const scores = this.dailyUserScores[userId];
300
+ const scores = dailyUserScores[userId];
352
301
 
353
- // fetch existing history for this user (if present)
354
302
  const existingProfiles = existingShards[shardIndex] || {};
355
- // clone to avoid mutating snapshot data directly
356
- const history = (existingProfiles[userId] || []).slice();
303
+ const history = (existingProfiles[userId] || []).slice(); // clone
357
304
 
358
305
  history.push({
359
- date: todayStr,
306
+ date: dateStr,
360
307
  ...scores,
361
- pnl: (this.pnlScores && (userId in this.pnlScores)) ? this.pnlScores[userId] : 0
308
+ pnl: (pnlScores[userId] || 0)
362
309
  });
363
310
 
364
311
  const newHistory = history.slice(-ROLLING_DAYS);
@@ -377,36 +324,35 @@ class UserInvestmentProfile {
377
324
  avg_time /= N;
378
325
  avg_pnl /= N;
379
326
 
380
- // Normalize PNL: avg_pnl is decimal percent (0.005 -> 0.5%). Map to 0-10 scale:
381
- // multiply by 1000 (0.005 -> 5). Clamp to [-10, 10] to avoid outliers.
382
327
  const normalizedPnl = Math.max(-10, Math.min(10, avg_pnl * 1000));
383
-
384
- // Final IS (weights): discipline 40%, risk/div 30%, timing 20%, pnl 10%
385
328
  const finalISRaw = (avg_disc * 0.4) + (avg_rd * 0.3) + (avg_time * 0.2) + (normalizedPnl * 0.1);
386
329
  const finalIS = Math.max(0, Math.min(10, finalISRaw));
387
330
 
388
- // store in prepared shard result under 'profiles'
389
331
  const shardKey = `${SHARD_COLLECTION_NAME}_shard_${shardIndex}`;
390
332
  shardedResults[shardKey].profiles[userId] = newHistory;
391
-
392
- // also set the daily investor score
393
333
  dailyInvestorScoreMap[userId] = finalIS;
394
334
  }
395
-
335
+
336
+ logger.log('INFO', `[UserInvestmentProfile] Finalized IS scores for ${Object.keys(dailyInvestorScoreMap).length} users.`);
337
+
338
+ // Return the final result object
396
339
  return {
397
340
  sharded_user_profile: shardedResults,
398
341
  daily_investor_scores: dailyInvestorScoreMap
399
342
  };
400
343
  }
401
344
 
345
+ /**
346
+ * getResult is no longer used by the meta-runner.
347
+ */
348
+ async getResult() {
349
+ return null;
350
+ }
351
+
352
+ /**
353
+ * reset is no longer used by the meta-runner.
354
+ */
402
355
  reset() {
403
- this.dailyUserScores = {};
404
- this.dependenciesLoaded = false;
405
- this.priceMap = null;
406
- this.sectorMap = null;
407
- this.pnlScores = null;
408
- this.dates = {};
409
- this.dependencyLoadedSuccess = false; // <-- MODIFICATION
410
356
  }
411
357
  }
412
358
 
@@ -0,0 +1,63 @@
1
+ /**
2
+ * @fileoverview Calculates the average percentage increase in allocation
3
+ * specifically towards assets already held on the previous day.
4
+ */
5
+
6
+ class ReallocationIncreasePercentage {
7
+ constructor() {
8
+ this.accumulatedIncreasePercentage = 0;
9
+ this.userCount = 0;
10
+ }
11
+
12
+ process(todayPortfolio, yesterdayPortfolio, userId) {
13
+ if (!todayPortfolio || !yesterdayPortfolio || !todayPortfolio.AggregatedPositions || !yesterdayPortfolio.AggregatedPositions) {
14
+ // Requires AggregatedPositions which contain the 'Invested' percentage
15
+ return;
16
+ }
17
+
18
+ const yesterdayPositions = new Map(yesterdayPortfolio.AggregatedPositions.map(p => [p.InstrumentID, p]));
19
+ let userTotalIncreasePercentage = 0;
20
+
21
+ for (const todayPos of todayPortfolio.AggregatedPositions) {
22
+ const yesterdayPos = yesterdayPositions.get(todayPos.InstrumentID);
23
+
24
+ // Check if the asset was held yesterday
25
+ if (yesterdayPos) {
26
+ // Ensure 'Invested' property exists and is a number
27
+ const todayInvested = typeof todayPos.Invested === 'number' ? todayPos.Invested : 0;
28
+ const yesterdayInvested = typeof yesterdayPos.Invested === 'number' ? yesterdayPos.Invested : 0;
29
+
30
+ const deltaInvested = todayInvested - yesterdayInvested;
31
+
32
+ // Accumulate only the increases
33
+ if (deltaInvested > 0) {
34
+ userTotalIncreasePercentage += deltaInvested;
35
+ }
36
+ }
37
+ }
38
+
39
+ // Only count users who had positions on both days for this metric
40
+ if (yesterdayPortfolio.AggregatedPositions.length > 0 && todayPortfolio.AggregatedPositions.length > 0) {
41
+ this.accumulatedIncreasePercentage += userTotalIncreasePercentage;
42
+ this.userCount++;
43
+ }
44
+ }
45
+
46
+ getResult() {
47
+ if (this.userCount === 0) {
48
+ return {};
49
+ }
50
+
51
+ return {
52
+ // Calculate the final average directly
53
+ average_reallocation_increase_percentage: this.accumulatedIncreasePercentage / this.userCount
54
+ };
55
+ }
56
+
57
+ reset() {
58
+ this.accumulatedIncreasePercentage = 0;
59
+ this.userCount = 0;
60
+ }
61
+ }
62
+
63
+ module.exports = ReallocationIncreasePercentage;
@@ -0,0 +1,91 @@
1
+ /**
2
+ * @fileoverview Calculates the average stop loss distance (percent and value)
3
+ * for long and short positions, grouped by SECTOR.
4
+ */
5
+ const { getInstrumentSectorMap } = require('../../utils/sector_mapping_provider');
6
+
7
+ class StopLossDistanceBySector {
8
+ constructor() {
9
+ this.instrumentData = {};
10
+ this.instrumentToSector = null;
11
+ }
12
+
13
+ process(portfolioData, yesterdayPortfolio, userId, context) {
14
+ if (!portfolioData || !portfolioData.PublicPositions) return;
15
+
16
+ for (const position of portfolioData.PublicPositions) {
17
+ const { InstrumentID, Leverage, StopLossRate, CurrentRate, IsBuy } = position;
18
+ if (Leverage <= 1 || StopLossRate <= 0.0001 || CurrentRate <= 0) continue;
19
+
20
+ const distance_value = IsBuy ? CurrentRate - StopLossRate : StopLossRate - CurrentRate;
21
+ const distance_percent = (distance_value / CurrentRate) * 100;
22
+
23
+ if (distance_percent > 0) {
24
+ const posType = IsBuy ? 'long' : 'short';
25
+ if (!this.instrumentData[InstrumentID]) this.instrumentData[InstrumentID] = {};
26
+ if (!this.instrumentData[InstrumentID][posType]) {
27
+ this.instrumentData[InstrumentID][posType] = {
28
+ distance_percent_sum: 0,
29
+ distance_value_sum: 0,
30
+ count: 0
31
+ };
32
+ }
33
+ const agg = this.instrumentData[InstrumentID][posType];
34
+ agg.distance_percent_sum += distance_percent;
35
+ agg.distance_value_sum += distance_value;
36
+ agg.count++;
37
+ }
38
+ }
39
+ }
40
+
41
+ async getResult() {
42
+ if (Object.keys(this.instrumentData).length === 0) return {};
43
+ if (!this.instrumentToSector) {
44
+ this.instrumentToSector = await getInstrumentSectorMap();
45
+ }
46
+
47
+ const sectorData = {};
48
+ for (const instrumentId in this.instrumentData) {
49
+ const sector = this.instrumentToSector[instrumentId] || 'N/A';
50
+ if (!sectorData[sector]) sectorData[sector] = {};
51
+
52
+ for (const posType in this.instrumentData[instrumentId]) {
53
+ if (!sectorData[sector][posType]) {
54
+ sectorData[sector][posType] = {
55
+ distance_percent_sum: 0,
56
+ distance_value_sum: 0,
57
+ count: 0
58
+ };
59
+ }
60
+ const source = this.instrumentData[instrumentId][posType];
61
+ const target = sectorData[sector][posType];
62
+ target.distance_percent_sum += source.distance_percent_sum;
63
+ target.distance_value_sum += source.distance_value_sum;
64
+ target.count += source.count;
65
+ }
66
+ }
67
+
68
+ const result = {};
69
+ for (const sector in sectorData) {
70
+ result[sector] = {};
71
+ for (const posType in sectorData[sector]) {
72
+ const data = sectorData[sector][posType];
73
+ // REFACTOR: Perform final calculation and return in standardized format.
74
+ if (data.count > 0) {
75
+ result[sector][posType] = {
76
+ average_distance_percent: data.distance_percent_sum / data.count,
77
+ average_distance_value: data.distance_value_sum / data.count,
78
+ count: data.count
79
+ };
80
+ }
81
+ }
82
+ }
83
+ return result;
84
+ }
85
+
86
+ reset() {
87
+ this.instrumentData = {};
88
+ }
89
+ }
90
+
91
+ module.exports = StopLossDistanceBySector;
@@ -0,0 +1,73 @@
1
+ /**
2
+ * @fileoverview Calculates the average stop loss distance (percent and value)
3
+ * for long and short positions, grouped by TICKER.
4
+ */
5
+ const { loadInstrumentMappings } = require('../../utils/sector_mapping_provider');
6
+
7
+ class StopLossDistanceByTicker {
8
+ constructor() {
9
+ this.instrumentData = {};
10
+ this.instrumentToTicker = null;
11
+ }
12
+
13
+ process(portfolioData, yesterdayPortfolio, userId, context) {
14
+ if (!portfolioData || !portfolioData.PublicPositions) return;
15
+
16
+ for (const position of portfolioData.PublicPositions) {
17
+ const { InstrumentID, Leverage, StopLossRate, CurrentRate, IsBuy } = position;
18
+ if (Leverage <= 1 || StopLossRate <= 0.0001 || CurrentRate <= 0) continue;
19
+
20
+ const distance_value = IsBuy ? CurrentRate - StopLossRate : StopLossRate - CurrentRate;
21
+ const distance_percent = (distance_value / CurrentRate) * 100;
22
+
23
+ if (distance_percent > 0) {
24
+ const posType = IsBuy ? 'long' : 'short';
25
+ if (!this.instrumentData[InstrumentID]) this.instrumentData[InstrumentID] = {};
26
+ if (!this.instrumentData[InstrumentID][posType]) {
27
+ this.instrumentData[InstrumentID][posType] = {
28
+ distance_percent_sum: 0,
29
+ distance_value_sum: 0,
30
+ count: 0
31
+ };
32
+ }
33
+ const agg = this.instrumentData[InstrumentID][posType];
34
+ agg.distance_percent_sum += distance_percent;
35
+ agg.distance_value_sum += distance_value;
36
+ agg.count++;
37
+ }
38
+ }
39
+ }
40
+
41
+ async getResult() {
42
+ if (Object.keys(this.instrumentData).length === 0) return {};
43
+ if (!this.instrumentToTicker) {
44
+ const mappings = await loadInstrumentMappings();
45
+ this.instrumentToTicker = mappings.instrumentToTicker;
46
+ }
47
+
48
+ const result = {};
49
+ for (const instrumentId in this.instrumentData) {
50
+ const ticker = this.instrumentToTicker[instrumentId] || `unknown_${instrumentId}`;
51
+ if (!result[ticker]) result[ticker] = {};
52
+
53
+ for (const posType in this.instrumentData[instrumentId]) {
54
+ const data = this.instrumentData[instrumentId][posType];
55
+ // REFACTOR: Perform final calculation and return in standardized format.
56
+ if (data.count > 0) {
57
+ result[ticker][posType] = {
58
+ average_distance_percent: data.distance_percent_sum / data.count,
59
+ average_distance_value: data.distance_value_sum / data.count,
60
+ count: data.count
61
+ };
62
+ }
63
+ }
64
+ }
65
+ return result;
66
+ }
67
+
68
+ reset() {
69
+ this.instrumentData = {};
70
+ }
71
+ }
72
+
73
+ module.exports = StopLossDistanceByTicker;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aiden-shared-calculations-unified",
3
- "version": "1.0.36",
3
+ "version": "1.0.38",
4
4
  "description": "Shared calculation modules for the BullTrackers Computation System.",
5
5
  "main": "index.js",
6
6
  "files": [