aiden-shared-calculations-unified 1.0.34 → 1.0.36
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/README.MD +77 -77
- package/calculations/activity/historical/activity_by_pnl_status.js +85 -85
- package/calculations/activity/historical/daily_asset_activity.js +85 -85
- package/calculations/activity/historical/daily_user_activity_tracker.js +144 -144
- package/calculations/activity/historical/speculator_adjustment_activity.js +76 -76
- package/calculations/asset_metrics/asset_position_size.js +57 -57
- package/calculations/backtests/strategy-performance.js +229 -245
- package/calculations/behavioural/historical/asset_crowd_flow.js +165 -170
- package/calculations/behavioural/historical/drawdown_response.js +58 -58
- package/calculations/behavioural/historical/dumb-cohort-flow.js +249 -249
- package/calculations/behavioural/historical/gain_response.js +57 -57
- package/calculations/behavioural/historical/in_loss_asset_crowd_flow.js +98 -98
- package/calculations/behavioural/historical/in_profit_asset_crowd_flow.js +99 -99
- package/calculations/behavioural/historical/paper_vs_diamond_hands.js +39 -39
- package/calculations/behavioural/historical/position_count_pnl.js +67 -67
- package/calculations/behavioural/historical/smart-cohort-flow.js +250 -250
- package/calculations/behavioural/historical/smart_money_flow.js +165 -165
- package/calculations/behavioural/historical/user-investment-profile.js +412 -412
- package/calculations/capital_flow/historical/crowd-cash-flow-proxy.js +121 -121
- package/calculations/capital_flow/historical/deposit_withdrawal_percentage.js +117 -117
- package/calculations/capital_flow/historical/new_allocation_percentage.js +49 -49
- package/calculations/insights/daily_bought_vs_sold_count.js +55 -55
- package/calculations/insights/daily_buy_sell_sentiment_count.js +49 -49
- package/calculations/insights/daily_ownership_delta.js +55 -55
- package/calculations/insights/daily_total_positions_held.js +39 -39
- package/calculations/meta/capital_deployment_strategy.js +129 -137
- package/calculations/meta/capital_liquidation_performance.js +121 -163
- package/calculations/meta/capital_vintage_performance.js +121 -158
- package/calculations/meta/cash-flow-deployment.js +110 -124
- package/calculations/meta/cash-flow-liquidation.js +126 -142
- package/calculations/meta/crowd_sharpe_ratio_proxy.js +83 -91
- package/calculations/meta/profit_cohort_divergence.js +77 -91
- package/calculations/meta/smart-dumb-divergence-index.js +116 -138
- package/calculations/meta/social_flow_correlation.js +99 -125
- package/calculations/pnl/asset_pnl_status.js +46 -46
- package/calculations/pnl/historical/profitability_migration.js +57 -57
- package/calculations/pnl/historical/user_profitability_tracker.js +117 -117
- package/calculations/pnl/profitable_and_unprofitable_status.js +64 -64
- package/calculations/sectors/historical/diversification_pnl.js +76 -76
- package/calculations/sectors/historical/sector_rotation.js +67 -67
- package/calculations/sentiment/historical/crowd_conviction_score.js +80 -80
- package/calculations/socialPosts/social-asset-posts-trend.js +52 -52
- package/calculations/socialPosts/social-top-mentioned-words.js +102 -102
- package/calculations/socialPosts/social-topic-interest-evolution.js +53 -53
- package/calculations/socialPosts/social-word-mentions-trend.js +62 -62
- package/calculations/socialPosts/social_activity_aggregation.js +103 -103
- package/calculations/socialPosts/social_event_correlation.js +121 -121
- package/calculations/socialPosts/social_sentiment_aggregation.js +114 -114
- package/calculations/speculators/historical/risk_appetite_change.js +54 -54
- package/calculations/speculators/historical/tsl_effectiveness.js +74 -74
- package/index.js +33 -33
- package/package.json +32 -32
- package/utils/firestore_utils.js +76 -76
- package/utils/price_data_provider.js +142 -142
- package/utils/sector_mapping_provider.js +74 -74
- package/calculations/capital_flow/historical/reallocation_increase_percentage.js +0 -63
- package/calculations/speculators/stop_loss_distance_by_sector_short_long_breakdown.js +0 -91
- package/calculations/speculators/stop_loss_distance_by_ticker_short_long_breakdown.js +0 -73
|
@@ -1,58 +1,58 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @fileoverview Calculates the migration of users between profitability states.
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
class ProfitabilityMigration {
|
|
6
|
-
constructor() {
|
|
7
|
-
this.profitableToUnprofitable = 0;
|
|
8
|
-
this.unprofitableToProfitable = 0;
|
|
9
|
-
this.remainedProfitable = 0;
|
|
10
|
-
this.remainedUnprofitable = 0;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
process(todayPortfolio, yesterdayPortfolio, userId) {
|
|
14
|
-
if (!todayPortfolio || !yesterdayPortfolio) {
|
|
15
|
-
return;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
const todayProfit = this.calculateTotalPnl(todayPortfolio);
|
|
19
|
-
const yesterdayProfit = this.calculateTotalPnl(yesterdayPortfolio);
|
|
20
|
-
|
|
21
|
-
if (todayProfit === null || yesterdayProfit === null) {
|
|
22
|
-
return;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
const wasProfitable = yesterdayProfit > 0;
|
|
26
|
-
const isProfitable = todayProfit > 0;
|
|
27
|
-
|
|
28
|
-
if (wasProfitable && !isProfitable) {
|
|
29
|
-
this.profitableToUnprofitable++;
|
|
30
|
-
} else if (!wasProfitable && isProfitable) {
|
|
31
|
-
this.unprofitableToProfitable++;
|
|
32
|
-
} else if (wasProfitable && isProfitable) {
|
|
33
|
-
this.remainedProfitable++;
|
|
34
|
-
} else {
|
|
35
|
-
this.remainedUnprofitable++;
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
calculateTotalPnl(portfolio) {
|
|
40
|
-
if (portfolio && portfolio.AggregatedPositions) {
|
|
41
|
-
return portfolio.AggregatedPositions.reduce((sum, pos) => sum + pos.NetProfit, 0);
|
|
42
|
-
} else if (portfolio && portfolio.PublicPositions) {
|
|
43
|
-
return portfolio.PublicPositions.reduce((sum, pos) => sum + pos.NetProfit, 0);
|
|
44
|
-
}
|
|
45
|
-
return null;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
getResult() {
|
|
49
|
-
return {
|
|
50
|
-
profitable_to_unprofitable: this.profitableToUnprofitable,
|
|
51
|
-
unprofitable_to_profitable: this.unprofitableToProfitable,
|
|
52
|
-
remained_profitable: this.remainedProfitable,
|
|
53
|
-
remained_unprofitable: this.remainedUnprofitable,
|
|
54
|
-
};
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Calculates the migration of users between profitability states.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
class ProfitabilityMigration {
|
|
6
|
+
constructor() {
|
|
7
|
+
this.profitableToUnprofitable = 0;
|
|
8
|
+
this.unprofitableToProfitable = 0;
|
|
9
|
+
this.remainedProfitable = 0;
|
|
10
|
+
this.remainedUnprofitable = 0;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
process(todayPortfolio, yesterdayPortfolio, userId) {
|
|
14
|
+
if (!todayPortfolio || !yesterdayPortfolio) {
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const todayProfit = this.calculateTotalPnl(todayPortfolio);
|
|
19
|
+
const yesterdayProfit = this.calculateTotalPnl(yesterdayPortfolio);
|
|
20
|
+
|
|
21
|
+
if (todayProfit === null || yesterdayProfit === null) {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const wasProfitable = yesterdayProfit > 0;
|
|
26
|
+
const isProfitable = todayProfit > 0;
|
|
27
|
+
|
|
28
|
+
if (wasProfitable && !isProfitable) {
|
|
29
|
+
this.profitableToUnprofitable++;
|
|
30
|
+
} else if (!wasProfitable && isProfitable) {
|
|
31
|
+
this.unprofitableToProfitable++;
|
|
32
|
+
} else if (wasProfitable && isProfitable) {
|
|
33
|
+
this.remainedProfitable++;
|
|
34
|
+
} else {
|
|
35
|
+
this.remainedUnprofitable++;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
calculateTotalPnl(portfolio) {
|
|
40
|
+
if (portfolio && portfolio.AggregatedPositions) {
|
|
41
|
+
return portfolio.AggregatedPositions.reduce((sum, pos) => sum + pos.NetProfit, 0);
|
|
42
|
+
} else if (portfolio && portfolio.PublicPositions) {
|
|
43
|
+
return portfolio.PublicPositions.reduce((sum, pos) => sum + pos.NetProfit, 0);
|
|
44
|
+
}
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
getResult() {
|
|
49
|
+
return {
|
|
50
|
+
profitable_to_unprofitable: this.profitableToUnprofitable,
|
|
51
|
+
unprofitable_to_profitable: this.unprofitableToProfitable,
|
|
52
|
+
remained_profitable: this.remainedProfitable,
|
|
53
|
+
remained_unprofitable: this.remainedUnprofitable,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
58
|
module.exports = ProfitabilityMigration;
|
|
@@ -1,118 +1,118 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @fileoverview Tracks user profitability over a 7-day rolling window.
|
|
3
|
-
* This version shards the output AND calculates the user's *weighted average daily PNL (as a decimal %)*.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
const { Firestore } = require('@google-cloud/firestore');
|
|
7
|
-
const firestore = new Firestore();
|
|
8
|
-
|
|
9
|
-
const NUM_SHARDS = 50;
|
|
10
|
-
|
|
11
|
-
class UserProfitabilityTracker {
|
|
12
|
-
constructor() {
|
|
13
|
-
// This will store { userId: { weightedPnlSum: 0, totalInvested: 0 } }
|
|
14
|
-
this.dailyData = {};
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Calculates the weighted PNL for the day.
|
|
19
|
-
* NetProfit is a decimal % return (e.g., 0.03)
|
|
20
|
-
* Invested is a decimal % weight (e.g., 0.05)
|
|
21
|
-
*/
|
|
22
|
-
calculateWeightedDailyPnl(portfolio) {
|
|
23
|
-
if (!portfolio || !portfolio.AggregatedPositions || portfolio.AggregatedPositions.length === 0) {
|
|
24
|
-
return { weightedPnl: 0, totalInvested: 0 };
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
let weightedPnlSum = 0;
|
|
28
|
-
let totalInvested = 0;
|
|
29
|
-
|
|
30
|
-
for (const pos of portfolio.AggregatedPositions) {
|
|
31
|
-
// Use NetProfit (the % return)
|
|
32
|
-
const netProfit = ('NetProfit' in pos) ? pos.NetProfit : (pos.ProfitAndLoss || 0);
|
|
33
|
-
// Use InvestedAmount (the % portfolio weight)
|
|
34
|
-
const invested = pos.InvestedAmount || pos.Amount || 0;
|
|
35
|
-
|
|
36
|
-
if (invested > 0) {
|
|
37
|
-
weightedPnlSum += netProfit * invested;
|
|
38
|
-
totalInvested += invested;
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
return { weightedPnlSum, totalInvested };
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
process(todayPortfolio, yesterdayPortfolio, userId) {
|
|
46
|
-
if (!todayPortfolio) return;
|
|
47
|
-
|
|
48
|
-
const { weightedPnlSum, totalInvested } = this.calculateWeightedDailyPnl(todayPortfolio);
|
|
49
|
-
|
|
50
|
-
if (totalInvested > 0) {
|
|
51
|
-
this.dailyData[userId] = { weightedPnlSum, totalInvested };
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
async getResult() {
|
|
56
|
-
// --- START MODIFICATION ---
|
|
57
|
-
// If no data was processed (e.g., no portfolios found for the day),
|
|
58
|
-
// return null to allow the backfill to retry.
|
|
59
|
-
if (Object.keys(this.dailyData).length === 0) {
|
|
60
|
-
console.warn('[UserProfitabilityTracker] No daily data was processed. Returning null for backfill.');
|
|
61
|
-
return null;
|
|
62
|
-
}
|
|
63
|
-
// --- END MODIFICATION ---
|
|
64
|
-
|
|
65
|
-
const today = new Date().toISOString().slice(0, 10);
|
|
66
|
-
const results = {}; // For sharded history
|
|
67
|
-
const dailyPnlMap = {}; // For the new profile calc
|
|
68
|
-
|
|
69
|
-
// Prepare sharded data structure
|
|
70
|
-
for (let i = 0; i < NUM_SHARDS; i++) {
|
|
71
|
-
results[`user_profitability_shard_${i}`] = {};
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// ... (Fetch existing shards logic, same as your file) ...
|
|
75
|
-
const shardPromises = [];
|
|
76
|
-
for (let i = 0; i < NUM_SHARDS; i++) {
|
|
77
|
-
const docRef = firestore.collection('historical_insights').doc(`user_profitability_shard_${i}`);
|
|
78
|
-
shardPromises.push(docRef.get());
|
|
79
|
-
}
|
|
80
|
-
const shardSnapshots = await Promise.all(shardPromises);
|
|
81
|
-
const existingData = shardSnapshots.map(snap => (snap.exists ? snap.data().profits : {}));
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
for (const userId in this.dailyData) {
|
|
85
|
-
const { weightedPnlSum, totalInvested } = this.dailyData[userId];
|
|
86
|
-
|
|
87
|
-
// Calculate the final weighted average % return for the day
|
|
88
|
-
// We cap totalInvested at 1.0 (100%) in case of data issues
|
|
89
|
-
const totalWeight = Math.min(1.0, totalInvested);
|
|
90
|
-
const dailyAvgPnl = (totalWeight > 0) ? (weightedPnlSum / totalWeight) : 0;
|
|
91
|
-
|
|
92
|
-
// Store this for the profile calc dependency
|
|
93
|
-
dailyPnlMap[userId] = dailyAvgPnl;
|
|
94
|
-
|
|
95
|
-
// --- Now, update the sharded history ---
|
|
96
|
-
const shardIndex = parseInt(userId, 10) % NUM_SHARDS;
|
|
97
|
-
const userHistory = existingData[shardIndex][userId] || [];
|
|
98
|
-
|
|
99
|
-
// Store the decimal % pnl in the history
|
|
100
|
-
userHistory.push({ date: today, pnl: dailyAvgPnl });
|
|
101
|
-
|
|
102
|
-
const shardKey = `user_profitability_shard_${shardIndex}`;
|
|
103
|
-
if (!results[shardKey]) results[shardKey] = {};
|
|
104
|
-
results[shardKey][userId] = userHistory.slice(-7);
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
return {
|
|
108
|
-
sharded_user_profitability: results,
|
|
109
|
-
daily_pnl_map: dailyPnlMap // <-- This now correctly outputs the weighted avg % PNL
|
|
110
|
-
};
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
reset() {
|
|
114
|
-
this.dailyData = {};
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Tracks user profitability over a 7-day rolling window.
|
|
3
|
+
* This version shards the output AND calculates the user's *weighted average daily PNL (as a decimal %)*.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const { Firestore } = require('@google-cloud/firestore');
|
|
7
|
+
const firestore = new Firestore();
|
|
8
|
+
|
|
9
|
+
const NUM_SHARDS = 50;
|
|
10
|
+
|
|
11
|
+
class UserProfitabilityTracker {
|
|
12
|
+
constructor() {
|
|
13
|
+
// This will store { userId: { weightedPnlSum: 0, totalInvested: 0 } }
|
|
14
|
+
this.dailyData = {};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Calculates the weighted PNL for the day.
|
|
19
|
+
* NetProfit is a decimal % return (e.g., 0.03)
|
|
20
|
+
* Invested is a decimal % weight (e.g., 0.05)
|
|
21
|
+
*/
|
|
22
|
+
calculateWeightedDailyPnl(portfolio) {
|
|
23
|
+
if (!portfolio || !portfolio.AggregatedPositions || portfolio.AggregatedPositions.length === 0) {
|
|
24
|
+
return { weightedPnl: 0, totalInvested: 0 };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
let weightedPnlSum = 0;
|
|
28
|
+
let totalInvested = 0;
|
|
29
|
+
|
|
30
|
+
for (const pos of portfolio.AggregatedPositions) {
|
|
31
|
+
// Use NetProfit (the % return)
|
|
32
|
+
const netProfit = ('NetProfit' in pos) ? pos.NetProfit : (pos.ProfitAndLoss || 0);
|
|
33
|
+
// Use InvestedAmount (the % portfolio weight)
|
|
34
|
+
const invested = pos.InvestedAmount || pos.Amount || 0;
|
|
35
|
+
|
|
36
|
+
if (invested > 0) {
|
|
37
|
+
weightedPnlSum += netProfit * invested;
|
|
38
|
+
totalInvested += invested;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return { weightedPnlSum, totalInvested };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
process(todayPortfolio, yesterdayPortfolio, userId) {
|
|
46
|
+
if (!todayPortfolio) return;
|
|
47
|
+
|
|
48
|
+
const { weightedPnlSum, totalInvested } = this.calculateWeightedDailyPnl(todayPortfolio);
|
|
49
|
+
|
|
50
|
+
if (totalInvested > 0) {
|
|
51
|
+
this.dailyData[userId] = { weightedPnlSum, totalInvested };
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async getResult() {
|
|
56
|
+
// --- START MODIFICATION ---
|
|
57
|
+
// If no data was processed (e.g., no portfolios found for the day),
|
|
58
|
+
// return null to allow the backfill to retry.
|
|
59
|
+
if (Object.keys(this.dailyData).length === 0) {
|
|
60
|
+
console.warn('[UserProfitabilityTracker] No daily data was processed. Returning null for backfill.');
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
// --- END MODIFICATION ---
|
|
64
|
+
|
|
65
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
66
|
+
const results = {}; // For sharded history
|
|
67
|
+
const dailyPnlMap = {}; // For the new profile calc
|
|
68
|
+
|
|
69
|
+
// Prepare sharded data structure
|
|
70
|
+
for (let i = 0; i < NUM_SHARDS; i++) {
|
|
71
|
+
results[`user_profitability_shard_${i}`] = {};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ... (Fetch existing shards logic, same as your file) ...
|
|
75
|
+
const shardPromises = [];
|
|
76
|
+
for (let i = 0; i < NUM_SHARDS; i++) {
|
|
77
|
+
const docRef = firestore.collection('historical_insights').doc(`user_profitability_shard_${i}`);
|
|
78
|
+
shardPromises.push(docRef.get());
|
|
79
|
+
}
|
|
80
|
+
const shardSnapshots = await Promise.all(shardPromises);
|
|
81
|
+
const existingData = shardSnapshots.map(snap => (snap.exists ? snap.data().profits : {}));
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
for (const userId in this.dailyData) {
|
|
85
|
+
const { weightedPnlSum, totalInvested } = this.dailyData[userId];
|
|
86
|
+
|
|
87
|
+
// Calculate the final weighted average % return for the day
|
|
88
|
+
// We cap totalInvested at 1.0 (100%) in case of data issues
|
|
89
|
+
const totalWeight = Math.min(1.0, totalInvested);
|
|
90
|
+
const dailyAvgPnl = (totalWeight > 0) ? (weightedPnlSum / totalWeight) : 0;
|
|
91
|
+
|
|
92
|
+
// Store this for the profile calc dependency
|
|
93
|
+
dailyPnlMap[userId] = dailyAvgPnl;
|
|
94
|
+
|
|
95
|
+
// --- Now, update the sharded history ---
|
|
96
|
+
const shardIndex = parseInt(userId, 10) % NUM_SHARDS;
|
|
97
|
+
const userHistory = existingData[shardIndex][userId] || [];
|
|
98
|
+
|
|
99
|
+
// Store the decimal % pnl in the history
|
|
100
|
+
userHistory.push({ date: today, pnl: dailyAvgPnl });
|
|
101
|
+
|
|
102
|
+
const shardKey = `user_profitability_shard_${shardIndex}`;
|
|
103
|
+
if (!results[shardKey]) results[shardKey] = {};
|
|
104
|
+
results[shardKey][userId] = userHistory.slice(-7);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
sharded_user_profitability: results,
|
|
109
|
+
daily_pnl_map: dailyPnlMap // <-- This now correctly outputs the weighted avg % PNL
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
reset() {
|
|
114
|
+
this.dailyData = {};
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
118
|
module.exports = UserProfitabilityTracker;
|
|
@@ -1,65 +1,65 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @fileoverview Counts the number of users whose overall portfolio is in profit vs. loss for the current day.
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
class DailyUserPnlStatus {
|
|
6
|
-
constructor() {
|
|
7
|
-
// Initialize counters
|
|
8
|
-
this.profitableUsers = 0;
|
|
9
|
-
this.unprofitableUsers = 0;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Processes a single user's portfolio for the current day.
|
|
14
|
-
* @param {object} todayPortfolio - The portfolio data for the current day.
|
|
15
|
-
* @param {object} yesterdayPortfolio - Not used in this calculation.
|
|
16
|
-
* @param {string} userId - The user's ID.
|
|
17
|
-
* @param {object} context - Shared context data (not used here).
|
|
18
|
-
*/
|
|
19
|
-
process(todayPortfolio, yesterdayPortfolio, userId, context) {
|
|
20
|
-
// Prefer AggregatedPositions as it typically contains NetProfit
|
|
21
|
-
const positions = todayPortfolio?.AggregatedPositions || todayPortfolio?.PublicPositions;
|
|
22
|
-
|
|
23
|
-
// Ensure we have portfolio data and positions
|
|
24
|
-
if (!positions || !Array.isArray(positions) || positions.length === 0) {
|
|
25
|
-
return; // Skip if no positions data for the user today
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
// Calculate the sum of NetProfit across all positions for the user
|
|
29
|
-
let totalUserPnl = 0;
|
|
30
|
-
for (const position of positions) {
|
|
31
|
-
// Ensure NetProfit exists and is a number, default to 0 otherwise
|
|
32
|
-
totalUserPnl += (typeof position.NetProfit === 'number' ? position.NetProfit : 0);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
// Increment the appropriate counter based on the total P/L
|
|
36
|
-
if (totalUserPnl > 0) {
|
|
37
|
-
this.profitableUsers++;
|
|
38
|
-
} else if (totalUserPnl < 0) {
|
|
39
|
-
this.unprofitableUsers++;
|
|
40
|
-
}
|
|
41
|
-
// Users with exactly zero P/L are ignored
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Returns the final aggregated counts.
|
|
46
|
-
* @returns {object} Object containing the counts of profitable and unprofitable users.
|
|
47
|
-
*/
|
|
48
|
-
getResult() {
|
|
49
|
-
// Return the raw counts
|
|
50
|
-
return {
|
|
51
|
-
profitable_user_count: this.profitableUsers,
|
|
52
|
-
unprofitable_user_count: this.unprofitableUsers
|
|
53
|
-
};
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Resets the internal counters for the next processing run.
|
|
58
|
-
*/
|
|
59
|
-
reset() {
|
|
60
|
-
this.profitableUsers = 0;
|
|
61
|
-
this.unprofitableUsers = 0;
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Counts the number of users whose overall portfolio is in profit vs. loss for the current day.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
class DailyUserPnlStatus {
|
|
6
|
+
constructor() {
|
|
7
|
+
// Initialize counters
|
|
8
|
+
this.profitableUsers = 0;
|
|
9
|
+
this.unprofitableUsers = 0;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Processes a single user's portfolio for the current day.
|
|
14
|
+
* @param {object} todayPortfolio - The portfolio data for the current day.
|
|
15
|
+
* @param {object} yesterdayPortfolio - Not used in this calculation.
|
|
16
|
+
* @param {string} userId - The user's ID.
|
|
17
|
+
* @param {object} context - Shared context data (not used here).
|
|
18
|
+
*/
|
|
19
|
+
process(todayPortfolio, yesterdayPortfolio, userId, context) {
|
|
20
|
+
// Prefer AggregatedPositions as it typically contains NetProfit
|
|
21
|
+
const positions = todayPortfolio?.AggregatedPositions || todayPortfolio?.PublicPositions;
|
|
22
|
+
|
|
23
|
+
// Ensure we have portfolio data and positions
|
|
24
|
+
if (!positions || !Array.isArray(positions) || positions.length === 0) {
|
|
25
|
+
return; // Skip if no positions data for the user today
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Calculate the sum of NetProfit across all positions for the user
|
|
29
|
+
let totalUserPnl = 0;
|
|
30
|
+
for (const position of positions) {
|
|
31
|
+
// Ensure NetProfit exists and is a number, default to 0 otherwise
|
|
32
|
+
totalUserPnl += (typeof position.NetProfit === 'number' ? position.NetProfit : 0);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Increment the appropriate counter based on the total P/L
|
|
36
|
+
if (totalUserPnl > 0) {
|
|
37
|
+
this.profitableUsers++;
|
|
38
|
+
} else if (totalUserPnl < 0) {
|
|
39
|
+
this.unprofitableUsers++;
|
|
40
|
+
}
|
|
41
|
+
// Users with exactly zero P/L are ignored
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Returns the final aggregated counts.
|
|
46
|
+
* @returns {object} Object containing the counts of profitable and unprofitable users.
|
|
47
|
+
*/
|
|
48
|
+
getResult() {
|
|
49
|
+
// Return the raw counts
|
|
50
|
+
return {
|
|
51
|
+
profitable_user_count: this.profitableUsers,
|
|
52
|
+
unprofitable_user_count: this.unprofitableUsers
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Resets the internal counters for the next processing run.
|
|
58
|
+
*/
|
|
59
|
+
reset() {
|
|
60
|
+
this.profitableUsers = 0;
|
|
61
|
+
this.unprofitableUsers = 0;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
65
|
module.exports = DailyUserPnlStatus;
|
|
@@ -1,77 +1,77 @@
|
|
|
1
|
-
// CORRECTED PATH: ../utils/ instead of ../../utils/
|
|
2
|
-
const { getInstrumentSectorMap } = require('../../../utils/sector_mapping_provider');
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Aggregates P/L by the number of unique sectors a user is invested in.
|
|
6
|
-
*/
|
|
7
|
-
class DiversificationPnl {
|
|
8
|
-
constructor() {
|
|
9
|
-
this.pnl_by_sector_count = {};
|
|
10
|
-
this.sectorMapping = null;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
_initBucket(count) {
|
|
14
|
-
if (!this.pnl_by_sector_count[count]) {
|
|
15
|
-
this.pnl_by_sector_count[count] = { pnl_sum: 0, count: 0 };
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* FIX: Helper function to calculate total P&L from positions
|
|
21
|
-
* @param {object} portfolio
|
|
22
|
-
* @returns {number|null}
|
|
23
|
-
*/
|
|
24
|
-
_calculateTotalPnl(portfolio) {
|
|
25
|
-
const positions = portfolio?.AggregatedPositions || portfolio?.PublicPositions;
|
|
26
|
-
if (positions && Array.isArray(positions)) {
|
|
27
|
-
// Sum all NetProfit fields, defaulting to 0 if a position has no NetProfit
|
|
28
|
-
return positions.reduce((sum, pos) => sum + (pos.NetProfit || 0), 0);
|
|
29
|
-
}
|
|
30
|
-
return null;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
async process(todayPortfolio, yesterdayPortfolio, userId) { // Added async
|
|
34
|
-
// FIX: Only need todayPortfolio for this logic
|
|
35
|
-
if (!todayPortfolio) {
|
|
36
|
-
return;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
if(!this.sectorMapping) {
|
|
40
|
-
this.sectorMapping = await getInstrumentSectorMap();
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
const positions = todayPortfolio.AggregatedPositions || todayPortfolio.PublicPositions;
|
|
44
|
-
|
|
45
|
-
if (!positions || !Array.isArray(positions)) {
|
|
46
|
-
return;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
const uniqueSectors = new Set();
|
|
50
|
-
for (const position of positions) {
|
|
51
|
-
const sector = this.sectorMapping[position.InstrumentID] || 'Other';
|
|
52
|
-
uniqueSectors.add(sector);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
const sectorCount = uniqueSectors.size;
|
|
56
|
-
if (sectorCount === 0) {
|
|
57
|
-
return;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// FIX: Calculate dailyPnl by summing NetProfit from all positions
|
|
61
|
-
const dailyPnl = this._calculateTotalPnl(todayPortfolio);
|
|
62
|
-
|
|
63
|
-
if (dailyPnl === null) {
|
|
64
|
-
return; // Cannot calculate P&L for this user
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
this._initBucket(sectorCount);
|
|
68
|
-
this.pnl_by_sector_count[sectorCount].pnl_sum += dailyPnl;
|
|
69
|
-
this.pnl_by_sector_count[sectorCount].count++;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
getResult() {
|
|
73
|
-
return this.pnl_by_sector_count;
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
1
|
+
// CORRECTED PATH: ../utils/ instead of ../../utils/
|
|
2
|
+
const { getInstrumentSectorMap } = require('../../../utils/sector_mapping_provider');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Aggregates P/L by the number of unique sectors a user is invested in.
|
|
6
|
+
*/
|
|
7
|
+
class DiversificationPnl {
|
|
8
|
+
constructor() {
|
|
9
|
+
this.pnl_by_sector_count = {};
|
|
10
|
+
this.sectorMapping = null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
_initBucket(count) {
|
|
14
|
+
if (!this.pnl_by_sector_count[count]) {
|
|
15
|
+
this.pnl_by_sector_count[count] = { pnl_sum: 0, count: 0 };
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* FIX: Helper function to calculate total P&L from positions
|
|
21
|
+
* @param {object} portfolio
|
|
22
|
+
* @returns {number|null}
|
|
23
|
+
*/
|
|
24
|
+
_calculateTotalPnl(portfolio) {
|
|
25
|
+
const positions = portfolio?.AggregatedPositions || portfolio?.PublicPositions;
|
|
26
|
+
if (positions && Array.isArray(positions)) {
|
|
27
|
+
// Sum all NetProfit fields, defaulting to 0 if a position has no NetProfit
|
|
28
|
+
return positions.reduce((sum, pos) => sum + (pos.NetProfit || 0), 0);
|
|
29
|
+
}
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async process(todayPortfolio, yesterdayPortfolio, userId) { // Added async
|
|
34
|
+
// FIX: Only need todayPortfolio for this logic
|
|
35
|
+
if (!todayPortfolio) {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if(!this.sectorMapping) {
|
|
40
|
+
this.sectorMapping = await getInstrumentSectorMap();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const positions = todayPortfolio.AggregatedPositions || todayPortfolio.PublicPositions;
|
|
44
|
+
|
|
45
|
+
if (!positions || !Array.isArray(positions)) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const uniqueSectors = new Set();
|
|
50
|
+
for (const position of positions) {
|
|
51
|
+
const sector = this.sectorMapping[position.InstrumentID] || 'Other';
|
|
52
|
+
uniqueSectors.add(sector);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const sectorCount = uniqueSectors.size;
|
|
56
|
+
if (sectorCount === 0) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// FIX: Calculate dailyPnl by summing NetProfit from all positions
|
|
61
|
+
const dailyPnl = this._calculateTotalPnl(todayPortfolio);
|
|
62
|
+
|
|
63
|
+
if (dailyPnl === null) {
|
|
64
|
+
return; // Cannot calculate P&L for this user
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
this._initBucket(sectorCount);
|
|
68
|
+
this.pnl_by_sector_count[sectorCount].pnl_sum += dailyPnl;
|
|
69
|
+
this.pnl_by_sector_count[sectorCount].count++;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
getResult() {
|
|
73
|
+
return this.pnl_by_sector_count;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
77
|
module.exports = DiversificationPnl;
|