aiden-shared-calculations-unified 1.0.95 → 1.0.96
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/ghost-book/cost-basis-density.js +1 -2
- package/calculations/ghost-book/liquidity-vacuum.js +1 -2
- package/calculations/ghost-book/retail-gamma-exposure.js +0 -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,120 +0,0 @@
|
|
|
1
|
-
/**
|
|
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.
|
|
9
|
-
*/
|
|
10
|
-
class PositionCountPnl {
|
|
11
|
-
constructor() {
|
|
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
|
-
};
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Defines the output schema for this calculation.
|
|
24
|
-
* @returns {object} JSON Schema object
|
|
25
|
-
*/
|
|
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+';
|
|
67
|
-
return null;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
process(todayPortfolio, yesterdayPortfolio) {
|
|
71
|
-
// This calculation only needs today's portfolio state
|
|
72
|
-
if (!todayPortfolio) {
|
|
73
|
-
return;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
const positions = todayPortfolio.AggregatedPositions || todayPortfolio.PublicPositions;
|
|
77
|
-
const positionCount = Array.isArray(positions) ? positions.length : 0;
|
|
78
|
-
|
|
79
|
-
if (positionCount === 0) {
|
|
80
|
-
return;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
const bucketKey = this._getBucket(positionCount);
|
|
84
|
-
if (!bucketKey) {
|
|
85
|
-
return;
|
|
86
|
-
}
|
|
87
|
-
|
|
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++;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
getResult() {
|
|
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
|
-
};
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
module.exports = PositionCountPnl;
|
|
@@ -1,232 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @fileoverview Calculation (Pass 4) for positive expectancy cohort flow.
|
|
3
|
-
*
|
|
4
|
-
* This metric calculates the "Net Crowd Flow Percentage" for the
|
|
5
|
-
* "Positive Expectancy Cohort" (users with a high expectancy score).
|
|
6
|
-
*
|
|
7
|
-
* This calculation *depends* on 'user_expectancy_score'
|
|
8
|
-
* to identify the cohort.
|
|
9
|
-
*/
|
|
10
|
-
const { loadInstrumentMappings } = require('../../utils/sector_mapping_provider');
|
|
11
|
-
|
|
12
|
-
class PositiveExpectancyCohortFlow {
|
|
13
|
-
constructor() {
|
|
14
|
-
this.assetData = new Map();
|
|
15
|
-
this.sectorData = new Map();
|
|
16
|
-
this.mappings = null;
|
|
17
|
-
this.posExpCohortUserIds = null;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Defines the output schema for this calculation.
|
|
22
|
-
* @returns {object} JSON Schema object
|
|
23
|
-
*/
|
|
24
|
-
static getSchema() {
|
|
25
|
-
const flowSchema = {
|
|
26
|
-
"type": "object",
|
|
27
|
-
"properties": {
|
|
28
|
-
"net_flow_percentage": { "type": "number" },
|
|
29
|
-
"total_invested_today": { "type": "number" },
|
|
30
|
-
"total_invested_yesterday": { "type": "number" }
|
|
31
|
-
},
|
|
32
|
-
"required": ["net_flow_percentage", "total_invested_today", "total_invested_yesterday"]
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
return {
|
|
36
|
-
"type": "object",
|
|
37
|
-
"description": "Calculates net capital flow % (price-adjusted) for the 'Positive Expectancy' cohort (score > 0.5), aggregated by asset and sector.",
|
|
38
|
-
"properties": {
|
|
39
|
-
"cohort_size": {
|
|
40
|
-
"type": "number",
|
|
41
|
-
"description": "The number of users identified as being in the Positive Expectancy Cohort."
|
|
42
|
-
},
|
|
43
|
-
"assets": {
|
|
44
|
-
"type": "object",
|
|
45
|
-
"description": "Price-adjusted net flow per asset.",
|
|
46
|
-
"patternProperties": { "^.*$": flowSchema }, // Ticker
|
|
47
|
-
"additionalProperties": flowSchema
|
|
48
|
-
},
|
|
49
|
-
"sectors": {
|
|
50
|
-
"type": "object",
|
|
51
|
-
"description": "Price-adjusted net flow per sector.",
|
|
52
|
-
"patternProperties": { "^.*$": flowSchema }, // Sector
|
|
53
|
-
"additionalProperties": flowSchema
|
|
54
|
-
}
|
|
55
|
-
},
|
|
56
|
-
"required": ["cohort_size", "assets", "sectors"]
|
|
57
|
-
};
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Statically declare dependencies.
|
|
62
|
-
*/
|
|
63
|
-
static getDependencies() {
|
|
64
|
-
return ['user_expectancy_score']; // Pass 3
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
_getPortfolioPositions(portfolio) {
|
|
68
|
-
return portfolio?.PublicPositions || portfolio?.AggregatedPositions;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
_initAsset(instrumentId) {
|
|
72
|
-
if (!this.assetData.has(instrumentId)) {
|
|
73
|
-
this.assetData.set(instrumentId, {
|
|
74
|
-
total_invested_yesterday: 0,
|
|
75
|
-
total_invested_today: 0,
|
|
76
|
-
price_change_yesterday: 0,
|
|
77
|
-
});
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
_initSector(sector) {
|
|
82
|
-
if (!this.sectorData.has(sector)) {
|
|
83
|
-
this.sectorData.set(sector, {
|
|
84
|
-
total_invested_yesterday: 0,
|
|
85
|
-
total_invested_today: 0,
|
|
86
|
-
price_change_yesterday: 0,
|
|
87
|
-
});
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
_getPosExpCohort(fetchedDependencies) {
|
|
92
|
-
if (this.posExpCohortUserIds) {
|
|
93
|
-
return this.posExpCohortUserIds;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
const expectancyData = fetchedDependencies['user_expectancy_score'];
|
|
97
|
-
if (!expectancyData) {
|
|
98
|
-
return new Set();
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
this.posExpCohortUserIds = new Set();
|
|
102
|
-
for (const [userId, data] of Object.entries(expectancyData)) {
|
|
103
|
-
// Definition: Expectancy score > 0.5
|
|
104
|
-
if (data.expectancy_score > 0.5) {
|
|
105
|
-
this.posExpCohortUserIds.add(userId);
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
return this.posExpCohortUserIds;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
process(todayPortfolio, yesterdayPortfolio, userId, context, todayInsights, yesterdayInsights, fetchedDependencies) {
|
|
112
|
-
const cohort = this._getPosExpCohort(fetchedDependencies);
|
|
113
|
-
|
|
114
|
-
if (!cohort.has(userId)) {
|
|
115
|
-
return;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
if (!todayPortfolio || !yesterdayPortfolio) {
|
|
119
|
-
return;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
const yPos = this._getPortfolioPositions(yesterdayPortfolio);
|
|
123
|
-
const tPos = this._getPortfolioPositions(todayPortfolio);
|
|
124
|
-
|
|
125
|
-
const yPosMap = new Map(yPos?.map(p => [p.InstrumentID, p]) || []);
|
|
126
|
-
const tPosMap = new Map(tPos?.map(p => [p.InstrumentID, p]) || []);
|
|
127
|
-
|
|
128
|
-
const allInstrumentIds = new Set([...yPosMap.keys(), ...tPosMap.keys()]);
|
|
129
|
-
|
|
130
|
-
if (!this.mappings) {
|
|
131
|
-
this.mappings = context.mappings;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
for (const instrumentId of allInstrumentIds) {
|
|
135
|
-
if (!instrumentId) continue;
|
|
136
|
-
|
|
137
|
-
this._initAsset(instrumentId);
|
|
138
|
-
const asset = this.assetData.get(instrumentId);
|
|
139
|
-
|
|
140
|
-
const yP = yPosMap.get(instrumentId);
|
|
141
|
-
const tP = tPosMap.get(instrumentId);
|
|
142
|
-
|
|
143
|
-
const yInvested = yP?.InvestedAmount || yP?.Amount || 0;
|
|
144
|
-
const tInvested = tP?.InvestedAmount || tP?.Amount || 0;
|
|
145
|
-
|
|
146
|
-
const sector = this.mappings.instrumentToSector[instrumentId] || 'Other';
|
|
147
|
-
this._initSector(sector);
|
|
148
|
-
const sectorAsset = this.sectorData.get(sector);
|
|
149
|
-
|
|
150
|
-
if (yInvested > 0) {
|
|
151
|
-
const yPriceChange = (yP?.PipsRate || 0) / (yP?.OpenRate || 1);
|
|
152
|
-
|
|
153
|
-
asset.total_invested_yesterday += yInvested;
|
|
154
|
-
asset.price_change_yesterday += yPriceChange * yInvested;
|
|
155
|
-
|
|
156
|
-
sectorAsset.total_invested_yesterday += yInvested;
|
|
157
|
-
sectorAsset.price_change_yesterday += yPriceChange * yInvested;
|
|
158
|
-
}
|
|
159
|
-
if (tInvested > 0) {
|
|
160
|
-
asset.total_invested_today += tInvested;
|
|
161
|
-
sectorAsset.total_invested_today += tInvested;
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
_calculateFlow(dataMap) {
|
|
167
|
-
const result = {};
|
|
168
|
-
for (const [key, data] of dataMap.entries()) {
|
|
169
|
-
const { total_invested_yesterday, total_invested_today, price_change_yesterday } = data;
|
|
170
|
-
|
|
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;
|
|
176
|
-
|
|
177
|
-
result[key] = {
|
|
178
|
-
net_flow_percentage: net_flow_percentage,
|
|
179
|
-
total_invested_today: total_invested_today,
|
|
180
|
-
total_invested_yesterday: total_invested_yesterday
|
|
181
|
-
};
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
return result;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
async getResult(fetchedDependencies) {
|
|
188
|
-
if (!this.mappings) {
|
|
189
|
-
this.mappings = await loadInstrumentMappings();
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
const cohort = this._getPosExpCohort(fetchedDependencies);
|
|
193
|
-
|
|
194
|
-
// 1. Calculate Asset Flow
|
|
195
|
-
const assetResult = {};
|
|
196
|
-
for (const [instrumentId, data] of this.assetData.entries()) {
|
|
197
|
-
const ticker = this.mappings.instrumentToTicker[instrumentId] || `id_${instrumentId}`;
|
|
198
|
-
const { total_invested_yesterday, total_invested_today, price_change_yesterday } = data;
|
|
199
|
-
|
|
200
|
-
if (total_invested_yesterday > 0) {
|
|
201
|
-
const avg_price_change_pct = price_change_yesterday / total_invested_yesterday;
|
|
202
|
-
const price_contribution = total_invested_yesterday * avg_price_change_pct;
|
|
203
|
-
const flow_contribution = total_invested_today - (total_invested_yesterday + price_contribution);
|
|
204
|
-
const net_flow_percentage = (flow_contribution / total_invested_yesterday) * 100;
|
|
205
|
-
|
|
206
|
-
assetResult[ticker] = {
|
|
207
|
-
net_flow_percentage: net_flow_percentage,
|
|
208
|
-
total_invested_today: total_invested_today,
|
|
209
|
-
total_invested_yesterday: total_invested_yesterday
|
|
210
|
-
};
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
// 2. Calculate Sector Flow
|
|
215
|
-
const sectorResult = this._calculateFlow(this.sectorData);
|
|
216
|
-
|
|
217
|
-
return {
|
|
218
|
-
cohort_size: cohort.size,
|
|
219
|
-
assets: assetResult,
|
|
220
|
-
sectors: sectorResult
|
|
221
|
-
};
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
reset() {
|
|
225
|
-
this.assetData.clear();
|
|
226
|
-
this.sectorData.clear();
|
|
227
|
-
this.mappings = null;
|
|
228
|
-
this.posExpCohortUserIds = null;
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
module.exports = PositiveExpectancyCohortFlow;
|
|
@@ -1,115 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @fileoverview Calculation (Pass 4) for profit cohort divergence.
|
|
3
|
-
*
|
|
4
|
-
* This metric answers: "What divergence signals can be found by
|
|
5
|
-
* comparing the net asset flow of the 'in-profit' cohort vs.
|
|
6
|
-
* the 'in-loss' cohort?"
|
|
7
|
-
*
|
|
8
|
-
* e.g.,
|
|
9
|
-
* - Profit cohort selling, Loss cohort holding = "Profit Taking"
|
|
10
|
-
* - Profit cohort holding, Loss cohort selling = "Capitulation"
|
|
11
|
-
* - Both buying = "Confirmation"
|
|
12
|
-
* - Both selling = "Exodus"
|
|
13
|
-
*
|
|
14
|
-
* It *depends* on 'in_profit_asset_crowd_flow' and
|
|
15
|
-
* 'in_loss_asset_crowd_flow'.
|
|
16
|
-
*/
|
|
17
|
-
class ProfitCohortDivergence {
|
|
18
|
-
constructor() {
|
|
19
|
-
// No per-user processing
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Defines the output schema for this calculation.
|
|
24
|
-
* @returns {object} JSON Schema object
|
|
25
|
-
*/
|
|
26
|
-
static getSchema() {
|
|
27
|
-
const signalSchema = {
|
|
28
|
-
"type": "object",
|
|
29
|
-
"properties": {
|
|
30
|
-
"status": {
|
|
31
|
-
"type": "string",
|
|
32
|
-
"enum": ["Profit Taking", "Capitulation", "Confirmation (Buy)", "Confirmation (Sell)", "Divergence (Profit Buy / Loss Sell)", "Divergence (Profit Sell / Loss Buy)", "Neutral"]
|
|
33
|
-
},
|
|
34
|
-
"profit_cohort_flow_pct": { "type": "number" },
|
|
35
|
-
"loss_cohort_flow_pct": { "type": "number" }
|
|
36
|
-
},
|
|
37
|
-
"required": ["status", "profit_cohort_flow_pct", "loss_cohort_flow_pct"]
|
|
38
|
-
};
|
|
39
|
-
|
|
40
|
-
return {
|
|
41
|
-
"type": "object",
|
|
42
|
-
"description": "Generates divergence signals by comparing net flow of 'in-profit' vs. 'in-loss' cohorts.",
|
|
43
|
-
"patternProperties": {
|
|
44
|
-
"^.*$": signalSchema // Ticker
|
|
45
|
-
},
|
|
46
|
-
"additionalProperties": signalSchema
|
|
47
|
-
};
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Statically declare dependencies.
|
|
52
|
-
*/
|
|
53
|
-
static getDependencies() {
|
|
54
|
-
return [
|
|
55
|
-
'in_profit_asset_crowd_flow', // Pass 3
|
|
56
|
-
'in_loss_asset_crowd_flow' // Pass 3
|
|
57
|
-
];
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
process() {
|
|
61
|
-
// No-op
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
getResult(fetchedDependencies) {
|
|
65
|
-
const profitFlowData = fetchedDependencies['in_profit_asset_crowd_flow'];
|
|
66
|
-
const lossFlowData = fetchedDependencies['in_loss_asset_crowd_flow'];
|
|
67
|
-
|
|
68
|
-
if (!profitFlowData || !lossFlowData) {
|
|
69
|
-
return {};
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
const allTickers = new Set([...Object.keys(profitFlowData), ...Object.keys(lossFlowData)]);
|
|
73
|
-
const result = {};
|
|
74
|
-
const THRESHOLD = 1; // Min flow % to be considered 'active'
|
|
75
|
-
|
|
76
|
-
for (const ticker of allTickers) {
|
|
77
|
-
const pFlow = profitFlowData[ticker]?.net_flow_percentage || 0;
|
|
78
|
-
const lFlow = lossFlowData[ticker]?.net_flow_percentage || 0;
|
|
79
|
-
|
|
80
|
-
let status = 'Neutral';
|
|
81
|
-
|
|
82
|
-
if (pFlow > THRESHOLD && lFlow > THRESHOLD) {
|
|
83
|
-
status = 'Confirmation (Buy)';
|
|
84
|
-
} else if (pFlow < -THRESHOLD && lFlow < -THRESHOLD) {
|
|
85
|
-
status = 'Confirmation (Sell)';
|
|
86
|
-
} else if (pFlow > THRESHOLD && Math.abs(lFlow) < THRESHOLD) {
|
|
87
|
-
status = 'Divergence (Profit Buy / Loss Sell)'; // Profit cohort buying, loss cohort holding
|
|
88
|
-
} else if (pFlow < -THRESHOLD && Math.abs(lFlow) < THRESHOLD) {
|
|
89
|
-
status = 'Profit Taking';
|
|
90
|
-
} else if (Math.abs(pFlow) < THRESHOLD && lFlow < -THRESHOLD) {
|
|
91
|
-
status = 'Capitulation';
|
|
92
|
-
} else if (Math.abs(pFlow) < THRESHOLD && lFlow > THRESHOLD) {
|
|
93
|
-
status = 'Divergence (Profit Sell / Loss Buy)'; // Loss cohort buying, profit cohort holding
|
|
94
|
-
} else if (pFlow > THRESHOLD && lFlow < -THRESHOLD) {
|
|
95
|
-
status = 'Divergence (Profit Buy / Loss Sell)';
|
|
96
|
-
} else if (pFlow < -THRESHOLD && lFlow > THRESHOLD) {
|
|
97
|
-
status = 'Divergence (Profit Sell / Loss Buy)';
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
result[ticker] = {
|
|
101
|
-
status: status,
|
|
102
|
-
profit_cohort_flow_pct: pFlow,
|
|
103
|
-
loss_cohort_flow_pct: lFlow
|
|
104
|
-
};
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
return result;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
reset() {
|
|
111
|
-
// No state
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
module.exports = ProfitCohortDivergence;
|
|
@@ -1,104 +0,0 @@
|
|
|
1
|
-
/**
|
|
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.
|
|
8
|
-
*/
|
|
9
|
-
class ProfitabilityMigration {
|
|
10
|
-
constructor() {
|
|
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;
|
|
16
|
-
}
|
|
17
|
-
|
|
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
|
-
}
|
|
51
|
-
|
|
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
|
-
}
|
|
59
|
-
|
|
60
|
-
process(todayPortfolio, yesterdayPortfolio) {
|
|
61
|
-
if (!todayPortfolio || !yesterdayPortfolio) {
|
|
62
|
-
return;
|
|
63
|
-
}
|
|
64
|
-
|
|
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
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
this.total_processed++;
|
|
73
|
-
|
|
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++;
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
getResult() {
|
|
86
|
-
return {
|
|
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
|
|
92
|
-
};
|
|
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
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
module.exports = ProfitabilityMigration;
|
|
@@ -1,104 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @fileoverview Calculation (Pass 2) for reallocation increase percentage.
|
|
3
|
-
*
|
|
4
|
-
* This metric answers: "On average, what was the total percentage
|
|
5
|
-
* increase in allocation to *existing* positions today?"
|
|
6
|
-
*
|
|
7
|
-
* This measures "doubling down" or adding to winners/losers.
|
|
8
|
-
*/
|
|
9
|
-
class ReallocationIncreasePercentage {
|
|
10
|
-
constructor() {
|
|
11
|
-
this.total_reallocation_increase_pct = 0;
|
|
12
|
-
this.users_with_reallocations = 0;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Defines the output schema for this calculation.
|
|
17
|
-
* @returns {object} JSON Schema object
|
|
18
|
-
*/
|
|
19
|
-
static getSchema() {
|
|
20
|
-
return {
|
|
21
|
-
"type": "object",
|
|
22
|
-
"description": "Calculates the average portfolio percentage *added* to *existing* positions today.",
|
|
23
|
-
"properties": {
|
|
24
|
-
"average_reallocation_increase_pct": {
|
|
25
|
-
"type": "number",
|
|
26
|
-
"description": "The average percentage of a portfolio added to existing positions, for users who reallocated."
|
|
27
|
-
},
|
|
28
|
-
"total_reallocation_increase_pct_sum": {
|
|
29
|
-
"type": "number",
|
|
30
|
-
"description": "The sum of all reallocation increase percentages."
|
|
31
|
-
},
|
|
32
|
-
"user_count_with_reallocations": {
|
|
33
|
-
"type": "number",
|
|
34
|
-
"description": "The count of users who added to at least one existing position."
|
|
35
|
-
}
|
|
36
|
-
},
|
|
37
|
-
"required": ["average_reallocation_increase_pct", "total_reallocation_increase_pct_sum", "user_count_with_reallocations"]
|
|
38
|
-
};
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
_getPortfolioPositionMap(portfolio) {
|
|
42
|
-
// We MUST use AggregatedPositions for the 'Invested' %
|
|
43
|
-
const positions = portfolio?.AggregatedPositions;
|
|
44
|
-
if (!positions || !Array.isArray(positions)) {
|
|
45
|
-
return new Map();
|
|
46
|
-
}
|
|
47
|
-
// Map<InstrumentID, { invested: number }>
|
|
48
|
-
return new Map(positions.map(p => [p.InstrumentID, {
|
|
49
|
-
invested: p.InvestedAmount || p.Invested || 0
|
|
50
|
-
}]));
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
process(todayPortfolio, yesterdayPortfolio) {
|
|
54
|
-
if (!todayPortfolio || !yesterdayPortfolio) {
|
|
55
|
-
return;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
const yPosMap = this._getPortfolioPositionMap(yesterdayPortfolio);
|
|
59
|
-
const tPosMap = this._getPortfolioPositionMap(todayPortfolio);
|
|
60
|
-
|
|
61
|
-
if (tPosMap.size === 0 || yPosMap.size === 0) {
|
|
62
|
-
return; // No positions to compare
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
let userReallocationIncrease = 0;
|
|
66
|
-
|
|
67
|
-
for (const [tId, tPosData] of tPosMap.entries()) {
|
|
68
|
-
// Check if this position also existed yesterday
|
|
69
|
-
if (yPosMap.has(tId)) {
|
|
70
|
-
const yPosData = yPosMap.get(tId);
|
|
71
|
-
const increase = tPosData.invested - yPosData.invested;
|
|
72
|
-
|
|
73
|
-
// We only care about *increases*
|
|
74
|
-
if (increase > 0) {
|
|
75
|
-
userReallocationIncrease += increase;
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
if (userReallocationIncrease > 0) {
|
|
81
|
-
this.total_reallocation_increase_pct += userReallocationIncrease;
|
|
82
|
-
this.users_with_reallocations++;
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
getResult() {
|
|
87
|
-
const avg_pct = (this.users_with_reallocations > 0)
|
|
88
|
-
? (this.total_reallocation_increase_pct / this.users_with_reallocations)
|
|
89
|
-
: 0;
|
|
90
|
-
|
|
91
|
-
return {
|
|
92
|
-
average_reallocation_increase_pct: avg_pct,
|
|
93
|
-
total_reallocation_increase_pct_sum: this.total_reallocation_increase_pct,
|
|
94
|
-
user_count_with_reallocations: this.users_with_reallocations
|
|
95
|
-
};
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
reset() {
|
|
99
|
-
this.total_reallocation_increase_pct = 0;
|
|
100
|
-
this.users_with_reallocations = 0;
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
module.exports = ReallocationIncreasePercentage;
|