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,57 +1,136 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* @fileoverview Calculation (Pass 2) for gain response.
|
|
3
|
+
*
|
|
4
|
+
* This metric answers: "How do users behave when a position has a
|
|
5
|
+
* gain of over 10%?"
|
|
6
|
+
*
|
|
7
|
+
* It checks all positions from yesterday that were in >10% profit
|
|
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. Reduced (position still open, but Invested % is smaller)
|
|
3
12
|
*/
|
|
4
13
|
class GainResponse {
|
|
5
14
|
constructor() {
|
|
6
|
-
this.
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
15
|
+
this.actions = {
|
|
16
|
+
held: 0,
|
|
17
|
+
closed: 0,
|
|
18
|
+
reduced: 0
|
|
10
19
|
};
|
|
20
|
+
this.total_in_gain = 0;
|
|
11
21
|
}
|
|
12
22
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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 gain.",
|
|
31
|
+
"properties": {
|
|
32
|
+
"total_positions_in_gain": {
|
|
33
|
+
"type": "number",
|
|
34
|
+
"description": "Total positions from yesterday that were in >10% gain."
|
|
35
|
+
},
|
|
36
|
+
"action_held_pct": {
|
|
37
|
+
"type": "number",
|
|
38
|
+
"description": "Percentage of gain positions that were held."
|
|
39
|
+
},
|
|
40
|
+
"action_closed_pct": {
|
|
41
|
+
"type": "number",
|
|
42
|
+
"description": "Percentage of gain positions that were closed (profit-taking)."
|
|
43
|
+
},
|
|
44
|
+
"action_reduced_pct": {
|
|
45
|
+
"type": "number",
|
|
46
|
+
"description": "Percentage of gain positions that were reduced (scaling out)."
|
|
47
|
+
},
|
|
48
|
+
"raw_counts": {
|
|
49
|
+
"type": "object",
|
|
50
|
+
"properties": {
|
|
51
|
+
"held": { "type": "number" },
|
|
52
|
+
"closed": { "type": "number" },
|
|
53
|
+
"reduced": { "type": "number" }
|
|
54
|
+
},
|
|
55
|
+
"required": ["held", "closed", "reduced"]
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
"required": ["total_positions_in_gain", "action_held_pct", "action_closed_pct", "action_reduced_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;
|
|
16
78
|
}
|
|
17
79
|
|
|
18
|
-
const
|
|
19
|
-
const
|
|
80
|
+
const yPosMap = this._getPortfolioMap(yesterdayPortfolio);
|
|
81
|
+
const tPosMap = this._getPortfolioMap(todayPortfolio);
|
|
20
82
|
|
|
21
|
-
if (
|
|
83
|
+
if (yPosMap.size === 0) {
|
|
22
84
|
return;
|
|
23
85
|
}
|
|
24
86
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
// Check if this position was in a >10% gain yesterday
|
|
35
|
-
if (gainPercent > 10.0) {
|
|
36
|
-
const todayPos = todayPositions.get(yPosId);
|
|
37
|
-
|
|
38
|
-
if (!todayPos) {
|
|
39
|
-
// Position was closed (took full profit)
|
|
40
|
-
this.gain_events.closed_position++;
|
|
41
|
-
} else if (todayPos.Invested < yPos.Invested) {
|
|
42
|
-
// FIX: Use 'Invested' (percentage) to check for reduction
|
|
43
|
-
// User reduced the position (took partial profit)
|
|
44
|
-
this.gain_events.reduced_position++;
|
|
87
|
+
for (const [yPosId, yPosData] of yPosMap.entries()) {
|
|
88
|
+
// Check if position was in >10% gain
|
|
89
|
+
if (yPosData.pnl > 0.10) {
|
|
90
|
+
this.total_in_gain++;
|
|
91
|
+
|
|
92
|
+
// Now, check what happened today
|
|
93
|
+
if (!tPosMap.has(yPosId)) {
|
|
94
|
+
// 1. Position was closed (Profit Taking)
|
|
95
|
+
this.actions.closed++;
|
|
45
96
|
} else {
|
|
46
|
-
|
|
47
|
-
|
|
97
|
+
const tPosData = tPosMap.get(yPosId);
|
|
98
|
+
// 2. Position was reduced (check for > 1% reduction to avoid noise)
|
|
99
|
+
if (tPosData.invested < (yPosData.invested * 0.99)) {
|
|
100
|
+
this.actions.reduced++;
|
|
101
|
+
} else {
|
|
102
|
+
// 3. Position was held
|
|
103
|
+
this.actions.held++;
|
|
104
|
+
}
|
|
48
105
|
}
|
|
49
106
|
}
|
|
50
107
|
}
|
|
51
108
|
}
|
|
52
109
|
|
|
53
110
|
getResult() {
|
|
54
|
-
|
|
111
|
+
const total = this.total_in_gain;
|
|
112
|
+
if (total === 0) {
|
|
113
|
+
return {
|
|
114
|
+
total_positions_in_gain: 0,
|
|
115
|
+
action_held_pct: 0,
|
|
116
|
+
action_closed_pct: 0,
|
|
117
|
+
action_reduced_pct: 0,
|
|
118
|
+
raw_counts: this.actions
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
total_positions_in_gain: total,
|
|
124
|
+
action_held_pct: (this.actions.held / total) * 100,
|
|
125
|
+
action_closed_pct: (this.actions.closed / total) * 100,
|
|
126
|
+
action_reduced_pct: (this.actions.reduced / total) * 100,
|
|
127
|
+
raw_counts: this.actions
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
reset() {
|
|
132
|
+
this.actions = { held: 0, closed: 0, reduced: 0 };
|
|
133
|
+
this.total_in_gain = 0;
|
|
55
134
|
}
|
|
56
135
|
}
|
|
57
136
|
|
|
@@ -1,69 +1,84 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @fileoverview Pass
|
|
3
|
-
*
|
|
2
|
+
* @fileoverview Calculation (Pass 2) for aggregating user performance.
|
|
3
|
+
*
|
|
4
|
+
* This class processes each user's daily P&L and aggregates it into
|
|
5
|
+
* buckets for statistical analysis. It collects all individual P&Ls
|
|
6
|
+
* to build a distribution.
|
|
7
|
+
*
|
|
8
|
+
* This is a foundational calculation needed by many Pass 3 metrics.
|
|
4
9
|
*/
|
|
5
10
|
class HistoricalPerformanceAggregator {
|
|
6
11
|
constructor() {
|
|
7
|
-
|
|
12
|
+
// Stores the 7-day weighted P&L for every user.
|
|
13
|
+
this.userPnlHistory = [];
|
|
8
14
|
}
|
|
9
15
|
|
|
10
16
|
/**
|
|
11
|
-
*
|
|
12
|
-
* @
|
|
13
|
-
* @param {string} userId - The user's ID.
|
|
14
|
-
* @param {object} context - Shared context data.
|
|
15
|
-
* @param {null} todayInsights - Not used.
|
|
16
|
-
* @param {null} yesterdayInsights - Not used.
|
|
17
|
-
* @param {null} todaySocialPostInsights - Not used.
|
|
18
|
-
* @param {null} yesterdaySocialPostInsights - Not used.
|
|
19
|
-
* @param {object} todayHistoryData - The full map of { [userId]: history }
|
|
20
|
-
* @param {null} yesterdayHistoryData - Not used.
|
|
17
|
+
* Defines the output schema for this calculation.
|
|
18
|
+
* @returns {object} JSON Schema object
|
|
21
19
|
*/
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
20
|
+
static getSchema() {
|
|
21
|
+
return {
|
|
22
|
+
"type": "object",
|
|
23
|
+
"description": "Aggregates historical P&L from 'user_profitability_tracker' for all users to build a performance distribution.",
|
|
24
|
+
"properties": {
|
|
25
|
+
"user_pnl_distribution": {
|
|
26
|
+
"type": "array",
|
|
27
|
+
"description": "An array of 7-day weighted average P&L values, one for each user.",
|
|
28
|
+
"items": { "type": "number" }
|
|
29
|
+
},
|
|
30
|
+
"user_count": {
|
|
31
|
+
"type": "number",
|
|
32
|
+
"description": "Total number of users processed."
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"required": ["user_pnl_distribution", "user_count"]
|
|
36
|
+
};
|
|
37
|
+
}
|
|
32
38
|
|
|
33
|
-
|
|
39
|
+
/**
|
|
40
|
+
* Statically declare dependencies.
|
|
41
|
+
*/
|
|
42
|
+
static getDependencies() {
|
|
43
|
+
return ['user_profitability_tracker'];
|
|
44
|
+
}
|
|
34
45
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
46
|
+
/**
|
|
47
|
+
* process() is a no-op. All logic is in getResult().
|
|
48
|
+
* This calculation doesn't process portfolios; it processes
|
|
49
|
+
* the *result* of another calculation.
|
|
50
|
+
*/
|
|
51
|
+
process() {
|
|
52
|
+
// No-op
|
|
53
|
+
}
|
|
39
54
|
|
|
40
|
-
|
|
55
|
+
/**
|
|
56
|
+
* Aggregates results from the dependency.
|
|
57
|
+
*/
|
|
58
|
+
getResult(fetchedDependencies) {
|
|
59
|
+
const profitabilityData = fetchedDependencies['user_profitability_tracker'];
|
|
41
60
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
// We only care about users with a meaningful trade history
|
|
48
|
-
if (!stats.totalTrades || stats.totalTrades < 10) {
|
|
49
|
-
return;
|
|
61
|
+
if (!profitabilityData || !profitabilityData.user_details) {
|
|
62
|
+
return {
|
|
63
|
+
user_pnl_distribution: [],
|
|
64
|
+
user_count: 0
|
|
65
|
+
};
|
|
50
66
|
}
|
|
67
|
+
|
|
68
|
+
// Extract the 7-day weighted P&L for all users
|
|
69
|
+
const pnlDistribution = Object.values(profitabilityData.user_details)
|
|
70
|
+
.map(details => details.weighted_avg_pnl_7d)
|
|
71
|
+
// Filter out any null/undefined/NaN values
|
|
72
|
+
.filter(pnl => typeof pnl === 'number' && !isNaN(pnl));
|
|
51
73
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
avgLossPct: stats.avgLossPct,
|
|
56
|
-
totalTrades: stats.totalTrades,
|
|
57
|
-
avgHoldingTime: stats.avgHoldingTimeInMinutes
|
|
74
|
+
return {
|
|
75
|
+
user_pnl_distribution: pnlDistribution,
|
|
76
|
+
user_count: pnlDistribution.length
|
|
58
77
|
};
|
|
59
78
|
}
|
|
60
79
|
|
|
61
|
-
async getResult() {
|
|
62
|
-
return this.allUserStats;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
80
|
reset() {
|
|
66
|
-
this.
|
|
81
|
+
this.userPnlHistory = [];
|
|
67
82
|
}
|
|
68
83
|
}
|
|
69
84
|
|
|
@@ -1,99 +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 Loss" 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 *at a loss*
|
|
6
|
+
* on that specific asset.
|
|
7
|
+
*
|
|
8
|
+
* This helps identify if losers are capitulating or doubling down.
|
|
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 InLossAssetCrowdFlow {
|
|
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.inLossCohorts = 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 loss on that asset.",
|
|
29
|
+
"patternProperties": {
|
|
30
|
+
// Ticker
|
|
31
|
+
"^.*$": {
|
|
32
|
+
"type": "object",
|
|
33
|
+
"description": "Net flow metrics for a specific asset ticker from its 'in loss' cohort.",
|
|
34
|
+
"properties": {
|
|
35
|
+
"net_flow_percentage": {
|
|
36
|
+
"type": "number",
|
|
37
|
+
"description": "Net capital flow % from the 'in loss' 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
|
+
_getInLossCohorts(fetchedDependencies) {
|
|
84
|
+
if (this.inLossCohorts) {
|
|
85
|
+
return this.inLossCohorts;
|
|
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.inLossCohorts = new Map();
|
|
96
|
+
for (const [ticker, data] of Object.entries(pnlStatusData)) {
|
|
97
|
+
const userSet = new Set(data.users_in_loss.map(u => u.userId));
|
|
98
|
+
this.inLossCohorts.set(ticker, userSet);
|
|
27
99
|
}
|
|
100
|
+
return this.inLossCohorts;
|
|
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._getInLossCohorts(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
|
-
const tNetProfit = tPos?.NetProfit || 0;
|
|
49
|
-
if (tNetProfit >= 0) { // Note: >= 0 (includes zero profit)
|
|
50
|
-
continue; // Skip this asset for this user
|
|
51
|
-
}
|
|
52
|
-
// --- END COHORT LOGIC ---
|
|
129
|
+
const ticker = this.mappings.instrumentToTicker[instrumentId] || `id_${instrumentId}`;
|
|
130
|
+
const cohort = cohorts.get(ticker);
|
|
53
131
|
|
|
132
|
+
// This user is not in the "in loss" cohort for this asset, skip.
|
|
133
|
+
if (!cohort || !cohort.has(userId)) {
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// User *is* in the cohort, process their data
|
|
54
138
|
this._initAsset(instrumentId);
|
|
55
|
-
this.
|
|
56
|
-
|
|
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
|
+
}
|
|
57
156
|
}
|
|
58
|
-
this.user_count++;
|
|
59
157
|
}
|
|
60
158
|
|
|
61
159
|
async getResult() {
|
|
62
|
-
if (
|
|
63
|
-
|
|
64
|
-
const [priceData, mappingData] = await Promise.all([
|
|
65
|
-
loadAllPriceData(),
|
|
66
|
-
loadInstrumentMappings()
|
|
67
|
-
]);
|
|
68
|
-
this.priceMap = priceData;
|
|
69
|
-
this.mappings = mappingData;
|
|
160
|
+
if (!this.mappings) {
|
|
161
|
+
this.mappings = await loadInstrumentMappings();
|
|
70
162
|
}
|
|
71
|
-
|
|
72
|
-
const finalResults = {};
|
|
73
|
-
const todayStr = this.dates.today;
|
|
74
|
-
const yesterdayStr = this.dates.yesterday;
|
|
75
163
|
|
|
76
|
-
|
|
164
|
+
const result = {};
|
|
165
|
+
|
|
166
|
+
for (const [instrumentId, data] of this.assetData.entries()) {
|
|
77
167
|
const ticker = this.mappings.instrumentToTicker[instrumentId] || `id_${instrumentId}`;
|
|
78
|
-
|
|
79
|
-
const avg_day1_value = this.asset_values[instrumentId].day1_value_sum / this.user_count;
|
|
80
|
-
const avg_day2_value = this.asset_values[instrumentId].day2_value_sum / this.user_count;
|
|
81
|
-
const priceChangePct = getDailyPriceChange(instrumentId, yesterdayStr, todayStr, this.priceMap);
|
|
82
168
|
|
|
83
|
-
|
|
169
|
+
const { total_invested_yesterday, total_invested_today, price_change_yesterday, cohort } = data;
|
|
84
170
|
|
|
85
|
-
|
|
86
|
-
|
|
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;
|
|
87
176
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
+
}
|
|
93
184
|
}
|
|
94
|
-
return
|
|
185
|
+
return result;
|
|
95
186
|
}
|
|
96
187
|
|
|
97
|
-
reset() {
|
|
188
|
+
reset() {
|
|
189
|
+
this.assetData.clear();
|
|
190
|
+
this.mappings = null;
|
|
191
|
+
this.inLossCohorts = null;
|
|
192
|
+
}
|
|
98
193
|
}
|
|
194
|
+
|
|
99
195
|
module.exports = InLossAssetCrowdFlow;
|