aiden-shared-calculations-unified 1.0.35 → 1.0.37
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 -165
- package/calculations/behavioural/historical/drawdown_response.js +58 -58
- package/calculations/behavioural/historical/dumb-cohort-flow.js +217 -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 +217 -250
- package/calculations/behavioural/historical/smart_money_flow.js +165 -165
- package/calculations/behavioural/historical/user-investment-profile.js +358 -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,218 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @fileoverview Calculates "Net Crowd Flow" and "Sector Rotation"
|
|
3
|
-
* *only* for the "Dumb Cohort" (Bottom 20% of Investor Scores).
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
const {
|
|
12
|
-
|
|
13
|
-
const
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
if (
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
const
|
|
197
|
-
const
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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
|
+
* --- META REFACTOR ---
|
|
6
|
+
* This calculation is now `type: "meta"` to consume in-memory dependencies.
|
|
7
|
+
* It runs ONCE per day, receives the in-memory cache, and must
|
|
8
|
+
* perform its own user data streaming.
|
|
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
|
+
const { loadDataByRefs } = require('../../../../bulltrackers-module/functions/computation-system/utils/data_loader'); // Adjust path as needed
|
|
16
|
+
|
|
17
|
+
const COHORT_PERCENTILE = 0.2; // Bottom 20%
|
|
18
|
+
const PROFILE_CALC_ID = 'user-investment-profile'; // The calc to read IS scores from
|
|
19
|
+
|
|
20
|
+
class DumbCohortFlow {
|
|
21
|
+
constructor() {
|
|
22
|
+
// Meta-calc, no constructor state needed
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Loads the Investor Scores, calculates the cohort threshold, and builds the Set of user IDs.
|
|
27
|
+
* --- MODIFIED: Reads from in-memory 'computedDependencies' ---
|
|
28
|
+
*/
|
|
29
|
+
_loadCohort(logger, computedDependencies) {
|
|
30
|
+
logger.log('INFO', '[DumbCohortFlow] Loading Investor Scores from in-memory cache...');
|
|
31
|
+
|
|
32
|
+
const profileData = computedDependencies[PROFILE_CALC_ID];
|
|
33
|
+
|
|
34
|
+
if (!profileData || !profileData.daily_investor_scores || Object.keys(profileData.daily_investor_scores).length === 0) {
|
|
35
|
+
logger.log('WARN', `[DumbCohortFlow] Cannot find dependency in-memory: ${PROFILE_CALC_ID}. Cohort will not be built.`);
|
|
36
|
+
return null; // Return null to signal failure
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const scores = profileData.daily_investor_scores;
|
|
40
|
+
const allScores = Object.entries(scores).map(([userId, score]) => ({ userId, score }));
|
|
41
|
+
allScores.sort((a, b) => a.score - b.score);
|
|
42
|
+
|
|
43
|
+
const thresholdIndex = Math.floor(allScores.length * COHORT_PERCENTILE);
|
|
44
|
+
const thresholdScore = allScores[thresholdIndex]?.score || 0; // Get 20th percentile score
|
|
45
|
+
|
|
46
|
+
const dumbCohortIds = new Set(
|
|
47
|
+
allScores.filter(s => s.score <= thresholdScore).map(s => s.userId) // Get users *at or below*
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
logger.log('INFO', `[DumbCohortFlow] Cohort built. ${dumbCohortIds.size} users at or below ${thresholdScore.toFixed(2)} (20th percentile).`);
|
|
51
|
+
return dumbCohortIds;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// --- Asset Flow Helpers (unchanged) ---
|
|
55
|
+
_initAsset(asset_values, instrumentId) {
|
|
56
|
+
if (!asset_values[instrumentId]) {
|
|
57
|
+
asset_values[instrumentId] = { day1_value_sum: 0, day2_value_sum: 0 };
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
_sumAssetValue(positions) {
|
|
61
|
+
const valueMap = {};
|
|
62
|
+
if (!positions || !Array.isArray(positions)) return valueMap;
|
|
63
|
+
for (const pos of positions) {
|
|
64
|
+
if (pos && pos.InstrumentID && pos.Value) {
|
|
65
|
+
valueMap[pos.InstrumentID] = (valueMap[pos.InstrumentID] || 0) + pos.Value;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return valueMap;
|
|
69
|
+
}
|
|
70
|
+
// --- Sector Rotation Helper (unchanged) ---
|
|
71
|
+
_accumulateSectorInvestment(portfolio, target, sectorMap) {
|
|
72
|
+
if (portfolio && portfolio.AggregatedPositions) {
|
|
73
|
+
for (const pos of portfolio.AggregatedPositions) {
|
|
74
|
+
const sector = sectorMap[pos.InstrumentID] || 'N/A';
|
|
75
|
+
target[sector] = (target[sector] || 0) + (pos.Invested || pos.Amount || 0);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* PROCESS: META REFACTOR
|
|
82
|
+
* This now runs ONCE, loads all data, streams users, and returns one big result.
|
|
83
|
+
*/
|
|
84
|
+
async process(dateStr, dependencies, config, computedDependencies) {
|
|
85
|
+
const { logger, db, rootData, calculationUtils } = dependencies;
|
|
86
|
+
const { portfolioRefs } = rootData;
|
|
87
|
+
logger.log('INFO', '[DumbCohortFlow] Starting meta-process...');
|
|
88
|
+
|
|
89
|
+
// 1. Load Cohort from in-memory dependency
|
|
90
|
+
const dumbCohortIds = this._loadCohort(logger, computedDependencies);
|
|
91
|
+
if (!dumbCohortIds) {
|
|
92
|
+
return null; // Dependency failed
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// 2. Load external dependencies (prices, sectors)
|
|
96
|
+
const [priceMap, mappings, sectorMap] = await Promise.all([
|
|
97
|
+
loadAllPriceData(),
|
|
98
|
+
loadInstrumentMappings(),
|
|
99
|
+
getInstrumentSectorMap()
|
|
100
|
+
]);
|
|
101
|
+
if (!priceMap || !mappings || !sectorMap || Object.keys(priceMap).length === 0) {
|
|
102
|
+
logger.log('ERROR', '[DumbCohortFlow] Failed to load critical price/mapping/sector data. Aborting.');
|
|
103
|
+
return null; // Return null to trigger backfill
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// 3. Load "yesterday's" portfolio data for comparison
|
|
107
|
+
const yesterdayDate = new Date(dateStr + 'T00:00:00Z');
|
|
108
|
+
yesterdayDate.setUTCDate(yesterdayDate.getUTCDate() - 1);
|
|
109
|
+
const yesterdayStr = yesterdayDate.toISOString().slice(0, 10);
|
|
110
|
+
const yesterdayRefs = await calculationUtils.getPortfolioPartRefs(config, dependencies, yesterdayStr);
|
|
111
|
+
const yesterdayPortfolios = await loadFullDayMap(config, dependencies, yesterdayRefs);
|
|
112
|
+
logger.log('INFO', `[DumbCohortFlow] Loaded ${yesterdayRefs.length} part refs for yesterday.`);
|
|
113
|
+
|
|
114
|
+
// 4. Stream "today's" portfolio data and process
|
|
115
|
+
|
|
116
|
+
// --- Local state for this run ---
|
|
117
|
+
const asset_values = {};
|
|
118
|
+
const todaySectorInvestment = {};
|
|
119
|
+
const yesterdaySectorInvestment = {};
|
|
120
|
+
let user_count = 0;
|
|
121
|
+
// --- End Local state ---
|
|
122
|
+
|
|
123
|
+
const batchSize = config.partRefBatchSize || 10;
|
|
124
|
+
for (let i = 0; i < portfolioRefs.length; i += batchSize) {
|
|
125
|
+
const batchRefs = portfolioRefs.slice(i, i + batchSize);
|
|
126
|
+
const todayPortfoliosChunk = await loadDataByRefs(config, dependencies, batchRefs);
|
|
127
|
+
|
|
128
|
+
for (const uid in todayPortfoliosChunk) {
|
|
129
|
+
|
|
130
|
+
// --- Filter user ---
|
|
131
|
+
if (!dumbCohortIds.has(uid)) {
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const pToday = todayPortfoliosChunk[uid];
|
|
136
|
+
const pYesterday = yesterdayPortfolios[uid];
|
|
137
|
+
|
|
138
|
+
if (!pToday || !pYesterday || !pToday.AggregatedPositions || !pYesterday.AggregatedPositions) {
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// --- User is in cohort, run logic ---
|
|
143
|
+
|
|
144
|
+
// 4a. RUN ASSET FLOW LOGIC
|
|
145
|
+
const yesterdayValues = this._sumAssetValue(pYesterday.AggregatedPositions);
|
|
146
|
+
const todayValues = this._sumAssetValue(pToday.AggregatedPositions);
|
|
147
|
+
const allInstrumentIds = new Set([...Object.keys(yesterdayValues), ...Object.keys(todayValues)]);
|
|
148
|
+
|
|
149
|
+
for (const instrumentId of allInstrumentIds) {
|
|
150
|
+
this._initAsset(asset_values, instrumentId);
|
|
151
|
+
asset_values[instrumentId].day1_value_sum += (yesterdayValues[instrumentId] || 0);
|
|
152
|
+
asset_values[instrumentId].day2_value_sum += (todayValues[instrumentId] || 0);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// 4b. RUN SECTOR ROTATION LOGIC
|
|
156
|
+
this._accumulateSectorInvestment(pToday, todaySectorInvestment, sectorMap);
|
|
157
|
+
this._accumulateSectorInvestment(pYesterday, yesterdaySectorInvestment, sectorMap);
|
|
158
|
+
|
|
159
|
+
user_count++;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
logger.log('INFO', `[DumbCohortFlow] Processed ${user_count} users in cohort.`);
|
|
164
|
+
|
|
165
|
+
// --- 5. GETRESULT LOGIC IS NOW INSIDE PROCESS ---
|
|
166
|
+
|
|
167
|
+
if (user_count === 0) {
|
|
168
|
+
logger.warn('[DumbCohortFlow] No users processed for dumb cohort. Returning null.');
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// 5a. Calculate Asset Flow
|
|
173
|
+
const finalAssetFlow = {};
|
|
174
|
+
for (const instrumentId in asset_values) {
|
|
175
|
+
const ticker = mappings.instrumentToTicker[instrumentId] || `id_${instrumentId}`;
|
|
176
|
+
const avg_day1_value = asset_values[instrumentId].day1_value_sum / user_count;
|
|
177
|
+
const avg_day2_value = asset_values[instrumentId].day2_value_sum / user_count;
|
|
178
|
+
const priceChangePct = getDailyPriceChange(instrumentId, yesterdayStr, dateStr, priceMap);
|
|
179
|
+
|
|
180
|
+
if (priceChangePct === null) continue;
|
|
181
|
+
|
|
182
|
+
const expected_day2_value = avg_day1_value * (1 + priceChangePct);
|
|
183
|
+
const net_crowd_flow_pct = avg_day2_value - expected_day2_value;
|
|
184
|
+
|
|
185
|
+
finalAssetFlow[ticker] = {
|
|
186
|
+
net_crowd_flow_pct: net_crowd_flow_pct,
|
|
187
|
+
avg_value_day1_pct: avg_day1_value,
|
|
188
|
+
avg_value_day2_pct: avg_day2_value
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// 5b. Calculate Sector Rotation
|
|
193
|
+
const finalSectorRotation = {};
|
|
194
|
+
const allSectors = new Set([...Object.keys(todaySectorInvestment), ...Object.keys(yesterdaySectorInvestment)]);
|
|
195
|
+
for (const sector of allSectors) {
|
|
196
|
+
const todayAmount = todaySectorInvestment[sector] || 0;
|
|
197
|
+
const yesterdayAmount = yesterdaySectorInvestment[sector] || 0;
|
|
198
|
+
finalSectorRotation[sector] = todayAmount - yesterdayAmount;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (Object.keys(finalAssetFlow).length === 0) {
|
|
202
|
+
logger.warn('[DumbCohortFlow] No asset flow calculated (likely all price data missing). Returning null.');
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// 6. Return combined result
|
|
207
|
+
return {
|
|
208
|
+
asset_flow: finalAssetFlow,
|
|
209
|
+
sector_rotation: finalSectorRotation,
|
|
210
|
+
user_sample_size: user_count
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
async getResult() { return null; }
|
|
215
|
+
reset() { }
|
|
216
|
+
}
|
|
217
|
+
|
|
250
218
|
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;
|