aiden-shared-calculations-unified 1.0.95 → 1.0.97

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.
Files changed (61) hide show
  1. package/calculations/capitulation/asset-volatility-estimator.js +1 -2
  2. package/calculations/capitulation/retail-capitulation-risk-forecast.js +1 -2
  3. package/calculations/core/Insights-total-long-per-stock +56 -0
  4. package/calculations/core/insights-daily-bought-vs-sold-count.js +74 -0
  5. package/calculations/core/insights-daily-ownership-delta.js +70 -0
  6. package/calculations/core/insights-sentimet-per-stock.js +68 -0
  7. package/calculations/core/insights-total-long-per-sector +73 -0
  8. package/calculations/core/insights-total-positions-held.js +49 -0
  9. package/calculations/ghost-book/cost-basis-density.js +1 -2
  10. package/calculations/ghost-book/liquidity-vacuum.js +4 -4
  11. package/calculations/ghost-book/retail-gamma-exposure.js +0 -1
  12. package/calculations/helix/winner-loser-flow.js +1 -1
  13. package/calculations/predicative-alpha/cognitive-dissonance.js +1 -2
  14. package/calculations/predicative-alpha/diamond-hand-fracture.js +1 -2
  15. package/calculations/predicative-alpha/mimetic-latency.js +1 -2
  16. package/package.json +1 -1
  17. package/calculations/legacy/activity_by_pnl_status.js +0 -119
  18. package/calculations/legacy/asset_crowd_flow.js +0 -163
  19. package/calculations/legacy/capital_deployment_strategy.js +0 -108
  20. package/calculations/legacy/capital_liquidation_performance.js +0 -139
  21. package/calculations/legacy/capital_vintage_performance.js +0 -136
  22. package/calculations/legacy/cash-flow-deployment.js +0 -144
  23. package/calculations/legacy/cash-flow-liquidation.js +0 -146
  24. package/calculations/legacy/crowd-cash-flow-proxy.js +0 -128
  25. package/calculations/legacy/crowd_conviction_score.js +0 -261
  26. package/calculations/legacy/crowd_sharpe_ratio_proxy.js +0 -137
  27. package/calculations/legacy/daily_asset_activity.js +0 -128
  28. package/calculations/legacy/daily_user_activity_tracker.js +0 -182
  29. package/calculations/legacy/deposit_withdrawal_percentage.js +0 -125
  30. package/calculations/legacy/diversification_pnl.js +0 -115
  31. package/calculations/legacy/drawdown_response.js +0 -137
  32. package/calculations/legacy/dumb-cohort-flow.js +0 -238
  33. package/calculations/legacy/gain_response.js +0 -137
  34. package/calculations/legacy/historical_performance_aggregator.js +0 -85
  35. package/calculations/legacy/in_loss_asset_crowd_flow.js +0 -168
  36. package/calculations/legacy/in_profit_asset_crowd_flow.js +0 -168
  37. package/calculations/legacy/negative_expectancy_cohort_flow.js +0 -232
  38. package/calculations/legacy/new_allocation_percentage.js +0 -98
  39. package/calculations/legacy/paper_vs_diamond_hands.js +0 -107
  40. package/calculations/legacy/position_count_pnl.js +0 -120
  41. package/calculations/legacy/positive_expectancy_cohort_flow.js +0 -232
  42. package/calculations/legacy/profit_cohort_divergence.js +0 -115
  43. package/calculations/legacy/profitability_migration.js +0 -104
  44. package/calculations/legacy/reallocation_increase_percentage.js +0 -104
  45. package/calculations/legacy/risk_appetite_change.js +0 -97
  46. package/calculations/legacy/sector_rotation.js +0 -117
  47. package/calculations/legacy/shark_attack_signal.js +0 -112
  48. package/calculations/legacy/smart-cohort-flow.js +0 -238
  49. package/calculations/legacy/smart-dumb-divergence-index.js +0 -143
  50. package/calculations/legacy/smart_dumb_divergence_index_v2.js +0 -138
  51. package/calculations/legacy/smart_money_flow.js +0 -198
  52. package/calculations/legacy/social-predictive-regime-state.js +0 -102
  53. package/calculations/legacy/social-topic-driver-index.js +0 -147
  54. package/calculations/legacy/social-topic-predictive-potential.js +0 -461
  55. package/calculations/legacy/social_flow_correlation.js +0 -112
  56. package/calculations/legacy/speculator_adjustment_activity.js +0 -103
  57. package/calculations/legacy/strategy-performance.js +0 -265
  58. package/calculations/legacy/tsl_effectiveness.js +0 -85
  59. package/calculations/legacy/user-investment-profile.js +0 -313
  60. package/calculations/legacy/user_expectancy_score.js +0 -106
  61. package/calculations/legacy/user_profitability_tracker.js +0 -131
