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.
- package/calculations/behavioural/historical/dumb-cohort-flow.js +130 -162
- package/calculations/behavioural/historical/smart-cohort-flow.js +129 -162
- package/calculations/behavioural/historical/user-investment-profile.js +103 -157
- package/calculations/capital_flow/historical/reallocation_increase_percentage.js +63 -0
- package/calculations/speculators/stop_loss_distance_by_sector_short_long_breakdown.js +91 -0
- package/calculations/speculators/stop_loss_distance_by_ticker_short_long_breakdown.js +73 -0
- package/package.json +1 -1
|
@@ -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
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
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 {
|
|
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;
|
|
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
|
-
//
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
//
|
|
43
|
-
|
|
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
|
-
|
|
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(
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
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
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
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
|
-
//
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
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
|
-
|
|
321
|
-
|
|
322
|
-
|
|
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:
|
|
283
|
+
shardedResults[shardKey] = { profiles: {}, lastUpdated: dateStr };
|
|
329
284
|
}
|
|
330
285
|
|
|
331
286
|
const dailyInvestorScoreMap = {};
|
|
332
287
|
|
|
333
|
-
//
|
|
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
|
-
|
|
342
|
-
|
|
343
|
-
|
|
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 =
|
|
300
|
+
const scores = dailyUserScores[userId];
|
|
352
301
|
|
|
353
|
-
// fetch existing history for this user (if present)
|
|
354
302
|
const existingProfiles = existingShards[shardIndex] || {};
|
|
355
|
-
|
|
356
|
-
const history = (existingProfiles[userId] || []).slice();
|
|
303
|
+
const history = (existingProfiles[userId] || []).slice(); // clone
|
|
357
304
|
|
|
358
305
|
history.push({
|
|
359
|
-
date:
|
|
306
|
+
date: dateStr,
|
|
360
307
|
...scores,
|
|
361
|
-
pnl: (
|
|
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;
|