aiden-shared-calculations-unified 1.0.64 → 1.0.66
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 +1 -1
- package/calculations/activity/historical/activity_by_pnl_status.js +33 -0
- package/calculations/activity/historical/daily_asset_activity.js +42 -0
- package/calculations/activity/historical/daily_user_activity_tracker.js +37 -0
- package/calculations/activity/historical/speculator_adjustment_activity.js +26 -0
- package/calculations/asset_metrics/asset_position_size.js +36 -0
- package/calculations/backtests/strategy-performance.js +41 -0
- package/calculations/behavioural/historical/asset_crowd_flow.js +124 -127
- package/calculations/behavioural/historical/drawdown_response.js +113 -35
- package/calculations/behavioural/historical/dumb-cohort-flow.js +191 -171
- package/calculations/behavioural/historical/gain_response.js +113 -34
- package/calculations/behavioural/historical/historical_performance_aggregator.js +63 -48
- package/calculations/behavioural/historical/in_loss_asset_crowd_flow.js +159 -63
- package/calculations/behavioural/historical/in_profit_asset_crowd_flow.js +159 -64
- package/calculations/behavioural/historical/paper_vs_diamond_hands.js +86 -19
- package/calculations/behavioural/historical/position_count_pnl.js +91 -39
- package/calculations/behavioural/historical/smart-cohort-flow.js +192 -172
- package/calculations/behavioural/historical/smart_money_flow.js +160 -151
- package/calculations/capital_flow/historical/crowd-cash-flow-proxy.js +95 -89
- package/calculations/capital_flow/historical/deposit_withdrawal_percentage.js +88 -81
- package/calculations/capital_flow/historical/new_allocation_percentage.js +75 -26
- package/calculations/capital_flow/historical/reallocation_increase_percentage.js +73 -32
- package/calculations/insights/daily_buy_sell_sentiment_count.js +47 -32
- package/calculations/insights/daily_total_positions_held.js +28 -24
- package/calculations/insights/historical/daily_bought_vs_sold_count.js +101 -36
- package/calculations/insights/historical/daily_ownership_delta.js +95 -32
- package/calculations/meta/capital_deployment_strategy.js +78 -110
- package/calculations/meta/capital_liquidation_performance.js +114 -111
- package/calculations/meta/cash-flow-deployment.js +114 -107
- package/calculations/meta/cash-flow-liquidation.js +114 -107
- package/calculations/meta/crowd_sharpe_ratio_proxy.js +94 -54
- package/calculations/meta/negative_expectancy_cohort_flow.js +185 -177
- package/calculations/meta/positive_expectancy_cohort_flow.js +186 -181
- package/calculations/meta/profit_cohort_divergence.js +83 -59
- package/calculations/meta/shark_attack_signal.js +91 -39
- package/calculations/meta/smart-dumb-divergence-index.js +114 -98
- package/calculations/meta/smart_dumb_divergence_index_v2.js +109 -98
- package/calculations/meta/social-predictive-regime-state.js +76 -155
- package/calculations/meta/social-topic-driver-index.js +74 -127
- package/calculations/meta/user_expectancy_score.js +83 -31
- package/calculations/pnl/asset_pnl_status.js +120 -31
- package/calculations/pnl/average_daily_pnl_all_users.js +42 -27
- package/calculations/pnl/average_daily_pnl_per_sector.js +84 -26
- package/calculations/pnl/average_daily_pnl_per_stock.js +71 -29
- package/calculations/pnl/average_daily_position_pnl.js +49 -21
- package/calculations/pnl/historical/profitability_migration.js +81 -35
- package/calculations/pnl/historical/user_profitability_tracker.js +107 -104
- package/calculations/pnl/pnl_distribution_per_stock.js +65 -45
- package/calculations/pnl/profitability_ratio_per_stock.js +78 -21
- package/calculations/pnl/profitability_skew_per_stock.js +86 -31
- package/calculations/pnl/profitable_and_unprofitable_status.js +45 -45
- package/calculations/sanity/users_processed.js +24 -1
- package/calculations/sectors/historical/diversification_pnl.js +104 -42
- package/calculations/sectors/historical/sector_rotation.js +94 -45
- package/calculations/sectors/total_long_per_sector.js +55 -20
- package/calculations/sectors/total_short_per_sector.js +55 -20
- package/calculations/sentiment/historical/crowd_conviction_score.js +233 -53
- package/calculations/short_and_long_stats/long_position_per_stock.js +50 -14
- package/calculations/short_and_long_stats/sentiment_per_stock.js +76 -19
- package/calculations/short_and_long_stats/short_position_per_stock.js +50 -13
- package/calculations/short_and_long_stats/total_long_figures.js +34 -13
- package/calculations/short_and_long_stats/total_short_figures.js +34 -14
- package/calculations/socialPosts/social-asset-posts-trend.js +96 -29
- package/calculations/socialPosts/social-top-mentioned-words.js +95 -74
- package/calculations/socialPosts/social-topic-interest-evolution.js +92 -29
- package/calculations/socialPosts/social-topic-sentiment-matrix.js +70 -78
- package/calculations/socialPosts/social-word-mentions-trend.js +96 -38
- package/calculations/socialPosts/social_activity_aggregation.js +106 -77
- package/calculations/socialPosts/social_sentiment_aggregation.js +115 -86
- package/calculations/speculators/distance_to_stop_loss_per_leverage.js +82 -43
- package/calculations/speculators/distance_to_tp_per_leverage.js +81 -42
- package/calculations/speculators/entry_distance_to_sl_per_leverage.js +80 -44
- package/calculations/speculators/entry_distance_to_tp_per_leverage.js +81 -44
- package/calculations/speculators/historical/risk_appetite_change.js +89 -32
- package/calculations/speculators/historical/tsl_effectiveness.js +57 -47
- package/calculations/speculators/holding_duration_per_asset.js +83 -23
- package/calculations/speculators/leverage_per_asset.js +68 -19
- package/calculations/speculators/leverage_per_sector.js +86 -25
- package/calculations/speculators/risk_reward_ratio_per_asset.js +82 -28
- package/calculations/speculators/speculator_asset_sentiment.js +100 -48
- package/calculations/speculators/speculator_danger_zone.js +101 -33
- package/calculations/speculators/stop_loss_distance_by_sector_short_long_breakdown.js +93 -66
- package/calculations/speculators/stop_loss_distance_by_ticker_short_long_breakdown.js +94 -47
- package/calculations/speculators/stop_loss_per_asset.js +94 -26
- package/calculations/speculators/take_profit_per_asset.js +95 -27
- package/calculations/speculators/tsl_per_asset.js +77 -23
- package/package.json +1 -1
- package/utils/price_data_provider.js +142 -142
- package/utils/sector_mapping_provider.js +74 -74
|
@@ -1,58 +1,104 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @fileoverview
|
|
2
|
+
* @fileoverview Calculation (Pass 2) for profitability migration.
|
|
3
|
+
*
|
|
4
|
+
* This metric answers: "How many users migrated between
|
|
5
|
+
* profitable and unprofitable states today?"
|
|
6
|
+
*
|
|
7
|
+
* It tracks the "churn" between P&L states.
|
|
3
8
|
*/
|
|
4
|
-
|
|
5
9
|
class ProfitabilityMigration {
|
|
6
10
|
constructor() {
|
|
7
|
-
this.
|
|
8
|
-
this.
|
|
9
|
-
this.
|
|
10
|
-
this.
|
|
11
|
+
this.to_profit_count = 0;
|
|
12
|
+
this.to_loss_count = 0;
|
|
13
|
+
this.remained_profit_count = 0;
|
|
14
|
+
this.remained_loss_count = 0;
|
|
15
|
+
this.total_processed = 0;
|
|
11
16
|
}
|
|
12
17
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
18
|
+
/**
|
|
19
|
+
* Defines the output schema for this calculation.
|
|
20
|
+
* @returns {object} JSON Schema object
|
|
21
|
+
*/
|
|
22
|
+
static getSchema() {
|
|
23
|
+
return {
|
|
24
|
+
"type": "object",
|
|
25
|
+
"description": "Tracks the migration of users between profitable and unprofitable states day-over-day.",
|
|
26
|
+
"properties": {
|
|
27
|
+
"to_profit_count": {
|
|
28
|
+
"type": "number",
|
|
29
|
+
"description": "Count of users who were in loss yesterday and are in profit today."
|
|
30
|
+
},
|
|
31
|
+
"to_loss_count": {
|
|
32
|
+
"type": "number",
|
|
33
|
+
"description": "Count of users who were in profit yesterday and are in loss today."
|
|
34
|
+
},
|
|
35
|
+
"remained_profit_count": {
|
|
36
|
+
"type": "number",
|
|
37
|
+
"description": "Count of users who were in profit yesterday and today."
|
|
38
|
+
},
|
|
39
|
+
"remained_loss_count": {
|
|
40
|
+
"type": "number",
|
|
41
|
+
"description": "Count of users who were in loss yesterday and today."
|
|
42
|
+
},
|
|
43
|
+
"total_processed": {
|
|
44
|
+
"type": "number",
|
|
45
|
+
"description": "Total users who had a P&L status on both days."
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
"required": ["to_profit_count", "to_loss_count", "remained_profit_count", "remained_loss_count", "total_processed"]
|
|
49
|
+
};
|
|
50
|
+
}
|
|
17
51
|
|
|
18
|
-
|
|
19
|
-
|
|
52
|
+
_getPnlState(portfolio) {
|
|
53
|
+
// This checks *overall portfolio* P&L for the day
|
|
54
|
+
const dailyPnl = portfolio?.Summary?.NetProfit || 0;
|
|
55
|
+
if (dailyPnl > 0) return 'profit';
|
|
56
|
+
if (dailyPnl < 0) return 'loss';
|
|
57
|
+
return 'neutral';
|
|
58
|
+
}
|
|
20
59
|
|
|
21
|
-
|
|
60
|
+
process(todayPortfolio, yesterdayPortfolio) {
|
|
61
|
+
if (!todayPortfolio || !yesterdayPortfolio) {
|
|
22
62
|
return;
|
|
23
63
|
}
|
|
24
64
|
|
|
25
|
-
const
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
if (
|
|
29
|
-
|
|
30
|
-
} else if (!wasProfitable && isProfitable) {
|
|
31
|
-
this.unprofitableToProfitable++;
|
|
32
|
-
} else if (wasProfitable && isProfitable) {
|
|
33
|
-
this.remainedProfitable++;
|
|
34
|
-
} else {
|
|
35
|
-
this.remainedUnprofitable++;
|
|
65
|
+
const yState = this._getPnlState(yesterdayPortfolio);
|
|
66
|
+
const tState = this._getPnlState(todayPortfolio);
|
|
67
|
+
|
|
68
|
+
if (yState === 'neutral' || tState === 'neutral') {
|
|
69
|
+
return; // Only track transitions between profit/loss
|
|
36
70
|
}
|
|
37
|
-
|
|
71
|
+
|
|
72
|
+
this.total_processed++;
|
|
38
73
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
74
|
+
if (yState === 'profit' && tState === 'profit') {
|
|
75
|
+
this.remained_profit_count++;
|
|
76
|
+
} else if (yState === 'loss' && tState === 'loss') {
|
|
77
|
+
this.remained_loss_count++;
|
|
78
|
+
} else if (yState === 'loss' && tState === 'profit') {
|
|
79
|
+
this.to_profit_count++;
|
|
80
|
+
} else if (yState === 'profit' && tState === 'loss') {
|
|
81
|
+
this.to_loss_count++;
|
|
44
82
|
}
|
|
45
|
-
return null;
|
|
46
83
|
}
|
|
47
84
|
|
|
48
85
|
getResult() {
|
|
49
86
|
return {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
87
|
+
to_profit_count: this.to_profit_count,
|
|
88
|
+
to_loss_count: this.to_loss_count,
|
|
89
|
+
remained_profit_count: this.remained_profit_count,
|
|
90
|
+
remained_loss_count: this.remained_loss_count,
|
|
91
|
+
total_processed: this.total_processed
|
|
54
92
|
};
|
|
55
93
|
}
|
|
94
|
+
|
|
95
|
+
reset() {
|
|
96
|
+
this.to_profit_count = 0;
|
|
97
|
+
this.to_loss_count = 0;
|
|
98
|
+
this.remained_profit_count = 0;
|
|
99
|
+
this.remained_loss_count = 0;
|
|
100
|
+
this.total_processed = 0;
|
|
101
|
+
}
|
|
56
102
|
}
|
|
57
103
|
|
|
58
104
|
module.exports = ProfitabilityMigration;
|
|
@@ -1,128 +1,131 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @fileoverview
|
|
3
|
-
*
|
|
2
|
+
* @fileoverview Calculation (Pass 2) for user profitability tracker.
|
|
3
|
+
*
|
|
4
|
+
* This metric answers: "What is each user's weighted average
|
|
5
|
+
* daily P&L, and what is their 7-day rolling profitability history?"
|
|
6
|
+
*
|
|
7
|
+
* This is a foundational calculation for identifying 'smart' and
|
|
8
|
+
* 'dumb' cohorts in later passes.
|
|
9
|
+
*
|
|
10
|
+
* It is *stateful* and requires reading the *previous day's*
|
|
11
|
+
* result of *this same calculation* from Firestore.
|
|
4
12
|
*/
|
|
5
|
-
|
|
6
|
-
const { Firestore } = require('@google-cloud/firestore');
|
|
7
|
-
const firestore = new Firestore();
|
|
8
|
-
|
|
9
|
-
const NUM_SHARDS = 50;
|
|
10
|
-
|
|
11
13
|
class UserProfitabilityTracker {
|
|
12
14
|
constructor() {
|
|
13
|
-
//
|
|
14
|
-
this.
|
|
15
|
+
// Map<userId, { weighted_avg_pnl_7d, profitable_days_7d, pnl_history_7d: [] }>
|
|
16
|
+
this.userHistory = new Map();
|
|
15
17
|
}
|
|
16
18
|
|
|
17
19
|
/**
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
* Invested is a decimal % weight (e.g., 0.05)
|
|
20
|
+
* Defines the output schema for this calculation.
|
|
21
|
+
* @returns {object} JSON Schema object
|
|
21
22
|
*/
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
totalInvested += invested;
|
|
43
|
-
}
|
|
44
|
-
}
|
|
23
|
+
static getSchema() {
|
|
24
|
+
const userDetailSchema = {
|
|
25
|
+
"type": "object",
|
|
26
|
+
"properties": {
|
|
27
|
+
"weighted_avg_pnl_7d": {
|
|
28
|
+
"type": "number",
|
|
29
|
+
"description": "Weighted average P&L over the last 7 days (recent days weighted more)."
|
|
30
|
+
},
|
|
31
|
+
"profitable_days_7d": {
|
|
32
|
+
"type": "number",
|
|
33
|
+
"description": "Count of profitable days in the last 7 days."
|
|
34
|
+
},
|
|
35
|
+
"pnl_history_7d": {
|
|
36
|
+
"type": "array",
|
|
37
|
+
"description": "List of the last 7 days' P&L values.",
|
|
38
|
+
"items": { "type": "number" }
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
"required": ["weighted_avg_pnl_7d", "profitable_days_7d", "pnl_history_7d"]
|
|
42
|
+
};
|
|
45
43
|
|
|
46
|
-
return {
|
|
44
|
+
return {
|
|
45
|
+
"type": "object",
|
|
46
|
+
"description": "Tracks 7-day rolling profitability for each user; used to define cohorts.",
|
|
47
|
+
"properties": {
|
|
48
|
+
"ranked_users": {
|
|
49
|
+
"type": "array",
|
|
50
|
+
"description": "List of all users, sorted ascending by their 7-day weighted average P&L.",
|
|
51
|
+
"items": {
|
|
52
|
+
"type": "object",
|
|
53
|
+
"properties": {
|
|
54
|
+
"userId": { "type": "string" },
|
|
55
|
+
"weighted_avg_pnl_7d": { "type": "number" }
|
|
56
|
+
},
|
|
57
|
+
"required": ["userId", "weighted_avg_pnl_7d"]
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
"user_details": {
|
|
61
|
+
"type": "object",
|
|
62
|
+
"description": "A map of user IDs to their detailed profitability data.",
|
|
63
|
+
"patternProperties": {
|
|
64
|
+
"^.*$": userDetailSchema // UserID
|
|
65
|
+
},
|
|
66
|
+
"additionalProperties": userDetailSchema
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
"required": ["ranked_users", "user_details"]
|
|
70
|
+
};
|
|
47
71
|
}
|
|
48
72
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
73
|
+
_calculateWeightedAverage(history) {
|
|
74
|
+
const weights = [0.25, 0.20, 0.15, 0.12, 0.10, 0.08, 0.10]; // Example weights (sum to 1.0)
|
|
75
|
+
let weightedSum = 0;
|
|
76
|
+
|
|
77
|
+
for (let i = 0; i < history.length; i++) {
|
|
78
|
+
// Apply weights in reverse (most recent gets highest weight)
|
|
79
|
+
weightedSum += history[i] * (weights[i] || 0);
|
|
56
80
|
}
|
|
81
|
+
return weightedSum;
|
|
57
82
|
}
|
|
58
83
|
|
|
59
|
-
|
|
60
|
-
//
|
|
61
|
-
//
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
shardPromises.push(docRef.get());
|
|
83
|
-
}
|
|
84
|
-
const shardSnapshots = await Promise.all(shardPromises);
|
|
85
|
-
const existingData = shardSnapshots.map(snap => (snap.exists ? snap.data() : {}) || {});
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
for (const userId in this.dailyData) {
|
|
89
|
-
const { weightedPnlSum, totalInvested } = this.dailyData[userId];
|
|
90
|
-
|
|
91
|
-
// Calculate the final weighted average % return for the day
|
|
92
|
-
// We cap totalInvested at 1.0 (100%) in case of data issues
|
|
93
|
-
const totalWeight = Math.min(1.0, totalInvested);
|
|
94
|
-
const dailyAvgPnl = (totalWeight > 0) ? (weightedPnlSum / totalWeight) : 0;
|
|
95
|
-
|
|
96
|
-
// Store this for the profile calc dependency
|
|
97
|
-
dailyPnlMap[userId] = dailyAvgPnl;
|
|
98
|
-
|
|
99
|
-
// --- Now, update the sharded history ---
|
|
100
|
-
const shardIndex = parseInt(userId, 10) % NUM_SHARDS;
|
|
84
|
+
process(todayPortfolio, yesterdayPortfolio, userId, context, todayInsights, yesterdayInsights, fetchedDependencies) {
|
|
85
|
+
// 1. Get this user's history from *yesterday's* run of this metric
|
|
86
|
+
// This data is pre-loaded into context by the computation system
|
|
87
|
+
const yHistoryData = context.yesterdaysDependencyData['user_profitability_tracker'];
|
|
88
|
+
const userHistory = yHistoryData?.user_details?.[userId]?.pnl_history_7d || [];
|
|
89
|
+
|
|
90
|
+
// 2. Get today's P&L
|
|
91
|
+
const todayPnl = todayPortfolio?.Summary?.NetProfit || 0;
|
|
92
|
+
|
|
93
|
+
// 3. Create new 7-day history
|
|
94
|
+
const newHistory = [todayPnl, ...userHistory].slice(0, 7);
|
|
95
|
+
|
|
96
|
+
// 4. Calculate new metrics
|
|
97
|
+
const weightedAvg = this._calculateWeightedAverage(newHistory);
|
|
98
|
+
const profitableDays = newHistory.filter(pnl => pnl > 0).length;
|
|
99
|
+
|
|
100
|
+
// 5. Store for getResult()
|
|
101
|
+
this.userHistory.set(userId, {
|
|
102
|
+
weighted_avg_pnl_7d: weightedAvg,
|
|
103
|
+
profitable_days_7d: profitableDays,
|
|
104
|
+
pnl_history_7d: newHistory
|
|
105
|
+
});
|
|
106
|
+
}
|
|
101
107
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
if (!results[shardKey]) results[shardKey] = {};
|
|
114
|
-
results[shardKey][userId] = { history: userHistory.slice(-7) };
|
|
115
|
-
}
|
|
108
|
+
getResult() {
|
|
109
|
+
const user_details = Object.fromEntries(this.userHistory);
|
|
110
|
+
|
|
111
|
+
// Create the ranked list for cohorts
|
|
112
|
+
const ranked_users = Array.from(this.userHistory.entries())
|
|
113
|
+
.map(([userId, data]) => ({
|
|
114
|
+
userId: userId,
|
|
115
|
+
weighted_avg_pnl_7d: data.weighted_avg_pnl_7d
|
|
116
|
+
}))
|
|
117
|
+
// Sort ascending (lowest P&L first)
|
|
118
|
+
.sort((a, b) => a.weighted_avg_pnl_7d - b.weighted_avg_pnl_7d);
|
|
116
119
|
|
|
117
120
|
return {
|
|
118
|
-
|
|
119
|
-
|
|
121
|
+
ranked_users: ranked_users,
|
|
122
|
+
user_details: user_details
|
|
120
123
|
};
|
|
121
124
|
}
|
|
122
125
|
|
|
123
126
|
reset() {
|
|
124
|
-
this.
|
|
127
|
+
this.userHistory.clear();
|
|
125
128
|
}
|
|
126
129
|
}
|
|
127
130
|
|
|
128
|
-
module.exports = UserProfitabilityTracker;
|
|
131
|
+
module.exports = UserProfitabilityTracker;
|
|
@@ -1,65 +1,85 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @fileoverview
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* @fileoverview Calculation (Pass 1) for P&L distribution.
|
|
3
|
+
*
|
|
4
|
+
* This metric answers: "What are the sum, sum of squares, and
|
|
5
|
+
* count of P&L for each stock?"
|
|
6
|
+
*
|
|
7
|
+
* This is a foundational calculation used by other metrics
|
|
8
|
+
* (like 'crowd_sharpe_ratio_proxy') to calculate variance
|
|
9
|
+
* and standard deviation.
|
|
5
10
|
*/
|
|
6
|
-
|
|
7
|
-
|
|
11
|
+
// No mappings needed here, as the consumer (e.g., Sharpe Ratio)
|
|
12
|
+
// will handle the mapping. This class just aggregates by ID.
|
|
8
13
|
class PnlDistributionPerStock {
|
|
9
14
|
constructor() {
|
|
10
|
-
|
|
11
|
-
this.
|
|
15
|
+
// We will store { [instrumentId]: { sum: 0, sumSq: 0, count: 0 } }
|
|
16
|
+
this.assets = {};
|
|
12
17
|
}
|
|
13
18
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
19
|
+
/**
|
|
20
|
+
* Defines the output schema for this calculation.
|
|
21
|
+
* @returns {object} JSON Schema object
|
|
22
|
+
*/
|
|
23
|
+
static getSchema() {
|
|
24
|
+
const distSchema = {
|
|
25
|
+
"type": "object",
|
|
26
|
+
"description": "Raw statistical components for P&L on a single asset.",
|
|
27
|
+
"properties": {
|
|
28
|
+
"sum": {
|
|
29
|
+
"type": "number",
|
|
30
|
+
"description": "Sum of P&L values."
|
|
31
|
+
},
|
|
32
|
+
"sumSq": {
|
|
33
|
+
"type": "number",
|
|
34
|
+
"description": "Sum of squared P&L values (for variance calculation)."
|
|
35
|
+
},
|
|
36
|
+
"count": {
|
|
37
|
+
"type": "number",
|
|
38
|
+
"description": "Count of positions."
|
|
33
39
|
}
|
|
40
|
+
},
|
|
41
|
+
"required": ["sum", "sumSq", "count"]
|
|
42
|
+
};
|
|
34
43
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
44
|
+
return {
|
|
45
|
+
"type": "object",
|
|
46
|
+
"description": "Collects P&L distribution components (sum, sumSq, count) per asset, keyed by InstrumentID.",
|
|
47
|
+
"patternProperties": {
|
|
48
|
+
"^[0-9]+$": distSchema // InstrumentID (numeric string)
|
|
49
|
+
},
|
|
50
|
+
"additionalProperties": distSchema
|
|
51
|
+
};
|
|
40
52
|
}
|
|
41
53
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
54
|
+
process(portfolioData) {
|
|
55
|
+
const positions = portfolioData.PublicPositions || portfolioData.AggregatedPositions;
|
|
56
|
+
if (!positions || !Array.isArray(positions)) {
|
|
57
|
+
return;
|
|
45
58
|
}
|
|
46
59
|
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
60
|
+
for (const pos of positions) {
|
|
61
|
+
const instrumentId = pos.InstrumentID;
|
|
62
|
+
if (!instrumentId) continue;
|
|
63
|
+
|
|
64
|
+
if (!this.assets[instrumentId]) {
|
|
65
|
+
this.assets[instrumentId] = { sum: 0, sumSq: 0, count: 0 };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const pnl = pos.NetProfit || 0;
|
|
69
|
+
|
|
70
|
+
this.assets[instrumentId].sum += pnl;
|
|
71
|
+
this.assets[instrumentId].sumSq += (pnl * pnl);
|
|
72
|
+
this.assets[instrumentId].count++;
|
|
51
73
|
}
|
|
74
|
+
}
|
|
52
75
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
return
|
|
56
|
-
pnl_distribution_by_asset: result
|
|
57
|
-
};
|
|
76
|
+
getResult() {
|
|
77
|
+
// Return the raw aggregated data, keyed by instrumentId
|
|
78
|
+
return this.assets;
|
|
58
79
|
}
|
|
59
80
|
|
|
60
81
|
reset() {
|
|
61
|
-
this.
|
|
62
|
-
this.mappings = null;
|
|
82
|
+
this.assets = {};
|
|
63
83
|
}
|
|
64
84
|
}
|
|
65
85
|
|
|
@@ -1,30 +1,80 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @fileoverview
|
|
2
|
+
* @fileoverview Calculation (Pass 1) for profitability ratio per stock.
|
|
3
|
+
*
|
|
4
|
+
* This metric answers: "For each stock, what is the count of
|
|
5
|
+
* profitable versus unprofitable positions?"
|
|
3
6
|
*/
|
|
4
7
|
const { loadInstrumentMappings } = require('../../utils/sector_mapping_provider');
|
|
5
8
|
|
|
6
9
|
class ProfitabilityRatioPerStock {
|
|
7
10
|
constructor() {
|
|
8
|
-
|
|
11
|
+
// We will store { [instrumentId]: { profitable: 0, unprofitable: 0 } }
|
|
12
|
+
this.assets = new Map();
|
|
9
13
|
this.mappings = null;
|
|
10
14
|
}
|
|
11
15
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
16
|
+
/**
|
|
17
|
+
* Defines the output schema for this calculation.
|
|
18
|
+
* @returns {object} JSON Schema object
|
|
19
|
+
*/
|
|
20
|
+
static getSchema() {
|
|
21
|
+
const tickerSchema = {
|
|
22
|
+
"type": "object",
|
|
23
|
+
"description": "Profit/loss position counts for a specific asset.",
|
|
24
|
+
"properties": {
|
|
25
|
+
"profitable_count": {
|
|
26
|
+
"type": "number",
|
|
27
|
+
"description": "Count of positions in profit."
|
|
28
|
+
},
|
|
29
|
+
"unprofitable_count": {
|
|
30
|
+
"type": "number",
|
|
31
|
+
"description": "Count of positions in loss."
|
|
32
|
+
},
|
|
33
|
+
"ratio": {
|
|
34
|
+
"type": ["number", "null"],
|
|
35
|
+
"description": "Ratio of profitable to unprofitable (Profit / Loss). Null if no losing positions."
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
"required": ["profitable_count", "unprofitable_count", "ratio"]
|
|
39
|
+
};
|
|
15
40
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
41
|
+
return {
|
|
42
|
+
"type": "object",
|
|
43
|
+
"description": "Calculates the count of profitable vs. unprofitable positions for each asset.",
|
|
44
|
+
"patternProperties": {
|
|
45
|
+
"^.*$": tickerSchema // Ticker
|
|
46
|
+
},
|
|
47
|
+
"additionalProperties": tickerSchema
|
|
48
|
+
};
|
|
49
|
+
}
|
|
19
50
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
51
|
+
_initAsset(instrumentId) {
|
|
52
|
+
if (!this.assets.has(instrumentId)) {
|
|
53
|
+
this.assets.set(instrumentId, {
|
|
54
|
+
profitable: 0,
|
|
55
|
+
unprofitable: 0
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
}
|
|
23
59
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
60
|
+
process(portfolioData) {
|
|
61
|
+
const positions = portfolioData.PublicPositions || portfolioData.AggregatedPositions;
|
|
62
|
+
if (!positions || !Array.isArray(positions)) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
for (const pos of positions) {
|
|
67
|
+
const instrumentId = pos.InstrumentID;
|
|
68
|
+
if (!instrumentId) continue;
|
|
69
|
+
|
|
70
|
+
this._initAsset(instrumentId);
|
|
71
|
+
const assetData = this.assets.get(instrumentId);
|
|
72
|
+
const pnl = pos.NetProfit || 0;
|
|
73
|
+
|
|
74
|
+
if (pnl > 0) {
|
|
75
|
+
assetData.profitable++;
|
|
76
|
+
} else if (pnl < 0) {
|
|
77
|
+
assetData.unprofitable++;
|
|
28
78
|
}
|
|
29
79
|
}
|
|
30
80
|
}
|
|
@@ -33,17 +83,24 @@ class ProfitabilityRatioPerStock {
|
|
|
33
83
|
if (!this.mappings) {
|
|
34
84
|
this.mappings = await loadInstrumentMappings();
|
|
35
85
|
}
|
|
86
|
+
|
|
36
87
|
const result = {};
|
|
37
|
-
for (const instrumentId
|
|
38
|
-
const ticker = this.mappings.instrumentToTicker[instrumentId] || instrumentId
|
|
39
|
-
|
|
88
|
+
for (const [instrumentId, data] of this.assets.entries()) {
|
|
89
|
+
const ticker = this.mappings.instrumentToTicker[instrumentId] || `id_${instrumentId}`;
|
|
90
|
+
|
|
91
|
+
if (data.profitable > 0 || data.unprofitable > 0) {
|
|
92
|
+
result[ticker] = {
|
|
93
|
+
profitable_count: data.profitable,
|
|
94
|
+
unprofitable_count: data.unprofitable,
|
|
95
|
+
ratio: (data.unprofitable > 0) ? (data.profitable / data.unprofitable) : null
|
|
96
|
+
};
|
|
97
|
+
}
|
|
40
98
|
}
|
|
41
|
-
|
|
42
|
-
return { profitability_by_asset: result };
|
|
99
|
+
return result;
|
|
43
100
|
}
|
|
44
101
|
|
|
45
102
|
reset() {
|
|
46
|
-
this.
|
|
103
|
+
this.assets.clear();
|
|
47
104
|
this.mappings = null;
|
|
48
105
|
}
|
|
49
106
|
}
|