aiden-shared-calculations-unified 1.0.63 → 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,227 +1,232 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @fileoverview
|
|
3
|
-
* *only* for the "Smart Cohort" (Top 20% of Investor Scores).
|
|
2
|
+
* @fileoverview Calculation (Pass 4) for positive expectancy cohort flow.
|
|
4
3
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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
9
|
*/
|
|
10
|
+
const { loadInstrumentMappings } = require('../../../utils/sector_mapping_provider');
|
|
10
11
|
|
|
11
|
-
|
|
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.8; // Top 20%
|
|
19
|
-
const PROFILE_CALC_ID = 'user-investment-profile'; // The calc to read IS scores from
|
|
20
|
-
|
|
21
|
-
class SmartCohortFlow {
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* (NEW) Statically declare dependencies.
|
|
25
|
-
*/
|
|
26
|
-
static getDependencies() {
|
|
27
|
-
return ['user_expectancy_score'];
|
|
28
|
-
}
|
|
29
|
-
|
|
12
|
+
class PositiveExpectancyCohortFlow {
|
|
30
13
|
constructor() {
|
|
31
|
-
|
|
14
|
+
this.assetData = new Map();
|
|
15
|
+
this.sectorData = new Map();
|
|
16
|
+
this.mappings = null;
|
|
17
|
+
this.posExpCohortUserIds = null;
|
|
32
18
|
}
|
|
33
19
|
|
|
34
20
|
/**
|
|
35
|
-
*
|
|
21
|
+
* Defines the output schema for this calculation.
|
|
22
|
+
* @returns {object} JSON Schema object
|
|
36
23
|
*/
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
+
};
|
|
42
34
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
+
};
|
|
46
58
|
}
|
|
47
59
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
// 3. Sort by score, lowest to highest
|
|
55
|
-
allScores.sort((a, b) => a.score - b.score);
|
|
56
|
-
|
|
57
|
-
// 4. Find the 80th percentile (Top 20%)
|
|
58
|
-
const thresholdIndex = Math.floor(allScores.length * 0.80);
|
|
59
|
-
const thresholdScore = allScores[thresholdIndex]?.score || 999;
|
|
60
|
-
|
|
61
|
-
// 5. Filter for users with a *positive expectancy score* AND are in the top 20%
|
|
62
|
-
const cohortIds = new Set(
|
|
63
|
-
allScores.filter(s => s.score >= thresholdScore && s.score > 0)
|
|
64
|
-
.map(s => s.userId)
|
|
65
|
-
);
|
|
60
|
+
/**
|
|
61
|
+
* Statically declare dependencies.
|
|
62
|
+
*/
|
|
63
|
+
static getDependencies() {
|
|
64
|
+
return ['user_expectancy_score']; // Pass 3
|
|
65
|
+
}
|
|
66
66
|
|
|
67
|
-
|
|
68
|
-
|
|
67
|
+
_getPortfolioPositions(portfolio) {
|
|
68
|
+
return portfolio?.PublicPositions || portfolio?.AggregatedPositions;
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
79
|
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
if (!
|
|
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
|
-
return valueMap;
|
|
86
89
|
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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);
|
|
92
106
|
}
|
|
93
107
|
}
|
|
108
|
+
return this.posExpCohortUserIds;
|
|
94
109
|
}
|
|
95
110
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
* @param {string} dateStr The date to run the analysis for (e.g., "2025-10-31").
|
|
99
|
-
* @param {object} dependencies The shared dependencies (db, logger, rootData, etc.).
|
|
100
|
-
* @param {object} config The computation system configuration.
|
|
101
|
-
* @param {object} fetchedDependencies In-memory results from previous passes.
|
|
102
|
-
* @returns {Promise<object|null>} The analysis result or null.
|
|
103
|
-
*/
|
|
104
|
-
async process(dateStr, dependencies, config, fetchedDependencies) {
|
|
105
|
-
const { logger, db, rootData, calculationUtils } = dependencies;
|
|
106
|
-
const { portfolioRefs } = rootData;
|
|
107
|
-
logger.log('INFO', '[SmartCohortFlow] Starting meta-process...');
|
|
111
|
+
process(todayPortfolio, yesterdayPortfolio, userId, context, todayInsights, yesterdayInsights, fetchedDependencies) {
|
|
112
|
+
const cohort = this._getPosExpCohort(fetchedDependencies);
|
|
108
113
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
if (!smartCohortIds) {
|
|
112
|
-
return null; // Dependency failed
|
|
114
|
+
if (!cohort.has(userId)) {
|
|
115
|
+
return;
|
|
113
116
|
}
|
|
114
117
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
loadAllPriceData(),
|
|
118
|
-
loadInstrumentMappings(),
|
|
119
|
-
getInstrumentSectorMap()
|
|
120
|
-
]);
|
|
121
|
-
if (!priceMap || !mappings || !sectorMap || Object.keys(priceMap).length === 0) {
|
|
122
|
-
logger.log('ERROR', '[SmartCohortFlow] Failed to load critical price/mapping/sector data. Aborting.');
|
|
123
|
-
return null;
|
|
118
|
+
if (!todayPortfolio || !yesterdayPortfolio) {
|
|
119
|
+
return;
|
|
124
120
|
}
|
|
125
121
|
|
|
126
|
-
|
|
127
|
-
const
|
|
128
|
-
yesterdayDate.setUTCDate(yesterdayDate.getUTCDate() - 1);
|
|
129
|
-
const yesterdayStr = yesterdayDate.toISOString().slice(0, 10);
|
|
130
|
-
const yesterdayRefs = await getPortfolioPartRefs(config, dependencies, yesterdayStr);
|
|
131
|
-
const yesterdayPortfolios = await loadFullDayMap(config, dependencies, yesterdayRefs);
|
|
132
|
-
logger.log('INFO', `[SmartCohortFlow] Loaded ${yesterdayRefs.length} part refs for yesterday.`);
|
|
133
|
-
|
|
134
|
-
// 4. Stream "today's" portfolio data and process
|
|
135
|
-
const asset_values = {};
|
|
136
|
-
const todaySectorInvestment = {};
|
|
137
|
-
const yesterdaySectorInvestment = {};
|
|
138
|
-
let user_count = 0;
|
|
139
|
-
|
|
140
|
-
const batchSize = config.partRefBatchSize || 10;
|
|
141
|
-
for (let i = 0; i < portfolioRefs.length; i += batchSize) {
|
|
142
|
-
const batchRefs = portfolioRefs.slice(i, i + batchSize);
|
|
143
|
-
const todayPortfoliosChunk = await loadDataByRefs(config, dependencies, batchRefs);
|
|
144
|
-
|
|
145
|
-
for (const uid in todayPortfoliosChunk) {
|
|
146
|
-
|
|
147
|
-
if (!smartCohortIds.has(uid)) continue; // --- Filter user ---
|
|
148
|
-
|
|
149
|
-
const pToday = todayPortfoliosChunk[uid];
|
|
150
|
-
const pYesterday = yesterdayPortfolios[uid];
|
|
151
|
-
|
|
152
|
-
if (!pToday || !pYesterday || !pToday.AggregatedPositions || !pYesterday.AggregatedPositions) {
|
|
153
|
-
continue;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
// 4a. RUN ASSET FLOW LOGIC
|
|
157
|
-
const yesterdayValues = this._sumAssetValue(pYesterday.AggregatedPositions);
|
|
158
|
-
const todayValues = this._sumAssetValue(pToday.AggregatedPositions);
|
|
159
|
-
const allInstrumentIds = new Set([...Object.keys(yesterdayValues), ...Object.keys(todayValues)]);
|
|
160
|
-
|
|
161
|
-
for (const instrumentId of allInstrumentIds) {
|
|
162
|
-
this._initAsset(asset_values, instrumentId);
|
|
163
|
-
asset_values[instrumentId].day1_value_sum += (yesterdayValues[instrumentId] || 0);
|
|
164
|
-
asset_values[instrumentId].day2_value_sum += (todayValues[instrumentId] || 0);
|
|
165
|
-
}
|
|
122
|
+
const yPos = this._getPortfolioPositions(yesterdayPortfolio);
|
|
123
|
+
const tPos = this._getPortfolioPositions(todayPortfolio);
|
|
166
124
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
logger.log('INFO', `[SmartCohortFlow] Processed ${user_count} users in cohort.`);
|
|
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()]);
|
|
175
129
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
logger.warn('[SmartCohortFlow] No users processed for smart cohort. Returning null.');
|
|
179
|
-
return null;
|
|
130
|
+
if (!this.mappings) {
|
|
131
|
+
this.mappings = context.mappings;
|
|
180
132
|
}
|
|
181
133
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
const
|
|
187
|
-
const avg_day2_value = asset_values[instrumentId].day2_value_sum / user_count;
|
|
188
|
-
const priceChangePct = getDailyPriceChange(instrumentId, yesterdayStr, dateStr, priceMap);
|
|
134
|
+
for (const instrumentId of allInstrumentIds) {
|
|
135
|
+
if (!instrumentId) continue;
|
|
136
|
+
|
|
137
|
+
this._initAsset(instrumentId);
|
|
138
|
+
const asset = this.assetData.get(instrumentId);
|
|
189
139
|
|
|
190
|
-
|
|
140
|
+
const yP = yPosMap.get(instrumentId);
|
|
141
|
+
const tP = tPosMap.get(instrumentId);
|
|
191
142
|
|
|
192
|
-
const
|
|
193
|
-
const
|
|
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);
|
|
194
149
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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
|
+
}
|
|
200
163
|
}
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
const
|
|
205
|
-
for (const
|
|
206
|
-
const
|
|
207
|
-
|
|
208
|
-
|
|
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
|
+
}
|
|
209
183
|
}
|
|
184
|
+
return result;
|
|
185
|
+
}
|
|
210
186
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
187
|
+
async getResult(fetchedDependencies) {
|
|
188
|
+
if (!this.mappings) {
|
|
189
|
+
this.mappings = await loadInstrumentMappings();
|
|
214
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);
|
|
215
216
|
|
|
216
217
|
return {
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
218
|
+
cohort_size: cohort.size,
|
|
219
|
+
assets: assetResult,
|
|
220
|
+
sectors: sectorResult
|
|
220
221
|
};
|
|
221
222
|
}
|
|
222
223
|
|
|
223
|
-
|
|
224
|
-
|
|
224
|
+
reset() {
|
|
225
|
+
this.assetData.clear();
|
|
226
|
+
this.sectorData.clear();
|
|
227
|
+
this.mappings = null;
|
|
228
|
+
this.posExpCohortUserIds = null;
|
|
229
|
+
}
|
|
225
230
|
}
|
|
226
231
|
|
|
227
|
-
module.exports =
|
|
232
|
+
module.exports = PositiveExpectancyCohortFlow;
|
|
@@ -1,91 +1,115 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @fileoverview
|
|
3
|
-
* of the "In Profit" cohort vs. the "In Loss" cohort to find
|
|
4
|
-
* powerful divergence signals (e.g., profit-taking, capitulation).
|
|
2
|
+
* @fileoverview Calculation (Pass 4) for profit cohort divergence.
|
|
5
3
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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'.
|
|
9
16
|
*/
|
|
10
|
-
|
|
11
17
|
class ProfitCohortDivergence {
|
|
12
|
-
|
|
18
|
+
constructor() {
|
|
19
|
+
// No per-user processing
|
|
20
|
+
}
|
|
21
|
+
|
|
13
22
|
/**
|
|
14
|
-
*
|
|
23
|
+
* Defines the output schema for this calculation.
|
|
24
|
+
* @returns {object} JSON Schema object
|
|
15
25
|
*/
|
|
16
|
-
static
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
+
};
|
|
22
48
|
}
|
|
23
49
|
|
|
24
50
|
/**
|
|
25
|
-
*
|
|
26
|
-
* @param {string} dateStr The date to run the analysis for (e.g., "2025-10-31").
|
|
27
|
-
* @param {object} dependencies The shared dependencies (db, logger).
|
|
28
|
-
* @param {object} config The computation system configuration.
|
|
29
|
-
* @param {object} fetchedDependencies In-memory results from previous passes.
|
|
30
|
-
* e.g., { 'in-profit-asset-crowd-flow': ..., 'in-loss-asset-crowd-flow': ... }
|
|
31
|
-
* @returns {Promise<object|null>} The analysis result or null.
|
|
51
|
+
* Statically declare dependencies.
|
|
32
52
|
*/
|
|
33
|
-
|
|
34
|
-
|
|
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
|
+
}
|
|
35
63
|
|
|
36
|
-
|
|
37
|
-
const profitFlowData = fetchedDependencies['
|
|
38
|
-
const lossFlowData = fetchedDependencies['
|
|
64
|
+
getResult(fetchedDependencies) {
|
|
65
|
+
const profitFlowData = fetchedDependencies['in_profit_asset_crowd_flow'];
|
|
66
|
+
const lossFlowData = fetchedDependencies['in_loss_asset_crowd_flow'];
|
|
39
67
|
|
|
40
|
-
// 2. Handle missing dependencies
|
|
41
68
|
if (!profitFlowData || !lossFlowData) {
|
|
42
|
-
|
|
43
|
-
return null;
|
|
69
|
+
return {};
|
|
44
70
|
}
|
|
45
71
|
|
|
46
|
-
const results = {};
|
|
47
72
|
const allTickers = new Set([...Object.keys(profitFlowData), ...Object.keys(lossFlowData)]);
|
|
73
|
+
const result = {};
|
|
74
|
+
const THRESHOLD = 1; // Min flow % to be considered 'active'
|
|
48
75
|
|
|
49
|
-
// 3. Correlate
|
|
50
76
|
for (const ticker of allTickers) {
|
|
51
|
-
const
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
const profitSells = profitFlow <= -this.flowThreshold;
|
|
55
|
-
const profitBuys = profitFlow >= this.flowThreshold;
|
|
56
|
-
const lossSells = lossFlow <= -this.flowThreshold;
|
|
57
|
-
const lossBuys = lossFlow >= this.flowThreshold;
|
|
77
|
+
const pFlow = profitFlowData[ticker]?.net_flow_percentage || 0;
|
|
78
|
+
const lFlow = lossFlowData[ticker]?.net_flow_percentage || 0;
|
|
58
79
|
|
|
59
|
-
let status = '
|
|
60
|
-
let detail = 'Both cohorts are acting similarly or flow is insignificant.';
|
|
80
|
+
let status = 'Neutral';
|
|
61
81
|
|
|
62
|
-
if (
|
|
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) {
|
|
63
89
|
status = 'Profit Taking';
|
|
64
|
-
|
|
65
|
-
} else if (profitBuys && lossSells) {
|
|
90
|
+
} else if (Math.abs(pFlow) < THRESHOLD && lFlow < -THRESHOLD) {
|
|
66
91
|
status = 'Capitulation';
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
} else if (
|
|
72
|
-
status = '
|
|
73
|
-
detail = 'All cohorts are net-selling.';
|
|
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)';
|
|
74
98
|
}
|
|
75
99
|
|
|
76
|
-
|
|
100
|
+
result[ticker] = {
|
|
77
101
|
status: status,
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
loss_cohort_flow: lossFlow
|
|
102
|
+
profit_cohort_flow_pct: pFlow,
|
|
103
|
+
loss_cohort_flow_pct: lFlow
|
|
81
104
|
};
|
|
82
105
|
}
|
|
83
|
-
|
|
84
|
-
return
|
|
106
|
+
|
|
107
|
+
return result;
|
|
85
108
|
}
|
|
86
109
|
|
|
87
|
-
|
|
88
|
-
|
|
110
|
+
reset() {
|
|
111
|
+
// No state
|
|
112
|
+
}
|
|
89
113
|
}
|
|
90
114
|
|
|
91
115
|
module.exports = ProfitCohortDivergence;
|