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,413 +1,359 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @fileoverview Calculates a rolling 90-day "Investor Score" (IS) for each normal user.
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
const {
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
const
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
//
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
const
|
|
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
|
-
if (
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
//
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
const
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
const
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
date: todayStr,
|
|
360
|
-
...scores,
|
|
361
|
-
pnl: (this.pnlScores && (userId in this.pnlScores)) ? this.pnlScores[userId] : 0
|
|
362
|
-
});
|
|
363
|
-
|
|
364
|
-
const newHistory = history.slice(-ROLLING_DAYS);
|
|
365
|
-
|
|
366
|
-
// compute rolling averages
|
|
367
|
-
let avg_rd = 0, avg_disc = 0, avg_time = 0, avg_pnl = 0;
|
|
368
|
-
for (const entry of newHistory) {
|
|
369
|
-
avg_rd += (entry.score_rd || 0);
|
|
370
|
-
avg_disc += (entry.score_disc || 0);
|
|
371
|
-
avg_time += (entry.score_time || 0);
|
|
372
|
-
avg_pnl += (entry.pnl || 0);
|
|
373
|
-
}
|
|
374
|
-
const N = newHistory.length || 1;
|
|
375
|
-
avg_rd /= N;
|
|
376
|
-
avg_disc /= N;
|
|
377
|
-
avg_time /= N;
|
|
378
|
-
avg_pnl /= N;
|
|
379
|
-
|
|
380
|
-
// Normalize PNL: avg_pnl is decimal percent (0.005 -> 0.5%). Map to 0-10 scale:
|
|
381
|
-
// multiply by 1000 (0.005 -> 5). Clamp to [-10, 10] to avoid outliers.
|
|
382
|
-
const normalizedPnl = Math.max(-10, Math.min(10, avg_pnl * 1000));
|
|
383
|
-
|
|
384
|
-
// Final IS (weights): discipline 40%, risk/div 30%, timing 20%, pnl 10%
|
|
385
|
-
const finalISRaw = (avg_disc * 0.4) + (avg_rd * 0.3) + (avg_time * 0.2) + (normalizedPnl * 0.1);
|
|
386
|
-
const finalIS = Math.max(0, Math.min(10, finalISRaw));
|
|
387
|
-
|
|
388
|
-
// store in prepared shard result under 'profiles'
|
|
389
|
-
const shardKey = `${SHARD_COLLECTION_NAME}_shard_${shardIndex}`;
|
|
390
|
-
shardedResults[shardKey].profiles[userId] = newHistory;
|
|
391
|
-
|
|
392
|
-
// also set the daily investor score
|
|
393
|
-
dailyInvestorScoreMap[userId] = finalIS;
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
return {
|
|
397
|
-
sharded_user_profile: shardedResults,
|
|
398
|
-
daily_investor_scores: dailyInvestorScoreMap
|
|
399
|
-
};
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
reset() {
|
|
403
|
-
this.dailyUserScores = {};
|
|
404
|
-
this.dependenciesLoaded = false;
|
|
405
|
-
this.priceMap = null;
|
|
406
|
-
this.sectorMap = null;
|
|
407
|
-
this.pnlScores = null;
|
|
408
|
-
this.dates = {};
|
|
409
|
-
this.dependencyLoadedSuccess = false; // <-- MODIFICATION
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Calculates a rolling 90-day "Investor Score" (IS) for each normal user.
|
|
3
|
+
*
|
|
4
|
+
* --- META REFACTOR ---
|
|
5
|
+
* This calculation is now `type: "meta"` to consume in-memory dependencies.
|
|
6
|
+
* It runs ONCE per day, receives the in-memory cache, and must
|
|
7
|
+
* perform its own user data streaming.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const { Firestore } = require('@google-cloud/firestore');
|
|
11
|
+
const firestore = new Firestore();
|
|
12
|
+
const { loadAllPriceData, getDailyPriceChange } = require('../../../utils/price_data_provider');
|
|
13
|
+
const { loadInstrumentMappings, getInstrumentSectorMap } = require('../../../utils/sector_mapping_provider');
|
|
14
|
+
const { loadDataByRefs } = require('../../../../bulltrackers-module/functions/computation-system/utils/data_loader'); // Adjust path as needed
|
|
15
|
+
|
|
16
|
+
// Config
|
|
17
|
+
const NUM_SHARDS = 50; // Must match the number of shards to read/write
|
|
18
|
+
const ROLLING_DAYS = 90;
|
|
19
|
+
const SHARD_COLLECTION_NAME = 'user_profile_history'; // The collection to store sharded history
|
|
20
|
+
const PNL_TRACKER_CALC_ID = 'user-profitability-tracker'; // The calc to read PNL from
|
|
21
|
+
|
|
22
|
+
// Helper: stable shard index for numeric or string IDs
|
|
23
|
+
function getShardIndex(id) {
|
|
24
|
+
const n = parseInt(id, 10);
|
|
25
|
+
if (!Number.isNaN(n)) return Math.abs(n) % NUM_SHARDS;
|
|
26
|
+
let h = 0;
|
|
27
|
+
for (let i = 0; i < id.length; i++) {
|
|
28
|
+
h = ((h << 5) - h) + id.charCodeAt(i);
|
|
29
|
+
h |= 0;
|
|
30
|
+
}
|
|
31
|
+
return Math.abs(h) % NUM_SHARDS;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
class UserInvestmentProfile {
|
|
35
|
+
constructor() {
|
|
36
|
+
// --- META REFACTOR ---
|
|
37
|
+
// All state is now managed inside the `process` function.
|
|
38
|
+
// The constructor, getResult, and reset methods are no longer used
|
|
39
|
+
// by the meta-runner, but we leave them for compatibility.
|
|
40
|
+
// --- END REFACTOR ---
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// ... [Heuristic calculation functions _calculateRiskAndDivScore, _calculateDisciplineScore, _calculateMarketTimingScore] ...
|
|
44
|
+
// These helper functions remain identical to your original file.
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* HEURISTIC 1: Risk & Diversification Score (0-10).
|
|
48
|
+
*/
|
|
49
|
+
_calculateRiskAndDivScore(todayPortfolio, sectorMap) {
|
|
50
|
+
if (!todayPortfolio.AggregatedPositions || todayPortfolio.AggregatedPositions.length === 0) {
|
|
51
|
+
return 5; // neutral
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const positions = todayPortfolio.AggregatedPositions;
|
|
55
|
+
let totalInvested = 0;
|
|
56
|
+
let weightedRetSum = 0;
|
|
57
|
+
let weightedRetSqSum = 0;
|
|
58
|
+
let maxPosition = 0;
|
|
59
|
+
const sectors = new Set();
|
|
60
|
+
|
|
61
|
+
for (const pos of positions) {
|
|
62
|
+
const invested = pos.InvestedAmount || pos.Amount || 0;
|
|
63
|
+
const netProfit = ('NetProfit' in pos) ? pos.NetProfit : (pos.ProfitAndLoss || 0); // decimal % return
|
|
64
|
+
const ret = invested > 0 ? (netProfit) : netProfit; // if invested==0 we still include ret but weight 0
|
|
65
|
+
|
|
66
|
+
weightedRetSum += ret * invested;
|
|
67
|
+
weightedRetSqSum += (ret * ret) * invested;
|
|
68
|
+
totalInvested += invested;
|
|
69
|
+
if (invested > maxPosition) maxPosition = invested;
|
|
70
|
+
|
|
71
|
+
sectors.add(sectorMap[pos.InstrumentID] || 'N/A');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Weighted mean & variance of returns
|
|
75
|
+
const meanReturn = totalInvested > 0 ? (weightedRetSum / totalInvested) : 0;
|
|
76
|
+
const meanReturnSq = totalInvested > 0 ? (weightedRetSqSum / totalInvested) : (meanReturn * meanReturn);
|
|
77
|
+
const variance = Math.max(0, meanReturnSq - (meanReturn * meanReturn));
|
|
78
|
+
const stdReturn = Math.sqrt(variance);
|
|
79
|
+
|
|
80
|
+
// dispersion proxy: mean / std (if std is zero we treat as neutral 0)
|
|
81
|
+
let dispersionRiskProxy = stdReturn > 0 ? meanReturn / stdReturn : 0;
|
|
82
|
+
|
|
83
|
+
// cap and map dispersion proxy to [0..10].
|
|
84
|
+
// dispersionRiskProxy can be outside [-2..4], clamp to reasonable bounds first.
|
|
85
|
+
const capped = Math.max(-2, Math.min(4, dispersionRiskProxy));
|
|
86
|
+
const scoreSharpe = ((capped + 2) / 6) * 10; // maps [-2..4] -> [0..10]
|
|
87
|
+
|
|
88
|
+
// Sector diversification (monotonic - diminishing returns)
|
|
89
|
+
const sectorCount = sectors.size;
|
|
90
|
+
let scoreDiversification = 0;
|
|
91
|
+
if (sectorCount === 1) scoreDiversification = 0;
|
|
92
|
+
else if (sectorCount <= 4) scoreDiversification = 5;
|
|
93
|
+
else if (sectorCount <= 7) scoreDiversification = 8;
|
|
94
|
+
else scoreDiversification = 10;
|
|
95
|
+
|
|
96
|
+
// Position sizing / concentration penalty
|
|
97
|
+
const concentrationRatio = totalInvested > 0 ? (maxPosition / totalInvested) : 0;
|
|
98
|
+
let scoreSizing = 0;
|
|
99
|
+
if (concentrationRatio > 0.8) scoreSizing = 0;
|
|
100
|
+
else if (concentrationRatio > 0.5) scoreSizing = 2;
|
|
101
|
+
else if (concentrationRatio > 0.3) scoreSizing = 5;
|
|
102
|
+
else if (concentrationRatio > 0.15) scoreSizing = 8;
|
|
103
|
+
else scoreSizing = 10;
|
|
104
|
+
|
|
105
|
+
const final = (scoreSharpe * 0.4) + (scoreDiversification * 0.3) + (scoreSizing * 0.3);
|
|
106
|
+
return Math.max(0, Math.min(10, final));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* HEURISTIC 2: Discipline Score (0-10).
|
|
111
|
+
*/
|
|
112
|
+
_calculateDisciplineScore(yesterdayPortfolio = {}, todayPortfolio = {}) {
|
|
113
|
+
const yPositions = yesterdayPortfolio.AggregatedPositions || [];
|
|
114
|
+
const tPositions = new Map((todayPortfolio.AggregatedPositions || []).map(p => [p.PositionID, p]));
|
|
115
|
+
|
|
116
|
+
if (yPositions.length === 0) {
|
|
117
|
+
return 5; // neutral if nothing to judge
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
let eventPoints = 0;
|
|
121
|
+
let eventCount = 0;
|
|
122
|
+
|
|
123
|
+
for (const yPos of yPositions) {
|
|
124
|
+
const profitAndLoss = ('ProfitAndLoss' in yPos) ? yPos.ProfitAndLoss : (yPos.NetProfit || 0);
|
|
125
|
+
const invested = yPos.InvestedAmount || yPos.Amount || 0;
|
|
126
|
+
const pnlPercent = profitAndLoss; // This is already the decimal % return
|
|
127
|
+
|
|
128
|
+
const tPos = tPositions.get(yPos.PositionID);
|
|
129
|
+
|
|
130
|
+
if (!tPos) {
|
|
131
|
+
// Closed position
|
|
132
|
+
eventCount++;
|
|
133
|
+
if (pnlPercent < -0.05) eventPoints += 10; // cut loser (good)
|
|
134
|
+
else if (pnlPercent > 0.20) eventPoints += 8; // took big profit (good)
|
|
135
|
+
else if (pnlPercent > 0 && pnlPercent < 0.05) eventPoints += 2; // paper hands (bad)
|
|
136
|
+
else eventPoints += 5; // neutral close
|
|
137
|
+
} else {
|
|
138
|
+
// Held or modified
|
|
139
|
+
if (pnlPercent < -0.10) {
|
|
140
|
+
eventCount++;
|
|
141
|
+
const tInvested = tPos.InvestedAmount || tPos.Amount || 0;
|
|
142
|
+
if (tInvested > invested) eventPoints += 0; // averaged down (very poor)
|
|
143
|
+
else eventPoints += 3; // held loser (poor)
|
|
144
|
+
} else if (pnlPercent > 0.15) {
|
|
145
|
+
eventCount++;
|
|
146
|
+
eventPoints += 10; // held/added to winner (good)
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const avg = (eventCount > 0) ? (eventPoints / eventCount) : 5;
|
|
152
|
+
return Math.max(0, Math.min(10, avg));
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* HEURISTIC 3: Market Timing Score (0-10).
|
|
157
|
+
*/
|
|
158
|
+
_calculateMarketTimingScore(yesterdayPortfolio = {}, todayPortfolio = {}, priceMap) {
|
|
159
|
+
const yIds = new Set((yesterdayPortfolio.AggregatedPositions || []).map(p => p.PositionID));
|
|
160
|
+
const newPositions = (todayPortfolio.AggregatedPositions || []).filter(p => !yIds.has(p.PositionID));
|
|
161
|
+
|
|
162
|
+
if (newPositions.length === 0) return 5;
|
|
163
|
+
|
|
164
|
+
let timingPoints = 0;
|
|
165
|
+
let timingCount = 0;
|
|
166
|
+
|
|
167
|
+
for (const tPos of newPositions) {
|
|
168
|
+
const prices = priceMap[tPos.InstrumentID];
|
|
169
|
+
if (!prices) continue;
|
|
170
|
+
|
|
171
|
+
// Accept prices as either array or {date:price} map; build sorted array of prices
|
|
172
|
+
let historyPrices = [];
|
|
173
|
+
if (Array.isArray(prices)) {
|
|
174
|
+
// assume array of numbers or objects with .price/.close
|
|
175
|
+
historyPrices = prices
|
|
176
|
+
.map(p => (typeof p === 'number' ? p : (p.price || p.close || null)))
|
|
177
|
+
.filter(v => v != null);
|
|
178
|
+
} else {
|
|
179
|
+
// object keyed by date -> price
|
|
180
|
+
const entries = Object.keys(prices)
|
|
181
|
+
.map(d => ({ d, p: prices[d] }))
|
|
182
|
+
.filter(e => e.p != null)
|
|
183
|
+
.sort((a, b) => new Date(a.d) - new Date(b.d));
|
|
184
|
+
historyPrices = entries.map(e => e.p);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const last30 = historyPrices.slice(-30);
|
|
188
|
+
if (last30.length < 2) continue;
|
|
189
|
+
|
|
190
|
+
const minPrice = Math.min(...last30);
|
|
191
|
+
const maxPrice = Math.max(...last30);
|
|
192
|
+
const openRate = tPos.OpenRate;
|
|
193
|
+
const range = maxPrice - minPrice;
|
|
194
|
+
if (!isFinite(range) || range === 0) continue;
|
|
195
|
+
|
|
196
|
+
let proximity = (openRate - minPrice) / range; // 0 = at low, 1 = at high
|
|
197
|
+
proximity = Math.max(0, Math.min(1, proximity)); // clamp to [0,1]
|
|
198
|
+
|
|
199
|
+
timingCount++;
|
|
200
|
+
if (proximity < 0.2) timingPoints += 10;
|
|
201
|
+
else if (proximity < 0.4) timingPoints += 8;
|
|
202
|
+
else if (proximity > 0.9) timingPoints += 1;
|
|
203
|
+
else if (proximity > 0.7) timingPoints += 3;
|
|
204
|
+
else timingPoints += 5;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const avg = (timingCount > 0) ? (timingPoints / timingCount) : 5;
|
|
208
|
+
return Math.max(0, Math.min(10, avg));
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* PROCESS: META REFACTOR
|
|
214
|
+
* This now runs ONCE, loads all data, streams users, and returns one big result.
|
|
215
|
+
*/
|
|
216
|
+
async process(dateStr, dependencies, config, computedDependencies) {
|
|
217
|
+
const { logger, db, rootData, calculationUtils } = dependencies;
|
|
218
|
+
const { portfolioRefs } = rootData;
|
|
219
|
+
|
|
220
|
+
logger.log('INFO', '[UserInvestmentProfile] Starting meta-process...');
|
|
221
|
+
|
|
222
|
+
// 1. Get Pass 1 dependency from in-memory cache
|
|
223
|
+
const pnlTrackerResult = computedDependencies[PNL_TRACKER_CALC_ID];
|
|
224
|
+
if (!pnlTrackerResult || !pnlTrackerResult.daily_pnl_map) {
|
|
225
|
+
logger.log('WARN', `[UserInvestmentProfile] Missing in-memory dependency '${PNL_TRACKER_CALC_ID}'. Aborting.`);
|
|
226
|
+
return null; // Return null to signal failure
|
|
227
|
+
}
|
|
228
|
+
const pnlScores = pnlTrackerResult.daily_pnl_map;
|
|
229
|
+
logger.log('INFO', `[UserInvestmentProfile] Received ${Object.keys(pnlScores).length} PNL scores in-memory.`);
|
|
230
|
+
|
|
231
|
+
// 2. Load external dependencies (prices, sectors)
|
|
232
|
+
const [priceMap, sectorMap] = await Promise.all([
|
|
233
|
+
loadAllPriceData(),
|
|
234
|
+
getInstrumentSectorMap()
|
|
235
|
+
]);
|
|
236
|
+
if (!priceMap || !sectorMap) {
|
|
237
|
+
logger.log('ERROR', '[UserInvestmentProfile] Failed to load priceMap or sectorMap.');
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// 3. Load "yesterday's" portfolio data for comparison
|
|
242
|
+
const yesterdayDate = new Date(dateStr + 'T00:00:00Z');
|
|
243
|
+
yesterdayDate.setUTCDate(yesterdayDate.getUTCDate() - 1);
|
|
244
|
+
const yesterdayStr = yesterdayDate.toISOString().slice(0, 10);
|
|
245
|
+
const yesterdayRefs = await calculationUtils.getPortfolioPartRefs(config, dependencies, yesterdayStr);
|
|
246
|
+
const yesterdayPortfolios = await loadFullDayMap(config, dependencies, yesterdayRefs);
|
|
247
|
+
logger.log('INFO', `[UserInvestmentProfile] Loaded ${yesterdayRefs.length} part refs for yesterday.`);
|
|
248
|
+
|
|
249
|
+
// 4. Stream "today's" portfolio data and process
|
|
250
|
+
const batchSize = config.partRefBatchSize || 10;
|
|
251
|
+
const dailyUserScores = {}; // Local state for this run
|
|
252
|
+
|
|
253
|
+
for (let i = 0; i < portfolioRefs.length; i += batchSize) {
|
|
254
|
+
const batchRefs = portfolioRefs.slice(i, i + batchSize);
|
|
255
|
+
const todayPortfoliosChunk = await loadDataByRefs(config, dependencies, batchRefs);
|
|
256
|
+
|
|
257
|
+
for (const uid in todayPortfoliosChunk) {
|
|
258
|
+
const pToday = todayPortfoliosChunk[uid];
|
|
259
|
+
if (!pToday || !pToday.AggregatedPositions) continue; // Skip speculators or empty
|
|
260
|
+
|
|
261
|
+
const pYesterday = yesterdayPortfolios[uid] || {};
|
|
262
|
+
|
|
263
|
+
// Run the heuristic calculations
|
|
264
|
+
const score_rd = this._calculateRiskAndDivScore(pToday, sectorMap);
|
|
265
|
+
const score_disc = this._calculateDisciplineScore(pYesterday, pToday);
|
|
266
|
+
const score_time = this._calculateMarketTimingScore(pYesterday, pToday, priceMap);
|
|
267
|
+
|
|
268
|
+
dailyUserScores[uid] = {
|
|
269
|
+
score_rd,
|
|
270
|
+
score_disc,
|
|
271
|
+
score_time
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
logger.log('INFO', `[UserInvestmentProfile] Calculated daily scores for ${Object.keys(dailyUserScores).length} users.`);
|
|
276
|
+
|
|
277
|
+
// --- 5. GETRESULT LOGIC IS NOW INSIDE PROCESS ---
|
|
278
|
+
// (This is the logic from your original getResult())
|
|
279
|
+
|
|
280
|
+
const shardedResults = {};
|
|
281
|
+
for (let i = 0; i < NUM_SHARDS; i++) {
|
|
282
|
+
const shardKey = `${SHARD_COLLECTION_NAME}_shard_${i}`;
|
|
283
|
+
shardedResults[shardKey] = { profiles: {}, lastUpdated: dateStr };
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const dailyInvestorScoreMap = {};
|
|
287
|
+
|
|
288
|
+
// Fetch existing shards in parallel
|
|
289
|
+
const shardPromises = [];
|
|
290
|
+
for (let i = 0; i < NUM_SHARDS; i++) {
|
|
291
|
+
const docRef = firestore.collection(SHARD_COLLECTION_NAME).doc(`${SHARD_COLLECTION_NAME}_shard_${i}`);
|
|
292
|
+
shardPromises.push(docRef.get());
|
|
293
|
+
}
|
|
294
|
+
const shardSnapshots = await Promise.all(shardPromises);
|
|
295
|
+
const existingShards = shardSnapshots.map((snap) => (snap.exists ? snap.data().profiles : {}));
|
|
296
|
+
|
|
297
|
+
// Process users
|
|
298
|
+
for (const userId of Object.keys(dailyUserScores)) {
|
|
299
|
+
const shardIndex = getShardIndex(userId);
|
|
300
|
+
const scores = dailyUserScores[userId];
|
|
301
|
+
|
|
302
|
+
const existingProfiles = existingShards[shardIndex] || {};
|
|
303
|
+
const history = (existingProfiles[userId] || []).slice(); // clone
|
|
304
|
+
|
|
305
|
+
history.push({
|
|
306
|
+
date: dateStr,
|
|
307
|
+
...scores,
|
|
308
|
+
pnl: (pnlScores[userId] || 0)
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
const newHistory = history.slice(-ROLLING_DAYS);
|
|
312
|
+
|
|
313
|
+
// compute rolling averages
|
|
314
|
+
let avg_rd = 0, avg_disc = 0, avg_time = 0, avg_pnl = 0;
|
|
315
|
+
for (const entry of newHistory) {
|
|
316
|
+
avg_rd += (entry.score_rd || 0);
|
|
317
|
+
avg_disc += (entry.score_disc || 0);
|
|
318
|
+
avg_time += (entry.score_time || 0);
|
|
319
|
+
avg_pnl += (entry.pnl || 0);
|
|
320
|
+
}
|
|
321
|
+
const N = newHistory.length || 1;
|
|
322
|
+
avg_rd /= N;
|
|
323
|
+
avg_disc /= N;
|
|
324
|
+
avg_time /= N;
|
|
325
|
+
avg_pnl /= N;
|
|
326
|
+
|
|
327
|
+
const normalizedPnl = Math.max(-10, Math.min(10, avg_pnl * 1000));
|
|
328
|
+
const finalISRaw = (avg_disc * 0.4) + (avg_rd * 0.3) + (avg_time * 0.2) + (normalizedPnl * 0.1);
|
|
329
|
+
const finalIS = Math.max(0, Math.min(10, finalISRaw));
|
|
330
|
+
|
|
331
|
+
const shardKey = `${SHARD_COLLECTION_NAME}_shard_${shardIndex}`;
|
|
332
|
+
shardedResults[shardKey].profiles[userId] = newHistory;
|
|
333
|
+
dailyInvestorScoreMap[userId] = finalIS;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
logger.log('INFO', `[UserInvestmentProfile] Finalized IS scores for ${Object.keys(dailyInvestorScoreMap).length} users.`);
|
|
337
|
+
|
|
338
|
+
// Return the final result object
|
|
339
|
+
return {
|
|
340
|
+
sharded_user_profile: shardedResults,
|
|
341
|
+
daily_investor_scores: dailyInvestorScoreMap
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* getResult is no longer used by the meta-runner.
|
|
347
|
+
*/
|
|
348
|
+
async getResult() {
|
|
349
|
+
return null;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* reset is no longer used by the meta-runner.
|
|
354
|
+
*/
|
|
355
|
+
reset() {
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
413
359
|
module.exports = UserInvestmentProfile;
|