aiden-shared-calculations-unified 1.0.63 → 1.0.65
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,38 +1,83 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @fileoverview
|
|
3
|
-
*
|
|
2
|
+
* @fileoverview Calculation (Pass 1) for profitability skew.
|
|
3
|
+
*
|
|
4
|
+
_ This metric answers: "For each stock, what is the sum and
|
|
5
|
+
* count of *profits* and *losses*?"
|
|
6
|
+
*
|
|
7
|
+
* This helps determine if returns are skewed (e.g., many small
|
|
8
|
+
* wins and a few huge losses).
|
|
4
9
|
*/
|
|
5
10
|
const { loadInstrumentMappings } = require('../../utils/sector_mapping_provider');
|
|
6
11
|
|
|
7
12
|
class ProfitabilitySkewPerStock {
|
|
8
13
|
constructor() {
|
|
9
|
-
|
|
14
|
+
// { [instrumentId]: { profit_sum: 0, profit_count: 0, loss_sum: 0, loss_count: 0 } }
|
|
15
|
+
this.assets = new Map();
|
|
10
16
|
this.mappings = null;
|
|
11
17
|
}
|
|
12
18
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
19
|
+
/**
|
|
20
|
+
* Defines the output schema for this calculation.
|
|
21
|
+
* @returns {object} JSON Schema object
|
|
22
|
+
*/
|
|
23
|
+
static getSchema() {
|
|
24
|
+
const tickerSchema = {
|
|
25
|
+
"type": "object",
|
|
26
|
+
"description": "P&L skew metrics for a specific asset.",
|
|
27
|
+
"properties": {
|
|
28
|
+
"profit_sum": { "type": "number" },
|
|
29
|
+
"profit_count": { "type": "number" },
|
|
30
|
+
"loss_sum": { "type": "number" },
|
|
31
|
+
"loss_count": { "type": "number" },
|
|
32
|
+
"skew_ratio": {
|
|
33
|
+
"type": ["number", "null"],
|
|
34
|
+
"description": "Ratio of (Average Profit / Average Loss). Null if no losses."
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
"required": ["profit_sum", "profit_count", "loss_sum", "loss_count", "skew_ratio"]
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
"type": "object",
|
|
42
|
+
"description": "Calculates the skew of returns (avg profit vs. avg loss) for each asset.",
|
|
43
|
+
"patternProperties": {
|
|
44
|
+
"^.*$": tickerSchema // Ticker
|
|
45
|
+
},
|
|
46
|
+
"additionalProperties": tickerSchema
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
_initAsset(instrumentId) {
|
|
51
|
+
if (!this.assets.has(instrumentId)) {
|
|
52
|
+
this.assets.set(instrumentId, {
|
|
53
|
+
profit_sum: 0,
|
|
54
|
+
profit_count: 0,
|
|
55
|
+
loss_sum: 0, // Will be negative
|
|
56
|
+
loss_count: 0
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
}
|
|
29
60
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
61
|
+
process(portfolioData) {
|
|
62
|
+
const positions = portfolioData.PublicPositions || portfolioData.AggregatedPositions;
|
|
63
|
+
if (!positions || !Array.isArray(positions)) {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
for (const pos of positions) {
|
|
68
|
+
const instrumentId = pos.InstrumentID;
|
|
69
|
+
if (!instrumentId) continue;
|
|
70
|
+
|
|
71
|
+
this._initAsset(instrumentId);
|
|
72
|
+
const assetData = this.assets.get(instrumentId);
|
|
73
|
+
const pnl = pos.NetProfit || 0;
|
|
74
|
+
|
|
75
|
+
if (pnl > 0) {
|
|
76
|
+
assetData.profit_sum += pnl;
|
|
77
|
+
assetData.profit_count++;
|
|
78
|
+
} else if (pnl < 0) {
|
|
79
|
+
assetData.loss_sum += pnl;
|
|
80
|
+
assetData.loss_count++;
|
|
36
81
|
}
|
|
37
82
|
}
|
|
38
83
|
}
|
|
@@ -41,17 +86,27 @@ class ProfitabilitySkewPerStock {
|
|
|
41
86
|
if (!this.mappings) {
|
|
42
87
|
this.mappings = await loadInstrumentMappings();
|
|
43
88
|
}
|
|
89
|
+
|
|
44
90
|
const result = {};
|
|
45
|
-
for (const instrumentId
|
|
46
|
-
const ticker = this.mappings.instrumentToTicker[instrumentId] || instrumentId
|
|
47
|
-
|
|
91
|
+
for (const [instrumentId, data] of this.assets.entries()) {
|
|
92
|
+
const ticker = this.mappings.instrumentToTicker[instrumentId] || `id_${instrumentId}`;
|
|
93
|
+
|
|
94
|
+
const avgProfit = (data.profit_count > 0) ? (data.profit_sum / data.profit_count) : 0;
|
|
95
|
+
const avgLoss = (data.loss_count > 0) ? (Math.abs(data.loss_sum) / data.loss_count) : 0;
|
|
96
|
+
|
|
97
|
+
result[ticker] = {
|
|
98
|
+
profit_sum: data.profit_sum,
|
|
99
|
+
profit_count: data.profit_count,
|
|
100
|
+
loss_sum: data.loss_sum,
|
|
101
|
+
loss_count: data.loss_count,
|
|
102
|
+
skew_ratio: (avgLoss > 0) ? (avgProfit / avgLoss) : null
|
|
103
|
+
};
|
|
48
104
|
}
|
|
49
|
-
|
|
50
|
-
return { profitability_skew_by_asset: result };
|
|
105
|
+
return result;
|
|
51
106
|
}
|
|
52
107
|
|
|
53
108
|
reset() {
|
|
54
|
-
this.
|
|
109
|
+
this.assets.clear();
|
|
55
110
|
this.mappings = null;
|
|
56
111
|
}
|
|
57
112
|
}
|
|
@@ -1,65 +1,65 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @fileoverview
|
|
2
|
+
* @fileoverview Calculation (Pass 1) for overall profitable status.
|
|
3
|
+
*
|
|
4
|
+
* This metric answers: "How many users have an *overall portfolio*
|
|
5
|
+
* in profit versus in loss today?"
|
|
3
6
|
*/
|
|
4
|
-
|
|
5
|
-
class DailyUserPnlStatus {
|
|
7
|
+
class ProfitableAndUnprofitableStatus {
|
|
6
8
|
constructor() {
|
|
7
|
-
|
|
8
|
-
this.
|
|
9
|
-
this.unprofitableUsers = 0;
|
|
9
|
+
this.total_in_profit = 0;
|
|
10
|
+
this.total_in_loss = 0;
|
|
10
11
|
}
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
|
-
*
|
|
14
|
-
* @
|
|
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).
|
|
14
|
+
* Defines the output schema for this calculation.
|
|
15
|
+
* @returns {object} JSON Schema object
|
|
18
16
|
*/
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
17
|
+
static getSchema() {
|
|
18
|
+
return {
|
|
19
|
+
"type": "object",
|
|
20
|
+
"description": "Tracks the count of users whose *overall portfolio* is in profit vs. in loss.",
|
|
21
|
+
"properties": {
|
|
22
|
+
"total_in_profit": {
|
|
23
|
+
"type": "number",
|
|
24
|
+
"description": "Count of users with a total portfolio P&L > 0."
|
|
25
|
+
},
|
|
26
|
+
"total_in_loss": {
|
|
27
|
+
"type": "number",
|
|
28
|
+
"description": "Count of users with a total portfolio P&L < 0."
|
|
29
|
+
},
|
|
30
|
+
"profit_ratio_pct": {
|
|
31
|
+
"type": "number",
|
|
32
|
+
"description": "Percentage of users in profit (In Profit / Total)."
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"required": ["total_in_profit", "total_in_loss", "profit_ratio_pct"]
|
|
36
|
+
};
|
|
37
|
+
}
|
|
34
38
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
39
|
+
process(portfolioData) {
|
|
40
|
+
// Use the P&L from the summary, which is for the *day*
|
|
41
|
+
const dailyPnl = portfolioData.Summary?.NetProfit || 0;
|
|
42
|
+
|
|
43
|
+
if (dailyPnl > 0) {
|
|
44
|
+
this.total_in_profit++;
|
|
45
|
+
} else if (dailyPnl < 0) {
|
|
46
|
+
this.total_in_loss++;
|
|
40
47
|
}
|
|
41
|
-
// Users with exactly zero P/L are ignored
|
|
42
48
|
}
|
|
43
49
|
|
|
44
|
-
/**
|
|
45
|
-
* Returns the final aggregated counts.
|
|
46
|
-
* @returns {object} Object containing the counts of profitable and unprofitable users.
|
|
47
|
-
*/
|
|
48
50
|
getResult() {
|
|
49
|
-
|
|
51
|
+
const total = this.total_in_profit + this.total_in_loss;
|
|
50
52
|
return {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
+
total_in_profit: this.total_in_profit,
|
|
54
|
+
total_in_loss: this.total_in_loss,
|
|
55
|
+
profit_ratio_pct: (total > 0) ? (this.total_in_profit / total) * 100 : 0
|
|
53
56
|
};
|
|
54
57
|
}
|
|
55
58
|
|
|
56
|
-
/**
|
|
57
|
-
* Resets the internal counters for the next processing run.
|
|
58
|
-
*/
|
|
59
59
|
reset() {
|
|
60
|
-
this.
|
|
61
|
-
this.
|
|
60
|
+
this.total_in_profit = 0;
|
|
61
|
+
this.total_in_loss = 0;
|
|
62
62
|
}
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
-
module.exports =
|
|
65
|
+
module.exports = ProfitableAndUnprofitableStatus;
|
|
@@ -7,12 +7,35 @@ class UsersProcessed {
|
|
|
7
7
|
this.userCount = 0;
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
+
/**
|
|
11
|
+
* Defines the output schema for this calculation.
|
|
12
|
+
* @returns {object} JSON Schema object
|
|
13
|
+
*/
|
|
14
|
+
static getSchema() {
|
|
15
|
+
return {
|
|
16
|
+
"type": "object",
|
|
17
|
+
"description": "A simple counter for the total number of users processed.",
|
|
18
|
+
"properties": {
|
|
19
|
+
"rawTotalUsersProcessed": {
|
|
20
|
+
"type": "number",
|
|
21
|
+
"description": "The total count of users processed by the computation system."
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"required": ["rawTotalUsersProcessed"]
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
10
28
|
process(portfolioData, yesterdayPortfolio, userId, context) {
|
|
11
29
|
this.userCount++;
|
|
12
30
|
}
|
|
13
31
|
|
|
14
32
|
getResult() {
|
|
15
|
-
if (this.userCount === 0)
|
|
33
|
+
if (this.userCount === 0) {
|
|
34
|
+
// Even if 0, we should return the defined schema shape
|
|
35
|
+
return {
|
|
36
|
+
rawTotalUsersProcessed: 0
|
|
37
|
+
};
|
|
38
|
+
}
|
|
16
39
|
// FIX: Rename the key to use the 'raw' prefix for correct aggregation.
|
|
17
40
|
return {
|
|
18
41
|
rawTotalUsersProcessed: this.userCount
|
|
@@ -1,76 +1,138 @@
|
|
|
1
|
-
// CORRECTED PATH: ../utils/ instead of ../../utils/
|
|
2
|
-
const { getInstrumentSectorMap } = require('../../../utils/sector_mapping_provider');
|
|
3
|
-
|
|
4
1
|
/**
|
|
5
|
-
*
|
|
2
|
+
* @fileoverview Calculation (Pass 2) for P&L by diversification.
|
|
3
|
+
*
|
|
4
|
+
* This metric answers: "What is the average daily P&L for users,
|
|
5
|
+
* bucketed by the number of *unique sectors* they are invested in?"
|
|
6
6
|
*/
|
|
7
|
+
const { loadInstrumentMappings } = require('../../../utils/sector_mapping_provider');
|
|
8
|
+
|
|
7
9
|
class DiversificationPnl {
|
|
8
10
|
constructor() {
|
|
9
|
-
|
|
10
|
-
this.
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
}
|
|
11
|
+
// { [bucket]: { pnl_sum: 0, user_count: 0 } }
|
|
12
|
+
this.buckets = {
|
|
13
|
+
'1': { pnl_sum: 0, user_count: 0 },
|
|
14
|
+
'2-3': { pnl_sum: 0, user_count: 0 },
|
|
15
|
+
'4-5': { pnl_sum: 0, user_count: 0 },
|
|
16
|
+
'6-10': { pnl_sum: 0, user_count: 0 },
|
|
17
|
+
'11+': { pnl_sum: 0, user_count: 0 },
|
|
18
|
+
};
|
|
19
|
+
this.mappings = null;
|
|
17
20
|
}
|
|
18
21
|
|
|
19
22
|
/**
|
|
20
|
-
*
|
|
21
|
-
* @
|
|
22
|
-
* @returns {number|null}
|
|
23
|
+
* Defines the output schema for this calculation.
|
|
24
|
+
* @returns {object} JSON Schema object
|
|
23
25
|
*/
|
|
24
|
-
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
26
|
+
static getSchema() {
|
|
27
|
+
const bucketSchema = {
|
|
28
|
+
"type": "object",
|
|
29
|
+
"description": "Aggregated P&L metrics for a sector diversification bucket.",
|
|
30
|
+
"properties": {
|
|
31
|
+
"average_daily_pnl": {
|
|
32
|
+
"type": "number",
|
|
33
|
+
"description": "The average daily P&L for users in this bucket."
|
|
34
|
+
},
|
|
35
|
+
"user_count": {
|
|
36
|
+
"type": "number",
|
|
37
|
+
"description": "The number of users in this bucket."
|
|
38
|
+
},
|
|
39
|
+
"pnl_sum": {
|
|
40
|
+
"type": "number",
|
|
41
|
+
"description": "The sum of all P&L for users in this bucket."
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
"required": ["average_daily_pnl", "user_count", "pnl_sum"]
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
"type": "object",
|
|
49
|
+
"description": "Average daily P&L bucketed by the number of unique sectors a user is invested in.",
|
|
50
|
+
"properties": {
|
|
51
|
+
"1": bucketSchema,
|
|
52
|
+
"2-3": bucketSchema,
|
|
53
|
+
"4-5": bucketSchema,
|
|
54
|
+
"6-10": bucketSchema,
|
|
55
|
+
"11+": bucketSchema
|
|
56
|
+
},
|
|
57
|
+
"required": ["1", "2-3", "4-5", "6-10", "11+"]
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
_getBucket(count) {
|
|
62
|
+
if (count === 1) return '1';
|
|
63
|
+
if (count >= 2 && count <= 3) return '2-3';
|
|
64
|
+
if (count >= 4 && count <= 5) return '4-5';
|
|
65
|
+
if (count >= 6 && count <= 10) return '6-10';
|
|
66
|
+
if (count >= 11) return '11+';
|
|
30
67
|
return null;
|
|
31
68
|
}
|
|
32
69
|
|
|
33
|
-
|
|
34
|
-
//
|
|
70
|
+
process(todayPortfolio, yesterdayPortfolio, userId, context) {
|
|
71
|
+
// This calculation only needs today's portfolio state
|
|
35
72
|
if (!todayPortfolio) {
|
|
36
73
|
return;
|
|
37
74
|
}
|
|
38
|
-
|
|
39
|
-
if(!this.
|
|
40
|
-
|
|
75
|
+
|
|
76
|
+
if (!this.mappings) {
|
|
77
|
+
// Context contains the mappings loaded in Pass 1
|
|
78
|
+
this.mappings = context.mappings;
|
|
41
79
|
}
|
|
42
80
|
|
|
43
81
|
const positions = todayPortfolio.AggregatedPositions || todayPortfolio.PublicPositions;
|
|
44
|
-
|
|
45
|
-
if (!positions || !Array.isArray(positions)) {
|
|
82
|
+
if (!positions || !Array.isArray(positions) || !this.mappings) {
|
|
46
83
|
return;
|
|
47
84
|
}
|
|
48
|
-
|
|
85
|
+
|
|
86
|
+
// Find unique sectors for this user
|
|
49
87
|
const uniqueSectors = new Set();
|
|
50
|
-
for (const
|
|
51
|
-
const
|
|
52
|
-
|
|
88
|
+
for (const pos of positions) {
|
|
89
|
+
const instrumentId = pos.InstrumentID;
|
|
90
|
+
if (instrumentId) {
|
|
91
|
+
const sector = this.mappings.instrumentToSector[instrumentId] || 'Other';
|
|
92
|
+
uniqueSectors.add(sector);
|
|
93
|
+
}
|
|
53
94
|
}
|
|
54
|
-
|
|
95
|
+
|
|
55
96
|
const sectorCount = uniqueSectors.size;
|
|
56
97
|
if (sectorCount === 0) {
|
|
57
98
|
return;
|
|
58
99
|
}
|
|
59
100
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
if (dailyPnl === null) {
|
|
64
|
-
return; // Cannot calculate P&L for this user
|
|
101
|
+
const bucketKey = this._getBucket(sectorCount);
|
|
102
|
+
if (!bucketKey) {
|
|
103
|
+
return;
|
|
65
104
|
}
|
|
66
105
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
106
|
+
// Use the P&L from the summary, which is for the *day*
|
|
107
|
+
const dailyPnl = todayPortfolio.Summary?.NetProfit || 0;
|
|
108
|
+
|
|
109
|
+
const bucket = this.buckets[bucketKey];
|
|
110
|
+
bucket.pnl_sum += dailyPnl;
|
|
111
|
+
bucket.user_count++;
|
|
70
112
|
}
|
|
71
113
|
|
|
72
114
|
getResult() {
|
|
73
|
-
|
|
115
|
+
const result = {};
|
|
116
|
+
for (const key in this.buckets) {
|
|
117
|
+
const bucket = this.buckets[key];
|
|
118
|
+
result[key] = {
|
|
119
|
+
average_daily_pnl: (bucket.user_count > 0) ? (bucket.pnl_sum / bucket.user_count) : 0,
|
|
120
|
+
user_count: bucket.user_count,
|
|
121
|
+
pnl_sum: bucket.pnl_sum
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
return result;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
reset() {
|
|
128
|
+
this.buckets = {
|
|
129
|
+
'1': { pnl_sum: 0, user_count: 0 },
|
|
130
|
+
'2-3': { pnl_sum: 0, user_count: 0 },
|
|
131
|
+
'4-5': { pnl_sum: 0, user_count: 0 },
|
|
132
|
+
'6-10': { pnl_sum: 0, user_count: 0 },
|
|
133
|
+
'11+': { pnl_sum: 0, user_count: 0 },
|
|
134
|
+
};
|
|
135
|
+
this.mappings = null;
|
|
74
136
|
}
|
|
75
137
|
}
|
|
76
138
|
|
|
@@ -1,68 +1,117 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @fileoverview
|
|
2
|
+
* @fileoverview Calculation (Pass 2) for sector rotation.
|
|
3
|
+
*
|
|
4
|
+
* This metric answers: "What is the net change in investment
|
|
5
|
+
* (as a portfolio percentage) for each sector today?"
|
|
6
|
+
*
|
|
7
|
+
* This measures which sectors the crowd is rotating into or out of.
|
|
3
8
|
*/
|
|
4
|
-
|
|
5
|
-
const { getInstrumentSectorMap } = require('../../../utils/sector_mapping_provider');
|
|
9
|
+
const { loadInstrumentMappings } = require('../../../utils/sector_mapping_provider');
|
|
6
10
|
|
|
7
11
|
class SectorRotation {
|
|
8
|
-
// ... (rest of the code is unchanged) ...
|
|
9
12
|
constructor() {
|
|
10
|
-
|
|
11
|
-
this.
|
|
12
|
-
this.
|
|
13
|
+
// We will store { [sector]: { y_invested_sum: 0, t_invested_sum: 0 } }
|
|
14
|
+
this.sectors = new Map();
|
|
15
|
+
this.mappings = null;
|
|
13
16
|
}
|
|
14
17
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
18
|
+
/**
|
|
19
|
+
* Defines the output schema for this calculation.
|
|
20
|
+
* @returns {object} JSON Schema object
|
|
21
|
+
*/
|
|
22
|
+
static getSchema() {
|
|
23
|
+
const sectorSchema = {
|
|
24
|
+
"type": "object",
|
|
25
|
+
"description": "Net change in portfolio allocation for a specific sector.",
|
|
26
|
+
"properties": {
|
|
27
|
+
"net_change_pct": {
|
|
28
|
+
"type": "number",
|
|
29
|
+
"description": "The net change in percentage allocation (Today Sum - Yesterday Sum)."
|
|
30
|
+
},
|
|
31
|
+
"y_total_invested_pct": {
|
|
32
|
+
"type": "number",
|
|
33
|
+
"description": "Total portfolio percentage invested in this sector yesterday."
|
|
34
|
+
},
|
|
35
|
+
"t_total_invested_pct": {
|
|
36
|
+
"type": "number",
|
|
37
|
+
"description": "Total portfolio percentage invested in this sector today."
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
"required": ["net_change_pct", "y_total_invested_pct", "t_total_invested_pct"]
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
"type": "object",
|
|
45
|
+
"description": "Calculates the net rotation of portfolio percentage allocation between sectors.",
|
|
46
|
+
"patternProperties": {
|
|
47
|
+
"^.*$": sectorSchema // Sector name
|
|
48
|
+
},
|
|
49
|
+
"additionalProperties": sectorSchema
|
|
50
|
+
};
|
|
51
|
+
}
|
|
19
52
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
53
|
+
_initSector(sector) {
|
|
54
|
+
if (!this.sectors.has(sector)) {
|
|
55
|
+
this.sectors.set(sector, {
|
|
56
|
+
y_invested_sum: 0,
|
|
57
|
+
t_invested_sum: 0
|
|
58
|
+
});
|
|
25
59
|
}
|
|
26
60
|
}
|
|
27
61
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
if (
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
const sector = this.
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
target[sector] += (pos.Invested || pos.Amount || 0); // Added fallback
|
|
62
|
+
_sumSectorAllocations(portfolio, mapKey) {
|
|
63
|
+
const positions = portfolio?.AggregatedPositions; // Must use Aggregated for 'Invested' %
|
|
64
|
+
if (!positions || !Array.isArray(positions) || !this.mappings) {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
for (const pos of positions) {
|
|
69
|
+
const instrumentId = pos.InstrumentID;
|
|
70
|
+
if (instrumentId) {
|
|
71
|
+
const sector = this.mappings.instrumentToSector[instrumentId] || 'Other';
|
|
72
|
+
this._initSector(sector);
|
|
73
|
+
|
|
74
|
+
const investedPct = pos.InvestedAmount || pos.Invested || 0;
|
|
75
|
+
this.sectors.get(sector)[mapKey] += investedPct;
|
|
43
76
|
}
|
|
44
77
|
}
|
|
45
|
-
// Optional: Handle PublicPositions if needed, though they lack 'Invested'
|
|
46
|
-
else if (portfolio && portfolio.PublicPositions) {
|
|
47
|
-
// Logic to handle PublicPositions if necessary, e.g., using 'Amount'
|
|
48
|
-
// console.warn("Using PublicPositions for sector rotation, 'Invested' amount missing.");
|
|
49
|
-
}
|
|
50
78
|
}
|
|
51
79
|
|
|
80
|
+
process(todayPortfolio, yesterdayPortfolio, userId, context) {
|
|
81
|
+
if (!todayPortfolio || !yesterdayPortfolio) {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
52
84
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
85
|
+
if (!this.mappings) {
|
|
86
|
+
// Context contains the mappings loaded in Pass 1
|
|
87
|
+
this.mappings = context.mappings;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
this._sumSectorAllocations(yesterdayPortfolio, 'y_invested_sum');
|
|
91
|
+
this._sumSectorAllocations(todayPortfolio, 't_invested_sum');
|
|
92
|
+
}
|
|
56
93
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
94
|
+
getResult() {
|
|
95
|
+
const result = {};
|
|
96
|
+
for (const [sector, data] of this.sectors.entries()) {
|
|
97
|
+
const netChange = data.t_invested_sum - data.y_invested_sum;
|
|
98
|
+
|
|
99
|
+
// Only report sectors that had some activity
|
|
100
|
+
if (data.t_invested_sum > 0 || data.y_invested_sum > 0) {
|
|
101
|
+
result[sector] = {
|
|
102
|
+
net_change_pct: netChange,
|
|
103
|
+
y_total_invested_pct: data.y_invested_sum,
|
|
104
|
+
t_total_invested_pct: data.t_invested_sum
|
|
105
|
+
};
|
|
106
|
+
}
|
|
61
107
|
}
|
|
108
|
+
return result;
|
|
109
|
+
}
|
|
62
110
|
|
|
63
|
-
|
|
111
|
+
reset() {
|
|
112
|
+
this.sectors.clear();
|
|
113
|
+
this.mappings = null;
|
|
64
114
|
}
|
|
65
115
|
}
|
|
66
116
|
|
|
67
|
-
|
|
68
117
|
module.exports = SectorRotation;
|