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.
- package/calculations/capitulation/asset-volatility-estimator.js +1 -2
- package/calculations/capitulation/retail-capitulation-risk-forecast.js +1 -2
- package/calculations/core/Insights-total-long-per-stock +56 -0
- package/calculations/core/insights-daily-bought-vs-sold-count.js +74 -0
- package/calculations/core/insights-daily-ownership-delta.js +70 -0
- package/calculations/core/insights-sentimet-per-stock.js +68 -0
- package/calculations/core/insights-total-long-per-sector +73 -0
- package/calculations/core/insights-total-positions-held.js +49 -0
- package/calculations/ghost-book/cost-basis-density.js +1 -2
- package/calculations/ghost-book/liquidity-vacuum.js +4 -4
- package/calculations/ghost-book/retail-gamma-exposure.js +0 -1
- package/calculations/helix/winner-loser-flow.js +1 -1
- package/calculations/predicative-alpha/cognitive-dissonance.js +1 -2
- package/calculations/predicative-alpha/diamond-hand-fracture.js +1 -2
- package/calculations/predicative-alpha/mimetic-latency.js +1 -2
- package/package.json +1 -1
- package/calculations/legacy/activity_by_pnl_status.js +0 -119
- package/calculations/legacy/asset_crowd_flow.js +0 -163
- package/calculations/legacy/capital_deployment_strategy.js +0 -108
- package/calculations/legacy/capital_liquidation_performance.js +0 -139
- package/calculations/legacy/capital_vintage_performance.js +0 -136
- package/calculations/legacy/cash-flow-deployment.js +0 -144
- package/calculations/legacy/cash-flow-liquidation.js +0 -146
- package/calculations/legacy/crowd-cash-flow-proxy.js +0 -128
- package/calculations/legacy/crowd_conviction_score.js +0 -261
- package/calculations/legacy/crowd_sharpe_ratio_proxy.js +0 -137
- package/calculations/legacy/daily_asset_activity.js +0 -128
- package/calculations/legacy/daily_user_activity_tracker.js +0 -182
- package/calculations/legacy/deposit_withdrawal_percentage.js +0 -125
- package/calculations/legacy/diversification_pnl.js +0 -115
- package/calculations/legacy/drawdown_response.js +0 -137
- package/calculations/legacy/dumb-cohort-flow.js +0 -238
- package/calculations/legacy/gain_response.js +0 -137
- package/calculations/legacy/historical_performance_aggregator.js +0 -85
- package/calculations/legacy/in_loss_asset_crowd_flow.js +0 -168
- package/calculations/legacy/in_profit_asset_crowd_flow.js +0 -168
- package/calculations/legacy/negative_expectancy_cohort_flow.js +0 -232
- package/calculations/legacy/new_allocation_percentage.js +0 -98
- package/calculations/legacy/paper_vs_diamond_hands.js +0 -107
- package/calculations/legacy/position_count_pnl.js +0 -120
- package/calculations/legacy/positive_expectancy_cohort_flow.js +0 -232
- package/calculations/legacy/profit_cohort_divergence.js +0 -115
- package/calculations/legacy/profitability_migration.js +0 -104
- package/calculations/legacy/reallocation_increase_percentage.js +0 -104
- package/calculations/legacy/risk_appetite_change.js +0 -97
- package/calculations/legacy/sector_rotation.js +0 -117
- package/calculations/legacy/shark_attack_signal.js +0 -112
- package/calculations/legacy/smart-cohort-flow.js +0 -238
- package/calculations/legacy/smart-dumb-divergence-index.js +0 -143
- package/calculations/legacy/smart_dumb_divergence_index_v2.js +0 -138
- package/calculations/legacy/smart_money_flow.js +0 -198
- package/calculations/legacy/social-predictive-regime-state.js +0 -102
- package/calculations/legacy/social-topic-driver-index.js +0 -147
- package/calculations/legacy/social-topic-predictive-potential.js +0 -461
- package/calculations/legacy/social_flow_correlation.js +0 -112
- package/calculations/legacy/speculator_adjustment_activity.js +0 -103
- package/calculations/legacy/strategy-performance.js +0 -265
- package/calculations/legacy/tsl_effectiveness.js +0 -85
- package/calculations/legacy/user-investment-profile.js +0 -313
- package/calculations/legacy/user_expectancy_score.js +0 -106
- 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;
|