aiden-shared-calculations-unified 1.0.95 → 1.0.97
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/calculations/capitulation/asset-volatility-estimator.js +1 -2
- package/calculations/capitulation/retail-capitulation-risk-forecast.js +1 -2
- package/calculations/core/Insights-total-long-per-stock +56 -0
- package/calculations/core/insights-daily-bought-vs-sold-count.js +74 -0
- package/calculations/core/insights-daily-ownership-delta.js +70 -0
- package/calculations/core/insights-sentimet-per-stock.js +68 -0
- package/calculations/core/insights-total-long-per-sector +73 -0
- package/calculations/core/insights-total-positions-held.js +49 -0
- package/calculations/ghost-book/cost-basis-density.js +1 -2
- package/calculations/ghost-book/liquidity-vacuum.js +4 -4
- package/calculations/ghost-book/retail-gamma-exposure.js +0 -1
- package/calculations/helix/winner-loser-flow.js +1 -1
- package/calculations/predicative-alpha/cognitive-dissonance.js +1 -2
- package/calculations/predicative-alpha/diamond-hand-fracture.js +1 -2
- package/calculations/predicative-alpha/mimetic-latency.js +1 -2
- package/package.json +1 -1
- package/calculations/legacy/activity_by_pnl_status.js +0 -119
- package/calculations/legacy/asset_crowd_flow.js +0 -163
- package/calculations/legacy/capital_deployment_strategy.js +0 -108
- package/calculations/legacy/capital_liquidation_performance.js +0 -139
- package/calculations/legacy/capital_vintage_performance.js +0 -136
- package/calculations/legacy/cash-flow-deployment.js +0 -144
- package/calculations/legacy/cash-flow-liquidation.js +0 -146
- package/calculations/legacy/crowd-cash-flow-proxy.js +0 -128
- package/calculations/legacy/crowd_conviction_score.js +0 -261
- package/calculations/legacy/crowd_sharpe_ratio_proxy.js +0 -137
- package/calculations/legacy/daily_asset_activity.js +0 -128
- package/calculations/legacy/daily_user_activity_tracker.js +0 -182
- package/calculations/legacy/deposit_withdrawal_percentage.js +0 -125
- package/calculations/legacy/diversification_pnl.js +0 -115
- package/calculations/legacy/drawdown_response.js +0 -137
- package/calculations/legacy/dumb-cohort-flow.js +0 -238
- package/calculations/legacy/gain_response.js +0 -137
- package/calculations/legacy/historical_performance_aggregator.js +0 -85
- package/calculations/legacy/in_loss_asset_crowd_flow.js +0 -168
- package/calculations/legacy/in_profit_asset_crowd_flow.js +0 -168
- package/calculations/legacy/negative_expectancy_cohort_flow.js +0 -232
- package/calculations/legacy/new_allocation_percentage.js +0 -98
- package/calculations/legacy/paper_vs_diamond_hands.js +0 -107
- package/calculations/legacy/position_count_pnl.js +0 -120
- package/calculations/legacy/positive_expectancy_cohort_flow.js +0 -232
- package/calculations/legacy/profit_cohort_divergence.js +0 -115
- package/calculations/legacy/profitability_migration.js +0 -104
- package/calculations/legacy/reallocation_increase_percentage.js +0 -104
- package/calculations/legacy/risk_appetite_change.js +0 -97
- package/calculations/legacy/sector_rotation.js +0 -117
- package/calculations/legacy/shark_attack_signal.js +0 -112
- package/calculations/legacy/smart-cohort-flow.js +0 -238
- package/calculations/legacy/smart-dumb-divergence-index.js +0 -143
- package/calculations/legacy/smart_dumb_divergence_index_v2.js +0 -138
- package/calculations/legacy/smart_money_flow.js +0 -198
- package/calculations/legacy/social-predictive-regime-state.js +0 -102
- package/calculations/legacy/social-topic-driver-index.js +0 -147
- package/calculations/legacy/social-topic-predictive-potential.js +0 -461
- package/calculations/legacy/social_flow_correlation.js +0 -112
- package/calculations/legacy/speculator_adjustment_activity.js +0 -103
- package/calculations/legacy/strategy-performance.js +0 -265
- package/calculations/legacy/tsl_effectiveness.js +0 -85
- package/calculations/legacy/user-investment-profile.js +0 -313
- package/calculations/legacy/user_expectancy_score.js +0 -106
- package/calculations/legacy/user_profitability_tracker.js +0 -131
|
@@ -1,128 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @fileoverview Tracks the flow of unique users opening or closing positions
|
|
3
|
-
* on a per-asset basis. This measures the "focus" of the crowd's activity.
|
|
4
|
-
*/
|
|
5
|
-
const { loadInstrumentMappings } = require('../../utils/sector_mapping_provider');
|
|
6
|
-
|
|
7
|
-
class DailyAssetActivity {
|
|
8
|
-
constructor() {
|
|
9
|
-
// We will store { [instrumentId]: { new_users: Set(), closed_users: Set() } }
|
|
10
|
-
this.assetActivity = new Map();
|
|
11
|
-
this.mappings = null;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Defines the output schema for this calculation.
|
|
16
|
-
* @returns {object} JSON Schema object
|
|
17
|
-
*/
|
|
18
|
-
static getSchema() {
|
|
19
|
-
return {
|
|
20
|
-
"type": "object",
|
|
21
|
-
"description": "Tracks the net flow of unique users opening or closing positions per asset.",
|
|
22
|
-
"patternProperties": {
|
|
23
|
-
// This matches any string key (which will be a ticker)
|
|
24
|
-
"^.*$": {
|
|
25
|
-
"type": "object",
|
|
26
|
-
"description": "User flow metrics for a specific asset ticker.",
|
|
27
|
-
"properties": {
|
|
28
|
-
"opened_by_user_count": {
|
|
29
|
-
"type": "number",
|
|
30
|
-
"description": "Count of unique users who opened a position in this asset."
|
|
31
|
-
},
|
|
32
|
-
"closed_by_user_count": {
|
|
33
|
-
"type": "number",
|
|
34
|
-
"description": "Count of unique users who closed their position in this asset."
|
|
35
|
-
},
|
|
36
|
-
"net_user_flow": {
|
|
37
|
-
"type": "number",
|
|
38
|
-
"description": "Net change in users (opened - closed)."
|
|
39
|
-
}
|
|
40
|
-
},
|
|
41
|
-
"required": ["opened_by_user_count", "closed_by_user_count", "net_user_flow"]
|
|
42
|
-
}
|
|
43
|
-
},
|
|
44
|
-
// Use 'additionalProperties' as a fallback for patternProperties
|
|
45
|
-
"additionalProperties": {
|
|
46
|
-
"type": "object",
|
|
47
|
-
"properties": {
|
|
48
|
-
"opened_by_user_count": { "type": "number" },
|
|
49
|
-
"closed_by_user_count": { "type": "number" },
|
|
50
|
-
"net_user_flow": { "type": "number" }
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
};
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
_initAsset(instrumentId) {
|
|
57
|
-
if (!this.assetActivity.has(instrumentId)) {
|
|
58
|
-
this.assetActivity.set(instrumentId, {
|
|
59
|
-
new_users: new Set(),
|
|
60
|
-
closed_users: new Set()
|
|
61
|
-
});
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
_getInstrumentIds(portfolio) {
|
|
66
|
-
const positions = portfolio?.AggregatedPositions || portfolio?.PublicPositions;
|
|
67
|
-
if (!positions || !Array.isArray(positions)) {
|
|
68
|
-
return new Set();
|
|
69
|
-
}
|
|
70
|
-
return new Set(positions.map(p => p.InstrumentID).filter(Boolean));
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
process(todayPortfolio, yesterdayPortfolio, userId) {
|
|
74
|
-
if (!todayPortfolio || !yesterdayPortfolio) {
|
|
75
|
-
return;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
const yIds = this._getInstrumentIds(yesterdayPortfolio);
|
|
79
|
-
const tIds = this._getInstrumentIds(todayPortfolio);
|
|
80
|
-
|
|
81
|
-
// Find new positions (in today but not yesterday)
|
|
82
|
-
for (const tId of tIds) {
|
|
83
|
-
if (!yIds.has(tId)) {
|
|
84
|
-
this._initAsset(tId);
|
|
85
|
-
this.assetActivity.get(tId).new_users.add(userId);
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// Find closed positions (in yesterday but not today)
|
|
90
|
-
for (const yId of yIds) {
|
|
91
|
-
if (!tIds.has(yId)) {
|
|
92
|
-
this._initAsset(yId);
|
|
93
|
-
this.assetActivity.get(yId).closed_users.add(userId);
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
async getResult() {
|
|
99
|
-
if (!this.mappings) {
|
|
100
|
-
this.mappings = await loadInstrumentMappings();
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
const result = {};
|
|
104
|
-
for (const [instrumentId, data] of this.assetActivity.entries()) {
|
|
105
|
-
const ticker = this.mappings.instrumentToTicker[instrumentId] || `id_${instrumentId}`;
|
|
106
|
-
|
|
107
|
-
const openCount = data.new_users.size;
|
|
108
|
-
const closeCount = data.closed_users.size;
|
|
109
|
-
|
|
110
|
-
if (openCount > 0 || closeCount > 0) {
|
|
111
|
-
result[ticker] = {
|
|
112
|
-
opened_by_user_count: openCount,
|
|
113
|
-
closed_by_user_count: closeCount,
|
|
114
|
-
// "Net User Flow" - positive means more users joined than left
|
|
115
|
-
net_user_flow: openCount - closeCount
|
|
116
|
-
};
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
return result;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
reset() {
|
|
123
|
-
this.assetActivity.clear();
|
|
124
|
-
this.mappings = null;
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
module.exports = DailyAssetActivity;
|
|
@@ -1,182 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @fileoverview Tracks user activity by comparing portfolio snapshots.
|
|
3
|
-
* This is a historical calculation that defines an "active user" as someone
|
|
4
|
-
* who has opened, closed, or reallocated a position within the last 24 hours.
|
|
5
|
-
*
|
|
6
|
-
* This provides the "Daily Active Users" count for the monitored cohort.
|
|
7
|
-
* This depreciates the user activity sampler cloud function which was inefficient for the api usage and now provides this data for free
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
class DailyUserActivityTracker {
|
|
11
|
-
constructor() {
|
|
12
|
-
this.activeUserIds = new Set();
|
|
13
|
-
this.activityEvents = {
|
|
14
|
-
new_position: 0,
|
|
15
|
-
closed_position: 0,
|
|
16
|
-
reallocation: 0
|
|
17
|
-
};
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Defines the output schema for this calculation.
|
|
22
|
-
* @returns {object} JSON Schema object
|
|
23
|
-
*/
|
|
24
|
-
static getSchema() {
|
|
25
|
-
return {
|
|
26
|
-
"type": "object",
|
|
27
|
-
"description": "Tracks the count of daily active users based on portfolio changes.",
|
|
28
|
-
"properties": {
|
|
29
|
-
"rawActiveUserCount": {
|
|
30
|
-
"type": "number",
|
|
31
|
-
"description": "The total count of unique users who were active today (opened, closed, or reallocated)."
|
|
32
|
-
},
|
|
33
|
-
"activityBreakdown": {
|
|
34
|
-
"type": "object",
|
|
35
|
-
"description": "A breakdown of the first activity event type recorded for active users.",
|
|
36
|
-
"properties": {
|
|
37
|
-
"new_position": {
|
|
38
|
-
"type": "number",
|
|
39
|
-
"description": "Count of users whose first detected activity was opening a new position."
|
|
40
|
-
},
|
|
41
|
-
"closed_position": {
|
|
42
|
-
"type": "number",
|
|
43
|
-
"description": "Count of users whose first detected activity was closing an existing position."
|
|
44
|
-
},
|
|
45
|
-
"reallocation": {
|
|
46
|
-
"type": "number",
|
|
47
|
-
"description": "Count of users whose first detected activity was reallocating an existing position."
|
|
48
|
-
}
|
|
49
|
-
},
|
|
50
|
-
"required": ["new_position", "closed_position", "reallocation"]
|
|
51
|
-
}
|
|
52
|
-
},
|
|
53
|
-
"required": ["rawActiveUserCount", "activityBreakdown"]
|
|
54
|
-
};
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* Helper to get a simplified map of positions for comparison.
|
|
59
|
-
* @param {object} portfolio - A user's full portfolio object.
|
|
60
|
-
* @returns {object} { posMap: Map<InstrumentID, {invested: number}>, hasAggregated: boolean }
|
|
61
|
-
*/
|
|
62
|
-
_getPortfolioMaps(portfolio) {
|
|
63
|
-
// Prioritize AggregatedPositions, but fall back to PublicPositions
|
|
64
|
-
const positions = portfolio?.AggregatedPositions || portfolio?.PublicPositions;
|
|
65
|
-
if (!positions || !Array.isArray(positions)) {
|
|
66
|
-
return { posMap: new Map(), hasAggregated: false };
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
const posMap = new Map();
|
|
70
|
-
for (const pos of positions) {
|
|
71
|
-
const key = pos.InstrumentID;
|
|
72
|
-
if (key) {
|
|
73
|
-
posMap.set(key, {
|
|
74
|
-
// 'InvestedAmount' or 'Invested' is the portfolio percentage
|
|
75
|
-
// We use this for reallocation logic.
|
|
76
|
-
invested: pos.InvestedAmount || pos.Invested || pos.Amount || 0
|
|
77
|
-
});
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
// Return the map and a flag indicating if we can trust the 'invested' field
|
|
81
|
-
return { posMap, hasAggregated: !!portfolio.AggregatedPositions };
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* Processes a single user's daily data.
|
|
86
|
-
*/
|
|
87
|
-
process(todayPortfolio, yesterdayPortfolio, userId) {
|
|
88
|
-
// This calculation requires both days to find changes.
|
|
89
|
-
if (!todayPortfolio || !yesterdayPortfolio) {
|
|
90
|
-
return;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
const { posMap: yPosMap, hasAggregated: yHasAgg } = this._getPortfolioMaps(yesterdayPortfolio);
|
|
94
|
-
const { posMap: tPosMap, hasAggregated: tHasAgg } = this._getPortfolioMaps(todayPortfolio);
|
|
95
|
-
|
|
96
|
-
// Skip if user has no positions on either day
|
|
97
|
-
if (tPosMap.size === 0 && yPosMap.size === 0) {
|
|
98
|
-
return;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
const yIds = new Set(yPosMap.keys());
|
|
102
|
-
const tIds = new Set(tPosMap.keys());
|
|
103
|
-
let isActive = false;
|
|
104
|
-
|
|
105
|
-
// 1. Check for new positions (high-confidence activity)
|
|
106
|
-
for (const tId of tIds) {
|
|
107
|
-
if (!yIds.has(tId)) {
|
|
108
|
-
isActive = true;
|
|
109
|
-
this.activityEvents.new_position++;
|
|
110
|
-
break; // Found activity, no need to check more
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
if (isActive) {
|
|
115
|
-
this.activeUserIds.add(userId);
|
|
116
|
-
return;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
// 2. Check for closed positions (high-confidence activity)
|
|
120
|
-
for (const yId of yIds) {
|
|
121
|
-
if (!tIds.has(yId)) {
|
|
122
|
-
isActive = true;
|
|
123
|
-
this.activityEvents.closed_position++;
|
|
124
|
-
break; // Found activity
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
if (isActive) {
|
|
129
|
-
this.activeUserIds.add(userId);
|
|
130
|
-
return;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
// 3. Check for reallocation (only possible if we have AggregatedPositions for both days)
|
|
134
|
-
// This checks for changes in the 'Invested' percentage
|
|
135
|
-
// 3. Check for reallocation (only possible if we have AggregatedPositions for both days)
|
|
136
|
-
if (yHasAgg && tHasAgg) {
|
|
137
|
-
for (const tId of tIds) {
|
|
138
|
-
const tInvested = tPosMap.get(tId).invested;
|
|
139
|
-
const yInvested = yPosMap.get(tId)?.invested ?? 0;
|
|
140
|
-
|
|
141
|
-
// Check for a meaningful change (e.g., > 0.01% to avoid float noise)
|
|
142
|
-
if (Math.abs(tInvested - yInvested) > 0.0001) {
|
|
143
|
-
isActive = true;
|
|
144
|
-
this.activityEvents.reallocation++;
|
|
145
|
-
break;
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
if (isActive) {
|
|
152
|
-
this.activeUserIds.add(userId);
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
/**
|
|
157
|
-
* Returns the final aggregated counts for the day.
|
|
158
|
-
*/
|
|
159
|
-
getResult() {
|
|
160
|
-
return {
|
|
161
|
-
// This is the main metric for your graph
|
|
162
|
-
rawActiveUserCount: this.activeUserIds.size,
|
|
163
|
-
|
|
164
|
-
// This is a bonus metric to see *what* users are doing
|
|
165
|
-
activityBreakdown: this.activityEvents
|
|
166
|
-
};
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
/**
|
|
170
|
-
* Resets the counters for the next run.
|
|
171
|
-
*/
|
|
172
|
-
reset() {
|
|
173
|
-
this.activeUserIds.clear();
|
|
174
|
-
this.activityEvents = {
|
|
175
|
-
new_position: 0,
|
|
176
|
-
closed_position: 0,
|
|
177
|
-
reallocation: 0
|
|
178
|
-
};
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
module.exports = DailyUserActivityTracker;
|
|
@@ -1,125 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @fileoverview Calculation (Pass 2) for deposit/withdrawal percentage.
|
|
3
|
-
*
|
|
4
|
-
* This metric estimates the net cash flow (deposits vs. withdrawals)
|
|
5
|
-
* by analyzing changes in portfolio percentages.
|
|
6
|
-
*
|
|
7
|
-
* It's a proxy based on the assumption that changes in the *sum* of
|
|
8
|
-
* position percentages not accounted for by P&L represent new cash.
|
|
9
|
-
*
|
|
10
|
-
* (DEPRECIATED by crowd-cash-flow-proxy, but kept for reference)
|
|
11
|
-
*/
|
|
12
|
-
class DepositWithdrawalPercentage {
|
|
13
|
-
constructor() {
|
|
14
|
-
this.invested_pct_yesterday_sum = 0;
|
|
15
|
-
this.invested_pct_today_sum = 0;
|
|
16
|
-
this.pnl_pct_today_sum = 0;
|
|
17
|
-
this.user_count = 0;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Defines the output schema for this calculation.
|
|
22
|
-
* @returns {object} JSON Schema object
|
|
23
|
-
*/
|
|
24
|
-
static getSchema() {
|
|
25
|
-
return {
|
|
26
|
-
"type": "object",
|
|
27
|
-
"description": "(DEPRECIATED) Estimates net cash flow by analyzing changes in portfolio position percentages.",
|
|
28
|
-
"properties": {
|
|
29
|
-
"estimated_net_flow_pct": {
|
|
30
|
-
"type": "number",
|
|
31
|
-
"description": "The estimated net cash flow (deposit/withdrawal) as an average percentage change across all users."
|
|
32
|
-
},
|
|
33
|
-
"user_count": {
|
|
34
|
-
"type": "number",
|
|
35
|
-
"description": "Number of users processed."
|
|
36
|
-
},
|
|
37
|
-
"debug": {
|
|
38
|
-
"type": "object",
|
|
39
|
-
"properties": {
|
|
40
|
-
"invested_pct_yesterday_sum": { "type": "number" },
|
|
41
|
-
"invested_pct_today_sum": { "type": "number" },
|
|
42
|
-
"pnl_pct_today_sum": { "type": "number" }
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
},
|
|
46
|
-
"required": ["estimated_net_flow_pct", "user_count"]
|
|
47
|
-
};
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
_getPortfolioSums(portfolio) {
|
|
51
|
-
const positions = portfolio?.AggregatedPositions;
|
|
52
|
-
if (!positions || !Array.isArray(positions)) {
|
|
53
|
-
return { investedPct: 0, pnlPct: 0 };
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
let investedPct = 0;
|
|
57
|
-
let pnlPct = 0;
|
|
58
|
-
|
|
59
|
-
for (const pos of positions) {
|
|
60
|
-
investedPct += pos.InvestedAmount || pos.Invested || 0;
|
|
61
|
-
pnlPct += (pos.NetProfit || 0) / 100; // Assuming NetProfit is in %
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
return { investedPct, pnlPct };
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
process(todayPortfolio, yesterdayPortfolio) {
|
|
68
|
-
if (!todayPortfolio || !yesterdayPortfolio) {
|
|
69
|
-
return;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
const ySums = this._getPortfolioSums(yesterdayPortfolio);
|
|
73
|
-
const tSums = this._getPortfolioSums(todayPortfolio);
|
|
74
|
-
|
|
75
|
-
if (ySums.investedPct === 0 && tSums.investedPct === 0) {
|
|
76
|
-
return; // Skip users with no positions
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
this.invested_pct_yesterday_sum += ySums.investedPct;
|
|
80
|
-
this.invested_pct_today_sum += tSums.investedPct;
|
|
81
|
-
this.pnl_pct_today_sum += tSums.pnlPct; // Use today's P&L
|
|
82
|
-
this.user_count++;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
getResult() {
|
|
86
|
-
if (this.user_count === 0) {
|
|
87
|
-
return {
|
|
88
|
-
estimated_net_flow_pct: 0,
|
|
89
|
-
user_count: 0,
|
|
90
|
-
debug: {
|
|
91
|
-
invested_pct_yesterday_sum: 0,
|
|
92
|
-
invested_pct_today_sum: 0,
|
|
93
|
-
pnl_pct_today_sum: 0
|
|
94
|
-
}
|
|
95
|
-
};
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
// Avg % change in invested capital
|
|
99
|
-
const avg_invested_change = (this.invested_pct_today_sum - this.invested_pct_yesterday_sum) / this.user_count;
|
|
100
|
-
// Avg P&L
|
|
101
|
-
const avg_pnl = this.pnl_pct_today_sum / this.user_count;
|
|
102
|
-
|
|
103
|
-
// The change not accounted for by P&L is the proxy flow
|
|
104
|
-
const net_flow_pct = avg_invested_change - avg_pnl;
|
|
105
|
-
|
|
106
|
-
return {
|
|
107
|
-
estimated_net_flow_pct: net_flow_pct,
|
|
108
|
-
user_count: this.user_count,
|
|
109
|
-
debug: {
|
|
110
|
-
invested_pct_yesterday_sum: this.invested_pct_yesterday_sum,
|
|
111
|
-
invested_pct_today_sum: this.invested_pct_today_sum,
|
|
112
|
-
pnl_pct_today_sum: this.pnl_pct_today_sum
|
|
113
|
-
}
|
|
114
|
-
};
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
reset() {
|
|
118
|
-
this.invested_pct_yesterday_sum = 0;
|
|
119
|
-
this.invested_pct_today_sum = 0;
|
|
120
|
-
this.pnl_pct_today_sum = 0;
|
|
121
|
-
this.user_count = 0;
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
module.exports = DepositWithdrawalPercentage;
|
|
@@ -1,115 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @fileoverview Calculation (Pass 1) for Diversification vs. P&L.
|
|
3
|
-
*
|
|
4
|
-
* This metric answers: "Is there a correlation between the number
|
|
5
|
-
* of sectors a user is invested in and their average P&L?"
|
|
6
|
-
*
|
|
7
|
-
* REFACTOR: This calculation now aggregates the results on the server,
|
|
8
|
-
* returning the average P&L per score, not raw data arrays.
|
|
9
|
-
*/
|
|
10
|
-
class DiversificationPnl {
|
|
11
|
-
constructor() {
|
|
12
|
-
// Stores { [score]: { pnlSum: 0, count: 0 } }
|
|
13
|
-
// The 'score' is the number of unique sectors
|
|
14
|
-
this.pnlByScore = new Map();
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Defines the output schema for this calculation.
|
|
19
|
-
* REFACTOR: Schema now describes the final aggregated result.
|
|
20
|
-
* @returns {object} JSON Schema object
|
|
21
|
-
*/
|
|
22
|
-
static getSchema() {
|
|
23
|
-
const scoreSchema = {
|
|
24
|
-
"type": "object",
|
|
25
|
-
"description": "Aggregated P&L data for a specific diversification score.",
|
|
26
|
-
"properties": {
|
|
27
|
-
"average_pnl": {
|
|
28
|
-
"type": "number",
|
|
29
|
-
"description": "The average P&L (in USD) for all users with this score."
|
|
30
|
-
},
|
|
31
|
-
"user_count": {
|
|
32
|
-
"type": "number",
|
|
33
|
-
"description": "The number of users who had this diversification score."
|
|
34
|
-
}
|
|
35
|
-
},
|
|
36
|
-
"required": ["average_pnl", "user_count"]
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
return {
|
|
40
|
-
"type": "object",
|
|
41
|
-
"description": "Calculates the average P&L correlated by diversification score (number of unique sectors).",
|
|
42
|
-
"patternProperties": {
|
|
43
|
-
"^[0-9]+$": scoreSchema // Key is the score (e.g., "1", "2")
|
|
44
|
-
},
|
|
45
|
-
"additionalProperties": scoreSchema
|
|
46
|
-
};
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
_initScore(score) {
|
|
50
|
-
if (!this.pnlByScore.has(score)) {
|
|
51
|
-
this.pnlByScore.set(score, { pnlSum: 0, count: 0 });
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* @param {object} todayPortfolio
|
|
57
|
-
* @param {object} yesterdayPortfolio
|
|
58
|
-
*/
|
|
59
|
-
process(todayPortfolio, yesterdayPortfolio) {
|
|
60
|
-
const tPositions = todayPortfolio.PublicPositions || todayPortfolio.AggregatedPositions;
|
|
61
|
-
if (!tPositions || !Array.isArray(tPositions) || tPositions.length === 0) {
|
|
62
|
-
return;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
const yPortfolio = yesterdayPortfolio.Portfolio;
|
|
66
|
-
if (!yPortfolio) return;
|
|
67
|
-
|
|
68
|
-
// 1. Calculate diversification score (count of unique sectors)
|
|
69
|
-
const sectors = new Set();
|
|
70
|
-
for (const pos of tPositions) {
|
|
71
|
-
if (pos.SectorID) {
|
|
72
|
-
sectors.add(pos.SectorID);
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
const diversificationScore = sectors.size;
|
|
76
|
-
|
|
77
|
-
// Skip users with 0 positions/sectors
|
|
78
|
-
if (diversificationScore === 0) {
|
|
79
|
-
return;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// 2. Get total P&L from yesterday's snapshot
|
|
83
|
-
const pnl = yPortfolio.Equity - yPortfolio.EquityBase; // Total P&L in USD
|
|
84
|
-
|
|
85
|
-
// 3. Store P&L sum and count by score
|
|
86
|
-
this._initScore(diversificationScore);
|
|
87
|
-
const data = this.pnlByScore.get(diversificationScore);
|
|
88
|
-
data.pnlSum += pnl;
|
|
89
|
-
data.count++;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* REFACTOR: This method now calculates the final average P&L per score.
|
|
94
|
-
*/
|
|
95
|
-
getResult() {
|
|
96
|
-
const result = {};
|
|
97
|
-
|
|
98
|
-
for (const [score, data] of this.pnlByScore.entries()) {
|
|
99
|
-
if (data.count > 0) {
|
|
100
|
-
result[score] = {
|
|
101
|
-
average_pnl: data.pnlSum / data.count,
|
|
102
|
-
user_count: data.count
|
|
103
|
-
};
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
return result;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
reset() {
|
|
111
|
-
this.pnlByScore.clear();
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
module.exports = DiversificationPnl;
|
|
@@ -1,137 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @fileoverview Calculation (Pass 2) for drawdown response.
|
|
3
|
-
*
|
|
4
|
-
* This metric answers: "How do users behave when a position has a
|
|
5
|
-
* drawdown of over 10%?"
|
|
6
|
-
*
|
|
7
|
-
* It checks all positions from yesterday that were in >10% loss
|
|
8
|
-
* and tracks whether the user:
|
|
9
|
-
* 1. Held (position still open today, same size)
|
|
10
|
-
* 2. Closed (position no longer open today)
|
|
11
|
-
* 3. Added (position still open, but Invested % is larger)
|
|
12
|
-
*/
|
|
13
|
-
class DrawdownResponse {
|
|
14
|
-
constructor() {
|
|
15
|
-
this.actions = {
|
|
16
|
-
held: 0,
|
|
17
|
-
closed: 0,
|
|
18
|
-
added: 0
|
|
19
|
-
};
|
|
20
|
-
this.total_in_drawdown = 0;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Defines the output schema for this calculation.
|
|
25
|
-
* @returns {object} JSON Schema object
|
|
26
|
-
*/
|
|
27
|
-
static getSchema() {
|
|
28
|
-
return {
|
|
29
|
-
"type": "object",
|
|
30
|
-
"description": "Analyzes user behavior in response to a >10% position drawdown.",
|
|
31
|
-
"properties": {
|
|
32
|
-
"total_positions_in_drawdown": {
|
|
33
|
-
"type": "number",
|
|
34
|
-
"description": "Total positions from yesterday that were in >10% drawdown."
|
|
35
|
-
},
|
|
36
|
-
"action_held_pct": {
|
|
37
|
-
"type": "number",
|
|
38
|
-
"description": "Percentage of drawdown positions that were held."
|
|
39
|
-
},
|
|
40
|
-
"action_closed_pct": {
|
|
41
|
-
"type": "number",
|
|
42
|
-
"description": "Percentage of drawdown positions that were closed."
|
|
43
|
-
},
|
|
44
|
-
"action_added_pct": {
|
|
45
|
-
"type": "number",
|
|
46
|
-
"description": "Percentage of drawdown positions that were added to."
|
|
47
|
-
},
|
|
48
|
-
"raw_counts": {
|
|
49
|
-
"type": "object",
|
|
50
|
-
"properties": {
|
|
51
|
-
"held": { "type": "number" },
|
|
52
|
-
"closed": { "type": "number" },
|
|
53
|
-
"added": { "type": "number" }
|
|
54
|
-
},
|
|
55
|
-
"required": ["held", "closed", "added"]
|
|
56
|
-
}
|
|
57
|
-
},
|
|
58
|
-
"required": ["total_positions_in_drawdown", "action_held_pct", "action_closed_pct", "action_added_pct", "raw_counts"]
|
|
59
|
-
};
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
_getPortfolioMap(portfolio) {
|
|
63
|
-
// We MUST use PositionID to track specific trades
|
|
64
|
-
const positions = portfolio?.AggregatedPositions || portfolio?.PublicPositions;
|
|
65
|
-
if (!positions || !Array.isArray(positions)) {
|
|
66
|
-
return new Map();
|
|
67
|
-
}
|
|
68
|
-
// Map<PositionID, { pnl: number, invested: number }>
|
|
69
|
-
return new Map(positions.map(p => [p.PositionID, {
|
|
70
|
-
pnl: (p.NetProfit || 0) / (p.InvestedAmount || p.Amount || 1), // PNL as %
|
|
71
|
-
invested: p.InvestedAmount || p.Amount || 0
|
|
72
|
-
}]));
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
process(todayPortfolio, yesterdayPortfolio) {
|
|
76
|
-
if (!todayPortfolio || !yesterdayPortfolio) {
|
|
77
|
-
return;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
const yPosMap = this._getPortfolioMap(yesterdayPortfolio);
|
|
81
|
-
const tPosMap = this._getPortfolioMap(todayPortfolio);
|
|
82
|
-
|
|
83
|
-
if (yPosMap.size === 0) {
|
|
84
|
-
return;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
for (const [yPosId, yPosData] of yPosMap.entries()) {
|
|
88
|
-
// Check if position was in >10% drawdown
|
|
89
|
-
if (yPosData.pnl < -0.10) {
|
|
90
|
-
this.total_in_drawdown++;
|
|
91
|
-
|
|
92
|
-
// Now, check what happened today
|
|
93
|
-
if (!tPosMap.has(yPosId)) {
|
|
94
|
-
// 1. Position was closed
|
|
95
|
-
this.actions.closed++;
|
|
96
|
-
} else {
|
|
97
|
-
const tPosData = tPosMap.get(yPosId);
|
|
98
|
-
// 2. Position was added to (check for > 1% increase to avoid noise)
|
|
99
|
-
if (tPosData.invested > (yPosData.invested * 1.01)) {
|
|
100
|
-
this.actions.added++;
|
|
101
|
-
} else {
|
|
102
|
-
// 3. Position was held
|
|
103
|
-
this.actions.held++;
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
getResult() {
|
|
111
|
-
const total = this.total_in_drawdown;
|
|
112
|
-
if (total === 0) {
|
|
113
|
-
return {
|
|
114
|
-
total_positions_in_drawdown: 0,
|
|
115
|
-
action_held_pct: 0,
|
|
116
|
-
action_closed_pct: 0,
|
|
117
|
-
action_added_pct: 0,
|
|
118
|
-
raw_counts: this.actions
|
|
119
|
-
};
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
return {
|
|
123
|
-
total_positions_in_drawdown: total,
|
|
124
|
-
action_held_pct: (this.actions.held / total) * 100,
|
|
125
|
-
action_closed_pct: (this.actions.closed / total) * 100,
|
|
126
|
-
action_added_pct: (this.actions.added / total) * 100,
|
|
127
|
-
raw_counts: this.actions
|
|
128
|
-
};
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
reset() {
|
|
132
|
-
this.actions = { held: 0, closed: 0, added: 0 };
|
|
133
|
-
this.total_in_drawdown = 0;
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
module.exports = DrawdownResponse;
|