aiden-shared-calculations-unified 1.0.34 → 1.0.36
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 +77 -77
- package/calculations/activity/historical/activity_by_pnl_status.js +85 -85
- package/calculations/activity/historical/daily_asset_activity.js +85 -85
- package/calculations/activity/historical/daily_user_activity_tracker.js +144 -144
- package/calculations/activity/historical/speculator_adjustment_activity.js +76 -76
- package/calculations/asset_metrics/asset_position_size.js +57 -57
- package/calculations/backtests/strategy-performance.js +229 -245
- package/calculations/behavioural/historical/asset_crowd_flow.js +165 -170
- package/calculations/behavioural/historical/drawdown_response.js +58 -58
- package/calculations/behavioural/historical/dumb-cohort-flow.js +249 -249
- package/calculations/behavioural/historical/gain_response.js +57 -57
- package/calculations/behavioural/historical/in_loss_asset_crowd_flow.js +98 -98
- package/calculations/behavioural/historical/in_profit_asset_crowd_flow.js +99 -99
- package/calculations/behavioural/historical/paper_vs_diamond_hands.js +39 -39
- package/calculations/behavioural/historical/position_count_pnl.js +67 -67
- package/calculations/behavioural/historical/smart-cohort-flow.js +250 -250
- package/calculations/behavioural/historical/smart_money_flow.js +165 -165
- package/calculations/behavioural/historical/user-investment-profile.js +412 -412
- package/calculations/capital_flow/historical/crowd-cash-flow-proxy.js +121 -121
- package/calculations/capital_flow/historical/deposit_withdrawal_percentage.js +117 -117
- package/calculations/capital_flow/historical/new_allocation_percentage.js +49 -49
- package/calculations/insights/daily_bought_vs_sold_count.js +55 -55
- package/calculations/insights/daily_buy_sell_sentiment_count.js +49 -49
- package/calculations/insights/daily_ownership_delta.js +55 -55
- package/calculations/insights/daily_total_positions_held.js +39 -39
- package/calculations/meta/capital_deployment_strategy.js +129 -137
- package/calculations/meta/capital_liquidation_performance.js +121 -163
- package/calculations/meta/capital_vintage_performance.js +121 -158
- package/calculations/meta/cash-flow-deployment.js +110 -124
- package/calculations/meta/cash-flow-liquidation.js +126 -142
- package/calculations/meta/crowd_sharpe_ratio_proxy.js +83 -91
- package/calculations/meta/profit_cohort_divergence.js +77 -91
- package/calculations/meta/smart-dumb-divergence-index.js +116 -138
- package/calculations/meta/social_flow_correlation.js +99 -125
- package/calculations/pnl/asset_pnl_status.js +46 -46
- package/calculations/pnl/historical/profitability_migration.js +57 -57
- package/calculations/pnl/historical/user_profitability_tracker.js +117 -117
- package/calculations/pnl/profitable_and_unprofitable_status.js +64 -64
- package/calculations/sectors/historical/diversification_pnl.js +76 -76
- package/calculations/sectors/historical/sector_rotation.js +67 -67
- package/calculations/sentiment/historical/crowd_conviction_score.js +80 -80
- package/calculations/socialPosts/social-asset-posts-trend.js +52 -52
- package/calculations/socialPosts/social-top-mentioned-words.js +102 -102
- package/calculations/socialPosts/social-topic-interest-evolution.js +53 -53
- package/calculations/socialPosts/social-word-mentions-trend.js +62 -62
- package/calculations/socialPosts/social_activity_aggregation.js +103 -103
- package/calculations/socialPosts/social_event_correlation.js +121 -121
- package/calculations/socialPosts/social_sentiment_aggregation.js +114 -114
- package/calculations/speculators/historical/risk_appetite_change.js +54 -54
- package/calculations/speculators/historical/tsl_effectiveness.js +74 -74
- package/index.js +33 -33
- package/package.json +32 -32
- package/utils/firestore_utils.js +76 -76
- package/utils/price_data_provider.js +142 -142
- package/utils/sector_mapping_provider.js +74 -74
- package/calculations/capital_flow/historical/reallocation_increase_percentage.js +0 -63
- package/calculations/speculators/stop_loss_distance_by_sector_short_long_breakdown.js +0 -91
- package/calculations/speculators/stop_loss_distance_by_ticker_short_long_breakdown.js +0 -73
|
@@ -1,171 +1,166 @@
|
|
|
1
|
-
const { loadAllPriceData, getDailyPriceChange } = require('../../../utils/price_data_provider');
|
|
2
|
-
const { loadInstrumentMappings } = require('../../../utils/sector_mapping_provider');
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* @fileoverview Calculates "Net Crowd Flow" for each asset.
|
|
6
|
-
*
|
|
7
|
-
* This isolates the change in an asset's average portfolio percentage
|
|
8
|
-
* that is *not* explained by the asset's own price movement.
|
|
9
|
-
*
|
|
10
|
-
* Net Crowd Flow = (Actual % Change) - (Expected % Change from Price Move)
|
|
11
|
-
*
|
|
12
|
-
* A positive value means the crowd actively bought (flowed into) the asset.
|
|
13
|
-
* A negative value means the crowd actively sold (flowed out of) the asset.
|
|
14
|
-
*/
|
|
15
|
-
class AssetCrowdFlow {
|
|
16
|
-
constructor() {
|
|
17
|
-
this.asset_values = {}; // Stores { day1_value_sum: 0, day2_value_sum: 0 }
|
|
18
|
-
this.user_count = 0;
|
|
19
|
-
this.priceMap = null;
|
|
20
|
-
this.mappings = null;
|
|
21
|
-
this.dates = {}; // To store { today: '...', yesterday: '...' }
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Helper to safely initialize an asset entry.
|
|
26
|
-
*/
|
|
27
|
-
_initAsset(instrumentId) {
|
|
28
|
-
if (!this.asset_values[instrumentId]) {
|
|
29
|
-
this.asset_values[instrumentId] = {
|
|
30
|
-
day1_value_sum: 0,
|
|
31
|
-
day2_value_sum: 0
|
|
32
|
-
};
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Helper to sum the 'Value' field from an AggregatedPositions array.
|
|
38
|
-
*/
|
|
39
|
-
_sumAssetValue(positions) {
|
|
40
|
-
const valueMap = {};
|
|
41
|
-
if (!positions || !Array.isArray(positions)) {
|
|
42
|
-
return valueMap;
|
|
43
|
-
}
|
|
44
|
-
for (const pos of positions) {
|
|
45
|
-
if (pos && pos.InstrumentID && pos.Value) {
|
|
46
|
-
valueMap[pos.InstrumentID] = (valueMap[pos.InstrumentID] || 0) + pos.Value;
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
return valueMap;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
process(todayPortfolio, yesterdayPortfolio, userId, context) {
|
|
53
|
-
// This is a historical calculation, requires both days
|
|
54
|
-
if (!todayPortfolio || !yesterdayPortfolio || !todayPortfolio.AggregatedPositions || !yesterdayPortfolio.AggregatedPositions) {
|
|
55
|
-
return;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// Capture dates from context on the first run
|
|
59
|
-
if (!this.dates.today && context.todayDateStr && context.yesterdayDateStr) {
|
|
60
|
-
this.dates.today = context.todayDateStr;
|
|
61
|
-
this.dates.yesterday = context.yesterdayDateStr;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
const yesterdayValues = this._sumAssetValue(yesterdayPortfolio.AggregatedPositions);
|
|
65
|
-
const todayValues = this._sumAssetValue(todayPortfolio.AggregatedPositions);
|
|
66
|
-
|
|
67
|
-
// Use a set of all unique instruments held across both days
|
|
68
|
-
const allInstrumentIds = new Set([
|
|
69
|
-
...Object.keys(yesterdayValues),
|
|
70
|
-
...Object.keys(todayValues)
|
|
71
|
-
]);
|
|
72
|
-
|
|
73
|
-
for (const instrumentId of allInstrumentIds) {
|
|
74
|
-
this._initAsset(instrumentId);
|
|
75
|
-
this.asset_values[instrumentId].day1_value_sum += (yesterdayValues[instrumentId] || 0);
|
|
76
|
-
this.asset_values[instrumentId].day2_value_sum += (todayValues[instrumentId] || 0);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
this.user_count++;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
async getResult() {
|
|
83
|
-
if (this.user_count === 0 || !this.dates.today) {
|
|
84
|
-
console.warn('[AssetCrowdFlow] No users processed or dates missing.');
|
|
85
|
-
return null; // <---
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
// Load priceMap and mappings if not loaded
|
|
89
|
-
if (!this.priceMap || !this.mappings) {
|
|
90
|
-
try {
|
|
91
|
-
const [priceData, mappingData] = await Promise.all([
|
|
92
|
-
loadAllPriceData(),
|
|
93
|
-
loadInstrumentMappings()
|
|
94
|
-
]);
|
|
95
|
-
this.priceMap = priceData;
|
|
96
|
-
this.mappings = mappingData;
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
//
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
this.mappings = null;
|
|
167
|
-
this.dates = {};
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
|
|
1
|
+
const { loadAllPriceData, getDailyPriceChange } = require('../../../utils/price_data_provider');
|
|
2
|
+
const { loadInstrumentMappings } = require('../../../utils/sector_mapping_provider');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @fileoverview Calculates "Net Crowd Flow" for each asset.
|
|
6
|
+
*
|
|
7
|
+
* This isolates the change in an asset's average portfolio percentage
|
|
8
|
+
* that is *not* explained by the asset's own price movement.
|
|
9
|
+
*
|
|
10
|
+
* Net Crowd Flow = (Actual % Change) - (Expected % Change from Price Move)
|
|
11
|
+
*
|
|
12
|
+
* A positive value means the crowd actively bought (flowed into) the asset.
|
|
13
|
+
* A negative value means the crowd actively sold (flowed out of) the asset.
|
|
14
|
+
*/
|
|
15
|
+
class AssetCrowdFlow {
|
|
16
|
+
constructor() {
|
|
17
|
+
this.asset_values = {}; // Stores { day1_value_sum: 0, day2_value_sum: 0 }
|
|
18
|
+
this.user_count = 0;
|
|
19
|
+
this.priceMap = null;
|
|
20
|
+
this.mappings = null;
|
|
21
|
+
this.dates = {}; // To store { today: '...', yesterday: '...' }
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Helper to safely initialize an asset entry.
|
|
26
|
+
*/
|
|
27
|
+
_initAsset(instrumentId) {
|
|
28
|
+
if (!this.asset_values[instrumentId]) {
|
|
29
|
+
this.asset_values[instrumentId] = {
|
|
30
|
+
day1_value_sum: 0,
|
|
31
|
+
day2_value_sum: 0
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Helper to sum the 'Value' field from an AggregatedPositions array.
|
|
38
|
+
*/
|
|
39
|
+
_sumAssetValue(positions) {
|
|
40
|
+
const valueMap = {};
|
|
41
|
+
if (!positions || !Array.isArray(positions)) {
|
|
42
|
+
return valueMap;
|
|
43
|
+
}
|
|
44
|
+
for (const pos of positions) {
|
|
45
|
+
if (pos && pos.InstrumentID && pos.Value) {
|
|
46
|
+
valueMap[pos.InstrumentID] = (valueMap[pos.InstrumentID] || 0) + pos.Value;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return valueMap;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
process(todayPortfolio, yesterdayPortfolio, userId, context) {
|
|
53
|
+
// This is a historical calculation, requires both days
|
|
54
|
+
if (!todayPortfolio || !yesterdayPortfolio || !todayPortfolio.AggregatedPositions || !yesterdayPortfolio.AggregatedPositions) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Capture dates from context on the first run
|
|
59
|
+
if (!this.dates.today && context.todayDateStr && context.yesterdayDateStr) {
|
|
60
|
+
this.dates.today = context.todayDateStr;
|
|
61
|
+
this.dates.yesterday = context.yesterdayDateStr;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const yesterdayValues = this._sumAssetValue(yesterdayPortfolio.AggregatedPositions);
|
|
65
|
+
const todayValues = this._sumAssetValue(todayPortfolio.AggregatedPositions);
|
|
66
|
+
|
|
67
|
+
// Use a set of all unique instruments held across both days
|
|
68
|
+
const allInstrumentIds = new Set([
|
|
69
|
+
...Object.keys(yesterdayValues),
|
|
70
|
+
...Object.keys(todayValues)
|
|
71
|
+
]);
|
|
72
|
+
|
|
73
|
+
for (const instrumentId of allInstrumentIds) {
|
|
74
|
+
this._initAsset(instrumentId);
|
|
75
|
+
this.asset_values[instrumentId].day1_value_sum += (yesterdayValues[instrumentId] || 0);
|
|
76
|
+
this.asset_values[instrumentId].day2_value_sum += (todayValues[instrumentId] || 0);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
this.user_count++;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async getResult() {
|
|
83
|
+
if (this.user_count === 0 || !this.dates.today) {
|
|
84
|
+
console.warn('[AssetCrowdFlow] No users processed or dates missing.');
|
|
85
|
+
return null; // <--- Returns null if no users
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Load priceMap and mappings if not loaded
|
|
89
|
+
if (!this.priceMap || !this.mappings) {
|
|
90
|
+
try {
|
|
91
|
+
const [priceData, mappingData] = await Promise.all([
|
|
92
|
+
loadAllPriceData(),
|
|
93
|
+
loadInstrumentMappings()
|
|
94
|
+
]);
|
|
95
|
+
this.priceMap = priceData;
|
|
96
|
+
this.mappings = mappingData;
|
|
97
|
+
|
|
98
|
+
if (!this.priceMap || Object.keys(this.priceMap).length === 0) {
|
|
99
|
+
console.error('[AssetCrowdFlow] CRITICAL: Price map is empty or failed to load. Aborting calculation to allow backfill.');
|
|
100
|
+
return null; // Return null to trigger backfill
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
} catch (err) {
|
|
104
|
+
console.error('[AssetCrowdFlow] Failed to load dependencies:', err);
|
|
105
|
+
return null; // <--- Return null on error
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const finalResults = {};
|
|
110
|
+
const todayStr = this.dates.today;
|
|
111
|
+
const yesterdayStr = this.dates.yesterday;
|
|
112
|
+
|
|
113
|
+
for (const rawInstrumentId in this.asset_values) {
|
|
114
|
+
const instrumentId = String(rawInstrumentId); // normalize
|
|
115
|
+
const ticker = this.mappings.instrumentToTicker?.[instrumentId] || `id_${instrumentId}`;
|
|
116
|
+
|
|
117
|
+
const avg_day1_value = this.asset_values[rawInstrumentId].day1_value_sum / this.user_count;
|
|
118
|
+
const avg_day2_value = this.asset_values[rawInstrumentId].day2_value_sum / this.user_count;
|
|
119
|
+
|
|
120
|
+
// --- THIS IS THE FIX YOU APPLIED ---
|
|
121
|
+
const priceChangePct = getDailyPriceChange(instrumentId, yesterdayStr, todayStr, this.priceMap);
|
|
122
|
+
// --- END FIX ---
|
|
123
|
+
|
|
124
|
+
if (priceChangePct === null) {
|
|
125
|
+
// If price is missing, we simply skip this ticker.
|
|
126
|
+
// It will not be in the final output.
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// --- THIS IS THE LOGIC THAT IS BROKEN IN YOUR FILE ---
|
|
131
|
+
// Calculate expected day2 value from price movement
|
|
132
|
+
const expected_day2_value = avg_day1_value * (1 + priceChangePct);
|
|
133
|
+
|
|
134
|
+
// Net crowd flow = actual minus expected
|
|
135
|
+
const net_crowd_flow_pct = avg_day2_value - expected_day2_value;
|
|
136
|
+
|
|
137
|
+
finalResults[ticker] = {
|
|
138
|
+
net_crowd_flow_pct,
|
|
139
|
+
avg_value_day1_pct: avg_day1_value, // <-- Note: I've also fixed the key name here
|
|
140
|
+
avg_value_day2_pct: avg_day2_value, // <-- Note: I've also fixed the key name here
|
|
141
|
+
expected_day2_value,
|
|
142
|
+
price_change_pct: priceChangePct,
|
|
143
|
+
user_sample_size: this.user_count
|
|
144
|
+
};
|
|
145
|
+
// --- END BROKEN LOGIC ---
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (Object.keys(finalResults).length === 0) {
|
|
149
|
+
console.warn(`[AssetCrowdFlow] No results generated for ${this.dates.today}. This likely means all price data was missing. Returning null for backfill.`);
|
|
150
|
+
return null; // <--- Returns null if all tickers failed
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return finalResults;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
reset() {
|
|
158
|
+
this.asset_values = {};
|
|
159
|
+
this.user_count = 0;
|
|
160
|
+
this.priceMap = null;
|
|
161
|
+
this.mappings = null;
|
|
162
|
+
this.dates = {};
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
171
166
|
module.exports = AssetCrowdFlow;
|
|
@@ -1,59 +1,59 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Analyzes user behavior after a position experiences a >10% drawdown.
|
|
3
|
-
*/
|
|
4
|
-
class DrawdownResponse {
|
|
5
|
-
constructor() {
|
|
6
|
-
this.drawdown_events = {
|
|
7
|
-
held_position: 0,
|
|
8
|
-
closed_position: 0,
|
|
9
|
-
added_to_position: 0
|
|
10
|
-
};
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
process(todayPortfolio, yesterdayPortfolio, userId) {
|
|
14
|
-
if (!yesterdayPortfolio || !todayPortfolio) {
|
|
15
|
-
return; // Need both days for comparison
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
const yPositions = yesterdayPortfolio.AggregatedPositions || yesterdayPortfolio.PublicPositions;
|
|
19
|
-
const tPositions = todayPortfolio.AggregatedPositions || todayPortfolio.PublicPositions;
|
|
20
|
-
|
|
21
|
-
if (!yPositions || !Array.isArray(yPositions) || !tPositions || !Array.isArray(tPositions)) {
|
|
22
|
-
return;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
// Use PositionID if available (as in original file), fallback to InstrumentID
|
|
26
|
-
const todayPositions = new Map(tPositions.map(p => [p.PositionID || p.InstrumentID, p]));
|
|
27
|
-
|
|
28
|
-
for (const yPos of yPositions) {
|
|
29
|
-
// FIX: Use the NetProfit field, which is already a percentage.
|
|
30
|
-
// Your data sample (e.g., -83.6) shows the threshold should be -10.0.
|
|
31
|
-
const drawdownPercent = yPos.NetProfit || 0;
|
|
32
|
-
const yPosId = yPos.PositionID || yPos.InstrumentID;
|
|
33
|
-
|
|
34
|
-
// Check if this position was in a >10% drawdown yesterday
|
|
35
|
-
if (drawdownPercent < -10.0) {
|
|
36
|
-
const todayPos = todayPositions.get(yPosId);
|
|
37
|
-
|
|
38
|
-
if (!todayPos) {
|
|
39
|
-
// Position was closed
|
|
40
|
-
this.drawdown_events.closed_position++;
|
|
41
|
-
} else if (todayPos.Invested > yPos.Invested) {
|
|
42
|
-
// FIX: Use 'Invested' (percentage) to check for increase
|
|
43
|
-
// User added money to the losing position
|
|
44
|
-
this.drawdown_events.added_to_position++;
|
|
45
|
-
} else {
|
|
46
|
-
// Position was held (or reduced, but not added to)
|
|
47
|
-
this.drawdown_events.held_position++;
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
getResult() {
|
|
54
|
-
// Return final calculated values
|
|
55
|
-
return this.drawdown_events;
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Analyzes user behavior after a position experiences a >10% drawdown.
|
|
3
|
+
*/
|
|
4
|
+
class DrawdownResponse {
|
|
5
|
+
constructor() {
|
|
6
|
+
this.drawdown_events = {
|
|
7
|
+
held_position: 0,
|
|
8
|
+
closed_position: 0,
|
|
9
|
+
added_to_position: 0
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
process(todayPortfolio, yesterdayPortfolio, userId) {
|
|
14
|
+
if (!yesterdayPortfolio || !todayPortfolio) {
|
|
15
|
+
return; // Need both days for comparison
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const yPositions = yesterdayPortfolio.AggregatedPositions || yesterdayPortfolio.PublicPositions;
|
|
19
|
+
const tPositions = todayPortfolio.AggregatedPositions || todayPortfolio.PublicPositions;
|
|
20
|
+
|
|
21
|
+
if (!yPositions || !Array.isArray(yPositions) || !tPositions || !Array.isArray(tPositions)) {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Use PositionID if available (as in original file), fallback to InstrumentID
|
|
26
|
+
const todayPositions = new Map(tPositions.map(p => [p.PositionID || p.InstrumentID, p]));
|
|
27
|
+
|
|
28
|
+
for (const yPos of yPositions) {
|
|
29
|
+
// FIX: Use the NetProfit field, which is already a percentage.
|
|
30
|
+
// Your data sample (e.g., -83.6) shows the threshold should be -10.0.
|
|
31
|
+
const drawdownPercent = yPos.NetProfit || 0;
|
|
32
|
+
const yPosId = yPos.PositionID || yPos.InstrumentID;
|
|
33
|
+
|
|
34
|
+
// Check if this position was in a >10% drawdown yesterday
|
|
35
|
+
if (drawdownPercent < -10.0) {
|
|
36
|
+
const todayPos = todayPositions.get(yPosId);
|
|
37
|
+
|
|
38
|
+
if (!todayPos) {
|
|
39
|
+
// Position was closed
|
|
40
|
+
this.drawdown_events.closed_position++;
|
|
41
|
+
} else if (todayPos.Invested > yPos.Invested) {
|
|
42
|
+
// FIX: Use 'Invested' (percentage) to check for increase
|
|
43
|
+
// User added money to the losing position
|
|
44
|
+
this.drawdown_events.added_to_position++;
|
|
45
|
+
} else {
|
|
46
|
+
// Position was held (or reduced, but not added to)
|
|
47
|
+
this.drawdown_events.held_position++;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
getResult() {
|
|
54
|
+
// Return final calculated values
|
|
55
|
+
return this.drawdown_events;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
59
|
module.exports = DrawdownResponse;
|