aiden-shared-calculations-unified 1.0.64 → 1.0.65
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,58 +1,136 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
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)
|
|
3
12
|
*/
|
|
4
13
|
class DrawdownResponse {
|
|
5
14
|
constructor() {
|
|
6
|
-
this.
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
15
|
+
this.actions = {
|
|
16
|
+
held: 0,
|
|
17
|
+
closed: 0,
|
|
18
|
+
added: 0
|
|
10
19
|
};
|
|
20
|
+
this.total_in_drawdown = 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 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;
|
|
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% 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++;
|
|
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++;
|
|
45
96
|
} else {
|
|
46
|
-
|
|
47
|
-
|
|
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
|
+
}
|
|
48
105
|
}
|
|
49
106
|
}
|
|
50
107
|
}
|
|
51
108
|
}
|
|
52
109
|
|
|
53
110
|
getResult() {
|
|
54
|
-
|
|
55
|
-
|
|
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;
|
|
56
134
|
}
|
|
57
135
|
}
|
|
58
136
|
|
|
@@ -1,218 +1,238 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @fileoverview
|
|
3
|
-
* *only* for the "Dumb Cohort" (Bottom 20% of Investor Scores).
|
|
2
|
+
* @fileoverview Calculation (Pass 3) for dumb cohort flow.
|
|
4
3
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
4
|
+
* This metric calculates the "Net Crowd Flow Percentage" for the
|
|
5
|
+
* "Dumb Cohort" (bottom 20% of investors).
|
|
6
|
+
*
|
|
7
|
+
* This calculation *depends* on 'user_profitability_tracker'
|
|
8
|
+
* to identify the cohort.
|
|
9
9
|
*/
|
|
10
|
-
|
|
11
|
-
const { Firestore } = require('@google-cloud/firestore');
|
|
12
|
-
const firestore = new Firestore();
|
|
13
|
-
const { loadAllPriceData, getDailyPriceChange } = require('../../../utils/price_data_provider');
|
|
14
|
-
const { loadInstrumentMappings, getInstrumentSectorMap } = require('../../../utils/sector_mapping_provider');
|
|
15
|
-
// NOTE: Corrected relative path for data_loader
|
|
16
|
-
const { loadDataByRefs, getPortfolioPartRefs, loadFullDayMap } = require('../../../../bulltrackers-module/functions/computation-system/utils/data_loader');
|
|
17
|
-
|
|
18
|
-
const COHORT_PERCENTILE = 0.2; // Bottom 20%
|
|
19
|
-
const PROFILE_CALC_ID = 'user-investment-profile'; // The calc to read IS scores from
|
|
10
|
+
const { loadInstrumentMappings } = require('../../../utils/sector_mapping_provider');
|
|
20
11
|
|
|
21
12
|
class DumbCohortFlow {
|
|
13
|
+
constructor() {
|
|
14
|
+
this.assetData = new Map();
|
|
15
|
+
this.sectorData = new Map();
|
|
16
|
+
this.mappings = null;
|
|
17
|
+
this.dumbCohortUserIds = null;
|
|
18
|
+
}
|
|
22
19
|
|
|
23
20
|
/**
|
|
24
|
-
*
|
|
21
|
+
* Defines the output schema for this calculation.
|
|
22
|
+
* @returns {object} JSON Schema object
|
|
25
23
|
*/
|
|
26
|
-
static
|
|
27
|
-
|
|
28
|
-
|
|
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
|
+
};
|
|
29
34
|
|
|
30
|
-
|
|
31
|
-
|
|
35
|
+
return {
|
|
36
|
+
"type": "object",
|
|
37
|
+
"description": "Calculates net capital flow % (price-adjusted) for the 'Dumb Cohort' (bottom 20% users), aggregated by asset and sector.",
|
|
38
|
+
"properties": {
|
|
39
|
+
"cohort_size": {
|
|
40
|
+
"type": "number",
|
|
41
|
+
"description": "The number of users identified as being in the Dumb 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
|
+
};
|
|
32
58
|
}
|
|
33
59
|
|
|
34
60
|
/**
|
|
35
|
-
*
|
|
61
|
+
* Statically declare dependencies.
|
|
36
62
|
*/
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
const profileData = fetchedDependencies[PROFILE_CALC_ID];
|
|
41
|
-
|
|
42
|
-
if (!profileData || !profileData.daily_investor_scores || Object.keys(profileData.daily_investor_scores).length === 0) {
|
|
43
|
-
logger.log('WARN', `[DumbCohortFlow] Cannot find dependency in fetched data: ${PROFILE_CALC_ID}. Cohort will not be built.`);
|
|
44
|
-
return null; // Return null to signal failure
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
const scores = profileData.daily_investor_scores;
|
|
48
|
-
const allScores = Object.entries(scores).map(([userId, score]) => ({ userId, score }));
|
|
49
|
-
allScores.sort((a, b) => a.score - b.score);
|
|
50
|
-
|
|
51
|
-
const thresholdIndex = Math.floor(allScores.length * COHORT_PERCENTILE);
|
|
52
|
-
const thresholdScore = allScores[thresholdIndex]?.score || 0; // Get 20th percentile score
|
|
53
|
-
|
|
54
|
-
const dumbCohortIds = new Set(
|
|
55
|
-
allScores.filter(s => s.score <= thresholdScore).map(s => s.userId) // Get users *at or below*
|
|
56
|
-
);
|
|
57
|
-
|
|
58
|
-
logger.log('INFO', `[DumbCohortFlow] Cohort built. ${dumbCohortIds.size} users at or below ${thresholdScore.toFixed(2)} (20th percentile).`);
|
|
59
|
-
return dumbCohortIds;
|
|
63
|
+
static getDependencies() {
|
|
64
|
+
return ['user_profitability_tracker'];
|
|
60
65
|
}
|
|
61
66
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
if (!asset_values[instrumentId]) {
|
|
65
|
-
asset_values[instrumentId] = { day1_value_sum: 0, day2_value_sum: 0 };
|
|
66
|
-
}
|
|
67
|
+
_getPortfolioPositions(portfolio) {
|
|
68
|
+
return portfolio?.PublicPositions || portfolio?.AggregatedPositions;
|
|
67
69
|
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
if (!
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
+
});
|
|
75
78
|
}
|
|
76
|
-
return valueMap;
|
|
77
79
|
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
+
});
|
|
84
88
|
}
|
|
85
89
|
}
|
|
86
90
|
|
|
87
91
|
/**
|
|
88
|
-
*
|
|
89
|
-
* @param {string} dateStr The date to run the analysis for (e.g., "2025-10-31").
|
|
90
|
-
* @param {object} dependencies The shared dependencies (db, logger, rootData, etc.).
|
|
91
|
-
* @param {object} config The computation system configuration.
|
|
92
|
-
* @param {object} fetchedDependencies In-memory results from previous passes.
|
|
93
|
-
* @returns {Promise<object|null>} The analysis result or null.
|
|
92
|
+
* Helper to get the cohort IDs from the dependency.
|
|
94
93
|
*/
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
94
|
+
_getDumbCohort(fetchedDependencies) {
|
|
95
|
+
if (this.dumbCohortUserIds) {
|
|
96
|
+
return this.dumbCohortUserIds;
|
|
97
|
+
}
|
|
99
98
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
return null; // Dependency failed
|
|
99
|
+
const profitabilityData = fetchedDependencies['user_profitability_tracker'];
|
|
100
|
+
if (!profitabilityData || !profitabilityData.ranked_users) {
|
|
101
|
+
return new Set();
|
|
104
102
|
}
|
|
105
103
|
|
|
106
|
-
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
logger.log('ERROR', '[DumbCohortFlow] Failed to load critical price/mapping/sector data. Aborting.');
|
|
114
|
-
return null;
|
|
115
|
-
}
|
|
104
|
+
const rankedUsers = profitabilityData.ranked_users;
|
|
105
|
+
const cohortSize = Math.floor(rankedUsers.length * 0.20);
|
|
106
|
+
|
|
107
|
+
// The 'ranked_users' are sorted ascending by P&L, so bottom 20% is the first 20%.
|
|
108
|
+
this.dumbCohortUserIds = new Set(rankedUsers.slice(0, cohortSize).map(u => u.userId));
|
|
109
|
+
return this.dumbCohortUserIds;
|
|
110
|
+
}
|
|
116
111
|
|
|
117
|
-
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
// 4. Stream "today's" portfolio data and process
|
|
126
|
-
const asset_values = {};
|
|
127
|
-
const todaySectorInvestment = {};
|
|
128
|
-
const yesterdaySectorInvestment = {};
|
|
129
|
-
let user_count = 0;
|
|
130
|
-
|
|
131
|
-
const batchSize = config.partRefBatchSize || 10;
|
|
132
|
-
for (let i = 0; i < portfolioRefs.length; i += batchSize) {
|
|
133
|
-
const batchRefs = portfolioRefs.slice(i, i + batchSize);
|
|
134
|
-
const todayPortfoliosChunk = await loadDataByRefs(config, dependencies, batchRefs);
|
|
135
|
-
|
|
136
|
-
for (const uid in todayPortfoliosChunk) {
|
|
137
|
-
|
|
138
|
-
if (!dumbCohortIds.has(uid)) continue; // --- Filter user ---
|
|
139
|
-
|
|
140
|
-
const pToday = todayPortfoliosChunk[uid];
|
|
141
|
-
const pYesterday = yesterdayPortfolios[uid];
|
|
142
|
-
|
|
143
|
-
if (!pToday || !pYesterday || !pToday.AggregatedPositions || !pYesterday.AggregatedPositions) {
|
|
144
|
-
continue;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
// 4a. RUN ASSET FLOW LOGIC
|
|
148
|
-
const yesterdayValues = this._sumAssetValue(pYesterday.AggregatedPositions);
|
|
149
|
-
const todayValues = this._sumAssetValue(pToday.AggregatedPositions);
|
|
150
|
-
const allInstrumentIds = new Set([...Object.keys(yesterdayValues), ...Object.keys(todayValues)]);
|
|
151
|
-
|
|
152
|
-
for (const instrumentId of allInstrumentIds) {
|
|
153
|
-
this._initAsset(asset_values, instrumentId);
|
|
154
|
-
asset_values[instrumentId].day1_value_sum += (yesterdayValues[instrumentId] || 0);
|
|
155
|
-
asset_values[instrumentId].day2_value_sum += (todayValues[instrumentId] || 0);
|
|
156
|
-
}
|
|
112
|
+
process(todayPortfolio, yesterdayPortfolio, userId, context, todayInsights, yesterdayInsights, fetchedDependencies) {
|
|
113
|
+
const dumbCohort = this._getDumbCohort(fetchedDependencies);
|
|
114
|
+
|
|
115
|
+
// This user is not in the "dumb cohort", skip.
|
|
116
|
+
if (!dumbCohort.has(userId)) {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
157
119
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
this._accumulateSectorInvestment(pYesterday, yesterdaySectorInvestment, sectorMap);
|
|
161
|
-
user_count++;
|
|
162
|
-
}
|
|
120
|
+
if (!todayPortfolio || !yesterdayPortfolio) {
|
|
121
|
+
return;
|
|
163
122
|
}
|
|
123
|
+
|
|
124
|
+
const yPos = this._getPortfolioPositions(yesterdayPortfolio);
|
|
125
|
+
const tPos = this._getPortfolioPositions(todayPortfolio);
|
|
126
|
+
|
|
127
|
+
const yPosMap = new Map(yPos?.map(p => [p.InstrumentID, p]) || []);
|
|
128
|
+
const tPosMap = new Map(tPos?.map(p => [p.InstrumentID, p]) || []);
|
|
129
|
+
|
|
130
|
+
const allInstrumentIds = new Set([...yPosMap.keys(), ...tPosMap.keys()]);
|
|
164
131
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
if (user_count === 0) {
|
|
169
|
-
logger.warn('[DumbCohortFlow] No users processed for dumb cohort. Returning null.');
|
|
170
|
-
return null;
|
|
132
|
+
if (!this.mappings) {
|
|
133
|
+
// Context contains the mappings loaded in Pass 1
|
|
134
|
+
this.mappings = context.mappings;
|
|
171
135
|
}
|
|
172
136
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
const
|
|
178
|
-
const avg_day2_value = asset_values[instrumentId].day2_value_sum / user_count;
|
|
179
|
-
const priceChangePct = getDailyPriceChange(instrumentId, yesterdayStr, dateStr, priceMap);
|
|
137
|
+
for (const instrumentId of allInstrumentIds) {
|
|
138
|
+
if (!instrumentId) continue;
|
|
139
|
+
|
|
140
|
+
this._initAsset(instrumentId);
|
|
141
|
+
const asset = this.assetData.get(instrumentId);
|
|
180
142
|
|
|
181
|
-
|
|
143
|
+
const yP = yPosMap.get(instrumentId);
|
|
144
|
+
const tP = tPosMap.get(instrumentId);
|
|
182
145
|
|
|
183
|
-
const
|
|
184
|
-
const
|
|
146
|
+
const yInvested = yP?.InvestedAmount || yP?.Amount || 0;
|
|
147
|
+
const tInvested = tP?.InvestedAmount || tP?.Amount || 0;
|
|
148
|
+
|
|
149
|
+
// Get sector and initialize it
|
|
150
|
+
const sector = this.mappings.instrumentToSector[instrumentId] || 'Other';
|
|
151
|
+
this._initSector(sector);
|
|
152
|
+
const sectorAsset = this.sectorData.get(sector);
|
|
185
153
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
154
|
+
if (yInvested > 0) {
|
|
155
|
+
const yPriceChange = (yP?.PipsRate || 0) / (yP?.OpenRate || 1);
|
|
156
|
+
|
|
157
|
+
asset.total_invested_yesterday += yInvested;
|
|
158
|
+
asset.price_change_yesterday += yPriceChange * yInvested;
|
|
159
|
+
|
|
160
|
+
sectorAsset.total_invested_yesterday += yInvested;
|
|
161
|
+
sectorAsset.price_change_yesterday += yPriceChange * yInvested;
|
|
162
|
+
}
|
|
163
|
+
if (tInvested > 0) {
|
|
164
|
+
asset.total_invested_today += tInvested;
|
|
165
|
+
sectorAsset.total_invested_today += tInvested;
|
|
166
|
+
}
|
|
191
167
|
}
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
const
|
|
196
|
-
for (const
|
|
197
|
-
const
|
|
198
|
-
|
|
199
|
-
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
_calculateFlow(dataMap) {
|
|
171
|
+
const result = {};
|
|
172
|
+
for (const [key, data] of dataMap.entries()) {
|
|
173
|
+
const { total_invested_yesterday, total_invested_today, price_change_yesterday } = data;
|
|
174
|
+
|
|
175
|
+
if (total_invested_yesterday > 0) {
|
|
176
|
+
const avg_price_change_pct = price_change_yesterday / total_invested_yesterday;
|
|
177
|
+
const price_contribution = total_invested_yesterday * avg_price_change_pct;
|
|
178
|
+
const flow_contribution = total_invested_today - (total_invested_yesterday + price_contribution);
|
|
179
|
+
const net_flow_percentage = (flow_contribution / total_invested_yesterday) * 100;
|
|
180
|
+
|
|
181
|
+
result[key] = {
|
|
182
|
+
net_flow_percentage: net_flow_percentage,
|
|
183
|
+
total_invested_today: total_invested_today,
|
|
184
|
+
total_invested_yesterday: total_invested_yesterday
|
|
185
|
+
};
|
|
186
|
+
}
|
|
200
187
|
}
|
|
188
|
+
return result;
|
|
189
|
+
}
|
|
201
190
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
191
|
+
async getResult(fetchedDependencies) {
|
|
192
|
+
// Ensure mappings are loaded (can be from context or loaded now)
|
|
193
|
+
if (!this.mappings) {
|
|
194
|
+
this.mappings = await loadInstrumentMappings();
|
|
205
195
|
}
|
|
196
|
+
|
|
197
|
+
// Ensure cohort is calculated at least once
|
|
198
|
+
const dumbCohort = this._getDumbCohort(fetchedDependencies);
|
|
199
|
+
|
|
200
|
+
// 1. Calculate Asset Flow
|
|
201
|
+
const assetResult = {};
|
|
202
|
+
for (const [instrumentId, data] of this.assetData.entries()) {
|
|
203
|
+
const ticker = this.mappings.instrumentToTicker[instrumentId] || `id_${instrumentId}`;
|
|
204
|
+
const { total_invested_yesterday, total_invested_today, price_change_yesterday } = data;
|
|
205
|
+
|
|
206
|
+
if (total_invested_yesterday > 0) {
|
|
207
|
+
const avg_price_change_pct = price_change_yesterday / total_invested_yesterday;
|
|
208
|
+
const price_contribution = total_invested_yesterday * avg_price_change_pct;
|
|
209
|
+
const flow_contribution = total_invested_today - (total_invested_yesterday + price_contribution);
|
|
210
|
+
const net_flow_percentage = (flow_contribution / total_invested_yesterday) * 100;
|
|
211
|
+
|
|
212
|
+
assetResult[ticker] = {
|
|
213
|
+
net_flow_percentage: net_flow_percentage,
|
|
214
|
+
total_invested_today: total_invested_today,
|
|
215
|
+
total_invested_yesterday: total_invested_yesterday
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// 2. Calculate Sector Flow
|
|
221
|
+
const sectorResult = this._calculateFlow(this.sectorData);
|
|
206
222
|
|
|
207
223
|
return {
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
224
|
+
cohort_size: dumbCohort.size,
|
|
225
|
+
assets: assetResult,
|
|
226
|
+
sectors: sectorResult
|
|
211
227
|
};
|
|
212
228
|
}
|
|
213
229
|
|
|
214
|
-
|
|
215
|
-
|
|
230
|
+
reset() {
|
|
231
|
+
this.assetData.clear();
|
|
232
|
+
this.sectorData.clear();
|
|
233
|
+
this.mappings = null;
|
|
234
|
+
this.dumbCohortUserIds = null;
|
|
235
|
+
}
|
|
216
236
|
}
|
|
217
237
|
|
|
218
238
|
module.exports = DumbCohortFlow;
|