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,100 +1,195 @@
|
|
|
1
|
-
const { loadAllPriceData, getDailyPriceChange } = require('../../../utils/price_data_provider');
|
|
2
|
-
const { loadInstrumentMappings } = require('../../../utils/sector_mapping_provider');
|
|
3
|
-
|
|
4
1
|
/**
|
|
5
|
-
* @fileoverview
|
|
6
|
-
*
|
|
7
|
-
*
|
|
2
|
+
* @fileoverview Calculation (Pass 3) for "In Profit" cohort asset flow.
|
|
3
|
+
*
|
|
4
|
+
* This metric calculates the "Net Crowd Flow Percentage" for each asset,
|
|
5
|
+
* but *only* for the cohort of users who are currently *in profit*
|
|
6
|
+
* on that specific asset.
|
|
7
|
+
*
|
|
8
|
+
* This helps identify if winners are taking profit or adding to positions.
|
|
9
|
+
*
|
|
10
|
+
* This calculation *depends* on 'asset_pnl_status' to identify the cohort.
|
|
8
11
|
*/
|
|
12
|
+
const { loadInstrumentMappings } = require('../../../utils/sector_mapping_provider');
|
|
13
|
+
|
|
9
14
|
class InProfitAssetCrowdFlow {
|
|
10
15
|
constructor() {
|
|
11
|
-
this.
|
|
12
|
-
this.user_count = 0;
|
|
13
|
-
this.priceMap = null;
|
|
16
|
+
this.assetData = new Map();
|
|
14
17
|
this.mappings = null;
|
|
15
|
-
this.
|
|
18
|
+
this.inProfitCohorts = null; // Map<ticker, Set<userId>>
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Defines the output schema for this calculation.
|
|
23
|
+
* @returns {object} JSON Schema object
|
|
24
|
+
*/
|
|
25
|
+
static getSchema() {
|
|
26
|
+
return {
|
|
27
|
+
"type": "object",
|
|
28
|
+
"description": "Calculates net capital flow % (price-adjusted) per asset, but only for the cohort of users currently in profit on that asset.",
|
|
29
|
+
"patternProperties": {
|
|
30
|
+
// Ticker
|
|
31
|
+
"^.*$": {
|
|
32
|
+
"type": "object",
|
|
33
|
+
"description": "Net flow metrics for a specific asset ticker from its 'in profit' cohort.",
|
|
34
|
+
"properties": {
|
|
35
|
+
"net_flow_percentage": {
|
|
36
|
+
"type": "number",
|
|
37
|
+
"description": "Net capital flow % from the 'in profit' cohort, adjusted for price changes."
|
|
38
|
+
},
|
|
39
|
+
"total_invested_today": { "type": "number" },
|
|
40
|
+
"total_invested_yesterday": { "type": "number" },
|
|
41
|
+
"cohort_size": { "type": "number" }
|
|
42
|
+
},
|
|
43
|
+
"required": ["net_flow_percentage", "total_invested_today", "total_invested_yesterday", "cohort_size"]
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
"additionalProperties": {
|
|
47
|
+
"type": "object",
|
|
48
|
+
"properties": {
|
|
49
|
+
"net_flow_percentage": { "type": "number" },
|
|
50
|
+
"total_invested_today": { "type": "number" },
|
|
51
|
+
"total_invested_yesterday": { "type": "number" },
|
|
52
|
+
"cohort_size": { "type": "number" }
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Statically declare dependencies.
|
|
60
|
+
*/
|
|
61
|
+
static getDependencies() {
|
|
62
|
+
return ['asset_pnl_status'];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
_getPortfolioPositions(portfolio) {
|
|
66
|
+
return portfolio?.PublicPositions || portfolio?.AggregatedPositions;
|
|
16
67
|
}
|
|
17
68
|
|
|
18
69
|
_initAsset(instrumentId) {
|
|
19
|
-
if (!this.
|
|
20
|
-
this.
|
|
70
|
+
if (!this.assetData.has(instrumentId)) {
|
|
71
|
+
this.assetData.set(instrumentId, {
|
|
72
|
+
total_invested_yesterday: 0,
|
|
73
|
+
total_invested_today: 0,
|
|
74
|
+
price_change_yesterday: 0,
|
|
75
|
+
cohort: new Set()
|
|
76
|
+
});
|
|
21
77
|
}
|
|
22
78
|
}
|
|
23
79
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
80
|
+
/**
|
|
81
|
+
* Helper to get the cohort data from the dependency.
|
|
82
|
+
*/
|
|
83
|
+
_getInProfitCohorts(fetchedDependencies) {
|
|
84
|
+
if (this.inProfitCohorts) {
|
|
85
|
+
return this.inProfitCohorts;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const pnlStatusData = fetchedDependencies['asset_pnl_status'];
|
|
89
|
+
if (!pnlStatusData) {
|
|
90
|
+
return new Map();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Re-structure the data for efficient lookup
|
|
94
|
+
// Map<ticker, Set<userId>>
|
|
95
|
+
this.inProfitCohorts = new Map();
|
|
96
|
+
for (const [ticker, data] of Object.entries(pnlStatusData)) {
|
|
97
|
+
const userSet = new Set(data.users_in_profit.map(u => u.userId));
|
|
98
|
+
this.inProfitCohorts.set(ticker, userSet);
|
|
27
99
|
}
|
|
100
|
+
return this.inProfitCohorts;
|
|
101
|
+
}
|
|
28
102
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
103
|
+
process(todayPortfolio, yesterdayPortfolio, userId, context, todayInsights, yesterdayInsights, fetchedDependencies) {
|
|
104
|
+
if (!todayPortfolio || !yesterdayPortfolio) {
|
|
105
|
+
return;
|
|
32
106
|
}
|
|
33
107
|
|
|
34
|
-
|
|
35
|
-
|
|
108
|
+
if (!this.mappings) {
|
|
109
|
+
// Context contains the mappings loaded in Pass 1
|
|
110
|
+
this.mappings = context.mappings;
|
|
111
|
+
}
|
|
36
112
|
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
113
|
+
const cohorts = this._getInProfitCohorts(fetchedDependencies);
|
|
114
|
+
if (cohorts.size === 0) {
|
|
115
|
+
return; // No dependency data
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const yPos = this._getPortfolioPositions(yesterdayPortfolio);
|
|
119
|
+
const tPos = this._getPortfolioPositions(todayPortfolio);
|
|
120
|
+
|
|
121
|
+
const yPosMap = new Map(yPos?.map(p => [p.InstrumentID, p]) || []);
|
|
122
|
+
const tPosMap = new Map(tPos?.map(p => [p.InstrumentID, p]) || []);
|
|
123
|
+
|
|
124
|
+
const allInstrumentIds = new Set([...yPosMap.keys(), ...tPosMap.keys()]);
|
|
41
125
|
|
|
42
126
|
for (const instrumentId of allInstrumentIds) {
|
|
43
|
-
|
|
44
|
-
const tPos = todayPositions.get(instrumentId);
|
|
127
|
+
if (!instrumentId) continue;
|
|
45
128
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
// We check *today's* profit status as the primary signal.
|
|
49
|
-
const tNetProfit = tPos?.NetProfit || 0;
|
|
50
|
-
if (tNetProfit <= 0) {
|
|
51
|
-
continue; // Skip this asset for this user
|
|
52
|
-
}
|
|
53
|
-
// --- END COHORT LOGIC ---
|
|
129
|
+
const ticker = this.mappings.instrumentToTicker[instrumentId] || `id_${instrumentId}`;
|
|
130
|
+
const cohort = cohorts.get(ticker);
|
|
54
131
|
|
|
132
|
+
// This user is not in the "in profit" cohort for this asset, skip.
|
|
133
|
+
if (!cohort || !cohort.has(userId)) {
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// User *is* in the cohort, process their data
|
|
55
138
|
this._initAsset(instrumentId);
|
|
56
|
-
this.
|
|
57
|
-
|
|
139
|
+
const asset = this.assetData.get(instrumentId);
|
|
140
|
+
asset.cohort.add(userId); // Track cohort size
|
|
141
|
+
|
|
142
|
+
const yP = yPosMap.get(instrumentId);
|
|
143
|
+
const tP = tPosMap.get(instrumentId);
|
|
144
|
+
|
|
145
|
+
const yInvested = yP?.InvestedAmount || yP?.Amount || 0;
|
|
146
|
+
const tInvested = tP?.InvestedAmount || tP?.Amount || 0;
|
|
147
|
+
|
|
148
|
+
if (yInvested > 0) {
|
|
149
|
+
asset.total_invested_yesterday += yInvested;
|
|
150
|
+
const yPriceChange = (yP?.PipsRate || 0) / (yP?.OpenRate || 1);
|
|
151
|
+
asset.price_change_yesterday += yPriceChange * yInvested;
|
|
152
|
+
}
|
|
153
|
+
if (tInvested > 0) {
|
|
154
|
+
asset.total_invested_today += tInvested;
|
|
155
|
+
}
|
|
58
156
|
}
|
|
59
|
-
this.user_count++; // Note: This is user_count of *all* users, which is fine for avg.
|
|
60
157
|
}
|
|
61
158
|
|
|
62
159
|
async getResult() {
|
|
63
|
-
if (
|
|
64
|
-
|
|
65
|
-
const [priceData, mappingData] = await Promise.all([
|
|
66
|
-
loadAllPriceData(),
|
|
67
|
-
loadInstrumentMappings()
|
|
68
|
-
]);
|
|
69
|
-
this.priceMap = priceData;
|
|
70
|
-
this.mappings = mappingData;
|
|
160
|
+
if (!this.mappings) {
|
|
161
|
+
this.mappings = await loadInstrumentMappings();
|
|
71
162
|
}
|
|
72
|
-
|
|
73
|
-
const finalResults = {};
|
|
74
|
-
const todayStr = this.dates.today;
|
|
75
|
-
const yesterdayStr = this.dates.yesterday;
|
|
76
163
|
|
|
77
|
-
|
|
164
|
+
const result = {};
|
|
165
|
+
|
|
166
|
+
for (const [instrumentId, data] of this.assetData.entries()) {
|
|
78
167
|
const ticker = this.mappings.instrumentToTicker[instrumentId] || `id_${instrumentId}`;
|
|
79
|
-
|
|
80
|
-
const avg_day1_value = this.asset_values[instrumentId].day1_value_sum / this.user_count;
|
|
81
|
-
const avg_day2_value = this.asset_values[instrumentId].day2_value_sum / this.user_count;
|
|
82
|
-
const priceChangePct = getDailyPriceChange(instrumentId, yesterdayStr, todayStr, this.priceMap);
|
|
83
168
|
|
|
84
|
-
|
|
169
|
+
const { total_invested_yesterday, total_invested_today, price_change_yesterday, cohort } = data;
|
|
85
170
|
|
|
86
|
-
|
|
87
|
-
|
|
171
|
+
if (total_invested_yesterday > 0) {
|
|
172
|
+
const avg_price_change_pct = price_change_yesterday / total_invested_yesterday;
|
|
173
|
+
const price_contribution = total_invested_yesterday * avg_price_change_pct;
|
|
174
|
+
const flow_contribution = total_invested_today - (total_invested_yesterday + price_contribution);
|
|
175
|
+
const net_flow_percentage = (flow_contribution / total_invested_yesterday) * 100;
|
|
88
176
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
177
|
+
result[ticker] = {
|
|
178
|
+
net_flow_percentage: net_flow_percentage,
|
|
179
|
+
total_invested_today: total_invested_today,
|
|
180
|
+
total_invested_yesterday: total_invested_yesterday,
|
|
181
|
+
cohort_size: cohort.size
|
|
182
|
+
};
|
|
183
|
+
}
|
|
94
184
|
}
|
|
95
|
-
return
|
|
185
|
+
return result;
|
|
96
186
|
}
|
|
97
187
|
|
|
98
|
-
reset() {
|
|
188
|
+
reset() {
|
|
189
|
+
this.assetData.clear();
|
|
190
|
+
this.mappings = null;
|
|
191
|
+
this.inProfitCohorts = null;
|
|
192
|
+
}
|
|
99
193
|
}
|
|
194
|
+
|
|
100
195
|
module.exports = InProfitAssetCrowdFlow;
|
|
@@ -1,40 +1,107 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @fileoverview
|
|
2
|
+
* @fileoverview Calculation (Pass 2) for Paper vs Diamond Hands.
|
|
3
|
+
*
|
|
4
|
+
* This metric provides a simple ratio of positions that were
|
|
5
|
+
* closed today ("paper hands") vs. positions that were held
|
|
6
|
+
* ("diamond hands").
|
|
7
|
+
*
|
|
8
|
+
* It gives a general sense of market turnover.
|
|
3
9
|
*/
|
|
4
|
-
|
|
5
10
|
class PaperVsDiamondHands {
|
|
6
11
|
constructor() {
|
|
7
|
-
this.
|
|
8
|
-
this.
|
|
9
|
-
this.heldPositions = 0;
|
|
12
|
+
this.paper_hands = 0; // Positions closed
|
|
13
|
+
this.diamond_hands = 0; // Positions held
|
|
10
14
|
}
|
|
11
15
|
|
|
12
|
-
|
|
16
|
+
/**
|
|
17
|
+
* Defines the output schema for this calculation.
|
|
18
|
+
* @returns {object} JSON Schema object
|
|
19
|
+
*/
|
|
20
|
+
static getSchema() {
|
|
21
|
+
return {
|
|
22
|
+
"type": "object",
|
|
23
|
+
"description": "Calculates the ratio of positions closed ('paper hands') vs. held ('diamond hands') from yesterday to today.",
|
|
24
|
+
"properties": {
|
|
25
|
+
"paper_hands_count": {
|
|
26
|
+
"type": "number",
|
|
27
|
+
"description": "Total count of positions that existed yesterday but were closed today."
|
|
28
|
+
},
|
|
29
|
+
"diamond_hands_count": {
|
|
30
|
+
"type": "number",
|
|
31
|
+
"description": "Total count of positions that existed yesterday and are still held today."
|
|
32
|
+
},
|
|
33
|
+
"total_positions_yesterday": {
|
|
34
|
+
"type": "number",
|
|
35
|
+
"description": "The sum of paper and diamond hands counts."
|
|
36
|
+
},
|
|
37
|
+
"paper_hands_ratio": {
|
|
38
|
+
"type": "number",
|
|
39
|
+
"description": "Ratio of paper hands to diamond hands (Paper / Diamond). Null if no diamond hands."
|
|
40
|
+
},
|
|
41
|
+
"paper_hands_pct": {
|
|
42
|
+
"type": "number",
|
|
43
|
+
"description": "Percentage of positions that were 'paper handed' (closed)."
|
|
44
|
+
},
|
|
45
|
+
"diamond_hands_pct": {
|
|
46
|
+
"type": "number",
|
|
47
|
+
"description": "Percentage of positions that were 'diamond handed' (held)."
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
"required": ["paper_hands_count", "diamond_hands_count", "total_positions_yesterday", "paper_hands_pct", "diamond_hands_pct"]
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
_getPortfolioPositionIds(portfolio) {
|
|
55
|
+
// We MUST use PositionID to track specific trades
|
|
56
|
+
const positions = portfolio?.AggregatedPositions || portfolio?.PublicPositions;
|
|
57
|
+
if (!positions || !Array.isArray(positions)) {
|
|
58
|
+
return new Set();
|
|
59
|
+
}
|
|
60
|
+
return new Set(positions.map(p => p.PositionID).filter(Boolean));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
process(todayPortfolio, yesterdayPortfolio) {
|
|
13
64
|
if (!todayPortfolio || !yesterdayPortfolio) {
|
|
14
65
|
return;
|
|
15
66
|
}
|
|
16
67
|
|
|
17
|
-
const
|
|
18
|
-
const
|
|
68
|
+
const yPosIds = this._getPortfolioPositionIds(yesterdayPortfolio);
|
|
69
|
+
const tPosIds = this._getPortfolioPositionIds(todayPortfolio);
|
|
19
70
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
71
|
+
if (yPosIds.size === 0) {
|
|
72
|
+
return; // No positions yesterday to analyze
|
|
73
|
+
}
|
|
23
74
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
75
|
+
for (const yPosId of yPosIds) {
|
|
76
|
+
if (tPosIds.has(yPosId)) {
|
|
77
|
+
// Position was held
|
|
78
|
+
this.diamond_hands++;
|
|
79
|
+
} else {
|
|
80
|
+
// Position was closed
|
|
81
|
+
this.paper_hands++;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
27
84
|
}
|
|
28
85
|
|
|
29
86
|
getResult() {
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
87
|
+
const total = this.paper_hands + this.diamond_hands;
|
|
88
|
+
|
|
33
89
|
return {
|
|
34
|
-
|
|
35
|
-
|
|
90
|
+
paper_hands_count: this.paper_hands,
|
|
91
|
+
diamond_hands_count: this.diamond_hands,
|
|
92
|
+
total_positions_yesterday: total,
|
|
93
|
+
// Ratio of paper-to-diamond. Can be null if diamond_hands is 0.
|
|
94
|
+
paper_hands_ratio: (this.diamond_hands > 0) ? (this.paper_hands / this.diamond_hands) : null,
|
|
95
|
+
// Percentage of total positions
|
|
96
|
+
paper_hands_pct: (total > 0) ? (this.paper_hands / total) * 100 : 0,
|
|
97
|
+
diamond_hands_pct: (total > 0) ? (this.diamond_hands / total) * 100 : 0,
|
|
36
98
|
};
|
|
37
99
|
}
|
|
100
|
+
|
|
101
|
+
reset() {
|
|
102
|
+
this.paper_hands = 0;
|
|
103
|
+
this.diamond_hands = 0;
|
|
104
|
+
}
|
|
38
105
|
}
|
|
39
106
|
|
|
40
107
|
module.exports = PaperVsDiamondHands;
|
|
@@ -1,67 +1,119 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
2
|
+
* @fileoverview Calculation (Pass 2) for P&L by position count.
|
|
3
|
+
*
|
|
4
|
+
* This metric answers: "What is the average daily P&L for users,
|
|
5
|
+
* bucketed by the number of positions they hold?"
|
|
6
|
+
*
|
|
7
|
+
* This helps determine if holding more positions (diversifying)
|
|
8
|
+
* correlates with better or worse P&L.
|
|
4
9
|
*/
|
|
5
10
|
class PositionCountPnl {
|
|
6
11
|
constructor() {
|
|
7
|
-
// We will store
|
|
8
|
-
this.
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
}
|
|
12
|
+
// We will store { [count_bucket]: { pnl_sum: 0, user_count: 0 } }
|
|
13
|
+
this.buckets = {
|
|
14
|
+
'1': { pnl_sum: 0, user_count: 0 },
|
|
15
|
+
'2-5': { pnl_sum: 0, user_count: 0 },
|
|
16
|
+
'6-10': { pnl_sum: 0, user_count: 0 },
|
|
17
|
+
'11-20': { pnl_sum: 0, user_count: 0 },
|
|
18
|
+
'21+': { pnl_sum: 0, user_count: 0 },
|
|
19
|
+
};
|
|
15
20
|
}
|
|
16
21
|
|
|
17
22
|
/**
|
|
18
|
-
*
|
|
19
|
-
* @
|
|
20
|
-
* @returns {number|null}
|
|
23
|
+
* Defines the output schema for this calculation.
|
|
24
|
+
* @returns {object} JSON Schema object
|
|
21
25
|
*/
|
|
22
|
-
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
static getSchema() {
|
|
27
|
+
const bucketSchema = {
|
|
28
|
+
"type": "object",
|
|
29
|
+
"description": "Aggregated P&L metrics for a position count 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 positions a user holds.",
|
|
50
|
+
"properties": {
|
|
51
|
+
"1": bucketSchema,
|
|
52
|
+
"2-5": bucketSchema,
|
|
53
|
+
"6-10": bucketSchema,
|
|
54
|
+
"11-20": bucketSchema,
|
|
55
|
+
"21+": bucketSchema
|
|
56
|
+
},
|
|
57
|
+
"required": ["1", "2-5", "6-10", "11-20", "21+"]
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
_getBucket(count) {
|
|
62
|
+
if (count === 1) return '1';
|
|
63
|
+
if (count >= 2 && count <= 5) return '2-5';
|
|
64
|
+
if (count >= 6 && count <= 10) return '6-10';
|
|
65
|
+
if (count >= 11 && count <= 20) return '11-20';
|
|
66
|
+
if (count >= 21) return '21+';
|
|
28
67
|
return null;
|
|
29
68
|
}
|
|
30
69
|
|
|
31
|
-
process(todayPortfolio, yesterdayPortfolio
|
|
32
|
-
//
|
|
70
|
+
process(todayPortfolio, yesterdayPortfolio) {
|
|
71
|
+
// This calculation only needs today's portfolio state
|
|
33
72
|
if (!todayPortfolio) {
|
|
34
73
|
return;
|
|
35
74
|
}
|
|
36
75
|
|
|
37
76
|
const positions = todayPortfolio.AggregatedPositions || todayPortfolio.PublicPositions;
|
|
77
|
+
const positionCount = Array.isArray(positions) ? positions.length : 0;
|
|
38
78
|
|
|
39
|
-
if (!positions || !Array.isArray(positions)) {
|
|
40
|
-
return; // Skip users with no positions array
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
const positionCount = positions.length;
|
|
44
79
|
if (positionCount === 0) {
|
|
45
|
-
return;
|
|
80
|
+
return;
|
|
46
81
|
}
|
|
47
82
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
if (dailyPnl === null) {
|
|
52
|
-
return; // Cannot calculate P&L for this user
|
|
83
|
+
const bucketKey = this._getBucket(positionCount);
|
|
84
|
+
if (!bucketKey) {
|
|
85
|
+
return;
|
|
53
86
|
}
|
|
54
87
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
88
|
+
// Use the P&L from the summary, which is for the *day*
|
|
89
|
+
const dailyPnl = todayPortfolio.Summary?.NetProfit || 0;
|
|
90
|
+
|
|
91
|
+
const bucket = this.buckets[bucketKey];
|
|
92
|
+
bucket.pnl_sum += dailyPnl;
|
|
93
|
+
bucket.user_count++;
|
|
58
94
|
}
|
|
59
95
|
|
|
60
96
|
getResult() {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
97
|
+
const result = {};
|
|
98
|
+
for (const key in this.buckets) {
|
|
99
|
+
const bucket = this.buckets[key];
|
|
100
|
+
result[key] = {
|
|
101
|
+
average_daily_pnl: (bucket.user_count > 0) ? (bucket.pnl_sum / bucket.user_count) : 0,
|
|
102
|
+
user_count: bucket.user_count,
|
|
103
|
+
pnl_sum: bucket.pnl_sum
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
return result;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
reset() {
|
|
110
|
+
this.buckets = {
|
|
111
|
+
'1': { pnl_sum: 0, user_count: 0 },
|
|
112
|
+
'2-5': { pnl_sum: 0, user_count: 0 },
|
|
113
|
+
'6-10': { pnl_sum: 0, user_count: 0 },
|
|
114
|
+
'11-20': { pnl_sum: 0, user_count: 0 },
|
|
115
|
+
'21+': { pnl_sum: 0, user_count: 0 },
|
|
116
|
+
};
|
|
65
117
|
}
|
|
66
118
|
}
|
|
67
119
|
|