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,250 +1,250 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @fileoverview Calculates "Net Crowd Flow" and "Sector Rotation"
|
|
3
|
-
* *only* for the "Dumb Cohort" (Bottom 20% of Investor Scores).
|
|
4
|
-
*
|
|
5
|
-
* This calc depends on 'user-investment-profile.js' being run first for the same day.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
const { Firestore } = require('@google-cloud/firestore');
|
|
9
|
-
const firestore = new Firestore();
|
|
10
|
-
const { loadAllPriceData, getDailyPriceChange } = require('../../../utils/price_data_provider');
|
|
11
|
-
const { loadInstrumentMappings, getInstrumentSectorMap } = require('../../../utils/sector_mapping_provider');
|
|
12
|
-
|
|
13
|
-
const COHORT_PERCENTILE = 0.2; // Bottom 20%
|
|
14
|
-
const PROFILE_CALC_ID = 'user-investment-profile'; // The calc to read IS scores from
|
|
15
|
-
|
|
16
|
-
class DumbCohortFlow {
|
|
17
|
-
constructor() {
|
|
18
|
-
// Asset Flow
|
|
19
|
-
this.asset_values = {}; // { instrumentId: { day1_value_sum: 0, day2_value_sum: 0 } }
|
|
20
|
-
// Sector Rotation
|
|
21
|
-
this.todaySectorInvestment = {};
|
|
22
|
-
this.yesterdaySectorInvestment = {};
|
|
23
|
-
|
|
24
|
-
// --- START MODIFICATION ---
|
|
25
|
-
this.dumbCohortIds = null; // Set to null. Will be a Set on success.
|
|
26
|
-
// --- END MODIFICATION ---
|
|
27
|
-
|
|
28
|
-
this.user_count = 0; // Number of *cohort* users
|
|
29
|
-
this.priceMap = null;
|
|
30
|
-
this.mappings = null;
|
|
31
|
-
this.sectorMap = null;
|
|
32
|
-
this.dates = {};
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Loads the Investor Scores, calculates the cohort threshold, and builds the Set of user IDs.
|
|
37
|
-
*/
|
|
38
|
-
async _loadCohort(context, dependencies) {
|
|
39
|
-
const { db, logger } = dependencies;
|
|
40
|
-
logger.log('INFO', '[DumbCohortFlow] Loading Investor Scores to build cohort...');
|
|
41
|
-
|
|
42
|
-
try {
|
|
43
|
-
const scoreMapRef = db.collection(context.config.resultsCollection).doc(context.todayDateStr)
|
|
44
|
-
.collection(context.config.resultsSubcollection).doc('behavioural')
|
|
45
|
-
.collection(context.config.computationsSubcollection).doc(PROFILE_CALC_ID);
|
|
46
|
-
|
|
47
|
-
const doc = await scoreMapRef.get();
|
|
48
|
-
|
|
49
|
-
// --- START MODIFICATION ---
|
|
50
|
-
// Check for doc, data, and that the scores map isn't empty
|
|
51
|
-
if (!doc.exists || !doc.data().daily_investor_scores || Object.keys(doc.data().daily_investor_scores).length === 0) {
|
|
52
|
-
logger.log('WARN', '[DumbCohortFlow] Cannot find dependency: daily_investor_scores. Cohort will not be built. Returning null on getResult.');
|
|
53
|
-
// Keep this.dumbCohortIds = null
|
|
54
|
-
return; // Abort
|
|
55
|
-
}
|
|
56
|
-
// --- END MODIFICATION ---
|
|
57
|
-
|
|
58
|
-
const scores = doc.data().daily_investor_scores;
|
|
59
|
-
const allScores = Object.entries(scores).map(([userId, score]) => ({ userId, score }));
|
|
60
|
-
allScores.sort((a, b) => a.score - b.score);
|
|
61
|
-
|
|
62
|
-
const thresholdIndex = Math.floor(allScores.length * COHORT_PERCENTILE);
|
|
63
|
-
const thresholdScore = allScores[thresholdIndex]?.score || 0; // Get 20th percentile score
|
|
64
|
-
|
|
65
|
-
// --- START MODIFICATION ---
|
|
66
|
-
// Successfully loaded, now create the Set
|
|
67
|
-
this.dumbCohortIds = new Set(
|
|
68
|
-
allScores.filter(s => s.score <= thresholdScore).map(s => s.userId) // Get users *at or below*
|
|
69
|
-
);
|
|
70
|
-
// --- END MODIFICATION ---
|
|
71
|
-
|
|
72
|
-
logger.log('INFO', `[DumbCohortFlow] Cohort built. ${this.dumbCohortIds.size} users at or below ${thresholdScore.toFixed(2)} (20th percentile).`);
|
|
73
|
-
|
|
74
|
-
} catch (e) {
|
|
75
|
-
logger.log('ERROR', '[DumbCohortFlow] Failed to load cohort.', { error: e.message });
|
|
76
|
-
// Keep this.dumbCohortIds = null on error
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// --- Asset Flow Helpers ---
|
|
81
|
-
_initAsset(instrumentId) {
|
|
82
|
-
if (!this.asset_values[instrumentId]) {
|
|
83
|
-
this.asset_values[instrumentId] = { day1_value_sum: 0, day2_value_sum: 0 };
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
_sumAssetValue(positions) {
|
|
87
|
-
const valueMap = {};
|
|
88
|
-
if (!positions || !Array.isArray(positions)) return valueMap;
|
|
89
|
-
for (const pos of positions) {
|
|
90
|
-
if (pos && pos.InstrumentID && pos.Value) {
|
|
91
|
-
valueMap[pos.InstrumentID] = (valueMap[pos.InstrumentID] || 0) + pos.Value;
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
return valueMap;
|
|
95
|
-
}
|
|
96
|
-
// --- Sector Rotation Helper ---
|
|
97
|
-
_accumulateSectorInvestment(portfolio, target) {
|
|
98
|
-
if (portfolio && portfolio.AggregatedPositions) {
|
|
99
|
-
for (const pos of portfolio.AggregatedPositions) {
|
|
100
|
-
const sector = this.sectorMap[pos.InstrumentID] || 'N/A';
|
|
101
|
-
target[sector] = (target[sector] || 0) + (pos.Invested || pos.Amount || 0);
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* PROCESS: Runs daily for each user.
|
|
108
|
-
*/
|
|
109
|
-
async process(todayPortfolio, yesterdayPortfolio, userId, context) {
|
|
110
|
-
// 1. Load cohort on first run
|
|
111
|
-
if (!this.dumbCohortIds) {
|
|
112
|
-
await this._loadCohort(context, context.dependencies);
|
|
113
|
-
this.dates.today = context.todayDateStr;
|
|
114
|
-
this.dates.yesterday = context.yesterdayDateStr;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
// 2. Filter user
|
|
118
|
-
// --- START MODIFICATION ---
|
|
119
|
-
// If cohort failed to load, this.dumbCohortIds will be null, and this check will fail correctly.
|
|
120
|
-
if (!this.dumbCohortIds || !this.dumbCohortIds.has(userId) || !todayPortfolio || !yesterdayPortfolio || !todayPortfolio.AggregatedPositions || !yesterdayPortfolio.AggregatedPositions) {
|
|
121
|
-
return;
|
|
122
|
-
}
|
|
123
|
-
// --- END MODIFICATION ---
|
|
124
|
-
|
|
125
|
-
// 3. User is in the cohort, load maps if needed
|
|
126
|
-
if (!this.sectorMap) {
|
|
127
|
-
this.sectorMap = await getInstrumentSectorMap();
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
// --- 4. RUN ASSET FLOW LOGIC ---
|
|
131
|
-
const yesterdayValues = this._sumAssetValue(yesterdayPortfolio.AggregatedPositions);
|
|
132
|
-
const todayValues = this._sumAssetValue(todayPortfolio.AggregatedPositions);
|
|
133
|
-
const allInstrumentIds = new Set([...Object.keys(yesterdayValues), ...Object.keys(todayValues)]);
|
|
134
|
-
|
|
135
|
-
for (const instrumentId of allInstrumentIds) {
|
|
136
|
-
this._initAsset(instrumentId);
|
|
137
|
-
this.asset_values[instrumentId].day1_value_sum += (yesterdayValues[instrumentId] || 0);
|
|
138
|
-
this.asset_values[instrumentId].day2_value_sum += (todayValues[instrumentId] || 0);
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
// --- 5. RUN SECTOR ROTATION LOGIC ---
|
|
142
|
-
this._accumulateSectorInvestment(todayPortfolio, this.todaySectorInvestment);
|
|
143
|
-
this._accumulateSectorInvestment(yesterdayPortfolio, this.yesterdaySectorInvestment);
|
|
144
|
-
|
|
145
|
-
this.user_count++;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
/**
|
|
149
|
-
* GETRESULT: Aggregates and returns the flow data for the cohort.
|
|
150
|
-
*/
|
|
151
|
-
async getResult() {
|
|
152
|
-
// --- START MODIFICATION ---
|
|
153
|
-
// If cohort IDs were never loaded due to dependency failure, return null.
|
|
154
|
-
if (this.dumbCohortIds === null) {
|
|
155
|
-
console.warn('[DumbCohortFlow] Skipping getResult because dependency (user-investment-profile) failed to load.');
|
|
156
|
-
return null;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
// If cohort loaded but no users were processed, also return null (or an empty object, but null is safer for backfill)
|
|
160
|
-
if (this.user_count === 0 || !this.dates.today) {
|
|
161
|
-
console.warn('[DumbCohortFlow] No users processed for dumb cohort. Returning null.');
|
|
162
|
-
return null;
|
|
163
|
-
}
|
|
164
|
-
// --- END MODIFICATION ---
|
|
165
|
-
|
|
166
|
-
// 1. Load dependencies
|
|
167
|
-
if (!this.priceMap || !this.mappings) {
|
|
168
|
-
// --- START MODIFICATION ---
|
|
169
|
-
// Add error handling for this load, and check for empty priceMap
|
|
170
|
-
try {
|
|
171
|
-
const [priceData, mappingData] = await Promise.all([
|
|
172
|
-
loadAllPriceData(),
|
|
173
|
-
loadInstrumentMappings()
|
|
174
|
-
]);
|
|
175
|
-
this.priceMap = priceData;
|
|
176
|
-
this.mappings = mappingData;
|
|
177
|
-
|
|
178
|
-
if (!this.priceMap || Object.keys(this.priceMap).length === 0) {
|
|
179
|
-
console.error('[DumbCohortFlow] CRITICAL: Price map is empty or failed to load. Aborting calculation to allow backfill.');
|
|
180
|
-
return null; // Return null to trigger backfill
|
|
181
|
-
}
|
|
182
|
-
} catch (e) {
|
|
183
|
-
console.error('[DumbCohortFlow] Failed to load price/mapping dependencies:', e);
|
|
184
|
-
return null;
|
|
185
|
-
}
|
|
186
|
-
// --- END MODIFICATION ---
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
// --- 2. Calculate Asset Flow ---
|
|
190
|
-
const finalAssetFlow = {};
|
|
191
|
-
const todayStr = this.dates.today;
|
|
192
|
-
const yesterdayStr = this.dates.yesterday;
|
|
193
|
-
|
|
194
|
-
for (const instrumentId in this.asset_values) {
|
|
195
|
-
const ticker = this.mappings.instrumentToTicker[instrumentId] || `id_${instrumentId}`;
|
|
196
|
-
const avg_day1_value = this.asset_values[instrumentId].day1_value_sum / this.user_count;
|
|
197
|
-
const avg_day2_value = this.asset_values[instrumentId].day2_value_sum / this.user_count;
|
|
198
|
-
const priceChangePct = getDailyPriceChange(instrumentId, yesterdayStr, todayStr, this.priceMap);
|
|
199
|
-
|
|
200
|
-
if (priceChangePct === null) continue; // Skip if price data missing
|
|
201
|
-
|
|
202
|
-
const expected_day2_value = avg_day1_value * (1 + priceChangePct);
|
|
203
|
-
const net_crowd_flow_pct = avg_day2_value - expected_day2_value;
|
|
204
|
-
|
|
205
|
-
finalAssetFlow[ticker] = {
|
|
206
|
-
net_crowd_flow_pct: net_crowd_flow_pct,
|
|
207
|
-
avg_value_day1_pct: avg_day1_value,
|
|
208
|
-
avg_value_day2_pct: avg_day2_value
|
|
209
|
-
};
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
// --- 3. Calculate Sector Rotation ---
|
|
213
|
-
const finalSectorRotation = {};
|
|
214
|
-
const allSectors = new Set([...Object.keys(this.todaySectorInvestment), ...Object.keys(this.yesterdaySectorInvestment)]);
|
|
215
|
-
for (const sector of allSectors) {
|
|
216
|
-
const todayAmount = this.todaySectorInvestment[sector] || 0;
|
|
217
|
-
const yesterdayAmount = this.yesterdaySectorInvestment[sector] || 0;
|
|
218
|
-
finalSectorRotation[sector] = todayAmount - yesterdayAmount;
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
// --- START MODIFICATION ---
|
|
222
|
-
// If no asset flow was calculated (e.g., all price data missing), fail
|
|
223
|
-
if (Object.keys(finalAssetFlow).length === 0) {
|
|
224
|
-
console.warn('[DumbCohortFlow] No asset flow calculated (likely all price data missing). Returning null.');
|
|
225
|
-
return null;
|
|
226
|
-
}
|
|
227
|
-
// --- END MODIFICATION ---
|
|
228
|
-
|
|
229
|
-
// 4. Return combined result
|
|
230
|
-
return {
|
|
231
|
-
asset_flow: finalAssetFlow,
|
|
232
|
-
sector_rotation: finalSectorRotation,
|
|
233
|
-
user_sample_size: this.user_count
|
|
234
|
-
};
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
reset() {
|
|
238
|
-
this.asset_values = {};
|
|
239
|
-
this.todaySectorInvestment = {};
|
|
240
|
-
this.yesterdaySectorInvestment = {};
|
|
241
|
-
this.dumbCohortIds = null;
|
|
242
|
-
this.user_count = 0;
|
|
243
|
-
this.priceMap = null;
|
|
244
|
-
this.mappings = null;
|
|
245
|
-
this.sectorMap = null;
|
|
246
|
-
this.dates = {};
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Calculates "Net Crowd Flow" and "Sector Rotation"
|
|
3
|
+
* *only* for the "Dumb Cohort" (Bottom 20% of Investor Scores).
|
|
4
|
+
*
|
|
5
|
+
* This calc depends on 'user-investment-profile.js' being run first for the same day.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const { Firestore } = require('@google-cloud/firestore');
|
|
9
|
+
const firestore = new Firestore();
|
|
10
|
+
const { loadAllPriceData, getDailyPriceChange } = require('../../../utils/price_data_provider');
|
|
11
|
+
const { loadInstrumentMappings, getInstrumentSectorMap } = require('../../../utils/sector_mapping_provider');
|
|
12
|
+
|
|
13
|
+
const COHORT_PERCENTILE = 0.2; // Bottom 20%
|
|
14
|
+
const PROFILE_CALC_ID = 'user-investment-profile'; // The calc to read IS scores from
|
|
15
|
+
|
|
16
|
+
class DumbCohortFlow {
|
|
17
|
+
constructor() {
|
|
18
|
+
// Asset Flow
|
|
19
|
+
this.asset_values = {}; // { instrumentId: { day1_value_sum: 0, day2_value_sum: 0 } }
|
|
20
|
+
// Sector Rotation
|
|
21
|
+
this.todaySectorInvestment = {};
|
|
22
|
+
this.yesterdaySectorInvestment = {};
|
|
23
|
+
|
|
24
|
+
// --- START MODIFICATION ---
|
|
25
|
+
this.dumbCohortIds = null; // Set to null. Will be a Set on success.
|
|
26
|
+
// --- END MODIFICATION ---
|
|
27
|
+
|
|
28
|
+
this.user_count = 0; // Number of *cohort* users
|
|
29
|
+
this.priceMap = null;
|
|
30
|
+
this.mappings = null;
|
|
31
|
+
this.sectorMap = null;
|
|
32
|
+
this.dates = {};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Loads the Investor Scores, calculates the cohort threshold, and builds the Set of user IDs.
|
|
37
|
+
*/
|
|
38
|
+
async _loadCohort(context, dependencies) {
|
|
39
|
+
const { db, logger } = dependencies;
|
|
40
|
+
logger.log('INFO', '[DumbCohortFlow] Loading Investor Scores to build cohort...');
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
const scoreMapRef = db.collection(context.config.resultsCollection).doc(context.todayDateStr)
|
|
44
|
+
.collection(context.config.resultsSubcollection).doc('behavioural')
|
|
45
|
+
.collection(context.config.computationsSubcollection).doc(PROFILE_CALC_ID);
|
|
46
|
+
|
|
47
|
+
const doc = await scoreMapRef.get();
|
|
48
|
+
|
|
49
|
+
// --- START MODIFICATION ---
|
|
50
|
+
// Check for doc, data, and that the scores map isn't empty
|
|
51
|
+
if (!doc.exists || !doc.data().daily_investor_scores || Object.keys(doc.data().daily_investor_scores).length === 0) {
|
|
52
|
+
logger.log('WARN', '[DumbCohortFlow] Cannot find dependency: daily_investor_scores. Cohort will not be built. Returning null on getResult.');
|
|
53
|
+
// Keep this.dumbCohortIds = null
|
|
54
|
+
return; // Abort
|
|
55
|
+
}
|
|
56
|
+
// --- END MODIFICATION ---
|
|
57
|
+
|
|
58
|
+
const scores = doc.data().daily_investor_scores;
|
|
59
|
+
const allScores = Object.entries(scores).map(([userId, score]) => ({ userId, score }));
|
|
60
|
+
allScores.sort((a, b) => a.score - b.score);
|
|
61
|
+
|
|
62
|
+
const thresholdIndex = Math.floor(allScores.length * COHORT_PERCENTILE);
|
|
63
|
+
const thresholdScore = allScores[thresholdIndex]?.score || 0; // Get 20th percentile score
|
|
64
|
+
|
|
65
|
+
// --- START MODIFICATION ---
|
|
66
|
+
// Successfully loaded, now create the Set
|
|
67
|
+
this.dumbCohortIds = new Set(
|
|
68
|
+
allScores.filter(s => s.score <= thresholdScore).map(s => s.userId) // Get users *at or below*
|
|
69
|
+
);
|
|
70
|
+
// --- END MODIFICATION ---
|
|
71
|
+
|
|
72
|
+
logger.log('INFO', `[DumbCohortFlow] Cohort built. ${this.dumbCohortIds.size} users at or below ${thresholdScore.toFixed(2)} (20th percentile).`);
|
|
73
|
+
|
|
74
|
+
} catch (e) {
|
|
75
|
+
logger.log('ERROR', '[DumbCohortFlow] Failed to load cohort.', { error: e.message });
|
|
76
|
+
// Keep this.dumbCohortIds = null on error
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// --- Asset Flow Helpers ---
|
|
81
|
+
_initAsset(instrumentId) {
|
|
82
|
+
if (!this.asset_values[instrumentId]) {
|
|
83
|
+
this.asset_values[instrumentId] = { day1_value_sum: 0, day2_value_sum: 0 };
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
_sumAssetValue(positions) {
|
|
87
|
+
const valueMap = {};
|
|
88
|
+
if (!positions || !Array.isArray(positions)) return valueMap;
|
|
89
|
+
for (const pos of positions) {
|
|
90
|
+
if (pos && pos.InstrumentID && pos.Value) {
|
|
91
|
+
valueMap[pos.InstrumentID] = (valueMap[pos.InstrumentID] || 0) + pos.Value;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return valueMap;
|
|
95
|
+
}
|
|
96
|
+
// --- Sector Rotation Helper ---
|
|
97
|
+
_accumulateSectorInvestment(portfolio, target) {
|
|
98
|
+
if (portfolio && portfolio.AggregatedPositions) {
|
|
99
|
+
for (const pos of portfolio.AggregatedPositions) {
|
|
100
|
+
const sector = this.sectorMap[pos.InstrumentID] || 'N/A';
|
|
101
|
+
target[sector] = (target[sector] || 0) + (pos.Invested || pos.Amount || 0);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* PROCESS: Runs daily for each user.
|
|
108
|
+
*/
|
|
109
|
+
async process(todayPortfolio, yesterdayPortfolio, userId, context) {
|
|
110
|
+
// 1. Load cohort on first run
|
|
111
|
+
if (!this.dumbCohortIds) {
|
|
112
|
+
await this._loadCohort(context, context.dependencies);
|
|
113
|
+
this.dates.today = context.todayDateStr;
|
|
114
|
+
this.dates.yesterday = context.yesterdayDateStr;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// 2. Filter user
|
|
118
|
+
// --- START MODIFICATION ---
|
|
119
|
+
// If cohort failed to load, this.dumbCohortIds will be null, and this check will fail correctly.
|
|
120
|
+
if (!this.dumbCohortIds || !this.dumbCohortIds.has(userId) || !todayPortfolio || !yesterdayPortfolio || !todayPortfolio.AggregatedPositions || !yesterdayPortfolio.AggregatedPositions) {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
// --- END MODIFICATION ---
|
|
124
|
+
|
|
125
|
+
// 3. User is in the cohort, load maps if needed
|
|
126
|
+
if (!this.sectorMap) {
|
|
127
|
+
this.sectorMap = await getInstrumentSectorMap();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// --- 4. RUN ASSET FLOW LOGIC ---
|
|
131
|
+
const yesterdayValues = this._sumAssetValue(yesterdayPortfolio.AggregatedPositions);
|
|
132
|
+
const todayValues = this._sumAssetValue(todayPortfolio.AggregatedPositions);
|
|
133
|
+
const allInstrumentIds = new Set([...Object.keys(yesterdayValues), ...Object.keys(todayValues)]);
|
|
134
|
+
|
|
135
|
+
for (const instrumentId of allInstrumentIds) {
|
|
136
|
+
this._initAsset(instrumentId);
|
|
137
|
+
this.asset_values[instrumentId].day1_value_sum += (yesterdayValues[instrumentId] || 0);
|
|
138
|
+
this.asset_values[instrumentId].day2_value_sum += (todayValues[instrumentId] || 0);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// --- 5. RUN SECTOR ROTATION LOGIC ---
|
|
142
|
+
this._accumulateSectorInvestment(todayPortfolio, this.todaySectorInvestment);
|
|
143
|
+
this._accumulateSectorInvestment(yesterdayPortfolio, this.yesterdaySectorInvestment);
|
|
144
|
+
|
|
145
|
+
this.user_count++;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* GETRESULT: Aggregates and returns the flow data for the cohort.
|
|
150
|
+
*/
|
|
151
|
+
async getResult() {
|
|
152
|
+
// --- START MODIFICATION ---
|
|
153
|
+
// If cohort IDs were never loaded due to dependency failure, return null.
|
|
154
|
+
if (this.dumbCohortIds === null) {
|
|
155
|
+
console.warn('[DumbCohortFlow] Skipping getResult because dependency (user-investment-profile) failed to load.');
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// If cohort loaded but no users were processed, also return null (or an empty object, but null is safer for backfill)
|
|
160
|
+
if (this.user_count === 0 || !this.dates.today) {
|
|
161
|
+
console.warn('[DumbCohortFlow] No users processed for dumb cohort. Returning null.');
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
// --- END MODIFICATION ---
|
|
165
|
+
|
|
166
|
+
// 1. Load dependencies
|
|
167
|
+
if (!this.priceMap || !this.mappings) {
|
|
168
|
+
// --- START MODIFICATION ---
|
|
169
|
+
// Add error handling for this load, and check for empty priceMap
|
|
170
|
+
try {
|
|
171
|
+
const [priceData, mappingData] = await Promise.all([
|
|
172
|
+
loadAllPriceData(),
|
|
173
|
+
loadInstrumentMappings()
|
|
174
|
+
]);
|
|
175
|
+
this.priceMap = priceData;
|
|
176
|
+
this.mappings = mappingData;
|
|
177
|
+
|
|
178
|
+
if (!this.priceMap || Object.keys(this.priceMap).length === 0) {
|
|
179
|
+
console.error('[DumbCohortFlow] CRITICAL: Price map is empty or failed to load. Aborting calculation to allow backfill.');
|
|
180
|
+
return null; // Return null to trigger backfill
|
|
181
|
+
}
|
|
182
|
+
} catch (e) {
|
|
183
|
+
console.error('[DumbCohortFlow] Failed to load price/mapping dependencies:', e);
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
// --- END MODIFICATION ---
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// --- 2. Calculate Asset Flow ---
|
|
190
|
+
const finalAssetFlow = {};
|
|
191
|
+
const todayStr = this.dates.today;
|
|
192
|
+
const yesterdayStr = this.dates.yesterday;
|
|
193
|
+
|
|
194
|
+
for (const instrumentId in this.asset_values) {
|
|
195
|
+
const ticker = this.mappings.instrumentToTicker[instrumentId] || `id_${instrumentId}`;
|
|
196
|
+
const avg_day1_value = this.asset_values[instrumentId].day1_value_sum / this.user_count;
|
|
197
|
+
const avg_day2_value = this.asset_values[instrumentId].day2_value_sum / this.user_count;
|
|
198
|
+
const priceChangePct = getDailyPriceChange(instrumentId, yesterdayStr, todayStr, this.priceMap);
|
|
199
|
+
|
|
200
|
+
if (priceChangePct === null) continue; // Skip if price data missing
|
|
201
|
+
|
|
202
|
+
const expected_day2_value = avg_day1_value * (1 + priceChangePct);
|
|
203
|
+
const net_crowd_flow_pct = avg_day2_value - expected_day2_value;
|
|
204
|
+
|
|
205
|
+
finalAssetFlow[ticker] = {
|
|
206
|
+
net_crowd_flow_pct: net_crowd_flow_pct,
|
|
207
|
+
avg_value_day1_pct: avg_day1_value,
|
|
208
|
+
avg_value_day2_pct: avg_day2_value
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// --- 3. Calculate Sector Rotation ---
|
|
213
|
+
const finalSectorRotation = {};
|
|
214
|
+
const allSectors = new Set([...Object.keys(this.todaySectorInvestment), ...Object.keys(this.yesterdaySectorInvestment)]);
|
|
215
|
+
for (const sector of allSectors) {
|
|
216
|
+
const todayAmount = this.todaySectorInvestment[sector] || 0;
|
|
217
|
+
const yesterdayAmount = this.yesterdaySectorInvestment[sector] || 0;
|
|
218
|
+
finalSectorRotation[sector] = todayAmount - yesterdayAmount;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// --- START MODIFICATION ---
|
|
222
|
+
// If no asset flow was calculated (e.g., all price data missing), fail
|
|
223
|
+
if (Object.keys(finalAssetFlow).length === 0) {
|
|
224
|
+
console.warn('[DumbCohortFlow] No asset flow calculated (likely all price data missing). Returning null.');
|
|
225
|
+
return null;
|
|
226
|
+
}
|
|
227
|
+
// --- END MODIFICATION ---
|
|
228
|
+
|
|
229
|
+
// 4. Return combined result
|
|
230
|
+
return {
|
|
231
|
+
asset_flow: finalAssetFlow,
|
|
232
|
+
sector_rotation: finalSectorRotation,
|
|
233
|
+
user_sample_size: this.user_count
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
reset() {
|
|
238
|
+
this.asset_values = {};
|
|
239
|
+
this.todaySectorInvestment = {};
|
|
240
|
+
this.yesterdaySectorInvestment = {};
|
|
241
|
+
this.dumbCohortIds = null;
|
|
242
|
+
this.user_count = 0;
|
|
243
|
+
this.priceMap = null;
|
|
244
|
+
this.mappings = null;
|
|
245
|
+
this.sectorMap = null;
|
|
246
|
+
this.dates = {};
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
250
|
module.exports = DumbCohortFlow;
|
|
@@ -1,58 +1,58 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Analyzes user behavior after a position experiences a >10% gain.
|
|
3
|
-
*/
|
|
4
|
-
class GainResponse {
|
|
5
|
-
constructor() {
|
|
6
|
-
this.gain_events = {
|
|
7
|
-
held_position: 0,
|
|
8
|
-
closed_position: 0,
|
|
9
|
-
reduced_position: 0 // e.g., took partial profit
|
|
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., 23.5) shows the threshold should be 10.0.
|
|
31
|
-
const gainPercent = yPos.NetProfit || 0;
|
|
32
|
-
const yPosId = yPos.PositionID || yPos.InstrumentID;
|
|
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++;
|
|
45
|
-
} else {
|
|
46
|
-
// Position was held (or added to)
|
|
47
|
-
this.gain_events.held_position++;
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
getResult() {
|
|
54
|
-
return this.gain_events;
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Analyzes user behavior after a position experiences a >10% gain.
|
|
3
|
+
*/
|
|
4
|
+
class GainResponse {
|
|
5
|
+
constructor() {
|
|
6
|
+
this.gain_events = {
|
|
7
|
+
held_position: 0,
|
|
8
|
+
closed_position: 0,
|
|
9
|
+
reduced_position: 0 // e.g., took partial profit
|
|
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., 23.5) shows the threshold should be 10.0.
|
|
31
|
+
const gainPercent = yPos.NetProfit || 0;
|
|
32
|
+
const yPosId = yPos.PositionID || yPos.InstrumentID;
|
|
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++;
|
|
45
|
+
} else {
|
|
46
|
+
// Position was held (or added to)
|
|
47
|
+
this.gain_events.held_position++;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
getResult() {
|
|
54
|
+
return this.gain_events;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
58
|
module.exports = GainResponse;
|