@@ -1,313 +0,0 @@
1
- /**
2
- * @fileoverview Calculates a rolling 90-day "Investor Score" (IS) for each normal user.
3
- *
4
- * --- META REFACTOR (v2) ---
5
- * This calculation is now `type: "meta"` and expects its dependencies
6
- * to be fetched by the pass runner and provided to its `process` method.
7
- * It also dynamically fetches its own historical user data.
8
- */
9
-
10
- const { Firestore } = require('@google-cloud/firestore');
11
- const firestore = new Firestore();
12
- // NOTE: Corrected relative path for data_loader
13
- const { loadDataByRefs, getPortfolioPartRefs, loadFullDayMap } = require('../../../bulltrackers-module/functions/computation-system/utils/data_loader');
14
- const { loadAllPriceData, getInstrumentSectorMap } = require('../../utils/price_data_provider');
15
-
16
- // Config
17
- const NUM_SHARDS = 50;
18
- const ROLLING_DAYS = 90;
19
- const SHARD_COLLECTION_NAME = 'user_profile_history';
20
- const PNL_TRACKER_CALC_ID = 'user-profitability-tracker';
21
-
22
- // Helper: stable shard index
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
-
36
- /**
37
- * (NEW) Statically declare dependencies.
38
- * The runner will fetch these results for the *current date*
39
- * and pass them to `process`.
40
- */
41
- static getDependencies() {
42
- return [PNL_TRACKER_CALC_ID];
43
- }
44
-
45
- /**
46
- * HEURISTIC 1: Risk & Diversification Score (0-10).
47
- */
48
- _calculateRiskAndDivScore(todayPortfolio, sectorMap) {
49
- if (!todayPortfolio.AggregatedPositions || todayPortfolio.AggregatedPositions.length === 0) {
50
- return 5; // neutral
51
- }
52
-
53
- const positions = todayPortfolio.AggregatedPositions;
54
- let totalInvested = 0;
55
- let weightedRetSum = 0;
56
- let weightedRetSqSum = 0;
57
- let maxPosition = 0;
58
- const sectors = new Set();
59
-
60
- for (const pos of positions) {
61
- const invested = pos.InvestedAmount || pos.Amount || 0;
62
- const netProfit = ('NetProfit' in pos) ? pos.NetProfit : (pos.ProfitAndLoss || 0); // decimal % return
63
- const ret = invested > 0 ? (netProfit) : netProfit;
64
-
65
- weightedRetSum += ret * invested;
66
- weightedRetSqSum += (ret * ret) * invested;
67
- totalInvested += invested;
68
- if (invested > maxPosition) maxPosition = invested;
69
-
70
- sectors.add(sectorMap[pos.InstrumentID] || 'N/A');
71
- }
72
-
73
- const meanReturn = totalInvested > 0 ? (weightedRetSum / totalInvested) : 0;
74
- const meanReturnSq = totalInvested > 0 ? (weightedRetSqSum / totalInvested) : (meanReturn * meanReturn);
75
- const variance = Math.max(0, meanReturnSq - (meanReturn * meanReturn));
76
- const stdReturn = Math.sqrt(variance);
77
- let dispersionRiskProxy = stdReturn > 0 ? meanReturn / stdReturn : 0;
78
- const capped = Math.max(-2, Math.min(4, dispersionRiskProxy));
79
- const scoreSharpe = ((capped + 2) / 6) * 10;
80
- const sectorCount = sectors.size;
81
- let scoreDiversification = 0;
82
- if (sectorCount === 1) scoreDiversification = 0;
83
- else if (sectorCount <= 4) scoreDiversification = 5;
84
- else if (sectorCount <= 7) scoreDiversification = 8;
85
- else scoreDiversification = 10;
86
- const concentrationRatio = totalInvested > 0 ? (maxPosition / totalInvested) : 0;
87
- let scoreSizing = 0;
88
- if (concentrationRatio > 0.8) scoreSizing = 0;
89
- else if (concentrationRatio > 0.5) scoreSizing = 2;
90
- else if (concentrationRatio > 0.3) scoreSizing = 5;
91
- else if (concentrationRatio > 0.15) scoreSizing = 8;
92
- else scoreSizing = 10;
93
- const final = (scoreSharpe * 0.4) + (scoreDiversification * 0.3) + (scoreSizing * 0.3);
94
- return Math.max(0, Math.min(10, final));
95
- }
96
-
97
- /**
98
- * HEURISTIC 2: Discipline Score (0-10).
99
- */
100
- _calculateDisciplineScore(yesterdayPortfolio = {}, todayPortfolio = {}) {
101
- const yPositions = yesterdayPortfolio.AggregatedPositions || [];
102
- const tPositions = new Map((todayPortfolio.AggregatedPositions || []).map(p => [p.PositionID, p]));
103
- if (yPositions.length === 0) {
104
- return 5; // neutral
105
- }
106
- let eventPoints = 0;
107
- let eventCount = 0;
108
- for (const yPos of yPositions) {
109
- const profitAndLoss = ('ProfitAndLoss' in yPos) ? yPos.ProfitAndLoss : (yPos.NetProfit || 0);
110
- const invested = yPos.InvestedAmount || yPos.Amount || 0;
111
- const pnlPercent = profitAndLoss;
112
- const tPos = tPositions.get(yPos.PositionID);
113
- if (!tPos) {
114
- eventCount++;
115
- if (pnlPercent < -0.05) eventPoints += 10;
116
- else if (pnlPercent > 0.20) eventPoints += 8;
117
- else if (pnlPercent > 0 && pnlPercent < 0.05) eventPoints += 2;
118
- else eventPoints += 5;
119
- } else {
120
- if (pnlPercent < -0.10) {
121
- eventCount++;
122
- const tInvested = tPos.InvestedAmount || tPos.Amount || 0;
123
- if (tInvested > invested) eventPoints += 0;
124
- else eventPoints += 3;
125
- } else if (pnlPercent > 0.15) {
126
- eventCount++;
127
- eventPoints += 10;
128
- }
129
- }
130
- }
131
- const avg = (eventCount > 0) ? (eventPoints / eventCount) : 5;
132
- return Math.max(0, Math.min(10, avg));
133
- }
134
-
135
- /**
136
- * HEURISTIC 3: Market Timing Score (0-10).
137
- */
138
- _calculateMarketTimingScore(yesterdayPortfolio = {}, todayPortfolio = {}, priceMap) {
139
- const yIds = new Set((yesterdayPortfolio.AggregatedPositions || []).map(p => p.PositionID));
140
- const newPositions = (todayPortfolio.AggregatedPositions || []).filter(p => !yIds.has(p.PositionID));
141
- if (newPositions.length === 0) return 5;
142
- let timingPoints = 0;
143
- let timingCount = 0;
144
- for (const tPos of newPositions) {
145
- const prices = priceMap[tPos.InstrumentID];
146
- if (!prices) continue;
147
- let historyPrices = [];
148
- if (Array.isArray(prices)) {
149
- historyPrices = prices
150
- .map(p => (typeof p === 'number' ? p : (p.price || p.close || null)))
151
- .filter(v => v != null);
152
- } else {
153
- const entries = Object.keys(prices)
154
- .map(d => ({ d, p: prices[d] }))
155
- .filter(e => e.p != null)
156
- .sort((a, b) => new Date(a.d) - new Date(b.d));
157
- historyPrices = entries.map(e => e.p);
158
- }
159
- const last30 = historyPrices.slice(-30);
160
- if (last30.length < 2) continue;
161
- const minPrice = Math.min(...last30);
162
- const maxPrice = Math.max(...last30);
163
- const openRate = tPos.OpenRate;
164
- const range = maxPrice - minPrice;
165
- if (!isFinite(range) || range === 0) continue;
166
- let proximity = (openRate - minPrice) / range;
167
- proximity = Math.max(0, Math.min(1, proximity));
168
- timingCount++;
169
- if (proximity < 0.2) timingPoints += 10;
170
- else if (proximity < 0.4) timingPoints += 8;
171
- else if (proximity > 0.9) timingPoints += 1;
172
- else if (proximity > 0.7) timingPoints += 3;
173
- else timingPoints += 5;
174
- }
175
- const avg = (timingCount > 0) ? (timingPoints / timingCount) : 5;
176
- return Math.max(0, Math.min(10, avg));
177
- }
178
-
179
- /**
180
- * REFACTORED PROCESS METHOD
181
- * @param {string} dateStr - The date being processed (YYYY-MM-DD).
182
- * @param {object} dependencies - Shared dependencies (db, logger, rootData, etc.).
183
- * @param {object} config - The computation system configuration.
184
- * @param {object} fetchedDependencies - An object containing the results of this
185
- * calc's dependencies (e.g., { 'user-profitability-tracker': ... }).
186
- */
187
- async process(dateStr, dependencies, config, fetchedDependencies) {
188
- const { logger, db, rootData } = dependencies;
189
- const { portfolioRefs } = rootData;
190
-
191
- logger.log('INFO', '[UserInvestmentProfile] Starting meta-process...');
192
-
193
- // 1. Get Pass 1 dependency from the `fetchedDependencies` argument
194
- const pnlTrackerResult = fetchedDependencies[PNL_TRACKER_CALC_ID];
195
- if (!pnlTrackerResult || !pnlTrackerResult.daily_pnl_map) {
196
- logger.log('WARN', `[UserInvestmentProfile] Dependency '${PNL_TRACKER_CALC_ID}' not found in fetchedDependencies. Aborting.`);
197
- return null; // Return null to signal failure
198
- }
199
- const pnlScores = pnlTrackerResult.daily_pnl_map;
200
- logger.log('INFO', `[UserInvestmentProfile] Received ${Object.keys(pnlScores).length} PNL scores from dependency.`);
201
-
202
- // 2. Load external dependencies (prices, sectors)
203
- const [priceMap, sectorMap] = await Promise.all([
204
- loadAllPriceData(),
205
- getInstrumentSectorMap()
206
- ]);
207
- if (!priceMap || !sectorMap) {
208
- logger.log('ERROR', '[UserInvestmentProfile] Failed to load priceMap or sectorMap.');
209
- return null;
210
- }
211
-
212
- // 3. Load "yesterday's" portfolio data for comparison
213
- const yesterdayDate = new Date(dateStr + 'T00:00:00Z');
214
- yesterdayDate.setUTCDate(yesterdayDate.getUTCDate() - 1);
215
- const yesterdayStr = yesterdayDate.toISOString().slice(0, 10);
216
- const yesterdayRefs = await getPortfolioPartRefs(config, dependencies, yesterdayStr);
217
- const yesterdayPortfolios = await loadFullDayMap(config, dependencies, yesterdayRefs);
218
- logger.log('INFO', `[UserInvestmentProfile] Loaded ${yesterdayRefs.length} part refs for yesterday.`);
219
-
220
- // 4. Stream "today's" portfolio data and process
221
- const batchSize = config.partRefBatchSize || 10;
222
- const dailyUserScores = {}; // Local state for this run
223
-
224
- for (let i = 0; i < portfolioRefs.length; i += batchSize) {
225
- const batchRefs = portfolioRefs.slice(i, i + batchSize);
226
- const todayPortfoliosChunk = await loadDataByRefs(config, dependencies, batchRefs);
227
-
228
- for (const uid in todayPortfoliosChunk) {
229
- const pToday = todayPortfoliosChunk[uid];
230
- if (!pToday || !pToday.AggregatedPositions) continue; // Skip
231
-
232
- const pYesterday = yesterdayPortfolios[uid] || {};
233
-
234
- const score_rd = this._calculateRiskAndDivScore(pToday, sectorMap);
235
- const score_disc = this._calculateDisciplineScore(pYesterday, pToday);
236
- const score_time = this._calculateMarketTimingScore(pYesterday, pToday, priceMap);
237
-
238
- dailyUserScores[uid] = {
239
- score_rd,
240
- score_disc,
241
- score_time
242
- };
243
- }
244
- }
245
- logger.log('INFO', `[UserInvestmentProfile] Calculated daily scores for ${Object.keys(dailyUserScores).length} users.`);
246
-
247
- // 5. GETRESULT LOGIC (Final aggregation)
248
- const shardedResults = {};
249
- for (let i = 0; i < NUM_SHARDS; i++) {
250
- const shardKey = `${SHARD_COLLECTION_NAME}_shard_${i}`;
251
- shardedResults[shardKey] = { profiles: {}, lastUpdated: dateStr };
252
- }
253
- const dailyInvestorScoreMap = {};
254
-
255
- // Fetch existing shards in parallel
256
- const shardPromises = [];
257
- for (let i = 0; i < NUM_SHARDS; i++) {
258
- const docRef = firestore.collection(SHARD_COLLECTION_NAME).doc(`${SHARD_COLLECTION_NAME}_shard_${i}`);
259
- shardPromises.push(docRef.get());
260
- }
261
- const shardSnapshots = await Promise.all(shardPromises);
262
- const existingShards = shardSnapshots.map((snap) => (snap.exists ? snap.data().profiles : {}));
263
-
264
- // Process users
265
- for (const userId of Object.keys(dailyUserScores)) {
266
- const shardIndex = getShardIndex(userId);
267
- const scores = dailyUserScores[userId];
268
- const existingProfiles = existingShards[shardIndex] || {};
269
- const history = (existingProfiles[userId] || []).slice(); // clone
270
- history.push({
271
- date: dateStr,
272
- ...scores,
273
- pnl: (pnlScores[userId] || 0)
274
- });
275
- const newHistory = history.slice(-ROLLING_DAYS);
276
-
277
- // compute rolling averages
278
- let avg_rd = 0, avg_disc = 0, avg_time = 0, avg_pnl = 0;
279
- for (const entry of newHistory) {
280
- avg_rd += (entry.score_rd || 0);
281
- avg_disc += (entry.score_disc || 0);
282
- avg_time += (entry.score_time || 0);
283
- avg_pnl += (entry.pnl || 0);
284
- }
285
- const N = newHistory.length || 1;
286
- avg_rd /= N;
287
- avg_disc /= N;
288
- avg_time /= N;
289
- avg_pnl /= N;
290
-
291
- const normalizedPnl = Math.max(-10, Math.min(10, avg_pnl * 1000));
292
- const finalISRaw = (avg_disc * 0.4) + (avg_rd * 0.3) + (avg_time * 0.2) + (normalizedPnl * 0.1);
293
- const finalIS = Math.max(0, Math.min(10, finalISRaw));
294
-
295
- const shardKey = `${SHARD_COLLECTION_NAME}_shard_${shardIndex}`;
296
- shardedResults[shardKey].profiles[userId] = newHistory;
297
- dailyInvestorScoreMap[userId] = finalIS;
298
- }
299
-
300
- logger.log('INFO', `[UserInvestmentProfile] Finalized IS scores for ${Object.keys(dailyInvestorScoreMap).length} users.`);
301
-
302
- // Return the final result object
303
- return {
304
- sharded_user_profile: shardedResults,
305
- daily_investor_scores: dailyInvestorScoreMap
306
- };
307
- }
308
-
309
- async getResult() { return null; }
310
- reset() {}
311
- }
312
-
313
- module.exports = UserInvestmentProfile;
@@ -1,106 +0,0 @@
1
- /**
2
- * @fileoverview Calculation (Pass 3) for user expectancy score.
3
- *
4
- * This metric calculates a "trader expectancy score" for each user.
5
- * Expectancy = (Win % * Avg Win Size) - (Loss % * Avg Loss Size)
6
- *
7
- * It *depends* on 'user_profitability_tracker' to get the
8
- * historical win/loss data for each user.
9
- */
10
- class UserExpectancyScore {
11
- constructor() {
12
- // No per-user processing
13
- }
14
-
15
- /**
16
- * Defines the output schema for this calculation.
17
- * @returns {object} JSON Schema object
18
- */
19
- static getSchema() {
20
- const userSchema = {
21
- "type": "object",
22
- "properties": {
23
- "expectancy_score": {
24
- "type": "number",
25
- "description": "Trader expectancy score: (Win % * Avg Win) - (Loss % * Avg Loss). Measures P&L per trade."
26
- },
27
- "win_rate_pct": { "type": "number" },
28
- "avg_win_pct": { "type": "number" },
29
- "loss_rate_pct": { "type": "number" },
30
- "avg_loss_pct": { "type": "number" },
31
- "total_days_processed": { "type": "number" }
32
- },
33
- "required": ["expectancy_score", "win_rate_pct", "avg_win_pct", "loss_rate_pct", "avg_loss_pct"]
34
- };
35
-
36
- return {
37
- "type": "object",
38
- "description": "Calculates a 'trader expectancy score' for each user based on their 7-day P&L history.",
39
- "patternProperties": {
40
- "^.*$": userSchema // UserID
41
- },
42
- "additionalProperties": userSchema
43
- };
44
- }
45
-
46
- /**
47
- * Statically declare dependencies.
48
- */
49
- static getDependencies() {
50
- return [
51
- 'user_profitability_tracker' // Pass 2
52
- ];
53
- }
54
-
55
- process() {
56
- // No-op
57
- }
58
-
59
- getResult(fetchedDependencies) {
60
- const profitabilityData = fetchedDependencies['user_profitability_tracker']?.user_details;
61
-
62
- if (!profitabilityData) {
63
- return {};
64
- }
65
-
66
- const result = {};
67
-
68
- for (const [userId, data] of Object.entries(profitabilityData)) {
69
- const history = data.pnl_history_7d || [];
70
- if (history.length === 0) continue;
71
-
72
- const wins = history.filter(pnl => pnl > 0);
73
- const losses = history.filter(pnl => pnl < 0);
74
- const totalTrades = history.length;
75
-
76
- if (totalTrades === 0) continue;
77
-
78
- const winRate = (wins.length / totalTrades);
79
- const lossRate = (losses.length / totalTrades);
80
-
81
- const avgWin = (wins.length > 0) ? (wins.reduce((a, b) => a + b, 0) / wins.length) : 0;
82
- // AvgLoss should be a positive number
83
- const avgLoss = (losses.length > 0) ? (Math.abs(losses.reduce((a, b) => a + b, 0)) / losses.length) : 0;
84
-
85
- // Expectancy = (Win % * Avg Win) - (Loss % * Avg Loss)
86
- const expectancy = (winRate * avgWin) - (lossRate * avgLoss);
87
-
88
- result[userId] = {
89
- expectancy_score: expectancy,
90
- win_rate_pct: winRate * 100,
91
- avg_win_pct: avgWin,
92
- loss_rate_pct: lossRate * 100,
93
- avg_loss_pct: avgLoss,
94
- total_days_processed: totalTrades
95
- };
96
- }
97
-
98
- return result;
99
- }
100
-
101
- reset() {
102
- // No state
103
- }
104
- }
105
-
106
- module.exports = UserExpectancyScore;
@@ -1,131 +0,0 @@
1
- /**
2
- * @fileoverview Calculation (Pass 2) for user profitability tracker.
3
- *
4
- * This metric answers: "What is each user's weighted average
5
- * daily P&L, and what is their 7-day rolling profitability history?"
6
- *
7
- * This is a foundational calculation for identifying 'smart' and
8
- * 'dumb' cohorts in later passes.
9
- *
10
- * It is *stateful* and requires reading the *previous day's*
11
- * result of *this same calculation* from Firestore.
12
- */
13
- class UserProfitabilityTracker {
14
- constructor() {
15
- // Map<userId, { weighted_avg_pnl_7d, profitable_days_7d, pnl_history_7d: [] }>
16
- this.userHistory = new Map();
17
- }
18
-
19
- /**
20
- * Defines the output schema for this calculation.
21
- * @returns {object} JSON Schema object
22
- */
23
- static getSchema() {
24
- const userDetailSchema = {
25
- "type": "object",
26
- "properties": {
27
- "weighted_avg_pnl_7d": {
28
- "type": "number",
29
- "description": "Weighted average P&L over the last 7 days (recent days weighted more)."
30
- },
31
- "profitable_days_7d": {
32
- "type": "number",
33
- "description": "Count of profitable days in the last 7 days."
34
- },
35
- "pnl_history_7d": {
36
- "type": "array",
37
- "description": "List of the last 7 days' P&L values.",
38
- "items": { "type": "number" }
39
- }
40
- },
41
- "required": ["weighted_avg_pnl_7d", "profitable_days_7d", "pnl_history_7d"]
42
- };
43
-
44
- return {
45
- "type": "object",
46
- "description": "Tracks 7-day rolling profitability for each user; used to define cohorts.",
47
- "properties": {
48
- "ranked_users": {
49
- "type": "array",
50
- "description": "List of all users, sorted ascending by their 7-day weighted average P&L.",
51
- "items": {
52
- "type": "object",
53
- "properties": {
54
- "userId": { "type": "string" },
55
- "weighted_avg_pnl_7d": { "type": "number" }
56
- },
57
- "required": ["userId", "weighted_avg_pnl_7d"]
58
- }
59
- },
60
- "user_details": {
61
- "type": "object",
62
- "description": "A map of user IDs to their detailed profitability data.",
63
- "patternProperties": {
64
- "^.*$": userDetailSchema // UserID
65
- },
66
- "additionalProperties": userDetailSchema
67
- }
68
- },
69
- "required": ["ranked_users", "user_details"]
70
- };
71
- }
72
-
73
- _calculateWeightedAverage(history) {
74
- const weights = [0.25, 0.20, 0.15, 0.12, 0.10, 0.08, 0.10]; // Example weights (sum to 1.0)
75
- let weightedSum = 0;
76
-
77
- for (let i = 0; i < history.length; i++) {
78
- // Apply weights in reverse (most recent gets highest weight)
79
- weightedSum += history[i] * (weights[i] || 0);
80
- }
81
- return weightedSum;
82
- }
83
-
84
- process(todayPortfolio, yesterdayPortfolio, userId, context, todayInsights, yesterdayInsights, fetchedDependencies) {
85
- // 1. Get this user's history from *yesterday's* run of this metric
86
- // This data is pre-loaded into context by the computation system
87
- const yHistoryData = context.yesterdaysDependencyData['user_profitability_tracker'];
88
- const userHistory = yHistoryData?.user_details?.[userId]?.pnl_history_7d || [];
89
-
90
- // 2. Get today's P&L
91
- const todayPnl = todayPortfolio?.Summary?.NetProfit || 0;
92
-
93
- // 3. Create new 7-day history
94
- const newHistory = [todayPnl, ...userHistory].slice(0, 7);
95
-
96
- // 4. Calculate new metrics
97
- const weightedAvg = this._calculateWeightedAverage(newHistory);
98
- const profitableDays = newHistory.filter(pnl => pnl > 0).length;
99
-
100
- // 5. Store for getResult()
101
- this.userHistory.set(userId, {
102
- weighted_avg_pnl_7d: weightedAvg,
103
- profitable_days_7d: profitableDays,
104
- pnl_history_7d: newHistory
105
- });
106
- }
107
-
108
- getResult() {
109
- const user_details = Object.fromEntries(this.userHistory);
110
-
111
- // Create the ranked list for cohorts
112
- const ranked_users = Array.from(this.userHistory.entries())
113
- .map(([userId, data]) => ({
114
- userId: userId,
115
- weighted_avg_pnl_7d: data.weighted_avg_pnl_7d
116
- }))
117
- // Sort ascending (lowest P&L first)
118
- .sort((a, b) => a.weighted_avg_pnl_7d - b.weighted_avg_pnl_7d);
119
-
120
- return {
121
- ranked_users: ranked_users,
122
- user_details: user_details
123
- };
124
- }
125
-
126
- reset() {
127
- this.userHistory.clear();
128
- }
129
- }
130
-
131
- module.exports = UserProfitabilityTracker;